alexdum commited on
Commit
4d34c05
·
1 Parent(s): 497c982

refactor: Migrate interactive map from Leaflet to Maplibre GL JS using the mapgl package.

Browse files
Files changed (5) hide show
  1. Dockerfile +2 -2
  2. global.R +43 -3
  3. server.R +205 -70
  4. ui.R +78 -1
  5. utils/get_color_palette.R +2 -2
Dockerfile CHANGED
@@ -6,8 +6,8 @@ WORKDIR /code
6
  RUN install2.r --error \
7
  ggplot2 \
8
  shiny \
9
- leaflet \
10
- leaflet.extras \
11
  arrow \
12
  dplyr \
13
  plotly \
 
6
  RUN install2.r --error \
7
  ggplot2 \
8
  shiny \
9
+ mapgl \
10
+ sf \
11
  arrow \
12
  dplyr \
13
  plotly \
global.R CHANGED
@@ -1,8 +1,9 @@
1
  library(shiny)
2
  library(bslib)
3
  library(bsicons)
4
- library(leaflet)
5
- library(leaflet.extras)
 
6
  library(arrow)
7
  library(dplyr)
8
  library(ggplot2)
@@ -26,7 +27,46 @@ combined_data <- data %>% left_join(meta, by = "id")
26
  # Calculate the bounds of the data
