qfes / rdeck

Deck.gl widget for R
https://qfes.github.io/rdeck
MIT License
97 stars 0 forks source link

Zoom-dependent layer visibility #122

Open yvanrichard opened 1 week ago

yvanrichard commented 1 week ago

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.

anthonynorth commented 1 week ago

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?

yvanrichard commented 1 week ago

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.

anthonynorth commented 1 week ago

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 commented 1 week ago

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)
yvanrichard commented 1 week ago

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