Chapter 9: Making maps with R

Key Learnings from, and Solutions to the exercises in Chapter 8 of the book Geocomputation with R by Robin Lovelace, Jakub Nowosad and Jannes Muenchow.

Geocomputation with R
Textbook Solutions
Author

Aditya Dahiya

Published

March 2, 2025

In this chapter, I use {tmap} (Tennekes 2018a), but I also use {ggplot2} (Wickham 2016) to produce equivalent maps, as produced by {tmap} (Tennekes 2018b) in the textbook. In addition, I use {cols4all} (Tennekes and Puts 2023) palettes for colour and fill scales.

Using pacman for quick loading and updating of packages.

Code
pacman::p_load(
  sf,          # Simple Features in R
  terra,       # Handling rasters in R
  tidyterra,   # For plotting rasters in ggplot2
  tidyverse,   # All things tidy; Data Wrangling
  magrittr,    # Using pipes with raster objects
  
  spData,      # Spatial Datasets
  spDataLarge, # Large Spatial Datasets
  
  patchwork,   # Composing plots
  gt,          # Display GT tables with R
  
  tmap,        # Using {tmap} for maps
  cols4all     # Colour Palettes
)

# nz_elev = rast(system.file("raster/nz_elev.tif", package = "spDataLarge"))

# install.packages("spDataLarge", repos = "https://geocompr.r-universe.dev")

9.1 Introduction

  • Cartography is a crucial aspect of geographic research, blending communication, detail, and creativity.
  • Static maps in R can be created using the plot() function, but advanced cartography benefits from dedicated packages.
  • The chapter focuses in-depth on the tmap package rather than multiple tools superficially.
  • Some example colour palettes to use in maps is shown below in Table 1.
Code
# install.packages("cols4all")

# A nice way to pick colour palettes for maps etc.
# cols4all::c4a_gui()
# pacman::p_load(cols4all)

cols4all::c4a_overview() |> 
  as_tibble() |>
  pivot_longer(
    cols = -c(1, 2),
    names_to = "type",
    values_to = "value"
  ) |> 
  left_join(cols4all::c4a_types() |> rename(val_name = description)) |> 
  select(-type) |> 
  pivot_wider(
    id_cols = c(1, 2),
    names_from = val_name,
    values_from = value
  ) |> 
  gt::gt() |> 
  gt::tab_style(
    style = cell_text(font = "monospace", weight = "bold"),
    locations = cells_body(columns = "series")
  ) |> 
  gtExtras::gt_theme_espn() |> 
  gt::opt_interactive(
    page_size_default = 5
  ) |> 
  gt::cols_label_with(fn = snakecase::to_title_case)
Table 1: Avaialable palettes in

9.2 Static maps

  • Most common type of geo-computation output, stored as .png (raster) and .pdf (vector).
  • Base R’s plot() is the fastest way to create static maps from sf or terra, ideal for quick visual checks.
  • tmap package offers:
    • Simple, ggplot2-like syntax.
    • Static and interactive maps with tmap_mode().
    • Support for multiple spatial classes (sf, terra).

9.2.1 tmap basics

  • Grammar of graphics: Like ggplot2, tmap follows a structured approach, separating input data from aesthetics (visual properties). Example, shown in Figure 1 .
  • Basic structure: Uses tm_shape() to define the input dataset (vector or raster), followed by layer elements like tm_fill() and tm_borders().
  • Layering approach:
    • tm_fill(): Fills (multi)polygon areas.
    • tm_borders(): Adds border outlines to (multi)polygons.
    • tm_polygons(): Combines fill and border.
    • tm_lines(): Draws lines for (multi)linestrings.
    • tm_symbols(): Adds symbols for points, lines, and polygons.
    • tm_raster(): Displays raster data.
    • tm_rgb(): Handles multi-layer rasters.
    • tm_text(): Adds text labels.
  • Layering operator: The + operator is used to add multiple layers.
  • Quick maps: qtm() provides a fast way to generate thematic maps (qtm(nz)tm_shape(nz) + tm_fill() + tm_borders()).
    • Limitations of qtm(): Less control over aesthetics, so not covered in detail in this chapter.
Code
g1 <- nz |> 
  ggplot() +
  geom_sf(
    colour = "grey",
    fill = "grey"
  ) +
  ggthemes::theme_map() +
  theme(
    panel.background = element_rect()
  )


g2 <- nz |> 
  ggplot() +
  geom_sf(
    colour = "black",
    fill = "white"
  ) +
  ggthemes::theme_map() +
  theme(
    panel.background = element_rect()
  )


