Simon Woodward
Simon Woodward

Reputation: 2026

Can't get Leaflet.Spin plugin working in R Shiny

I have a shiny app which involves drawing a large number of lines on a map. I would like to use a spinner to show the user that rendering is underway. Most shiny approaches don't work because they only show the spinner while the data is being sent to leaflet, not when leaflet is rendering. The Leaflet.Spin plugin looks promising but I have been struggling to get it to work. The examples I have been following are

https://gist.github.com/jcheng5/c084a59717f18e947a17955007dc5f92

leaflet plugin and leafletProxy with polylineDecorator as Example

How do I get the js events to fire properly and show Leaflet.Spin when the lines (circles in this example) are rendering?

Update: Spinner now works, but events fire for each individual circle added, so if number of circles declines, spinner doesn't turn off correctly.

library(shiny)
library(leaflet)
library(htmltools) # for htmlDependency
library(htmlwidgets) # for onRender

# https://gist.github.com/jcheng5/c084a59717f18e947a17955007dc5f92
# https://stackoverflow.com/questions/52846472/leaflet-plugin-and-leafletproxy-with-polylinedecorator-as-example
spinPlugin <- htmlDependency(
  "spin.js", 
  "2.3.2",
  src = c(href = "https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2"),
  script = "spin.min.js") # there's no spin.css

leafletspinPlugin <- htmlDependency(
  "Leaflet.Spin", 
  "1.1.2",
  src = c(href = "https://cdnjs.cloudflare.com/ajax/libs/Leaflet.Spin/1.1.2"),
  script = "leaflet.spin.min.js")

registerPlugin <- function(map, plugin) {
  map$dependencies <- c(map$dependencies, list(plugin))
  map
}

# Note: Ctrl-Shift-J opens the javascript console in the browser
spin_event <- "function(el, x) {
  console.log('spin event added'); 
  var mymap = this;
  mymap.on('layerremove', function(e) {
    console.log('layerremove fired');
    mymap.spin(true);
  });
  mymap.on('layeradd', function(e) {
    console.log('layeradd fired');
    mymap.spin(false); 
  });
}"

dlat <- 1 / 111000 * 100 # degrees per metre

ui <- fluidRow(
  tags$h2("Using Leaflet.Spin in Shiny"),
  actionButton("plotbutton", label = "Show Spinner While Adding Markers"),
  leafletOutput("map")
)

server <- function(input, output, session) {
  output$map <- renderLeaflet({
    cat("renderLeaflet\n")
    leaflet() %>%
      addTiles() %>%
      setView(175.322, -37.789, zoom = 17) %>% 
      registerPlugin(spinPlugin) %>% 
      registerPlugin(leafletspinPlugin) %>% 
      onRender(spin_event) %>% 
      clearShapes() %>% # initialise spinner
      addCircles(
        lng = 175.322,
        lat = -37.789,
        radius = 0,
        opacity = 0
      )
  })
  
  observeEvent(input$plotbutton, {
    cat("input$plotbutton\n")
    n <- ceiling(runif(1) * 10000)
    leafletProxy("map") %>%
      clearShapes() %>% 
      addCircles(
        lng = 175.322 + (runif(n) * 2 - 1) * dlat * 6,
        lat = -37.789 + (runif(n) * 2 - 1) * dlat * 1.5,
        radius = dlat * runif(n) * dlat
      )
  })
}

shinyApp(ui = ui, server = server)

Upvotes: 3

Views: 1044

Answers (2)

rbasa
rbasa

Reputation: 462

One year late, but I was looking for a way to have a busy spinner for leaflet.

I realized that your original code would create x number of spin instances on clearShapes(). If the following addCircles() has less than x number of circles, there would be spin instances left running.

My solution is to use a dummy layer to watch for from Javascript. In the example below, a circle with layerId = 'spinnerMarker'. To start the leafletproxy update, that layer is removed with removeShape(layerId = 'spinnerMarker') which triggers JS layerremove. The data circles are then added. The update is ended by adding a circle with layerId = 'spinnerMarker' to trigger the JS layeradd.

