28  Faceting

How do you compare the same plot across groups? A facet splits one plot into small multiples: one panel per category of a column. A frame is the space a plot is drawn in, with its own x and y axes. Wilkinson calls a facet a frame of frames. The facet column’s categories make the outer frame, and each of its cells is a panel. Each panel is an inner frame: the same plot, drawn from one category’s rows. The marks, channels, and transforms you have already learned draw every panel, and by default all the panels share the same scales.

The picture has been invented more than once, so you may know it under another name. Becker and Cleveland’s group at Bell Labs built it into S, the language R was modeled on, as Trellis display (Becker et al., 1996). The name comes from the garden trellis, because the panels are arranged in rows and columns like one. R still draws it this way in the lattice package (Sarkar, 2026), and the package’s own book explains the display in full (Sarkar, 2008). Base R’s coplot() is the same idea under a third name, the conditioning plot, because what a trellis conditions on is the facet column. That is also why the operator in the next section is |, the sign that statistics already uses for conditioning. Composition gives | a second meaning, and says where both meanings come from.

Tufte’s term, small multiples (Tufte, 1990), names the result rather than the operation, and this book keeps the two apart: small multiples for the picture, facet for the thing you write.

28.1 |: panels side by side

The scatter of income against life expectancy draws the countries of all five continents in one panel. You want to see each continent on its own without losing the comparison between them. The facet() atom names the column that does the splitting, and | arranges the panels in columns:

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) | facet(continent)
data(gapminder_2007) + point + x(col.gdp) + y(col.life) | facet(col.continent)
data(gapminder_2007) + point + x(:gdp) + y(:life) | facet(:continent)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  across(col.continent))
0 20K 40K 40 50 60 70 80 0 20K 40K 0 20K 40K 0 20K 40K 0 20K 40K Asia Europe Africa Americas Oceania Life Gdp

“Given gapminder 2007: points, x is gdp, y is life, split into panel columns by continent.”

The panels share both axes, so a difference between continents is as easy to see as a pattern within one. The shared scale is what faceting is for. Five separate plots, each with its own scale, would look almost the same as these five panels. Separate scales break the comparison, and nothing on the page warns the reader.

A facet joins the plot with | or /, never with +, because the operator decides where the panels go: | puts them in columns, / in rows. Write + facet(continent) and gog refuses, and the message names | and /.

28.2 /: panels stacked

Are lives in Africa shorter than in Europe, and by how much? That is a question about where each distribution is along one axis. The plot also draws the Americas as a third panel. The three_continents table holds the 2007 rows of these three continents. / stacks the panels in rows. Stacking the panels aligns their x axes, so choose rows when the question is where the values are along x:

three_continents: first 5 of 107 rows
country continent year life population gdp
Albania Europe 2007 76.423 3600523 5937.030
Algeria Africa 2007 72.301 33333216 6223.367
Angola Africa 2007 42.731 12420476 4797.231
Argentina Americas 2007 75.320 40301927 12779.380
Austria Europe 2007 79.829 8199783 36126.493
data(three_continents) + bar * bin + x(life) / facet(continent)
data(three_continents) + bar * bin + x(col.life) / facet(col.continent)
data(three_continents) + bar * bin + x(:life) / facet(:continent)
plot(data(three_continents), layer(bar, bin), x(col.life),
  down(col.continent))
0 5 10 15 0 5 10 15 50 60 70 80 0 5 10 15 Europe Africa Americas Count Life

“Given the three continents: bars derived by bin, x is life, split into panel rows by continent.”

Each panel’s bars are directly above the bars covering the same span of life expectancy in the panel below. Africa’s life expectancies spread wide toward the low end, Europe’s cluster in a narrow band at the high end, and the Americas’ fall between the two, closer to Europe’s.

That reading only works because the bars are the same width in all three panels. A histogram under a facet needs two separate decisions: which rows are counted, and where the bins are cut. Each panel counts its own rows, because a statistic is computed on the panel’s subset as if that subset were the whole table. That is true of every transform on every mark, which is the No Exceptions law applied one panel at a time.