27
  map_bounds <- list(
28
  lng_min = min(meta$longitude - 1, na.rm = TRUE),
29
- lng_max = max(meta$longitude + 1, na.rm = TRUE),
30
  lat_min = min(meta$latitude - 1, na.rm = TRUE),
 
31
  lat_max = max(meta$latitude + 1, na.rm = TRUE)
32
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  library(shiny)
2
  library(bslib)
3
  library(bsicons)
4
+ library(mapgl)
5
+ library(sf)
6
+ library(jsonlite)
7
  library(arrow)
8
  library(dplyr)
9
  library(ggplot2)
 
27
  # Calculate the bounds of the data
28
  map_bounds <- list(
29
  lng_min = min(meta$longitude - 1, na.rm = TRUE),
 
30
  lat_min = min(meta$latitude - 1, na.rm = TRUE),
31
+ lng_max = max(meta$longitude + 1, na.rm = TRUE),
32
  lat_max = max(meta$latitude + 1, na.rm = TRUE)
33
  )
34
+
35
+ # Define custom raster styles for OSM and Esri
36
+ osm_style <- list(
37
+ version = 8,
38
+ sources = list(
39
+ osm = list(
40
+ type = "raster",
41
+ tiles = list("https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"),
42
+ tileSize = 256,
43
+ attribution = "&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"
44
+ )
45
+ ),
46
+ layers = list(
47
+ list(
48
+ id = "osm",
49
+ type = "raster",
50
+ source = "osm"
51
+ )
52
+ )
53
+ )
54
+
55
+ esri_style <- list(
56
+ version = 8,
57
+ sources = list(
58
+ esri = list(
59
+ type = "raster",
60
+ tiles = list("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"),
61
+ tileSize = 256,
62
+ attribution = "Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community"
63
+ )
64
+ ),
65
+ layers = list(
66
+ list(
67
+ id = "esri",
68
+ type = "raster",
69
+ source = "esri"
70
+ )
71
+ )
72
+ )
server.R CHANGED
@@ -8,26 +8,45 @@ server <- function(input, output, session) {
8
  # Initialize a reactive value to store the clicked or selected station ID
9
  selected_station_id <- reactiveVal(NULL)
10
 
 
 
 
11
  # Update the selected station ID based on the dropdown selection
12
  observeEvent(input$stationSelect, {
 
 
13
  selected_station <- meta %>%
14
  filter(name == input$stationSelect) %>%
15
  pull(id)
16
 
 
 
17
  selected_station_id(selected_station)
18
  })
19
 
20
  # Listen for click events on the map markers
21
- observeEvent(input$map_marker_click, {
22
- clicked_station <- input$map_marker_click$id
23
-
24
- if (!is.null(clicked_station)) {
25
- # Also update the dropdown to reflect the selected station
26
- selected_station_name <- meta %>%
27
- filter(id == clicked_station) %>%
28
- pull(name)
29
-
30
- updateSelectInput(session, "stationSelect", selected = selected_station_name)
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
  })
33
 
@@ -187,81 +206,197 @@ server <- function(input, output, session) {
187
  }
188
  )
189
 
190
- output$map <- renderLeaflet({
191
- center_lat <- 44
192
- center_lng <- 25
193
-
194
- leaflet(options = leafletOptions(minZoom = 6, maxZoom = 18)) %>%
195
- addTiles(group = "OpenStreetMap") %>% # Default OpenStreetMap
196
- addProviderTiles(providers$Esri.WorldTopoMap, group = "Esri World Topo Map") %>%
197
- addProviderTiles(providers$Esri.WorldImagery, group = "Esri World Imagery") %>%
198
- fitBounds(
199
- lng1 = map_bounds$lng_min, lat1 = map_bounds$lat_min,
200
- lng2 = map_bounds$lng_max, lat2 = map_bounds$lat_max
201
- ) %>%
202
- setMaxBounds(
203
- lng1 = map_bounds$lng_min, lat1 = map_bounds$lat_min,
204
- lng2 = map_bounds$lng_max, lat2 = map_bounds$lat_max
205
- ) %>%
206
- addLayersControl(
207
- baseGroups = c("Esri World Topo Map", "OpenStreetMap", "Esri World Imagery"),
208
- options = layersControlOptions(collapsed = T)
209
- ) %>%
210
- addResetMapButton()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  })
212
 
213
  # Observe changes and update markers accordingly
 
214
  observe({
215
  req(filtered_data()) # Ensure that filtered_data is available
216
 
 
 
 
 
217
  # Retrieve the current selected station ID
218
  selected_id <- selected_station_id()
219
 
 
 
 
 
 
 
220
  # Define color palettes
221
- color_pal <- get_color_palette(input$variable, domain = filtered_data()$multi_annual_value, reverse = FALSE)
222
- color_pal2 <- get_color_palette(input$variable, domain = filtered_data()$multi_annual_value, reverse = TRUE)
223
-
224
- # Update the markers on the map
225
- leafletProxy("map", data = filtered_data()) %>%
226
- clearMarkers() %>%
227
- addCircleMarkers(
228
- lng = ~longitude,
229
- lat = ~latitude,
230
- label = ~ paste0(
 
 
231
  "<strong>Name: </strong>", name,
232
  "<br><strong>", input$variable, ": </strong>", round(multi_annual_value, 1),
233
  "<br><span style='color:red;'>click to update</span>"
234
- ) %>% lapply(htmltools::HTML), # Ensure HTML format for label
235
- radius = ~ ifelse(id == selected_id, 8, 5), # Larger radius for selected station
236
- # color = ~ifelse(id == selected_id, "#808080", color_pal2(multi_annual_value)), # Different color for selected
237
- # Border color (stroke) changes when selected, otherwise it's transparent or a default value
238
- color = ~ ifelse(id == selected_id, "#FF0000", "#00000000"), # Red border for selected, transparent for others
239
-
240
- # Inside color stays the same for all, based on the color palette
241
- fillColor = ~ color_pal2(multi_annual_value),
242
- stroke = ~ ifelse(id == selected_id, TRUE, FALSE), # Add a border stroke for selected
243
- weight = ~ ifelse(id == selected_id, 2, 1), # Thicker border for selected
244
- fillOpacity = 0.9, # Increased opacity for better visibility
245
- layerId = ~id # Ensure layerId is set for interactivity
246
  ) %>%
247
- # addLabelOnlyMarkers(
248
- # lng = ~longitude[selected_id == id],
249
- # lat = ~latitude[selected_id == id],
250
- # label = ~paste0(
251
- # "<strong>Name: </strong>", name[selected_id == id],
252
- # "<br><strong>", input$variable, ": </strong>", round(multi_annual_value[selected_id == id], 1)
253
- # ) %>% lapply(htmltools::HTML), # Ensure HTML format for label
254
- # labelOptions = labelOptions(noHide = TRUE, direction = 'auto')
255
- # ) %>%
256
- clearControls() %>%
257
- addLegend(
258
- "bottomright",
259
- pal = color_pal,
260
- values = ~multi_annual_value,
261
- title = input$variable,
262
- opacity = 0.7,
263
- labFormat = labelFormat(transform = function(x) sort(x, decreasing = TRUE))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  )
 
 
 
 
 
 
 
 
 
 
 
 
265
  })
266
 
267
  # Render the plot title dynamically
 
8
  # Initialize a reactive value to store the clicked or selected station ID
9
  selected_station_id <- reactiveVal(NULL)
10
 
11
+ # Reactive value to trigger station layer refresh after style change
12
+ style_change_trigger <- reactiveVal(0)
13
+
14
  # Update the selected station ID based on the dropdown selection
15
  observeEvent(input$stationSelect, {
16
+ print(paste("Dropdown Change - New Selection:", input$stationSelect))
17
+
18
  selected_station <- meta %>%
19
  filter(name == input$stationSelect) %>%
20
  pull(id)
21
 
22
+ print(paste("Dropdown Change - Resolved ID:", selected_station))
23
+
24
  selected_station_id(selected_station)
25
  })
26
 
27
  # Listen for click events on the map markers
28
+ observeEvent(input$map_feature_click, {
29
+ clicked_data <- input$map_feature_click
30
+ print("Feature Click Event:")
31
+ print(str(clicked_data))
32
+
33
+ # Check if the click was on the "stations" layer
34
+ # Use isTRUE to handle NULLs safely, and check for "layer" or "layer_id"
35
+ if (!is.null(clicked_data) && (isTRUE(clicked_data$layer_id == "stations") || isTRUE(clicked_data$layer == "stations"))) {
36
+ # The ID is in the properties
37
+ clicked_station <- clicked_data$properties$id
38
+
39
+ if (!is.null(clicked_station)) {
40
+ # Also update the dropdown to reflect the selected station
41
+ selected_station_name <- meta %>%
42
+ filter(id == clicked_station) %>%
43
+ pull(name)
44
+
45
+ print(paste("Map Click - Station ID:", clicked_station))
46
+ print(paste("Map Click - Found Name:", selected_station_name))
47
+
48
+ updateSelectInput(session, "stationSelect", selected = selected_station_name)
49
+ }
50
  }
51
  })
52
 
 
206
  }
207
  )