In JS, the layer being added or removed is checked with e.layer.options.layerId == 'spinnerMarker' to run mymap.spin().

This way there is only one spin instance running.

library(shiny)
library(leaflet)
library(htmltools) # for htmlDependency
library(htmlwidgets) # for onRender

# https://gist.github.com/jcheng5/c084a59717f18e947a17955007dc5f92
# https://stackoverflow.com/questions/52846472/leaflet-plugin-and-leafletproxy-with-polylinedecorator-as-example
spinPlugin <- htmlDependency(
  "spin.js", 
  "2.3.2",
  src = c(href = "https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2"),
  script = "spin.min.js") # there's no spin.css

leafletspinPlugin <- htmlDependency(
  "Leaflet.Spin", 
  "1.1.2",
  src = c(href = "https://cdnjs.cloudflare.com/ajax/libs/Leaflet.Spin/1.1.2"),
  script = "leaflet.spin.min.js")

registerPlugin <- function(map, plugin) {
  map$dependencies <- c(map$dependencies, list(plugin))
  map
}

# Note: Ctrl-Shift-J opens the javascript console in the browser
spin_event <- "function(el, x) {
  console.log('spin event added'); 
  var mymap = this;
  mymap.on('layerremove', function(e) {
    console.log('layerremove fired');
    if (e.layer.options.layerId == 'spinnerMarker') {
      console.log(e.layer.options.layerId);
      mymap.spin(true);
    }
  });
  mymap.on('layeradd', function(e) {
    console.log('layeradd fired');
    if (e.layer.options.layerId == 'spinnerMarker') {
      console.log(e.layer.options.layerId);
      mymap.spin(false);
    }
  });
}"

dlat <- 1 / 111000 * 100 # degrees per metre

ui <- fluidRow(
  tags$h2("Using Leaflet.Spin in Shiny"),
  actionButton("plotbutton", label = "Show Spinner While Adding Markers"),
  leafletOutput("map")
)

server <- function(input, output, session) {
  output$map <- renderLeaflet({
    cat("renderLeaflet\n")
    leaflet() %>%
      addTiles() %>%
      setView(175.322, -37.789, zoom = 17) %>% 
      registerPlugin(spinPlugin) %>% 
      registerPlugin(leafletspinPlugin) %>% 
      onRender(spin_event) %>% 
      clearShapes() %>% # initialise spinner
      addCircles(     # invisible placeholder
        lng = 175.322,
        lat = -37.789,
        radius = 0,
        opacity = 0,
        layerId = 'spinnerMarker'   # identifier, can be found in js: e.layer.options.layerId
      )
  })
  
  observeEvent(input$plotbutton, {
    cat("input$plotbutton\n")
    n <- ceiling(runif(1) * 10000)
    leafletProxy("map") %>%
      removeShape(layerId = 'spinnerMarker') %>%    # this triggers mymap.spin(true)
      clearShapes() %>% 
      addCircles(
        lng = 175.322 + (runif(n) * 2 - 1) * dlat * 6,
        lat = -37.789 + (runif(n) * 2 - 1) * dlat * 1.5,
        radius = dlat * runif(n) * dlat
      ) %>%
      addCircles(         # invisible placeholder to trigger the mymap.spin(false)
        lng = 175.322,
        lat = -37.789,
        radius = 0,
        opacity = 0,
        layerId = 'spinnerMarker'   # identifier, can be found in js: e.layer.options.layerId
      )
  })
}

shinyApp(ui = ui, server = server)

Upvotes: 5

St&#233;phane Laurent
St&#233;phane Laurent

Reputation: 84529

The URLs you provide are not valid. Try

spinPlugin <- htmlDependency(
  "spin.js", 
  "4.1.0",
  src = c(href = "https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2"),
  script = "spin.min.js") # there's no spin.css

leafletspinPlugin <- htmlDependency(
  "Leaflet.Spin", 
  "1.1.2",
  src = c(href = "https://cdnjs.cloudflare.com/ajax/libs/Leaflet.Spin/1.1.2"),
  script = "leaflet.spin.min.js")

Upvotes: 2

Related Questions