qfes / rdeck Goto Github PK
View Code? Open in Web Editor NEWDeck.gl widget for R
Home Page: https://qfes.github.io/rdeck
License: MIT License
Deck.gl widget for R
Home Page: https://qfes.github.io/rdeck
License: MIT License
Browsers limit the number of active webgl contexts on a page; for chrome it's 16. Each rdeck map uses 2 webgl contexts (if using a basemap); this limits us to 8 rdeck maps per html report, which is a lot, but this is occasionally a barrier.
It is unlikely that more than 8 maps could be visible at one time on a html report, so this limit is only imposed due to eagerly loading rdeck widgets. If maps could be optionally rendered when they're scrolled into view, we can effectively have no limit to the number of maps we can add to a report.
Lazy loading of maps would result in some rendering lag when they become visible, because they're being re-initialised. However, a report with lazily loaded maps that are initially not visible should render faster than an identical report with eagerly loaded maps.
library(rdeck)library(sf)
#> Linking to GEOS 3.10.1, GDAL 3.4.0, PROJ 8.2.0; sf_use_s2() is TRUE
reprex_data <-structure(list(state_hazmat_mobilised = 0L, geometry = structure(list( structure(c(153.408180931689, -27.9478084866912), class = c("XY",
"POINT", "sfg"))), class = c("sfc_POINT", "sfc"), precision = 0, bbox = structure(c(xmin = 153.408180931689, ymin = -27.9478084866912, xmax = 153.408180931689, ymax = -27.9478084866912 ), class = "bbox"), crs = structure(list(input = "EPSG:4326", wkt = "GEOGCRS[\"WGS 84\",\n ENSEMBLE[\"World Geodetic System 1984 ensemble\",\n MEMBER[\"World Geodetic System 1984 (Transit)\"],\n MEMBER[\"World Geodetic System 1984 (G730)\"],\n MEMBER[\"World Geodetic System 1984 (G873)\"],\n MEMBER[\"World Geodetic System 1984 (G1150)\"],\n MEMBER[\"World Geodetic System 1984 (G1674)\"],\n MEMBER[\"World Geodetic System 1984 (G1762)\"],\n MEMBER[\"World Geodetic System 1984 (G2139)\"],\n ELLIPSOID[\"WGS 84\",6378137,298.257223563,\n LENGTHUNIT[\"metre\",1]],\n ENSEMBLEACCURACY[2.0]],\n PRIMEM[\"Greenwich\",0,\n ANGLEUNIT[\"degree\",0.0174532925199433]],\n CS[ellipsoidal,2],\n AXIS[\"geodetic latitude (Lat)\",north,\n ORDER[1],\n ANGLEUNIT[\"degree\",0.0174532925199433]],\n AXIS[\"geodetic longitude (Lon)\",east,\n ORDER[2],\n ANGLEUNIT[\"degree\",0.0174532925199433]],\n USAGE[\n SCOPE[\"Horizontal component of 3D system.\"],\n AREA[\"World.\"],\n BBOX[-90,-180,90,180]],\n ID[\"EPSG\",4326]]"), class = "crs"), n_empty = 0L)), row.names = c(NA,
-1L), sf_column = "geometry", agr = structure(c(state_hazmat_mobilised = NA_integer_), .Label = c("constant",
"aggregate", "identity"), class = "factor"), class = c("sf",
"tbl_df", "tbl", "data.frame"))
rdeck() |>
add_scatterplot_layer(
data = reprex_data,
get_fill_color = scale_color_category(
state_hazmat_mobilised
)
)
#> Error in `add_scatterplot_layer()`:
#> ! Failed to create layer
#> Caused by error:#> ! Assertion failed: is_discrete(x)
#> • Continuous value supplied to discrete scale
Created on 2022-04-13 by the reprex package (v2.0.1)
Slightly surprising since integer is 'discrete'.
Hidden layers (i.e. layer$visible = FALSE
) are present on the legend, if those layers have a scale
with legend = TRUE
. Layers that are not visible should not be present on the legend.
Add scaling support to *_weight
, *_width
, _height
and *_size
accessors.
Layer serialisation is exporting sf objects sf_column
even when unused, due to sf's sticky geometry "feature"
Scales throw an error computing the domain, when data has no rows
Warning in min(x) : no non-missing arguments to min; returning Inf
Warning in max(x) : no non-missing arguments to max; returning -Inf
Error in seq.default: 'from' must be a finite number
h3_hexagon_layer
, h3_cluster_layer
and s2_layer
fail if get_polygon
refers a column that doesn't exist. Either remove the prop from these layers, or make it default NULL.
Error in make_accessor(rlang::enquo(get_polygon), data, TRUE) :
rlang::is_empty(data) || rlang::has_name(data, name) is not TRUE
Accessors interpret non-symbol expressions as values, not columns. The escape hatch is to convert the expression to a name, which isn't obvious.
Related #54
It looks like transparent_color is not respected when the image is passed as an array to add_bitmap_layer().
Here's a toy example, where I'd like the black background of the image to be transparent.
library(rdeck)
library(raster)
## Some bounds around New Zealand
bounds <- c(xmin = 165.88693, ymin=-52.57695, xmax=183.88419, ymax=-29.22611)
## Get an image of NZ
download.file('https://upload.wikimedia.org/wikipedia/commons/thumb/e/eb/Map_of_New_Zealand_%28blank%29.svg/1200px-Map_of_New_Zealand_%28blank%29.svg.png', 'image.png')
nz <- raster('image.png')
nz[] <- nz[] / 255
nz[nz[] < 0.8] <- 0
nz[] <- round(nz[]) # ensure color values are either 0 or 1
## Deck
deck <- rdeck(map_style = 'mapbox://styles/mapbox/dark-v9', initial_bounds = st_bbox(bounds, crs=4326)) %>%
add_bitmap_layer(
name = 'NZ'
, bounds = bounds
, image = as.array(nz)
, color_format = 'RGB'
, transparent_color = '#000000'
)
deck
Hi Anthony,
I was trying to use your rdeck for doing some plotting work. It offers extra functionality compared to mapdeck, which would be very convenient for me (tooltips, layer customization, etc.). However, there seems to be an error in how add_hexagon_layer() function operates with weighting variables. Could you please look into it?
I provide a reproducible example below. Data is taken from COVID19 package.
library(tidyverse)
library(COVID19)
library(paletteer)
library(rdeck)
library(sf)
df_2 <- covid19("Germany", level = 3, start=ymd(20201231), end=ymd(20201231)) %>%
filter(!is.na(latitude), !is.na(latitude)) %>%
mutate(position = sfc_point(longitude, latitude))
# Hexagon layer with weighting variable doesn't work
(hexagon_1 <-
rdeck(map_style = mapbox_light(),
theme='light',
initial_bounds = st_bbox(df_2$position),
initial_view_state = view_state(pitch=45)
) %>%
add_hexagon_layer(data = df_2,
get_color_weight = deaths,
get_elevation_weight = deaths,
position_format = "XY",
radius = 10000,
color_range = paletteer_d("RColorBrewer::YlOrBr", 6),
pickable = TRUE,
extruded = TRUE,
tooltip = c(administrative_area_level_3))
)
# Hexagon layer without weighting variable works just fine
(hexagon_2 <-
rdeck(map_style = mapbox_light(),
theme='light',
initial_bounds = st_bbox(df_2$position),
initial_view_state = view_state(pitch=45)
) %>%
add_hexagon_layer(data = df_2,
# get_color_weight = deaths,
# get_elevation_weight = deaths,
position_format = "XY",
radius = 10000,
color_range = paletteer_d("RColorBrewer::YlOrBr", 6),
pickable = TRUE,
extruded = TRUE,
tooltip = c(administrative_area_level_3))
)
# For comparison, heatmap layer with weighting variable works just fine
(heatmap <-
rdeck(map_style = mapbox_light(),
theme='light',
initial_bounds = st_bbox(df_2$position)) %>%
add_heatmap_layer(data = df_2,
get_weight = deaths,
opacity = 0.3,
pickable = TRUE,
color_range = paletteer_d("RColorBrewer::YlOrBr", 6))
)
Useful where you have a column that you know is already transformed and you want to include that in the legend generated for the scale.
n_incidents, # already transformed
transform_txt = "sqrt(n_incidents)"
)
The legend then says:
Demand Heatmap
colored by sqrt(incidents)
Allow for controlling layer visibility through the user interface, via a collapsible layer toggle.
Allowing for programmatically toggling a layer's visibility is achievable, but potentially annoying to deal with in shiny. Layer visibility changes would need to be handled in shiny and merged into layers that are updated later.
Rdeck layers aren't intended to exist as mutable objects currently, so maintaining a layer's state currently would require some external state object that is referenced when re-rendering layers to the client; this is a little weird.
Simpler, at least initially, is to set each layer's visibility from the the visible
prop on initial render (the current behaviour), but is thereafter not-controllable from R (the visibility prop would be ignored).
For layers with a single geometry accessor, if data is an sf
object, the geometry accessor is unconditionally set to attr(data, "sf_column")
. This can result in confusing behaviour (or an error) when the active sf column is not the column that was intended to be used in a layer.
2 possible fixes:
sf_column()
utility to resolve the sf column from an sf objectNothing showing in the JS console, but nothing showing on the map. The map scrolling is a bit chunky so I feel like it thinks it is showing me something.
Reprex:
library(nycflights13)
library(dplyr)
library(rdeck)
library(sf)
library(viridis)
flights_lon_lat <-
flights %>%
left_join(airports,
by = c("origin" = "faa"),
) %>%
left_join(airports,
by = c("dest" = "faa"),
suffix = c("_origin", "_dest")
) %>%
st_as_sf(coords = c("lon_origin", "lat_origin"), crs = 4326) %>%
mutate(origin_position = geometry) %>%
st_as_sf(coords = c("lon_dest", "lat_dest"), crs = 4326) %>%
mutate(dest_position = geometry)
rdeck() %>%
add_arc_layer(
data = flights_lon_lat,
get_source_position = origin_position,
get_target_position = dest_position,
get_source_color = scale_color_linear(
distance,
viridis(12)
),
get_target_color = scale_color_linear(
distance,
viridis(12)
),
get_height = scale_linear(
distance
),
get_width = 10,
great_circle = TRUE,
blending_mode = "additive"
)
A layer's visibility cannot be changed if the following conditions are met:
rdeck_proxy
layer_selector = TRUE
and layer visibility_toggle = TRUE
https://github.com/anthonynorth/rdeck/blob/master/widget/widget.tsx#L49-L50
This constraint exists because add_x_layer() updates all of a layer's props, but the layer's visibility cannot (currently) be known in shiny. Updating a layer's visibility every time a reactive changes would be an irritating user experience.
Proposed change: visible = NULL
to use the state in the browser, TRUE / FALSE to change it.
Example:
observe({
rdeck_proxy("map") |>
add_h3_hexagon_layer(
id = "layer_id",
data = some_new_data(),
# client = truth
visible = NULL,
)
})
Add support for client-side data filtering, using DataFilter layer extension, allowing for performant filtering of layer data already available on the client.
Each filter should support multiple layers and multiple fields per layer; example: a single date-range filter can be applied to many layers with each layer specifying one or more fields.
Limitations:
Possible api
add_range_filter <- function(rdeck, name, min, max, ...) {}
add_set_filter <- function(rdeck, name, values, ...) {}
add_value_filter <- function(rdeck, name, value, ...) {}
rdeck(...) |>
# layer-1 filtered by the foo column, layer-2 filtered by the bar column
add_range_filter(name = "range filter", min = 0, max = 1, "layer-1" = foo, "layer-2" = bar) |>
add_scatterplot_layer(id = "layer-1", ...) |>
add_polygon_layer(id = "layer-2", ...)
Trying to get the Manhattn example from the documentation to work. The first hurdle is that: tooltip(gender, geometry)
seems not to work. It can't find "geometry". I've put it in quotes, just to get the map to build. When it does build, it displays, but if I attempt to interact with the map it crashes (white screen).
I see in the console:
vendor.js:16 This page appears to be missing CSS declarations for Mapbox GL JS, which may cause the map to display incorrectly. Please ensure your page includes mapbox-gl.css, as described in https://www.mapbox.com/mapbox-gl-js/api/.
b @ vendor.js:16
vendor.js:32 TypeError: Cannot read property '13432' of undefined
at d (rdeck.js:1)
at rdeck.js:1
at Array.map (<anonymous>)
at rdeck.js:1
at Qo (vendor.js:32)
at Ra (vendor.js:32)
at Da (vendor.js:32)
at gs (vendor.js:32)
at ul (vendor.js:32)
at sl (vendor.js:32)
ts @ vendor.js:32
vendor.js:32 Uncaught TypeError: Cannot read property '13432' of undefined
at d (rdeck.js:1)
at rdeck.js:1
at Array.map (<anonymous>)
at rdeck.js:1
at Qo (vendor.js:32)
at Ra (vendor.js:32)
at Da (vendor.js:32)
at gs (vendor.js:32)
at ul (vendor.js:32)
at sl (vendor.js:32)
code:
library(sf)
library(jsonlite)
library(rdeck)
scatterplot_data <- read_json(
"https://raw.githubusercontent.com/uber-common/deck.gl-data/master/examples/scatterplot/manhattan.json"
) %>%
lapply(unlist) %>%
do.call(rbind, .) %>%
as_tibble() %>%
setNames(c("lon", "lat", "gender")) %>%
st_as_sf(coords = c("lon", "lat"), crs = 4326)
rdeck(
picking_radius = 5,
initial_view_state = view_state(
center = c(-74, 40.76),
zoom = 11
)
) %>%
add_scatterplot_layer(
data = scatterplot_data,
radius_scale = 10,
radius_min_pixels = 0.5,
get_fill_color = ~ gender == 1 ? c(0, 128, 255):c(255, 0, 128),
pickable = TRUE,
tooltip = "tooltip(gender, geometry)"
)
Other widgets that have a common javascript dependency (defined in the dependencies yaml, or htmltools::htmlDependency
) fail to load in a document where an rdeck map is present.
Map fails to render if a layer's data
contains empty geometries.
Repex:
library(rdeck)
rdeck(
map_style = mapbox_dark()
) %>%
add_polygon_layer(
data = absmapsdata::lga2021,
)
Datasets containing empty geometries
For example, a purplish hex here has tooltip 3.18 which should be yellow, by the legend.
This is based on your radio tower map code.
add_h3_hexagon_layer(
data = hex_data,
id = "demand",
line_width_units = "pixels",
get_line_width = 0.05,
opacity = 0.5,
get_fill_color = scale_color_power(
vehicle_hours,
palette = plasma(6),
exponent = 0.11111111
),
extruded = FALSE,
pickable = TRUE,
tooltip = TRUE
)
Add an information / about this map widget.
Add scale_threshold()
https://github.com/d3/d3-scale#threshold-scales
Named parameters must be in global environment (it seems)? Creating map layers with the following pattern fails (layer_data
cannot be resolved):
add_some_layer <- function(map_instance, some_data) {
# make some new layer data
layer_data <- some_data %>%
mutate()
map_instance %>%
add_foo_layer(
layer_id = "foo_id",
data = layer_data
)
}
scale_log
fails when breaks is NULL, due to log(NULL)
> rlang::last_error()
<error/rlang_error>
non-numeric argument to mathematical function
Backtrace:
1. rlang::with_abort(...)
3. rdeck::add_polygon_layer(...)
7. rdeck:::accessor_scale(rlang::enquo(get_fill_color), data)
9. rdeck:::scale_ticks.scale_log(scale)
16. rdeck:::trans(scale$breaks)
Run `rlang::last_trace()` to see the full context.
> rlang::last_trace()
<error/rlang_error>
non-numeric argument to mathematical function
Backtrace:
x
1. +-rlang::with_abort(...)
2. | \-base::withCallingHandlers(...)
3. +-rdeck::add_polygon_layer(...)
4. | +-rdeck:::layer(...)
5. | +-rdeck:::layer.default(...)
6. | | \-rlang::dots_list(..., .ignore_empty = "all")
7. | \-rdeck:::accessor_scale(rlang::enquo(get_fill_color), data)
8. | +-rdeck:::scale_ticks(scale)
9. | \-rdeck:::scale_ticks.scale_log(scale)
10. | +-`%>%`(...)
11. | +-rdeck:::ticks(scale$n_ticks, trans(scale$limits), trans(scale$breaks))
12. | \-rdeck:::trans(scale$breaks)
13. +-rdeck:::format_ticks(., scale$tick_format)
14. | \-rdeck:::tick_format(ticks)
15. | \-base::formatC(...)
16. \-rdeck:::invert(.)
By design, all parameters are optional and everything is serialised; this unfortunately creates some headaches with forgetting to include data, including data as the first parameter (which is id
) and American spelling of color
.
Raise warnings for the following:
data
missingcolour
scale
and accessor
topicsApplies to legends for all scale_*
types
In the mapbox studio, we can slide a layer down others, makeing their labels appear on top of the MVTs. Assuming add_mvt_layer
is translated to the map.addLayer
function of mapbox, the latter also have the beforeId
parameter (https://docs.mapbox.com/mapbox-gl-js/api/map/#map#addlayer), that would let us specify where the added layer appears in relation to the stack of other layers in the mapbox style.
From some documentation coming from mapbox, here is an example of how we can supply the layer under another part of the style. In this case, line geometries under the road labels:
map.addLayer({
'id': 'tiger-roads-id',
'type': 'line',
'source': 'tiger-roads-src',
'source-layer': 'roads',
'layout': {
'line-join': 'round',
'line-cap': 'round'
},
'paint': {
'line-color': '#ff69b4',
'line-width': 10
}
},
'road-label');
});
I tried taking advantage of the ...
argument of add_mvt_layer
and supply the beforeId = 'road-label'
, but without success. Is this achievable in the context of rdeck?
Again, thanks for you great package!
{crosstalk} allows filtering markers on a leaflet map without needing a full blown shiny server is a great feature of leaflet.
Is it possible to add this feature to rdeck?
Unmapped values aren't represented in the legend for scale_color_category
. Add parameter unmapped_tick
to explicitly add the unmapped colour to the legend.
Optionally render widget inside shadow dom to isolate rdeck from global styles present in rmarkdown, xaringan, etc.
Add scale_ordinal()
for ordinal (categorical) variables.
https://github.com/d3/d3-scale#scaleOrdinal
Auto highlight for multi polygons is highlighting only the polygon that is hovered, instead of the entire feature
When using rdeck_proxy to update a map in Shiny, get_fill_color
is failing when passed a reactive input containing a column name. It gives the error message "Error in assert_rgba: get_fill_color must be a valid rgb[a] hex string". Trying to use a reactive input inside one of the scale functions also fails, with "Error in rlang::eval_tidy: object 'input' not found".
The following reprex contrasts the correct/expected behaviour when updating pickable
with a reactive input with the incorrect/unexpected behaviour when updating get_fill_color
with a reactive input. (The MVT tiles are publicly accessible, so you should be able to run the reprex on your end. I've also confirmed the same behaviour with a local sf file, and am happy to provide that reprex as well.)
# Initialize map
test_map <-
rdeck(initial_view_state = view_state(center = c(-73.58, 45.53), zoom = 9)) |>
add_mvt_layer(id = "test",
data = mvt_url("dwachsmuth.borough_6"),
pickable = TRUE,
auto_highlight = TRUE,
get_fill_color = canale_ind_q5_2016)
# Works
shinyApp(
ui = fillPage(
rdeckOutput("map", height = "100%"),
absolutePanel(top = 10, left = 10,
selectInput("pickable", label = "Pickable",
choices = list("TRUE", "FALSE")
)
)
),
server = function(input, output) {
output$map <- renderRdeck(test_map)
picker <- reactive(if (input$pickable == "TRUE") TRUE else FALSE)
observe({
rdeck_proxy("map") |>
add_mvt_layer(id = "test",
pickable = picker(),
auto_highlight = TRUE,
get_fill_color = canale_ind_q5_2016)
})
}
)
# Does not work
shinyApp(
ui = fillPage(
rdeckOutput("map", height = "100%"),
absolutePanel(top = 10, left = 10,
selectInput("fill", label = "Variable", choices = list(
canale = "canale_ind_q5_2016",
housing = "housing_tenant_pct_q5_2016"))
)
),
server = function(input, output) {
output$map <- renderRdeck(test_map)
observe({
rdeck_proxy("map") |>
add_mvt_layer(id = "test", get_fill_color = input$fill)
})
}
)
scale_threshold()
breaks can fall outside the bounds of limits.
Add a map control to toggle fullscreen on & off for a single map.
Add api for retrieving widget instances, and manipulating a widget instance (e.g. changing layer visibility, theme, mapStyle etc) in the browser.
Apologies if I'm missing something obvious, but I can't figure out how to set the fill colour of an MVT layer with a scale referencing a data field in the MVT source.
I've got:
rdeck(initial_view_state = view_state(center = c(-73.58, 45.53), zoom = 9)) |>
add_mvt_layer(data = mvt_url("dwachsmuth.borough_5"),
get_fill_color = scale_color_linear(
col = "canale_ind_2016",
palette = c("#FF00FF", "#00FF00"),
limits = c(0, 5)))
Which I hope would scale the fill colour with reference to the "canale_ind_2016" field present in my MVT data source, but instead all polygons are filled with the fallback colour (black).
Is there some way to accomplish this?
Add a control to allow for switching the basemap style to an alternative style (e.g. satellite).
Map renders full viewport width and height; should be constrained by the map container. Probably a one-liner fix:
.rdeck {
position: relative;
}
Map breaks with:
react.js:9 TypeError: e.palette.map is not a function
at L (rdeck.js:1)
at rdeck.js:1
at Array.map (<anonymous>)
at rdeck.js:1
at new x (rdeck.js:1)
at create (rdeck.js:1)
at Array.map (<anonymous>)
at V (rdeck.js:1)
at ro (react.js:9)
at Hu (react.js:9)
R does warn:
Input to asJSON(keep_vec_names=TRUE) is a named vector. In a future version of jsonlite, this option will not be supported, and named vectors will be translated into arrays instead of objects. If you want JSON object output, please use a named list instead. See ?toJSON.
We agree that this parameter is just for controlling whether the layer appears in the internal layer control, not whether the layer can be toggled via external calls to setLayerVisibility
.
Hi,
I've loaded an R Deck into shinyapps.io. Everything works fine, except I can't seem to figure out a way to get my map box key to be read on shinyapps.io, so locally, you can see the mapbox tiles, but when it's hosted, the tiles are missing. Is there a way to do something akin to the following and have that token stored from the local environment onto the shiny app that gets hosted?
token <- Sys.get("MAPBOX_ACCESS_TOKEN")
rdeck::mapbox_access_token(token)
Aggregation layer tooltips are showing unrelated information -- properties of other objects that haven't been picked --- instead of null values (which is the expected result given the current implementation).
Tooltips on these layers should include the point count (if available) and the aggregated value (if available).
Hi Anthony,
I'm just wondering what the best way to go about writing a custom theme would be in the way you've written "kepler" and "light"? Ideally, I'd like to make adjustments to the theme, like this. I know I could do this by adding some CSS after I save a self-contained file, but wondering if there's a way to create a custom theme for use with rdeck? Here's an example:
Hello Anthony!
Maybe you've already implemented this: is there a way to retrieve the view states from the map in R, at the moment with rdeck? States such as zoom and central point. Mapdeck does this with input${map_id}_view_change
, I guess using onViewStateChange
function from deck.gl.
And thanks for your package!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.