But bin is not only a statistic. It cuts the axis into bins and counts the rows in each one. The cut is a set of positions on the x axis, so it belongs with the shared scale. One set of bins is cut from all three panels’ rows at once, and each panel then counts only its own rows into those bins.

Suppose each panel cut its own bins. Europe’s narrow spread would get narrow bins and Africa’s wide spread would get wide ones, so bars of different widths would be drawn against a single axis. An African bar about five years wide and a European bar under two years wide could each hold eight countries, and so be drawn at the same height. But the African bar holds about one and a half countries for each year of its width, and the European bar nearly five: more than three times as many. The panels are still aligned and the axis still looks correct, and yet a reader cannot compare the three distributions.

Like bin, density is not only a statistic. Its bandwidth, which sets how smooth each curve is, belongs to the whole plot, as the cut does: it is the average of the bandwidths each curve would get from its own rows. Each panel estimates its curve from its own rows with that bandwidth. A difference in shape between panels then comes from the data and not from the smoothing.

data(three_continents) + area * density + x(life) / facet(continent)
data(three_continents) + area * density + x(col.life) / facet(col.continent)
data(three_continents) + area * density + x(:life) / facet(:continent)
plot(data(three_continents), layer(area, density), x(col.life),
  down(col.continent))
0.00 0.05 0.10 0.00 0.05 0.10 40 60 80 0.00 0.05 0.10 Europe Africa Americas Density Life

“Given the three continents: an area derived by density, x is life, split into panel rows by continent.”

In this plot, Europe’s curve has one peak, near 80 years. Drawn alone, Europe gets a bandwidth of its own. Europe’s spread is narrow, so its own bandwidth is narrower than the one the three panels share. A narrower bandwidth smooths less. The gm_europe table holds the thirty European countries of 2007:

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) + area * density + x(life)
data(gm_europe) + area * density + x(col.life)
data(gm_europe) + area * density + x(:life)
plot(data(gm_europe), layer(area, density), x(col.life))
70 75 80 85 0.00 0.05 0.10 Density Life

Drawn alone, Europe’s curve has two peaks, near 75 and near 80 years. Its x axis also covers a narrower range, so the same curve is spread across more of the page. Spreading a curve out does not add a peak. The two curves for Europe come from the same thirty rows, and only the bandwidth differs.

If each panel used its own bandwidth, Europe’s panel would show these two peaks. Africa’s curve, with the widest spread, would be smoother than it is in the faceted plot. A reader would then mistake a difference in smoothing for a difference between the continents. One shared bandwidth has a drawback: it can hide detail in a continent with a narrow spread. To see one continent’s own shape, draw it alone, as the plot of gm_europe does.

28.3 Crossing into a grid

Every plot so far has split on one column. Two columns can matter at once. Here the question is how each continent’s countries changed between 1957 and 2007, which is a split by continent and by year together. The gm_eras table holds those two years, with one row per country in each. It stores the year twice: year is a number, and era is the same year as a label. A facet needs the label, for the reason the facet column’s type gives. Write both operators to cross the two columns into a grid: every category of one is paired with every category of the other. Each operator applies to the facet written after it, read left to right.

gm_eras: first 5 of 284 rows
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

The change to compare is in life expectancy, on y. Panels side by side align their y axes, as stacked panels align their x axes. So the years go in columns with |, and the continents in rows with /. Five rows of panels share the plot’s height, which is 600 pixels unless the plot states another, so each row would get less than 120 pixels. theme(height = 1000) makes this plot 1000 pixels tall (theme(width =, height =)):

data(gm_eras) + point + x(gdp) + y(life) + theme(height = 1000) |
  facet(era) / facet(continent)
(data(gm_eras) + point + x(col.gdp) + y(col.life) + theme(height = 1000) |
  facet(col.era) / facet(col.continent))
data(gm_eras) + point + x(:gdp) + y(:life) + theme(height = 1000) |
  facet(:era) / facet(:continent)
