29  Composition

How do you put two different plots in one figure? Faceting cannot do it: every panel it draws comes from one plot and one sentence. Composition takes plots that were never one plot and places them in one figure. Wilkinson separates the two, noting that “some tabled graphics are really two or more graphics glued together” (Wilkinson, 2005). gog separates them too, with the operators you have already learned. | places two things side by side, and / places one above the other, whether those things are panels or whole plots:

A facet splits one plot by a column of its data. An arrangement places finished plots side by side or one above the other, and says nothing about any plot’s data. | and / place things the same way in both cases. What you write after the operator decides what is placed: a facet() splits the plot into panels, and a second plot makes an arrangement.

29.1 Two plots, one arrangement

Two views of one table often belong side by side, even when they share no axis. Describe each plot in full, inside its own parentheses, then put | between them:

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) + point + x(gdp) + y(life)) |
  (data(gapminder_2007) + bar * count + x(continent))
((data(gapminder_2007) + point + x(col.gdp) + y(col.life)) |
  (data(gapminder_2007) + bar * count + x(col.continent)))
(data(gapminder_2007) + point + x(:gdp) + y(:life)) |
  (data(gapminder_2007) + bar * count + x(:continent))
beside(plot(data(gapminder_2007), point, x(col.gdp), y(col.life)),
  plot(data(gapminder_2007), layer(bar, count), x(col.continent)))
0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp Asia Europe Africa Americas Oceania 0 20 40 Count Continent

“Given gapminder 2007: points, x is gdp, y is life, beside bars derived by count, x is continent.”

The two plots measure entirely different things: a country’s income against its life expectancy, and how many countries each continent holds. They share nothing, not a scale, not an axis, not a legend, because there is nothing to share. Each plot is drawn in a cell, its own part of the arrangement. The two cells are equal, and a narrow gap between them leaves each a little under half the width. Inside each cell, a panel holds the marks, and the axes and their labels take the rest. The arrangement does align the two panels at the top and the bottom.

/ places the same two plots one above the other. Each one now has the full width of the arrangement:

(data(gapminder_2007) + point + x(gdp) + y(life)) /
  (data(gapminder_2007) + bar * count + x(continent))
((data(gapminder_2007) + point + x(col.gdp) + y(col.life)) /
  (data(gapminder_2007) + bar * count + x(col.continent)))
(data(gapminder_2007) + point + x(:gdp) + y(:life)) /
  (data(gapminder_2007) + bar * count + x(:continent))
below(plot(data(gapminder_2007), point, x(col.gdp), y(col.life)),
  plot(data(gapminder_2007), layer(bar, count), x(col.continent)))
0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp Asia Europe Africa Americas Oceania 0 20 40 Count Continent

“Given gapminder 2007: points, x is gdp, y is life, above bars derived by count, x is continent.”

One above the other, the two panels align at the left and the right, even when one plot’s tick labels are wider.

Here the parentheses are required. / groups before +, so without them / would join only y(life) and the second data(), and gog would refuse that sentence. With |, gog draws the arrangement without them in R and Python, but refuses it in Julia. So whenever this book composes plots, each plot goes inside its own parentheses, whichever operator joins them. Parentheses in an arrangement shows what each language’s order does to an arrangement.

Both operators take arrangements as well as plots, so one arrangement can hold another. (a | b) / c is a row of two plots above a third. The marginal plot later in this chapter is built the other way: one plot above a row of two.

In R, Python and Julia, / groups before |, so a | b / c reads as a | (b / c). Write the parentheses whenever you mix the two operators. JavaScript spells the operators as words, so there is no order to remember. The JavaScript chapter shows those words.

29.2 A column two plots share

The two plots in those figures share no column. One shared column changes what the arrangement draws. When two composed plots name the same column on the same axis and read it the same way, with the same scale (and its base) and the same limits, gog fits one scale to both. The arrangement decides whether they also share one drawn axis. Plots one above the other align their x axes, and plots side by side align their y axes. When both plots name the column on the axis the arrangement aligns, gog does not draw two matching axes. It draws one axis:

The same column, read the same way, on the aligned axis of two composed plots is one axis: one scale, one panel extent, drawn once.

The shared column may be on the other axis instead, such as gdp on x in two plots side by side. Each plot then draws its own copy of that axis from the one scale, so both copies mark the same values. Here a histogram of Europe’s countries, from the gm_europe table, is drawn beside the scatter of the whole world:

gm_europe: first 5 of 30 rows
country continent year life population gdp
Albania Europe 2007 76.423 3600523 5937.030
Austria Europe 2007 79.829 8199783 36126.493
Belgium Europe 2007 79.441 10392226 33692.605
Bosnia and Herzegovina Europe 2007 74.852 4552198 7446.299
Bulgaria Europe 2007 73.005 7322858 10680.793
(data(gm_europe) + bar * bin + x(gdp)) |
  (data(gapminder_2007) + point + x(gdp) + y(life))