g3 <- nz |> 
  ggplot() +
  geom_sf(
    colour = "black",
    fill = "grey"
  ) +
  ggthemes::theme_map() +
  theme(
    panel.background = element_rect()
  )

g <- g1 + g2 + g3 +
  plot_annotation(
    title = "Using `colour` and `fill` arguments in geom_sf() to\nachieve same results as {tmap} with {ggplot2}",
    theme = theme(
      plot.title = element_text(
        hjust = 0.5,
        lineheight = 0.9
      )
    )
  )

ggsave(
  filename = here::here("book_solutions", "images", "chapter9-2-1.png"),
  plot = g,
  height = 1000,
  width  = 2000,
  units = "px"
)
Figure 1

9.2.2 Map objects

  • {tmap} allows storing maps as objects, enabling modifications and layer additions.
  • Use tm_polygons() to create a map object, combining tm_fill() and tm_borders().
  • Stored maps can be plotted later by simply calling the object.
  • Additional layers are added using + tm_shape(new_obj), where new_obj represents a new spatial object.
  • Aesthetic functions apply to the most recently added shape until another is introduced.
  • Spatial objects can be manipulated with sf, e.g., st_union(), st_buffer(), and st_cast().
  • Multiple layers can be added, such as:
    • Raster elevation (tm_raster())
    • Territorial waters (tm_lines())
    • High points (tm_symbols())
  • tmap_arrange() combines multiple tmap objects into a single visualization.
  • The + operator adds layers, but aesthetics are controlled within layer functions.
Code
g1 <- ggplot() +
  geom_spatraster(
    data = nz_elev,
    alpha = 0.5
  ) +
  geom_sf(
    data = nz,
    fill = NA
  ) +
  scale_fill_stepsn(
    colours = c4a("brewer.blues", n = 5),
    name = "Elevation (metres)",
    na.value = "transparent"
  ) +
  ggthemes::theme_map() +
  theme(
    legend.position = "bottom",
    panel.background = element_rect()
  )


g2 <- g1 + 
  geom_sf(
    data = st_buffer(
      st_union(nz), 22200
    ),
    fill = NA,
    colour = "black"
  )

g3 <- g2 +
  geom_sf(
    data = nz_height,
    size = 4,
    colour = "grey10",
    fill = "grey",
    pch = 21
  )


g <- g1 + g2 + g3 +
  plot_layout(
    guides = "collect"
  ) +
  plot_annotation(
    title = "Using added geom_sf() and st_buffer() to\nachieve same results as {tmap} with {ggplot2}",
    theme = theme(
      plot.title = element_text(
        hjust = 0.5,
        lineheight = 0.9
      )
    )
  ) &
  theme(
    legend.position = "bottom",
    legend.direction = "horizontal",
    legend.margin = margin(0,0,0,0, "pt"),
    legend.key.width = unit(50, "pt")
  )

ggsave(
  filename = here::here("book_solutions", "images", "chapter9-2-2.png"),
  plot = g,
  height = 1000,
  width  = 2000,
  units = "px"
)
Figure 2

9.2.3 Visual variables

  • Default aesthetics in tmap:
    • tm_fill() and tm_symbols() use gray shades.
    • tm_lines() uses a continuous black line.
    • Defaults can be overridden for customization.
  • Types of map aesthetics:
    • Variable-dependent aesthetics (change with data).
    • Fixed aesthetics (constant values).
  • Key aesthetic arguments in tmap:
    • fill: Polygon fill color.
    • col: Border, line, point, or raster color.
    • lwd: Line width.
    • lty: Line type.
    • size: Symbol size.
    • shape: Symbol shape.
    • fill_alpha, col_alpha: Transparency for fill and border.
  • Applying aesthetics:
    • Use a column name to map a variable. Pass a character string referring to a column name.
    • Use a fixed value for constant aesthetics.
  • Additional arguments for visual variables:
    • .scale: Controls representation on the map and legend.
    • .legend: Customizes legend settings.
    • .free: Defines whether each facet uses the same or different scales.
Code
g1 <- ggplot() +
  geom_sf(
    data = nz,
    mapping = aes(fill = Land_area),
    colour = "transparent"
  ) +
  scale_fill_stepsn(
    colors = c4a(palette = "brewer.blues", type = "seq"),
    name = "Land Area"
  ) +
  ggthemes::theme_map() +
  theme(
    legend.position = "inside",
    legend.position.inside = c(0.9, 0.1),
    legend.justification = c(1, 0),
    panel.background = element_rect()
  )

