Solar Farm Analysis


This report provides an in-depth analysis of a small-scale solar farm composed of 12 photovoltaic panels, offering a total nominal capacity of 5.6 kWh.
The primary goal is to evaluate the farm’s performance and operational aspects to offer a realistic view of what it’s like to own and manage a home solar power installation in a temperate climate.

Author: MK

Date: November 5, 2024

Data Preparation

Creating a bot using Selenium

To analyse the solar panels data we need to firstly obtain it from server to which inverter is connected. It can be done using SEMS API or SEMS portal report feature. As the first option isn’t easily accessible (at least in Poland), the choice is obvious. However, due to some limitations of the mentioned reporting feature, it’s necessary to create a bot which can download a hundreds of reports (one report can include data from up to 7 days if the hourly reporting is chosen).

  1. SEMS Login Process: The semsLogin function automates the login process to the SEMS portal. It opens the SEMS login page, inputs the user’s email and password from the configuration settings, agrees to the SEMS policy, and clicks the login button automatically unless manual intervention is specified.

  2. Daily Report Downloading: The semsDailyReport function is responsible for downloading the daily report. It includes a retry mechanism that attempts to execute actions up to five times in case of errors, handling any pop-up messages that might appear. The function navigates through the SEMS interface to select the desired date range for the report, interacts with various page elements to set the “From” and “To” dates, and initiates the export of the report by clicking the necessary buttons. It also incorporates pauses (Sys.sleep) to allow the webpage to process actions.

  3. Date Range Calculation: The dateRange function calculates the range of dates between the start and end dates provided, converting them into a numerical range based on the difference from the current date.

Code
if (param.UseScraper) {
    ### Login to SEMS using default credentials
  semsLogin <- function(manual = F) {
    # Opening SEMS homepage
    open_url("https://www.semsportal.com/home/login")
    # Pass e-mail
    session |>
      find_element(css = "#username.require") |>
      elem_click() |>
      elem_send_keys(config$login)
    # Pass password
    session |>
      find_element(css = "#password" ) |>
      elem_click() |>
      elem_send_keys(config$password)
    # Accept SEMS policy
    session %>%
      find_element(css = "#readStatement") %>%
      elem_click()
    # Click login button
    if (!manual) {
      session %>%
        find_element(css = "#btnLogin") %>%
        elem_click()
    }
  }
  
  
  ### Download report of the day with a frequency of a minute
  semsDailyReport <- function(reportDate) {
    retry <- function(expr, retries = 5, wait = 5, handlePopup = TRUE) {
      attempt <- 1
      while (attempt <= retries) {
        result <- tryCatch(
          expr,
          error = function(e) {
            message(sprintf("Error: %s. Retrying (%d/%d)...", e$message, attempt, retries))
            if (handlePopup) {
              tryCatch(
                session %>%
                  find_element(css = ".gdw-message-box-button-group.gmbbg-up-line.button") %>%
                  elem_click(),
                error = function(e) {
                  message("No blocking popup found.")
                }
              )
            }
            return(NULL)
          }
        )
        if (!is.null(result)) {
          return(result)
        }
        Sys.sleep(wait)
        attempt <- attempt + 1
      }
      stop("Maximum retry attempts reached")
    }
  
    retry({
      session %>%
        find_element(css = ".history_data") %>%
        elem_click()
    })
  
    iDate <- Sys.Date() |> as.POSIXct()
    newStart <- NULL
    newEnd <- NULL
  
    while (iDate >= reportDate[1]) {
      print(c(iDate, reportDate[1]))
      newStart <- format(
        iDate - as.difftime(1, units = 'days'),
        "%d/%m/%y %H:%M:%S"
      ) |> as.character()
      newEnd <- format(
        iDate,
        "%d/%m/%y %H:%M:%S"
      ) |> as.character()
  
      retry({
        session %>%
          find_element(css = ".el-range-input[placeholder='From']" ) %>%
          elem_click() %>%
          elem_set_value(newStart)
      })
      retry({
        session %>%
          find_element(css = ".el-range-input[placeholder='To']" ) %>%
          elem_click() %>%
          elem_set_value(newEnd)
      })
  
      iDate <- iDate - as.difftime(1, units = "days")
    }
  
    retry({
      session %>%
        find_element(css = ".el-button.el-picker-panel__link-btn.el-button--default.el-button--mini.is-plain") %>%
        elem_click()
    })
  
    Sys.sleep(7)
  
    retry({
      session %>%
        find_element(css = ".el-collapse-item__header") %>%
        elem_click()
    })
    retry({
      session %>%
        find_element(xpath = '//*[@id="55000DTS225W0789"]') %>%
        elem_click()
    })
  
    Sys.sleep(2)
  
    retry({
      session %>%
        find_element(css = ".his_btn_all.btn-nor") %>%
        elem_click()
    })
    retry({
      session %>%
        find_element(xpath = '//*[@id="app"]/div[1]/div[4]/div/div[3]/div/div[4]/button[2]') %>%
        elem_click()
    })
  
    Sys.sleep(7)
  
    retry({
      session %>%
        find_element(css = ".btn-nor.p_btn_export") %>%
        elem_click()
    })
  
    Sys.sleep(7)
  }
  
  
  dateRange <- function(start, end) {
    x <- (as.Date(c(start, end), format = "%Y.%m.%d") - Sys.Date()) |>
    as.numeric() |> abs() |> unique()
    x[1]:x[2]
  }
}

