3 types of Cartograms in R with {sf} and {cartogram}

Creating Cartograms – contiguous, non-contiguous and packed-circles – in R with {cartogram}, and making non-overlapping text annotations in maps, and custom callouts in Quarto.

Cartogram
{cartogram}
{ggrepel}
Quarto customization
Author

Aditya Dahiya

Published

October 25, 2024

Introduction

On this webpage, we’ll explore how to create cartograms in R, using population data from the CIA World Factbook. Cartograms are a unique type of thematic map that reshape geographic regions to represent data variables rather than their actual geographic area. By resizing areas to reflect variables like population, cartograms reveal spatial patterns and disparities in a more visually striking way, making them a powerful tool for storytelling with data.

Unlike traditional maps, where region size is based solely on geographical area, cartograms alter these sizes to communicate insights about underlying data trends. This approach offers several advantages: it enhances visualization by making patterns more apparent, communicates complex data to a broad audience effectively, and highlights disparities between regions, drawing attention to areas of interest. Additionally, cartograms facilitate comparative analysis by allowing viewers to easily compare regions resized according to a single variable.

To create cartograms in R, we’ll use a combination of packages, including {cartogram} (Jeworutzki 2023)for cartogram-specific functions, {sf} (Pebesma and Bivand 2023) for handling spatial data, {ggplot2} (Wickham 2016) for flexible mapping and plotting, and {tidyverse} (Wickham et al. 2019) for streamlined data manipulation. The {cartogram} package provides various cartogram types, including

  • Dorling cartograms (Figure 5) that represent regions as resized circles,

  • Contiguous area cartograms (Figure 3) that maintain topological relationships between regions, and

  • Non-contiguous area cartograms (Figure 4) that allow flexibility in resizing by ignoring boundaries.

About the Data

The dataset used in this tutorial is sourced from the CIA World Factbook, specifically the Country Comparisons from 2014. This resource provides essential statistics on population, area, and other key indicators for 265 global entities. Through the {openintro} and {usdatasets} R packages, we access population metrics that allow us to create cartograms—maps where countries’ sizes are distorted according to population values rather than geographic area. This dataset, which required no additional cleaning, enables the visualization of demographic distributions, highlighting countries’ population density and size in an intuitive way for mapping exercises in R.

Key Learnings
  1. Creating Cartograms with {cartogram}, such as contiguous, non-contiguous, and Dorling cartograms to visually communicate data through shape transformations.

  2. Custom Callouts in Quarto with the Custom Callout Extension, which enhances document structure and readability, such as the present call-out.

  3. Repelling Overlapping Text Labels with {ggrepel} with geom_sf() and geom_sf_text() for improved clarity on maps.

Step 1: Getting libraries and raw data

In this step, we are setting up our workspace to create a population-based cartogram using data from the CIA World Factbook. We begin by loading essential libraries, including {tidyverse} for data manipulation and visualization, {sf} for handling spatial data, and {cartogram} for creating cartograms. We load the cia_factbook dataset and use the {countrycode} package to add ISO3 country codes for mapping. The world_map object is created using the {rnaturalearth} package, which provides geographic data in sf format. Additionally, we set up custom fonts using {showtext} and define color palettes for filling and labeling countries, enhancing the map’s readability and aesthetic.

Code
# Load essential libraries
library(tidyverse)         # For data wrangling and visualization
library(sf)                # For handling spatial objects in R
library(ggrepel)           # For repelling overlapping labels in plots
library(cartogram)         # For creating different types of cartograms
library(showtext)          # For using custom Google Fonts in plots

# Load and prepare the CIA Factbook data
cia_data <- openintro::cia_factbook |> 
  mutate(
    # Convert country names to ISO3 codes for easy matching with 
    # Geographical Maps data
    iso_a3 = countrycode::countrycode(country, "country.name", "iso3c") 
  )

# Retrieve the world map data
world_map <- rnaturalearth::ne_countries(
  scale = "small",      # Use small scale for manageable detail
  returnclass = "sf"    # Return as an 'sf' object for spatial handling
  )

# Add a custom Google font for captions
font_add_google("Saira Extra Condensed", "caption_font")
showtext_auto()           # Automatically apply custom fonts

# Display the size of the world_map object in KB
# object.size(world_map) |> print(units = "Kb")

# Define colors for country fill and text
# Fill color palette for countries
fill_palette <- paletteer::paletteer_d("khroma::stratigraphy")

