jma / funs /plot_weather_jma.R
alexdum's picture
fix: filter NA DateTime values for robust sequence generation and remove default `plotly::plot_ly` type and mode.
0962165
# funs/plot_weather_jma.R
library(plotly)
library(dplyr)
library(lubridate)
#' Create a placeholder plot for missing data
#' @param message Message to display
#' @return A plotly object
create_empty_plot <- function(message = "Data not available for this parameter") {
plotly::plot_ly() %>%
plotly::add_trace(type = "scatter", mode = "markers", marker = list(opacity = 0), showlegend = FALSE) %>%
plotly::add_annotations(
text = message,
showarrow = FALSE,
xref = "paper", yref = "paper",
x = 0.5, y = 0.5,
font = list(size = 16, color = "#666")
) %>%
plotly::layout(
xaxis = list(visible = FALSE),
yaxis = list(visible = FALSE),
margin = list(t = 50, b = 50, l = 50, r = 50)
) %>%
plotly::config(displaylogo = FALSE)
}
#' Ensure Temporal Continuity
#' Fills missing time steps to prevent Plotly from linking gaps.
ensure_temporal_continuity <- function(df, resolution = NULL) {
if (is.null(df) || nrow(df) < 2) {
return(df)
}
# Ensure we have a datetime column
if (!"DateTime" %in% names(df)) {
return(df)
}
seq_by <- NULL
if (!is.null(resolution)) {
seq_by <- switch(resolution,
"Hourly" = "hour",
"Daily" = "day",
"Monthly" = "month",
"10 Minutes" = "10 min",
NULL
)
}
# Auto-detect if not provided
valid_dt <- df$DateTime[!is.na(df$DateTime)]
if (length(valid_dt) < 2) return(df)
if (is.null(seq_by)) {
# Use valid dates for diff
dt_vals <- as.numeric(difftime(valid_dt[2:min(length(valid_dt), 200)], valid_dt[1:(min(length(valid_dt), 200) - 1)], units = "mins"))
dt_vals <- dt_vals[!is.na(dt_vals) & dt_vals > 0]
if (length(dt_vals) > 0) {
dt <- min(dt_vals)
if (dt >= 40000) seq_by <- "month"
else if (dt >= 1400) seq_by <- "day"
else if (dt >= 50) seq_by <- "hour"
else seq_by <- "10 min"
} else {
return(df)
}
}
# Generate complete sequence
full_dates <- data.frame(
DateTime = seq(
from = min(valid_dt),
to = max(valid_dt),
by = seq_by
)
)
# Merge
# Use full_join if dplyr available, else merge
df_complete <- full_join(full_dates, df, by = "DateTime") %>%
arrange(DateTime)
return(df_complete)
}
#' Split Dataframe into Continuous Chunks
split_into_chunks <- function(df, value_col) {
if (nrow(df) == 0) return(list())
is_na_vec <- is.na(df[[value_col]])
if (all(is_na_vec)) return(list())
rle_res <- rle(is_na_vec)
chunk_ids <- rep(seq_along(rle_res$values), rle_res$lengths)
df$chunk_id <- chunk_ids
valid_chunks <- df[!is_na_vec, ]
if (nrow(valid_chunks) == 0) return(list())
split(valid_chunks, valid_chunks$chunk_id)
}
#' Clean JMA Data (Safety Check)
clean_jma_data <- function(df) {
if (is.null(df) || nrow(df) == 0) return(df)
# List of possible numeric columns to check
# JMA data from scraper usually has explicit NAs already, but let's be safe against flags
cols_to_check <- c(
"Temperature", "Temp_Mean", "Temp_Max", "Temp_Min",
"Precipitation", "Wind_Speed", "Humidity", "Pressure",
"Sunshine_Hours", "Sunshine_Minutes",
"Solar_Radiation",
"Snow_Depth", "Snowfall"
)
for (col in cols_to_check) {
if (col %in% names(df)) {
# If strictly character (e.g. some accidental parsing), force numeric
if (!is.numeric(df[[col]])) {
df[[col]] <- suppressWarnings(as.numeric(as.character(df[[col]])))
}
}
}
return(df)
}
#' Calculate Dew Point (Magnus Formula)
calculate_dew_point <- function(temp, rh) {
if (is.null(temp) | is.null(rh)) return(NA)
a <- 17.625
b <- 243.04
alpha <- log(rh / 100) + (a * temp) / (b + temp)
dp <- (b * alpha) / (a - alpha)
return(dp)
}
#' Create Temperature Plot (JMA Style)
#' Adapts DWD style: Red line, with bands for Min/Max if available
create_temperature_plot_jma <- function(df, resolution = NULL) {
df <- clean_jma_data(df)
df <- ensure_temporal_continuity(df, resolution)
if (is.null(df) || nrow(df) == 0) return(create_empty_plot("No Temperature Data"))
# Determine columns
has_temp <- "Temperature" %in% names(df) && any(!is.na(df$Temperature))
has_temp_mean <- "Temp_Mean" %in% names(df) && any(!is.na(df$Temp_Mean))
has_tmax <- "Temp_Max" %in% names(df) && any(!is.na(df$Temp_Max))
has_tmin <- "Temp_Min" %in% names(df) && any(!is.na(df$Temp_Min))
# For Daily/Monthly, we often have Temp_Mean. For Hourly, just Temperature.
# Normalize main temp column
main_col <- if (has_temp) "Temperature" else if (has_temp_mean) "Temp_Mean" else NULL
if (is.null(main_col) && !has_tmax && !has_tmin) {
return(create_empty_plot("No Temperature Data"))
}
# Date Range Title
date_range_str <- paste(format(min(df$DateTime), "%d %b %Y"), "-", format(max(df$DateTime), "%d %b %Y"))
p <- plotly::plot_ly(type = "scatter", mode = "lines")
# Add Bands if Tmin/Tmax exist
if (has_tmin && has_tmax) {
# Split chunks to avoid gap bridging on fills
# Use Tmin for chunking
chunks <- split_into_chunks(df, "Temp_Min")
for (i in seq_along(chunks)) {
chunk <- chunks[[i]]
if (nrow(chunk) < 1) next
show_leg <- (i == 1)
p <- p %>% plotly::add_trace(
data = chunk, x = ~DateTime, y = ~Temp_Min,
type = "scatter", mode = "lines",
name = "Daily Min",
line = list(color = "rgba(211, 47, 47, 0.4)", width = 1),
hovertemplate = "Min: %{y:.1f}°C<extra></extra>",
showlegend = FALSE,
legendgroup = "daily_range"
) %>% plotly::add_trace(
data = chunk, x = ~DateTime, y = ~Temp_Max,
type = "scatter", mode = "lines",
fill = "tonexty",
name = "Daily Range",
line = list(color = "rgba(211, 47, 47, 0.4)", width = 1),
fillcolor = "rgba(211, 47, 47, 0.2)",
hovertemplate = "Max: %{y:.1f}°C<extra></extra>",
showlegend = show_leg,
legendgroup = "daily_range"
)
}
}
# Main Line
if (!is.null(main_col)) {
p <- p %>% plotly::add_lines(
data = df, x = ~DateTime, y = as.formula(paste0("~", main_col)),
name = "Temperature",
line = list(color = "#b71c1c", width = 2),
hovertemplate = "Temp: %{y:.1f}°C<extra></extra>",
connectgaps = FALSE
)
}
p %>% plotly::layout(
title = list(text = paste("Temperature:", date_range_str), font = list(size = 14)),
xaxis = list(title = "", type = "date"),
yaxis = list(title = "Temperature (°C)"),
hovermode = "x unified",
legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.1),
margin = list(t = 50, b = 50, l = 50, r = 20)
) %>% plotly::config(displaylogo = FALSE)
}
#' Create Precipitation Plot (JMA Style)
#' Blue bars
create_precipitation_plot_jma <- function(df, resolution = NULL) {
df <- clean_jma_data(df)
df <- ensure_temporal_continuity(df, resolution)
if (is.null(df) || nrow(df) == 0) return(create_empty_plot("No Precipitation Data"))
if (!"Precipitation" %in% names(df)) return(create_empty_plot("No Precipitation Data"))
# Check if any data
if (all(is.na(df$Precipitation))) return(create_empty_plot("No Precipitation Data"))
date_range_str <- paste(format(min(df$DateTime), "%d %b %Y"), "-", format(max(df$DateTime), "%d %b %Y"))
plotly::plot_ly(data = df, x = ~DateTime, y = ~Precipitation, type = "bar",
name = "Precipitation",
marker = list(color = "#0277bd"),
hovertemplate = "Precip: %{y:.1f} mm<extra></extra>"
) %>% plotly::layout(
title = list(text = paste("Precipitation:", date_range_str), font = list(size = 14)),
xaxis = list(title = "", type = "date"),
yaxis = list(title = "Precipitation (mm)"),
hovermode = "x unified",
margin = list(t = 50, b = 50, l = 50, r = 20)
) %>% plotly::config(displaylogo = FALSE)
}
#' Create Wind Plot (JMA Style)
#' Green area for speed, points for Direction if available
create_wind_plot_jma <- function(df, resolution = NULL) {
df <- clean_jma_data(df)
df <- ensure_temporal_continuity(df, resolution)
if (is.null(df) || nrow(df) == 0) return(create_empty_plot("No Wind Data"))
if (!"Wind_Speed" %in% names(df)) return(create_empty_plot("No Wind Data"))
date_range_str <- paste(format(min(df$DateTime), "%d %b %Y"), "-", format(max(df$DateTime), "%d %b %Y"))
p <- plotly::plot_ly(type = "scatter", mode = "none")
# Wind Speed Area
chunks <- split_into_chunks(df, "Wind_Speed")
for (i in seq_along(chunks)) {
chunk <- chunks[[i]]
if (nrow(chunk) < 1) next
p <- p %>% plotly::add_lines(
data = chunk, x = ~DateTime, y = ~Wind_Speed,
name = "Wind Speed",
line = list(color = "#43a047", width = 1),
fill = "tozeroy",
fillcolor = "rgba(67, 160, 71, 0.5)",
hovertemplate = "Speed: %{y:.1f} m/s<extra></extra>",
showlegend = (i == 1),
legendgroup = "wind_speed"
)
}
# Wind Max Speed (Points)
if ("Wind_Max_Speed" %in% names(df) && any(!is.na(df$Wind_Max_Speed))) {
df_max <- df %>% filter(!is.na(Wind_Max_Speed))
# Use markers for Max Speed (DWD style)
p <- p %>% plotly::add_markers(
data = df_max, x = ~DateTime, y = ~Wind_Max_Speed,
name = "Max Speed",
marker = list(color = "#2e7d32", size = 5, symbol = "circle"),
hovertemplate = "Max: %{y:.1f} m/s<extra></extra>",
legendgroup = "wind_max"
)
}
# Wind Direction (if available) - Scatter Points
# Use Wind_Direction_Deg
if ("Wind_Direction_Deg" %in% names(df) && any(!is.na(df$Wind_Direction_Deg))) {
# Filter to non-NA
df_dir <- df %>% filter(!is.na(Wind_Direction_Deg))
# If too many points, maybe downsample or just rely on plotly's handling?
# DWD doesn't plot direction on the main timeline usually, they use a Wind Rose.
# But existing JMA code plotted it as points. Let's replicate that but better.
# Map degrees to cardinal for hover
p <- p %>% plotly::add_markers(
data = df_dir, x = ~DateTime, y = ~Wind_Direction_Deg,
yaxis = "y2",
name = "Direction",
marker = list(color = "purple", size = 3, opacity = 0.5),
hovertemplate = "Dir: %{y:.0f}°<extra></extra>"
)
}
layout_args <- list(
title = list(text = paste("Wind:", date_range_str), font = list(size = 14)),
xaxis = list(title = "", type = "date"),
yaxis = list(title = "Wind Speed (m/s)"),
margin = list(t = 50, b = 50, l = 50, r = 50),
hovermode = "x unified",
legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.1)
)
if ("Wind_Direction_Deg" %in% names(df) && any(!is.na(df$Wind_Direction_Deg))) {
layout_args$yaxis2 <- list(
title = "Direction (°)",
overlaying = "y",
side = "right",
range = c(0, 360),
tickmode = "array",
tickvals = seq(0, 360, 45),
ticktext = c("N", "NE", "E", "SE", "S", "SW", "W", "NW", "N")
)
}
do.call(plotly::layout, c(list(p), layout_args)) %>% plotly::config(displaylogo = FALSE)
}
#' Create Humidity Plot (JMA Style)
#' Dew Point vs RH
create_humidity_plot_jma <- function(df, resolution = NULL) {
df <- clean_jma_data(df)
df <- ensure_temporal_continuity(df, resolution)
if (is.null(df) || nrow(df) == 0) return(create_empty_plot("No Humidity Data"))
if (!"Humidity" %in% names(df)) return(create_empty_plot("No Humidity Data"))
# Calculate Dew Point if possible
# Need Temperature
has_temp <- "Temperature" %in% names(df) || "Temp_Mean" %in% names(df)
temp_col <- if ("Temperature" %in% names(df)) "Temperature" else "Temp_Mean"
if (has_temp) {
df$Dew_Point <- calculate_dew_point(df[[temp_col]], df$Humidity)
}
date_range_str <- paste(format(min(df$DateTime), "%d %b %Y"), "-", format(max(df$DateTime), "%d %b %Y"))
p <- plotly::plot_ly(type = "scatter", mode = "none")
# Dew Point (Blue Line)
if ("Dew_Point" %in% names(df) && any(!is.na(df$Dew_Point))) {
p <- p %>% plotly::add_lines(
data = df, x = ~DateTime, y = ~Dew_Point,
name = "Dew Point",
line = list(color = "#1e88e5", width = 2),
hovertemplate = "Dew Pt: %{y:.1f}°C<extra></extra>",
connectgaps = FALSE
)
}
# Humidity (Green Dot Line, Right Axis)
p <- p %>% plotly::add_lines(
data = df, x = ~DateTime, y = ~Humidity,
name = "Humidity",
line = list(color = "#43a047", width = 1.5, dash = "dot"),
hovertemplate = "RH: %{y:.0f}%<extra></extra>",
yaxis = "y2",
connectgaps = FALSE
)
p %>% plotly::layout(
title = list(text = paste("Humidity:", date_range_str), font = list(size = 14)),
xaxis = list(title = "", type = "date"),
yaxis = list(title = "Dew Point (°C)"),
yaxis2 = list(title = "Relative Humidity (%)", overlaying = "y", side = "right", range = c(0, 100)),
margin = list(t = 50, b = 50, l = 50, r = 50),
hovermode = "x unified",
legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.1)
) %>% plotly::config(displaylogo = FALSE)
}
#' Create Sunshine Plot (JMA Style)
#' Yellow Bars
create_sunshine_plot_jma <- function(df, resolution = NULL) {
df <- clean_jma_data(df)
df <- ensure_temporal_continuity(df, resolution)
col <- if ("Sunshine_Hours" %in% names(df)) "Sunshine_Hours" else if ("Sunshine_Minutes" %in% names(df)) "Sunshine_Minutes" else NULL
if (is.null(col) || all(is.na(df[[col]]))) return(create_empty_plot("No Sunshine Data"))
date_range_str <- paste(format(min(df$DateTime), "%d %b %Y"), "-", format(max(df$DateTime), "%d %b %Y"))
unit <- if (col == "Sunshine_Hours") "hours" else "min"
plotly::plot_ly(data = df, x = ~DateTime, y = as.formula(paste0("~", col)), type = "bar",
name = "Sunshine",
marker = list(color = "rgba(255, 215, 0, 0.6)", line = list(color = "#FFD700", width = 1.5)),
hovertemplate = paste0("Sunshine: %{y:.1f} ", unit, "<extra></extra>")
) %>% plotly::layout(
title = list(text = paste("Sunshine Duration:", date_range_str), font = list(size = 14)),
xaxis = list(title = "", type = "date"),
yaxis = list(title = paste0("Duration (", unit, ")")),
hovermode = "x unified",
margin = list(t = 50, b = 50, l = 50, r = 20)
) %>% plotly::config(displaylogo = FALSE)
}
#' Create Pressure Plot (JMA Style)
#' Purple Line
create_pressure_plot_jma <- function(df, resolution = NULL) {
df <- clean_jma_data(df)
df <- ensure_temporal_continuity(df, resolution)
has_sta <- "Pressure" %in% names(df) && any(!is.na(df$Pressure))
has_sea <- "Pressure_Sea_Level" %in% names(df) && any(!is.na(df$Pressure_Sea_Level))
if (is.null(df) || nrow(df) == 0 || (!has_sta && !has_sea)) {
return(create_empty_plot("No Pressure Data"))
}
date_range_str <- paste(format(min(df$DateTime), "%d %b %Y"), "-", format(max(df$DateTime), "%d %b %Y"))
p <- plotly::plot_ly(type = "scatter", mode = "lines")
if (has_sta) {
p <- p %>% plotly::add_lines(
data = df, x = ~DateTime, y = ~Pressure,
name = "Station Pressure",
line = list(color = "#7b1fa2", width = 1.5),
hovertemplate = "Station: %{y:.1f} hPa<extra></extra>",
connectgaps = FALSE
)
}
if (has_sea) {
p <- p %>% plotly::add_lines(
data = df, x = ~DateTime, y = ~Pressure_Sea_Level,
name = "Sea Level Pressure",
line = list(color = "#AB47BC", width = 1.5, dash = "dot"),
hovertemplate = "Sea Level: %{y:.1f} hPa<extra></extra>",
connectgaps = FALSE
)
}
p %>% plotly::add_lines(
x = c(min(df$DateTime), max(df$DateTime)),
y = c(1013.25, 1013.25),
name = "Standard (1013.25)",
line = list(color = "rgba(100, 100, 100, 0.5)", width = 1, dash = "dash"),
showlegend = TRUE,
hoverinfo = "skip"
) %>% plotly::layout(
title = list(text = paste("Pressure:", date_range_str), font = list(size = 14)),
xaxis = list(title = "", type = "date"),
yaxis = list(title = "Pressure (hPa)"),
hovermode = "x unified",
margin = list(t = 50, b = 50, l = 50, r = 20),
legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.1)
) %>% plotly::config(displaylogo = FALSE)
}
#' Create Solar Radiation Plot (JMA Style)
#' Filled Yellow Area
create_solar_plot_jma <- function(df, resolution = NULL) {
df <- clean_jma_data(df)
df <- ensure_temporal_continuity(df, resolution)
if (is.null(df) || nrow(df) == 0) return(create_empty_plot("No Solar Data"))
# Check column name - JMA might call it Solar_Radiation
if (!"Solar_Radiation" %in% names(df) || all(is.na(df$Solar_Radiation))) return(create_empty_plot("No Solar Data"))
date_range_str <- paste(format(min(df$DateTime), "%d %b %Y"), "-", format(max(df$DateTime), "%d %b %Y"))
p <- plotly::plot_ly(type = "scatter", mode = "none")
chunks <- split_into_chunks(df, "Solar_Radiation")
for (i in seq_along(chunks)) {
chunk <- chunks[[i]]
if (nrow(chunk) < 1) next
p <- p %>% plotly::add_trace(
data = chunk,
x = ~DateTime,
y = ~Solar_Radiation,
name = "Global Radiation",
type = "scatter",
mode = "lines",
fill = "tozeroy",
line = list(color = "rgba(255, 179, 0, 0.9)", width = 1),
fillcolor = "rgba(255, 179, 0, 0.3)",
hovertemplate = "Solar: %{y:.2f} MJ/m²<extra></extra>",
connectgaps = FALSE,
showlegend = (i == 1),
legendgroup = "solar_rad"
)
}
p %>% plotly::layout(
title = list(text = paste("Solar Radiation:", date_range_str), font = list(size = 14)),
xaxis = list(title = "", type = "date"),
yaxis = list(title = "Radiation (MJ/m²)"),
hovermode = "x unified",
margin = list(t = 50, b = 50, l = 50, r = 20),
legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.1)
) %>% plotly::config(displaylogo = FALSE)
}
#' Create Snow Plot (JMA Style)
#' Depth (Area) + Snowfall (Bar)
create_snow_plot_jma <- function(df, resolution = NULL) {
df <- clean_jma_data(df)
df <- ensure_temporal_continuity(df, resolution)
has_depth <- "Snow_Depth" %in% names(df) && any(!is.na(df$Snow_Depth))
has_fall <- "Snowfall" %in% names(df) && any(!is.na(df$Snowfall))
if (!has_depth && !has_fall) return(create_empty_plot("No Snow Data"))
date_range_str <- paste(format(min(df$DateTime), "%d %b %Y"), "-", format(max(df$DateTime), "%d %b %Y"))
p <- plotly::plot_ly()
if (has_depth) {
chunks <- split_into_chunks(df, "Snow_Depth")
for (i in seq_along(chunks)) {
chunk <- chunks[[i]]
if (nrow(chunk) < 1) next
p <- p %>% plotly::add_lines(
data = chunk, x = ~DateTime, y = ~Snow_Depth,
name = "Snow Depth",
line = list(color = "#90caf9", width = 1),
fill = "tozeroy",
fillcolor = "rgba(144, 202, 249, 0.5)",
hovertemplate = "Depth: %{y:.0f} cm<extra></extra>",
showlegend = (i == 1),
legendgroup = "snow_depth"
)
}
}
if (has_fall) {
p <- p %>% plotly::add_bars(
data = df, x = ~DateTime, y = ~Snowfall,
name = "Snowfall",
yaxis = "y2",
marker = list(color = "#1565c0"),
hovertemplate = "Snowfall: %{y:.0f} cm<extra></extra>"
)
}
layout_args <- list(
title = list(text = paste("Snow:", date_range_str), font = list(size = 14)),
xaxis = list(title = "", type = "date"),
yaxis = list(title = "Snow Depth (cm)"),
margin = list(t = 50, b = 50, l = 50, r = 50),
hovermode = "x unified",
legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.1)
)
if (has_fall) {
layout_args$yaxis2 <- list(
title = "Snowfall (cm)",
overlaying = "y",
side = "right",
rangemode = "tozero"
)
}
do.call(plotly::layout, c(list(p), layout_args)) %>% plotly::config(displaylogo = FALSE)
}
#' Create Wind Rose Plot (JMA)
#'
#' @param df Data frame with JMA data
#' @return A plotly polar bar chart
#' @export
create_wind_rose_jma <- function(df, resolution = "Hourly") {
if (is.null(df) || nrow(df) == 0) {
return(plotly::plot_ly() %>% plotly::layout(title = "No data"))
}
# Ensure Wind_Speed and (converted) Wind_Direction_Deg exist
if (!all(c("Wind_Speed", "Wind_Direction_Deg") %in% names(df))) {
return(plotly::plot_ly() %>% plotly::layout(title = "Wind data not available (Speed/Dir missing)"))
}
# Filter valid data
wind_df <- df %>%
dplyr::filter(!is.na(Wind_Speed), !is.na(Wind_Direction_Deg))
if (nrow(wind_df) == 0) {
return(plotly::plot_ly() %>% plotly::layout(title = "No valid wind data (N/A)"))
}
date_range_str <- paste(
format(min(df$DateTime), "%Y-%m-%d"), "-",
format(max(df$DateTime), "%Y-%m-%d")
)
# Binning Logic (same as DWD)
wind_df <- wind_df %>%
dplyr::mutate(
# Bin Direction to 16 sectors (22.5 deg)
dir_bin = round(Wind_Direction_Deg / 22.5) * 22.5,
dir_bin = ifelse(dir_bin == 360, 0, dir_bin),
# Bin Speed
speed_cat = cut(
Wind_Speed,
breaks = c(-0.1, 2, 5, 10, 20, Inf),
labels = c("0-2", "2-5", "5-10", "10-20", "20+")
)
) %>%
dplyr::group_by(dir_bin, speed_cat) %>%
dplyr::summarise(count = dplyr::n(), .groups = "drop")
total_obs <- sum(wind_df$count)
wind_df <- wind_df %>%
dplyr::mutate(percentage = count / total_obs * 100)
# Compass labels
compass <- data.frame(
dir_bin = seq(0, 337.5, by = 22.5),
label = c("N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW")
)
p <- plotly::plot_ly(type = "barpolar")
speed_levels <- levels(wind_df$speed_cat)
# DWD Colors: Blue -> Green -> Yellow -> Orange -> Red
colors <- c("#2196F3", "#4CAF50", "#FFC107", "#FF9800", "#F44336")
for (i in seq_along(speed_levels)) {
lvl <- speed_levels[i]
lvl_df <- wind_df %>% dplyr::filter(speed_cat == lvl)
if (nrow(lvl_df) > 0) {
p <- p %>%
plotly::add_trace(
data = lvl_df, type = "barpolar",
r = ~percentage, theta = ~dir_bin,
name = lvl, marker = list(color = colors[i]),
hovertemplate = paste0("Speed: ", lvl, " m/s<br>Freq: %{r:.1f}%<extra></extra>")
)
}
}
p %>%
plotly::layout(
title = list(text = paste("Wind Rose:", date_range_str), font = list(size = 14)),
polar = list(
radialaxis = list(ticksuffix = "%", angle = 45),
angularaxis = list(
rotation = 90, direction = "clockwise",
tickmode = "array", tickvals = compass$dir_bin, ticktext = compass$label
)
),
showlegend = TRUE,
legend = list(title = list(text = "Wind Speed (m/s)"), orientation = "v"),
margin = list(t = 50, b = 50, l = 50, r = 50)
)
}