plot(data(gm_eras), point, x(col.gdp), y(col.life),
  theme({ height: 1000 }), across(col.era), down(col.continent))
40 60 80 40 60 80 40 60 80 40 60 80 0 50K 100K 40 60 80 0 50K 100K 1957 2007 Asia Europe Africa Americas Oceania Life Gdp

“Given the gapminder eras: points, x is gdp, y is life, split into panel columns by era and panel rows by continent.”

Five continents and two years, fifty years apart, in one grid: every panel row is a continent, and every panel column a year. A continent’s two panels are side by side on one y axis, so the same life expectancy is at the same height in both. Read across a row to see the rise. In Europe’s row, all but one country is between 58 and 74 years in 1957, and every country between 71 and 82 in 2007.

The grid is a crossing in Wilkinson’s sense: every pair of a continent and a year gets a panel, even a pair the table holds no rows for. Every pair in this grid has rows, so no panel here is empty. When a pair has none, its panel is still drawn, and the empty panel shows that the pair is possible: it is information rather than an accident. Wilkinson’s other operation, nesting, draws only the pairs that exist. gog’s facet operators always cross. Despite its name, gog’s nest() atom is not Wilkinson’s nesting. It divides one panel into areas sized by a measure (Nest), and it makes no panels.

Facet is Wilkinson’s word too (Wilkinson, 2005), from the Latin facies (face): one side of a many-sided thing, like one face of a cut diamond. gog takes his word because it is the most general of the names for this picture. To him a trellis is one of several forms a facet can take, such as the scatterplot matrix, a grid with one scatterplot for each pair of columns. A grammar needs one term that covers them all.

28.4 Folding a long line of panels

A single facet column can cause a problem of its own. One column with many categories makes a long line of panels, and a long line of panels makes each one narrow. The gm_top table holds the ten most populous countries of 2007, with every year of the gapminder data for each. Each panel shows one country’s life expectancy from 1952 to 2007:

gm_top: first 5 of 120 rows
country continent year life population gdp
Bangladesh Asia 1952 37.484 46886859 684.2442
Bangladesh Asia 1957 39.348 51365468 661.6375
Bangladesh Asia 1962 41.216 56839289 686.3416
Bangladesh Asia 1967 43.453 62821884 721.1861
Bangladesh Asia 1972 45.252 70759295 630.2336
data(gm_top) + line + group(country) + x(year) + y(life) | facet(country)
data(gm_top) + line + group(col.country) + x(col.year) + y(col.life) | facet(col.country)
data(gm_top) + line + group(:country) + x(:year) + y(:life) |
  facet(:country)
plot(data(gm_top), line, group(col.country), x(col.year), y(col.life),
  across(col.country))
2000 40 50 60 70 80 2000 2000 2000 2000 2000 2000 2000 2000 2000 Bangladesh Brazil China India Indonesia Japan Mexico Nigeria Pakistan United States Life Year

“Given the gm top table: lines, x is year, y is life, grouped by country, split into panel columns by country.”

The lines are still readable, but the years under them are not: the year labels overlap. The last panel’s name is also wider than its strip, the band above the panel that names it. Ten categories is a common number, and ten panels in one line are already too narrow to read.

wrap folds that line into a rectangle. The number is how many panels to draw before the line begins again:

data(gm_top) + line + group(country) + x(year) + y(life) | facet(country, wrap = 4)
data(gm_top) + line + group(col.country) + x(col.year) + y(col.life) | facet(col.country, wrap = 4)
data(gm_top) + line + group(:country) + x(:year) + y(:life) |
  facet(:country, wrap = 4)
plot(data(gm_top), line, group(col.country), x(col.year), y(col.life),
  across(col.country, { wrap: 4 }))
40 50 60 70 80 40 50 60 70 80 1960 1980 2000 1960 1980 2000 1960 1980 2000 40 50 60 70 80 1960 1980 2000 Bangladesh Brazil China India Indonesia Japan Mexico Nigeria Pakistan United States Life Year

