library(tmap)
library(tmap.mapgl)
library(sf)
library(terra)
library(dplyr)
The World dataset contains the variable HPI
(Happy Planet Index), a measure of sustainable wellbeing combining life
expectancy, wellbeing, inequality, and ecological footprint.
"view" mode. Make a proportional symbol map
of World where:
fill colour encodes HPIsize encodes pop_est (population
estimate)tmap_mode("view")
data("World")
tm_shape(World) +
tm_symbols(fill = "HPI", size = "pop_est")
tm_shape(World) +
tm_symbols(fill = "HPI", size = "pop_est",
fill.scale = tm_scale_intervals(values = "pu_gn"),
size.scale = tm_scale_continuous(values.scale = 3))
values.scale = 3multiplies the default symbol sizes by 3. The"pu_gn"palette is a purple–green diverging scheme — low HPI maps to purple, high HPI to green. Alternatives can be explored viacols4all::c4a_gui()under the diverging tab.
tm_shape(World) +
tm_symbols(fill = "HPI", size = "pop_est",
fill.scale = tm_scale_intervals(values = "pu_gn"),
size.scale = tm_scale_continuous(values.scale = 3),
hover = "name")
popup.vars. Which did you choose and why?tm_shape(World) +
tm_symbols(
fill = "HPI",
size = "pop_est",
fill.scale = tm_scale_intervals(values = "pu_gn"),
size.scale = tm_scale_continuous(values.scale = 3),
hover = "name",
popup.vars = c("name", "HPI", "life_exp", "well_being",
"footprint", "gdp_cap_est", "pop_est")
)
life_expandwell_beingare HPI components;footprintis the ecological cost;gdp_cap_estprovides economic context. Includingnamekeeps the popup self-contained.
Switch to "maplibre" mode and reproduce the map from
1.1d.
You may need to increase values.scale — MapLibre renders
symbols at a different scale than view mode. Also note: in the globe
projection, bubbles are flat circles projected onto a sphere, which can
make them appear to “float” when rotating. Mapping to sphere
volume rather than circle area gives better results on
a globe: replace values.scale = ... with
values = tm_seq(0, 1, power = 1/3).
tmap_mode("maplibre")
tm_shape(World) +
tm_symbols(
fill = "HPI",
size = "pop_est",
fill.scale = tm_scale_intervals(values = "pu_gn"),
size.scale = tm_scale_continuous(values = tm_seq(0, 1, power = 1/3),
values.scale = 6),
hover = "name",
popup.vars = c("name", "HPI", "life_exp", "well_being",
"footprint", "gdp_cap_est", "pop_est")
)
values.scale = 6is needed in MapLibre because its default symbol scale is smaller than in Leaflet view mode.tm_seq(0, 1, power = 1/3)applies a cube-root transformation to the size mapping, which corresponds to volume scaling for spheres (radius ∝ value^(1/3)), reducing the visual dominance of the very largest bubbles and making the globe feel more natural when rotating.
tmap_providers() to inspect the available basemaps.
For diverging palettes, high contrast backgrounds work best — pure white
or black perform better than any shade of grey.
"ofm.bright" (OpenFreeMap) is a clean, freely available
bright basemap.tmap_providers()
tm_basemap("ofm.bright") +
tm_shape(World) +
tm_symbols(
fill = "HPI",
size = "pop_est",
fill.scale = tm_scale_intervals(values = "pu_gn"),
size.scale = tm_scale_continuous(values = tm_seq(0, 1, power = 1/3),
values.scale = 6),
hover = "name",
popup.vars = c("name", "HPI", "life_exp", "well_being",
"footprint", "gdp_cap_est", "pop_est")
)
A bright white basemap keeps the full range of the purple–green palette visible. Grey basemaps reduce apparent saturation of mid-range values, making it harder to distinguish countries near the HPI median. Dark basemaps have the same problem at the other end of the scale.
In this exercise you will download the 2025 global population raster from WorldPop, crop it to a country or continent, aggregate it to a workable resolution, and visualise it as a 3D extruded map in MapLibre mode.
About the raster cell size
The WorldPop data are stored in a geographic coordinate system (WGS 84 longitude/latitude). This means all cells have the same angular size — approximately 0.0083° × 0.0083° at 1 km resolution — but their physical size varies with latitude. Cell height (north–south) is constant at roughly 59 km after aggregation by a factor of 64. Cell width (east–west) shrinks as you move away from the equator, because lines of longitude converge toward the poles: at the equator a cell is ~59 km wide, while near the poles it can be as narrow as ~6 km. This is an inherent property of geographic rasters, not a data artefact.
In practice, when you aggregate by a fixed factor the resulting cells represent a larger physical area near the equator than near the poles. Keep this in mind when comparing population counts across different latitudes.
worldpop_url <- paste0(
"https://data.worldpop.org/GIS/Population/",
"Global_2015_2030/R2025A/2025/0_Mosaicked/v1/1km_ua/constrained/",
"global_pop_2025_CN_1km_R2025A_UA_v1.tif"
)
local_file <- "data/global_pop_2025_1km.tif"
dir.create("data", showWarnings = FALSE)
if (!file.exists(local_file)) {
message("Downloading WorldPop 2025 (~280 MB) — this will take a while...")
options(timeout = 2000)
download.file(worldpop_url, destfile = local_file, mode = "wb")
}
pop_global <- rast(local_file)
pop_global
Note: very small countries may not crop well at 1km resolution — larger countries work best.
A note on border cells: The
Worldboundaries are simplified polygons. Where a grid cell straddles a country border,terra::mask()decides by cell centre: if the centre falls inside the polygon the whole cell is kept; if outside, the whole cell is set toNA. This means border cells may contain population from the neighbouring country, or conversely some border cells from within the country may be dropped. The effect is small for large areas but can produce a slightly ragged edge at the boundary, and means population totals near borders are not precisely attributed to a single country.
data("World")
# --- Option A: Germany ---
area <- World |> filter(name == "Germany")
# --- Option B: Africa ---
# area <- World |> filter(continent == "Africa") |> st_union()
area_reproj <- st_transform(area, crs(pop_global))
pop_cropped <- crop(pop_global, area_reproj) |> mask(area_reproj)
names(pop_cropped) <- "pop"
pop_cropped
aggregate_pop <- function(r, fact = 8) {
r_agg <- terra::aggregate(r, fact = fact, fun = sum, na.rm = TRUE)
names(r_agg) <- "pop"
r_agg
}
# Germany: fact = 8 → ~8km cells
# Africa: fact = 64 → ~64km cells
pop_agg <- aggregate_pop(pop_cropped, fact = 8)
pop_agg
Note on CRS: For large areas like Germany or Africa the default globe CRS works well and has no artefacts.
tm_crs(3857)(Web Mercator) is only needed for small regions like the Netherlands where 3D polygon artefacts can appear.
tm_maplibre(pitch = 45) already
tilts the view to give a sense of perspective before adding
extrusion.tmap_mode("maplibre")
tm_shape(pop_agg) +
tm_raster(col = "pop",
col.scale = tm_scale_continuous(values = "brewer.yl_or_rd"),
col.legend = tm_legend("Population (2025)")) +
tm_maplibre(pitch = 45) +
tm_shape(pop_agg) +
tm_polygons_3d(
fill = "pop",
fill.scale = tm_scale_continuous(values = "brewer.yl_or_rd"),
fill.legend = tm_legend("Population (2025)"),
height = "pop",
height.scale = tm_scale_continuous(values.scale = 3),
options = opt_tm_polygons_3d(height.max = "10%", height.min = "0.1%")
) +
tm_maplibre(pitch = 45) +
height.max = "10%"caps the tallest bar at 10% of the viewport, preventing dense coastal cities from dwarfing everything else.tm_maplibre(pitch = 45)sets the initial tilt so the 3D effect is immediately visible.
# Africa
area2 <- World |> filter(continent == "Africa") |> st_union()
area2_reproj <- st_transform(area2, crs(pop_global))
pop_cropped2 <- crop(pop_global, area2_reproj) |> mask(area2_reproj)
names(pop_cropped2) <- "pop"
pop_agg2 <- aggregate_pop(pop_cropped2, fact = 64)
tm_shape(pop_agg2) +
tm_polygons_3d(
fill = "pop",
fill.scale = tm_scale_continuous(values = "brewer.yl_or_rd"),
fill.legend = tm_legend("Population (2025)"),
height = "pop",
height.scale = tm_scale_continuous(values.scale = 3),
options = opt_tm_polygons_3d(height.max = "10%", height.min = "0.1%")
) +
tm_maplibre(pitch = 45) +
Germany shows a polycentric pattern — recognisable peaks at Berlin, Hamburg, Munich, Frankfurt, Cologne, and the Rhine-Ruhr conurbation. Africa at the continental scale reveals very different urban structures: dense peaks around Lagos and the Niger delta, the Nile delta, Kinshasa, and the East African highlands, with large sparsely populated stretches of the Sahara and Kalahari in between.