Spaces:
Running
Running
| #' Plot CLIMAT Observational Data | |
| #' | |
| #' Creates 4 separate Plotly visualizations for observational climate data. | |
| #' Returns a list of 4 plots that can be displayed in separate cards. | |
| #' | |
| #' @param df Data frame with columns: Date, MeanTemp, MaxTempAbs, MinTempAbs, Precipitation, SunshineDuration, etc. | |
| #' @param station_name Title for the plot | |
| #' @return A list of 4 plotly objects: temperature, precipitation, sunshine, pressure | |
| #' @export | |
| plot_climat_obs_data <- function(df, station_name = "") { | |
| library(plotly) | |
| library(dplyr) | |
| # --- Helper Functions (Local) --- | |
| ensure_temporal_continuity <- function(df, resolution = NULL) { | |
| if (is.null(df) || nrow(df) < 2) { | |
| return(df) | |
| } | |
| # Auto-detect resolution if not provided | |
| if (is.null(resolution)) { | |
| dt_diff <- as.numeric(difftime(df$Date[2], df$Date[1], units = "days")) | |
| if (!is.na(dt_diff)) { | |
| if (dt_diff > 25) { | |
| resolution <- "monthly" | |
| } else if (dt_diff >= 0.9) { | |
| resolution <- "daily" | |
| } else { | |
| resolution <- "hourly" | |
| } | |
| } else { | |
| resolution <- "daily" # Fallback | |
| } | |
| } | |
| # Determine step | |
| if (resolution == "monthly") { | |
| step <- "1 month" | |
| } else if (resolution == "daily") { | |
| step <- "1 day" | |
| } else { | |
| step <- "1 hour" | |
| } | |
| full_time <- seq(from = min(df$Date), to = max(df$Date), by = step) | |
| complete_df <- data.frame(Date = full_time) | |
| df_out <- dplyr::left_join(complete_df, df, by = "Date") | |
| return(df_out) | |
| } | |
| split_into_chunks <- function(df, value_col) { | |
| if (!any(is.na(df[[value_col]]))) { | |
| return(list(df)) | |
| } | |
| is_valid <- !is.na(df[[value_col]]) | |
| r <- rle(is_valid) | |
| end_indices <- cumsum(r$lengths) | |
| start_indices <- c(1, head(end_indices, -1) + 1) | |
| chunks <- list() | |
| for (i in seq_along(r$values)) { | |
| if (r$values[i]) { | |
| chunks[[length(chunks) + 1]] <- df[start_indices[i]:end_indices[i], ] | |
| } | |
| } | |
| return(chunks) | |
| } | |
| empty_plot <- function(msg = "No data available") { | |
| plotly::plot_ly() %>% | |
| plotly::add_annotations( | |
| text = msg, | |
| showarrow = FALSE, | |
| xref = "paper", yref = "paper", | |
| x = 0.5, y = 0.5 | |
| ) %>% | |
| plotly::layout(xaxis = list(visible = FALSE), yaxis = list(visible = FALSE)) | |
| } | |
| if (is.null(df) || nrow(df) == 0) { | |
| return(list( | |
| temp = empty_plot(), | |
| precip = empty_plot(), | |
| sun = empty_plot(), | |
| pressure = empty_plot() | |
| )) | |
| } | |
| # Ensure Date is Date object | |
| df$Date <- as.Date(df$Date) | |
| # Ensure Continuity (Auto-detect) | |
| df <- ensure_temporal_continuity(df) | |
| # Calculate date range for display | |
| data_max <- max(df$Date, na.rm = TRUE) | |
| data_min <- min(df$Date, na.rm = TRUE) | |
| view_min <- data_max - (365 * 5) | |
| if (view_min < data_min) view_min <- data_min | |
| # Common x-axis configuration | |
| common_xaxis <- list( | |
| title = "", | |
| gridcolor = "rgba(200,200,200,0.3)", | |
| range = c(view_min, data_max), | |
| rangeselector = list( | |
| x = 1, y = 1.05, xanchor = "right", | |
| buttons = list( | |
| list(count = 1, label = "1y", step = "year", stepmode = "backward"), | |
| list(count = 5, label = "5y", step = "year", stepmode = "backward"), | |
| list(count = 10, label = "10y", step = "year", stepmode = "backward"), | |
| list(step = "all") | |
| ) | |
| ), | |
| rangeslider = list(visible = FALSE), | |
| type = "date" | |
| ) | |
| # --- Plot 1: Temperature --- | |
| has_temp <- "MeanTemp" %in% names(df) && any(!is.na(df$MeanTemp)) | |
| p_temp <- plotly::plot_ly(type = "scatter", mode = "lines") | |
| if (has_temp) { | |
| # Absolute Max/Min (Filled Envelope) | |
| if ("MinTempAbs" %in% names(df) && "MaxTempAbs" %in% names(df)) { | |
| # We need valid pairs for envelope. If one is NA, we can't draw the range. | |
| # So we split based on one of them (assuming they usually go together). | |
| chunks <- split_into_chunks(df, "MaxTempAbs") | |
| for (i in seq_along(chunks)) { | |
| chunk <- chunks[[i]] | |
| if (nrow(chunk) < 2) next | |
| p_temp <- p_temp %>% | |
| add_trace( | |
| data = chunk, | |
| x = ~Date, y = ~MinTempAbs, type = "scatter", mode = "lines", | |
| line = list(color = "rgba(211, 47, 47, 0.1)", width = 1), | |
| showlegend = FALSE, name = "Abs Min", | |
| hovertemplate = "Abs Min: %{y:.1f}°C<extra></extra>", | |
| connectgaps = FALSE, | |
| legendgroup = "abs_range" | |
| ) %>% | |
| add_trace( | |
| data = chunk, | |
| x = ~Date, y = ~MaxTempAbs, type = "scatter", mode = "lines", | |
| fill = "tonexty", fillcolor = "rgba(211, 47, 47, 0.1)", | |
| line = list(color = "rgba(211, 47, 47, 0.1)", width = 1), | |
| name = "Abs Max", | |
| showlegend = (i == 1), | |
| hovertemplate = "Abs Max: %{y:.1f}°C<extra></extra>", | |
| connectgaps = FALSE, | |
| legendgroup = "abs_range" | |
| ) | |
| } | |
| } | |
| # Mean Max/Min (Filled Envelope) | |
| if ("MeanMinTemp" %in% names(df) && "MeanMaxTemp" %in% names(df)) { | |
| chunks <- split_into_chunks(df, "MeanMaxTemp") | |
| for (i in seq_along(chunks)) { | |
| chunk <- chunks[[i]] | |
| if (nrow(chunk) < 2) next | |
| p_temp <- p_temp %>% | |
| add_trace( | |
| data = chunk, | |
| x = ~Date, y = ~MeanMinTemp, type = "scatter", mode = "lines", | |
| line = list(color = "rgba(211, 47, 47, 0.3)", width = 1), | |
| showlegend = FALSE, name = "Mean Min", | |
| hovertemplate = "Mean Min: %{y:.1f}°C<extra></extra>", | |
| connectgaps = FALSE, | |
| legendgroup = "mean_range" | |
| ) %>% | |
| add_trace( | |
| data = chunk, | |
| x = ~Date, y = ~MeanMaxTemp, type = "scatter", mode = "lines", | |
| fill = "tonexty", fillcolor = "rgba(211, 47, 47, 0.3)", | |
| line = list(color = "rgba(211, 47, 47, 0.3)", width = 1), | |
| name = "Mean Max", | |
| showlegend = (i == 1), | |
| hovertemplate = "Mean Max: %{y:.1f}°C<extra></extra>", | |
| connectgaps = FALSE, | |
| legendgroup = "mean_range" | |
| ) | |
| } | |
| } | |
| # Main Mean Temp Line (Use full DF with NAs for simple line break) | |
| p_temp <- p_temp %>% add_lines( | |
| data = df, | |
| x = ~Date, y = ~MeanTemp, name = "Mean Temp", | |
| line = list(color = "#b71c1c", width = 2), | |
| hovertemplate = "Mean Temp: %{y:.1f}°C<extra></extra>", | |
| connectgaps = FALSE | |
| ) | |
| } else { | |
| p_temp <- empty_plot("No temperature data") | |
| } | |
| p_temp <- p_temp %>% | |
| layout( | |
| title = list(text = "Temperature", font = list(size = 12), x = 0.05), | |
| yaxis = list(title = "Temp (°C)", gridcolor = "rgba(200,200,200,0.3)"), | |
| xaxis = common_xaxis, | |
| plot_bgcolor = "rgba(250,250,252,1)", | |
| hovermode = "x unified", | |
| legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.15), | |
| margin = list(t = 50, r = 10, b = 60, l = 50) | |
| ) %>% | |
| config(displaylogo = FALSE) | |
| # --- Plot 2: Precipitation --- | |
| has_precip <- "Precipitation" %in% names(df) && any(!is.na(df$Precipitation)) | |
| has_precip_days <- "PrecipDays" %in% names(df) && any(!is.na(df$PrecipDays)) | |
| p_precip <- plotly::plot_ly() | |
| if (has_precip) { | |
| p_precip <- p_precip %>% add_bars( | |
| data = df, | |
| x = ~Date, y = ~Precipitation, name = "Precipitation", | |
| marker = list(color = "#0277bd"), | |
| hovertemplate = "Precip: %{y:.1f} mm<extra></extra>" | |
| ) | |
| if (has_precip_days) { | |
| p_precip <- p_precip %>% add_trace( | |
| data = df, | |
| x = ~Date, y = ~PrecipDays, name = "Precip Days", | |
| yaxis = "y2", type = "scatter", mode = "markers", | |
| marker = list(color = "#388e3c", size = 5, line = list(color = "yellow", width = 1)), | |
| hovertemplate = "Days >1mm: %{y:.0f}<extra></extra>" | |
| ) | |
| } | |
| } else { | |
| p_precip <- empty_plot("No precipitation data") | |
| } | |
| p_precip <- p_precip %>% | |
| layout( | |
| title = list(text = "Precipitation", font = list(size = 12), x = 0.05), | |
| yaxis = list(title = "Precip (mm)", rangemode = "tozero", gridcolor = "rgba(200,200,200,0.3)"), | |
| yaxis2 = list(title = "Days", overlaying = "y", side = "right", showgrid = FALSE, rangemode = "tozero"), | |
| xaxis = common_xaxis, | |
| plot_bgcolor = "rgba(250,250,252,1)", | |
| hovermode = "x unified", | |
| legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.15), | |
| margin = list(t = 50, r = 50, b = 60, l = 50) | |
| ) %>% | |
| config(displaylogo = FALSE) | |
| # --- Plot 3: Sunshine Duration --- | |
| has_sun <- "SunshineDuration" %in% names(df) && any(!is.na(df$SunshineDuration)) | |
| p_sun <- plotly::plot_ly() | |
| if (has_sun) { | |
| p_sun <- p_sun %>% add_bars( | |
| data = df, | |
| x = ~Date, y = ~SunshineDuration, name = "Sunshine", | |
| marker = list(color = "#fbc02d"), | |
| hovertemplate = "Sun: %{y:.1f} h<extra></extra>" | |
| ) | |
| } else { | |
| p_sun <- empty_plot("No sunshine data") | |
| } | |
| p_sun <- p_sun %>% | |
| layout( | |
| title = list(text = "Sunshine Duration", font = list(size = 12), x = 0.05), | |
| yaxis = list(title = "Sun (h)", gridcolor = "rgba(200,200,200,0.3)"), | |
| xaxis = common_xaxis, | |
| plot_bgcolor = "rgba(250,250,252,1)", | |
| hovermode = "x unified", | |
| legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.15), | |
| margin = list(t = 50, r = 10, b = 60, l = 50) | |
| ) %>% | |
| config(displaylogo = FALSE) | |
| # --- Plot 4: Pressure (MSLP + Vapour Pressure) --- | |
| has_vapour <- "VapourPressure" %in% names(df) && any(!is.na(df$VapourPressure)) | |
| has_press <- "MeanSeaLevelPressure" %in% names(df) && any(!is.na(df$MeanSeaLevelPressure)) | |
| p_pressure <- plotly::plot_ly() | |
| if (has_press) { | |
| p_pressure <- p_pressure %>% | |
| add_lines( | |
| data = df, | |
| x = ~Date, y = ~MeanSeaLevelPressure, name = "MSLP", | |
| line = list(color = "#7b1fa2", width = 2), | |
| hovertemplate = "MSLP: %{y:.1f} hPa<extra></extra>", | |
| connectgaps = FALSE | |
| ) %>% | |
| add_lines( | |
| x = ~Date, y = rep(1013.25, nrow(df)), name = "Std (1013.25)", | |
| line = list(color = "grey", dash = "dash", width = 1), | |
| hoverinfo = "skip", | |
| inherit = FALSE | |
| ) | |
| } | |
| if (has_vapour) { | |
| p_pressure <- p_pressure %>% add_lines( | |
| data = df, | |
| x = ~Date, y = ~VapourPressure, name = "Vapour Pressure", | |
| yaxis = "y2", line = list(color = "#2e7d32", width = 2), | |
| hovertemplate = "VapPres: %{y:.1f} hPa<extra></extra>", | |
| connectgaps = FALSE | |
| ) | |
| } | |
| if (!has_press && !has_vapour) { | |
| p_pressure <- empty_plot("No pressure data") | |
| } | |
| p_pressure <- p_pressure %>% | |
| layout( | |
| title = list(text = "Pressure", font = list(size = 12), x = 0.05), | |
| yaxis = list(title = "MSLP (hPa)", gridcolor = "rgba(200,200,200,0.3)"), | |
| yaxis2 = list(title = "VP (hPa)", overlaying = "y", side = "right", showgrid = FALSE), | |
| xaxis = common_xaxis, | |
| plot_bgcolor = "rgba(250,250,252,1)", | |
| hovermode = "x unified", | |
| legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.15), | |
| margin = list(t = 50, r = 50, b = 60, l = 50) | |
| ) %>% | |
| config(displaylogo = FALSE) | |
| # Return list of 4 plots | |
| return(list( | |
| temp = p_temp, | |
| precip = p_precip, | |
| sun = p_sun, | |
| pressure = p_pressure | |
| )) | |
| } | |