Interactive Maps with tmap and Shiny

Session 2: The interactive view mode

Martijn Tennekes

Modes in tmap

Two built-in modes

tmap supports two output modes using the same map code:

  • "plot" — static maps (default), rendered via grid
  • "view" — interactive maps, powered by Leaflet

Additional modes via extensions:

  • "mapbox" and "maplibre" — via tmap.mapgl (covered in Session 7)

Switching modes

# Switch to interactive view mode
tmap_mode("view")

# Switch back to static plot mode
tmap_mode("plot")

# Toggle between the two built-in modes
ttm()

When more than two modes are loaded (e.g. after loading tmap.mapgl), use rtm() to rotate through all available modes.

The same map, two modes

library(tmap)

tm <- tm_shape(World) +
  tm_polygons(fill = "HPI", fill.legend = tm_legend("Happy Planet Index"))

# Static
tmap_mode("plot"); tm

# Interactive
tmap_mode("view"); tm

In "view" mode, a basemap is added by default and the map becomes interactive — zoom, pan, tooltips, popups, and more. Basemaps are covered in the next section.

Mode-specific options

Use tm_view() to control interactive-mode behaviour:

tmap_mode("view")

tm_shape(World) +
  tm_polygons("HPI") +
  tm_view(
    control.position = c("left", "bottom"),
    set_view = 2          # default zoom level
  )

See ?tm_view for all options (control box position, zoom limits, etc.).

Basemaps

What are basemaps?

  • A basemap is a pre-built map used as a geographic background
  • Drawn at the bottom, below all data layers
  • In "view" mode, basemaps are added by default
  • In "plot" mode, you need to add them explicitly via tm_basemap()

Use tm_basemap() to control which basemap is shown.

Basemap providers

  • Most basemaps are generated from OpenStreetMap data
  • Each provider styles OSM elements differently (colours, line widths, labels…)
  • The available providers depend on the mode: many work across modes, but there are differences — use tmap_providers() to see what is available for the active mode
  • Provider previews: Leaflet providers | MapLibre styles
# List providers available for the current mode
tmap_providers()

# Or use tab-completion:
.tmap_providers$

Using tm_basemap()

tmap_mode("view")

tm_shape(NLD_muni) +
  tm_polygons("employment_rate") +
  tm_basemap("OpenStreetMap")
# Disable basemap entirely
tm_shape(World) +
  tm_polygons("HPI") +
  tm_basemap(NULL)

Multiple basemaps — adding layers

Start with a choropleth and multiple switchable basemaps:

tmap_mode("view")

tm_shape(NLD_muni) +
  tm_polygons("employment_rate") +
tm_shape(NLD_prov) +
  tm_borders(lwd = 3) +
tm_basemap(c("OpenStreetMap",
             "CartoDB.Positron",
             "Esri.WorldImagery"))

The three basemaps appear as radio buttons — only one visible at a time.

Multiple basemaps — combining into a layer group

Assign both data layers to the same group so they toggle together:

tmap_mode("view")

tm_shape(NLD_muni) +
  tm_polygons("employment_rate", group = "Choropleth") +
tm_shape(NLD_prov) +
  tm_borders(lwd = 3, group = "Choropleth") +
tm_basemap(c("OpenStreetMap",
             "CartoDB.Positron",
             "Esri.WorldImagery"))

Now the municipality fill and province borders share a single “Choropleth” checkbox in the layer control.

Multiple basemaps — radio toggle between basemap and choropleth

Use tm_group() with control = "radio" to make the choropleth switch like a basemap — so only one of the basemap or the choropleth is visible at a time:

tmap_mode("view")

tm_shape(NLD_muni) +
  tm_polygons("employment_rate", group = "Choropleth") +
tm_shape(NLD_prov) +
  tm_borders(lwd = 3, group = "Choropleth") +
tm_basemap("OpenStreetMap") +
tm_group("Choropleth", control = "radio")

Overlay maps with tm_tiles()

  • An overlay map contains only vector elements (roads, labels, boundaries) with a transparent background
  • Use tm_tiles() — drawn in call order, unlike tm_basemap() which is always at the bottom
  • Multiple overlays become check box toggles (vs radio buttons for basemaps)