208
 
209
+ output$map <- renderMaplibre({
210
+ print("DEBUG: renderMaplibre called - Map is initializing/re-rendering")
211
+ maplibre(
212
+ style = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json", # Base style (Positron Vector)
213
+ center = c(25, 44),
214
+ zoom = 6
215
+ ) %>%
216
+ add_navigation_control(show_compass = FALSE, visualize_pitch = FALSE)
217
+ })
218
+
219
+
220
+ # Reactive value to trigger style updates
221
+ style_change_trigger <- reactiveVal(0)
222
+
223
+ # Flag to track if map has been initialized
224
+ map_initialized <- reactiveVal(FALSE)
225
+
226
+ # Initialize map bounds only once
227
+ observe({
228
+ req(!map_initialized())
229
+ # Wait for map to be ready (zoom is reported)
230
+ req(input$map_zoom)
231
+
232
+ maplibre_proxy("map") %>%
233
+ fit_bounds(
234
+ c(map_bounds$lng_min, map_bounds$lat_min, map_bounds$lng_max, map_bounds$lat_max)
235
+ )
236
+
237
+ map_initialized(TRUE)
238
+ })
239
+
240
+ # Home Zoom Button Handler
241
+ observeEvent(input$home_zoom, {
242
+ req(map_bounds) # Ensure bounds are available
243
+ maplibre_proxy("map") %>%
244
+ fit_bounds(
245
+ c(map_bounds$lng_min, map_bounds$lat_min, map_bounds$lng_max, map_bounds$lat_max),
246
+ animate = TRUE
247
+ )
248
  })