# Define a darker color palette for text labels
colour_palette <- fill_palette |> 
  str_sub(start = 1, end = 7) |>  # Truncate hex codes to 6 characters
  colorspace::darken(0.5)         # Darken colors by 50% for better contrast

Step 2: Converting the data into a “tidy” tibble.

In this code snippet, we refine the world_map data and visualize it in the Mercator projection using the ggplot2 package. We start by selecting relevant columns, grouping by country name, and keeping the entry with the highest population estimate for countries with multiple entries. After joining this map data with the cia_data dataset, we filter out any countries without population data and apply the Pseudo-Mercator projection (CRS 3857) using {sf}’s st_transform() function. Finally, we use ggplot2 to plot the world map with geom_sf() and set a minimal theme and informative title and caption.

Table 1
Code
# Filter, join, and transform world map data for plotting

world_map <- world_map |> 
  select(name, geometry, pop_est, iso_a3) |>      # Select relevant columns
  group_by(name) |>                               # Group by country name
  slice_max(order_by = pop_est, n = 1) |>         # Retain country entry with max population estimate
  left_join(cia_data) |>                          # Join with CIA Factbook data
  filter(!is.na(population)) |>                   # Filter out entries without population data
  st_transform(crs = 3857) |>                     # Transform to Psuedo-Mercator projection (CRS = 3857)
  ungroup()
Table 2: The sf object morld map to be used in the susequent analysis
Name Pop Est Iso a 3 Country Area Birth Rate Death Rate Infant Mortality Rate Internet Users Life Exp at Birth Maternal Mortality Rate Net Migration Rate Population Population Growth Rate
Afghanistan 38,041,754 AFG Afghanistan 652,230 38.8 14.1 117.2 1,000,000.0 50.5 460.0 −1.8 31,822,848 2.3
Albania 2,854,191 ALB Albania 28,748 12.7 6.5 13.2 1,300,000.0 78.0 27.0 −3.3 3,020,209 0.3
Algeria 43,053,054 DZA Algeria 2,381,741 24.0 4.3 21.8 NA 76.4 97.0 −0.9 38,813,722 1.9
Angola 31,825,295 AGO Angola 1,246,700 39.0 11.7 80.0 NA 55.3 450.0 0.5 19,088,106 2.8
Argentina 44,938,712 ARG Argentina 2,780,400 16.9 7.3 10.0 13,694,000.0 77.5 77.0 0.0 43,024,374 0.9
Armenia 2,957,731 ARM Armenia 29,743 13.9 9.3 14.0 208,200.0 74.1 30.0 −5.9 3,060,631 −0.1
Australia 25,364,307 AUS Australia 7,741,220 12.2 7.1 4.4 15,810,000.0 82.1 7.0 5.7 22,507,617 1.1
Austria 8,877,067 AUT Austria 83,871 8.8 10.4 4.2 6,143,000.0 80.2 4.0 1.8 8,223,062 0.0
Azerbaijan 10,023,318 AZE Azerbaijan 86,600 17.0 7.1 26.7 2,420,000.0 71.9 43.0 0.0 9,686,210 1.0
Bahamas 389,482 BHS Bahamas, The 13,880 15.6 7.0 12.5 115,800.0 71.9 47.0 0.0 321,834 0.9
Bangladesh 163,046,161 BGD Bangladesh 143,998 21.6 5.6 45.7 617,300.0 70.7 240.0 0.0 166,280,712 1.6
Belarus 9,466,856 BLR Belarus 207,600 10.9 13.5 3.6 2,643,000.0 72.2 4.0 0.8 9,608,058 −0.2
Belgium 11,484,055 BEL Belgium 30,528 10.0 10.8 4.2 8,113,000.0 79.9 8.0 1.2 10,449,361 0.0
Belize 390,353 BLZ Belize 22,966 25.1 6.0 20.3 36,000.0 68.5 53.0 0.0 340,844 1.9
Benin 11,801,151 BEN Benin 112,622 36.5 8.4 57.1 NA 61.1 350.0 0.0 10,160,556 2.8
Bhutan 763,092 BTN Bhutan 38,394 18.1 6.8 37.9 50,000.0 69.0 180.0 0.0 733,643 1.1
Bolivia 11,513,100 BOL Bolivia 1,098,581 23.3 6.6 38.6 1,103,000.0 68.5 190.0 −0.7 10,631,486 1.6
Bosnia and Herz. 3,301,000 BIH Bosnia and Herzegovina 51,197 8.9 9.6 5.8 1,422,000.0 76.3 8.0 −0.4 3,871,643 −0.1
Botswana 2,303,697 BWA Botswana 581,730 21.3 13.3 9.4 120,000.0 54.1 160.0 4.6 2,155,784 1.3
Brazil 211,049,527 BRA Brazil 8,514,877 14.7 6.5 19.2 75,982,000.0 73.3 56.0 −0.1 202,656,788 0.8