tmap_mode("view")

tm_shape(NLD_muni) +
  tm_polygons("employment_rate") +
  tm_basemap("Esri.WorldImagery") +
  tm_tiles("CartoDB.PositronOnlyLabels")

Tile servers

Basemaps and overlays are served as map tiles — small PNG images assembled by the browser:

  • URL template example: "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
  • Variables: {s} = server, {z} = zoom, {x} and {y} = tile coordinates
  • You can pass a URL template directly to tm_basemap() or tm_tiles()

Plot mode + basemaps

Basemaps also work in "plot" mode — tmap downloads the tiles at the requested zoom level:

tmap_mode("plot")

tm_shape(NLD_muni) +
  tm_polygons("employment_rate") +
  tm_basemap("OpenStreetMap", zoom = 8)

Note: Thunderforest and Stadia maps require a free API key.

CRS in view mode

Leaflet and Web Mercator

In "view" mode, Leaflet uses Web Mercator (EPSG:3857) by default — because the vast majority of basemap tiles are served in this CRS, Leaflet adopts it as the map’s projection:

  • Your spatial data is reprojected on the fly to Web Mercator for display
  • Coordinates are shown as latitude/longitude (EPSG:4326)
  • The original CRS of your data is preserved in R — only the display uses 3857
tmap_mode("view")

# NLD_muni is in RD New (EPSG:28992), the official CRS for the Netherlands
tm_shape(NLD_muni) +
  tm_polygons("employment_rate")

Using a different CRS in view mode

You can set a different CRS via tm_crs(), but basemaps will usually disappear:

tmap_mode("view")

tm_shape(NLD_muni) +
  tm_polygons("employment_rate") +
tm_crs(28992) +       # RD New — virtually no tile providers support this
tm_basemap(NULL)      # disable basemap explicitly
  • Tile servers for CRS other than 3857 do exist, but require a custom tile URL template — not covered here
  • In practice: if you need a non-Web-Mercator CRS in view mode, disable basemaps

CRS in view mode — summary

By default, "view" mode uses Web Mercator (EPSG:3857) with basemaps. When you switch to any other CRS, basemaps are usually not available:

Mode Default CRS Basemaps
"view" (default) Web Mercator (3857) ✅ available
"view" + custom tm_crs() any ❌ usually not available
"maplibre" / "mapbox" Web Mercator (3857) ✅ available
"maplibre" / "mapbox" + Globe Globe ✅ some — even in 3D!

The "maplibre" and "mapbox" modes from tmap.mapgl work the same way, but add a Globe CRS and support 3D basemaps — more on that in Sessions 7 and 8. 🌐

World maps in view mode

Web Mercator distorts the poles heavily — fine for navigation and location, not recommended for thematic world maps.

Option 1 — use a world CRS and disable basemaps:

tmap_mode("view")
tm_shape(World) +
  tm_polygons("HPI") +
tm_crs("+proj=eqearth") +   # Equal Earth, Robinson, etc.
tm_basemap(NULL)             # basemaps not available for this CRS

Option 2 — use the "maplibre" mode from tmap.mapgl (Globe CRS by default, no distortion):

library(tmap.mapgl)
tmap_mode("maplibre")
tm_shape(World) +
  tm_polygons("HPI")

See also: tmap CRS vignette

Recap

  • Use tmap_mode("view") or ttm() to switch to interactive mode
  • The same tmap code works in both "plot" and "view" mode
  • In "view" mode, a basemap is added by default; use tm_basemap() to change or disable it
  • tm_tiles() adds overlay maps; multiple basemaps → radio buttons, multiple tiles → checkboxes
  • Use tmap_providers() to see which providers are available for the active mode
  • Most basemaps are served in Web Mercator — so Leaflet uses this as its default CRS
  • For thematic world maps: use a world CRS + tm_basemap(NULL), or use the "maplibre" mode from tmap.mapgl
  • Provider previews: Leaflet providers | MapLibre styles