Interactive Maps with tmap and Shiny

Session 11: Integrating tmap with Shiny dashboards

Martijn Tennekes

Overview

  • Shiny basics
  • Rendering a tmap map in Shiny with renderTmap()
  • Dynamic control with input widgets
  • Efficient map updates with tmapProxy()
  • Reacting to feature clicks and other map events

What is Shiny?

Shiny is an R package that lets you build interactive web apps using only R.

  • UI and server structure
  • Reactive programming model
  • Useful for data dashboards, maps, and more

Shiny App Skeleton

library(shiny)

ui <- fluidPage(
  h2("My Shiny App"),
  textInput("txt", "Type something"),
  textOutput("output")
)

server <- function(input, output) {
  output$output <- renderText({
    paste("You typed:", input$txt)
  })
}

shinyApp(ui, server)

Example: Simple Interactive Map

library(tmap)

tmap_mode("view")

tm_shape(World) +
  tm_polygons(fill = "well_being")

Example: map with renderTmap() in Shiny

library(shiny)
library(tmap)

tmap_mode("view")

ui <- fluidPage(
  tmapOutput("map")
)

server <- function(input, output) {
  output$map <- renderTmap({
    tm_shape(World) +
      tm_polygons(fill = "well_being")
  })
}

shinyApp(ui, server)

Example: map with input widgets

Use selectInput() to dynamically control the fill variable.

tmap_mode("view")

ui <- fluidPage(
  selectInput("var", "Variable",
              choices = c("inequality", "gender", "press")),
  tmapOutput("map")
)

server <- function(input, output) {
  output$map <- renderTmap({
    tm_shape(World) +
      tm_polygons(fill = input$var)
  })
}

shinyApp(ui, server)

Important: tmap mode and Shiny

  • Always set tmap_mode() before shinyApp() — not inside server or renderTmap()
  • It is not possible to switch modes from within a Shiny app
  • Shiny integration is implemented for all modes, also maplibre and mapbox
# ✅ correct
tmap_mode("view")
shinyApp(ui, server)

# ❌ does not work as expected
server <- function(input, output) {
  output$map <- renderTmap({
    tmap_mode("view")   # has no effect here
    tm_shape(World) + tm_polygons(fill = "HPI")
  })
}

Update the map with tmapProxy()

  • In the previous example, the map will rerender after selecting another variable
  • Better is to update the map, which will retain the current view
  • Only useful in view mode

Example with tmapProxy()

world_vars <- setdiff(names(World), c("iso_a3", "name", "sovereignt", "geometry"))
tmap_mode("view")
shinyApp(
  ui = fluidPage(
    tmapOutput("map", height = "600px"), selectInput("var", "Variable", world_vars)),
  server <- function(input, output, session) {
    output$map <- renderTmap({
      tm_shape(World) + tm_polygons(fill = world_vars[1], id = "iso_a3", zindex = 401)
    })
    observe({
      var <- input$var
      tmapProxy("map", session, {
        tm_remove_layer(401) +
          tm_shape(World) +
          tm_polygons(fill = var, id = "iso_a3", zindex = 401)
      })
    })
  }, options = list(launch.browser=TRUE)
)

Reacting to map events

Map inputs available in Shiny

When a tmap map is rendered with renderTmap(), Shiny automatically exposes these inputs (here for tmapOutput("map")):

Input What it contains
input$map_shape_click Clicked feature: id, coordinates
input$map_shape_mouseover Feature under cursor
input$map_shape_mouseout Feature cursor just left
input$map_bounds Current map bounding box
input$map_center Current map centre (lat/lon)
input$map_zoom Current zoom level
input$map_groups Active layer groups

These are Leaflet ("view" mode) specific

The id argument and click events

The id column from the layer function is returned as input$map_shape_click$id when a feature is clicked:

# e.g. if tmapOutput("map"):
input$map_shape_click$id      # the id value of the clicked feature
input$map_shape_click$lat     # latitude of click
input$map_shape_click$lng     # longitude of click

Example: show country info on click

tmap_mode("view")
ui <- fluidPage(
  tmapOutput("map"),
  verbatimTextOutput("info")
)
server <- function(input, output, session) {
  output$map <- renderTmap({
    tm_shape(World) +
      tm_polygons(fill = "HPI",
                  popup.vars = c("name", "HPI", "life_exp"),
                  id = "iso_a3")  # id at layer level
  })
  output$info <- renderText({
    print(input$map_shape_click)
    clicked <- input$map_shape_click$id
    if (is.null(clicked)) return("Click a country on the map")
    row <- World[World$iso_a3 == clicked, ]
    paste0(row$name, " — HPI: ", round(row$HPI, 1))
  })
}
shinyApp(ui, server)

Example: pan to selected country

Use input$map_zoom and leafletProxy() / tmapProxy() to pan the map when a user selects a country from a dropdown:

tmap_mode("view")

ui <- fluidPage(
  selectInput("country", "Go to country",
              choices = sort(World$name)),
  tmapOutput("map")
)

server <- function(input, output, session) {
  output$map <- renderTmap({
    tm_shape(World) +
      tm_polygons(fill = "HPI", id = "name")  # id at layer level
  })

  observe({
    sel <- World[World$name == input$country, ]
    bb  <- sf::st_bbox(sel)
    leaflet::leafletProxy("map") |>
      leaflet::fitBounds(bb[["xmin"]], bb[["ymin"]],
                         bb[["xmax"]], bb[["ymax"]])
  })
}

shinyApp(ui, server)

Click + tmapProxy() combined

world_vars <- setdiff(names(World), c("iso_a3", "name", "sovereignt", "geometry"))
tmap_mode("view")

ui <- fluidPage(
  selectInput("var", "Variable", world_vars),
  tmapOutput("map"),
  verbatimTextOutput("info")
)

server <- function(input, output, session) {
  output$map <- renderTmap({
    tm_shape(World) +
      tm_polygons(fill = world_vars[1], id = "iso_a3", zindex = 401)
  })
  observe({
    tmapProxy("map", session, {
      tm_remove_layer(401) +
        tm_shape(World) +
        tm_polygons(fill = input$var, id = "iso_a3", zindex = 401)
    })
  })
  output$info <- renderText({
    clicked <- input$map_shape_click$id
    if (is.null(clicked)) return("Click a country")
    World$name[World$iso_a3 == clicked]
  })
}
shinyApp(ui, server)

Recap

  • tmap integrates easily with Shiny via renderTmap() and tmapOutput()
  • Use reactive inputs like selectInput() to control map content
  • Use tmapProxy() to update the map without a full re-render (view mode only)
  • id in the layer function → returned on click as input$map_shape_click$id
  • Other map inputs: map_bounds, map_center, map_zoom, map_groups, map_shape_mouseover
  • More: tmap Shiny vignette