The first time you plot three
regions and get a whole brain back, it can look like sleight of hand.
You handed geom_brain() a few rows; it drew the entire
cortex, greyed the regions you left out, and coloured the ones you
named. There is no trick – just three small ggplot2 pieces doing their
jobs. Knowing what each one does explains why faceting, aggregation, and
colour scales behave the way they do, and how to build on them.
The whole brain always renders, even when your data covers only a
handful of regions. That comes from StatBrain, a ggplot2
stat that generates the atlas geometry instead of reading it
from your data. It is the same move geom_sf() makes: the
shape is computed, not supplied.
Map nothing and there is nothing of yours to colour by, so
geom_brain() falls back to the atlas’s own palette: it maps
fill to label, the key the palette is built
on. That is the atlas looked at for its own sake.
A bare atlas: the full shape, in the atlas palette.
Give it data and your mapping takes over: the regions you have values for take colour from your scale. Everything else stays grey, so the brain remains legible even when you have measured only a corner of it.
results <- data.frame(
region = sort(unique(ggseg.formats::atlas_regions(dk())))[1:3],
score = c(2.1, -1.4, 0.8)
)
ggplot(results, aes(fill = score)) +
geom_brain(atlas = dk(), show.legend = FALSE) +
scale_fill_gradient2()Three regions have values; the rest of the brain still renders.
Real data rarely arrives one row per region. You might have one row
per subject, or per session, or per run. When several rows land on the
same region, StatBrain reduces them to a single value
before drawing. The default is the mean, and the fun
argument takes any function that turns a vector into one number.
thickness <- data.frame(
region = rep(sort(unique(ggseg.formats::atlas_regions(dk())))[1:3], each = 4),
subject = rep(1:4, times = 3),
thickness = c(2.5, 2.6, 2.4, 2.7, 3.1, 3.0, 3.2, 2.9, 2.8, 2.7, 2.9, 2.6)
)
# mean per region (the default)
ggplot(thickness, aes(fill = thickness)) +
geom_brain(atlas = dk(), show.legend = FALSE)
#> Warning: ! Several `data` rows map to the same atlas region; combining them with `fun`.
#> ℹ Before ggseg 3.0.0 these rows overplotted, so the last one won. Pass `fun =
#> dplyr::last` to keep that behaviour.
#> ℹ Non-numeric columns take the first value regardless of `fun`.
#> This warning is displayed once per session.Repeated-measures data summarised by mean and by maximum.
# the maximum instead
ggplot(thickness, aes(fill = thickness)) +
geom_brain(atlas = dk(), fun = max, show.legend = FALSE)Repeated-measures data summarised by mean and by maximum.
Here is where being a real stat pays off. ggplot2 recomputes a stat
once per facet panel, so the complete brain appears in every panel
straight from your data. No group_by(), no copying the
atlas by hand – facet_wrap() and facet_grid()
behave exactly as they do for any other geom.
cohorts <- expand.grid(
region = sort(unique(ggseg.formats::atlas_regions(dk())))[1:4],
group = c("patients", "controls"),
stringsAsFactors = FALSE
)
cohorts$score <- rnorm(nrow(cohorts))
ggplot(cohorts, aes(fill = score)) +
geom_brain(atlas = dk(), show.legend = FALSE) +
facet_wrap(~group, ncol = 1) +
scale_fill_gradient2()One brain per cohort, each drawn from that cohort’s own rows.
Each panel is summarised on its own rows, so the same region can read differently from one panel to the next.
StatBrain hands the geom an ordinary continuous
fill. That one fact means every ggplot2 fill scale works
without ggseg doing anything special.
So how would you make a binned map – discrete steps instead of a smooth gradient? You do not reach for a ggseg feature; you reach for a scale. A stepped scale bins the aggregated value for you.
ggplot(thickness, aes(fill = thickness)) +
geom_brain(atlas = dk()) +
scale_fill_steps(n.breaks = 4)Binning the aggregated value with a stepped scale.
When your bins carry meaning – a threshold, a clinical band – cut the values yourself and map the resulting factor.
thickness$band <- cut(
thickness$thickness,
breaks = c(-Inf, 2.7, 3.0, Inf),
labels = c("thin", "typical", "thick")
)
ggplot(thickness, aes(fill = band)) +
geom_brain(atlas = dk())Named bands, made with cut() and mapped as a discrete fill.
The division of labour is the point: the stat decides how many rows become one value, and the scale decides how that value becomes a colour. Continuous gradients, viridis, diverging, binned, stepped, and manual palettes all compose the same way.
Under geom_brain() sit three ggproto objects, each with
a narrow job.
StatBrain generates the atlas geometry per panel,
reduces your values per region with fun, and joins them
onto the shape. It emits the polygon x and y,
the group and subgroup that encode ring
structure and holes, and passes your fill straight
through.
GeomBrain is a thin GeomPolygon subclass.
Its only additions are the brain defaults – a grey35
outline and a grey fill for an atlas with no palette of its own – and
both step aside the moment you map an aesthetic of your own.
LayerBrain is the layer that runs at plot-build time. It
flattens the atlas, hands the geometry to the stat, and quietly maps the
join keys as aesthetics so label and region
survive into the stat. Running at build time is also what lets it see a
fill set in the top-level ggplot() call before
deciding whether to supply the palette default.
stat_brain() is the stat-first spelling of
geom_brain() – both build the same layer. Reach for it when
you want StatBrain paired with a different geom, such as
drawing the atlas vertices as a point cloud.
StatBrain with a point geom instead of the polygon default.
Everything else you already use – position_brain()
layouts, coord_brain(), the scale_*_brain()
palettes, theme_brain() – sits on top of these three pieces
without changes.