The script initiates a web scraping process by starting a Selenium session with Chrome as the browser. It logs into the SEMS portal by running the semsLogin function, which automates the credential input and page navigation. Once logged in, the code proceeds to download daily reports for the dates within the specified range (July 13-16, 2024). For each day, it calculates a date range that spans from the current date minus the day count and invokes the semsDailyReport function to retrieve the data for those dates. This ensures that reports for multiple days are downloaded automatically without manual intervention.

Code
if (param.UseScraper) {
  # Start the selenium session
  session <- selenider_session(
    "selenium", browser = "chrome",
    #options = chromote_options(headless = F)
  )
  
  # Login to SEMS
  semsLogin(manual = F)
  
  # Download raport for each day
    lapply(dateRange("2023.03.28", "2024.07.16"), function(x) {
      reportDate <- c(
      Sys.Date() - as.difftime(x, unit = "days"),
      Sys.Date() - as.difftime(x + 1, unit = "days")
    )
    semsDailyReport(reportDate)
  })
}

Merging generated reports

It searches the C:/Users/_username_/Downloads directory for Excel files (.xls) that were created in the last 72 hours. These files, likely representing daily energy reports, are read using read_xls, with the first two rows skipped (containing metadata). The content from all reports is combined into a single dataframe, with duplicates removed, and saved as a CSV file (joined_report.csv).

Code
if (param.UseScraper) {
    list.files("C:/Users/micha/Downloads", full.names = TRUE) |>
    file.info() %>%
    filter(
      ctime <= as.POSIXct.numeric(Sys.time()),
      ctime >= as.POSIXct.numeric(Sys.time() - as.difftime(72, units = "hours")),
      stringi::stri_endswith_fixed(rownames(.), ".xls")
    ) |>
    rownames() -> reports
  
  lapply(reports, function(x) {
    readxl::read_xls(x, skip = 2)
  }) |>
    map_df(~ .x) |> unique() |> write_csv("joined_report.csv")
}

Next, the CSV is reloaded into dfReport for further data wrangling. The Time column is converted into a standard POSIX date-time format, enabling more granular time-based analysis. The script then extracts year, month, day, hour, and minute components from each timestamp, allowing for detailed temporal analysis of the data—ideal for tracking performance trends across different timeframes

Code
if (T) {
  dfReport <- 
  # Report scraped from SEMS portal using Selenium engine
  read_csv("../joined_report.csv") |>
  unique() |> # getting rid off the days duplicated during scraping
  mutate(
    Time = as.POSIXct(Time, format = "%d/%m/%y %H:%M:%S"),
    year = year(Time),
    month = month(Time),
    day = day(Time),
    hour = hour(Time),
    minute = minute(Time)
  ) 
  
  dfReport |> head(100) |>
    DT::datatable()
}

Breif data validation

Once the basic data is available in the R environment, the next step is to verify the accuracy of the dataset. If any discrepancies are found, it’s crucial to analyze the nature and source of these issues.

It was discovered that during Internet outages, the inverter failed to transmit historical data. Instead of the usual detailed logs, it becomes possible to infer the true values of power generation by analyzing the cumulative energy output and the total operational time of the inverter, which are recorded regardless of network connectivity.

Code
if (param.RunScripts) {
  dfReport |>
    group_by(year, month, day) |>
    summarise(n = n()) |>
    mutate(date = as.Date(paste(sep = "-", year, month, day))) |>
    ggplot(aes(
      x = date,
      y = n
    )) +
    geom_point(size = 4.2, color = "#0274BD", alpha = .6) +
    labs(x = "", y = "Number of observations") +
    facet_wrap(~ year, ncol = 1, scales = "free_x")

} 