“Given the gm top table: lines, x is year, y is life, grouped by country, split into panel columns by country, wrapped at 4.”

The panels and the scale are unchanged, and wrap = 4 is the only addition to the sentence. The years are readable, United States fits its strip, and each country’s rise is now large enough to read.

Notice what wrap does not say: which way the panels run. The operator already decides it. | runs the line across, so four to a row; / runs it down, so four to a column:

data(gm_top) + line + group(country) + x(year) + y(life) / facet(country, wrap = 4)
data(gm_top) + line + group(col.country) + x(col.year) + y(col.life) / facet(col.country, wrap = 4)
data(gm_top) + line + group(:country) + x(:year) + y(:life) /
  facet(:country, wrap = 4)
plot(data(gm_top), line, group(col.country), x(col.year), y(col.life),
  down(col.country, { wrap: 4 }))
40 50 60 70 80 40 50 60 70 80 1960 1980 2000 40 50 60 70 80 1960 1980 2000 40 50 60 70 80 1960 1980 2000 Bangladesh Indonesia Pakistan Brazil Japan United States China Mexico India Nigeria Life Year

This is the same number and the same ten countries, now read down the columns instead of across the rows. One number is enough because the direction is already in the sentence, which is why facet() takes no second argument for it. ggplot2 spells the same idea as nrow and ncol.

Two details in the wrapped plots come from the fold itself. The first is in the bottom right corner, where two cells stay empty. Ten panels folded four at a time fill twelve cells, so two cells are left over and nothing is drawn in them. They are not the empty panels of a crossed grid. In a crossing, an empty panel stands for a pair of categories that the table has no rows for. Here there is no pair at all, only leftover space.

The second is the extra axis beside the gap. Tick labels belong to the panels that touch the edge where the axis is drawn. In a full rectangle that edge is the bottom row. A folded line can leave the last row short, and then the panels above the gap would have no year labels. So gog uses a more general rule: a panel draws the x axis when no panel is below it. In the plot folded with |, Mexico and Nigeria are above the gap and keep their year labels. In the plot folded with /, the gap falls under United States.

Wrapping a crossed grid is refused. The crossing already makes a rectangle from two columns, so a wrap number would set the grid’s shape a second time:

data(gm_eras) + point + x(gdp) + y(life) |
  facet(continent, wrap = 2) / facet(era)
(data(gm_eras) + point + x(col.gdp) + y(col.life) |
  facet(col.continent, wrap = 2) / facet(col.era))
data(gm_eras) + point + x(:gdp) + y(:life) | facet(:continent, wrap = 2) /
  facet(:era)
plot(data(gm_eras), point, x(col.gdp), y(col.life),
  across(col.continent, { wrap: 2 }), down(col.era))
Error:
! gog: `wrap` folds one line of panels into a rectangle, but `continent` and `era` already cross into one. Drop `wrap`, or facet by one column and let `wrap` shape it.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

The message names the two columns that already make the rectangle, and asks you to drop wrap or to facet by one column.

28.5 Giving a panel its own scale

Everything so far has used one scale across every panel, which is what makes the panels comparable. It is also what makes some of them unreadable. Here is the world’s population by continent, one panel each. The gm_all table holds every year of the gapminder data:

gm_all: first 5 of 1704 rows
country continent year life population gdp
Afghanistan Asia 1952 28.801 8425333 779.4453
Afghanistan Asia 1957 30.332 9240934 820.8530
Afghanistan Asia 1962 31.997 10267083 853.1007
Afghanistan Asia 1967 34.020 11537966 836.1971
Afghanistan Asia 1972 36.088 13079460 739.9811
data(gm_all) + line * sum + x(year) + y(population) | facet(continent, wrap = 3)
data(gm_all) + line * sum + x(col.year) + y(col.population) | facet(col.continent, wrap = 3)
data(gm_all) + line * sum + x(:year) + y(:population) |
  facet(:continent, wrap = 3)
