Module 15 tmap

Author

Ethan Schatz

This module explores tmap and leaflet pacakges for R.


tmap

Packages

library(sf)
Linking to GEOS 3.14.1, GDAL 3.12.1, PROJ 9.7.1; sf_use_s2() is TRUE
library(terra)
terra 1.9.27
library(tmap)
library(leaflet)
library(htmltools)

sf_use_s2(FALSE)
Spherical geometry (s2) switched off

Example 1: Salt Lake County

Load the Data

## Census tracts for Salt Lake County with population density
tracts <- st_read("E:/Summer 2026/data/slc_tract/slc_tract_2015.shp", quiet = TRUE)
## Salt Lake light rail tracks
lightrail <- st_read("E:/Summer 2026/data/LightRail_UTA/LightRail_UTA.shp", quiet = TRUE)
## Salt Lake light rail stations
stations <- st_read("E:/Summer 2026/data/LightRailStations_UTA/LightRailStations_UTA.shp", quiet = TRUE)

Check CRS

st_crs(tracts)$epsg
[1] 4269
st_crs(lightrail)$epsg
[1] 26912

Reproject Tracts

tracts <- st_transform(tracts, st_crs(lightrail))

Load tmap

tm_shape(tracts) + 
  tm_borders()

tm_fill

tm_shape(tracts) + 
  tm_fill("density") +
  tm_borders()

tm_shape(tracts) + 
  tm_fill("density", palette = "Greens", style = "quantile") +
  tm_borders("lightgray")
── tmap v3 code detected ───────────────────────────────────────────────────────
[v3->v4] `tm_fill()`: instead of `style = "quantile"`, use fill.scale =
`tm_scale_intervals()`.
ℹ Migrate the argument(s) 'style', 'palette' (rename to 'values') to
  'tm_scale_intervals(<HERE>)'
[cols4all] color palettes: use palettes from the R package cols4all. Run
`cols4all::c4a_gui()` to explore them. The old palette name "Greens" is named
"brewer.greens"
Multiple palettes called "greens" found: "brewer.greens", "matplotlib.greens". The first one, "brewer.greens", is returned.

tm_shape(tracts) + 
  tm_fill("density", palette = "-magma", style = "cont") +
  tm_borders("lightgray")
── tmap v3 code detected ───────────────────────────────────────────────────────
[v3->v4] `tm_fill()`: instead of `style = "cont"`, use fill.scale =
`tm_scale_continuous()`.
ℹ Migrate the argument(s) 'palette' (rename to 'values') to
  'tm_scale_continuous(<HERE>)'

###Add Lightrail

tm_shape(tracts) + 
tm_shape(tracts) + 
  tm_fill("density", palette = "Greens", style = "quantile") +
  tm_borders("lightgray") +
  tm_shape(lightrail) +
  tm_lines(lwd = 2, lty = 'dashed', col = "darkorange")
── tmap v3 code detected ───────────────────────────────────────────────────────
[v3->v4] `tm_fill()`: instead of `style = "quantile"`, use fill.scale =
`tm_scale_intervals()`.
ℹ Migrate the argument(s) 'style', 'palette' (rename to 'values') to
  'tm_scale_intervals(<HERE>)'
[cols4all] color palettes: use palettes from the R package cols4all. Run
`cols4all::c4a_gui()` to explore them. The old palette name "Greens" is named
"brewer.greens"
Multiple palettes called "greens" found: "brewer.greens", "matplotlib.greens". The first one, "brewer.greens", is returned.

Distinct Routes

tm_shape(tracts) + 
  #tm_fill("density", palette = "Greens", style = "quantile") +
  tm_borders("lightgray") +
  tm_shape(lightrail) +
  tm_lines(lwd = 4, col = "ROUTE")

Stations

tm_shape(tracts) + 
  tm_fill("density", palette = "Greens", style = "quantile") +
  tm_borders("lightgray") +
  tm_shape(lightrail) +
  tm_lines(lwd = 2) +
  tm_shape(stations) +
  tm_dots(size = 0.25, shape = 23)
── tmap v3 code detected ───────────────────────────────────────────────────────
[v3->v4] `tm_fill()`: instead of `style = "quantile"`, use fill.scale =
`tm_scale_intervals()`.
ℹ Migrate the argument(s) 'style', 'palette' (rename to 'values') to
  'tm_scale_intervals(<HERE>)'
[cols4all] color palettes: use palettes from the R package cols4all. Run
`cols4all::c4a_gui()` to explore them. The old palette name "Greens" is named
"brewer.greens"
Multiple palettes called "greens" found: "brewer.greens", "matplotlib.greens". The first one, "brewer.greens", is returned.

Details

