walkerke / mapgl

R interface to Mapbox GL JS v3 and Maplibre GL JS
https://walker-data.com/mapgl
Other
91 stars 5 forks source link

proxy functions should be deferred until the shiny session is flushed by default #17

Open RWParsons opened 4 months ago

RWParsons commented 4 months ago

This bug was identified in the discussions of #12.

Default values for shiny inputs that are observed to modify the map via proxy function are not currently applied. This is because they are not deferred until the shiny session is flushed as is the default by leaflet. See the that the proxy function includes a deferUntilFlush argument (https://github.com/rstudio/leaflet/blob/92bc272caa9a268140e75ede1966bcdc7d585636/R/utils.R#L136) and the invocation of this check this value and defers the change until the shiny session is flushed (https://github.com/rstudio/leaflet/blob/92bc272caa9a268140e75ede1966bcdc7d585636/R/utils.R#L194).

reprex:

By default this app should show only one polygon on the map (with the current default inputs for the 'area_range'. If you reduce the lower bound of the input range, it updates the map but this should happen for the default view of the map.

library(shiny)
library(bslib)
library(colourpicker)
library(dplyr)
library(sf)
library(shinyWidgets)
devtools::load_all()

ui <- bootstrapPage(
  sliderInput("slider", "min value:", value = 0, min = -3, max = 3),
  numericRangeInput("area_range", label = "numeric range input for area", value = c(0.03, 0.042), step = 0.01),
  maplibreOutput("map")
)

server <- function(input, output, session) {
  nc <- st_read(system.file("shape/nc.shp", package = "sf"))
  nc$var1 <- rnorm(n = nrow(nc))
  nc$CNTY_ID <- as.character(nc$CNTY_ID)

  output$map <- renderMaplibre({
    maplibre() |>
      fit_bounds(nc, animate = FALSE) |>
      add_fill_layer(
        id = "polygon_layer",
        source = nc,
        fill_color = "blue",
        fill_opacity = 0.5,
        tooltip = "AREA"
      )
  })

  observe({
    ids <- nc |>
      filter(
        AREA >= min(input$area_range),
        AREA <= max(input$area_range)
      ) |>
      pull(CNTY_ID)

    cat(length(ids))

    maplibre_proxy("map") |>
      set_filter(
        "polygon_layer",
        list("in", "CNTY_ID", ids)
      )
  })
}

shinyApp(ui, server)

I think the solution would be to make some wrapper, like leaflet has invokeRemote() which checks the shiny session and defers if necessary, rather than each shiny util, like set_filter(), directly calling proxy$session$sendCustomMessage().

Thanks!

Rex

walkerke commented 3 months ago

I've pushed a temporary fix here that allows set_filter() to be used on a regular map object (within or outside of Shiny). So an app developer can hard-code the initial filter to correspond to the initial value of the slider and get the expected behavior. I'll keep looking into this solution as well though.

walkerke commented 3 months ago

Example usage:

library(shiny)
library(bslib)
library(colourpicker)
library(dplyr)
library(sf)
library(shinyWidgets)
devtools::load_all()

ui <- bootstrapPage(
  sliderInput("slider", "min value:", value = 0, min = -3, max = 3),
  numericRangeInput("area_range", label = "numeric range input for area", value = c(0.03, 0.042), step = 0.01),
  maplibreOutput("map")
)

server <- function(input, output, session) {
  nc <- st_read(system.file("shape/nc.shp", package = "sf"))
  nc$var1 <- rnorm(n = nrow(nc))
  nc$CNTY_ID <- as.character(nc$CNTY_ID)

  output$map <- renderMaplibre({

    initial_ids <- nc |>
      filter(
        AREA >= 0.03,
        AREA <= 0.042
      ) |>
      pull(CNTY_ID)

    maplibre() |>
      fit_bounds(nc, animate = FALSE) |>
      add_fill_layer(
        id = "polygon_layer",
        source = nc,
        fill_color = "blue",
        fill_opacity = 0.5,
        tooltip = "AREA"
      ) |>
      set_filter(
        "polygon_layer",
        c("in", "CNTY_ID", initial_ids)
      )
  })

  observe({
    ids <- nc |>
      filter(
        AREA >= min(input$area_range),
        AREA <= max(input$area_range)
      ) |>
      pull(CNTY_ID)

    cat(length(ids))

    maplibre_proxy("map") |>
      set_filter(
        "polygon_layer",
        c("in", "CNTY_ID", ids)
      )
  })
}

shinyApp(ui, server)