plot(data(gm_all), layer(line, sum), x(col.year), y(col.population),
  across(col.continent, { wrap: 3 }))
0 1B 2B 3B 4B 1960 1980 2000 1960 1980 2000 0 1B 2B 3B 4B 1960 1980 2000 Asia Europe Africa Americas Oceania Population Year

“Given all the gapminder years: a line derived by sum, x is year, y is population, split into panel columns by continent, wrapped at 3.”

Asia holds nearly four billion people and Oceania about twenty-five million. The axis is sized for Asia, so Oceania’s line lies flat along the bottom. The line is drawn correctly, but the axis is too tall for its shape to show. What the plot shows is that the five totals are of very different sizes. But the question was how each continent grew, and the plot answers a different one.

Write free = TRUE inside a position, and that position’s axis is fitted to each panel’s own rows:

data(gm_all) + line * sum + x(year) +
  y(population, free = TRUE) | facet(continent, wrap = 3)
(data(gm_all) + line * sum + x(col.year) +
  y(col.population, free = True) | facet(col.continent, wrap = 3))
data(gm_all) + line * sum + x(:year) + y(:population, free = true) |
  facet(:continent, wrap = 3)
plot(data(gm_all), layer(line, sum), x(col.year),
  y(col.population, { free: true }), across(col.continent, { wrap: 3 }))
1500M 2000M 2500M 3000M 3500M 450M 500M 550M 1960 1980 2000 400M 600M 800M 1960 1980 2000 400M 600M 800M 1960 1980 2000 10M 15M 20M 25M Asia Europe Africa Americas Oceania Population Year

“Given all the gapminder years: a line derived by sum, x is year, y is population, freed, split into panel columns by continent, wrapped at 3.”

Now every line shows its rise, and you can see that Oceania’s population more than doubled. Notice the y axis: each panel carries its own numbers and its own gridlines, because one set of labels along the edge is enough only when the scale is shared.

Notice also what the second plot loses. Each axis is now fitted to its own line, so every line rises from the bottom of its panel to the top, and the five look much alike. Nothing in the five shapes shows that one rise is counted in billions and another in tens of millions. A reader who looks at the shapes and not at the axes will believe the five continents grew by similar amounts. The first plot cannot mislead a reader this way; the second one can. So gog frees a scale only when you ask for it.

Where you write free decides which axis is freed. You write free inside x() or inside y(), so the sentence has already named the axis and free does not name it again. There is no free_x or free_y. To free both axes, write free in both:

data(gm_all) + point + x(gdp, free = TRUE) + y(life, free = TRUE) |
  facet(continent, wrap = 3)
(data(gm_all) + point + x(col.gdp, free = True) + y(col.life, free = True) |
  facet(col.continent, wrap = 3))
data(gm_all) + point + x(:gdp, free = true) + y(:life, free = true) |
  facet(:continent, wrap = 3)
plot(data(gm_all), point, x(col.gdp, { free: true }),
  y(col.life, { free: true }), across(col.continent, { wrap: 3 }))
0 50K 100K 40 60 80 0 10K 20K 30K 40K 50K 50 60 70 80 0 10K 20K 30 40 50 60 70 0 10K 20K 30K 40K 40 50 60 70 80 10K 20K 30K 70 75 80 Asia Europe Africa Americas Oceania Life Gdp

Each panel now has its own numbers on both axes: Asia’s gdp axis reaches past 100K, and Oceania’s starts near 10K.

There is a third position, z, and it takes free the same way, with no new word in the grammar. A cube is drawn in a panel like any other plot (Space). The plot below uses the gapminder_2007 table, shown in Panels side by side:

data(gapminder_2007) + point + x(gdp) + y(life) +
  z(population, free = TRUE) + color(continent) | facet(continent)
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  z(col.population, free = True) + color(col.continent) | facet(col.continent))
data(gapminder_2007) + point + x(:gdp) + y(:life) +
  z(:population, free = true) + color(:continent) | facet(:continent)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  z(col.population, { free: true }), color(col.continent),
  across(col.continent))