((data(gm_europe) + bar * bin + x(col.gdp)) |
  (data(gapminder_2007) + point + x(col.gdp) + y(col.life)))
(data(gm_europe) + bar * bin + x(:gdp)) |
  (data(gapminder_2007) + point + x(:gdp) + y(:life))
beside(plot(data(gm_europe), layer(bar, bin), x(col.gdp)),
  plot(data(gapminder_2007), point, x(col.gdp), y(col.life)))
0 10K 20K 30K 40K 50K 0 2 4 6 8 Count Gdp 0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

“Given gapminder Europe: bars derived by bin, x is gdp; beside points from gapminder 2007, x is gdp, y is life.”

Each plot draws its own x axis, and both run from 0 to 50K. Europe’s poorest country, Albania, has a GDP per person of about 5,900 dollars. Drawn alone, the histogram’s axis would start near 6K. Here it starts at 0, because the two plots read gdp on one scale, fitted to both tables.

Now stack two plots that both read gdp on x. The theme(height = 150) on the first asks for 150 pixels of the arrangement’s height:

(data(gapminder_2007) + bar * bin + x(gdp) + theme(height = 150)) /
  (data(gapminder_2007) + point + x(gdp) + y(life))
((data(gapminder_2007) + bar * bin + x(col.gdp) + theme(height = 150)) /
  (data(gapminder_2007) + point + x(col.gdp) + y(col.life)))
(data(gapminder_2007) + bar * bin + x(:gdp) + theme(height = 150)) /
  (data(gapminder_2007) + point + x(:gdp) + y(:life))
below(plot(data(gapminder_2007), layer(bar, bin), x(col.gdp),
  theme({ height: 150 })),
  plot(data(gapminder_2007), point, x(col.gdp), y(col.life)))
0 20 40 60 Count 0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

“Given gapminder 2007: bars derived by bin, x is gdp, above points, x is gdp, y is life.”

Look at what the shared-axis rule did. Both plots measure gdp over the same range, so each bar is drawn directly above the points it counts. And the gdp axis is drawn once, along the bottom, because there is only one axis to draw. Faceting makes the same decision when it draws the tick labels only under the panels with nothing below them.

A plot that does not draw an axis reserves no margin for it. That is why the histogram is close to the scatter, with no band of tick labels between the two panels.

gog cannot know what a column measures, so in an arrangement it compares column names. Two plots built from different tables share a scale whenever both name a column spelled the same way on the same axis, and read it the same way. That is correct when the two columns measure the same thing, such as gdp in two tables of countries. When they measure different things, rename one of them before you compose. Otherwise the plot with the smaller values is drawn in a small part of an axis sized for the larger.

There is one case where gog does know what an axis measures, because its own transform changed it. With stack(share = TRUE) or proportion, an axis that names a column shows shares, numbers from 0 to 1, instead of that column’s values. gog never merges such an axis with another, so the plot draws its own. The Bar chapter shows such an arrangement: two plots of people use one axis, and the plot of shares beside them draws its own.

Now let two plots read one column two ways. Write x(gdp, scale = "log") in one plot and plain x(gdp) in the other. Each plot then keeps its own scale and draws its own axis. One column drawn on both scales is the usual way to show what a log scale does. Both axes show gdp, so both are labeled Gdp unless you say otherwise. gog never writes the scale into an axis label (A scale is not a transform). Write it yourself with x_label(), which replaces the column name under the axis (Labels):

(data(gapminder_2007) + bar * bin + x(gdp) + x_label("GDP per person")) /
  (data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
     x_label("GDP per person (log scale)"))
((data(gapminder_2007) + bar * bin + x(col.gdp) + x_label("GDP per person")) /
  (data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
     x_label("GDP per person (log scale)")))
(data(gapminder_2007) + bar * bin + x(:gdp) + x_label("GDP per person")) /
  (data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
  x_label("GDP per person (log scale)"))
below(plot(data(gapminder_2007), layer(bar, bin), x(col.gdp),
  x_label("GDP per person")),
  plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
  y(col.life), x_label("GDP per person (log scale)")))
10K 20K 30K 40K 0 20 40 60 Count GDP per person 1K 10K 40 50 60 70 80 Life GDP per person (log scale)

“Given gapminder 2007: bars derived by bin, x is gdp, above points, x is gdp on a log scale, y is life.”

On the linear axis above, about half the countries fall into the first bar. On the log axis below, the same countries spread out from about 300 dollars to 50,000. The two plots are one above the other, but each draws its own gdp axis, because one axis cannot show a linear scale and a log scale at once. The labels tell a reader which axis is which before they read a tick. The rule is the same side by side, and for limits: two plots that state different limits for one column each keep their own scale and axis.

Two plots can also state the same limits with one end left open. limits = c(0, NA) states the lower end and leaves the upper one missing (Scales). When both plots state it for gdp, they read gdp the same way, so they share one axis. The axis starts at 0, as both plots ask, and gog fits its upper end to the rows of both tables. Here the ten countries with the most people in 2007, from the gm_populous table, are drawn above the whole world:

gm_populous: first 5 of 10 rows
country continent year life population gdp
China Asia 2007 72.961 1318683096 4959.115
India Asia 2007 64.698 1110396331 2452.210
United States Americas 2007 78.242 301139947 42951.653
Indonesia Asia 2007 70.650 223547000 3540.652
Brazil Americas 2007 72.390 190010647 9065.801
(data(gm_populous) + point + x(gdp, limits = c(0, NA)) + y(life)) /
  (data(gapminder_2007) + point + x(gdp, limits = c(0, NA)) + y(life))
((data(gm_populous) + point + x(col.gdp, limits = [0, None]) + y(col.life)) /
  (data(gapminder_2007) + point + x(col.gdp, limits = [0, None]) + y(col.life)))
(data(gm_populous) + point + x(:gdp, limits = [0, missing]) + y(:life)) /
  (data(gapminder_2007) + point + x(:gdp, limits = [0, missing]) +
  y(:life))
below(plot(data(gm_populous), point, x(col.gdp, { limits: [0, null] }),
  y(col.life)),
  plot(data(gapminder_2007), point, x(col.gdp, { limits: [0, null] }),
  y(col.life)))
40 50 60 70 80 Life 0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

“Given the ten most populous countries: points, x is gdp from 0, y is life; above points from gapminder 2007, x is gdp from 0, y is life.”

The United States is the richest of the ten, at about 43,000 dollars, and it is directly above its own point in the world plot. The shared axis still runs past 50K, because its upper end is fitted to the world’s richest country too.

29.2.1 One axis, but not one legend

You have seen an axis merge, so you might expect two legends for one column to merge the same way. Compose two plots that both map color and you get two legends, one for each:

(data(gapminder_2007) + point + x(gdp) + y(life) + color(continent)) |
  (data(gapminder_2007) + point + x(population) + y(life) + color(continent))
((data(gapminder_2007) + point + x(col.gdp) + y(col.life) + color(col.continent)) |
  (data(gapminder_2007) + point + x(col.population) + y(col.life) + color(col.continent)))
(data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent)) |
  (data(gapminder_2007) + point + x(:population) + y(:life) +
  color(:continent))
beside(plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  color(col.continent)),
  plot(data(gapminder_2007), point, x(col.population), y(col.life),
  color(col.continent)))
0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp Continent Asia Europe Africa Americas Oceania 0 500M 1000M Population Continent Asia Europe Africa Americas Oceania

“Given gapminder 2007: points, x is gdp, y is life, color by continent, beside points, x is population, y is life, color by continent.”

Look at what that figure draws once, and what it draws twice. Both plots read life on y, so that axis is shared and drawn once, on the left. Both color by continent as well, and there you get two legends, saying exactly the same thing.

Axes merge and legends do not, and that difference is deliberate. Axes merge because there is an exact test: when two plots read the same column the same way on the same axis, one scale is correct for both. A legend has no such test. These two plots do agree, because both read continent from one table. But each plot assigns its own colors, in the order the values first appear in its table. Color gapminder_2007 and gm_europe by continent, and Europe is orange in one plot and blue in the other. One legend over those two would be wrong for one of them. Nothing in the picture shows the error, because it still looks like a legend.

So gog merges no legend, and it never leaves one out on its own. By default, every plot in an arrangement draws its own legend. That is the safe choice, because a legend is always correct for the plot that draws it. But two identical legends side by side are redundant: they say the same thing twice, and each takes room from its plot.

Leaving one of them out is your decision. When you know that both plots give each value the same color, write legend = FALSE on one of them. It draws the channel without its legend (Leaving a legend out). Here legend = FALSE is on the left plot, so the arrangement keeps one legend, at its right edge.

A legend takes room, so the left plot would then use that room for its panel, and its panel would be wider than the right one. theme(width = 340) gives the left plot 340 of the arrangement’s 800 pixels, which makes the two panels the same width. The right number depends on how wide the legend is, so try a few and compare:

(data(gapminder_2007) + point + x(gdp) + y(life) +
   color(continent, legend = FALSE) + theme(width = 340)) |
  (data(gapminder_2007) + point + x(population) + y(life) + color(continent))
((data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
   color(col.continent, legend = False) + theme(width = 340)) |
  (data(gapminder_2007) + point + x(col.population) + y(col.life) + color(col.continent)))
(data(gapminder_2007) + point + x(:gdp) + y(:life) +
  color(:continent, legend = false) + theme(width = 340)) |
  (data(gapminder_2007) + point + x(:population) + y(:life) +
  color(:continent))
beside(plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  color(col.continent, { legend: false }), theme({ width: 340 })),
  plot(data(gapminder_2007), point, x(col.population), y(col.life),
  color(col.continent)))
0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp 0 500M 1000M Population Continent Asia Europe Africa Americas Oceania