tm_shape(tracts) + 
  tm_graticules(col = "lightgray") + 
  tm_fill("density", title = "Popn density", palette = "Greens", style = "quantile") +
  tm_borders("lightgray") +
  tm_shape(lightrail) +
  tm_lines(lwd = 2) +
  tm_shape(stations) +
  tm_dots(size = 0.25, shape = 23) +
  tm_compass(position = c("left", "bottom")) +
  tm_scalebar(position = c("right", "top")) +
  tm_layout(main.title = "Salt Lake County Light Rail", 
            legend.outside = TRUE,
            legend.outside.position = c("left"))
── tmap v3 code detected ───────────────────────────────────────────────────────
[v3->v4] `tm_fill()`: instead of `style = "quantile"`, use fill.scale =
`tm_scale_intervals()`.
ℹ Migrate the argument(s) 'style', 'palette' (rename to 'values') to
  'tm_scale_intervals(<HERE>)'
[v3->v4] `tm_fill()`: migrate the argument(s) related to the legend of the
visual variable `fill` namely 'title' to 'fill.legend = tm_legend(<HERE>)'
[v3->v4] `tm_layout()`: use `tm_title()` instead of `tm_layout(main.title = )`
[cols4all] color palettes: use palettes from the R package cols4all. Run
`cols4all::c4a_gui()` to explore them. The old palette name "Greens" is named
"brewer.greens"
Multiple palettes called "greens" found: "brewer.greens", "matplotlib.greens". The first one, "brewer.greens", is returned.

Clip

tracts_sub = tracts %>%
  dplyr::filter(TRACTCE %in% c(102500, 102600, 114000))

tm_shape(tracts, bbox = st_bbox(tracts_sub)) + 
  tm_borders("lightgray") +
  tm_shape(lightrail) +
  tm_lines(lwd = 2) +
  tm_shape(stations) +
  tm_dots(size = 0.25, shape = 23) +
  tm_text("STATIONNAM", ymod = -1, bg.color = "white", size = 0.8)
── tmap v3 code detected ───────────────────────────────────────────────────────

Interactive Maps

## Set interactive
tmap_mode("view")
ℹ tmap modes "plot" - "view"
ℹ toggle with `tmap::ttm()`
tm_shape(tracts) + 
  tm_fill("density", title = "Popn density", palette = "Greens", style = "quantile", id = "TRACTCE") +
  tm_borders("lightgray") +
  tm_shape(lightrail) +
  tm_lines(lwd = 2) +
  tm_shape(stations) +
  tm_dots(size = 0.25, shape = 23)
── tmap v3 code detected ───────────────────────────────────────────────────────
[v3->v4] `tm_fill()`: instead of `style = "quantile"`, use fill.scale =
`tm_scale_intervals()`.
ℹ Migrate the argument(s) 'style', 'palette' (rename to 'values') to
  'tm_scale_intervals(<HERE>)'
[v3->v4] `tm_fill()`: migrate the argument(s) related to the legend of the
visual variable `fill` namely 'title' to 'fill.legend = tm_legend(<HERE>)'
[cols4all] color palettes: use palettes from the R package cols4all. Run
`cols4all::c4a_gui()` to explore them. The old palette name "Greens" is named
"brewer.greens"
Multiple palettes called "greens" found: "brewer.greens", "matplotlib.greens". The first one, "brewer.greens", is returned.

Set back to Static

tmap_mode("plot")
ℹ tmap modes "plot" - "view"

Example 2: Western North America Climate

Load the Data

wna_climate <- read.csv("E:/Summer 2026/data/WNAclimate.csv")
wna_climate <- st_as_sf(wna_climate, 
                        coords = c("LONDD", "LATDD"),
                        crs = 4326)

Quick Map

tm_shape(wna_climate) +
  tm_symbols(col = "Jul_Tmp")

Natrual Earth Implementation

library(rnaturalearth)

countries50 <- ne_download(scale = 50, type = "admin_0_countries")
Reading 'ne_50m_admin_0_countries.zip' from naturalearth...
rivers50 <- ne_download(scale = 50, type = "rivers_lake_centerlines", category = "physical")
Reading 'ne_50m_rivers_lake_centerlines.zip' from naturalearth...

Map layers

tm_shape(countries50, bbox = st_bbox(wna_climate)) +
  tm_borders() +
  tm_shape(rivers50) +
  tm_lines("lightblue", lwd = 2) +
  tm_shape(wna_climate) +
  tm_symbols(col = "Jul_Tmp", palette = "-RdBu", alpha = 0.75,
             style = "cont", title.col = "degC")
── tmap v3 code detected ───────────────────────────────────────────────────────
[v3->v4] `symbols()`: instead of `style = "cont"`, use fill.scale =
`tm_scale_continuous()`.
ℹ Migrate the argument(s) 'palette' (rename to 'values') to
  'tm_scale_continuous(<HERE>)'