249
 
250
  # Observe changes and update markers accordingly
251
+ # Also depends on input$basemap so stations are re-added after style change
252
  observe({
253
  req(filtered_data()) # Ensure that filtered_data is available
254
 
255
+ # Add dependency on style_change_trigger to re-add layer after style change
256
+ style_change_trigger()
257
+
258
+
259
  # Retrieve the current selected station ID
260
  selected_id <- selected_station_id()
261
 
262
+ # Prepare data for mapgl
263
+ # We need to add styling properties directly to the data frame for data-driven styling if strict interpolations are hard
264
+ # For mapgl, we can use expressions, but computing colors in R is often simpler for dynamic palettes
265
+
266
+ map_data <- filtered_data()
267
+
268
  # Define color palettes
269
+ color_pal2 <- get_color_palette(input$variable, domain = map_data$multi_annual_value, reverse = TRUE)
270
+
271
+ # Add styling columns
272
+ map_data <- map_data %>%
273
+ mutate(
274
+ circle_color = color_pal2(multi_annual_value),
275
+ circle_radius = ifelse(id == selected_id, 8, 5),
276
+ circle_stroke_color = ifelse(id == selected_id, "#FF0000", "#00000000"),
277
+ circle_stroke_width = ifelse(id == selected_id, 2, 1),
278
+ # Sort to ensure selected is on top (if needed, though circle_sort_key might be used if fully supported, otherwise robust ordering in data usually works)
279
+ is_selected = ifelse(id == selected_id, 1, 0),
280
+ popup_content = paste0(
281
  "<strong>Name: </strong>", name,
282
  "<br><strong>", input$variable, ": </strong>", round(multi_annual_value, 1),
283
  "<br><span style='color:red;'>click to update</span>"
284
+ )
 
 
 
 
 
 
 
 
 
 
 
285
  ) %>%
286
+ arrange(is_selected) %>%
287
+ st_as_sf(coords = c("longitude", "latitude"), crs = 4326)
288
+
289
+ # Apply to map
290
+ maplibre_proxy("map") %>%
291
+ clear_layer("stations") %>% # Remove existing layer if any
292
+ add_circle_layer(
293
+ id = "stations",
294
+ source = map_data,
295
+ circle_color = get_column("circle_color"),
296
+ circle_radius = get_column("circle_radius"),
297
+ circle_stroke_color = get_column("circle_stroke_color"),
298
+ circle_stroke_width = get_column("circle_stroke_width"),
299
+ circle_opacity = 0.9,
300
+ # Create a tooltip for hover
301
+ tooltip = get_column("popup_content")
302
+ )
303
+ })
304
+
305
+ # Track the current active Esri layer ID
306
+ current_esri_layer_id <- reactiveVal(NULL)
307
+
308
+ # Observe changes in the basemap selection and update the map style
309
+ observeEvent(input$basemap, {
310
+ print(paste("Basemap Change - Selection:", input$basemap))
311
+
312
+ proxy <- maplibre_proxy("map")
313
+
314
+ # Clean up any existing Esri layer if it exists
315
+ active_esri_id <- current_esri_layer_id()
316
+ if (!is.null(active_esri_id)) {
317
+ print(paste("Cleaning up existing Esri layer:", active_esri_id))
318
+ # Attempt to remove the layer. Note: The source might linger but won't be visible.
319
+ # mapgl doesn't have a direct 'remove_source', but clearing the layer is key.
320
+ tryCatch(
321
+ {
322
+ proxy %>% clear_layer(active_esri_id)
323
+ },
324
+ error = function(e) {
325
+ print(paste("Error clearing Esri layer:", e$message))
326
+ }
327
+ )
328
+ current_esri_layer_id(NULL)
329
+ }
330
+
331
+ if (input$basemap == "esri_imagery") {
332
+ # For Esri, set a blank style first, then add raster layer dynamically
333
+ blank_style <- list(
334
+ version = 8,
335
+ sources = list(),
336
+ layers = list()
337
+ )
338
+ json_blank <- jsonlite::toJSON(blank_style, auto_unbox = TRUE)
339
+ blank_uri <- paste0("data:application/json,", URLencode(as.character(json_blank), reserved = TRUE))
340
+
341
+ proxy %>%
342
+ set_style(blank_uri)
343
+
344
+ # Capture session for later callback
345
+ session <- shiny::getDefaultReactiveDomain()
346
+
347
+ # Add Esri raster layer after style loads
348
+ later::later(function() {
349
+ shiny::withReactiveDomain(session, {
350
+ # RACE CONDITION CHECK: Ensure basemap is STILL Esri
351
+ if (isolate(input$basemap) != "esri_imagery") {
352
+ print("Basemap changed during delay - aborting Esri load")
353
+ return()
354
+ }
355
+
356
+ # Use unique IDs to avoid "Source already exists" race conditions
357
+ unique_suffix <- as.numeric(Sys.time()) * 1000
358
+ source_id <- paste0("esri_imagery_source_", unique_suffix)
359
+ layer_id <- paste0("esri_imagery_", unique_suffix)
360
+
361
+ # Store the ID so we can remove it later
362
+ current_esri_layer_id(layer_id)
363
+
364
+ maplibre_proxy("map") %>%
365
+ add_raster_source(
366
+ id = source_id,
367
+ tiles = c("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"),
368
+ tileSize = 256
369
+ ) %>%
370
+ add_layer(
371
+ id = layer_id,
372
+ type = "raster",
373
+ source = source_id,
374
+ paint = list("raster-opacity" = 1)
375
+ )
376
+
377
+ # Trigger station re-render
378
+ style_change_trigger(isolate(style_change_trigger()) + 1)
379
+ })
380
+ }, delay = 0.5)
381
+ } else {
382
+ # Use Carto Vector Style URLs
383
+ new_style <- switch(input$basemap,
384
+ "carto_positron" = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
385
+ "carto_dark_matter" = "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
386
+ "carto_voyager" = "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
387
  )
388
+
389
+ if (!is.null(new_style)) {
390
+ print("Setting new style...")
391
+ proxy %>%
392
+ set_style(new_style)
393
+
394
+ # Trigger re-render of stations after a short delay
395
+ # later::later(function() {
396
+ # style_change_trigger(isolate(style_change_trigger()) + 1)
397
+ # }, delay = 1)
398
+ }
399
+ }
400
  })