Code
difference <- NA
bias <- NA

if (param.RunScripts) {
  totalPower <- max(dfReport$`Total Generation(kWh)`)
  totalPowerRecorded <- 
    dfReport |>
    group_by(year, month, day, hour) |>
    summarise(avgPower = mean(`Power(W)`), .groups = "drop") |>
    summarise(totalPower = round(sum(avgPower) / 1000, 0))
  
  difference <- totalPowerRecorded - totalPower
  bias <- round((1 - (totalPowerRecorded / totalPower)) * 100, 2)
}

The difference between total power generated by solar panels vs. the power based on the reported data is -38.5 kWh that implies a 0.51% bias in the reported data.

Monthly Generation

The bar chart provides a monthly comparison of daily power generation (in kWh) between the years 2023 and 2024. The x-axis represents the months, and the y-axis shows the daily energy generation. Data from 2023 is shown in gray, while data from 2024 is depicted in blue, with exact generation values labeled on each bar for clarity.

Code
dfReport |>
  mutate(Date = paste0(month(Time), year(Time))) |>
  group_by(Date) |>
  summarise(
    startDay = min(Time),
    endDay = max(Time),
    year = year(max(Time)),
    month = month(max(Time)),
    minTotalPower = min(if_else(`Total Generation(kWh)` == 0, NA, `Total Generation(kWh)`), na.rm = T),
    maxTotalPower = max(`Total Generation(kWh)`),
    dailyGeneration = maxTotalPower - minTotalPower
  ) |>
  filter(!(year == 2024 & month == 7)) |>
  ggplot(aes(fill = factor(year), x = factor(month), y = dailyGeneration)) +
  geom_col(position = position_dodge(width = 0.5, preserve = "single")) +
  scale_fill_manual(values = c("#cccccc", "#008bd3")) +
  geom_text(aes(
    y = dailyGeneration/2,
    label = paste0(round(dailyGeneration, 0), "kWh"),
    color = if_else(year == 2024, "white", "#232323")
  ), angle = 90, position = position_dodge(width = 1),
  vjust = 0) +
  scale_color_identity() +
  labs(
    x = "Month",
    fill = "Year",
    y = "Total Generation",
    title = "The Total Monthly Generation by Year"
  )

  • Seasonal Trends: Both years exhibit a clear seasonal pattern, with daily power generation peaking around the mid-year months (June to August), coinciding with longer daylight hours and optimal solar conditions.

  • Yearly Comparison: While the 2023 values are consistently represented, data for 2024 only covers a few months. Where comparisons are possible, the generation values for 2024 (blue bars) tend to be slightly higher, suggesting either improved panel efficiency, favorable weather conditions, or maintenance optimizations.

  • Generation Drop in Winter: A notable decrease in power generation is visible from October onward, reflecting the typical reduction in solar intensity during late autumn and winter.

This comparison helps in evaluating the system’s year-over-year performance and assessing seasonal energy output variations, crucial for planning energy consumption and storage strategies throughout the year. Additionally, the data may indicate performance improvements or system degradation factors if monitored continuously across multiple years.

Solar Panels Seasonality

The chart displays the seasonal variation in average power generation for a small-scale solar installation, with separate panels for autumn, spring, summer, and winter. Each plot shows the daily power generation curve across hours of the day, with power measured in kilowatt-hours (kWh) on the y-axis and time in hours on the x-axis.

Code
dfReport |>
  mutate(
    season = case_when(
      month %in% c(12, 1, 2) ~ "Winter",
      month %in% 3:5 ~ "Spring",
      month %in% 6:8 ~ "Summer",
      month %in% 9:11 ~ "Autumn"
    )
  ) |>
  group_by(hour, season) |>
  summarise(avgPower = mean(`Power(W)`)) |>
ggplot(aes(x = hour, y = avgPower, color = factor(season))) +
  geom_line(linewidth = 2) +
  labs(x = "Time", y = "Average Power (kWh)", title = "The Power Generation Variety Across the Seasons", color = "season") +
  facet_wrap(~season) +
  gghighlight::gghighlight()

  • Summer exhibits the highest average power generation, with a broader, flatter peak extending from mid-morning to late afternoon. This season provides the most consistent and extended high-power output, a consequence of longer daylight hours and higher solar intensity.

  • Spring follows, showing a similar shape to summer but with slightly lower peak power values and a shorter span of high power output.

  • Autumn shows a comparable daily pattern to spring but with reduced peak values and a narrower high-power window, = reflecting decreasing daylight duration and solar angle.

  • Winter has the lowest overall power output, with a significantly narrower and lower peak. The curve is concentrated around noon, indicative of limited daylight and lower solar elevation during this season.

