39  Many groups

How do several groups compare? The comparison questions: several groups, one framework. Two families of answer: split the marks inside one panel (color, group), or split the plot into panels (|, /). The recipes run from one to the other.

39.1 How do the groups’ distributions compare?

The overlaid histogram: bin cuts one set of intervals for everyone, so the groups sit on shared edges, each a translucent fill under its own solid outline:

data(iris_flowers) + bar * bin + x(petal_length) + color(species)
data(iris_flowers) + bar * bin + x(col.petal_length) + color(col.species)
data(iris_flowers) + bar * bin + x(:petal_length) + color(:species)
plot(data(iris_flowers), layer(bar, bin), x(col.petal_length),
  color(col.species))
2 4 6 0 10 20 30 40 Count Petal Length Species setosa versicolor virginica

“Bars derived by bin: x is petal length, color by species.” The outline is what keeps each species’ silhouette readable through the pile-up.

39.2 And when the overlap gets heavy?

Drop the fills entirely: step * bin traces each group’s silhouette as an unfilled staircase, so nothing can hide anything:

data(iris_flowers) + step * bin + x(petal_length) + color(species)
data(iris_flowers) + step * bin + x(col.petal_length) + color(col.species)
data(iris_flowers) + step * bin + x(:petal_length) + color(:species)
plot(data(iris_flowers), layer(step, bin), x(col.petal_length),
  color(col.species))
2 4 6 0 10 20 30 40 Count Petal Length Species setosa versicolor virginica

One mark swapped, nothing else. Step owns the staircase.

39.3 Split, but don’t decorate

Sometimes the grouping matters and the identities don’t: you want the texture of many series, not a legend naming each. group splits without coloring:

data(gapminder_asia) + line + x(year) + y(life) + group(country)
data(gapminder_asia) + line + x(col.year) + y(col.life) + group(col.country)
data(gapminder_asia) + line + x(:year) + y(:life) + group(:country)
plot(data(gapminder_asia), line, x(col.year), y(col.life),
  group(col.country))
1960 1980 2000 40 50 60 70 80 Life Year

Five countries, five lines, no legend: the shape of the bundle is the answer. Bind color instead when the reader needs to know which is which.

39.4 One panel per group

When the groups deserve their own axes-worth of room, multiply the plot instead of the marks. Statistics run within each panel; the scale stays shared so panels stay comparable:

data(gapminder_2007) + bar * bin + x(life) | facet(continent)
data(gapminder_2007) + bar * bin + x(col.life) | facet(col.continent)
data(gapminder_2007) + bar * bin + x(:life) | facet(:continent)
plot(data(gapminder_2007), layer(bar, bin), x(col.life),
  across(col.continent))
50 60 70 80 0 5 10 15 50 60 70 80 50 60 70 80 50 60 70 80 50 60 70 80 Asia Europe Africa Americas Oceania Count Life

Five distributions on one common axis, cut into one common set of bins and counted continent by continent. Compare the shapes column by column: Africa’s mass sits left, Europe’s right, and because every bar covers the same span of years the heights mean the same thing in each panel. Faceting owns the rules, including why the cut is shared when the count is not.

39.5 Two grouping variables at once

| splits into panel columns, / into panel rows; written together they cross into a grid. Every combination gets a panel:

gm_years <- gm_all[gm_all$year %in% c(1957, 2007), ]
gm_years$period <- as.character(gm_years$year)

data(gm_years) + point + x(gdp, scale = "log") + y(life) |
  facet(continent) / facet(period)
40 60 80 1K 10K 100K 40 60 80 1K 10K 100K 1K 10K 100K 1K 10K 100K 1K 10K 100K Asia Europe Africa Americas Oceania 1957 2007 Life Gdp

Fifty years of change, read down each continent’s column. The period column exists because a facet variable names panels, which is also what this chapter’s refusal is about.

39.6 Which group is biggest?

Comparison reads best sorted, and no bare text column can say “order me by value”; that is what order() is for:

data(medals) + bar + x(country) + y(gold) + order(gold, desc = TRUE)
data(medals) + bar + x(col.country) + y(col.gold) + order(col.gold, desc = True)
data(medals) + bar + x(:country) + y(:gold) + order(:gold, desc = true)
plot(data(medals), bar, x(col.country), y(col.gold),
  order(col.gold, { desc: true }))
USA China Great Britain Russia Germany 0 10 20 30 40 Gold Country

One atom away. desc = FALSE (the default) for ascending; naming a column other than the axis’s own, as here, also overrides a factor’s declared levels.

39.7 Two groups, back to back

A population pyramid is a named chart type, and nothing in the kernel knows its name. It falls out of rules that were already there, for other reasons.

The mirror comes from a minus sign: one city’s counts are negated so its bars grow the other way. That is arithmetic, so it happens where the data lives:

mirrored <- census
mirrored$population <- ifelse(mirrored$city == "Busan",
                              -mirrored$population, mirrored$population)

R, because it is R’s job, and the other three bindings answer a minus sign the same way. What follows is the sentence, and it has four spellings like every other:

data(mirrored) + bar + y(age) + x(population) + color(city) +
  order(age, desc = TRUE)
(data(mirrored) + bar + y(col.age) + x(col.population) + color(col.city) +
  order(col.age, desc = True))
data(mirrored) + bar + y(:age) + x(:population) + color(:city) +
  order(:age, desc = true)
plot(data(mirrored), bar, y(col.age), x(col.population), color(col.city),
  order(col.age, { desc: true }))
-40K -20K 0K 20K 40K 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 Age Population City Busan Seoul

No new atom, and none was needed. Three things the grammar already said met here on their own. Orientation is read off the bindings, so y(age) being a category is what lays the bars down, and there is no flip atom because that would be a second way to say one thing. A bar runs from the zero baseline to its value in whichever direction that value sits, so a negative bar is not a special case; it is the same rectangle pointing the other way. And stack is something you ask for rather than something that happens, so two bars sharing one slot sit back to back instead of piling up.

order(age, desc = TRUE) is the atom from the section above, doing exactly what it did there. gog puts the first category first in reading order, which on a horizontal chart is the top, and a pyramid wants its youngest band on the floor.

Now look at the left half of that axis. It reads -40K, not 40,000.

This chart is often drawn with the tick text replaced by its absolute value, which hides the minus sign the bars were placed with. gog keeps the coordinates it drew, and this is the clearest case for why. Relabeled, the axis carries two different ticks both reading 40,000, and only a reader who knows the convention can tell them apart. That reader did not need the help; the one who did has been handed a plot that contradicts its own coordinates. Choosing your own tick text is a capability nothing here has ruled on in either direction, and Coverage keeps it on the open list. This is the argument on the against side.

39.8 What this section refuses

A bar is a rectangle; it has no glyph to vary, so shape on a bar is refused outright. The table of what each mark accepts is one table, with no per-mark exceptions:

data(medals) + bar + x(country) + y(gold) + shape(country)
Error:
! gog: `shape` cannot be bound to `bar` — a bar has no shape feature. Remove `shape(country)`, or use a mark that has one.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.