| 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 separate the groups. 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. Two of them also put the groups in an order you choose.
44.1 How do the groups’ distributions compare?
The first way is to split the marks. The iris_flowers table holds three species, and every row carries a petal length. The question is whether those lengths overlap or are separate. An overlaid histogram answers it: one histogram per species, drawn in one panel. bin cuts the axis into intervals, called bins, and it cuts one set of bins from all the rows, so every species is counted into the same bins. Nothing in the sentence below asks for transparency. The engine draws each species as a translucent fill under its own solid outline, so a fill that lies under another is still visible:
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.”
Setosa is separate from the other two and shares no bin with them. Versicolor and virginica overlap near 5 cm, where one fill lies over the other and the color looks darker. The solid outline is what keeps each species readable there. Owning chapters: Bar, Transforms, Channels.
44.2 What if the overlap gets worse?
Three translucent fills are still readable. With more groups, or with groups closer together, the overlaps blend into one dark band, and you can no longer see where one fill ends. Remove the fills entirely: step * bin draws each group’s outline as an unfilled staircase. A thin outline hides almost nothing behind it:
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.”
Only the mark changed, from bar to step. Each group is now one outline rather than a filled area, so far less of the plot is covered. Near 5 cm, versicolor’s outline steps down where virginica’s steps up, and both lines stay visible. Step owns the staircase.
44.3 What if the names do not matter?
Both recipes above gave each group its own color, and a legend matched each color to a name. Sometimes the grouping matters and the names do not: you want the shape of many series, not a legend naming each. group splits the rows by country, and line draws one stroke for each. group binds no color, so there is nothing for a legend to decode:
| 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. All five lines end higher than they start, and only one ever falls, between 1957 and 1962. Nothing on the plot says which country that is: what you read is the shape the five lines make together. Bind color instead when the reader needs each line’s country.
44.4 What if each group needs its own panel?
The first two recipes had only three groups to separate. The gapminder_2007 table holds five continents, and on one histogram up to four of their fills would lie in the same bin. Split the plot instead of the marks, so that each continent gets a panel of its own. Each panel counts only its own 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))“Given gapminder 2007: bars derived by bin, x is life, split into panel columns by continent.”
Each panel holds one continent’s distribution of life expectancy. Africa’s 52 countries run from about 40 to 76 years, and 40 of them are below 60. All 30 of Europe’s countries are above 70. Bins and the count axis are shared, so equal heights mean equal counts. Faceting owns the rules, including why the bins are shared while each panel counts only its own rows.
44.5 What if two columns group the rows?
The recipe above split the plot by one column, continent. This recipe splits the plot a second time. 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 gm_eras table holds the rows for those two years. It records each year twice, as the number year and as the text era:
| country | continent | year | life | population | gdp | era |
|---|---|---|---|---|---|---|
| Afghanistan | Asia | 1957 | 30.332 | 9240934 | 820.8530 | 1957 |
| Afghanistan | Asia | 2007 | 43.828 | 31889923 | 974.5803 | 2007 |
| Albania | Europe | 1957 | 59.280 | 1476505 | 1942.2842 | 1957 |
| Albania | Europe | 2007 | 76.423 | 3600523 | 5937.0295 | 2007 |
| Algeria | Africa | 1957 | 45.685 | 10270856 | 3013.9760 | 1957 |
data(gm_eras) + point + x(gdp, scale = "log") + y(life) |
facet(continent) / facet(era)(data(gm_eras) + point + x(col.gdp, scale = "log") + y(col.life) |
facet(col.continent) / facet(col.era))data(gm_eras) + point + x(:gdp, scale = "log") + y(:life) |
facet(:continent) / facet(:era)plot(data(gm_eras), point, x(col.gdp, { scale: "log" }), y(col.life),
across(col.continent), down(col.era))“Given the gapminder eras: points, x is gdp on a log scale, y is life, split into panel columns by continent and panel rows by era.”
Each panel column holds one continent, and its two panels show 1957 and 2007, fifty years apart. Stacked panels align their x axes, so read a panel column downward to see how income changed. To compare life expectancy, on y, put the years side by side instead, as Crossing into a grid does. The plot is split by era, not by year. A facet column must be categorical, because it names its panels, and year holds numbers. Over time shows the refusal you get for facet(year).
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. gog draws the categories of a column of text in the order their rows appear in the table, and order() lets you choose another order:
| 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)data(medals) + bar + x(col.country) + y(col.gold) + order(col.gold)data(medals) + bar + x(:country) + y(:gold) + order(:gold)plot(data(medals), bar, x(col.country), y(col.gold), order(col.gold))“Given the medals: bars, x is country, y is gold, ordered by gold, smallest first.”
The bars rise from left to right, from Germany’s 17 gold medals to the USA’s 46, so the country with the most gold comes last. order() names gold here, not the categorical column country. You name the column that holds the values, and the categories are sorted by those values.
One atom away. desc = TRUE puts the largest first. The table already lists the countries from most gold to least, so with desc = TRUE the bars keep the table’s own order. A sentence with no order() draws the same plot. Writing the order anyway keeps the plot sorted if a later version of the table lists the countries in a different order.
order() overrides a declared order as well. The next recipe uses it another way: it sorts by a column’s declared order, not by size.
44.7 Where does a population pyramid come from?
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 no atom in the kernel carries that name. You never ask for a pyramid; you describe one.
The census table has three columns: an age band, a city (Busan or Seoul), and a population. age holds the band names as text, from 0 to 85, in a declared order from youngest to oldest. Each city has 18 bands, so the table holds 36 rows. The numbers are invented; the columns are the ones a census would hold.
| age | city | population |
|---|---|---|
| 0 | Busan | 5200 |
| 5 | Busan | 3100 |
| 10 | Busan | 2600 |
| 15 | Busan | 9000 |
| 20 | Busan | 38500 |
The mirrored shape comes from one minus sign. Busan’s populations are negated, so Busan’s bars run to the left of zero. 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 negation above is shown in R only; Python, Julia and JavaScript each negate a column in their own way. What follows is the sentence:
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. First, the engine takes the orientation from the bindings, so a category on y makes the bars horizontal. Second, a bar runs from the zero baseline to its value, on whichever side of zero that value is, so a negative bar is not a special case. It is the same rectangle, extending the other way. Third, stack is something you ask for, never something the engine does on its own. This sentence does not ask for it, so the two bars in one slot are on opposite sides of zero. If you know ggplot2’s coord_flip() or matplotlib’s barh(), gog has no such word, because it would be a second way to say one thing.
order(age, desc = TRUE) is the atom from the medals recipe, used in a second way. There it named gold, a different column from the one on the axis. Here it names the categorical column itself, so it asks for that column’s own order: the declared order, youngest first. desc = TRUE reverses it. gog puts the first category at the top when the category is on y, so without desc = TRUE age 0 would be at the top. A pyramid needs its youngest band at the bottom.
Now look at the left half of the population axis. It reads -40K, not 40K.
Many versions of this chart replace each tick’s text with its absolute value. The minus sign that placed one city’s bars then disappears. gog keeps every tick showing the number at its own position, and this plot shows why. If the ticks were relabeled, the axis would carry two ticks reading 40K. Only a reader who knows the convention would know that the left 40K marks the position -40K. That reader could read -40K just as well, so the relabeling does not help. Any other reader gets bars whose positions no longer match the numbers on the axis. gog gives you no way to write your own tick text, and Coverage lists that among the capabilities with no decision recorded. These reasons argue against adding one.
44.8 What this section refuses
Most recipes here separated groups with color, group or a panel. shape separates categories too, with one glyph per category on a point, 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:
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 maps it: `point`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The message says a bar has no shape feature, and tells you to remove shape or to use a mark that has one. point is the only mark that does. One rule table decides every pairing of a mark and a channel, and it makes no exception for any mark. The companion grid prints all of it.