Key Learning: Using geom_text_repel() in place of geom_text_sf() with stat = "sf_coordinates"

In this code, we generate two versions of a world map using the Mercator projection (CRS = 3857). The first plot demonstrates how using geom_sf_text() without any adjustment can lead to overlapping labels, particularly in densely populated areas. The second plot corrects this with geom_text_repel() from the {ggrepel} package (Slowikowski 2024), which dynamically adjusts label positions to prevent overlap and improve readability. Each map includes labels based on country name, and the label sizes vary by population, offering a clear contrast between the two approaches for displaying map text labels.

Code
# Plot the transformed world map data with overlapping labels
g <- ggplot(world_map) +
  
  # Draws the base map with country shapes
  geom_sf(
    linewidth = 0.1
  ) +    
  
  # Adds country names as labels, without overlap prevention
  geom_sf_text(
    mapping = aes(
      label = country
    )
  ) +
  
  # Applies a minimal theme for a clean visual layout
  theme_minimal() +           
  
  # Sets title, subtitle, and caption for the plot
  labs(
    title = "World Map: Labels Overlapping when using geom_sf_text()",
    subtitle = "Map in the Mercator Projection (CRS = 3857)",
    caption = "Source: {rnaturalearth} package data retrieved with ne_countries() function"
  ) +
  
  theme(
    panel.grid = element_line(
      linewidth = 0.1
    )
  )

ggsave(
  filename = here::here("geocomputation",
                        "images",
                        "cartogram_types_1.png"),
  plot = g,
  height = 600,
  width = 800,
  units = "px"
)

# Plot the transformed world map data with repelled labels
g <- ggplot(world_map) +
  
  # Draws the base map with country shapes
  geom_sf(
    linewidth = 0.1,       # Sets line width for label positioning
    colour = "grey10"
  ) +    
  
  # Adds country names as labels with repel effect to prevent overlap
  geom_text_repel(
    mapping = aes(
      label = country,
      geometry = geometry,
      size = population
    ),
    stat = "sf_coordinates",   # Sets the stat for spatial coordinates
    family = "caption_font",   # Sets the font family for labels
    force_pull = 100,
    force = 0.01,
    linewidth = 0.01
  ) +
  
  # Scales the size of labels based on population
  scale_size_continuous(
    range = c(5, 25)
  ) +
  
  # Applies a minimal theme for a clean visual layout
  theme_minimal(
    base_size = 80
  ) +    
    
  # Sets title, subtitle, and caption for the plot
  labs(
    title = "World Map: Labels with geom_text_repel() with stat = \"sf_coordinates\"",
    caption = "Source: {rnaturalearth} package data retrieved with ne_countries() function",
    x = NULL, y = NULL
  ) +
  
  # Removes the legend for size
  theme(
    legend.position = "none",
    panel.grid = element_line(
      linewidth = 0.01
    )
  )

ggsave(
  filename = here::here("geocomputation",
                        "images",
                        "cartogram_types_2.png"),
  plot = g,
  height = 3700,
  width = 4900,
  units = "px",
  bg = "white"
)
Figure 1: Basic World Map: With no effort to prevent overlapping of labels
Figure 2: Labels Repelled from each other to prevent overlapping, using geom_text_repel() from package {ggrepel}

Step 3: Converting geometry into Cartograms geometry using {cartogram}

In this step, we generate three types of cartograms based on population data, each offering a unique way to represent global population distribution using the {cartogram} package. First, we transform the world map data to the Mercator projection (EPSG 3857), which is the standard projection for web maps. We then create three cartograms:

  • a contiguous cartogram that distorts countries proportionally to population while maintaining geographic adjacency,

  • a Dorling cartogram that represents each country as a circle sized by population, and

  • a non-contiguous cartogram that allows countries to resize independently, resulting in more accurate shapes but less geographic continuity.

