| 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 |
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:
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))“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))“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:
| 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))“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:
| 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))“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)“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:
| 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 }))“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.
| 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)| 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 }))“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.