Skip to contents

The viz surface at a glance

kind functions use when
composable layers geom_geoscale(), theme_geoscale() you are building your own ggplot() and want a choropleth as one layer among others
assembled figures geoscale_autoplot() (structure icicle; also plot()), geoscale_plot() (one-call choropleth) you want a finished figure in one call
layout / geometry workers geoscale_layout(), geoscale_geometry() you want the plain frames behind the figures

This article needs maps, so it runs on two fixtures that stay out of the package by design (geoscales ships integration code, not data): the offline energyRt::utopia_geoscale() reference layout, and — for the real-world tour — Iceland from Natural Earth’s admin-1 layer.

The integration contract

geom_geoscale() mirrors the design of timescales::geom_calendar(): the geoscale inputs are column names (z=, region=), not aes() mappings, and each call returns a standard ggplot2::geom_sf() layer whose data is derived from the plot data — the object’s geometry is dissolved at the requested geoframe (via geoscale_geometry()), the value column is aggregated per region with fun, and everything else — scales, facets, themes, extra layers — composes through the normal ggplot2 path.

Choropleths, composably

gs <- energyRt::utopia_geoscale("honeycomb")
gs
#> Geoscale: utopia 
#> Description: UTOPIA reference regions, nested nation -> zone -> region 
#> Geoframes (3, coarsest first):
#>   - nation (1)
#>     - zone (3)
#>       - region (11)
#> Atoms: 11
#> Weights: area (default: area)
#> CRS: NA
#> Geometry: attached (11 features)

x <- tibble(region = geoscale_regions(gs, "region"),
            capacity = c(5, 3, 8, 2, 6, 4, 7, 1, 9, 2, 5))

ggplot(x) +
  geom_geoscale(gs = gs, z = "capacity", geoframe = "region") +
  scale_fill_viridis_c(option = "G") +
  labs(title = "Capacity by region", fill = "GW") +
  theme_geoscale()

With z = NULL the layer draws plain boundaries — useful under other layers:

ggplot() +
  geom_geoscale(gs = gs, colour = "grey40", fill = "grey93") +
  theme_geoscale()

Recasting, seen on the map

The same data drawn at another geoframe is one geoframe= away — the layer dissolves and aggregates for you (here with fun = sum for an extensive quantity):

ggplot(x) +
  geom_geoscale(gs = gs, z = "capacity", geoframe = "zone", fun = sum) +
  scale_fill_viridis_c(option = "G") +
  labs(title = "The same capacity, by zone", fill = "GW") +
  theme_geoscale()

For full control, recast_geoscale() first and draw the result — the explicit route makes the rule visible (and lets intensive quantities use weighted_mean):

x |>
  recast_geoscale(gs, from = "region", to = "zone", rule = "sum") |>
  ggplot() +
  geom_geoscale(gs = gs, z = "capacity", geoframe = "zone") +
  scale_fill_viridis_c(option = "G") +
  theme_geoscale()

One panel per geoframe

Dissolved shapes are plain sf objects, so faceting a hierarchy is a small |> chain over geoscale_geometry():

shapes <- geoscale_geoframes(gs) |>
  lapply(function(gf) {
    geoscale_geometry(gs, gf) |>
      sf::st_sf() |>
      mutate(geoframe = gf, .before = 1) |>
      rename(code = 2)
  }) |>
  bind_rows() |>
  mutate(geoframe = factor(geoframe, levels = geoscale_geoframes(gs)))

ggplot(shapes) +
  geom_sf(aes(fill = code), show.legend = FALSE) +
  facet_wrap(~geoframe) +
  scale_fill_viridis_d(option = "G") +
  theme_geoscale()

Structure figures

The assembled counterparts need no geometry at all. geoscale_autoplot() (also plot()) draws the hierarchy as an icicle; its plain-data frame is geoscale_layout():

plot(gs)

head(geoscale_layout(gs), 4)
#>   geoframe  region rank      xmin      xmax ymin ymax   weight     share
#> 1   nation  UTOPIA    1 0.0000000 1.0000000    2  2.9 62.21484 1.0000000
#> 2     zone    WEST    2 0.0000000 0.2727273    1  1.9 16.96768 0.2727273
#> 3     zone CENTRAL    2 0.2727273 0.6363636    1  1.9 22.62358 0.3636364
#> 4     zone    EAST    2 0.6363636 1.0000000    1  1.9 22.62358 0.3636364

The icicle carries data too — data =/z = fill every band with the value recast to that geoframe (atoms keep their own values, coarser bands get the weighted mean):

cap <- data.frame(region = geoscale_regions(gs, "region"),
                  mw = c(40, 15, 75, 30, 60, 22, 8, 55, 34, 12, 48))
