rdeck icon indicating copy to clipboard operation
rdeck copied to clipboard

Zoom-dependent layer visibility

Open yvanrichard opened this issue 1 year ago • 5 comments

Is there any way of constraining a layer visibility based on the zoom level?

I'm aiming to display different text layers, each visible within a specific zoom range. Similar to country names at low zoom levels to town names at high zoom levels.

I have tried a few things using JavaScript but to no avail.

yvanrichard avatar Sep 22 '24 04:09 yvanrichard

This is doable. Via shiny is fairly straight forward using an observable() and doesn't require any javascript. Without shiny this will require an onRender() javascript function (not straight forward, non-public api).

Are you working in Shiny?

anthonynorth avatar Sep 23 '24 02:09 anthonynorth

I have been trying the onRender() approach, but I haven't managed to access the layer in the javascript function so far. Could you provide a pointer on how to access the layer by any chance? My aim is to create a standalone html page that can be shared easily, using saveWidget. I'm trying to avoid the use of Shiny as much as I can. Wouldn't the observable cause the re-creation of the whole deck element anyway, which would be slow?

Zoom-dependent visibility is a common feature in GIS viewers. I reckon adding a minZoom and maxZoom argument in the layer functions would be a great addition if possible.

yvanrichard avatar Sep 23 '24 02:09 yvanrichard

I have been trying the onRender() approach, but I haven't managed to access the layer in the javascript function so far. Could you provide a pointer on how to access the layer by any chance?

Sure. The following should get you started.

points_df <- vctrs::data_frame(position = wk::xy(runif(1e5, -180, 180), runif(1e5, -90, 90), "OGC:CRS84"))

map <- rdeck(map_style = stylebox::mapbox_light(), theme = "light") |>
  add_scatterplot_layer(id = "random-point-layer", name = "random_point", data = points_df, get_radius = 5, radius_units = "pixels", visible = FALSE) |>
  htmlwidgets::onRender("
    function(el, x) {
      // rdeck widget instance
      const map = this.instance;
      if (!map instanceof rdeck.Widget) {
        // bail
      }

      // set layer visibility on zoom change
      map.state.deckgl.onViewStateChange = ({viewState}) => map.setLayerVisibility([{ name: 'random_point', groupName: null, visible: viewState.zoom >= 3 }]);
    }
  ")

map

Zoom-dependent visibility is a common feature in GIS viewers. I reckon adding a minZoom and maxZoom argument in the layer functions would be a great addition if possible.

I agree. ~~I'll track this in a separate issue.~~ (edit: unnecessary)

anthonynorth avatar Sep 23 '24 04:09 anthonynorth

Wouldn't the observable cause the re-creation of the whole deck element anyway, which would be slow?

Rdeck supports partial updates of the map and layers, so a visibility toggle via shiny is efficient.

Looks something like this

library(rdeck)

points_df <- vctrs::data_frame(position = wk::xy(runif(1e5, -180, 180), runif(1e5, -90, 90), "OGC:CRS84"))

map <- rdeck(map_style = stylebox::mapbox_light(), theme = "light") |>
  add_scatterplot_layer(id = "random-point-layer", name = "random_point", data = points_df, get_radius = 5, radius_units = "pixels", visible = FALSE)

ui <- shiny::fillPage(rdeckOutput("map", height = "100%"))

server <- function(input, output, session) {
  output$map <- renderRdeck(map)

  observe({
    map_proxy <- rdeck_proxy("map", session)
    view_state <- get_view_state(map_proxy)

    if (!is.null(view_state$zoom)) {
      # payload {"map:layer": {id: "random-point-layer", visible: false}
      set_layer_visibility(map_proxy, "random-point-layer", visible = view_state$zoom >= 3)
    }
  })
}

shiny::shinyApp(ui, server)

anthonynorth avatar Sep 23 '24 04:09 anthonynorth

Just amazing. Both your responses are very useful and are opening a whole realm of possibilities to me. Thank you so much, Anthony!

yvanrichard avatar Sep 23 '24 04:09 yvanrichard