[v3->v4] `symbols()`: use 'fill' for the fill color of polygons/symbols
(instead of 'col'), and 'col' for the outlines (instead of 'border.col').
[v3->v4] `symbols()`: use `fill_alpha` instead of `alpha`.
[v3->v4] `symbols()`: migrate the argument(s) related to the legend of the
visual variable `fill` namely 'title.col' (rename to 'title') to 'fill.legend =
tm_legend(<HERE>)'
Multiple palettes called "rd_bu" found: "brewer.rd_bu", "matplotlib.rd_bu". The first one, "brewer.rd_bu", is returned.

[cols4all] color palettes: use palettes from the R package cols4all. Run
`cols4all::c4a_gui()` to explore them. The old palette name "-RdBu" is named
"rd_bu" (in long format "brewer.rd_bu")

Climate Map

m1 = tm_shape(countries50, bbox = st_bbox(wna_climate)) +
  tm_borders() +
  tm_shape(rivers50) +
  tm_lines("lightblue", lwd = 2) +
  tm_shape(wna_climate) +
  tm_symbols(col = "Jul_Tmp", palette = "-RdBu", alpha = 0.75,
             style = "cont", title.col = "degC")
── tmap v3 code detected ───────────────────────────────────────────────────────
[v3->v4] `symbols()`: instead of `style = "cont"`, use fill.scale =
`tm_scale_continuous()`.
ℹ Migrate the argument(s) 'palette' (rename to 'values') to
  'tm_scale_continuous(<HERE>)'
[v3->v4] `symbols()`: use `fill_alpha` instead of `alpha`.
[v3->v4] `symbols()`: migrate the argument(s) related to the legend of the
visual variable `fill` namely 'title.col' (rename to 'title') to 'fill.legend =
tm_legend(<HERE>)'
m2 = tm_shape(countries50, bbox = st_bbox(wna_climate)) +
  tm_borders() +
  tm_shape(rivers50) +
  tm_lines("lightblue", lwd = 2) +
  tm_shape(wna_climate) +
  tm_symbols(col = "annp", palette = "BuPu", alpha = 0.75,
             style = "cont", title.col = "mm/yr")
[v3->v4] `symbols()`: use `fill_alpha` instead of `alpha`.
[v3->v4] `symbols()`: migrate the argument(s) related to the legend of the
visual variable `fill` namely 'title.col' (rename to 'title') to 'fill.legend =
tm_legend(<HERE>)'
tmap_arrange(m1, m2)
Multiple palettes called "rd_bu" found: "brewer.rd_bu", "matplotlib.rd_bu". The first one, "brewer.rd_bu", is returned.
[cols4all] color palettes: use palettes from the R package cols4all. Run
`cols4all::c4a_gui()` to explore them. The old palette name "-RdBu" is named
"rd_bu" (in long format "brewer.rd_bu")
[cols4all] color palettes: use palettes from the R package cols4all. Run
`cols4all::c4a_gui()` to explore them. The old palette name "BuPu" is named
"brewer.bu_pu"
Multiple palettes called "bu_pu" found: "brewer.bu_pu", "matplotlib.bu_pu". The first one, "brewer.bu_pu", is returned.

Example 3: Raster images

Load the Data

## Landsat images for California
filenames <- paste0('E:/Summer 2026/data/rs/LC08_044034_20170614_B', 1:11, ".tif")
landsat <- rast(filenames)
## Places for California
ca_places <- st_read("E:/Summer 2026/data/ca_places/ca_places.shp", quiet = TRUE)

NDVI Layer

b4 <- landsat[[4]]
b5 <- landsat[[5]]

ndvi <- (b5 - b4) / (b5 + b4)

plot(ndvi, col=rev(terrain.colors(10)), main = "NDVI")

tm_shape(ndvi) +
  tm_raster()
[`tm_scale_intervals()`] Variable(s) "col" contains positive and negative
values, so midpoint is set to 0. Set midpoint = NA to show the full range of
visual values.
This message is displayed once per session.

Overlay

tm_shape(clamp(ndvi, lower = 0)) +
  tm_raster(palette = "Greens", style = "cont", title = "NDVI") +
  tm_shape(ca_places) +
  tm_borders(lwd = 2) +
  tm_layout(main.title = "Landsat 8 (2017/06/14)", 
            legend.position = c("left", "top"), 
            legend.bg.color = "white", legend.bg.alpha = 0.7)
── tmap v3 code detected ───────────────────────────────────────────────────────
[v3->v4] `tm_raster()`: instead of `style = "cont"`, use col.scale =
`tm_scale_continuous()`.
ℹ Migrate the argument(s) 'palette' (rename to 'values') to
  'tm_scale_continuous(<HERE>)'
