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