g2 <- ggplot() +
  geom_sf(
    data = nz,
    mapping = aes(fill = Land_area),
    colour = "black"
  ) +
  ggthemes::theme_map() +
  theme(
    legend.position = "inside",
    legend.position.inside = c(0.9, 0.1),
    legend.justification = c(1, 0),
    panel.background = element_rect()
  ) +
  scale_fill_viridis_b(option = "C")

# If we want to replicate the {tmap} style bin labels, wiht {ggplot2},
# some manual code in required (Credits: Grok3)
# Load the New Zealand data
nz <- spData::nz

# Define bin width
bin_width <- 10000

# Determine breaks based on the data range
breaks <- seq(
  from = floor(min(nz$Land_area) / bin_width) * bin_width, 
  to = ceiling(max(nz$Land_area) / bin_width) * bin_width, 
  by = bin_width
  )

# Create labels for the bins
labels <- paste0(
  format(breaks[-length(breaks)], big.mark = ","), 
  " - ", 
  format(breaks[-1] - 1, big.mark = ",")
  )

# Bin the land area data
nz <- nz |> 
  mutate(
    binned_land_area = cut(
      nz$Land_area, 
      breaks = breaks, 
      labels = labels, 
      include.lowest = TRUE
    )
  )

# Generate colors for the bins
n_bins <- length(levels(nz$binned_land_area))
mypal <- cols4all::c4a(palette = "brewer.blues", n = n_bins)

g3 <- ggplot() +
  geom_sf(
    data = nz,
    mapping = aes(fill = binned_land_area),
    colour = "transparent"
  ) +
  scale_fill_manual(
    values = mypal,
    name = "Land Area"
  ) +
  ggthemes::theme_map() +
  theme(
    legend.position = "inside",
    legend.position.inside = c(0.9, 0.1),
    legend.justification = c(1, 0),
    panel.background = element_rect(),
    legend.margin = margin(0,0,0,0, "pt"),
    legend.key = element_rect(
      colour = NA
    ),
    legend.text = element_text(
      hjust = 0
    )
  )



g <- g1 + g2 + g3 + 
  plot_annotation(
    tag_levels = "I",
    title = "Using scale_fill_stepsn() & scale_fill_viridis_b() to\nachieve same results as {tmap} with {ggplot2}",
    theme = theme(
      plot.title = element_text(
        hjust = 0.5,
        lineheight = 0.9,
        size = 20
      )
    )
  ) &
  theme(
    plot.tag.location = "panel",
    plot.tag.position = c(0.1, 0.9),
    plot.tag = element_text(
      face = "bold",
      size = 20
    )
  )

ggsave(
  filename = here::here("book_solutions", "images", "chapter9-2-3.png"),
  plot = g,
  height = 1800,
  width  = 4000,
  units = "px"
)
Figure 3

9.2.4 Scales

  • Scales define how values are visually represented in maps and legends, depending on the selected visual variable (e.g., fill.scale, col.scale, size.scale).
  • Default scale is tm_scale(), which auto-selects settings based on input data type (factor, numeric, integer).
  • Colour settings impact spatial variability; customization options include:
    • breaks: manually set classification thresholds.
    • n: define the number of bins.
    • values: assign colour schemes (e.g., "BuGn").
  • Family of scale functions in tmap:
  • Colour palettes are key for readability and should be carefully chosen:
  • Three main colour palette types:
    • Categorical: distinct colours for unordered categories (e.g., land cover classes).
    • Sequential: gradient from light to dark, for continuous numeric variables.
    • Diverging: two sequential palettes meeting at a neutral reference point (e.g., temperature anomalies).
  • Key considerations for colour choices:
    • Perceptibility: colours should match common associations (e.g., blue for water, green for vegetation).
    • Accessibility: use colour-blind-friendly palettes where possible.
Use of classInt::classify_intervals() for Binned Data in Maps

The classify_intervals() function from the classInt package in R is a powerful tool for visualizing continuous data in maps, such as choropleth maps. It assigns values of a continuous variable—like population density or income levels—to discrete intervals based on break points calculated by methods like Jenks or quantiles using classIntervals(). This classification enables the data to be paired with a discrete color scale, simplifying the interpretation of spatial patterns and variations across regions. For instance, after determining breaks with classIntervals(), classify_intervals() can categorize each region’s value into a bin, producing a factor suitable for plotting with libraries like ggplot2 or tmap, enhancing map readability with clear legend ranges (e.g., “10,000 - 20,000”).

Available Styles in classIntervals() and Their Uses

Below is a table summarizing the classification styles available in classIntervals() and their practical applications:

