Required packages

library(tmap)
library(sf)
library(dplyr)
library(cols4all)

tmap_mode("view")   # all maps in this exercise set are interactive

data("World"); data("World_rivers"); data("metro")
data("NLD_prov"); data("NLD_muni"); data("NLD_dist")

Datasets used — all from the tmap package, no downloads needed.

Object Rows Type Key variables
World 177 polygon continent, gdp_cap_est, life_exp, well_being, pop_est_dens, economy
World_rivers 1511 line scalerank
metro 436 point pop1960pop2030, name
NLD_prov 12 polygon code, name
NLD_muni 345 polygon population, dwelling_value, dwelling_ownership, employment_rate, income_low, income_high, edu_appl_sci, urbanity, province
NLD_dist 3340 polygon same variables as NLD_muni, district level

Exercise 1 — World data

1.1 — First interactive map

  1. Type ?World to find out what variables it contains. Make a (static) map of World, where the polygons have white border lines (line width 2) and are filled with purple.
tmap_mode("plot")

tm_shape(World) +
  tm_polygons(fill = "purple", lwd = 2, col = "white")
  1. Switch to "view" mode and reproduce the map. Pan, zoom, and click a country — what happens?
tmap_mode("view")

tm_shape(World) +
  tm_polygons(fill = "purple", lwd = 2, col = "white")

Clicking a country opens a popup showing all of its attribute values.

  1. Create an interactive choropleth of life expectancy by setting fill to the variable name "life_exp". Experiment with country border lines — which colour and line width work well?
tm_shape(World) +
  tm_polygons(fill = "life_exp", col = "white", lwd = 0.5)

Thin white borders (lwd = 0.5) separate countries without competing with the fill colour.

  1. Add tm_crs("auto") to apply an equal-area projection, and tm_basemap(NULL) to disable the basemap. Compared to the map in (c), which do you prefer and why?
tm_shape(World) +
  tm_polygons(fill = "life_exp", col = "white", lwd = 0.5) +
  tm_crs("auto") +
  tm_basemap(NULL)

The equal-area projection avoids the size distortion of Web Mercator (where Greenland appears as large as Africa). However, it loses the familiar basemap context. Preference is subjective — equal-area is more honest for comparing regions by size; Web Mercator is more recognisable.


1.2 — Stacking layers

  1. Build a map with three layers: World (grey fill, no borders), World_rivers (blue lines), and metro (gold bubbles). Click the layer control icon (top-left in the viewer) to toggle layers on and off. Experiment with line thickness and bubble size.
tm_shape(World) +
  tm_fill(fill = "grey90") +
tm_shape(World_rivers) +
  tm_lines(col = "steelblue", lwd = 2) +
tm_shape(metro) +
  tm_bubbles(fill = "gold", size = 0.5)
  1. The World_rivers dataset contains a variable "strokelwd" with pre-computed line widths. Assign it to lwd and set lwd.scale = tm_scale_asis() so the values are used directly rather than treated as a data variable.
tm_shape(World) +
  tm_fill(fill = "grey90") +
tm_shape(World_rivers) +
  tm_lines(col = "steelblue", lwd = "strokelwd", lwd.scale = tm_scale_asis()) +
tm_shape(metro) +
  tm_bubbles(fill = "gold", size = 0.5)

tm_scale_asis() passes the values straight to the renderer with no rescaling and no legend entry.

  1. The metro dataset contains population estimates per city, e.g. "pop2020". Assign this to size in tm_symbols().
tm_shape(World) +
  tm_fill(fill = "grey90") +
tm_shape(World_rivers) +
  tm_lines(col = "steelblue", lwd = "strokelwd", lwd.scale = tm_scale_asis()) +
tm_shape(metro) +
  tm_symbols(fill = "gold", size = "pop2020")
  1. Give each layer a meaningful name using the group argument in the layer function (e.g. tm_fill(..., group = "Land")). Check that the names appear correctly in the layer control.
tm_shape(World) +
  tm_fill(fill = "grey90", group = "Land") +
tm_shape(World_rivers) +
  tm_lines(col = "steelblue", lwd = "strokelwd",
           lwd.scale = tm_scale_asis(), group = "Rivers") +
tm_shape(metro) +
  tm_symbols(fill = "gold", size = "pop2020", group = "Cities")

Exercise 2 — Netherlands data

2.1 — Three scales

  1. Load NLD_prov, NLD_muni, and NLD_dist. Make three separate interactive maps — one per dataset — each filled with "steelblue". Zoom in and out on each to appreciate the resolution difference.
tm_shape(NLD_prov) +
  tm_polygons(fill = "steelblue")
tm_shape(NLD_muni) +
  tm_polygons(fill = "steelblue")