The chart highlights the strong seasonal dependence of solar power generation in a temperate climate, with peak generation capability in summer and spring and a marked reduction in winter. This variation emphasizes the need for seasonal adjustments in energy management and possibly supplementary energy sources during the winter months when solar generation is at its lowest.

Inventer Efficiency

Inventer Temperatures

Code
ggplot(dfReport, aes(x = Time, y = `Temperature(℃)`)) + 
  geom_point(alpha = .01, show.legend = F) +
  labs(
    title = "The Temperature trend of solar farm inverter",
    x = "",
    y = "Temperature(°C)"
  )

Inventer Efficiency

The visualization presents a detailed temporal breakdown of solar panel efficiency throughout each hour across the months of 2023 and 2024. The x-axis represents the hour of the day, segmented by month, while the y-axis shows the power output in watts as reported by the inverter every minute. Each dot signifies a reported power value, with colors indicating efficiency levels: from “0-50%” (red) to “95-100%” (green).

This grid-style faceted plot offers a month-by-month analysis, enabling a clear comparison of efficiency distributions across different hours and seasonal variations. Higher efficiencies (95-100%) appear in green and are most prevalent around midday, aligning with peak sunlight hours. The pattern of efficiency distribution displays a bell-shaped curve, with power rising in the morning, peaking around noon, and declining in the evening, reflecting the typical diurnal solar cycle.

Code
if (param.RunScripts) {
  dfReport |>
    mutate(
      Eff = `Power(W)` / 
        ((`V MPPT 1(V)` * `I MPPT 1(A)`) + (`V MPPT 2(V)` * `I MPPT 2(A)`)) 
    ) |>
    filter(Eff <= 1) |>
    mutate(
      Eff = cut(
        Eff, breaks = c(0, 50, 80, 95, 100)/100,
       labels = c("0-50%", "50-80%", "80-95%", "95-100%")
      )
    ) |>
    ggplot(aes(
      x = jitter(hour),
      y = `Power(W)`,
      color = Eff
    )) +
    geom_point(alpha = .1) +
    facet_grid(year ~ month) +
    scale_color_manual(values = c(
      "0-50%" = "#ea4440",
      "50-80%" = "#fdc914",
      "80-95%" = "#fff341",
      "95-100%" = "#228e5f"
    )) +
    guides(color = guide_legend(override.aes = list(alpha = 1))) +
    scale_x_continuous(breaks = c(5, 20)) +
    labs(
      x = "Hour",
      y = "Reported Power (W)",
      title = str_wrap("The solar panels efficiency throughout the hours, monthly breakdown", 60),
      subtitle = str_wrap("each dot represents a power in watts reported by inventer each minute")
    )
}

The visualization reveals seasonal efficiency variations, with summer months exhibiting a broader span of high-efficiency hours compared to winter. During winter, even midday hours show a mix of lower efficiency colors (yellow and orange, representing 50-95%), indicating reduced solar intensity or less optimal conditions. This pattern underscores the impact of seasonal sunlight availability on power output, with panels achieving maximum efficiency during prolonged daylight in summer.

Such a breakdown is invaluable for understanding daily and seasonal efficiency trends, guiding potential adjustments in energy usage and storage strategies. This insight can also aid in identifying hours where additional support from alternative energy sources might be necessary, particularly during the less efficient winter months.

Power Curve

The chart provides a detailed analysis of the solar farm’s power output in relation to ambient temperature, visualized alongside efficiency categories. The x-axis represents temperature in degrees Celsius, while the y-axis measures power output in watts. The data points are color-coded according to efficiency bands, ranging from “0-50%” (red) to “95-100%” (green), allowing for a nuanced view of how efficiency varies with temperature.

The plot includes a horizontal line at 5,600 W (purple), marking the approximate peak power capacity of the installation, labeled as “Max power = 5.6kWh.” A smooth trend line captures the overall relationship between temperature and maximum observed power, showing a bell-shaped curve where power peaks around the mid-temperature range before declining at higher temperatures. This trend suggests an optimal temperature range for power generation, beyond which efficiency and power output decrease.