50K 20K 80 70 50 1000M 500M 0 Gdp Life Population 50K 20K 80 70 50 80M 60M 40M 20M 0 Gdp Life Population 50K 20K 80 70 50 100M 50M 0 Gdp Life Population 50K 20K 80 70 50 300M 200M 100M 0 Gdp Life Population 50K 20K 80 70 50 20M 15M 10M 5M Gdp Life Population Asia Europe Africa Americas Oceania Continent Asia Europe Africa Americas Oceania

“Given gapminder 2007: points, x is gdp, y is life, z is population, freed, color by continent, split into panel columns by continent.”

If the height axis were shared, it would have to reach China’s population. Oceania’s two countries would then be at the very bottom, almost on top of each other. Freed, Oceania’s height axis runs in tens of millions, and its two countries are far apart. The loss is the same one the freed population lines had: the five cubes can no longer be compared with each other.

Freeing a position also changes the bin cut, and gog does this for you: it is not a setting you can write. The histogram earlier in this chapter shares one set of bin edges across every panel, because the cut is a set of positions on an axis that was shared. Free the axis and the edges are cut per panel too, for exactly the same reason:

data(gm_all) + bar * bin + x(gdp, free = TRUE) | facet(continent, wrap = 3)
data(gm_all) + bar * bin + x(col.gdp, free = True) | facet(col.continent, wrap = 3)
data(gm_all) + bar * bin + x(:gdp, free = true) |
  facet(:continent, wrap = 3)
plot(data(gm_all), layer(bar, bin), x(col.gdp, { free: true }),
  across(col.continent, { wrap: 3 }))
20K 40K 60K 80K 100K 0 100 200 300 400 10K 20K 30K 40K 5K 10K 15K 20K 10K 20K 30K 40K 0 100 200 300 400 15K 20K 25K 30K Asia Europe Africa Americas Oceania Count Gdp

Each panel’s bins are cut from its own rows, so Africa gets bins sized to Africa’s spread rather than bins sized for the richest countries in the data. You cannot free the axis and keep one shared set of bins. Shared bins are cut from the whole table, but a freed axis covers only its own panel’s values. A panel’s first and last bars usually reach beyond its values, so they would extend past the ends of its axis.

Freeing an axis changes a density in the same way. On a freed axis, each panel’s curves are smoothed with a bandwidth chosen from that panel’s rows alone. A shared bandwidth is there so that panels can be compared, and freed panels cannot be. Here it would only flatten the curve of a panel with a narrow spread.

Freeing also has limits. A free scale belongs to a panel, never to an animation frame. play, which turns a plot into an animation (Play), splits rows the same way a facet does, so free on a played plot looks like the same request. gog refuses it:

data(gm_all) + point + x(gdp) + y(life, free = TRUE) + play(year)
data(gm_all) + point + x(col.gdp) + y(col.life, free = True) + play(col.year)
data(gm_all) + point + x(:gdp) + y(:life, free = true) + play(:year)
plot(data(gm_all), point, x(col.gdp), y(col.life, { free: true }),
  play(col.year))
Error:
! gog: `free` on `y(life)` fits one scale per *panel*, and this plot has frames rather than panels. A frame replaces the one before it, so an axis refitted per frame would move under the data and the motion would be the scale's rather than the data's. Facet the plot to free a scale across panels, or leave the sequence on one scale.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

The reason is in the message. Panels are beside each other and a reader compares them. Each frame of an animation replaces the one before it, so a refitted axis would show different numbers in every frame. The reader would then see movement that comes from the changing axis rather than from the data. Across panels a free scale is safe. Across the frames of an animation, it would misreport the data.

Only x, y and z can be freed. A channel that is not a position has no axis to free. color() has one legend for all the panels instead, so gog refuses free on color():