[v3->v4] `tm_raster()`: migrate the argument(s) related to the legend of the
visual variable `col` namely 'title' to 'col.legend = tm_legend(<HERE>)'
[v3->v4] `tm_layout()`: use `tm_title()` instead of `tm_layout(main.title = )`
[cols4all] color palettes: use palettes from the R package cols4all. Run
`cols4all::c4a_gui()` to explore them. The old palette name "Greens" is named
"brewer.greens"

Color Composite

landsat_rgb <- landsat[[c("LC08_044034_20170614_B4", "LC08_044034_20170614_B3", "LC08_044034_20170614_B2")]]

tm_shape(landsat_rgb) +
  tm_rgb(r = 1, g = 2, b = 3, max.value = 1)
── tmap v3 code detected ───────────────────────────────────────────────────────
[v3->v4] `tm_rgb()`: use `col.scale = tm_scale_rgb(max_color_value = 1)`
instead of `max.value = 1`
Warning in blend_grobs(grb, a$blend, dst = dst): Compositing operator '3' is
not supported by the current graphics device. Falling back to no blending. Try
png(type = "cairo") or svg().

Stretch

landsat_stretch = stretch(landsat_rgb, minv = 0, maxv = 1, 
                          minq = 0.01, maxq = 0.99)

tm_shape(landsat_stretch) + 
  tm_rgb(r = 1, g = 2, b = 3, max.value = 1)
── tmap v3 code detected ───────────────────────────────────────────────────────
[v3->v4] `tm_rgb()`: use `col.scale = tm_scale_rgb(max_color_value = 1)`
instead of `max.value = 1`
Warning in blend_grobs(grb, a$blend, dst = dst): Compositing operator '3' is
not supported by the current graphics device. Falling back to no blending. Try
png(type = "cairo") or svg().

Leaflet

OpenStreetMap

m <- leaflet() %>%
  addTiles() %>%  # Add default OpenStreetMap map tiles
  addMarkers(lng=-111.8421, lat=40.7649, popup="The University of Utah")
m  # Print the map

##ESRI

m <- leaflet() %>%
  addProviderTiles("Esri.WorldStreetMap") %>%  
  addMarkers(lng=-111.8421, lat=40.7649, popup="The University of Utah")
m

Imagery

m <- leaflet() %>%
  addProviderTiles("Esri.WorldImagery") %>%  # Add default OpenStreetMap map tiles
  addMarkers(lng=-111.8421, lat=40.7649, popup="The University of Utah")
m

Reproject

tracts_ll <- st_transform(tracts, crs = 4326)
stations_ll <- st_transform(stations, crs = 4326)
lightrail_ll <- st_transform(lightrail, crs = 4326)

Map Construction

leaflet() %>%
  # add a dark basemap
  addProviderTiles("CartoDB.DarkMatter") %>%
  # add the polygons of the clusters
  addPolygons(
    data = tracts_ll,
    color = "#E2E2E2",
    opacity = 1, # set the opacity of the outline
    weight = 1, # set the stroke width in pixels
    fillOpacity = 0.2 # set the fill opacity
  ) %>%
  addPolylines(
    data = lightrail_ll
  ) %>%
  addMarkers(
    data = stations_ll,
    label = ~htmlEscape(STATIONNAM)
  )

Details

unique(stations_ll$LINENAME)
[1] "University"            "N/S TRAX"              "Med Center"           
[4] "Intermodal Hub"        "Airport"               "Draper"               
[7] "Mid-Jordan"            "West Valley"           "Sugar House Streetcar"
line_pal <- RColorBrewer::brewer.pal(9, "Set1")
line_no <- as.numeric(as.factor(stations_ll$LINENAME))

stations_ll$LINECOL <- line_pal[line_no]

Station Pop-ups

station_popup = paste0(
  "<b>Station: </b>",
  stations_ll$STATIONNAM,
  "<br>",
  "<b>Line Name: </b>",
  stations_ll$LINENAME,
  "<br>",
  "<b>Park n Ride: </b>",
  stations_ll$PARKNRIDE,
  "<br>",
  "<b>Address: </b>",
  stations_ll$ADDRESS
  
)
leaflet() %>%
  # add a dark basemap
  addProviderTiles("Esri.WorldStreetMap") %>%
  # add the polygons of the clusters
  addPolygons(
    data = tracts_ll,
    color = "#E2E2E2",
    # set the opacity of the outline
    opacity = 1,
    # set the stroke width in pixels
    weight = 1,
    # set the fill opacity
    fillOpacity = 0.2
  ) %>%
  addPolylines(
    data = lightrail_ll
  ) %>%
  addCircleMarkers(
    data = stations_ll,
    color = ~LINECOL,
    label = ~htmlEscape(STATIONNAM),
    popup = station_popup
  )

DISCLAIMER:

ChatGPT was used during the process of writing the code for the purpose of debugging and fixing errors in the code.