Code
maxPpT <- dfReport |>
  group_by(`Temperature(℃)`) |>
  summarise(`Power(W)` = max(`Power(W)`)) |>
  group_by(rounded_temperature = round(`Temperature(℃)`)) |>
  mutate(max_power = max(`Power(W)`)) |>
  ungroup()

dfReport |>
    mutate(
      Eff = `Power(W)` / 
        ((`V MPPT 1(V)` * `I MPPT 1(A)`) + (`V MPPT 2(V)` * `I MPPT 2(A)`)) 
    ) |>
    filter(Eff <= 1) |>
    mutate(
      Eff = cut(
        Eff, breaks = c(0, 50, 80, 95, 100)/100,
       labels = c("0-50%", "50-80%", "80-95%", "95-100%")
      )
    ) |>
  ggplot(aes(x = `Temperature(℃)`, y = `Power(W)`)) +
  geom_hline(yintercept = 5600, color = '#390099') +
  annotate("text", x = 14, y = 5700, 
         label = "Max power = 5.6KWh", color = "#390099", fontface = 'bold') +
  geom_smooth(data = maxPpT, aes(y = max_power), se = F, color = "#4cc9f0") +
  guides(color = guide_legend(override.aes = list(alpha = 1))) +
  geom_point(aes(color = Eff), alpha = 0.25, size = rel(0.8)) +
    scale_color_manual(values = c(
      "0-50%" = "#ea4440",
      "50-80%" = "#fdc914",
      "80-95%" = "#fff341",
      "95-100%" = "#228e5f"
    )) +
  scale_x_continuous(expand = c(0, 0), breaks = seq(0, 60, 6)) +
  scale_y_continuous(expand = c(0, 0), limits = c(0, 5900)) +
  labs(
    y = "Power (W)",
   x = "Temperature °C",
   color = "Efficiency"
  ) +
  theme_minimal() +
  theme(text = element_text(colour = "grey40"))

The color-coded efficiency zones show that the highest efficiencies (green, 95-100%) are generally achieved across a wide temperature range, indicating that the system can maintain high efficiency in various thermal conditions. However, the presence of yellow and orange zones (representing lower efficiencies) at certain temperatures implies that factors other than temperature alone influence efficiency, possibly including irradiance and system constraints.

This visualization aids in understanding the impact of temperature on solar panel performance, showing that while moderate temperatures support high power output, extremely high temperatures may lead to reduced efficiency. This insight is crucial for managing thermal effects and maximizing energy yield under fluctuating environmental conditions.

Solar Elevation

Code
pacman::p_load(
  "sp",
  "suncalc"
)
# Define coordinates
latitude <- 50.579340
longitude <- 17.344725

# Create a dataframe with timestamps
dfSolarPosition <- tibble(
  timestamp = seq(
    from = ymd_hms("2023-03-31 00:00:00", tz = "UTC"),
    to = ymd_hms("2024-04-01 23:50:00", tz = "UTC"),
    by = '60 min'
  )
) %>%
  # Calculate the solar position for each timestamp at given coordinates
  rowwise() %>%
  mutate(solar_position = list(getSunlightPosition(date = timestamp, lat = latitude, lon = longitude))) %>%
  unnest_wider(solar_position)

dfSolarPosition <- dfSolarPosition |>
  mutate(
    year = year(timestamp),
    month = month(timestamp),
    day = day(timestamp),
    hour = hour(timestamp)
  )

dfSolarPosition <- dfSolarPosition |>
  mutate(
    altitude_deg = altitude * 180 / pi,
    azimuth_deg = azimuth * 180 / pi
  )

Optimal Exposure

The chart illustrates the seasonal variation in optimal hours when solar panels in a temperate climate are positioned at an ideal altitude range (20° to 70°) to maximize solar exposure. This analysis is based on data collected from a small-scale solar installation with a nominal capacity of 5.6 kWh. The x-axis represents the timeline across one year, while the y-axis shows the daily average number of optimal hours.

The curve indicates a clear seasonal pattern, with peak optimal hours observed during the summer months (approximately 9 hours per day) and a significant reduction during the winter (reaching as low as around 1 hours per day). This fluctuation aligns with the expected solar trajectory and daylight hours in a temperate climate, reflecting the reduced solar efficiency in winter due to lower solar angles and shorter daylight duration.

Code
optimal_altitude_range <- c(20, 70)

dfOptimalHours <- dfSolarPosition %>%
  filter(altitude_deg >= optimal_altitude_range[1] & altitude_deg <= optimal_altitude_range[2]) %>%
  group_by(year, month, day) %>%
  summarise(optimal_hours = n()) %>%
  mutate(optimal_hours = optimal_hours)  

