Skip to contents

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 brain shape is drawn by a stat

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.

A generated shape has a consequence worth stating plainly. A bare atlas has no values to colour, so it draws grey. geom_brain() is for putting your numbers on the brain; when you want to look at the atlas itself, palette and all, reach for plot(dk()).

ggplot() +
  geom_brain(atlas = dk(), show.legend = FALSE)
The DK atlas in uniform grey with region outlines and no fill.

A bare atlas: the full shape, drawn grey.

Give it data and the regions you have values for take colour. Everything else stays grey, so the brain remains legible even when you have measured only a corner of it.

results <- data.frame(
  region = 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 on a red-blue diverging scale, the rest grey.

Three regions have values; the rest of the brain still renders.

Repeated regions are combined

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(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)
The three regions summarised by mean, then by maximum.

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)
The three regions summarised by mean, then by maximum.

Repeated-measures data summarised by mean and by maximum.

Faceting comes for free

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 = 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()
Two stacked brains, one per cohort, four regions coloured.

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.

Colour, including bins, is just a scale

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)
The atlas in four discrete colour steps from a binned scale.

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())
Regions coloured by a factor: thin, typical, and thick.

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.

The three pieces, if you want to build on them

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 grey fill and a grey35 outline – 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 region and label survive into the stat.

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.

ggplot() +
  stat_brain(atlas = dk(), geom = "point", size = 0.1)
The atlas as a dense cloud of small grey points.

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.