44  Many groups

How do several groups compare? There are two ways to answer. You can split the marks inside one panel, with color or group. You can split the plot into panels, with | or /. The recipes below use both.

44.1 How do the groups’ distributions compare?

Three species of iris share one petal length axis. The question is whether their petal lengths overlap or sit apart. The overlaid histogram: bin cuts one set of intervals for all three species, so the groups sit on shared edges, each a translucent fill under its own solid outline:

iris_flowers: first 5 of 150 rows
sepal_length sepal_width petal_length species
5.1 3.5 1.4 setosa
4.9 3.0 1.4 setosa
4.7 3.2 1.3 setosa
4.6 3.1 1.5 setosa
5.0 3.6 1.4 setosa
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

“Given the iris flowers: bars derived by bin, x is petal length, color by species.” The outline is what keeps each species’ silhouette readable where the fills overlap. Owning chapters: Bar, Transforms, Channels.

44.2 What if the overlap gets worse?

Three translucent fills are still readable. More groups, or groups that sit closer together, hide more of each fill. 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

“Given the iris flowers: step outlines derived by bin, x is petal length, color by species.”

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

44.3 Grouping without a legend

Sometimes the grouping matters and the identities do not: you want the shape of many series, not a legend naming each. group splits without coloring:

gapminder_asia: first 5 of 60 rows
country continent year life population gdp
China Asia 1952 44.00000 556263527 400.4486
China Asia 1957 50.54896 637408000 575.9870
China Asia 1962 44.50136 665770000 487.6740
China Asia 1967 58.38112 754550000 612.7057
China Asia 1972 63.11888 862030000 676.9001
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

“Given gapminder Asia: lines, x is year, y is life, grouped by country.”

Five countries, five lines, no legend. The shape the five lines make together is the answer. Bind color instead when the reader needs to know which is which.

44.4 One panel per group

Five continents on one histogram would be five overlaid fills; the recipes above had only three to separate. When each group needs its own pair of axes, split the plot into panels instead of splitting the marks. Statistics run within each panel; the scale stays shared so panels stay comparable:

gapminder_2007: first 5 of 142 rows
country continent year life population gdp
Afghanistan Asia 2007 43.828 31889923 974.5803
Albania Europe 2007 76.423 3600523 5937.0295
Algeria Africa 2007 72.301 33333216 6223.3675
Angola Africa 2007 42.731 12420476 4797.2313
Argentina Americas 2007 75.320 40301927 12779.3796
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

“Given gapminder 2007: bars derived by bin, x is life, split into panel columns by continent.”

Five distributions on one common axis, cut into one common set of bins and counted continent by continent. Compare the panels one at a time. Africa’s bars sit at the low end of the axis. Europe’s bars sit at the high end. Bins and the count axis are shared, so equal heights mean equal counts. Faceting owns the rules, including why the cut is shared when the count is not.

44.5 Two grouping variables at once

Continent is one grouping and year is another. You want each continent drawn in 1957 and again in 2007, to see what changed between the two years. | splits into panel columns, / into panel rows; written together they cross into a grid. Every combination gets a panel. The two years are selected in R, because subsetting a table is the host language’s job, and the other three bindings do it their own way:

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

“Given the gm years table: points, x is gdp on a log scale, y is life, split into panel columns by continent and panel rows by period.”

Fifty years of change, read down each continent’s column. year is a number, and a facet variable has to name its panels, so period is year written as text. That is what the Over time refusal is about.

44.6 Which group is biggest?

The medals table lists five countries, and the question is which of them won the most gold. A comparison is easiest to read when it is sorted, and a column of plain text carries no order of its own. order() is how you give it one:

medals: all 5 rows
country gold silver bronze
USA 46 37 38
China 38 31 22
Great Britain 29 17 19
Russia 19 18 9
Germany 17 10 15
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

“Given the medals: bars, x is country, y is gold, ordered by gold, largest first.”

One atom away. desc = FALSE, the default, sorts smallest first. order() names gold here, not the category column country: that is how a category is sorted by a value. It overrides a declared order as well.

44.7 Two groups, mirrored

Two cities each have a population count in every age band, and the question is how the two age structures compare. The chart drawn for that question is the population pyramid. It is a named chart type, and nothing in the kernel knows its name. It follows from rules that were already there, for other reasons.

The census table holds 36 rows: an age band, a city (Busan or Seoul), and a population for each of 18 bands per city. It is invented data, shaped like a census.

census: first 5 of 36 rows
age city population
0 Busan 5200
5 Busan 3100
10 Busan 2600
15 Busan 9000
20 Busan 38500

The mirror comes from a minus sign: one city’s populations are negated so its bars point the other way. That is arithmetic, so it happens in the table, before the sentence:

mirrored <- census
mirrored$population <- ifelse(mirrored$city == "Busan",
                              -mirrored$population, mirrored$population)
mirrored: first 5 of 36 rows
age city population
0 Busan -5200
5 Busan -3100
10 Busan -2600
15 Busan -9000
20 Busan -38500

The arithmetic is written in R, because it is R’s job, and the other three bindings negate a column 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

“Given the mirrored table: bars, y is age, x is population, color by city, ordered by age, largest first.”

No new atom, and none was needed. The pyramid comes from three rules the grammar already states. Orientation is read off the bindings, so a category on y lays the bars down. There is no coord_flip() as in ggplot2, and no barh() as in matplotlib, because either would be a second way to say one thing. A bar runs from the zero baseline to its value, in whichever direction that value lies. 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 on opposite sides of zero instead of stacking.

order(age, desc = TRUE) is the atom from the section above, doing exactly what it did there. gog puts the first category at the top of a horizontal chart, so age 0 would sit at the top. A pyramid needs its youngest band at the bottom, and desc = TRUE reverses the order.

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

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 40K, and only a reader who knows the convention can see which is which. That reader did not need the help. The reader who did need it is now given a plot that disagrees with its own axis. gog has no way to set your own tick text, and Coverage lists it among the capabilities with no decision recorded. The paragraph above is the case against adding one.

44.8 What this section refuses

shape separates categories on a point, one glyph per category, and you might expect the same on a bar. 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)
data(medals) + bar + x(col.country) + y(col.gold) + shape(col.country)
data(medals) + bar + x(:country) + y(:gold) + shape(:country)
plot(data(medals), bar, x(col.country), y(col.gold), shape(col.country))
Error:
! gog: `shape` cannot be bound to `bar` — a bar has no shape feature. Remove the `country` mapping from `shape`, or use a mark that has one.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.