“Given gapminder 2007: points, x is gdp, y is life, color by continent, without a legend, beside points, x is population, y is life, color by continent.”

gog does not check that one legend is correct for both plots, so you must. Here it is correct: both plots read continent from the same table, so each continent has the same color in both.

When your two plots could be one plot split by a column, use faceting instead. A facet colors every panel with one scale, so its single legend is correct for every panel. The choice between composing and faceting is the choice between two plots and one.

29.3 The marginal plot

A scatter shows how two columns vary together, but not how the rows spread along each one alone. Go back to the histogram that shared the scatter’s gdp axis. That histogram is a marginal: one column’s distribution, drawn along the edge of the plot that uses the column. Put a second marginal on the right and the picture is the marginal plot, which most tools offer as a named chart type:

(data(gapminder_2007) + bar * bin + x(gdp) + theme(height = 130)) /
  ((data(gapminder_2007) + point + x(gdp) + y(life)) |
     (data(gapminder_2007) + bar * bin + y(life) + theme(width = 130)))
((data(gapminder_2007) + bar * bin + x(col.gdp) + theme(height = 130)) /
  ((data(gapminder_2007) + point + x(col.gdp) + y(col.life)) |
     (data(gapminder_2007) + bar * bin + y(col.life) + theme(width = 130))))
(data(gapminder_2007) + bar * bin + x(:gdp) + theme(height = 130)) /
  ((data(gapminder_2007) + point + x(:gdp) + y(:life)) |
  (data(gapminder_2007) + bar * bin + y(:life) + theme(width = 130)))
below(plot(data(gapminder_2007), layer(bar, bin), x(col.gdp),
  theme({ height: 130 })),
  beside(plot(data(gapminder_2007), point, x(col.gdp), y(col.life)),
  plot(data(gapminder_2007), layer(bar, bin), y(col.life),
  theme({ width: 130 }))))
0 20 40 60 Count 0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp 0 10 20 30 Count

“Given gapminder 2007: bars derived by bin, x is gdp; above points, x is gdp, y is life, beside bars derived by bin, y is life.”

That figure is built from three ordinary plots and two operators. The top histogram shares gdp with the scatter, so its panel is drawn exactly as wide as the scatter’s. The right-hand one shares life, so its panel is exactly as tall as the scatter’s, and the life axis is drawn once, on the left. Nothing in the sentence draws the empty top-right corner. Both marginals stop where the scatter’s panel stops, and the corner is the room left over.

The marginals need no special rule either. bar * bin + y(life) is the histogram you already know. Its bars run left to right, because y now carries the bins and the count goes on x. A rule from A column two plots share also applies: a plot that does not draw an axis reserves no room for it. The scatter draws both shared axes, so only the gap between the plots separates each marginal from it.

A density curve can be drawn above the scatter instead of a histogram. The curve continues past the smallest and the largest gdp until it is nearly zero at both ends. Drawn alone, the curve’s gdp axis therefore starts below zero, where no country is. If those ends set the shared axis, the scatter’s axis would also start near −12,000, with an empty band on its left. Above the scatter, the shared axis covers only the rows of both plots, not the curve’s tails. The scatter’s axis is then the same as when it is drawn alone. The curve is cut to fit it, from the smallest to the largest gdp in the table:

(data(gapminder_2007) + line * density + x(gdp) + theme(height = 130)) /
  (data(gapminder_2007) + point + x(gdp) + y(life))
((data(gapminder_2007) + line * density + x(col.gdp) + theme(height = 130)) /
  (data(gapminder_2007) + point + x(col.gdp) + y(col.life)))
(data(gapminder_2007) + line * density + x(:gdp) + theme(height = 130)) /
  (data(gapminder_2007) + point + x(:gdp) + y(:life))
below(plot(data(gapminder_2007), layer(line, density), x(col.gdp),
  theme({ height: 130 })),
  plot(data(gapminder_2007), point, x(col.gdp), y(col.life)))
0.00000 0.00002 0.00004 Density 0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

“Given gapminder 2007: a line derived by density, x is gdp, above points, x is gdp, y is life.”

The curve starts high, at the poorest country, because it is cut there rather than falling to zero.

29.3.1 A shared categorical column

You may want each group’s summary drawn directly above the group’s own rows. The shared column need not be continuous for that. Two plots reading the same categorical column on x share that column’s slots, so each category keeps the same place in both:

(data(gapminder_2007) + box + x(continent) + y(life) + theme(height = 150)) /
  (data(gapminder_2007) + point + x(continent) + y(gdp) + color(continent))
((data(gapminder_2007) + box + x(col.continent) + y(col.life) + theme(height = 150)) /
  (data(gapminder_2007) + point + x(col.continent) + y(col.gdp) + color(col.continent)))
(data(gapminder_2007) + box + x(:continent) + y(:life) +
  theme(height = 150)) /
  (data(gapminder_2007) + point + x(:continent) + y(:gdp) +
  color(:continent))
