How do you put two things in one picture? You join them with an operator, and gog has exactly four. They are not atoms. They are the assembly rules: how atoms combine.
Hangeul (한글) works the same way. Its consonants and vowels are the atoms. Its operator is one rule: an initial consonant, a vowel and an optional final consonant make a syllable block. The rule never changes, and a small set of letters builds every syllable the language needs.
+ layer (add a layer, a channel, or a label to a specification)
* derive (combine a mark with a transform into a compound mark)
| facet ↔ (small multiples, arranged in columns)
/ facet ↕ (small multiples, arranged in rows)
| and / have a second use: they join whole plots into an arrangement. An arrangement is one picture that holds two or more finished plots, each in its own part of it. That use has a chapter of its own (Composition).
26.1+: layer
Every sentence you have written so far holds several atoms, and + joins most of them. Each mark, channel, label and setting enters a specification through it:
data(my_table) + point +x(height) +y(weight) +color(species) +title("My plot")
Adding a mark (point, line, bar, …) starts a new layer. A channel (x(), color(), …) written before any mark belongs to the plot, and one written after a mark belongs to that mark. A label atom (title(), x_label()) belongs to the plot wherever you write it (Encoding scope).
26.1.1 Layering with +
A second mark draws a second layer over the first, and video editing works the same way. A video is a stack of tracks: the picture, a logo in the corner, the captions. Each track is separate, they play at the same time, and you see one finished video.
Here the points are the first layer and the reference lines the second:
# Scatterplot: points, and a horizontal reference line at each banddata(gapminder_2007) +x(gdp) +y(life) + point +color(continent) +data(life_bands) + rule +style(color ="darkgray", pattern ="dashed") +title("GDP vs life expectancy, 2007")
(data(gapminder_2007) + x(col.gdp) + y(col.life) + point + color(col.continent) + data(life_bands) + rule + style(color ="darkgray", pattern ="dashed") + title("GDP vs life expectancy, 2007"))
data(gapminder_2007) +x(:gdp) +y(:life) + point +color(:continent) +data(life_bands) + rule +style(color ="darkgray", pattern ="dashed") +title("GDP vs life expectancy, 2007")
plot(data(gapminder_2007),x(col.gdp),y(col.life), point,color(col.continent),data(life_bands), rule,style({ color:"darkgray",pattern:"dashed" }),title("GDP vs life expectancy, 2007"))
“Given gapminder 2007: x is gdp, y is life, points colored by continent, and also rules from the life bands.”
The rules come after point in the sentence, so they are drawn over the points.
26.1.2 The order of +: data, mark, positions, refinements
This book writes its sentences in one order, and you may wonder whether the engine requires it. Inside a single layer the mark and the channels commute, which means that any order of them draws the same plot. The mark, x and color can be arranged six ways, so here are all six.
((data(iris_flowers) + bar * bin +x(petal_length) +color(species)) | (data(iris_flowers) + bar * bin +color(species) +x(petal_length))) /((data(iris_flowers) +x(petal_length) + bar * bin +color(species)) | (data(iris_flowers) +x(petal_length) +color(species) + bar * bin)) /((data(iris_flowers) +color(species) + bar * bin +x(petal_length)) | (data(iris_flowers) +color(species) +x(petal_length) + bar * bin))
(((data(iris_flowers) + bar *bin+ x(col.petal_length) + color(col.species)) | (data(iris_flowers) + bar *bin+ color(col.species) + x(col.petal_length))) /((data(iris_flowers) + x(col.petal_length) + bar *bin+ color(col.species)) | (data(iris_flowers) + x(col.petal_length) + color(col.species) + bar *bin)) /((data(iris_flowers) + color(col.species) + bar *bin+ x(col.petal_length)) | (data(iris_flowers) + color(col.species) + x(col.petal_length) + bar *bin)))
((data(iris_flowers) + bar * bin +x(:petal_length) +color(:species)) | (data(iris_flowers) + bar * bin +color(:species) +x(:petal_length))) / ((data(iris_flowers) +x(:petal_length) + bar * bin +color(:species)) | (data(iris_flowers) +x(:petal_length) +color(:species) + bar * bin)) / ((data(iris_flowers) +color(:species) + bar * bin +x(:petal_length)) | (data(iris_flowers) +color(:species) +x(:petal_length) + bar * bin))
“Given the iris flowers: bars derived by bin, x is petal length, color by species.”
Six sentences, and the same histogram six times. The six are not plots that merely look alike. The engine writes six files, identical byte for byte.
So if the order does not change the picture, why does this book use one order: data, mark, positions, refinements? Because a fixed order lets the code read like an English sentence, with a natural, subject-first shape:
data(...) first. It names the table the columns come from. A bare name like petal_length means nothing until a table is bound (Bind-Once), so the table is named first. The sentence must start here.
The mark and its *-chain second. This is the subject. Nothing is drawn without a mark (Minimum Syllable): bar * mean * dodge says what you are drawing and what was done to it, as one compound word. It is the initial consonant of the block; everything after is said about it.
The positions third.x and y say where the subject is drawn, and a mark cannot be drawn without them (the vowel to the mark’s consonant), so they follow immediately.
The refinements last.color, size, order, labels, title: everything optional. Delete every refinement and a legal plot remains; delete the mark or a position and nothing is drawn. Writing the removable parts last is also how you build a plot: the minimum sentence first, then refine (First plot).
Only two parts of the order are rules: the sentence starts with a source (data() or query()), and style() follows its mark. The rest is a reading convention: inside one layer, any order draws the same plot. Where you write a channel does matter once a plot has two layers, because a channel written before a mark and one written after it bind differently. Encoding scope gives the rule. Learn one order and use it everywhere. Then your plots and the plots in this book read the same way.
Which orders matter lists what the engine enforces under + and *, and what you are free to arrange.
26.1.2.1 A refinement far from what it refines
One part of the convention is easy to misread. A grouped bar chart pairs dodge with color, yet the sentence puts them at opposite ends, with the positions in between:
data(gm_eras) + bar * mean * dodge +x(continent) +y(life) +color(era) +title("Mean life expectancy by continent, 1957 vs 2007")
(data(gm_eras) + bar * mean * dodge + x(col.continent) + y(col.life) + color(col.era) + title("Mean life expectancy by continent, 1957 vs 2007"))
data(gm_eras) + bar * mean * dodge +x(:continent) +y(:life) +color(:era) +title("Mean life expectancy by continent, 1957 vs 2007")
plot(data(gm_eras),layer(bar, mean, dodge),x(col.continent),y(col.life),color(col.era),title("Mean life expectancy by continent, 1957 vs 2007"))
AsiaEuropeAfricaAmericasOceania020406080Mean life expectancy by continent, 1957 vs 2007LifeContinentEra19572007
“Given the gapminder eras: bars derived by mean and dodge, x is continent, y is life, color by era.”
dodge and color(era) are plainly related: on a bar, dodge has nothing to set side by side until a channel such as color names the groups. So why not write them together? Because the grammar deliberately gives them different operators. dodge belongs to *, because it modifies the mark. The same modifier works on box * dodge and interval * range * dodge. color belongs to +: it binds a column to a channel. color * dodge cannot be written: * needs a mark on its left. Joining the two would make dodge depend on which channel splits the groups, and a modifier must not depend on that.
The place of color(era) in the sentence is the book’s convention, not something dodge asks for. color(era) is optional, so it goes with the refinements, at the end. dodge is optional too, but it needs a channel that splits the bars, here color(era). So you delete the two together: delete both and a legal plot remains, with one bar per continent. You do not have to remember that dodge needs a split, because the engine checks it. Without one, bar * mean * dodge is refused rather than drawn as one bar per continent. The message tells you to add a channel that splits the bars, such as color. So color can be written far from dodge in the sentence and still work with it.
Three channels can make this split: color, pattern and group. Each cuts the mark into one piece per era, and dodge sets the pieces side by side. Without dodge, the pieces in one slot are drawn over one another. color gives each piece its own color, pattern gives each its own texture, and both draw a legend. group splits the mark and shows nothing more: no color, no texture, no legend. A box, an interval and a bar take all three. A bar takes group only when it is joined to a statistic (such as mean), to dodge or to stack. With no transform, each bar draws one row, so there is nothing to split.
The three sentences below differ only in that channel and in their titles: the top one splits the boxes by color(era), the middle one by pattern(era), and the bottom one by group(era). The theme(height = 900) after the outer parentheses makes the whole arrangement 900 pixels tall, so each plot has room (How big is the arrangement itself?).
“Given the gapminder eras: boxes derived by dodge, x is continent, y is life, color by era, above the same with pattern by era, above the same grouped by era.”
The boxes are in the same places in all three plots. In the bottom one, every box has the same color, and nothing says which box is 1957 and which is 2007. Use group when another layer already shows the groups. The last plot in Interval has two layers, and both use dodge. The bars are split by color(era) and the error bars by group(era). So each error bar is drawn in black on top of its own bar, and the bars’ legend explains both.
26.2*: derive
A mark on its own draws the rows as they are. A histogram counts them first, and a trend line fits a curve to them first. That computing step needs a word of its own. * joins a transform to a mark, and the result is one compound mark.
bar * bin # histogramline * smooth # smoothed trend lineline * density # kernel density curve
* has one rule and one freedom. The mark goes on the left and the transforms go on the right. Among the transforms themselves, any order draws the same plot. The rest of this section explains both.
26.2.1 Why the order is fixed
Start with the rule. In Hangeul, a syllable is always initial consonant + vowel, never the reverse. gog’s * works the same way: the mark is the subject; the transform is what is done to it.
bin * bar is refused, just as ㅏ (a) + ㄱ (g) is not a valid Hangeul syllable. In the right order, ㄱ (g) + ㅏ (a) makes 가 (ga). In the same way, the mark and then the transform make bar * bin:
bar * bin # a bar mark, derived by binning
The refusal names the fix:
bin * bar
Error in `*.gog_atom`:
! gog: `*` not defined for transform * mark.
Use `*` to combine a mark with a transform, e.g. bar * bin.
26.2.2 One fixed place, and one free order
The mark is always on the left, and a transform there is refused. That is what makes bar * bin a compound mark rather than a function call. Why +, and not a pipe explains why it lets the atoms stay nouns.
After the mark, the transforms commute, as the mark and channels do under +: any order of them draws the same plot. Here bin and mean are written both ways:
(data(gapminder_2007) + bar * bin * mean +x(gdp) +y(life) +title("bar * bin * mean")) | (data(gapminder_2007) + bar * mean * bin +x(gdp) +y(life) +title("bar * mean * bin"))
((data(gapminder_2007) + bar *bin* mean + x(col.gdp) + y(col.life) + title("bar * bin * mean")) | (data(gapminder_2007) + bar * mean *bin+ x(col.gdp) + y(col.life) + title("bar * mean * bin")))
(data(gapminder_2007) + bar * bin * mean +x(:gdp) +y(:life) +title("bar * bin * mean")) | (data(gapminder_2007) + bar * mean * bin +x(:gdp) +y(:life) +title("bar * mean * bin"))
beside(plot(data(gapminder_2007),layer(bar, bin, mean),x(col.gdp),y(col.life),title("bar * bin * mean")),plot(data(gapminder_2007),layer(bar, mean, bin),x(col.gdp),y(col.life),title("bar * mean * bin")))
10K20K30K40K020406080bar * bin * meanLifeGdp10K20K30K40Kbar * mean * binGdp
“Given gapminder 2007: bars derived by bin and mean, x is gdp, y is life, beside bars derived by mean and bin, x is gdp, y is life.”
The two plots are identical. The engine does not run the transforms in the order you type them. It finds the order the work needs, and here it runs bin first in both.
The order you write is therefore a matter of style, not a rule. This book writes the transforms in the order they run, so the sentence reads in the same order as the work: bar * bin * mean cuts the axis into bands, then averages inside each one.
bin must run first because of what mean needs. A statistic reduces a group to one number, and a continuous column has no groups: every value is a group of one, so its mean is itself. Ask for the mean of life at each value of gdp, and each group holds one country:
gog: `point * mean` groups the rows by their value of `gdp`, and here every group holds a single row, so each `mean` is one row's own value. Drawn as asked. To summarize, cut `gdp` into ranges first, `point * bin * mean`, or group by a column whose values repeat.
010K20K30K40K50K4050607080LifeGdp
“Given gapminder 2007: points derived by mean, x is gdp, y is life.”
The picture is the plain scatterplot, and the note printed with it says why: each mean is one row’s own value. bin is what makes the groups, by turning many distinct numbers into one band. So the cut comes first: without it, the mean has nothing to average.
So two transforms work together only when they do different jobs. There are four jobs: saying where the cells are, what is in them, what scale the answer is read on, and where the marks are drawn (Chaining transforms explains each one). A pair that does the same job twice is refused rather than run in sequence. bin and count both count the rows, so each cell would be measured twice:
Error:
! gog: `bar * bin * count` measures each cell twice — `bin` and `count` each invent their own measurement from the rows, and neither was handed a column to give way to, so there is no reading that keeps both. Keep whichever you meant: `bar * bin` or `bar * count`. To cut an axis into cells and measure something else inside them, the second transform has to be one you hand a column: `bar * bin * mean + x(<number>) + y(<column>)`. To read either as shares of the whole rather than as counts, `proportion` rescales whichever you keep: `bar * bin * proportion`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The order does not decide a refusal either: bar * count * bin(12) is refused just like the pair above. What cannot be chained, and why shows why a pair is refused, one refusal at a time, and Combinations has the whole grid of pairs.
26.2.3 Live examples
The start of this section listed three compound marks: bar * bin, line * smooth and line * density. You will use them often. Here each one is drawn, so you can see what its transform made from the rows.
# Histogram: life expectancy across all countries in 2007data(gapminder_2007) + bar * bin +x(life) +x_label("Life expectancy (years)") +title("Distribution of life expectancy, 2007")
(data(gapminder_2007) + bar *bin+ x(col.life) + x_label("Life expectancy (years)") + title("Distribution of life expectancy, 2007"))
data(gapminder_2007) + bar * bin +x(:life) +x_label("Life expectancy (years)") +title("Distribution of life expectancy, 2007")
plot(data(gapminder_2007),layer(bar, bin),x(col.life),x_label("Life expectancy (years)"),title("Distribution of life expectancy, 2007"))
506070800102030Distribution of life expectancy, 2007CountLife expectancy (years)
# Scatterplot, with a LOESS smooth on topdata(gapminder_2007) +x(gdp) +y(life) + point +color(continent) + line * smooth +title("GDP vs life expectancy, with a LOESS smooth")
(data(gapminder_2007) + x(col.gdp) + y(col.life) + point + color(col.continent) + line * smooth + title("GDP vs life expectancy, with a LOESS smooth"))
data(gapminder_2007) +x(:gdp) +y(:life) + point +color(:continent) + line * smooth +title("GDP vs life expectancy, with a LOESS smooth")
plot(data(gapminder_2007),x(col.gdp),y(col.life), point,color(col.continent),layer(line, smooth),title("GDP vs life expectancy, with a LOESS smooth"))
010K20K30K40K50K4050607080GDP vs life expectancy, with a LOESS smoothLifeGdpContinentAsiaEuropeAfricaAmericasOceania
“Given gapminder 2007: x is gdp, y is life, points colored by continent, and also a line derived by smooth.”
# Kernel density estimatedata(gapminder_2007) + line * density +x(life) +x_label("Life expectancy (years)") +title("Kernel density: life expectancy, 2007")
(data(gapminder_2007) + line * density + x(col.life) + x_label("Life expectancy (years)") + title("Kernel density: life expectancy, 2007"))
data(gapminder_2007) + line * density +x(:life) +x_label("Life expectancy (years)") +title("Kernel density: life expectancy, 2007")
plot(data(gapminder_2007),layer(line, density),x(col.life),x_label("Life expectancy (years)"),title("Kernel density: life expectancy, 2007"))
4060800.000.010.020.030.04Kernel density: life expectancy, 2007DensityLife expectancy (years)
“Given gapminder 2007: a line derived by density, x is life.”
In each plot, the compound mark draws what its transform computed rather than the rows. The bars are counts of countries in bands of life expectancy. The line through the points is a fitted curve, and the last line is an estimated density. gapminder_2007 has no count or density column. bin and density compute those numbers and put them on y themselves, so neither sentence needs a y(), and the axes are labeled Count and Density. You can also write y(count) or y(density), in lowercase, to give the axis a setting such as limits. Any other name in y() is refused (Columns a transform writes).
26.3| and /: facet, or compose
Two questions lead to the last two operators: how the same plot looks for each group, and how two different plots fit in one figure. | puts things side by side and / puts one above the other. What you write on the right decides the job: a facet() splits one plot into panels, and a second plot is placed beside or below the first.
With facet(), | produces small multiples arranged in columns, one panel per category of the column it names.
“Given gapminder 2007: points, x is gdp, y is life, split into panel rows by continent.”
Stacked, the panels line up their x axes, so a value is directly above the same value in the panel below. Side by side, they line up their y axes instead.
Write | and / in one sentence to combine them into a grid. Each operator applies to the facet written after it, read from left to right. See Faceting for when to choose rows or columns, the grid, what the panels share, and why the facet column must be categorical.
With a plot on the right, the same two operators place separate plots in one arrangement, and each plot keeps its own coordinate space:
“Given gapminder 2007: points, x is gdp, y is life, beside bars derived by count, x is continent.”
The plot on the left shows life expectancy on y, and the plot on the right shows a count, so each draws its own vertical axis. Two plots side by side that name the same column on y, and read it the same way, share that axis, and it is drawn once. In the same way, two plots stacked with / that name the same column on x share that axis.
One pair of operators, two jobs. Composition covers the second.
26.4 Which orders matter
This book writes every sentence in the same order: data, mark, positions, refinements. Most of that order is the book’s habit, and the engine draws the same plot if you change it. Only a few parts are rules that the engine enforces. The table below sorts the orderings under + and under * into three kinds, and only one kind needs your attention.
Kind
What it means
Under +
Under *
Free
Either way draws exactly the same picture.
In a plot with one mark: the mark, its positions and its other channels, in any order. Labels go anywhere. Only style() must follow its mark.
The transforms after one mark, in any order. The book writes them in the order they run, as a style; the engine finds that order itself.
Refused
The engine stops and names what to move.
Atoms with no table named before them: point + x(gdp). A style() written before any mark.
A transform on the left: bin * bar.
Meaningful
No error, and a different picture.
A channel before the marks, or after one of them, once a plot has two. Two marks swapped: the second draws on top.
Nothing. No order under * changes the picture.
Start with the cell where * meets Meaningful. * fixes one thing only: the mark is on the left. Once the mark is there, the order of what follows changes nothing.
Free covers most orderings. The two sentences below draw the identical plot, and the book writes the first because a fixed order is easier to read:
Both name life on y, so the two plots share that axis and it is drawn once, on the left. Each plot still draws its own gdp axis underneath it.
Refused means an atom comes before the thing it needs. A plot needs its table first, so + must have a plot on its left. A setting needs a mark, so a style() before any mark is refused:
Error:
! gog: `style()` has no mark to style. Put it after a mark, e.g. `point + style(color = "tomato")`.
Meaningful is the only kind that needs care, because it is the only kind the engine says nothing about. With two marks in a sentence, a channel written before them belongs to the plot and reaches both marks, and a channel written after one belongs to that mark. Both are legal, neither warns, and they draw different pictures. Here only color(era) moves, from before the marks to after point:
(data(gm_eras) +x(gdp, scale ="log") +y(life) +color(era) + point + line * smooth +title("color(era) before the marks")) | (data(gm_eras) +x(gdp, scale ="log") +y(life) + point +color(era) + line * smooth +title("color(era) after point"))
((data(gm_eras) + x(col.gdp, scale ="log") + y(col.life) + color(col.era) + point + line * smooth + title("color(era) before the marks")) | (data(gm_eras) + x(col.gdp, scale ="log") + y(col.life) + point + color(col.era) + line * smooth + title("color(era) after point")))
(data(gm_eras) +x(:gdp, scale ="log") +y(:life) +color(:era) + point + line * smooth +title("color(era) before the marks")) | (data(gm_eras) +x(:gdp, scale ="log") +y(:life) + point +color(:era) + line * smooth +title("color(era) after point"))
beside(plot(data(gm_eras),x(col.gdp, { scale:"log" }),y(col.life),color(col.era), point,layer(line, smooth),title("color(era) before the marks")),plot(data(gm_eras),x(col.gdp, { scale:"log" }),y(col.life), point,color(col.era),layer(line, smooth),title("color(era) after point")))
1K10K100K406080color(era) before the marksLifeGdpEra195720071K10K100Kcolor(era) after pointGdpEra19572007
“Given the gapminder eras: x is gdp on a log scale, y is life, color by era, points and also lines derived by smooth, beside x is gdp on a log scale, y is life, points colored by era, and also a line derived by smooth.”
On the left, color(era) reaches both marks, so the trend splits into one line per era. On the right, it belongs to the points alone, and one gray line is fitted to both eras together. Encoding scope gives the whole rule. It is also why this book keeps one order: when every sentence looks the same, a different order is a deliberate signal.
The order of the marks is the second case, and here too the engine gives no message. The marks are drawn in the order you wrote them, so the second mark is drawn on top of the first. Here the bars are light gray, so you can see which mark is on top:
(data(gapminder_2007) +x(continent) +y(life) + bar * mean +style(color ="lightgray") + point +title("bar * mean, then point")) | (data(gapminder_2007) +x(continent) +y(life) + point + bar * mean +style(color ="lightgray") +title("point, then bar * mean"))
((data(gapminder_2007) + x(col.continent) + y(col.life) + bar * mean + style(color ="lightgray") + point + title("bar * mean, then point")) | (data(gapminder_2007) + x(col.continent) + y(col.life) + point + bar * mean + style(color ="lightgray") + title("point, then bar * mean")))
(data(gapminder_2007) +x(:continent) +y(:life) + bar * mean +style(color ="lightgray") + point +title("bar * mean, then point")) | (data(gapminder_2007) +x(:continent) +y(:life) + point + bar * mean +style(color ="lightgray") +title("point, then bar * mean"))
beside(plot(data(gapminder_2007),x(col.continent),y(col.life),layer(bar, mean),style({ color:"lightgray" }), point,title("bar * mean, then point")),plot(data(gapminder_2007),x(col.continent),y(col.life), point,layer(bar, mean),style({ color:"lightgray" }),title("point, then bar * mean")))
AsiaEuropeAfricaAmericasOceania020406080bar * mean, then pointLifeContinentAsiaEuropeAfricaAmericasOceaniapoint, then bar * meanContinent
“Given gapminder 2007: x is continent, y is life, bars derived by mean, colored lightgray, and also points, beside points and also bars derived by mean, colored lightgray.”
On the left, every point is drawn over the bars. On the right, the bars are drawn last, so they cover the points below each mean, which show only faintly through the gray.
26.4.1 What groups first
The three kinds above, free, refused and meaningful, are about the order in which you write the atoms. Precedence answers a different question: when a sentence holds more than one operator, which operator groups its atoms first. Arithmetic has the same idea. 2 + 3 * 4 is 14 and not 20, because * groups before +. It is read as 2 + (3 * 4), as if the parentheses were written. A sentence in gog is read the same way, so precedence decides which atoms each operator joins.
This order of grouping belongs to the language you write in, not to gog, and gog cannot change it. R and Python share one order. Julia differs in one place: it groups | together with +. When the two meet, the one written first groups first (Julia shows where this changes a sentence). The table gives the order in each of the three languages:
Groups
R
Python
Julia
First
* and /
* and /
* and /
Next
+
+
+ and |, from left to right
Last
|
|
JavaScript has no column in the table, because it spells each operator as a word, such as beside(). The things the word joins are written inside its parentheses, so you see the grouping instead of remembering it (JavaScript).
For one plot, this is the order you want in all three languages. So a plot needs no parentheses of its own, even with a facet:
R reads that sentence as (data(gapminder_2007) + (bar * bin) + x(life)) | facet(continent). The transform joins its mark first, the atoms join the plot next, and the facet splits the finished plot last. Python and Julia read it the same way.
A row facet needs no parentheses either, because a facet always splits the whole plot.
An atom can also be written after a facet, like the title in the sentence below:
data(gapminder_2007) + point +x(gdp) +y(life) |facet(continent) +title("Each continent in its own panel")
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) | facet(col.continent) + title("Each continent in its own panel"))
data(gapminder_2007) + point +x(:gdp) +y(:life) |facet(:continent) +title("Each continent in its own panel")
plot(data(gapminder_2007), point,x(col.gdp),y(col.life),across(col.continent),title("Each continent in its own panel"))
020K40K4050607080020K40K020K40K020K40K020K40KAsiaEuropeAfricaAmericasOceaniaEach continent in its own panelLifeGdp
The title belongs to the whole plot, so it appears once, above all five panels, in every language.
26.4.2 Parentheses in an arrangement
That order works for one plot, with or without a facet. It can go wrong when | or / has no facet() on its right, and joins two separate plots into one arrangement instead (Composition). Then each side of the operator must be a whole plot, from its data() to its last atom. Without parentheses, precedence can join part of one plot to part of the other before either plot is complete. Here are the two plots from earlier in this chapter, set one above the other with no parentheses:
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)
Error:
! gog: `/` places one plot below another, and it groups before `+`, so here it joined `y()` to the plot after it. Put each plot in parentheses: `(data(df) + point + ...) / (data(df) + bar + ...)`.
/ groups before +, so it takes only the atom on each side of it: y(life) on the left and the second data() on the right. R reads the sentence as … + x(gdp) + (y(life) / data(gapminder_2007)) + …. For a facet, that grouping does no harm, because a facet splits the whole plot anyway. For an arrangement it does: / must place a whole plot below another, and y(life) is only one atom of a plot. So gog refuses the sentence, and its message names y(), the atom that / took. Python and Julia give the same refusal.
With | in place of /, the result depends on the language. R and Python group | last, so both plots are complete before | joins them, and the arrangement draws. Julia groups | together with +, from left to right. So | joins the first plot to the second plot’s data() alone, and gog refuses the sentence.
So whenever this book composes plots in R, Python or Julia, it writes each plot inside its own parentheses. Then every plot is complete before | or / joins it, and the arrangement means the same thing in all three languages. In JavaScript each plot is written as plot(…), so it is complete already.
Two more places need parentheses. The first is an arrangement inside an arrangement. / groups before | in R, Python and Julia, so a | b / c is read as a | (b / c): one plot beside two plots, one above the other. The examples below use three plots from this chapter, titled a, b and c:
“Given gapminder 2007: points, x is gdp, y is life, beside bars derived by count, x is continent; above bars derived by bin, x is life.”
Both arrangements draw the same in all three languages, because each one writes its parentheses out.
The second place is a theme() for the whole arrangement. Put parentheses around the arrangement, then add the theme(), and it sizes the whole arrangement. This one is 500 pixels wide, and the two plots share that width:
“Given gapminder 2007: points, x is gdp, y is life, beside bars derived by count, x is continent.”
Without the outer parentheses, the languages disagree. Julia still gives the theme() to the arrangement. R and Python give it to the second plot, because + groups before |, so only b is 500 pixels wide and the arrangement keeps its 800. To draw that arrangement in every language, Julia included, put the theme() inside the second plot’s parentheses:
“Given gapminder 2007: points, x is gdp, y is life, beside bars derived by count, x is continent.”
Written this way, with every parenthesis in place, each of the four arrangements draws the same in R, Python and Julia (How big is the arrangement itself?).
The table collects the rules for an arrangement. Each (…) holds one whole plot, from its data() to its last atom, and the last column gives the same arrangement in JavaScript’s words:
Arrangement
R, Python and Julia
JavaScript
Two plots side by side
(…) | (…)
beside(plot(…), plot(…))
One plot above another
(…) / (…)
below(plot(…), plot(…))
A row of two above a third
((…) | (…)) / (…)
below(beside(plot(…), plot(…)), plot(…))
A height for the whole arrangement
((…) | (…)) + theme(height = 260)
beside(plot(…), plot(…), theme({ height: 260 }))
Each language’s chapter shows these rules in its own spelling: R, Python, Julia and JavaScript.
26.4.3 What parentheses may group
The table above shows what parentheses group: whole plots and whole arrangements. You might also try them around several marks, to give a second table to all of them. That does not work, because parentheses do not group marks. A second data() applies only to the mark written directly after it (Data):
data(actuals) +x(year) +y(sales) + line + (data(forecast) + point + area)
(data(actuals) + x(col.year) + y(col.sales) + line + (data(forecast) + point + area))
data(actuals) +x(:year) +y(:sales) + line + (data(forecast) + point + area)
Error:
! gog: parentheses do not group marks, so everything inside these would be dropped. Write the marks in sequence instead, and repeat `data()` before each one that reads that table: `+ data(forecast) + point + data(forecast) + area`. Parentheses compose whole plots, with `|` and `/`.
gog refuses that instead of drawing it. The refusal is better than the alternative. The engine cannot give the second table to both marks inside the parentheses. Its only other choice is to discard both marks and draw a plot with two layers missing, without saying so. To put two marks on the second table, write data() before each of them.
The same refusal covers a position or a title inside the parentheses, for the same reason. In JavaScript, where the operators are words, the same mistake is a plot() handed to another plot(), and it is refused there too (JavaScript shows both spellings).
26.5 Why +, and not a pipe
Readers who know R’s tidyverse packages ask why gog does not use a pipe. A pipe passes a value into a function: a |> f means f(a). Hadley Wickham, ggplot2’s author, has said publicly that the pipe would have been the better interface (Wickham, 2018). ggplot, ggplot2’s predecessor, was written as function composition, and it could have used a pipe if one had existed. ggplot2 used + instead.
He also answers the question a reader asks next: why not change ggplot2 now? It cannot change: its functions take the mapping and the data first, while a pipe needs the plot as the first argument. And he judges that a new package built only to make this change would cost more work than the change would be worth. So ggplot2’s + comes from its history, not a fresh choice, and a grammar designed today has to make that choice for itself. He gives two reasons for preferring the pipe, and neither applies to gog. A third consideration is gog’s rather than his, and it is the one that decides the question.
His first reason is about one language. A pipe is a single idea that works everywhere in R, so a grammar that spells function composition differently is one more thing to learn. Switching between the two spellings is a frequent source of errors, his own included. That is true, and it is an argument about R and its packages. gog is one syntax written in four languages, and that changes the answer. + can be given a meaning in R, Python and Julia. The pipe cannot be given a gog meaning. Julia’s |> and R’s two pipes already mean something gog must not change. Python has no pipe operator at all, and no way to add one: you can give new meanings only to the operators it already has. In R and Julia a pipe would work with the meaning it already has. Python would need another spelling, and the languages would then have two different grammars. This project exists to avoid that. JavaScript decides nothing here, because it cannot overload any operator at all, which is why it spells all four as words (JavaScript).
His second reason is about arithmetic, and it is the stronger of his two reasons. A + looks like the plus sign of arithmetic, so you expect it to behave like one, in two ways. The order should not matter: a + b should equal b + a. And the grouping should not matter: a + (b + c) should equal (a + b) + c. ggplot2’s + does not always behave either way.
The six permutations drawn earlier in this chapter answer the first half, the order. In a plot with one mark, the mark and its channels commute. Move color ahead of x, or write the positions before the mark, and the engine produces the identical file. Once a plot has two marks, two orders change the picture: where a channel is written, and the order of the marks. In both, the order says something you chose: which marks a channel reaches, and which mark is drawn on top (Which orders matter).
The second half, the grouping, never comes up in gog. To group b + c first, b + c would have to mean something without a. In gog it cannot, because a sentence must start with a table. + joins an atom to a plot, and a plot starts with data(). So two atoms joined with no table before them, such as point and x(gdp), are refused:
point +x(gdp)
Error:
! gog: these atoms have no plot to join — the sentence starts with the data: `data(df) + point + x(gdp) + ...`.
The third point is gog’s own, and it is what decides the question. It is not an argument against the pipe. It is a statement of what a pipe would remove. A pipe applies functions, so every atom would have to become a function taking a plot and returning one. Make bar a function, then ask what bar * bin could mean. There is no answer: you cannot multiply two functions and get a histogram. The derive operator exists only because marks and transforms are things rather than actions. They are values, and they combine with each other before either one is joined to a plot.
This is the decision the book opens with: a plot is a specification, not drawing code. Hangeul shows the same idea. ㄱ is a letter, not an instruction: it fills one slot of a syllable block, and the block is built from its letters, not performed one step after another. gog’s atoms fill slots in the same way. The mark is the initial consonant, x, y and z are the vowel, and a channel with a legend is the final consonant (The grammar). A layer is built from them as a block is built from its letters. Hangeul’s letters are nouns, which is exactly why they combine, and gog’s atoms are nouns for the same reason. A grammar built on the pipe is a grammar of verbs: it can layer, because layering really is a sequence of additions, but it cannot derive. ggplot, ggplot2’s predecessor, was written as function composition, and it had no * either.
So + here is a choice rather than something inherited. Unlike the pipe, it can be given a meaning in R, Python and Julia. It commutes wherever order should not matter, and it leaves the atoms as nouns so that * can exist.
26.6 Summary
Each operator reads as a phrase when a plot is read aloud, and | and / have two readings each:
Operator
Reads as
Example
+
“…and also…”
point + line
*
“…derived by…”
bar * bin
|
“…split into panel columns by…”
data(gm) + point + x(gdp) + y(life) | facet(continent)
/
“…split into panel rows by…”
data(gm) + point + x(gdp) + y(life) / facet(era)
|
“…beside…”
(one specification) | (another)
/
“…above…”
(one specification) / (another)
In an arrangement, put each plot inside its own parentheses, and the arrangement inside parentheses before a theme(). Then the arrangement draws the same in R, Python and Julia.
A finite set of rules, and more plots than you will ever need to draw.
26.7 What comes next
The operators say how atoms combine into a sentence. Where you write each atom decides what it binds to, and Encoding scope gives that rule in full.
Becker, R. A., Cleveland, W. S., & Shyu, M.-J. (1996). The visual design and control of trellis display. Journal of Computational and Graphical Statistics, 5(2), 123–155. https://doi.org/10.1080/10618600.1996.10474701