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.
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
Creating Cartograms with {cartogram}, such as contiguous, non-contiguous, and Dorling cartograms to visually communicate data through shape transformations.
Custom Callouts in Quarto with the Custom Callout Extension, which enhances document structure and readability, such as the present call-out.
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 librarieslibrary(tidyverse) # For data wrangling and visualizationlibrary(sf) # For handling spatial objects in Rlibrary(ggrepel) # For repelling overlapping labels in plotslibrary(cartogram) # For creating different types of cartogramslibrary(showtext) # For using custom Google Fonts in plots# Load and prepare the CIA Factbook datacia_data <- openintro::cia_factbook |>mutate(# Convert country names to ISO3 codes for easy matching with # Geographical Maps dataiso_a3 = countrycode::countrycode(country, "country.name", "iso3c") )# Retrieve the world map dataworld_map <- rnaturalearth::ne_countries(scale ="small", # Use small scale for manageable detailreturnclass ="sf"# Return as an 'sf' object for spatial handling )# Add a custom Google font for captionsfont_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 countriesfill_palette <- paletteer::paletteer_d("khroma::stratigraphy")# Define a darker color palette for text labelscolour_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 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 labelsg <-ggplot(world_map) +# Draws the base map with country shapesgeom_sf(linewidth =0.1 ) +# Adds country names as labels, without overlap preventiongeom_sf_text(mapping =aes(label = country ) ) +# Applies a minimal theme for a clean visual layouttheme_minimal() +# Sets title, subtitle, and caption for the plotlabs(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 labelsg <-ggplot(world_map) +# Draws the base map with country shapesgeom_sf(linewidth =0.1, # Sets line width for label positioningcolour ="grey10" ) +# Adds country names as labels with repel effect to prevent overlapgeom_text_repel(mapping =aes(label = country,geometry = geometry,size = population ),stat ="sf_coordinates", # Sets the stat for spatial coordinatesfamily ="caption_font", # Sets the font family for labelsforce_pull =100,force =0.01,linewidth =0.01 ) +# Scales the size of labels based on populationscale_size_continuous(range =c(5, 25) ) +# Applies a minimal theme for a clean visual layouttheme_minimal(base_size =80 ) +# Sets title, subtitle, and caption for the plotlabs(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 sizetheme(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")
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 adjacencyworld_map_cont <- cartogram::cartogram_cont(world_map, "population")# Create a Dorling cartogram where each country is represented by a circleworld_map_dorling <- cartogram::cartogram_dorling(world_map, "population")# Create a non-contiguous cartogram where countries resize independentlyworld_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 orderg <- world_map_cont |>arrange(desc(population)) |># Initialize ggplot, mapping fill and color aesthetics to countryggplot(mapping =aes(fill = country,colour = country ) ) +# Add the country shapes without bordersgeom_sf(colour ="transparent" ) +# Add text labels for each country with size proportional to populationgeom_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 rangescale_size_continuous(range =c(1, 10) ) +# Apply manual color scale for fill and outline of countriesscale_fill_manual(values = fill_palette ) +scale_colour_manual(values = colour_palette ) +# Add plot title and remove x and y axis labelslabs(x =NULL, y =NULL,title ="A contiguous Cartogram of countries' population" ) +# Apply a minimal theme with custom font and sizetheme_minimal(base_family ="caption_font",base_size =16 ) +# Customize plot appearance with centered title and invisible legendtheme(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 backgroundggsave(plot = g,filename = here::here("geocomputation", "images","cartogram_types_3.png"),height =900,width =1200,units ="px",bg ="white")
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 orderg <- world_map_ncont |>arrange(desc(population)) |># Initialize ggplot, mapping fill and color aesthetics to countryggplot(mapping =aes(fill = country,colour = country ) ) +# Add the original world map with grey borders and white fillgeom_sf(data = world_map,fill ="white",colour ="grey60",linewidth =0.1 ) +# Add the non-contiguous cartogram countries with transparencygeom_sf(colour ="transparent",alpha =0.75 ) +# Add text labels for each country with size proportional to populationgeom_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 rangescale_size_continuous(range =c(1, 10) ) +# Apply manual color scale for fill and outline of countriesscale_fill_manual(values = fill_palette ) +scale_colour_manual(values = colour_palette ) +# Add plot title and remove x and y axis labelslabs(x =NULL, y =NULL,title ="A non-contiguous Cartogram of countries' population - preserves the country shapes" ) +# Apply a minimal theme with custom font and sizetheme_minimal(base_family ="caption_font",base_size =16 ) +# Customize plot appearance with centered title and invisible legendtheme(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 backgroundggsave(plot = g,filename = here::here("geocomputation", "images","cartogram_types_4.png"),height =900,width =1200,units ="px",bg ="white")
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 orderg <- world_map_dorling |>arrange(desc(population)) |># Initialize ggplot, mapping fill and color aesthetics to countryggplot(mapping =aes(fill = country,colour = country ) ) +# Add the non-overlapping circles representing countriesgeom_sf(colour ="transparent" ) +# Add text labels for each country, sized by populationgeom_sf_text(mapping =aes(label = country,size = population,geometry = geometry ),family ="caption_font",fontface ="bold" ) +# Set continuous scale for text size within a specified rangescale_size_continuous(range =c(1, 10) ) +# Apply manual color scale for fill and Text of countriesscale_fill_manual(values = fill_palette ) +scale_colour_manual(values = colour_palette ) +# Add plot title and remove x and y axis labelslabs(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 legendtheme(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 backgroundggsave(plot = g,filename = here::here("geocomputation", "images","cartogram_types_5.png"),height =900,width =1200,units ="px",bg ="white")
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.