data(gapminder_2007) + point + x(gdp) + y(life) +
  color(continent, free = TRUE) | facet(continent)
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  color(col.continent, free = True) | facet(col.continent))
data(gapminder_2007) + point + x(:gdp) + y(:life) +
  color(:continent, free = true) | facet(:continent)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  color(col.continent, { free: true }), across(col.continent))
Error:
! gog: `color(continent)` cannot be freed. `free` fits an axis to each panel's own rows, and only a position, `x`, `y` or `z`, has an axis. Write `free` on a position instead, or drop it.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

The message tells you to drop free, or to write it inside a position instead.

One more sentence is refused: free with no facet. A plot with no facet has one panel, and freeing its scale would change nothing:

data(gapminder_2007) + point + x(gdp) + y(life, free = TRUE)
data(gapminder_2007) + point + x(col.gdp) + y(col.life, free = True)
data(gapminder_2007) + point + x(:gdp) + y(:life, free = true)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life, { free: true }))
Error:
! gog: `free` on `y(life)` fits one scale per panel, and this plot has one panel. Facet it — `plot | facet(<column>)` — or drop `free`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

The message says the plot has one panel, and tells you to facet it or drop free.

28.6 A layer without the facet column

Return to the first plot in this chapter, with its five continents. Five panels are easier to compare when every one of them carries the same reference line, such as the world’s median life expectancy. A layer whose table does not have the facet column is drawn in full in every panel. That is how one shared reference line is drawn across small multiples:

world_median: all 2 rows
gdp life
277.5519 71.9355
49357.1902 71.9355
data(gapminder_2007) + point + x(gdp) + y(life) +
  data(world_median) + line + style(color = "gray", size = 1) |
  facet(continent)
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  data(world_median) + line + style(color = "gray", size = 1) |
  facet(col.continent))
data(gapminder_2007) + point + x(:gdp) + y(:life) + data(world_median) +
  line + style(color = "gray", size = 1) | facet(:continent)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  data(world_median), line, style({ color: "gray", size: 1 }),
  across(col.continent))
gog: table `world_median` has no column `continent`, so its layer is drawn in every panel. If it should be split too, add `continent` to that table.
0 20K 40K 40 50 60 70 80 0 20K 40K 0 20K 40K 0 20K 40K 0 20K 40K Asia Europe Africa Americas Oceania Life Gdp

“Given gapminder 2007: points, x is gdp, y is life, and also a line from the world median, split into panel columns by continent.”

The gray line, the world’s median life expectancy, repeats in every panel, so each continent is read against the same line. The engine reports this as an Assumption, a message that says what gog decided for you. world_median has no continent column, so its layer is drawn in every panel.

28.7 The facet column’s type

You may want one panel for small countries and one for large ones, which is a split by population. A facet column names the panels, so it must be a categorical column: a number is a position along an axis, not a name for a panel. gog refuses a facet on a continuous column, and the message says what to do instead:

data(gapminder_2007) + point + x(gdp) + y(life) | facet(population)
data(gapminder_2007) + point + x(col.gdp) + y(col.life) | facet(col.population)
data(gapminder_2007) + point + x(:gdp) + y(:life) | facet(:population)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  across(col.population))
Error:
! gog: `facet(population)` splits on a number column, but a facet variable names the panels, so it must be a category column. Store `population` as text or categories before plotting, or cut it into named groups first.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

shape and group need a categorical column for the same reason: each of them names groups rather than measures them. Cut the column into named groups, such as small and large; in R that is cut(). Turning it into text, with factor(), would give every country a panel of its own.

28.8 In short

Every panel is drawn by the same sentence and, unless you free a scale, read against the same scales. | puts the panels in columns, / puts them in rows, and writing both crosses two columns of the table into a grid. wrap folds one long line into a rectangle, and free gives each panel its own scale, fitted to its own rows. A layer whose table does not carry the facet column is drawn in full in every panel.

28.9 What comes next

Faceting splits one plot into panels that share a sentence and, by default, their axes. Composition does the opposite: it takes plots that were never one plot and places them in one arrangement.