401
 
402
  # Render the plot title dynamically
ui.R CHANGED
@@ -17,6 +17,49 @@ page_navbar(
17
  }
18
  });
19
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  "))
21
  ),
22
  fillable_mobile = T,
@@ -65,6 +108,7 @@ page_navbar(
65
  selected = "Monthly"
66
  ),
67
 
 
68
  # Select input for the month (only shown for monthly aggregation)
69
  conditionalPanel(
70
  condition = "input.aggregation == 'Monthly'",
@@ -94,7 +138,40 @@ page_navbar(
94
  card(
95
  full_screen = TRUE,
96
  card_header(h6(textOutput("map_title"))),
97
- leafletOutput("map", height = "300px"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  div(
99
  class = "d-flex justify-content-between align-items-center gap-2 flex-wrap",
100
  h6(textOutput("plot_title"), class = "mb-0"),
 
17
  }
18
  });
19
  });
20
+ ")),
21
+ tags$style(HTML("
22
+ /* Collapsible Map Control Styling */
23
+ .map-layer-control {
24
+ background-color: white;
25
+ border-radius: 4px;
26
+ box-shadow: 0 0 5px rgba(0,0,0,0.3);
27
+ padding: 5px;
28
+ width: 36px;
29
+ height: 36px;
30
+ overflow: hidden;
31
+ transition: width 0.3s ease, height 0.3s ease;
32
+ cursor: pointer;
33
+ display: flex;
34
+ flex-direction: column;
35
+ }
36
+ .map-layer-control:hover {
37
+ width: 200px;
38
+ height: auto;
39
+ padding: 10px;
40
+ }
41
+ .control-icon {
42
+ width: 26px;
43
+ height: 26px;
44
+ text-align: center;
45
+ margin-bottom: 5px;
46
+ font-size: 18px;
47
+ color: #333;
48
+ flex-shrink: 0;
49
+ }
50
+ .control-content {
51
+ opacity: 0;
52
+ transition: opacity 0.3s ease;
53
+ margin-top: 5px;
54
+ white-space: nowrap; /* Prevent wrapping during transition */
55
+ }
56
+ .map-layer-control:hover .control-content {
57
+ opacity: 1;
58
+ white-space: normal;
59
+ }
60
+ .map-layer-control:hover .control-icon {
61
+ display: none !important;
62
+ }
63
  "))
64
  ),
65
  fillable_mobile = T,
 
108
  selected = "Monthly"
109
  ),