geoscale_autoplot(gs, data = cap, z = "mw", label = TRUE) +
  labs(fill = "MW")

With geometry attached, type = "stack" draws the layer-stack view — the same atoms dissolved at every geoframe, stacked:

geoscale_autoplot(gs, type = "stack")

Points of view

The stack takes a view preset — from flat "top-down" through the oblique family ("cavalier", "cabinet") and the axonometric family ("military", "isometric", "dimetric", "trimetric") to "perspective", where receding planes shrink. Custom obliques come via angle/ratio, rotate= turns the plane (point North anywhere), direction= flips the stack, and gap defaults to planes almost touching:

# grouped by aspect so tall and flat views don't squeeze each other
views <- c("top-down", "military", "isometric",
           "dimetric", "trimetric", "cabinet",
           "oblique", "cavalier", "perspective")
views |>
  lapply(function(vw)
    geoscale_autoplot(gs, type = "stack", view = vw) +
      labs(title = vw) +
      theme(plot.title = element_text(size = 9, hjust = 0.5))) |>
  patchwork::wrap_plots(ncol = 3)

And geoscale_plot() is the one-call choropleth over the same machinery:

geoscale_plot(gs, x, geoframe = "region", fill = "capacity",
              palette = "G")

A real map: Iceland, with data attached

The same workflow on real boundaries. Natural Earth’s admin-1 layer gives nine units for Iceland that group into its eight regions (landshluti) — a genuinely ragged hierarchy, because Reykjavik and the surrounding capital area are separate features of one region:

s <- ne_source(geoframe = "states", country = "Iceland")
d <- as.data.frame(s)

isl <- geoscale_from_leaftable(
  data.frame(country    = "ISL",
             landshluti = d$gn_name,
             unit       = d$iso_3166_2),
  geoframes = c("country", "landshluti", "unit"),
  key = "unit", name = "iceland"
) |>
  attach_geometry_geoscale(s, by = "iso_3166_2", geoframe = "unit") |>
  add_area_geoscale(name = "km2")
isl
#> Geoscale: iceland 
#> Geoframes (3, coarsest first):
#>   - country (1)
#>     - landshluti (8)
#>       - unit (9)
#> Atoms: 9
#> Weights: km2 (default: km2)
#> CRS: WGS 84
#> Geometry: attached (9 features)

add_area_geoscale() just attached real data — equal-area km² per unit — which is also the object’s default weight. Drawn straight from the leaftable:

lt <- geoscale_leaftable(isl)

ggplot(tibble(unit = lt$region, km2 = lt$km2)) +
  geom_geoscale(gs = isl, z = "km2", geoframe = "unit",
                colour = "white", linewidth = 0.3) +
  scale_fill_viridis_c(option = "G", trans = "sqrt") +
  labs(title = "Iceland's nine Natural Earth units, by area",
       fill = "km2") +
  theme_geoscale()

And the full loop in one pipeline: a national total split down to the regions by area (rule = "sum" conserves it), drawn at the landshluti geoframe — where the two capital-area units render as one region:

tibble(country = "ISL", capacity_mw = 3000) |>
  recast_geoscale(isl, from = "country", to = "landshluti",
                  rule = "sum", weight = "km2") |>
  ggplot() +
  geom_geoscale(gs = isl, z = "capacity_mw", geoframe = "landshluti",
                colour = "white", linewidth = 0.3) +
  scale_fill_viridis_c(option = "G") +
  labs(title = "A 3 GW national total, split by area across 8 regions",
       fill = "MW") +
  theme_geoscale()

One resource, three geoframes

Model design is choosing a resolution. The wind-cluster Geoscale from the get-started vignette carries Iceland’s mean wind speed on its cluster atoms; recasting the same values up the hierarchy shows exactly what each aggregation keeps — the temporal twin of this figure (one wind year at three calendar resolutions) lives in the timescales visualization vignette:

iw <- readRDS("../../data-raw/iceland_wind.rds")
for (gf in c("cluster", "landshluti", "country")) {
  d <- if (gf == "cluster") iw$wind else
    recast_geoscale(iw$wind, iw$gs, from = "cluster", to = gf,
                    rule = "weighted_mean", weight = "km2")
  print(geoscale_plot(iw$gs, d, geoframe = gf, fill = "wind",
                      palette = "G") +
          labs(title = paste("Iceland mean wind speed at:", gf),
               fill = "m/s"))
}

See also

  • vignette("geoscales") — the 5-minute tour.
  • vignette("data-manipulation") — the conversion pipelines feeding these maps.
  • timescales’ Visualization with ggplot2 vignette — the temporal sibling: same contract, calendar heatmaps and wall calendars instead of choropleths.