below(plot(data(gapminder_2007), box, x(col.continent), y(col.life),
  theme({ height: 150 })),
  plot(data(gapminder_2007), point, x(col.continent), y(col.gdp),
  color(col.continent)))
40 50 60 70 80 Life Asia Europe Africa Americas Oceania 0 10K 20K 30K 40K 50K Gdp Continent Continent Asia Europe Africa Americas Oceania

“Given gapminder 2007: boxes, x is continent, y is life, above points, x is continent, y is gdp, color by continent.”

The five continents take the same slots in both panels, so each continent’s box is directly above that continent’s countries.

29.4 The scatterplot matrix

How do three columns vary together, when you want to see every pair in one figure? A scatterplot matrix answers with a grid of scatters, one for each pair of columns. Each pair appears twice, once on each side of the diagonal, with x and y swapped. The grid takes life, gdp and population in that order, from left to right and from top to bottom. Each plot puts its grid column’s name on x and its grid row’s name on y. On the diagonal, a plot would put one column on both x and y, which draws only a straight line. So the diagonal holds each column’s histogram, the picture the marginal plot drew along its edges. The figure is nine ordinary plots, three in each grid row, joined by |, with the three grid rows joined by /:

((data(gapminder_2007) + bar * bin + x(life)) |
   (data(gapminder_2007) + point + x(gdp, scale = "log") + y(life)) |
   (data(gapminder_2007) + point + x(population, scale = "log") + y(life))) /
((data(gapminder_2007) + point + x(life) + y(gdp, scale = "log")) |
   (data(gapminder_2007) + bar * bin + x(gdp, scale = "log")) |
   (data(gapminder_2007) + point + x(population, scale = "log") +
      y(gdp, scale = "log"))) /
((data(gapminder_2007) + point + x(life) + y(population, scale = "log")) |
   (data(gapminder_2007) + point + x(gdp, scale = "log") +
      y(population, scale = "log")) |
   (data(gapminder_2007) + bar * bin + x(population, scale = "log")))
(((data(gapminder_2007) + bar * bin + x(col.life)) |
   (data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life)) |
   (data(gapminder_2007) + point + x(col.population, scale = "log") + y(col.life))) /
((data(gapminder_2007) + point + x(col.life) + y(col.gdp, scale = "log")) |
   (data(gapminder_2007) + bar * bin + x(col.gdp, scale = "log")) |
   (data(gapminder_2007) + point + x(col.population, scale = "log") +
      y(col.gdp, scale = "log"))) /
((data(gapminder_2007) + point + x(col.life) + y(col.population, scale = "log")) |
   (data(gapminder_2007) + point + x(col.gdp, scale = "log") +
      y(col.population, scale = "log")) |
   (data(gapminder_2007) + bar * bin + x(col.population, scale = "log"))))
((data(gapminder_2007) + bar * bin + x(:life)) |
  (data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life)) |
  (data(gapminder_2007) + point + x(:population, scale = "log") +
  y(:life))) /
  ((data(gapminder_2007) + point + x(:life) + y(:gdp, scale = "log")) |
  (data(gapminder_2007) + bar * bin + x(:gdp, scale = "log")) |
  (data(gapminder_2007) + point + x(:population, scale = "log") +
  y(:gdp, scale = "log"))) /
  ((data(gapminder_2007) + point + x(:life) +
  y(:population, scale = "log")) |
  (data(gapminder_2007) + point + x(:gdp, scale = "log") +
  y(:population, scale = "log")) |
  (data(gapminder_2007) + bar * bin + x(:population, scale = "log")))
below(below(beside(beside(plot(data(gapminder_2007), layer(bar, bin),
  x(col.life)),
  plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
  y(col.life))),
  plot(data(gapminder_2007), point, x(col.population, { scale: "log" }),
  y(col.life))),
  beside(beside(plot(data(gapminder_2007), point, x(col.life),
  y(col.gdp, { scale: "log" })),
  plot(data(gapminder_2007), layer(bar, bin),
  x(col.gdp, { scale: "log" }))),
  plot(data(gapminder_2007), point, x(col.population, { scale: "log" }),
  y(col.gdp, { scale: "log" })))),
  beside(beside(plot(data(gapminder_2007), point, x(col.life),
  y(col.population, { scale: "log" })),
  plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
  y(col.population, { scale: "log" }))),
  plot(data(gapminder_2007), layer(bar, bin),
  x(col.population, { scale: "log" }))))
0 10 20 30 Count 40 50 60 70 80 Life 1K 10K Gdp 0 10 20 Count 40 50 60 70 80 1M 10M 100M 1B Population Life 1K 10K Gdp 1M 10M 100M 1B 0 10 20 30 Count Population

The shared-axis rule decides which axes are drawn. Every plot in a grid column names the same x, so that axis is drawn once, along the bottom. In each grid row, the two scatters name the same y, so they share one scale, and one value is at the same height in both. population runs from about 200,000 people to 1.3 billion, and gdp from about 280 dollars to 49,000, so both are read on a log scale. Every plot that names one of them writes scale = "log", because a column read two ways gets two axes.

