Skip to contents

Shiny is a framework for R and Python. Learn more

toro maps can be integrated into Shiny applications, allowing for interactive and dynamic visualisations. This allows more dynamic control over the structure of the map and the underlying data being visualised.

To use toro maps in Shiny, you can use the mapOutput() function to create a map output element in the UI, and the renderMap() function to render the map in the server. You can also use mapProxy() to modify the map after it has been rendered, skipping the need to re-render the entire map when user inputs change.

library(shiny)
library(toro)

ui <- fluidPage(
  toro::mapOutput("map")
)

server <- function(input, output, session) {
  output$map <- toro::renderMap({
    toro::map()
  })
}

if (interactive()) {
  shinyApp(ui, server)
}

Understanding mapProxy

To modify a map on a shiny page without re-rendering the whole widget you can use mapProxy.

Typically you use map to create the initial static map widget, and then use mapProxy to update more dynamic aspects of the map.

mapProxy requires the id of the map to be modified, which is the same as the output ID used in mapOutput() and renderMap(). This id will need to be appropriately scoped if the map is inside a module, via NS and session$ns.

In this example, we will create a Shiny app that allows users to change the map tiles using a select input. The set_tile_layer() function is used to update the tiles on the map when the user selects a different option from the input.

library(shiny)
library(toro)

ui <- fluidPage(
  toro::mapOutput("map"),
  selectInput(
    "tiles",
    "Tiles",
    choices = toro::get_tile_options(),
    selected = "satellite"
  ),
)

server <- function(input, output, session) {
  output$map <- toro::renderMap({
    toro::map(style = "satellite", loadedTiles = toro::get_tile_options())
  })

  observe({
    req(input$tiles)
    toro::mapProxy("map") |>
      set_tile_layer(input$tiles)
  })
}

if (interactive()) {
  shinyApp(ui, server)
}

The data source

In other vignettes we have added layers and directly specified the data to be used in those layers. Instead, here we can use add_source() to add a data source to the map, and then use that source in multiple layers.

library(shiny)
library(toro)

sample_data <- spData::cycle_hire

ui <- fluidPage(
  toro::mapOutput("map")
)

server <- function(input, output, session) {
  output$map <- toro::renderMap({
    toro::map() |>
      add_source("my_source", data = sample_data) |>
      add_circle_layer(
        id = "my_layer",
        source = "my_source"
      )
  })
}

if (interactive()) {
  shinyApp(ui, server)
}

Now that we have a source, we can use set_source_data() to update the data in that source without needing to re-render the entire map, via mapProxy(). This example randomly samples 10 points from the source every 2 seconds and updates the source with those points.

library(shiny)
library(toro)
sample_data <- spData::cycle_hire

ui <- fluidPage(
  toro::mapOutput("map")
)

server <- function(input, output, session) {
  output$map <- toro::renderMap({
    toro::map(
      center = c(-0.1, 51.5),
      zoom = 11
    ) |>
      add_source("my_source", data = sample_data |> dplyr::slice(1:10)) |>
      add_circle_layer(
        id = "my_layer",
        source = "my_source"
      )
  })

  observe({
    # Randomly sample data to simulate a change in the data source
    invalidateLater(2000, session)
    new_data <- sample_data[sample(nrow(sample_data), 10), ]
    toro::mapProxy("map") |>
      set_source_data("my_source", new_data)
  })
}

if (interactive()) {
  shinyApp(ui, server)
}

Controlling the data source allows for animations to be performed via a pure R Shiny approach. In this example, a time-bound dataset is animated via a slider input. The slider input controls the date range of the data to be displayed on the map, and the map is updated accordingly using set_source_data(). This allows you to visualise data in both space and time.

library(shiny)
library(toro)
library(sf)
#> Linking to GEOS 3.14.1, GDAL 3.12.1, PROJ 9.7.1; sf_use_s2() is TRUE

# Sample data: each point has a `date` it "appears" on.
set.seed(1)
n <- 60
pts <- sf::st_as_sf(
  data.frame(
    id   = 1:n,
    date = sort(as.Date("2024-01-01") + sample(0:120, n, replace = TRUE)),
    lon  = runif(n, 172.5, 178.5),
    lat  = runif(n, -41.5, -37.0)
  ),
  coords = c("lon", "lat"),
  crs = 4326
)

date_range <- range(pts$date)

ui <- fluidPage(
  mapOutput("map", height = "600px"),
  sliderInput(
    "when", "Show points up to:",
    min = date_range[1], max = date_range[2],
    value = date_range[1],
    timeFormat = "%Y-%m-%d",
    width = "100%",
    animate = animationOptions(interval = 400, loop = FALSE)  # gives you a play button
  )
)