110
 
111
+
112
  # Select input for the month (only shown for monthly aggregation)
113
  conditionalPanel(
114
  condition = "input.aggregation == 'Monthly'",
 
138
  card(
139
  full_screen = TRUE,
140
  card_header(h6(textOutput("map_title"))),
141
+ div(
142
+ style = "position: relative;",
143
+ maplibreOutput("map", height = "400px"),
144
+ absolutePanel(
145
+ top = 130, right = 10,
146
+ class = "map-layer-control",
147
+ style = "z-index: 1000;",
148
+ div(class = "control-icon", icon("layer-group")),
149
+ div(
150
+ class = "control-content",
151
+ radioButtons(
152
+ inputId = "basemap",
153
+ label = "Basemap",
154
+ choices = c(
155
+ "Carto Positron" = "carto_positron",
156
+ "Carto Dark Matter" = "carto_dark_matter",
157
+ "Carto Voyager" = "carto_voyager",
158
+ "Esri Imagery" = "esri_imagery"
159
+ ),
160
+ selected = "carto_positron"
161
+ )
162
+ )
163
+ ),
164
+ absolutePanel(
165
+ top = 80, right = 10,
166
+ style = "z-index: 1000;",
167
+ actionButton(
168
+ inputId = "home_zoom",
169
+ label = NULL,
170
+ icon = icon("house"),
171
+ style = "background-color: white; border: none; border-radius: 4px; box-shadow: 0 0 5px rgba(0,0,0,0.3); width: 36px; height: 36px; padding: 0; color: #333;"
172
+ )
173
+ )
174
+ ),
175
  div(
176
  class = "d-flex justify-content-between align-items-center gap-2 flex-wrap",
177
  h6(textOutput("plot_title"), class = "mb-0"),
utils/get_color_palette.R CHANGED
@@ -2,9 +2,9 @@
2
  get_color_palette <- function(variable, domain, reverse = FALSE) {
3
  if (variable %in% c("Tavg", "Tmax", "Tmin")) {
4
  # Use the RdYlBu palette for temperature variables
5
- colorNumeric(palette = "RdYlBu", domain = domain, reverse = reverse)
6
  } else {
7
  # Use the viridis palette for other variables like PREC
8
- colorNumeric(palette = "viridis", domain = domain, reverse = reverse)
9
  }
10
  }
 
2
  get_color_palette <- function(variable, domain, reverse = FALSE) {
3
  if (variable %in% c("Tavg", "Tmax", "Tmin")) {
4
  # Use the RdYlBu palette for temperature variables
5
+ scales::col_numeric(palette = "RdYlBu", domain = domain, reverse = reverse)
6
  } else {
7
  # Use the viridis palette for other variables like PREC
8
+ scales::col_numeric(palette = "viridis", domain = domain, reverse = reverse)
9
  }
10
  }