Each histogram puts the count on y, which no other plot in its grid row does, so it draws its own Count axis. In the top row the histogram is the leftmost plot, and its y is the count, not life. So the life axis is drawn on the left of the middle plot, the first plot in that row with life on y.

The two plots of gdp and life show the clearest pattern: people in richer countries live longer. population shows almost no pattern with either of the other two, so a country’s size tells you little about its income or its life expectancy.

Many tools offer this figure as a function of its own. In gog it is nine ordinary sentences, written with words you already know. Wilkinson counts this figure as a kind of facet (Faceting). gog’s facet() cannot draw it: a facet draws one sentence in every panel, and here the plots read different columns.

29.5 A clustered panel decides the order

A categorical axis needs an order, and most plots take the one the data arrived in. A panel with cluster computes a better one. cluster builds a tree that joins the most similar categories first, and reading its leaves along the axis gives an order that puts similar categories next to each other. When a clustered panel is composed with another, the axis they share takes the order the tree computed.

A shared categorical axis takes the order a clustered panel computed for it.

The other panels need no cluster of their own, and their categories take the tree’s order. gog refuses two panels that derive different orders for one shared axis, because one axis cannot hold two orders. Give both the same columns to cluster(), or cluster in one panel alone. The Cluster chapter uses this rule: a tile plot with a tree on two of its sides, each tree deciding one axis.

29.6 How thin is the marginal histogram? theme(width =, height =)

In the marginal plot, both histograms are thin because each one asks for a small size. The top histogram asks for 130 pixels of height, and the right-hand one for 130 pixels of width. A plot asks for room in pixels with theme(width = ) and theme(height = ). The plots that ask for nothing divide what is left, so the scatter takes the rest.

On a plot drawn alone, the same two arguments set the size of the whole image:

data(gapminder_2007) + point + x(gdp) + y(life) + theme(width = 420, height = 320)
data(gapminder_2007) + point + x(col.gdp) + y(col.life) + theme(width = 420, height = 320)
data(gapminder_2007) + point + x(:gdp) + y(:life) +
  theme(width = 420, height = 320)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  theme({ width: 420, height: 320 }))
0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

Alone or in an arrangement, the two arguments mean the same thing, which is Compositional Invariance, the sixth of the nine laws. A plot that asks for 130 pixels of height is 130 pixels tall, whether it is drawn alone or above another plot. Beside another plot it is also 130 pixels tall, and the plot beside it takes the same height.

Do not confuse theme(width =, height =) with theme(ratio = ). The second one shapes the panel inside the room the plot was given, and it never resizes the image. When a plot states a ratio, the arrangement does not align its panel with the other panels.

29.7 How big is the arrangement itself?

An arrangement that states no size is 800 by 600 pixels, the same as a plot that states none. Its plots can decide a different size. Plots one above the other that all ask for a height make the arrangement as tall as their heights plus the gaps. Plots side by side that all ask for a width make it as wide as their widths plus the gaps. Each gap is 20 pixels unless the arrangement sets its own with theme(gap = ), shown at the end of this section.

Plots side by side divide the arrangement’s width and share one height. A height written into one of them is the height of all of them, and of the arrangement. Plots side by side that ask for different heights are refused. Plots one above the other share one width, by the same rule. The marginal plot stays 800 by 600 pixels as a whole, because the scatter asks for nothing.

A plot that asks for nothing still needs room, so the other plots must leave some of the arrangement’s height for it:

(data(gapminder_2007) + point + x(gdp) + y(life) + theme(height = 700)) /
  (data(gapminder_2007) + bar * count + x(continent))
((data(gapminder_2007) + point + x(col.gdp) + y(col.life) + theme(height = 700)) /
  (data(gapminder_2007) + bar * count + x(col.continent)))
(data(gapminder_2007) + point + x(:gdp) + y(:life) +
  theme(height = 700)) /
  (data(gapminder_2007) + bar * count + x(:continent))
below(plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  theme({ height: 700 })),
  plot(data(gapminder_2007), layer(bar, count), x(col.continent)))
Error:
! gog: the plots below each other ask for 700px of height, and the 20px between them makes 720px; the arrangement has 600. A `theme(height = )` on a composed plot is how much of the arrangement that plot takes, so the ones that state it must leave room for the ones that do not.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

The bottom plot asks for nothing, so the arrangement keeps its 600 pixels. gog added the top plot’s 700 pixels and the 20-pixel gap, and compared 720 with those 600.

When the plots do not decide the arrangement’s size, you can choose it: give the arrangement its own theme(). Put parentheses around the whole arrangement before you add theme() to it. In R and Python, + groups before |, so without them the theme() would go to the second plot instead of the arrangement. Julia would give it to the arrangement, so the parentheses also make the sentence mean the same thing in every language:

