alexdum commited on
Commit
357f1ab
·
1 Parent(s): 1662122

feat: Enhance DWD metadata with date ranges and station availability, add 24h index expiry, remove parse date filtering, and improve solar plot with daily aggregation and dual axes.

Browse files
Files changed (6) hide show
  1. funs/dwd_metadata.R +9 -0
  2. funs/parse_dwd.R +7 -7
  3. funs/plot_weather_dwd.R +84 -28
  4. global.R +65 -5
  5. server.R +114 -22
  6. ui.R +2 -2
funs/dwd_metadata.R CHANGED
@@ -58,6 +58,13 @@ fetch_dwd_file_index <- function() {
58
  }
59
 
60
  if (length(zip_files) > 0) {
 
 
 
 
 
 
 
61
  # Create index DataFrame
62
  df <- data.frame(
63
  id = ids,
@@ -65,6 +72,8 @@ fetch_dwd_file_index <- function() {
65
  url = paste0(url, zip_files), # Absolute URL
66
  param = param,
67
  type = subtype,
 
 
68
  stringsAsFactors = FALSE
69
  )
70
  index_list[[paste(param, subtype, sep = "_")]] <- df
 
58
  }
59
 
60
  if (length(zip_files) > 0) {
61
+ # Extract dates if possible (format: _YYYYMMDD_YYYYMMDD_)
62
+ # Pattern: _(17|18|19|20)\d{6}_(17|18|19|20)\d{6}
63
+ date_pattern <- "_((?:17|18|19|20)\\d{6})_((?:17|18|19|20)\\d{6})"
64
+
65
+ start_dates <- str_match(zip_files, date_pattern)[, 2]
66
+ end_dates <- str_match(zip_files, date_pattern)[, 3]
67
+
68
  # Create index DataFrame
69
  df <- data.frame(
70
  id = ids,
 
72
  url = paste0(url, zip_files), # Absolute URL
73
  param = param,
74
  type = subtype,
75
+ start_date = start_dates,
76
+ end_date = end_dates,
77
  stringsAsFactors = FALSE
78
  )
79
  index_list[[paste(param, subtype, sep = "_")]] <- df
funs/parse_dwd.R CHANGED
@@ -100,13 +100,13 @@ read_dwd_data <- function(zip_path, start_date = NULL, end_date = NULL) {
100
  # Filter valid dates
101
  df <- df[!is.na(df$datetime), ]
102
 
103
- # Window Filter
104
- if (!is.null(start_date)) {
105
- df <- df[df$datetime >= (as.POSIXct(start_date) - days(1)), ]
106
- }
107
- if (!is.null(end_date)) {
108
- df <- df[df$datetime <= (as.POSIXct(end_date) + days(1)), ]
109
- }
110
 
111
  # Column Mapping
112
  weather_cols <- c("temp", "rh", "precip", "wind_speed", "wind_dir", "pressure", "station_pressure", "cloud_cover", "wind_gust_max", "solar_global", "sunshine_duration")
 
100
  # Filter valid dates
101
  df <- df[!is.na(df$datetime), ]
102
 
103
+ # Window Filter - REMOVED to allow caller to inspect full range
104
+ # if (!is.null(start_date)) {
105
+ # df <- df[df$datetime >= (as.POSIXct(start_date) - days(1)), ]
106
+ # }
107
+ # if (!is.null(end_date)) {
108
+ # df <- df[df$datetime <= (as.POSIXct(end_date) + days(1)), ]
109
+ # }
110
 
111
  # Column Mapping
112
  weather_cols <- c("temp", "rh", "precip", "wind_speed", "wind_dir", "pressure", "station_pressure", "cloud_cover", "wind_gust_max", "solar_global", "sunshine_duration")
funs/plot_weather_dwd.R CHANGED
@@ -628,7 +628,7 @@ create_diurnal_plot <- function(df, offset_hours = 0) {
628
  plotly::config(displaylogo = FALSE)
629
  }
630
 
631
- #' Create Solar Plot (Preserved)
632
  create_solar_plot <- function(df) {
633
  if (is.null(df) || nrow(df) == 0) {
634
  return(create_empty_plot("No data available for the selected period"))
@@ -637,49 +637,105 @@ create_solar_plot <- function(df) {
637
  # Centralized cleaning
638
  df <- clean_dwd_data(df)
639
 
 
640
  has_global <- "solar_global" %in% names(df) && any(!is.na(df$solar_global))
641
  has_sun <- "sunshine_duration" %in% names(df) && any(!is.na(df$sunshine_duration))
642
 
643
  if (!has_global && !has_sun) {
644
- return(create_empty_plot("Solar radiation and sunshine data not available"))
 
645
  }
646
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
  date_range_str <- paste(
648
- format(min(df$datetime), "%d %b %Y"), "-",
649
- format(max(df$datetime), "%d %b %Y")
650
  )
651
 
652
- p <- plotly::plot_ly(type = "scatter", mode = "none")
653
-
654
- # Global Radiation
655
- if ("solar_global" %in% names(df)) {
656
- p <- p %>% plotly::add_lines(
657
- data = df %>% dplyr::filter(!is.na(solar_global)),
658
- x = ~datetime,
659
- y = ~solar_global, name = "Global Rad (J/cm²)",
660
- line = list(color = "#ffb300"), type = "scatter", mode = "lines",
661
- showlegend = TRUE
662
- )
 
 
 
 
 
 
 
 
 
 
 
663
  }
664
 
665
- # Sunshine
666
- if ("sunshine_duration" %in% names(df)) {
667
- p <- p %>% plotly::add_lines(
668
- data = df %>% dplyr::filter(!is.na(sunshine_duration)),
669
- x = ~datetime,
670
- y = ~sunshine_duration, name = "Sunshine (min)",
671
- line = list(color = "#fbc02d", dash = "dot"), type = "scatter", mode = "lines",
672
- showlegend = TRUE
673
- )
 
 
 
 
 
 
 
674
  }
675
 
 
676
  p %>%
677
  plotly::layout(
678
- title = list(text = paste("Solar Radiation & Sunshine:", date_range_str), font = list(size = 14)),
679
- xaxis = list(title = "", type = "date"),
680
- yaxis = list(title = "Value"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
  hovermode = "x unified",
682
- hoverlabel = list()
 
 
 
683
  ) %>%
684
  plotly::config(displaylogo = FALSE)
685
  }
 
628
  plotly::config(displaylogo = FALSE)
629
  }
630
 
631
+ #' Create Solar Plot (Enhanced - Daily Aggregation)
632
  create_solar_plot <- function(df) {
633
  if (is.null(df) || nrow(df) == 0) {
634
  return(create_empty_plot("No data available for the selected period"))
 
637
  # Centralized cleaning
638
  df <- clean_dwd_data(df)
639
 
640
+ # Check availability
641
  has_global <- "solar_global" %in% names(df) && any(!is.na(df$solar_global))
642
  has_sun <- "sunshine_duration" %in% names(df) && any(!is.na(df$sunshine_duration))
643
 
644
  if (!has_global && !has_sun) {
645
+ # It's possible the data exists in the file but not this window.
646
+ return(create_empty_plot("No solar/sunshine data in selected time range"))
647
  }
648
 
649
+ # --- Daily Aggregation ---
650
+ df_daily <- df %>%
651
+ dplyr::mutate(date = as.Date(datetime)) %>%
652
+ dplyr::group_by(date) %>%
653
+ dplyr::summarise(
654
+ # Sum J/cm^2 per hour -> Total Daily Energy J/cm^2
655
+ daily_solar = if (all(is.na(solar_global))) NA_real_ else sum(solar_global, na.rm = TRUE),
656
+
657
+ # Sum Minutes -> Convert to Hours
658
+ daily_sun_hours = if (all(is.na(sunshine_duration))) NA_real_ else sum(sunshine_duration, na.rm = TRUE) / 60,
659
+ .groups = "drop"
660
+ )
661
+
662
  date_range_str <- paste(
663
+ format(min(df_daily$date), "%d %b %Y"), "-",
664
+ format(max(df_daily$date), "%d %b %Y")
665
  )
666
 
667
+ # Base Plot
668
+ p <- plotly::plot_ly()
669
+
670
+ # 1. Global Radiation (Filled Area, Primary Y-Axis)
671
+ # Intuition: "Total Energy per Day"
672
+ if (has_global) {
673
+ sub_g <- df_daily %>% dplyr::filter(!is.na(daily_solar))
674
+ if (nrow(sub_g) > 0) {
675
+ p <- p %>% plotly::add_trace(
676
+ data = sub_g,
677
+ x = ~date,
678
+ y = ~daily_solar,
679
+ name = "Daily Global Radiation",
680
+ type = "scatter",
681
+ mode = "lines",
682
+ fill = "tozeroy",
683
+ line = list(color = "rgba(255, 179, 0, 0.9)", width = 1), # Amber
684
+ fillcolor = "rgba(255, 179, 0, 0.3)",
685
+ hovertemplate = "Daily Rad: %{y:,.0f} J/cm²<extra></extra>", # Comma for thousands
686
+ yaxis = "y"
687
+ )
688
+ }
689
  }
690
 
691
+ # 2. Sunshine Duration (Bars, Secondary Y-Axis)
692
+ # Intuition: "Total Hours per Day"
693
+ if (has_sun) {
694
+ sub_s <- df_daily %>% dplyr::filter(!is.na(daily_sun_hours))
695
+ if (nrow(sub_s) > 0) {
696
+ # We use bar chart for sunshine
697
+ p <- p %>% plotly::add_bars(
698
+ data = sub_s,
699
+ x = ~date,
700
+ y = ~daily_sun_hours,
701
+ name = "Daily Sunshine",
702
+ marker = list(color = "rgba(0, 0, 0, 0.3)", line = list(color = "#FFD700", width = 1.5)), # Gold/Yellow outline
703
+ hovertemplate = "Sunshine: %{y:.1f} h<extra></extra>",
704
+ yaxis = "y2"
705
+ )
706
+ }
707
  }
708
 
709
+ # Layout Configuration for Dual Axis
710
  p %>%
711
  plotly::layout(
712
+ title = list(text = paste("Daily Solar Energy & Sunshine:", date_range_str), font = list(size = 14)),
713
+ xaxis = list(
714
+ title = "",
715
+ type = "date",
716
+ domain = c(0.05, 0.95)
717
+ ),
718
+ yaxis = list(
719
+ title = "Daily Radiation (J/cm²)",
720
+ titlefont = list(color = "#ffb300"),
721
+ tickfont = list(color = "#ffb300"),
722
+ side = "left",
723
+ showgrid = TRUE
724
+ ),
725
+ yaxis2 = list(
726
+ title = "Sunshine (Hours)",
727
+ titlefont = list(color = "#FFD700"),
728
+ tickfont = list(color = "#FFD700"),
729
+ side = "right",
730
+ overlaying = "y",
731
+ range = c(0, 18), # Max theoretical sunshine is < 17h in Germany
732
+ showgrid = FALSE
733
+ ),
734
  hovermode = "x unified",
735
+ hoverlabel = list(),
736
+ legend = list(orientation = "h", x = 0.5, xanchor = "center", y = 1.1),
737
+ margin = list(t = 60, b = 40, l = 60, r = 60),
738
+ modebar = list(orientation = "h")
739
  ) %>%
740
  plotly::config(displaylogo = FALSE)
741
  }
global.R CHANGED
@@ -42,13 +42,23 @@ if (!dir.exists("data")) dir.create("data")
42
  index_file <- "data/dwd_file_index.rds"
43
  station_file <- "data/dwd_stations.rds"
44
 
45
- # 1. File Index
46
- if (!file.exists(index_file)) {
47
- message("Building DWD File Index (this may take 10-20 seconds)...")
 
 
 
 
 
 
 
 
 
 
 
 
48
  dwd_index <- fetch_dwd_file_index()
49
  saveRDS(dwd_index, index_file)
50
- } else {
51
- dwd_index <- readRDS(index_file)
52
  }
53
 
54
  # 2. Station List
@@ -60,6 +70,56 @@ if (!file.exists(station_file)) {
60
  stations <- readRDS(station_file)
61
  }
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  # 3. Enrich Station List with Available Parameters
64
  if (exists("dwd_index") && exists("stations")) {
65
  param_summary <- dwd_index %>%
 
42
  index_file <- "data/dwd_file_index.rds"
43
  station_file <- "data/dwd_stations.rds"
44
 
45
+ # 1. File Index (with 24h Expiry)
46
+ if (file.exists(index_file)) {
47
+ # Check age
48
+ info <- file.info(index_file)
49
+ age_hours <- difftime(Sys.time(), info$mtime, units = "hours")
50
+
51
+ if (age_hours > 24) {
52
+ message(paste("File index is", round(age_hours, 1), "hours old. Rebuilding..."))
53
+ dwd_index <- fetch_dwd_file_index()
54
+ saveRDS(dwd_index, index_file)
55
+ } else {
56
+ dwd_index <- readRDS(index_file)
57
+ }
58
+ } else {
59
+ message("Building DWD File Index (first run)...")
60
  dwd_index <- fetch_dwd_file_index()
61
  saveRDS(dwd_index, index_file)
 
 
62
  }
63
 
64
  # 2. Station List
 
70
  stations <- readRDS(station_file)
71
  }
72
 
73
+ # 3. Enhance Station Metadata with Detailed Availability
74
+ # Group DWD Index by ID to get ranges per parameter
75
+ if (!is.null(dwd_index) && nrow(stations) > 0) {
76
+ param_summary <- dwd_index %>%
77
+ group_by(id, param) %>%
78
+ summarise(
79
+ # Combine ranges
80
+ # Suppress warnings for Inf/NA when no hist data exists
81
+ min_start = suppressWarnings(min(start_date, na.rm = TRUE)),
82
+ max_end = suppressWarnings(max(end_date, na.rm = TRUE)),
83
+ has_recent = any(type == "recent"),
84
+ has_hist = any(type == "historical"),
85
+ .groups = "drop"
86
+ ) %>%
87
+ mutate(
88
+ # Logic:
89
+ # If has_hist, we have a start date (e.g. 19800101) as character
90
+ # If has_recent only, min_start is Inf (numeric)
91
+
92
+ # Start Year
93
+ s_year = ifelse(is.character(min_start), substr(min_start, 1, 4), "Recent"),
94
+
95
+ # End Year
96
+ # If has_recent, assume "Present"
97
+ # If only hist, uses max_end (character)
98
+ e_year = ifelse(has_recent, "Present",
99
+ ifelse(is.character(max_end), substr(max_end, 1, 4), "Unknown")
100
+ ),
101
+
102
+ # Formatting
103
+ # Avoid "Recent-Present", just say "Recent"
104
+ label = ifelse(s_year == "Recent" & e_year == "Present",
105
+ paste0(str_to_title(param), " (Recent)"),
106
+ paste0(str_to_title(param), " (", s_year, "-", e_year, ")")
107
+ )
108
+ ) %>%
109
+ group_by(id) %>%
110
+ summarise(
111
+ detailed_summary = paste(label, collapse = ", ")
112
+ )
113
+
114
+ # Join back to stations
115
+ stations <- stations %>%
116
+ left_join(param_summary, by = "id") %>%
117
+ mutate(
118
+ # Fallback
119
+ detailed_summary = ifelse(is.na(detailed_summary), "No Data", detailed_summary)
120
+ )
121
+ }
122
+
123
  # 3. Enrich Station List with Available Parameters
124
  if (exists("dwd_index") && exists("stations")) {
125
  param_summary <- dwd_index %>%
server.R CHANGED
@@ -8,27 +8,75 @@ server <- function(input, output, session) {
8
 
9
  # Filtered stations based on Sidebar inputs
10
  filtered_stations <- reactive({
11
- req(stations)
12
  df <- stations
13
 
14
- # Filter by State
15
- if (!is.null(input$state_filter)) {
16
- df <- df %>% filter(state %in% input$state_filter)
17
- }
18
-
19
  # Filter by Date (metadata availability)
20
- # Using 'recent' file implies they are active or recent.
21
- # We don't have start/end year easily compatible with date range without more parsing.
22
- # For now, we assume all stations in the list are "available".
 
 
 
 
 
23
 
24
  df
25
  })
26
 
27
- # Initialize State Choices
28
  observe({
29
- req(stations)
30
- states <- sort(unique(stations$state))
31
- updateSelectInput(session, "state_filter", choices = states)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  })
33
 
34
  # Station Count
@@ -83,8 +131,8 @@ server <- function(input, output, session) {
83
  # Pre-process label with HTML
84
  df <- df %>% mutate(
85
  label_html = purrr::pmap_chr(
86
- list(name, id, state, start_date, end_date, available_params),
87
- function(name, id, state, start_date, end_date, available_params) {
88
  tryCatch(
89
  {
90
  s <- as.Date(as.character(start_date), format = "%Y%m%d")
@@ -95,11 +143,11 @@ server <- function(input, output, session) {
95
  "<b>", htmltools::htmlEscape(name), "</b> (", id, ")<br>",
96
  "<span style='font-size:80%;'>State: ", htmltools::htmlEscape(state), "</span><br>",
97
  "<span style='font-size:80%; color:#555;'>",
98
- "Period: ", format(s, "%Y-%m-%d"), " &ndash; ", format(e, "%Y-%m-%d"),
99
  "</span><br>",
100
- "<div style='font-size:80%; color:#333; font-weight:bold; margin-top:5px;'>Available Parameters:</div>",
101
- "<div style='font-size:75%; color:#333; font-style:italic; line-height: 1.1;'>• ",
102
- gsub(", ", "<br>• ", htmltools::htmlEscape(available_params)),
103
  "</div>",
104
  "</div>"
105
  )
@@ -131,6 +179,19 @@ server <- function(input, output, session) {
131
  # Set ID
132
  current_station_id(id_val)
133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  # Trigger Panel
135
  output$show_details_panel <- reactive(TRUE)
136
  outputOptions(output, "show_details_panel", suspendWhenHidden = FALSE)
@@ -503,12 +564,43 @@ server <- function(input, output, session) {
503
  distinct(datetime, .keep_all = TRUE) %>%
504
  arrange(datetime)
505
 
506
- # Final Window Filter (safety)
507
  s_date <- window_debounced()$start
508
  e_date <- window_debounced()$end
 
509
  if (!is.null(s_date) && !is.null(e_date)) {
510
- clean_df <- clean_df %>%
511
- filter(datetime >= as.POSIXct(s_date), datetime <= as.POSIXct(e_date) + days(1))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  }
513
 
514
  station_data(clean_df)
 
8
 
9
  # Filtered stations based on Sidebar inputs
10
  filtered_stations <- reactive({
11
+ req(stations, input$date_range)
12
  df <- stations
13
 
 
 
 
 
 
14
  # Filter by Date (metadata availability)
15
+ # We check if the station was active during any part of the selected range
16
+ range_start <- as.numeric(format(input$date_range[1], "%Y%m%d"))
17
+ range_end <- as.numeric(format(input$date_range[2], "%Y%m%d"))
18
+
19
+ df <- df %>% filter(
20
+ as.numeric(start_date) <= range_end &
21
+ as.numeric(end_date) >= range_start
22
+ )
23
 
24
  df
25
  })
26
 
27
+ # Initialize / Update Station Selector Choices
28
  observe({
29
+ df <- filtered_stations()
30
+ req(df)
31
+
32
+ # Create choices: "Station Name (ID)" = "ID"
33
+ choices <- setNames(df$id, paste0(df$name, " (", df$id, ")"))
34
+
35
+ # Preserve selection if still in filtered list
36
+ current_sel <- input$station_selector
37
+
38
+ updateSelectizeInput(session, "station_selector",
39
+ choices = choices,
40
+ selected = current_sel,
41
+ server = TRUE
42
+ )
43
+ })
44
+
45
+ # --- Selection Logic via Dropdown ---
46
+ observeEvent(input$station_selector, {
47
+ req(input$station_selector)
48
+ id_val <- input$station_selector
49
+
50
+ # Find station details
51
+ s_meta <- stations %>% filter(id == id_val)
52
+ req(nrow(s_meta) > 0)
53
+
54
+ lat_val <- s_meta$latitude[1]
55
+ lng_val <- s_meta$longitude[1]
56
+
57
+ # Set ID
58
+ current_station_id(id_val)
59
+
60
+ # Highlight & Zoom
61
+ leafletProxy("map") %>%
62
+ setView(lng = lng_val + 0.8, lat = lat_val, zoom = 9) %>%
63
+ removeMarker(layerId = "selected-highlight") %>%
64
+ addCircleMarkers(
65
+ lng = lng_val, lat = lat_val,
66
+ layerId = "selected-highlight",
67
+ radius = 8, color = "red", fill = FALSE, opacity = 1, weight = 3
68
+ )
69
+
70
+ # Trigger Panel
71
+ output$show_details_panel <- reactive(TRUE)
72
+ outputOptions(output, "show_details_panel", suspendWhenHidden = FALSE)
73
+
74
+ # Update Dates to last 366 days of available data for this station
75
+ e_date <- tryCatch(as.Date(as.character(s_meta$end_date[1]), format = "%Y%m%d"), error = function(e) Sys.Date())
76
+ if (is.na(e_date)) e_date <- Sys.Date()
77
+ s_date <- e_date - 366
78
+ updateDateInput(session, "modal_date_start", value = s_date)
79
+ updateDateInput(session, "modal_date_end", value = e_date)
80
  })
81
 
82
  # Station Count
 
131
  # Pre-process label with HTML
132
  df <- df %>% mutate(
133
  label_html = purrr::pmap_chr(
134
+ list(name, id, state, start_date, end_date, detailed_summary),
135
+ function(name, id, state, start_date, end_date, detailed_summary) {
136
  tryCatch(
137
  {
138
  s <- as.Date(as.character(start_date), format = "%Y%m%d")
 
143
  "<b>", htmltools::htmlEscape(name), "</b> (", id, ")<br>",
144
  "<span style='font-size:80%;'>State: ", htmltools::htmlEscape(state), "</span><br>",
145
  "<span style='font-size:80%; color:#555;'>",
146
+ "Station Active: ", format(s, "%Y-%m-%d"), " &ndash; ", format(e, "%Y-%m-%d"),
147
  "</span><br>",
148
+ "<div style='font-size:80%; color:#333; font-weight:bold; margin-top:5px;'>Data Availability:</div>",
149
+ "<div style='font-size:75%; color:#333; line-height: 1.1;'>• ",
150
+ gsub(", ", "<br>• ", htmltools::htmlEscape(detailed_summary)),
151
  "</div>",
152
  "</div>"
153
  )
 
179
  # Set ID
180
  current_station_id(id_val)
181
 
182
+ # Highlight & Zoom
183
+ leafletProxy("map") %>%
184
+ setView(lng = click$lng + 0.8, lat = click$lat, zoom = 9) %>%
185
+ removeMarker(layerId = "selected-highlight") %>%
186
+ addCircleMarkers(
187
+ lng = click$lng, lat = click$lat,
188
+ layerId = "selected-highlight",
189
+ radius = 8, color = "red", fill = FALSE, opacity = 1, weight = 3
190
+ )
191
+
192
+ # Sync dropdown
193
+ updateSelectizeInput(session, "station_selector", selected = id_val)
194
+
195
  # Trigger Panel
196
  output$show_details_panel <- reactive(TRUE)
197
  outputOptions(output, "show_details_panel", suspendWhenHidden = FALSE)
 
564
  distinct(datetime, .keep_all = TRUE) %>%
565
  arrange(datetime)
566
 
567
+ # Final Window Filter (safety) with Auto-Correction
568
  s_date <- window_debounced()$start
569
  e_date <- window_debounced()$end
570
+
571
  if (!is.null(s_date) && !is.null(e_date)) {
572
+ req_start <- as.POSIXct(s_date)
573
+ req_end <- as.POSIXct(e_date) + days(1)
574
+
575
+ filtered_df <- clean_df %>%
576
+ filter(datetime >= req_start, datetime <= req_end)
577
+
578
+ # Check for mismatch: Data exists but not in window
579
+ if (nrow(filtered_df) == 0 && nrow(clean_df) > 0) {
580
+ data_max <- max(clean_df$datetime, na.rm = TRUE)
581
+ data_min <- min(clean_df$datetime, na.rm = TRUE)
582
+
583
+ # If the requested window is completely outside the data range
584
+ if (req_start > data_max || req_end < data_min) {
585
+ msg <- paste0("No data in selected range. Auto-adjusting to latest available: ", format(data_max, "%Y-%m-%d"))
586
+ loading_diagnostics(msg)
587
+
588
+ # Auto-adjust dates
589
+ # This will trigger a re-fetch via the observer
590
+ new_end <- as.Date(data_max)
591
+ new_start <- new_end - 366
592
+
593
+ updateDateInput(session, "modal_date_start", value = new_start)
594
+ # Small delay for end date to ensure sliding window logic doesn't fight us
595
+ # confusing interaction possible, but setting both usually works
596
+ updateDateInput(session, "modal_date_end", value = new_end)
597
+
598
+ # Stop this cycle
599
+ return()
600
+ }
601
+ }
602
+
603
+ clean_df <- filtered_df
604
  }
605
 
606
  station_data(clean_df)
ui.R CHANGED
@@ -86,8 +86,8 @@ ui <- page_navbar(
86
  theme = bs_theme(version = 5, bootswatch = "cosmo"),
87
  sidebar = sidebar(
88
  title = "Filters",
89
- # State Filter
90
- selectInput("state_filter", "Bundesland (State)", choices = NULL, multiple = TRUE),
91
  div(
92
  style = "display: flex; align-items: center; gap: 8px;",
93
  tags$label("Select Date Range"),
 
86
  theme = bs_theme(version = 5, bootswatch = "cosmo"),
87
  sidebar = sidebar(
88
  title = "Filters",
89
+ # Station Search
90
+ selectizeInput("station_selector", "Find Station", choices = NULL, multiple = FALSE, options = list(placeholder = "Type to search...")),
91
  div(
92
  style = "display: flex; align-items: center; gap: 8px;",
93
  tags$label("Select Date Range"),