cboettig commited on
Commit
07d31be
·
1 Parent(s): e918ccc

use URL hashes

Browse files
Files changed (4) hide show
  1. app.R +8 -15
  2. inat-ranges.R +38 -32
  3. test.R +67 -80
  4. utils.R +65 -51
app.R CHANGED
@@ -13,15 +13,16 @@ library(overture)
13
  source("utils.R")
14
  source("inat-ranges.R")
15
 
 
16
 
17
  ui <- page_sidebar(
18
  shinybusy::add_busy_spinner(),
19
 
20
  title = "iNaturalist Species Ranges",
21
  sidebar = sidebar(
22
- textInput("location", "Location", "California"),
23
- varSelectInput("rank", NULL, taxa, selected = "scientificName"),
24
- textInput("taxon", NULL, "Canis lupus"),
25
  actionButton("button", "Go")
26
  ),
27
  card(
@@ -32,24 +33,16 @@ ui <- page_sidebar(
32
 
33
  server <- function(input, output, session) {
34
  output$map <- renderMaplibre({
35
- if (file.exists(cache)) {
36
- meta <- jsonlite::read_json(cache)
37
- print(meta$url)
38
- } else {
39
- aoi <- get_division(input$location)
40
- meta <- richness(inat, aoi, rank = input$rank, taxon = input$taxon)
41
- }
42
- m <- richness_map(meta)
43
  m
44
  })
45
 
46
  observeEvent(input$button, {
47
  print(input$location)
48
  aoi <- get_division(input$location)
49
- meta <- richness(inat, aoi, rank = input$rank, taxon = input$taxon)
50
-
51
- jsonlite::write_json(meta, cache, auto_unbox = TRUE)
52
- message(paste("rendering", meta$url))
53
 
54
  session$reload()
55
  })
 
13
  source("utils.R")
14
  source("inat-ranges.R")
15
 
16
+ duckdb_config(threads = 24) # I/O limited so overclock threads
17
 
18
  ui <- page_sidebar(
19
  shinybusy::add_busy_spinner(),
20
 
21
  title = "iNaturalist Species Ranges",
22
  sidebar = sidebar(
23
+ textInput("location", "Location", "United States"),
24
+ varSelectInput("rank", NULL, taxa, selected = "class"),
25
+ textInput("taxon", NULL, "Aves"),
26
  actionButton("button", "Go")
27
  ),
28
  card(
 
33
 
34
  server <- function(input, output, session) {
35
  output$map <- renderMaplibre({
36
+ aoi <- get_division(input$location)
37
+ url <- richness(inat, aoi, rank = input$rank, taxon = input$taxon)
38
+ m <- richness_map(url, aoi)
 
 
 
 
 
39
  m
40
  })
41
 
42
  observeEvent(input$button, {
43
  print(input$location)
44
  aoi <- get_division(input$location)
45
+ richness(inat, aoi, rank = input$rank, taxon = input$taxon)
 
 
 
46
 
47
  session$reload()
48
  })
inat-ranges.R CHANGED
@@ -23,8 +23,9 @@ taxa <- duckdbfs::open_dataset(
23
  recursive = FALSE
24
  )
25
 
26
- cache <- tempfile(fileext = ".json")
27
-
 
28
 
29
  # Also requires get_h3_aoi() from utils.R
30
  richness <- function(inat, aoi, rank = NULL, taxon = NULL, zoom = 3) {
@@ -32,12 +33,25 @@ richness <- function(inat, aoi, rank = NULL, taxon = NULL, zoom = 3) {
32
  "AWS_PUBLIC_ENDPOINT",
33
  Sys.getenv("AWS_S3_ENDPOINT")
34
  )
35
- hash <- digest::digest(list(aoi, rank, taxon))
36
  s3 <- paste0("s3://public-data/cache/inat/", hash, ".h3j")
 
37
 
38
  # check if hash exists
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- # filter
41
  if (!is.null(rank) && !is.null(taxon)) {
42
  taxa <- open_dataset(
43
  glue("s3://public-inat/taxonomy/taxa.parquet"),
@@ -50,41 +64,33 @@ richness <- function(inat, aoi, rank = NULL, taxon = NULL, zoom = 3) {
50
  inner_join(inat, by = "taxon_id")
51
  }
52
 
53
- h3_aoi <- get_h3_aoi(aoi, precision = 4) |> select(h3id)
54
 
55
- clock <- bench::bench_time({
56
- inat |>
57
- rename(h3id = h4) |>
58
- inner_join(h3_aoi, by = "h3id") |>
59
- distinct(taxon_id, h3id) |>
60
- group_by(h3id) |>
61
- summarise(n = n()) |>
62
- mutate(height = n / max(n)) |>
63
- duckdbfs::to_h3j(s3)
64
- # write_dataset("s3://public-data/inat-tmp-ranges.parquet")
65
- })
66
 
67
- center <- c(st_coordinates(st_centroid(st_as_sfc(st_bbox(aoi)))))
68
- bounds <- as.vector(st_bbox(aoi))
 
 
 
 
 
69
 
70
- url <- gsub("s3://", glue("https://{public_endpoint}/"), s3)
71
-
72
- meta <- list(
73
- X = center[1],
74
- Y = center[2],
75
- zoom = zoom,
76
- url = url,
77
- time = clock[[2]],
78
- bounds = bounds
79
- )
80
- return(meta)
81
  }
82
 
83
- richness_map <- function(meta) {
 
84
  m <-
85
  maplibre() |>
86
  add_draw_control() |>
87
- add_h3j_source("h3j_source", url = meta$url) |>
88
  add_fill_extrusion_layer(
89
  id = "h3j_layer",
90
  source = "h3j_source",
@@ -101,7 +107,7 @@ richness_map <- function(meta) {
101
  ),
102
  fill_extrusion_opacity = 0.7
103
  ) |>
104
- fit_bounds(meta$bounds, animate = TRUE)
105
 
106
  return(m)
107
  }
 
23
  recursive = FALSE
24
  )
25
 
26
+ get_hash <- function(aoi, rank, taxon) {
27
+ digest::digest(list(aoi, rank, taxon))
28
+ }
29
 
30
  # Also requires get_h3_aoi() from utils.R
31
  richness <- function(inat, aoi, rank = NULL, taxon = NULL, zoom = 3) {
 
33
  "AWS_PUBLIC_ENDPOINT",
34
  Sys.getenv("AWS_S3_ENDPOINT")
35
  )
36
+ hash <- get_hash(aoi, rank, taxon)
37
  s3 <- paste0("s3://public-data/cache/inat/", hash, ".h3j")
38
+ url <- gsub("s3://", glue("https://{public_endpoint}/"), s3)
39
 
40
  # check if hash exists
41
+ cache_hit <- tryCatch(
42
+ {
43
+ duckdbfs::open_dataset(s3)
44
+ TRUE
45
+ },
46
+ error = function(e) FALSE,
47
+ finally = FALSE
48
+ )
49
+
50
+ if (cache_hit) {
51
+ return(url)
52
+ }
53
 
54
+ # Subset by taxon, if requested
55
  if (!is.null(rank) && !is.null(taxon)) {
56
  taxa <- open_dataset(
57
  glue("s3://public-inat/taxonomy/taxa.parquet"),
 
64
  inner_join(inat, by = "taxon_id")
65
  }
66
 
67
+ inat <- inat |> rename(h3id = h4)
68
 
69
+ # Subset by area, if requested
70
+ if (nrow(aoi) > 0) {
71
+ inat <-
72
+ get_h3_aoi(aoi, precision = 4) |>
73
+ select(h3id) |>
74
+ inner_join(inat, by = "h3id")
75
+ }
 
 
 
 
76
 
77
+ # Aggregate to h4 hex
78
+ inat |>
79
+ distinct(taxon_id, h3id) |>
80
+ group_by(h3id) |>
81
+ summarise(n = n()) |>
82
+ mutate(height = n / max(n)) |>
83
+ duckdbfs::to_h3j(s3)
84
 
85
+ return(url)
 
 
 
 
 
 
 
 
 
 
86
  }
87
 
88
+ richness_map <- function(url, gdf) {
89
+ bounds <- as.vector(sf::st_bbox(gdf))
90
  m <-
91
  maplibre() |>
92
  add_draw_control() |>
93
+ add_h3j_source("h3j_source", url = url) |>
94
  add_fill_extrusion_layer(
95
  id = "h3j_layer",
96
  source = "h3j_source",
 
107
  ),
108
  fill_extrusion_opacity = 0.7
109
  ) |>
110
+ fit_bounds(bounds, animate = TRUE)
111
 
112
  return(m)
113
  }
test.R CHANGED
@@ -1,4 +1,3 @@
1
-
2
  source("utils.R")
3
  source("inat-ranges.R")
4
 
@@ -9,26 +8,25 @@ url <- "https://minio.carlboettiger.info/public-data/cache/inat/f0108ef86feababf
9
 
10
  #x <- jsonlite::read_json(url)
11
 
12
- m = maplibre(center = c(-110,37), zoom=3) |>
13
- add_draw_control() |>
14
- add_h3j_source("h3j_source",
15
- url = url
16
- ) |>
17
- add_fill_extrusion_layer(
18
- id = "h3j_layer",
19
- source = "h3j_source",
20
- tooltip = "n",
21
- fill_extrusion_color = viridis_pal("height"),
22
- fill_extrusion_height = list(
23
- "interpolate",
24
- list("linear"),
25
- list("zoom"),
26
- 0,
27
- 0, 1,
28
- list("*", 100000, list("get", "height"))
29
- ),
30
- fill_extrusion_opacity = 0.7
31
- )
32
 
33
 
34
  htmlwidgets::saveWidget(m, "test2.html")
@@ -81,54 +79,44 @@ htmlwidgets::saveWidget(m, "mammals-richness.html")
81
 
82
  # mutate(h3 = h3_cell_to_parent(h4, 3L))
83
 
 
 
 
 
 
84
 
85
 
86
- m <- maplibre(center = c(-110.5, 34.8), zoom = 4) |> add_draw_control()
87
- richness_map(m, "https://minio.carlboettiger.info/public-data/inat-tmp-ranges.h3j")
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
 
98
 
99
  library(htmlwidgets)
100
  htmlwidgets::saveWidget(m, "example.html")
101
 
102
 
 
 
 
 
103
 
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
- amphib = open_dataset("s3://public-inat/polygon/Amphibia.parquet", recursive = FALSE)
114
-
115
- gdf <- amphib |>
116
- filter(name == "Ambystoma californiense") |>
117
- to_sf(crs=4326)
118
 
119
  maplibre(center = c(-122.5, 37.8), zoom = 4) |>
120
- add_source(id = "gdf", gdf) |>
121
- add_layer("gdf-layer",
122
- type = "fill",
123
- source = "gdf",
124
- paint = list(
125
- "fill-color" = "darkgreen",
126
- "fill-opacity" = .9
127
- )
128
- )
129
-
130
-
131
-
132
 
133
 
134
  # Access SVI
@@ -144,30 +132,29 @@ ca <- tracts |>
144
  mutate(h4 = tolower(as.character(h4)))
145
 
146
  out <- ca |>
147
- inner_join(inat, by = "h4") |>
148
- count(STATE, COUNTY, FIPS, h5)
149
 
150
 
151
  # mutate(height = n / max(n)) |>
152
 
153
  url = "https://minio.carlboettiger.info/public-data/cache/inat/cec4b3087f0b6c41ecc384da2521f97c.h3j"
154
- maplibre() |>
155
- add_draw_control() |>
156
- add_h3j_source("h3j_source",
157
- url = url
158
- ) |>
159
- add_fill_extrusion_layer(
160
- id = "h3j_layer",
161
- source = "h3j_source",
162
- tooltip = "n",
163
- fill_extrusion_color = viridis_pal("height"),
164
- fill_extrusion_height = list(
165
- "interpolate",
166
- list("linear"),
167
- list("zoom"),
168
- 0,
169
- 0, 1,
170
- list("*", 100000, list("get", "height"))
171
- ),
172
- fill_extrusion_opacity = 0.7
173
- )
 
 
1
  source("utils.R")
2
  source("inat-ranges.R")
3
 
 
8
 
9
  #x <- jsonlite::read_json(url)
10
 
11
+ m = maplibre(center = c(-110, 37), zoom = 3) |>
12
+ add_draw_control() |>
13
+ add_h3j_source("h3j_source", url = url) |>
14
+ add_fill_extrusion_layer(
15
+ id = "h3j_layer",
16
+ source = "h3j_source",
17
+ tooltip = "n",
18
+ fill_extrusion_color = viridis_pal("height"),
19
+ fill_extrusion_height = list(
20
+ "interpolate",
21
+ list("linear"),
22
+ list("zoom"),
23
+ 0,
24
+ 0,
25
+ 1,
26
+ list("*", 100000, list("get", "height"))
27
+ ),
28
+ fill_extrusion_opacity = 0.7
29
+ )
 
30
 
31
 
32
  htmlwidgets::saveWidget(m, "test2.html")
 
79
 
80
  # mutate(h3 = h3_cell_to_parent(h4, 3L))
81
 
82
+ m <- maplibre(center = c(-110.5, 34.8), zoom = 4) |> add_draw_control()
83
+ richness_map(
84
+ m,
85
+ "https://minio.carlboettiger.info/public-data/inat-tmp-ranges.h3j"
86
+ )
87
 
88
 
89
+ install.packages(
90
+ 'spDataLarge',
91
+ repos = 'https://nowosad.github.io/drat/',
92
+ type = 'source'
93
+ )
 
 
 
 
 
 
94
 
95
 
96
  library(htmlwidgets)
97
  htmlwidgets::saveWidget(m, "example.html")
98
 
99
 
100
+ amphib = open_dataset(
101
+ "s3://public-inat/polygon/Amphibia.parquet",
102
+ recursive = FALSE
103
+ )
104
 
105
+ gdf <- amphib |>
106
+ filter(name == "Ambystoma californiense") |>
107
+ to_sf(crs = 4326)
 
 
 
 
 
 
 
 
 
 
 
108
 
109
  maplibre(center = c(-122.5, 37.8), zoom = 4) |>
110
+ add_source(id = "gdf", gdf) |>
111
+ add_layer(
112
+ "gdf-layer",
113
+ type = "fill",
114
+ source = "gdf",
115
+ paint = list(
116
+ "fill-color" = "darkgreen",
117
+ "fill-opacity" = .9
118
+ )
119
+ )
 
 
120
 
121
 
122
  # Access SVI
 
132
  mutate(h4 = tolower(as.character(h4)))
133
 
134
  out <- ca |>
135
+ inner_join(inat, by = "h4") |>
136
+ count(STATE, COUNTY, FIPS, h5)
137
 
138
 
139
  # mutate(height = n / max(n)) |>
140
 
141
  url = "https://minio.carlboettiger.info/public-data/cache/inat/cec4b3087f0b6c41ecc384da2521f97c.h3j"
142
+ maplibre() |>
143
+ add_draw_control() |>
144
+ add_h3j_source("h3j_source", url = url) |>
145
+ add_fill_extrusion_layer(
146
+ id = "h3j_layer",
147
+ source = "h3j_source",
148
+ tooltip = "n",
149
+ fill_extrusion_color = viridis_pal("height"),
150
+ fill_extrusion_height = list(
151
+ "interpolate",
152
+ list("linear"),
153
+ list("zoom"),
154
+ 0,
155
+ 0,
156
+ 1,
157
+ list("*", 100000, list("get", "height"))
158
+ ),
159
+ fill_extrusion_opacity = 0.7
160
+ )
 
utils.R CHANGED
@@ -1,4 +1,3 @@
1
-
2
  library(dplyr)
3
  library(duckdbfs)
4
  library(sf)
@@ -6,7 +5,9 @@ library(sf)
6
  as_dataset.sf <- function(sf, ...) {
7
  # cludgy way to get polygon into duckdb as spatial data
8
  tmp <- tempfile(fileext = ".fgb")
9
- sf |> sf::st_transform(4326) |> sf::write_sf(tmp, append = FALSE, quiet = TRUE)
 
 
10
  aoi <- duckdbfs::open_dataset(tmp, ...)
11
 
12
  aoi
@@ -14,23 +15,20 @@ as_dataset.sf <- function(sf, ...) {
14
 
15
  # promote bbox to polygon
16
  as_poly <- function(aoi) {
17
-
18
  crs <- st_crs(aoi)
19
- if (crs$input != "EPSG:4326" ) {
20
  aoi <- aoi |> st_transform(4326)
21
-
22
- }
23
  if (inherits(aoi, "bbox")) {
24
- aoi <- aoi |>
25
- st_as_sfc() |>
26
- st_as_sf() |>
27
  rename(geom = x)
28
  }
29
- aoi
30
  }
31
 
32
 
33
-
34
  get_h3_aoi <- function(aoi, zoom = 0L, precision = 6L, upper = FALSE) {
35
  duckdbfs::load_h3()
36
 
@@ -39,23 +37,30 @@ get_h3_aoi <- function(aoi, zoom = 0L, precision = 6L, upper = FALSE) {
39
  precision <- as.integer(precision)
40
  res <- paste0("h", precision)
41
 
42
- if(inherits(aoi, "sf") || inherits(aoi, "bbox")) {
43
  aoi <- as_poly(aoi)
44
  aoi <- as_dataset.sf(aoi)
45
  }
46
 
47
  # multipolygon dump may not be needed for draw tools.
48
  h3_aoi <- aoi |>
49
- dplyr::mutate(poly = array_extract(unnest(st_dump(geom)),"geom"),
50
- h3id = h3_polygon_wkt_to_cells(poly,{precision}),
51
- h3id = unnest(h3id)
52
- ) |>
53
- dplyr::mutate(h0 = h3_h3_to_string( h3_cell_to_parent(h3id, {zoom})),
54
- h3id = h3_h3_to_string (h3id) )
55
-
56
- if(upper) {
57
- h3_aoi <- h3_aoi |>
58
- dplyr::mutate(h0 = toupper(h0), h3id = toupper(h3id))
 
 
 
 
 
 
 
59
  }
60
 
61
  h3_aoi |>
@@ -64,68 +69,77 @@ get_h3_aoi <- function(aoi, zoom = 0L, precision = 6L, upper = FALSE) {
64
  }
65
 
66
  hex_res <- function(x) {
67
- x |>
68
  utils::head(1) |>
69
  dplyr::mutate(res = h3_get_resolution(h3id)) |>
70
  dplyr::pull(res)
71
  }
72
 
73
- hex_join <- function(x,y) {
74
  res_x <- hex_res(x)
75
  res_y <- hex_res(y)
76
 
77
  if (res_x > res_y) {
78
- y <- y |>
79
- dplyr::mutate(h3id = unnest(h3_cell_to_children(h3id, {res_x})),
80
- h3id = toupper(h3id))
 
 
 
 
81
  }
82
- if (res_x < res_y) {
83
- y <- y |>
84
- dplyr::mutate(h3id = h3_cell_to_parent(h3id, {res_x}))
 
 
 
 
85
  }
86
 
87
  dplyr::inner_join(x, y)
88
  }
89
 
90
-
91
  antimeridian_hexes <- function(zoom = 4, con = duckdbfs::cached_connection()) {
92
  duckdbfs::load_h3(con)
93
  duckdbfs::load_spatial(con)
94
- DBI::dbExecute(con,
95
- "
 
96
  CREATE OR REPLACE TABLE antimeridian AS (
97
  SELECT ST_GeomFromText(
98
  'POLYGON ((170 85, 190 85, 190 -85, 170 -85, 170 85))'
99
  ) AS geometry
100
  )
101
- ")
 
102
  zoom <- as.integer(zoom)
103
-
104
- am <-
105
  dplyr::tbl(con, "antimeridian") |>
106
- dplyr::mutate(h3id = unnest(
107
- h3_polygon_wkt_to_cells_string(geometry, {zoom})
108
- )) |>
 
 
 
 
109
  dplyr::select(h3id)
110
 
111
- am
112
  }
113
 
114
 
115
- viridis_pal <-
116
- function(column = "height",
117
- n = 61,
118
- min_v = 0,
119
- max_v = 1) {
120
- pal <- viridisLite::viridis(n)
121
- fill_color = mapgl::step_expr(
122
  column = column,
123
  base = pal[1],
124
  stops = pal[2:n],
125
- values = seq(min_v, max_v, length.out = n-1),
126
  na_color = "white"
127
  )
128
-
129
- fill_color
130
- }
131
 
 
 
 
 
1
  library(dplyr)
2
  library(duckdbfs)
3
  library(sf)
 
5
  as_dataset.sf <- function(sf, ...) {
6
  # cludgy way to get polygon into duckdb as spatial data
7
  tmp <- tempfile(fileext = ".fgb")
8
+ sf |>
9
+ sf::st_transform(4326) |>
10
+ sf::write_sf(tmp, append = FALSE, quiet = TRUE)
11
  aoi <- duckdbfs::open_dataset(tmp, ...)
12
 
13
  aoi
 
15
 
16
  # promote bbox to polygon
17
  as_poly <- function(aoi) {
 
18
  crs <- st_crs(aoi)
19
+ if (crs$input != "EPSG:4326") {
20
  aoi <- aoi |> st_transform(4326)
21
+ }
 
22
  if (inherits(aoi, "bbox")) {
23
+ aoi <- aoi |>
24
+ st_as_sfc() |>
25
+ st_as_sf() |>
26
  rename(geom = x)
27
  }
28
+ aoi
29
  }
30
 
31
 
 
32
  get_h3_aoi <- function(aoi, zoom = 0L, precision = 6L, upper = FALSE) {
33
  duckdbfs::load_h3()
34
 
 
37
  precision <- as.integer(precision)
38
  res <- paste0("h", precision)
39
 
40
+ if (inherits(aoi, "sf") || inherits(aoi, "bbox")) {
41
  aoi <- as_poly(aoi)
42
  aoi <- as_dataset.sf(aoi)
43
  }
44
 
45
  # multipolygon dump may not be needed for draw tools.
46
  h3_aoi <- aoi |>
47
+ dplyr::mutate(
48
+ poly = array_extract(unnest(st_dump(geom)), "geom"),
49
+ h3id = h3_polygon_wkt_to_cells(poly, {
50
+ precision
51
+ }),
52
+ h3id = unnest(h3id)
53
+ ) |>
54
+ dplyr::mutate(
55
+ h0 = h3_h3_to_string(h3_cell_to_parent(h3id, {
56
+ zoom
57
+ })),
58
+ h3id = h3_h3_to_string(h3id)
59
+ )
60
+
61
+ if (upper) {
62
+ h3_aoi <- h3_aoi |>
63
+ dplyr::mutate(h0 = toupper(h0), h3id = toupper(h3id))
64
  }
65
 
66
  h3_aoi |>
 
69
  }
70
 
71
  hex_res <- function(x) {
72
+ x |>
73
  utils::head(1) |>
74
  dplyr::mutate(res = h3_get_resolution(h3id)) |>
75
  dplyr::pull(res)
76
  }
77
 
78
+ hex_join <- function(x, y) {
79
  res_x <- hex_res(x)
80
  res_y <- hex_res(y)
81
 
82
  if (res_x > res_y) {
83
+ y <- y |>
84
+ dplyr::mutate(
85
+ h3id = unnest(h3_cell_to_children(h3id, {
86
+ res_x
87
+ })),
88
+ h3id = toupper(h3id)
89
+ )
90
  }
91
+ if (res_x < res_y) {
92
+ y <- y |>
93
+ dplyr::mutate(
94
+ h3id = h3_cell_to_parent(h3id, {
95
+ res_x
96
+ })
97
+ )
98
  }
99
 
100
  dplyr::inner_join(x, y)
101
  }
102
 
 
103
  antimeridian_hexes <- function(zoom = 4, con = duckdbfs::cached_connection()) {
104
  duckdbfs::load_h3(con)
105
  duckdbfs::load_spatial(con)
106
+ DBI::dbExecute(
107
+ con,
108
+ "
109
  CREATE OR REPLACE TABLE antimeridian AS (
110
  SELECT ST_GeomFromText(
111
  'POLYGON ((170 85, 190 85, 190 -85, 170 -85, 170 85))'
112
  ) AS geometry
113
  )
114
+ "
115
+ )
116
  zoom <- as.integer(zoom)
117
+
118
+ am <-
119
  dplyr::tbl(con, "antimeridian") |>
120
+ dplyr::mutate(
121
+ h3id = unnest(
122
+ h3_polygon_wkt_to_cells_string(geometry, {
123
+ zoom
124
+ })
125
+ )
126
+ ) |>
127
  dplyr::select(h3id)
128
 
129
+ am
130
  }
131
 
132
 
133
+ viridis_pal <-
134
+ function(column = "height", n = 61, min_v = 0, max_v = 1) {
135
+ pal <- viridisLite::viridis(n)
136
+ fill_color = mapgl::step_expr(
 
 
 
137
  column = column,
138
  base = pal[1],
139
  stops = pal[2:n],
140
+ values = seq(min_v, max_v, length.out = n - 1),
141
  na_color = "white"
142
  )
 
 
 
143
 
144
+ fill_color
145
+ }