((data(gapminder_2007) + point + x(gdp) + y(life)) |
   (data(gapminder_2007) + bar * count + x(continent))) + theme(height = 260)
(((data(gapminder_2007) + point + x(col.gdp) + y(col.life)) |
   (data(gapminder_2007) + bar * count + x(col.continent))) + theme(height = 260))
((data(gapminder_2007) + point + x(:gdp) + y(:life)) |
  (data(gapminder_2007) + bar * count + x(:continent))) +
  theme(height = 260)
beside(plot(data(gapminder_2007), point, x(col.gdp), y(col.life)),
  plot(data(gapminder_2007), layer(bar, count), x(col.continent)),
  theme({ height: 260 }))
0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp Asia Europe Africa Americas Oceania 0 20 40 Count Continent

The arrangement is now 260 pixels tall instead of 600, and both plots take that full height. An arrangement takes the same two size arguments as a plot.

In this sentence the outer parentheses happen to change nothing. Without them, R and Python give theme(height = 260) to the second plot, but plots side by side always share one height, so the arrangement is still 260 pixels tall. With a width, the parentheses matter. (plot_a | plot_b) + theme(width = 500) makes the whole arrangement 500 pixels wide. Without the parentheses, the arrangement stays 800 pixels wide: the second plot takes 500 of them, and the first plot is squeezed into the other 280.

Sizing the arrangement matters most when a plot cannot be stretched to fill its cell. A cube is drawn with one scale in both screen directions, so its shape is never stretched. In a tall, narrow cell the cube is therefore drawn small, with an empty band above it and another below it. The figure is then the wrong shape, and you correct it by sizing the arrangement.

An arrangement can also set the space between its plots. theme(gap = ) takes a number of pixels, 0 or more. Written on the outermost arrangement, it also sets the gaps inside each arrangement nested in it, unless that arrangement sets its own. Here the marginal plot’s histograms are drawn closer to the scatter than the default 20 pixels:

((data(gapminder_2007) + bar * bin + x(gdp) + theme(height = 130)) /
  ((data(gapminder_2007) + point + x(gdp) + y(life)) |
     (data(gapminder_2007) + bar * bin + y(life) + theme(width = 130)))) +
  theme(gap = 4)
(((data(gapminder_2007) + bar * bin + x(col.gdp) + theme(height = 130)) /
  ((data(gapminder_2007) + point + x(col.gdp) + y(col.life)) |
     (data(gapminder_2007) + bar * bin + y(col.life) + theme(width = 130)))) +
  theme(gap = 4))
((data(gapminder_2007) + bar * bin + x(:gdp) + theme(height = 130)) /
  ((data(gapminder_2007) + point + x(:gdp) + y(:life)) |
  (data(gapminder_2007) + bar * bin + y(:life) + theme(width = 130)))) +
  theme(gap = 4)
below(plot(data(gapminder_2007), layer(bar, bin), x(col.gdp),
  theme({ height: 130 })),
  beside(plot(data(gapminder_2007), point, x(col.gdp), y(col.life)),
  plot(data(gapminder_2007), layer(bar, bin), y(col.life),
  theme({ width: 130 }))), theme({ gap: 4 }))
0 20 40 60 Count 0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp 0 10 20 30 Count

An arrangement takes three settings and no others: width, height and gap. Every other property theme() sets belongs to one plot: its gridlines, its frame, its background, the angle of its tick labels. An arrangement is not a plot, so each of those properties is refused, and the refusal says to write it into the plot it describes:

((data(gapminder_2007) + point + x(gdp) + y(life)) |
  (data(gapminder_2007) + bar * count + x(continent))) + theme(grid = "none")
(((data(gapminder_2007) + point + x(col.gdp) + y(col.life)) |
  (data(gapminder_2007) + bar * count + x(col.continent))) + theme(grid = "none"))
((data(gapminder_2007) + point + x(:gdp) + y(:life)) |
  (data(gapminder_2007) + bar * count + x(:continent))) +
  theme(grid = "none")
beside(plot(data(gapminder_2007), point, x(col.gdp), y(col.life)),
  plot(data(gapminder_2007), layer(bar, count), x(col.continent)),
  theme({ grid: "none" }))
Error:
! gog: `theme(grid = )` describes a panel, and an arrangement is plots placed together rather than a panel of its own. On an arrangement, `theme()` states how big the figure is and how far apart its plots are — `theme(width = )`, `theme(height = )` and `theme(gap = )` — and nothing else. Write this into the plot it describes, before composing: `(plot + theme(grid = )) | other_plot`.

29.8 What an arrangement is not

A title over two plots seems to belong to the arrangement rather than to either plot, so you might try writing it there. An arrangement only places plots, so anything belonging to one plot is written into that plot before it is composed. An arrangement accepts theme(width = ), theme(height = ) and theme(gap = ). Every other atom added to an arrangement is refused, and the message names where it belongs:

((data(gapminder_2007) + point + x(gdp) + y(life)) |
  (data(gapminder_2007) + bar * count + x(continent))) + title("Two views")
