library(tmap)
library(tmap.mapgl)
tmap_mode("maplibre")
tm_shape(World) +
tm_polygons_3d(
height = "pop_est_dens", # variable controlling extrusion height
fill = "continent" # variable controlling fill colour
)Session 8: 3D maps and high-performance web maps
tm_polygons_3d() layertmap.mapgl introduces a new layer type available only in "mapbox" and "maplibre" modes:
tm_polygons_3d() is identical to tm_polygons() with one additional visual variable: height.
Use tm_maplibre() to tilt the camera:
In the browser: right-click + drag to tilt interactively.
Height = population density, colour = education level:
tmap_mode("maplibre")
NLD_dist$pop_dens <- NLD_dist$population / NLD_dist$area
tm_shape(NLD_dist) +
tm_polygons_3d(
height = "pop_dens",
fill = "edu_appl_sci",
fill.scale = tm_scale_intervals(style = "kmeans", values = "-pu_gn"),
fill.legend = tm_legend("Applied sci. degree (%)"),
hover = "name"
) +
tm_crs(crs = 3857) +
tm_maplibre(pitch = 45)pitch around 30–50° for a good balance of readability and 3D effectWorldPop provides global gridded population counts at ~1 km resolution.
global_pop_2025_CN_1km_R2025A_UA_v1.tiffact = 64 → 64 × 64 = 4096 cells merged into onefun = sum → total population per cell is preservedlibrary(tmap)
library(tmap.mapgl)
tmap_mode("maplibre")
tm_shape(pop_agg) +
tm_polygons_3d(
height = "pop",
fill = "pop",
fill.scale = tm_scale_intervals(
values = "-ocean.thermal",
style = "kmeans"), # handles skewed distribution well
fill.legend = tm_legend_hide()
) +
tm_basemap("ofm.bright") # light basemap so tall bars stand out
Global population density as extruded polygons. Height and colour both encode total population count.
style = "kmeans") handle the highly skewed population distribution better than equal-interval or quantile"-ocean.thermal" — reversed sequential palette with good contrast across the range"ofm.bright" (OpenFreeMap) — a free, light basemap that contrasts well with the coloured barstm_legend_hide() — omitted because height already makes values readable spatiallytmap_save(map, "map.html") to save as an interactive HTML file — the 3D map is fully interactive in the browser| Approach | Dataset size | Rendering | Notes |
|---|---|---|---|
Leaflet ("view") |
Small–medium | CPU | All data sent as GeoJSON |
| MapLibre/Mapbox | Medium–large | GPU (WebGL) | Much faster for large feature counts |
| Vector tiles | Very large | GPU (streaming) | Data loaded on demand per zoom level |
MapLibre and Mapbox GL render using WebGL — the GPU is involved:
Leaflet re-renders in the DOM (Document Object Model — the browser’s internal representation of the page) on every interaction, using the CPU — slower at scale.
Current tmap processes all data in R memory before rendering.
Version 4.3 introduces initial support for PMTiles via tmap.sources (early development):
tm_scale_intervals, tm_scale_categorical) generate browser-side rendering instructions instead of precomputing values in RThis makes truly large spatial datasets feasible in interactive tmap maps.
GeoArrow — compact columnar format for spatial data between R and the browser; avoids verbose GeoJSON. Together with WebGPU, enables much larger datasets.
Packages: geoarrowWidget, geoarrowDeckglLayers.
Direction of travel:
✅ Use "view" (Leaflet) when:
✅ Use "maplibre" when:
tm_polygons_3d() adds a height visual variable for polygon extrusiontm_maplibre(pitch = ...) (drag in browser to tilt interactively)terra::aggregate() + tm_polygons_3d()