server <- function(input, output, session) {
  output$map <- renderMap({
    map() |>
      set_bounds(bounds = pts, padding = 50) |>
      # Start with just the first day's points in the source
      add_source(
        source_id = "events",
        data = pts |> dplyr::filter(date <= date_range[1])
      ) |>
      add_circle_layer(
        id = "events_layer",
        source = "events"
      )
  })

  # Every time the slider moves, push the cumulative subset to the source
  observe({
    req(input$map_loaded)
    visible <- pts[pts$date <= input$when, ]
    mapProxy("map") |>
      set_source_data(source_id = "events", data = visible)
  }) |>
    bindEvent(input$when)
}

if (interactive()) {
  shinyApp(ui, server)
}

Updating map components via IDs

Up to this point all aspects of a toro map have used IDs in the function arguments, although these attributes were only accessed in some situations. In Shiny to modify parts of a map you need to provide the ID of the aspect you want to update. This section provides a reference of the different IDs used in toro maps, and which functions use them. As noted above, you will need to use mapProxy() to modify the map after it has been rendered, and the map ID will need to be appropriately scoped if the map is inside a module, via NS and session$ns.

With data modification abstracted to the add_source() and set_source_data() functions, the remaining customisation of toro maps is handled via functions add, remove or update layers based on the IDs set on creation.

Map IDs

The ID of the map in the shiny context. The map ID is also used in the events and status in the below sections.

Layer IDs

When you add a layer to the map you can provide an ID for that layer. This ID can then be used to modify the layer later on.

Add functions:

Visibility functions:

Controls that target layers:

Layer customisation:

Source IDs

When you add a data source to the map you can provide an ID for that source. This ID can then be used to modify the source later on. If you add a source inside of an add layer function then the source ID will be auto generated for you.

Add functions:

Modifying functions:

Control IDs

When you add a control to the map you can provide an ID for that control. This ID can then be used to modify the control later on.

Some controls are linked to other element IDs (like the cluster and visibility toggles needing layer IDs).

Add functions:

Remove functions:

Animation IDs

When you add a route to the map you can provide an ID for that route. This ID can then be used to modify the route later on.

Add functions:

Remove functions:

Other functions:

Other IDs

When using a draw control to draw shapes. Each shape will have an ID that can be used to delete the shape with delete_drawn_shape().


Inputs & events

Map loaded event

By default when the map initially loads it will trigger a loaded event in Shiny. You can use this event to trigger other actions in your Shiny app.

The Shiny input value is available as input$<mapId>_loaded, where <mapId> is the output ID of the map.

# In UI
mapOutput("my_map")

# In server loaded events watching <map_id>_loaded input values
observe({
  req(input$my_map_loaded)
  print("Map has loaded!")
}) |>
  bindEvent(input$my_map_loaded)

Feature click events

Anytime a feature (polygon, line, point) is clicked on the map, a feature_click event is fired.

You can listen for this event using an observer in your Shiny app. Just append _feature_click to the map ID to get the input ID (input$<mapId>_feature_click).

# In UI
mapOutput("my_map")

# In server feature click events watching <map_id>_feature_click input values
observe({
  print(get_clicked_feature(input$my_map_feature_click))
})

Map view status

Whenever the map view changes (zoom/bounds) a bounds / zoom event is fired. You can listen for this event using an observer in your Shiny app. Just append _bounds / _zoom to the map ID to get the input ID (input$<mapId>_bounds / input$<mapId>_zoom).

# In UI
mapOutput("my_map")

# In server bounds / zoom events watching <map_id>_bounds / <map_id>_zoom input values
observe({
  print(input$my_map_bounds)
})

observe({
  print(input$my_map_zoom)
})

Event Reference

Event Input ID Description
Map loaded <mapId>_loaded Triggered when the map has finished loading.
Feature click <mapId>_feature_click Triggered when a feature (polygon, line, point) is clicked on the map.
Map bounds <mapId>_bounds Triggered when the map bounds change (e.g., zooming or panning).
Map zoom <mapId>_zoom Triggered when the map zoom level changes.
Shape Created <mapId>_shape_created Triggered when a new shape is drawn on the map.
Shape Updated <mapId>_shape_updated Triggered when an existing shape is modified on the map.
Shape Deleted <mapId>_shape_deleted Triggered when a shape is deleted from the map.
Layer selected <mapId>_layer_selected Triggered when a layer is selected from a control panel.
Animation control <mapId>_animation_control Triggered when an animation control is used.
Animation speed changed <mapId>_animation_speed_changed Triggered when the animation speed is changed.