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. I am 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

9.2.5 Legends

  • Use tm_legend() to customize map legends in tmap, including:
    • title: sets a custom legend title (e.g., using expression() for formatting units or superscripts).
    • orientation: choose between "portrait" (default) or "landscape".
    • position: defines legend location using helper functions.
  • Customize legend placement with:
    • tm_pos_out(): places legend outside the map frame; accepts "left", "center", "right" (horizontal) and "bottom", "center", "top" (vertical).
    • tm_pos_in(): places legend inside the map frame using similar position arguments or a numeric vector (between 0 and 1).
Code
# nz <- nz
# cols4all::c4a_gui()

g <- nz |> 
  ggplot() +
  geom_sf(aes(fill = Land_area), colour = "white") +
  scale_fill_gradientn(
    colours = c4a("carto.purp_or"),
    labels = scales::label_number(big.mark = ",")
  ) +
  labs(
    fill = expression("Area (km"^2*")"),
    title = "Using expression() to get\nsuper-scripts in legend title"
  ) +
  ggthemes::theme_map() +
  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(
      hjust = 0.5,
      margin = margin(0,0,0,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-5.png"),
  plot = g,
  height = 1200,
  width  = 1000,
  units = "px"
)
Figure 5

9.2.6 Layouts

  • A map layout combines multiple visual elements—such as data layers, grids, scale bars, titles, and margins—into a coherent design.
  • Visual appeal and interpretability can be significantly influenced by both color settings (palette and breakpoints) and layout elements.
  • Key layout-enhancing functions in the tmap package include:
    • tm_graticules(): adds graticule lines (latitude/longitude grid).
    • tm_compass(): inserts a north arrow, with customizable styles and positioning.
    • tm_scalebar(): adds a scale bar with user-defined breaks and text size.
    • tm_title(): adds a title to the map.
  • The main function for layout customization is tm_layout(), which controls numerous settings such as:
    • scale (e.g., magnifying all map elements),
    • bg.color (e.g., background color),
    • frame (e.g., toggling the frame around the map).
  • Refer to args(tm_layout) or ?tm_layout for the full list of available options.
Code
g <- nz |> 
  ggplot() +
  geom_sf(
    alpha = 0.75,
    linewidth = 0.5
  ) +
  
  # Adding lines every 2 degrees of latitude and longitude
  scale_x_continuous(
    breaks = seq(162, 180, 2)
  ) +
  scale_y_continuous(
    breaks = seq(-32, -48, -2)
  ) +
  ggspatial::annotation_north_arrow(
    style = ggspatial::north_arrow_nautical(),
    location = "topleft", 
    
  ) +
  ggspatial::annotation_scale(
    location = "tl",
    width_hint = 0.3,
    style  = "bar",
    pad_y = unit(60, "pt")
  ) +
  labs(
    title = "Replicating {tmap}'s annotations in {ggplot2}\nwith {ggspatial}"
  ) +
  
  coord_sf(
    default_crs = "EPSG:4326"
  ) +
  theme_bw() +
  theme(
    panel.grid = element_line(
      colour = alpha("grey20", 0.7),
      linewidth = 0.1
    )
  )

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

9.2.7 Faceted Maps

  • Faceted maps, or small multiples, display multiple maps side-by-side (or stacked) to show spatial changes across a variable like time.

  • Useful for visualizing temporal evolution — e.g., urban population growth over decades — with each panel representing a different time slice.

  • Although time could be encoded via color, this can clutter the map since geographic features (like cities) don’t shift location.

  • Commonly, the same geometry (e.g., country outlines) is repeated across facets, while the attribute data (e.g., population) varies.

  • Faceted maps can also illustrate changing geometries, such as shifting point patterns over time (e.g., urban centers expanding).

  • tm_facets_wrap():

    • Used to create faceted maps by specifying a variable (by = "year") and layout (nrow, ncol).
    • Non-faceted layers (e.g., world polygons) are repeated in all panels.
  • tm_facets_grid() allows faceting by multiple variables (e.g., one per row, one per column, and one for pagination).

  • These maps are particularly useful for preparing animated maps, discussed in Section 9.3.

  • Creating the same map, as in the textbook, with ggplot2

Code
bts = 20
g <- ggplot() +
  geom_sf(
    data = world,
    fill = "grey80",
    colour = "grey10",
    linewidth = 0.2
  ) +
  geom_sf(
    data = urban_agglomerations |> 
            filter(year %in% c(1970, 1990, 2010, 2030)),
    mapping = aes(
      size = population_millions
    ),
    fill = "grey5",
    colour = "white",
    linewidth = 0.05,
    pch = 21
  ) +
  facet_wrap(~year) +
  coord_sf(crs = "ESRI:54030", expand = F) +
  
  theme_void(
    base_family = "body_font",
    base_size = bts,
    base_line_size = bts / 40,
    base_rect_size = bts / 40
  ) +
  theme(
    legend.position = "bottom",
    strip.background = element_rect(
      fill = "grey90"
    )
  )


ggsave(
  filename = here::here("book_solutions", "images", "chapter9-2-7.png"),
  plot = g,
  height = 1800,
  width  = 2500,
  units = "px"
)
Figure 7: Faceted map showing the top 30 largest urban agglomerations from 1970 to 2030 based on population projections by the United Nations.

9.2.8 Inset maps

  • Inset maps are smaller maps embedded within or alongside a main map to provide geographic context, focus on specific areas, or represent non-contiguous regions.

  • Common Uses:

    • Show the location of a zoomed-in area within a larger region.
    • Compare non-contiguous areas, such as Hawaii and Alaska with the mainland USA.
    • Display complementary information on the same area (e.g., different themes or topics).
  • Creating an Inset Map in R (using tmap):

    1. Define the area of interest using a spatial bounding box (e.g., a subregion of New Zealand).

    2. Render the main map showing detailed spatial data (e.g., elevation, symbols).

    3. Create the inset map that:

      • Shows the broader region.
      • Highlights the zoomed-in region using borders or shading.
    4. Calculate aspect ratios for correct map rendering using the norm_dim() function.

    5. Define viewports for main and inset maps using grid::viewport():

      • Main viewport shows the detailed map.
      • Inset viewport places the smaller contextual map in a corner.
  • Combining Maps:

    • Use grid.newpage() and print() with defined viewports to place inset maps over the main map canvas.
    • Save the result using tmap_save() with insets_tm and insets_vp arguments.
  • Special Case – USA Map:

    • Often includes non-contiguous states (Hawaii, Alaska) as insets.
    • Requires separate projections for accuracy (e.g., EPSG:9311 – US National Atlas Equal Area).
    • Combine three maps (mainland, Hawaii, Alaska) using individual viewports.
  • Caveats:

    • Ensure appropriate scale and placement to avoid misleading spatial interpretation (e.g., Alaska often appears larger than it is).
    • See the geocompkg us-map vignette for a refined approach to US inset maps.
  • Creating the same map, as in the textbook, with ggplot2

Code
nz_region <- st_bbox(
  c(
    xmin = 1340000, 
    xmax = 1450000,
    ymin = 5130000, 
    ymax = 5210000
  ),
  crs = st_crs(nz_height)
  ) |> 
  st_as_sfc()


g_base <- ggplot() +
  geom_spatraster(
    data = nz_elev |> terra::crop(nz_region)
  ) +
  geom_sf(
    data = nz_height |> st_crop(nz_region),
    pch = 2, 
    colour = "red",
    size = 8
  ) +
  paletteer::scale_fill_paletteer_c(
    "grDevices::Green-Yellow",
    direction = -1,
    na.value = "white",
    name = "Elevation\n(metres)"
  ) +
  ggspatial::annotation_scale(
    location = "bl",
    width_hint = 0.5
  ) +
  coord_sf(
    expand = FALSE
  ) +
  theme_void() +
  theme(
    legend.position = "inside",
    legend.position.inside = c(0.05, 0.95),
    legend.key.height = unit(30, "pt"),
    legend.justification = c(0, 1),
    legend.direction = "vertical",
    legend.background = element_rect(
      fill = NA, colour = "black"
    ),
    legend.margin = margin(5,5,5,5, "pt"),
    plot.background = element_rect(

    )
  )
  
g_inset <- ggplot() +
  geom_sf(
    data = nz,
    fill = "grey80"
  ) +
  geom_sf(
    data = nz_height,
    pch = 2,
    colour = "red",
    size = 2
  ) +
  geom_sf(
    data = nz_region,
    colour = "black",
    linewidth = 0.5,
    fill = NA
  ) +
  coord_sf(expand = F) +
  theme_void() +
  theme(
    panel.background = element_rect(
      fill = "lightblue",
      colour = "black"
    )
  )

# pacman::p_load(patchwork)

g <- g_base +
  inset_element(
    p = g_inset,
    left = 0.7,
    right = 0.95,
    bottom = 0.05,
    top = 0.5,
    align_to = "panel"
  )


ggsave(
  filename = here::here("book_solutions", "images", "chapter9-2-8.png"),
  plot = g,
  height = 1600,
  width  = 2000,
  units = "px"
)
Figure 8: Inset map providing a context – location of the central part of the Southern Alps in New Zealand.

9.3 Animated maps

  • Animated maps address limitations of faceted maps

    • Faceted maps (e.g., from Section 9.2.7) become cramped and hard to interpret when too many panels are displayed.
    • Animation overcomes this by displaying one time slice at a time, preserving clarity and focus.
  • Digital-first, but useful even in print

    • Though animations require digital formats, they can complement printed materials via links to online versions.
  • Creating animated maps using tmap

    • Built on familiar tmap syntax, similar to that used for faceted maps.
    • Instead of displaying all time points at once, animated maps iterate through them sequentially.
  • Key parameters for animation in tm_facets_wrap():

    • nrow = 1, ncol = 1: ensures only one frame/map is visible at a time.
    • free.coords = FALSE: maintains consistent spatial extent across frames for comparability.
  • Animation generation with tmap_animation()

    • Once the animation object (e.g., urb_anim) is created, it is exported as a .gif using tmap_animation().
  • The following code chunks produce animated maps with {ggplot2} and {gganimate} using examples from the book:

Code
# urban_agglomerations |> 
#   visdat::vis_miss()

urban_agglomerations |> 
  pull(year) |> 
  range()

pacman::p_load(gganimate)

# Clean data outside ggplot
urban_clean <- urban_agglomerations |>
  mutate(year = as.numeric(year)) |>
  filter(!is.na(year))

g <- ggplot(data = urban_clean) +
  geom_sf(
    data = world,
    fill = "grey80",
    colour = "grey10",
    linewidth = 0.05
  ) +
  geom_sf(
    mapping = aes(size = population_millions),
    colour = "red",
    alpha = 0.5
  ) +
  scale_size_continuous(
    range = c(0.5, 10),
    name = "Population\n(million)"
  ) +
  coord_sf(crs = "ESRI:54030") +
  labs(title = "Year: {floor(frame_time)}") +
  theme_void() +
  theme(
    legend.position = "inside",
    legend.position.inside = c(0.05, 0.2),
    legend.justification = c(0, 0),
    legend.direction = "horizontal",
    legend.title.position = "top",
    legend.text.position = "bottom",
    plot.title = element_text(
      hjust = 0.5,
      size = 24
    )
  ) +
  transition_time(year) +
  ease_aes("linear")

anim_save(
  animation = g,
  filename = here::here("book_solutions", "images", "chapter9-2-9.gif"),
  height = 400,
  width = 600,
  duration = 10,
  end_pause = 10
)
Figure 9: Global Urban Agglomerations (1950–2030): Shows the evolution of the world’s 30 largest cities.
Code
pacman::p_load(
  tidyverse,
  sf,
  historydata,
  lubridate,
  gganimate,
  scales
)
# pacman::p_install_gh("ropensci/USAboundaries")

pacman::p_load_gh("ropensci/USAboundaries")

# Step 1: Dates up to year 2000
unique_years <- unique(historydata::us_state_populations$year)
dates <- as.Date(paste0(unique_years, "-01-01"))
dates <- dates[dates <= as.Date("2000-12-31")]

# Step 2: Prepare population data
state_pop <- us_state_populations %>%
  select(year, state, population) %>%
  rename(name = state)

# Step 3: Fetch and join boundaries by year
boundary_list <- map(dates, function(date) {
  states <- us_states(map_date = date)
  year_val <- year(date)
  if (date == as.Date("2000-12-31")) year_val <- 2010
  states <- states %>%
    mutate(year = year_val) %>%
    left_join(state_pop %>% filter(year == year_val), by = c("name", "year"))
})

# Step 4: Combine all years
usb_all <- bind_rows(boundary_list)

# Step 5: Filter contiguous US and project
usb_all <- usb_all %>%
  filter(!str_detect(name, "Alaska|Hawaii")) %>%
  # Convert to NAD27 / US National Atlas Equal Area projection
  st_transform("EPSG:9311") |> 
  select(name, area_sqmi, terr_type, year, population) |> 
  drop_na()

rm(boundary_list, state_pop, dates, unique_years)

# Can we make it simpler for easier animation plotting
object.size(usb_all) |> print(units = "Kb")

usb_all |> 
  st_simplify(dTolerance = 5000) |> 
  object.size() |> 
  print(units = "Kb")

year_levels <- usb_all |> 
  st_drop_geometry() |> 
  distinct(year) |> 
  pull(year) |> 
  as.character()

plotdf <- usb_all |> 
  st_simplify(dTolerance = 5000) |> 
  mutate(year = fct(as.character(year), levels = year_levels))

g <- plotdf |> 
  ggplot() +
  geom_sf(
    mapping = aes(fill = population)
  ) +
  scale_fill_stepsn(
    n.breaks = 6,
    nice.breaks = T,
    limits = c(0, 30e6),
    oob = scales::squish,
    colours = paletteer::paletteer_d("ggprism::viridis", direction = -1),
    name = "Population",
    labels = scales::label_number(scale_cut = cut_short_scale())
  ) +
  labs(title = "US State Populations in {closest_state}") +
  theme_void(
    base_size = 16
  ) +
  theme(
    legend.position = "inside",
    legend.position.inside = c(0.02, 0.02),
    legend.justification = c(0,0),
    legend.direction = "vertical",
    legend.title.position = "top",
    legend.text.position = "right",
    plot.title = element_text(
      hjust = 0.5,
      size = 30
    ),
    legend.key.size = unit(30, "pt")
  ) +
  transition_states(year)

anim_save(
  g, 
  width = 900, 
  height = 600, 
  filename = here::here("book_solutions", 
                        "images", "chapter9-2-10.gif"),
  fps = 4, 
  duration = 15,
  end_pause = 2
  )
Figure 10: U.S. States Development (1790–2010): Depicts U.S. state formation and population growth over time.

9.4 Interactive Maps

  • Interactive maps enhance user engagement beyond static or animated maps by enabling actions like zooming, panning, clicking for pop-ups, tilting, and linking sub-plots (Pezanowski et al., 2018).

  • Slippy maps, or interactive web maps, are the most important kind. They became popular in R after the release of the leaflet package in 2015, which wraps the Leaflet.js library.

  • Several R packages build on Leaflet:

    • tmap: Offers both static and interactive maps using the same syntax.

      • Use tmap_mode("view") to switch to interactive mode.
      • Interactive features include tm_basemap() and synchronized facets with tm_facets_wrap(sync = TRUE).
      • Switch back with tmap_mode("plot").
    • mapview: A user-friendly one-liner for quick interactive visualization.

      • Provides GIS-like features (mouse position, scale bar, attribute queries).
      • Supports layering via + operator and works well with sf/SpatRaster objects.
      • Can auto-color attributes with zcol and supports alternate rendering backends (leafgl, mapdeck).
    • mapdeck: Leverages Deck.gl and WebGL for high-performance visualization.

      • Supports 2.5D views (tilt, rotate, extrude data).
      • Suitable for very large datasets (millions of points).
      • Requires Mapbox access token.
    • leaflet: The most mature and flexible package.

      • Offers fine-grained control over interactive features.
      • Supports adding layers, legends, mini-maps, and custom styling.
      • Based on the popular Leaflet.js library.
  • All interactive maps are rendered in the browser and are highly extensible and responsive, suitable for exploratory data analysis and presentation.

9.5 Mapping Applications

  • Limitations of Static Web Maps

    • While interactive web maps (e.g., those made with Leaflet) allow panning, zooming, and clicking, they are inherently static in terms of content and interface.
    • Web maps struggle to scale with large datasets.
    • Creating dashboards or visualizations with additional interactivity (e.g., graphs, filters) is difficult within the static web map paradigm.
  • Server-Based Alternatives for Scalable Mapping

    • Scalable solutions like GeoDjango, MapServer, and GeoServer can serve thousands of users.
    • These platforms require substantial setup, technical expertise, and often a team with roles like geospatial DBAs.
  • Enter shiny: Interactive Web Apps with R

    • shiny allows R programmers to build full-featured interactive web applications, including embedded maps using leaflet::renderLeaflet().

    • Every shiny app consists of:

      • UI (User Interface): Layout and input widgets (e.g., sliderInput, leafletOutput)
      • Server: Backend logic using reactive programming to generate outputs based on inputs.
  • Use shiny for Prototyping

    • shiny enables rapid prototyping of mapping tools in R — making it easier and quicker to test ideas.
    • Larger applications, like the Propensity to Cycle Tool (PCT), demonstrate shiny’s scalability and impact, even with 1000+ lines of code.
  • Caution: Plan Before Coding

    • Despite shiny’s power, avoid jumping into coding too early.
    • Start by sketching out the interface and goals on pen and paper — design should lead the code, not the other way around.
  • 🔗 Resources

9.6 Other Mapping Packages

  • Base R plotting with plot()

    • Mature and flexible option for static maps using base plot() methods from the sf and terra packages.
    • Can combine vector (sf) and raster (terra) data in a single plot.
    • Use ?plot and the sf5 vignette for advanced customization.
  • Mapping with ggplot2 and geom_sf()

    • Uses a layered approach to map-making; geom_sf() works seamlessly with sf objects.
    • Graticules are plotted by default; their appearance can be customized with coord_sf() or scale_x/y_continuous().
    • Offers strong flexibility and aesthetic control.
  • Interactivity with plotly

  • Enhancements with ggspatial

    • Adds elements like north arrows (annotation_north_arrow()), scale bars (annotation_scale()), and background map tiles.
    • Works with both sf and terra data using layer_spatial().
  • General-purpose mapping packages A selection of versatile R packages:

    Package Description
    ggplot2 Grammar of graphics framework for elegant visualizations
    googleway Interface to Google Maps APIs
    ggspatial Adds spatial elements to ggplot2
    leaflet Create interactive maps with Leaflet.js
    mapview Quick interactive viewing of spatial objects
    plotly Interactive visualizations using plotly.js
    rasterVis Advanced raster data visualization
    tmap Thematic mapping with static and interactive modes
  • Specific-purpose mapping packages For specialized map types:

    Package Purpose
    cartogram Create contiguous/non-contiguous/Dorling cartograms
    geogrid Transform polygons into hexagonal or regular grids
    geofacet Geographically arranged faceting for ggplot2
    linemap Create artistic line maps
    tanaka Shaded contour (Tanaka) maps
    rayshader 2D and 3D terrain visualization
  • Cartograms with cartogram package

    • Supports contiguous (cartogram_cont()), non-contiguous (cartogram_ncont()), and Dorling (cartogram_dorling()) cartograms.
    • Works with sf objects; visual output can be plotted using any mapping package like tmap or ggplot2.

9.7 Exercises

Code
pacman::p_load(spData, spDataLarge)
pacman::p_load(ggspatial)

# devtools::install_github("geocompr/geocompkg")

africa <- world |> 
  filter(continent == "Africa", !is.na(iso_a2)) |> 
  left_join(worldbank_df, by = "iso_a2") |> 
  select(name, subregion, gdpPercap, HDI, pop_growth) |> 
  st_transform("ESRI:102022") |> 
  st_make_valid() |> 
  st_collection_extract("POLYGON")

zion <- read_sf((system.file("vector/zion.gpkg", package = "spDataLarge")))
nlcd <- rast(system.file("raster/nlcd.tif", package = "spDataLarge"))

E1.

Create a map showing the geographic distribution of the Human Development Index (HDI) across Africa with base graphics (hint: use plot()) and tmap packages (hint: use tm_shape(africa) + ...).

  • Name two advantages of each based on the experience.

  • Name three other mapping packages and an advantage of each.

  • Bonus: create three more maps of Africa using these three other packages.

This solution creates a choropleth map of Africa showing the Human Development Index (HDI) distribution using ggplot2 and the geom_sf() function. The code demonstrates advanced mapping techniques by combining filled polygons colored by HDI values with country labels sized proportionally to their geographic area. Key features include using the Hawaii color palette for an aesthetically pleasing gradient, wrapping long country names for better readability, and positioning the legend strategically to avoid overlapping with the continent.

Code
g <- africa |> 
  mutate(area = as.numeric(st_area(geom))) |> 
  ggplot() +
  geom_sf(
    aes(fill = HDI),
    colour = "grey5"
  ) +
  geom_sf_text(
    aes(label = str_wrap(name, 9), size = area),
    colour = "grey5",
    lineheight = 0.7,
    family = "body_font"
  ) +
  paletteer::scale_fill_paletteer_c(
    "grDevices::Hawaii",
    direction = 1,
    na.value = "white",
    name = "Human\nDevelopment\nIndex"
  ) +
  scale_size_continuous(range = c(0.5, 3)) +
  guides(size = "none") +
  ggthemes::theme_map() +
  theme(
    legend.position = "inside",
    legend.position.inside = c(0.1, 0.1)
  )


ggsave(
  filename = here::here("book_solutions", "images", "chapter9-7-1.png"),
  plot = g,
  height = 1600,
  width  = 1200,
  units = "px"
)

E2.

Extend the tmap created for the previous exercise so the legend has three bins: “High” (HDI above 0.7), “Medium” (HDI between 0.55 and 0.7) and “Low” (HDI below 0.55). Bonus: improve the map aesthetics, for example by changing the legend title, class labels and color palette.

This solution transforms the continuous HDI values into categorical bins using case_when() to create three meaningful groups: “High” (above 0.7), “Medium” (0.55-0.7), and “Low” (below 0.55). The code enhances the map’s visual appeal by implementing a custom color palette from the paletteer package, positioning the legend inside the plot area, and adding country labels with text wrapping and size scaling based on country area for better readability.

Code
df1 <- africa |> 
  janitor::clean_names() |> 
  mutate(
    hdi = case_when(
      is.na(hdi) ~ NA,
      hdi > 0.7 ~ "High",
      hdi >= 0.55 ~ "Medium",
      hdi >= 0 ~ "Low",
      .default = NA
    )
  ) |> 
  filter(!is.na(hdi)) |> 
  mutate(
    hdi = fct(hdi, levels = c("Low", "Medium", "High")),
    area = as.numeric(st_area(geom))
  )

g <- df1 |> 
  ggplot() +
  geom_sf(
    aes(fill = hdi),
    colour = "grey95"
  ) +
  geom_sf_text(
    aes(label = str_wrap(name, 9), size = area),
    colour = "grey5",
    lineheight = 0.7,
    family = "body_font"
  ) +
  scale_size_continuous(range = c(0.5, 3)) +
  scale_fill_manual(
    values = paletteer::paletteer_d("beyonce::X41")[3:5]
  ) +
  guides(size = "none") +
  ggthemes::theme_map() +
  labs(fill = "Human Development\nIndex") +
  theme(
    legend.position = "inside",
    legend.position.inside = c(0.1, 0.1)
  )


ggsave(
  filename = here::here("book_solutions", "images", "chapter9-7-2.png"),
  plot = g,
  height = 1600,
  width  = 1200,
  units = "px"
)

E3.

Represent africa’s subregions on the map. Change the default color palette and legend title. Next, combine this map and the map created in the previous exercise into a single plot.

This solution creates a choropleth map of Africa colored by subregion, using a custom color palette and positioned legend, then combines it with the previous HDI map using patchwork. The code calculates country areas to size the text labels appropriately, applies the “sylvie” color palette from the paletteer package for better visual distinction between subregions, and uses plot_annotation() to add a unified title across both maps in the composite visualization.

Code
g2 <- africa |> 
  mutate(area = as.numeric(st_area(geom))) |> 
  ggplot() +
  geom_sf(
    mapping = aes(
      fill = subregion
    ),
    colour = "grey95"
  ) +
  geom_sf_text(
    aes(label = str_wrap(name, 9), size = area),
    colour = "grey5",
    lineheight = 0.7,
    family = "body_font"
  ) +
  scale_size_continuous(range = c(0.5, 3)) +
  scale_fill_manual(values = paletteer::paletteer_d("ltc::sylvie")) +
  guides(size = "none") +
  ggthemes::theme_map() +
  labs(fill = "Subregion of Africa") +
  theme(
    legend.position = "inside",
    legend.position.inside = c(0.1, 0.1)
  )

g_composite <- g + g2 +
  plot_annotation(
    title = "African countries' HDI vs. Subregions of Africa",
    theme = theme(
      plot.title = element_text(
        hjust = 0.5,
        size = 18
      )
    )
  )

ggsave(
  filename = here::here("book_solutions", "images", "chapter9-7-3.png"),
  plot = g_composite,
  height = 1600,
  width  = 2400,
  units = "px"
)

E4.

Create a land cover map of Zion National Park.

  • Change the default colors to match your perception of the land cover categories

  • Add a scale bar and north arrow and change the position of both to improve the map’s aesthetic appeal

  • Bonus: Add an inset map of Zion National Park’s location in the context of the state of Utah. (Hint: an object representing Utah can be subset from the us_states dataset.)

This solution creates a land cover map of Zion National Park by masking the National Land Cover Database (NLCD) raster data to the park boundaries and applying custom colors that intuitively represent different land cover types. The code transforms the park boundary to match the raster’s coordinate reference system, extracts the relevant land cover categories, and assigns meaningful colors (red for developed areas, various greens for vegetation types, blue for water, etc.). The map includes essential cartographic elements like a scale bar and north arrow positioned in the top-right corner, with the legend placed in the bottom-left to optimize the overall layout and visual appeal.

Code
rast1 <- nlcd |> 
  terra::mask(
    zion |> 
      st_transform(crs = crs(nlcd))
  )

custom_levels <- levels(rast1)[[1]] |> 
  as_tibble() |> 
  pull(levels) 
custom_levels <- custom_levels[c(3,4,5,6,7,8,9)]
  
g <- ggplot() +
  geom_spatraster(
    data = rast1,
    maxcell = 1e5
  ) +
  geom_sf(
    data = zion,
    fill = NA,
    linewidth = 0.5,
    colour = "black"
  ) +
  scale_fill_manual(
    breaks = custom_levels,
    values = c("red", "grey80", "#01360f", "#9db334",
               "lightgreen", "seagreen", "#76a0e8"),
    na.value = "transparent"
  ) +
  labs(fill = NULL) +
  annotation_scale(location = "tr", pad_y = unit(60, "pt")) +
  annotation_north_arrow(location = "tr") +
  ggthemes::theme_map() +
  theme(
    legend.position = "inside",
    legend.position.inside = c(0,0),
    legend.justification = c(0,0),
    legend.direction = "vertical",
    legend.key.spacing.y = unit(5, "pt")
  )

ggsave(
  filename = here::here("book_solutions", "images", "chapter9-7-4.png"),
  plot = g,
  height = 1600,
  width  = 1200,
  units = "px"
)

E5.

Create facet maps of countries in Eastern Africa:

  • With one facet showing HDI and the other representing population growth (hint: using variables HDI and pop_growth, respectively)

  • With a ‘small multiple’ per country

Code
g1 <- africa |> 
  filter(subregion == "Eastern Africa") |> 
  select(name, HDI, pop_growth) |> 
  pivot_longer(
    cols = c(HDI, pop_growth),
    names_to = "facet_var",
    values_to = "value"
  ) |> 
  ggplot(
    mapping = aes(fill = value)
  ) +
  geom_sf() +
  facet_wrap(
    ~facet_var
  ) +
  labs(
    title = "ggplot2 will not allow free scales on `fill`",
    subtitle = "That defeats the purpose of a faceted graph!",
    fill = "Value (HDI or Population Growth)"
  ) +
  paletteer::scale_fill_paletteer_c(
    "viridis::turbo",
    na.value = "white"
  ) +
  ggthemes::theme_map() +
  theme(
    legend.position = "bottom",
    legend.title.position = "top"
  )

g2 <- africa |> 
  filter(subregion == "Eastern Africa") |> 
  ggplot(
    mapping = aes(fill = HDI)
  ) +
  geom_sf() +
  paletteer::scale_fill_paletteer_c(
    "viridis::viridis",
    na.value = "white"
  ) +
  labs(
    title = "HDI",
    subtitle = "Eastern Africa",
    fill = "HDI"
  ) +
  ggthemes::theme_map() +
  theme(
    legend.position = "bottom",
    legend.title.position = "top"
  )

g3 <- africa |> 
  filter(subregion == "Eastern Africa") |> 
  ggplot(
    mapping = aes(fill = pop_growth)
  ) +
  geom_sf() +
  paletteer::scale_fill_paletteer_c(
    "pals::kovesi.rainbow_bgyr_35_85_c72",
    na.value = "white"
  ) +
  labs(
    title = "Population GRowth",
    subtitle = "Eastern Africa",
    fill = "Population Growth (%)"
  ) +
  ggthemes::theme_map() +
  theme(
    legend.position = "bottom",
    legend.title.position = "top"
  )

my_design = "AAAA#BBCC"

g <- patchwork::wrap_plots(
  g1, g2, g3
) +
  plot_layout(
    design = my_design
  ) &
  theme(
    plot.title = element_text(
      hjust = 0.5, 
      margin = margin(0,0,0,0, "pt")
    ),
    plot.subtitle = element_text(
      hjust = 0.5,
      margin = margin(0,0,0,0, "pt")
    ),
    legend.margin = margin(0,0,0,0, "pt"),
    legend.box.margin = margin(0,0,0,0, "pt"),
    legend.justification = c(0.5, 1)
  )

ggsave(
  filename = here::here("book_solutions", 
                        "images", "chapter9-7-5.png"),
  plot = g,
  height = 1200,
  width  = 3800,
  units = "px"
)

With a small mutiples per country using {patchwork}

Code
df_temp <- africa |> 
  filter(subregion == "Eastern Africa") |> 
  select(name, HDI, pop_growth) |> 
  mutate(id = row_number()) |> 
  relocate(id) |> 
  pivot_longer(
    cols = c(HDI, pop_growth),
    names_to = "facet_var",
    values_to = "value"
  ) |> 
  mutate(
    facet_var = if_else(
      facet_var == "pop_growth",
      "Pop. Growth",
      facet_var
    )
  )

i <- 3

for (i in 1:nrow(distinct(df_temp, name))) {
  assign(
    paste0("g", i),
    df_temp |> 
      filter(id == i) |> 
      ggplot(
        mapping = aes(fill = value)
      ) +
      geom_sf() +
      geom_sf_text(
        mapping = aes(
          label = paste0(
            facet_var,"\n",
            round(value, 2)
          )
        ),
        lineheight = 1
      ) +
      facet_wrap(~facet_var) +
      paletteer::scale_fill_paletteer_c(
        "viridis::turbo",
        na.value = "white",
        limits = c(0, 4)
      ) +
      labs(
        title = df_temp |> 
                  filter(id == i) |> 
                  distinct(name) |> 
                  pull(name)
      ) +
      coord_sf(expand = FALSE) +
      theme_void() +
      theme(
        plot.title = element_text(
          hjust = 0.5
        ),
        strip.text = element_blank()
      )
  )  
}

g <- wrap_plots(
  g1, g2, g3, g4, g5,
  g6, g7, g8, g9, g10,
  g11, g12, g13, g14, g15
) +
  plot_layout(
    ncol = 5,
    nrow = 3,
    guides = "collect"
  ) &
  theme(
    legend.title = element_blank(),
    legend.position = "bottom",
    legend.key.width = unit(100, "pt")
  )

ggsave(
  filename = here::here("book_solutions", 
                        "images", "chapter9-7-5-2.png"),
  plot = g,
  height = 2800,
  width  = 3800,
  units = "px"
)  

E6.

Building on the previous facet map examples, create animated maps of East Africa:

  • Showing each country in order

  • Showing each country in order with a legend showing the HDI

E7.

Create an interactive map of HDI in Africa:

  • With tmap

  • With mapview

  • With leaflet

  • Bonus: For each approach, add a legend (if not automatically provided) and a scale bar

E8.

Sketch on paper ideas for a web mapping application that could be used to make transport or land-use policies more evidence-based:

  • In the city you live, for a couple of users per day

  • In the country you live, for dozens of users per day

  • Worldwide for hundreds of users per day and large data serving requirements

E9.

Update the code in coffeeApp/app.R so that instead of centering on Brazil the user can select which country to focus on:

E10.

Reproduce Figure 9.1 and Figure 9.7 as closely as possible using the ggplot2 package.

E11.

Join us_states and us_states_df together and calculate a poverty rate for each state using the new dataset. Next, construct a continuous area cartogram based on total population. Finally, create and compare two maps of the poverty rate: (1) a standard choropleth map and (2) a map using the created cartogram boundaries. What is the information provided by the first and the second map? How do they differ from each other?

E12.

Visualize population growth in Africa. Next, compare it with the maps of a hexagonal and regular grid created using the geogrid package.

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.