tm_shape(NLD_dist) +
  tm_polygons(fill = "steelblue")
  1. Which are the richest districts? Create a choropleth of "dwelling_value" at district level.
tm_shape(NLD_dist) +
  tm_polygons(fill = "dwelling_value")
  1. Improve this map by adding thick province borders and less-thick municipality borders as additional layers, and removing the district borders (col = NULL). Which border thicknesses and colours work well?
tm_shape(NLD_dist) +
  tm_polygons(fill = "dwelling_value", col = NULL) +
tm_shape(NLD_muni) +
  tm_borders(lwd = 1, col = "black") +
tm_shape(NLD_prov) +
  tm_borders(lwd = 2, col = "black")

Removing district borders (col = NULL) reduces visual noise across 3340 polygons. Province borders (thicker) provide orientation; municipality borders (thinner) add detail when zoomed in.

  1. Compare tm_scale_intervals() and tm_scale_continuous_pseudo_log(). Make one map with each and explore their arguments (e.g. style and n for the interval scale, or manual breaks). Which do you prefer and why?
tm_shape(NLD_dist) +
  tm_polygons(
    fill = "dwelling_value",
    col = NULL,
    fill.scale = tm_scale_intervals(style = "kmeans", n = 5)) +
tm_shape(NLD_muni) + tm_borders(lwd = 1) +
tm_shape(NLD_prov) + tm_borders(lwd = 2)
tm_shape(NLD_dist) +
  tm_polygons(
    fill = "dwelling_value",
    col = NULL,
    fill.scale = tm_scale_continuous_pseudo_log()) +
tm_shape(NLD_muni) + tm_borders(lwd = 1) +
tm_shape(NLD_prov) + tm_borders(lwd = 2)

The pseudo-log scale stretches the lower end of the distribution, revealing variation among the many lower-value districts that interval classes might merge into a single bin. For a right-skewed variable like property values, this is often more informative. Manual breaks are also possible: tm_scale_intervals(breaks = c(75, 150, 250, 500, 750, Inf)).

  1. Explore the sequential and diverging colour palettes in cols4all:
c4a_gui()
  1. Apply your preferred palette to the map from (d) by setting values = "<palette name>" inside the scale function.
tm_shape(NLD_dist) +
  tm_polygons(
    fill = "dwelling_value",
    col = NULL,
    fill.scale = tm_scale_intervals(style = "kmeans", n = 5,
                                    values = "plasma")) +
tm_shape(NLD_muni) + tm_borders(lwd = 1) +
tm_shape(NLD_prov) + tm_borders(lwd = 2)
  1. Change the legend title to something meaningful (e.g. "WOZ value (×€1 000)") and move the legend to the top-right corner. Do this with the fill.legend argument: tm_legend(title = "...", position = c("right", "top")).
tm_shape(NLD_dist) +
  tm_polygons(
    fill = "dwelling_value",
    col = NULL,
    fill.scale = tm_scale_intervals(style = "kmeans", n = 5,
                                    values = "plasma"),
    fill.legend = tm_legend(title = "WOZ value (×€1 000)",
                            position = c("right", "top"))) +
tm_shape(NLD_muni) + tm_borders(lwd = 1) +
tm_shape(NLD_prov) + tm_borders(lwd = 2)
  1. Add a scale bar using tm_scalebar().
tm_shape(NLD_dist) +
  tm_polygons(
    fill = "dwelling_value",
    col = NULL,
    fill.scale = tm_scale_intervals(style = "kmeans", n = 5,
                                    values = "plasma"),
    fill.legend = tm_legend(title = "WOZ value (×€1 000)",
                            position = c("right", "top"))) +
tm_shape(NLD_muni) + tm_borders(lwd = 1) +
tm_shape(NLD_prov) + tm_borders(lwd = 2) +
tm_scalebar()
  1. Add a basemap of your choice. Browse the options at https://leaflet-extras.github.io/leaflet-providers/preview/ and add your chosen basemap with tm_basemap("<provider name>") at the top of the map. Which basemap complements the choropleth best?
tm_basemap("CartoDB.Positron") +
tm_shape(NLD_dist) +
  tm_polygons(
    fill = "dwelling_value",
    col = NULL,
    fill.scale = tm_scale_intervals(style = "kmeans", n = 5,
                                    values = "plasma"),
    fill.legend = tm_legend(title = "WOZ value (×€1 000)",
                            position = c("right", "top"))) +
tm_shape(NLD_muni) + tm_borders(lwd = 1) +
tm_shape(NLD_prov) + tm_borders(lwd = 2) +
tm_scalebar()

Light neutral basemaps like CartoDB.Positron keep the choropleth colours prominent. Satellite imagery (Esri.WorldImagery) works well when geographic context matters, but can compete with the fill colours.