# Transforming the data to different cartogram types based on population

# Create a contiguous cartogram where countries maintain adjacency
world_map_cont <- cartogram::cartogram_cont(world_map, "population")

# Create a Dorling cartogram where each country is represented by a circle
world_map_dorling <- cartogram::cartogram_dorling(world_map, "population")

# Create a non-contiguous cartogram where countries resize independently
world_map_ncont <- cartogram::cartogram_ncont(world_map, "population")

Results

Type 1: A Continuous Cartogram

In this code, we generate a contiguous cartogram plot, shown in Figure 3, using {ggplot2} and {sf} libraries, with countries sized according to population. The code begins by arranging world_map_cont in descending order of population (so that the countries with larger population are displayed first, while we use the argument check_overlap = TRUE with geom_sf_text(). The cartogram plot is created using geom_sf() for shapes and geom_sf_text() for country labels, with label sizes reflecting population. Manual scales are applied to align fill and text colors with predefined palettes. The plot includes a centered title and minimal theme.

Code
# Arrange the cartogram data by population in descending order
g <- world_map_cont |> 
  arrange(desc(population)) |> 

# Initialize ggplot, mapping fill and color aesthetics to country
  ggplot(
    mapping = aes(
      fill = country,
      colour = country
    )
  ) +

# Add the country shapes without borders
  geom_sf(
    colour = "transparent"
  ) +

# Add text labels for each country with size proportional to population
  geom_sf_text(
    mapping = aes(
      label = country,
      size = population,
      geometry = geometry
    ),
    family = "caption_font",
    fontface = "bold",
    check_overlap = TRUE
  ) +

# Set continuous scale for text size within a specified range
  scale_size_continuous(
    range = c(1, 10)
  ) +

# Apply manual color scale for fill and outline of countries
  scale_fill_manual(
    values = fill_palette
  ) +
  scale_colour_manual(
    values = colour_palette
  ) +

# Add plot title and remove x and y axis labels
  labs(
    x = NULL, y = NULL,
    title = "A contiguous Cartogram of countries' population"
  ) +

# Apply a minimal theme with custom font and size
  theme_minimal(
    base_family = "caption_font",
    base_size = 16
  ) +

# Customize plot appearance with centered title and invisible legend
  theme(
    legend.position = "none",
    panel.grid = element_line(
      colour = "grey90",
      linetype = 3,
      linewidth = 0.1
    ),
    plot.title = element_text(
      hjust = 0.5,
      margin = margin(0,0,0,0, "mm"),
      size = 32
    ),
    plot.margin = margin(0,0,0,0, "mm")
  )

# Save the plot as a PNG with defined size and white background
ggsave(
  plot = g,
  filename = here::here("geocomputation", "images",
                        "cartogram_types_3.png"),
  height = 900,
  width = 1200,
  units = "px",
  bg = "white"
)
Figure 3: A World Map Cartogram, with countries sized by population, using data from CIA World Factbook. The contiguous cartogram ensures that neighbousing countries keep touching each other, although shapes are distorted.

Type 2: A Non-continuous Cartogram

The next code chunk generates a non-contiguous cartogram, shown in Figure 4, where countries are resized according to population but maintain their original shapes, making it easier to recognize familiar geographic forms.. It uses two layers of geom_sf() to add the original world map with a grey outline for context and the resized cartogram countries with a semi-transparent overlay. Text labels are added for each country, sized by population, without overlapping.

Code
# Arrange the non-contiguous cartogram data by population in descending order
g <- world_map_ncont |> 
  arrange(desc(population)) |> 

# Initialize ggplot, mapping fill and color aesthetics to country
  ggplot(
    mapping = aes(
      fill = country,
      colour = country
    )
  ) +

# Add the original world map with grey borders and white fill
  geom_sf(
    data = world_map,
    fill = "white",
    colour = "grey60",
    linewidth = 0.1
  ) +

# Add the non-contiguous cartogram countries with transparency
  geom_sf(
    colour = "transparent",
    alpha = 0.75
  ) +

# Add text labels for each country with size proportional to population
  geom_sf_text(
    mapping = aes(
      label = country,
      size = population,
      geometry = geometry
    ),
    family = "caption_font",
    fontface = "bold",
    check_overlap = FALSE
  ) +

# Set continuous scale for text size within a specified range
  scale_size_continuous(
    range = c(1, 10)
  ) +

# Apply manual color scale for fill and outline of countries
  scale_fill_manual(
    values = fill_palette
  ) +
  scale_colour_manual(
    values = colour_palette
  ) +

# Add plot title and remove x and y axis labels
  labs(
    x = NULL, y = NULL,
    title = "A non-contiguous Cartogram of countries' population - preserves the country shapes"
  ) +

# Apply a minimal theme with custom font and size
  theme_minimal(
    base_family = "caption_font",
    base_size = 16
  ) +

# Customize plot appearance with centered title and invisible legend
  theme(
    legend.position = "none",
    panel.grid = element_line(
      colour = "grey90",
      linetype = 3,
      linewidth = 0.1
    ),
    plot.title = element_text(
      hjust = 0.5,
      margin = margin(0,0,0,0, "mm"),
      size = 28
    ),
    plot.margin = margin(0,0,0,0, "mm")
  )

# Save the plot as a PNG with defined size and white background
ggsave(
  plot = g,
  filename = here::here("geocomputation", "images",
                        "cartogram_types_4.png"),
  height = 900,
  width = 1200,
  units = "px",
  bg = "white"
)
Figure 4: A non-contiguous cartogram of countries population, using data from CIA Factbook, shows that while shapes of countries are preserved, their neighbouring countries don’t touch each others’ borders anymore.

Type 3: A non-overlapping circles Cartogram

This code snippet creates a Dorling cartogram, shown in Figure 5, where countries are represented as non-overlapping circles sized according to their populations. The ggplot function is used to set up the aesthetic mappings for fill and color based on the country. The geom_sf() function is employed to add the circular representations of countries without outlines, while geom_sf_text() adds text labels for each country, sized according to their populations.

Code
# Arrange the Dorling cartogram data by population in descending order
g <- world_map_dorling |> 
  arrange(desc(population)) |> 

# Initialize ggplot, mapping fill and color aesthetics to country
  ggplot(
    mapping = aes(
      fill = country,
      colour = country
    )
  ) +

# Add the non-overlapping circles representing countries
  geom_sf(
    colour = "transparent"
  ) +

# Add text labels for each country, sized by population
  geom_sf_text(
    mapping = aes(
      label = country,
      size = population,
      geometry = geometry
    ),
    family = "caption_font",
    fontface = "bold"
  ) +

# Set continuous scale for text size within a specified range
  scale_size_continuous(
    range = c(1, 10)
  ) +

# Apply manual color scale for fill and Text of countries
  scale_fill_manual(
    values = fill_palette
  ) +
  scale_colour_manual(
    values = colour_palette
  ) +

# Add plot title and remove x and y axis labels
  labs(
    x = NULL, y = NULL,
    title = "A non-overlapping circles Cartogram of countries' population."
  ) +

# Apply a map theme with custom font and size
  ggthemes::theme_map(
    base_family = "caption_font",
    base_size = 16
  ) +

# Customize plot appearance with centered title and invisible legend
  theme(
    legend.position = "none",
    plot.title = element_text(
      hjust = 0.5,
      margin = margin(0,0,0,0, "mm"),
      size = 28
    ),
    plot.margin = margin(0,0,0,0, "mm")
  )

# Save the plot as a PNG with defined size and white background
ggsave(
  plot = g,
  filename = here::here("geocomputation", "images",
                        "cartogram_types_5.png"),
  height = 900,
  width = 1200,
  units = "px",
  bg = "white"
)
Figure 5

References

Jeworutzki, Sebastian. 2023. “Cartogram: Create Cartograms with r.” https://CRAN.R-project.org/package=cartogram.
Pebesma, Edzer, and Roger Bivand. 2023. Spatial Data Science: With Applications in r.” https://doi.org/10.1201/9780429459016.
Slowikowski, Kamil. 2024. “Ggrepel: Automatically Position Non-Overlapping Text Labels with ’Ggplot2’.” https://CRAN.R-project.org/package=ggrepel.
Wickham, Hadley. 2016. “Ggplot2: Elegant Graphics for Data Analysis.” https://ggplot2.tidyverse.org.
Wickham, Hadley, Mara Averick, Jennifer Bryan, Winston Chang, Lucy D’Agostino McGowan, Romain François, Garrett Grolemund, et al. 2019. “Welcome to the Tidyverse 4: 1686. https://doi.org/10.21105/joss.01686.