(((data(gapminder_2007) + point + x(col.gdp) + y(col.life)) |
  (data(gapminder_2007) + bar * count + x(col.continent))) + title("Two views"))
((data(gapminder_2007) + point + x(:gdp) + y(:life)) |
  (data(gapminder_2007) + bar * count + x(:continent))) +
  title("Two views")
beside(plot(data(gapminder_2007), point, x(col.gdp), y(col.life)),
  plot(data(gapminder_2007), layer(bar, count), x(col.continent)),
  title("Two views"))
Error:
! gog: `title()` belongs to a plot, and the left side is an arrangement of them. Write it into the plot it describes, before composing: `(plot + title("...")) | other_plot`.

gog draws no title over a whole arrangement. In an arrangement of plots one above the other, the top plot’s title is above everything else, and the bottom plot’s caption is below everything else. When the plots are side by side, write the title in your document, as a line of text above the figure.

And an arrangement cannot be faceted, because a facet splits one plot by a column and an arrangement is not one plot:

((data(gapminder_2007) + point + x(gdp) + y(life)) |
  (data(gapminder_2007) + bar * count + x(continent))) | facet(continent)
(((data(gapminder_2007) + point + x(col.gdp) + y(col.life)) |
  (data(gapminder_2007) + bar * count + x(col.continent))) | facet(col.continent))
((data(gapminder_2007) + point + x(:gdp) + y(:life)) |
  (data(gapminder_2007) + bar * count + x(:continent))) |
  facet(:continent)
Error:
! gog: `|` faceted an arrangement of plots, and a facet splits *one* plot by a column. Facet the plots before composing them: `(plot | facet(g)) | other_plot`.

Facet first, then compose: (plot | facet(g)) | other_plot is a faceted plot beside another plot, and it is legal.

29.9 Where these operators come from

You have now seen | mean two things. In plot | facet(continent) it splits one plot into panels, and in plot_a | plot_b it puts two plots side by side. Neither meaning is this grammar’s invention, and the two were borrowed from different places.

Splitting a plot into panels is the older use of |. Trellis display wrote a conditioned plot as y ~ x | g (Becker et al., 1996), and lattice writes it that way still. There | means “given”, the same meaning it has in conditional probability. That is exactly what plot | facet(continent) says.

Putting plots side by side with | comes from patchwork (Pedersen, 2019). That package writes p1 | p2 for side by side and p1 / p2 for one above the other. gog spells composition the same way on purpose. A reader who already uses that package already knows both operators. A different spelling for an idea that already has one would only be one more thing to learn.

Composing pictures with operators is older than both of them. Henderson’s Functional Geometry (Henderson, 1982) describes an algebra of pictures, and it places pictures with beside and above. The textbook Structure and Interpretation of Computer Programs teaches the same picture language. Haskell’s diagrams library spells the two operations ||| and ===. The JavaScript binding has no operators to overload, so it spells the two operations as words: beside(), and below() for what Henderson called above. His above(a, b) and gog’s below(a, b) draw the same picture, with a on top and b under it.

gog follows Wilkinson in most of its decisions, and this is one place where it does not. His graph algebra also has a /, and there it means nesting (a/b, “a within b”), not one plot above another. That nesting is not gog’s nest() atom, which divides one panel into areas sized by a measure. gog’s facet operators always cross and never nest, as the Faceting chapter explains, so gog can use / to place one plot above another.

29.10 Layering, faceting, or composition?

Layering, faceting and composition answer different questions, so your data decides which one you want:

  • Layering. The layers draw different tables that measure the same thing. One coordinate space, one pair of axes, and the second table annotates the first: a threshold, a band, a note, a forecast. Data shows how to add a second table.
  • Faceting. The panels show the same measurement for different subsets. One scale, one legend, and you can compare the panels directly.
  • Composition. The plots show different measurements that a reader should see in one figure. Each keeps its own space, and the only thing they may share is a column.

In layering, the engine cannot check that a second table measures what the axes measure, so you have to check it yourself. A second table that measures something else still draws when you layer it: gog finds every column, draws every mark, and the picture is wrong. A sentence that puts sales figures on an income axis is legal, and gog draws it. So if the second table does not measure what the axes measure, compose two plots instead of layering one onto the other.

A marginal plot is composition because the histogram and the scatter measure different things: one counts countries, the other puts each country at its own position. They share gdp, and that one shared column is what the shared-axis rule turns into a single axis.

29.11 In short

| and / place the panels of one plot after a facet(), and whole plots in an arrangement. Composed plots that read the same column the same way, on the same axis, share one scale. On the axis the arrangement aligns, that scale is one axis, drawn once. A column read two ways gets two axes. An arrangement draws no mark of its own, and takes only its size and the gap between its plots.

29.12 What comes next

Composition places finished plots side by side or one above the other, and none of those plots shows where each part of a total went. Flow draws that: it carries each amount through a series of stages, read from the table’s own columns.