ggplot(dfOptimalHours, aes(x = ymd(paste(year, month, day, sep = "-")), y = optimal_hours)) +
  geom_line(color = "#2C3E50") +
  scale_y_continuous(breaks = 1:12) +
  labs(
    title = "The average time when the solar panels are optimally oriented towards the sun",
    x = "Date",
    y = "Hours"
  ) 

This trend underscores the seasonal dependency of solar energy production in such climates, where extended optimal positioning of panels during summer months results in greater energy generation. Conversely, the limited optimal hours in winter highlight potential constraints in energy output, impacting the overall performance and efficiency of the solar farm. This analysis is crucial for understanding and forecasting the variability in solar power generation over the year, assisting in effective energy management and storage planning.

Sun’s Path

This 3D scatter plot visualizes the relationship between azimuth, altitude, and power generation during the summer season for the small-scale solar installation. The x-axis represents the azimuth angle (orientation relative to the sun’s position), the y-axis shows the altitude angle, and the z-axis indicates power output. Each point on the plot corresponds to a specific position and orientation of the solar panels throughout the day, with color encoding the power output level.

The plot reveals a concentrated area where higher altitude angles (between 40° and 60°) and azimuth values around 0° to 50° correlate with peak power generation, as depicted by the warmer colors (yellow-green range). Lower power outputs, represented by cooler colors (purple), tend to occur at lower altitude angles and azimuth positions further from the optimal orientation.

Code
dfReport |>
  group_by(year, month, day, hour) |>
  summarise(power = mean(`Power(W)`, na.rm = T)) |>
  inner_join(
    dfSolarPosition,
    join_by(
      hour == hour,
      day == day,
      month == month,
      year == year
    )
  ) |>
  mutate(
    season = case_when(
      month %in% 3:5 ~ "Spring",
      month %in% 6:8 ~ "Summer",
      month %in% 9:11 ~ "Autumn",
      T ~ "Winter"
    )) -> dfReportSunPosition

dfReportSunPosition |>
  mutate(across(.cols = c("azimuth_deg", "altitude_deg", "power"), .fns = ~round(.x, 1))) |>
  filter(season == "Summer") |>
  plotly::plot_ly(
    x = ~azimuth_deg, 
    y = ~altitude_deg, 
    z = ~power, 
    color = ~power, 
    type = "scatter3d", 
    mode = "markers",
    marker = list(
      size = 9,
      colorscale = list(c(0, "yellow"), c(0.5, "orange"), c(1, "red")),  # Skala kolorów od żółtego do czerwonego
      opacity = 0.9
    ),
    hoverinfo = "text",  # Ustawienie tooltipa na niestandardowy tekst
    text = ~paste(
      "Azymut: ", azimuth_deg, "°<br>",
      "Altitude: ", altitude_deg, "°<br>",
      "Power: ", power, " W"
    )
  ) |>
  plotly::layout(
    scene = list(
      xaxis = list(
        title = "Azimuth (°)",
        backgroundcolor = "rgb(255, 255, 255)",  # Białe tło
        gridcolor = "rgb(200, 200, 200)",  # Subtelna siatka
        showbackground = TRUE,
        zerolinecolor = "rgb(150, 150, 150)"
      ),
      yaxis = list(
        title = "Altitude (°)",
        backgroundcolor = "rgb(255, 255, 255)",
        gridcolor = "rgb(200, 200, 200)",
        showbackground = TRUE,
        zerolinecolor = "rgb(150, 150, 150)"
      ),
      zaxis = list(
        title = "Power (W)",
        backgroundcolor = "rgb(255, 255, 255)",
        gridcolor = "rgb(200, 200, 200)",
        showbackground = TRUE,
        zerolinecolor = "rgb(150, 150, 150)"
      )
    ),
    title = list(
      text = "Daily Power Generation againast a position of the Sun",
      font = list(size = 18, color = "black")
    )
  ) |>
  plotly::config(
    displayModeBar = FALSE, 
    hovermode = "closest"  
  ) |>
  plotly::hide_colorbar()

This 3D representation helps in visualizing the optimal positioning of panels in relation to the sun’s path to maximize power production during the summer months. It also emphasizes the spatial and angular dependencies of power generation, which are critical for fine-tuning panel orientation and maximizing efficiency during peak sunlight conditions. This insight can be valuable for scheduling seasonal adjustments in panel positioning to optimize energy capture in varying seasonal conditions.