Style Description Use Case
fixed Uses user-defined, fixed break points. Custom intervals, such as policy-driven thresholds.
equal Splits the data range into equal-width intervals. Uniformly distributed data or when equal ranges are significant.
pretty Rounds breaks to “nice” numbers for readability. Visually appealing breaks for general audience maps.
quantile Ensures each interval has roughly equal observation counts. Skewed data distributions to show spread effectively.
jenks Minimizes within-class variance, maximizes between-class variance. Identifying natural clusters or groupings in the data.
hclust Uses hierarchical clustering for break points. Exploring hierarchical data structures or groupings.
kmeans Applies k-means clustering to define breaks. Data with distinct clusters needing clear separation.
sd Sets breaks based on standard deviations from the mean. Normally distributed data to highlight deviations.
bclust Employs bagged clustering for break determination. Noisy data requiring robust, stable classification.
fisher Optimizes variance within classes, similar to Jenks. Alternative to Jenks for natural breaks classification.
Code
# classInt::classify_intervals(nz$Median_income)

# classInt::classIntervals(nz$Median_income, style = "jenks")

custom_plot <- function(my_style = "pretty"){
  nz |> 
    mutate(
      median_income_binned = classInt::classify_intervals(
        Median_income,
        n = 5,
        style = my_style
      )
    ) |> 
    ggplot(aes(fill = median_income_binned)) +
    geom_sf() +
    scale_fill_manual(
      values = c4a("brewer.blues", n = 6)[2:6]
    ) +
    labs(
      title = paste0("style = `", my_style, "`"),
      fill = "Median Income (NZ $)"
    ) +
    ggthemes::theme_map()
}

g1 <- nz |> 
    mutate(
      median_income_binned = classInt::classify_intervals(
        Median_income,
        style = "pretty"
      )
    ) |> 
    ggplot(aes(fill = median_income_binned)) +
    geom_sf() +
    scale_fill_manual(
      values = c4a("brewer.blues", n = 6)
    ) +
    labs(
      title = paste0("style = `pretty`"),
      fill = "Median Income (NZ $)"
    ) +
    ggthemes::theme_map()

g2 <- custom_plot("equal")
g3 <- custom_plot("quantile")
g4 <- custom_plot("jenks")

g5 <- nz |> 
  ggplot(aes(fill = Population)) +
  geom_sf() +
  scale_fill_stepsn(
    colours = c4a("brewer.bu_pu", 3),
    transform = "log10",
    breaks = c(1e4, 1e5, 1e6, 1e7),
    limits = c(1e4, 1e7),
    labels = scales::label_number(big.mark = ",")
  ) +
  labs(
    title = paste0("style = `log10_pretty`"),
    fill = "Population"
  ) +
  ggthemes::theme_map()

g <- g1 + g2 + g3 + g4 + g5 +
  plot_layout(nrow = 2, ncol = 3) +
  plot_annotation(
    tag_levels = "I",
    title = "Using {ggplot2} + {cols4all} + {classInt} to replicate\n{tmap}'s tm_scale_intervals() function's style = `` argument",
    theme = theme(
      plot.title = element_text(
        hjust = 0.5,
        lineheight = 0.9,
        size = 20
      )
    )
  ) &
  theme(
    plot.tag.location = "panel",
    plot.tag.position = c(0.1, 0.9),
    plot.tag = element_text(
      face = "bold",
      size = 20
    ),
    plot.title = element_text(
      size = 20,
      margin = margin(30,0,-15,0, "pt")
    ),
    legend.position = "inside",
      legend.position.inside = c(1, 0),
      legend.justification = c(1, 0),
      legend.margin = margin(0,0,0,0, "pt"),
      legend.key = element_rect(
        colour = NA
      ),
      legend.text = element_text(
        hjust = 0
      )
  )

ggsave(
  filename = here::here("book_solutions", "images", "chapter9-2-4.png"),
  plot = g,
  height = 3500,
  width  = 3500,
  units = "px"
)
Figure 4

References

Tennekes, Martijn. 2018b. Tmap: Thematic Maps in r 84. https://doi.org/10.18637/jss.v084.i06.
———. 2018a. Tmap: Thematic Maps in r 84. https://doi.org/10.18637/jss.v084.i06.
Tennekes, Martijn, and Marco J. H. Puts. 2023. Cols4all: A Color Palette Analysis Tool.” EuroVis (Short Papers). https://doi.org/10.2312/evs.20231040.
Wickham, Hadley. 2016. “Ggplot2: Elegant Graphics for Data Analysis.” https://ggplot2.tidyverse.org.