26  Operators

How do you put two things in one picture? gog has exactly four operators. They are not atoms. They are the assembly rules: how atoms combine.

Think of them the way Hangeul (한글) works: consonants and vowels are the atoms; the rule “initial + vowel + optional final = syllable block” is the operator. The rule never changes, and the number of things you can build with it has no limit.

+   compose   (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 / also arrange whole plots on a page, and that second use has its own section below.


26.1 +: compose

Every sentence you have written so far holds several atoms, and one operator joins them. + is the general assembly operator. Every atom enters a specification via +:

data(df) + point + x(col_a) + y(col_b) + color(group) + title("My plot")

Adding a mark (point, line, bar, …) starts a new layer. Adding a channel (x(), color(), …) or label (title()) attaches to the appropriate scope (plot-level or the nearest preceding mark, see Encoding scope).

26.1.1 Layering with +

If you have edited video, you already know this idea. A video is a stack of tracks: the picture, the sound, the captions, the logo, the effects. Each track is separate, they play at the same time, and you see one finished video. A plot layers the same way.

Multiple marks in the same expression produce multiple layers, rendered in order:

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
life_bands: all 3 rows
life band
60 Low
70 Middle
80 High
# Scatter plot: points, and a horizontal reference line at each band
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")
(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"))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 GDP vs Life Expectancy, 2007 Life Gdp Continent Asia Europe Africa Americas Oceania

“Given gapminder 2007: x is gdp, y is life, points colored by continent, and also rules from the life bands.”

Layers draw in the order they are written: the first mark sits at the bottom. Add a line after point to draw the line on top of the points.

26.1.2 The order of +: data, mark, positions, refinements

Every sentence in this book is written in one order, and you may wonder whether the engine requires it. Inside a single layer the channels commute, and the mark commutes among them. Write the mark, x and color in any order and the plot is the same. Three things can be arranged six ways, so here are all six.

iris_flowers: first 5 of 150 rows
sepal_length sepal_width petal_length species
5.1 3.5 1.4 setosa
4.9 3.0 1.4 setosa
4.7 3.2 1.3 setosa
4.6 3.1 1.5 setosa
5.0 3.6 1.4 setosa
((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))
below(below(beside(plot(data(iris_flowers), layer(bar, bin),
  x(col.petal_length), color(col.species)),
  plot(data(iris_flowers), layer(bar, bin), color(col.species),
  x(col.petal_length))),
  beside(plot(data(iris_flowers), x(col.petal_length), layer(bar, bin),
  color(col.species)),
  plot(data(iris_flowers), x(col.petal_length), color(col.species),
  layer(bar, bin)))),
  beside(plot(data(iris_flowers), color(col.species), layer(bar, bin),
  x(col.petal_length)),
  plot(data(iris_flowers), color(col.species), x(col.petal_length),
  layer(bar, bin))))
2 4 6 0 10 20 30 40 Count Petal Length Species setosa versicolor virginica 2 4 6 0 10 20 30 40 Count Petal Length Species setosa versicolor virginica 2 4 6 0 10 20 30 40 Count Petal Length Species setosa versicolor virginica 2 4 6 0 10 20 30 40 Count Petal Length Species setosa versicolor virginica 2 4 6 0 10 20 30 40 Count Petal Length Species setosa versicolor virginica 2 4 6 0 10 20 30 40 Count Petal Length Species setosa versicolor virginica

“Given the iris flowers: bars derived by bin, x is petal length, color by species.”

Six sentences, and the same histogram six times. Not six plots that look alike: the engine emits the same bytes for every one of them.

So if the order does not change the picture, why does every plot in this book follow the same one: data, mark, positions, refinements? Because a fixed order lets a plot read like a sentence, and the sentence has 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. A visual is a mark before it is anything else (Minimum Syllable): bar * mean * dodge says what you are drawing and what was done to it, as one unbroken 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 stands, and a mark cannot render 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 renders. Writing the removable parts last is also how you build a plot: the minimum sentence first, then refine (First plot).

The order is a reading convention, not a grammar rule: the engine accepts any permutation that scopes the same way. Where position genuinely does matter (a channel written before or after a mark across two or more layers) is the subject of Encoding scope. One order everywhere is worth learning: every plot in the book, and every plot you write, then reads the same way.

Which orders matter below sets out 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:

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
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"))
Asia Europe Africa Americas Oceania 0 20 40 60 80 Mean life expectancy by continent, 1957 vs 2007 Life Continent Era 1957 2007

“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: dodge has nothing to set side by side until a color (or group) 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, and neither of those involves color. 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.

And the order is not a compromise; it is exactly right, once you see what dodge does. dodge subdivides the x slot: each continent’s space is cut into one sub-bar per era. So it needs both neighbors: x(continent) says which slot to divide, color(era) says into how many pieces. The sentence lists them in the order dodge uses them: the axis it splits along, then the groups it splits into. The positions do not separate dodge from what it needs; one of them is the slot dodge divides.

You also never have to scan the line to check the split is there. bar * mean * dodge with no color or group does not draw a lone bar; it is refused, pointing you at the missing channel. The legality check requires both, so writing color far from dodge is safe: the two are bound whether or not they sit side by side on the page.


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     # histogram
line * smooth # smoothed trend line
line * density # kernel density curve

Two rules govern *, and only two. The mark goes on the left, and the transforms go on the right. Then, among the transforms themselves, the order you write them in does not matter: any order draws the same plot. The rest of this section explains each rule.

26.2.1 Why the order is fixed

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.

bar * bin   # ✅  "a bar mark, derived by binning"
bin * bar   # ❌  Refused: `*` is not defined for transform * mark

26.2.2 What * is rigid about

The rule that matters is the one above: the mark is on the left, always, and no transform may stand there. That is what makes bar * bin a compound mark rather than a function call, and it is why the atoms can be nouns.

Write the transforms in the order they happen. This is a reading convention: any order draws the same plot, and this one lets you follow the steps as you read. bar * bin * mean cuts the axis into bands, then averages inside each one. That is the order it runs in, and there is no other order it could run in.

The reason is what mean needs. A statistic reduces a group to one number, and a continuous column has no groups: every value stands alone, so its mean is itself. bin is what makes the groups, by turning many distinct numbers into one band. So the cut comes first because otherwise the mean has nothing to average.

So two transforms work together only when they do different jobs. One makes the groups, and the other measures inside them or rescales the result. A pair that answers the same question twice is refused rather than sequenced, so bin and count (both of them tallies with groups of their own) cannot both be in one layer:

data(gapminder_2007) + bar * bin(12) * count + x(life)
data(gapminder_2007) + bar * bin(12) * count + x(col.life)
data(gapminder_2007) + bar * bin(12) * count + x(:life)
plot(data(gapminder_2007), layer(bar, bin(12), count), x(col.life))
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 engine decides the running order from the transforms themselves, not from where you typed them, so you cannot get it wrong. Write it the way you mean it, and the sentence will read in the order it runs. Chaining transforms goes through the legal transform pairs one by one.

26.2.3 Live examples

The three compound marks named above appear in the sentences you will write most often. Here each one is drawn, so you can see what the transform did to the rows before the mark drew them.

# Histogram: life expectancy across all countries in 2007
data(gapminder_2007) + bar * bin + x(life) + y(count) +
  x_label("Life expectancy (years)") + y_label("Count") +
  title("Distribution of Life Expectancy, 2007")
(data(gapminder_2007) + bar * bin + x(col.life) + y(col.count) +
  x_label("Life expectancy (years)") + y_label("Count") +
  title("Distribution of Life Expectancy, 2007"))
data(gapminder_2007) + bar * bin + x(:life) + y(:count) +
  x_label("Life expectancy (years)") + y_label("Count") +
  title("Distribution of Life Expectancy, 2007")
plot(data(gapminder_2007), layer(bar, bin), x(col.life), y(col.count),
  x_label("Life expectancy (years)"), y_label("Count"),
  title("Distribution of Life Expectancy, 2007"))
50 60 70 80 0 10 20 30 Distribution of Life Expectancy, 2007 Count Life expectancy (years)
# Scatter + LOESS smooth layer on top
data(gapminder_2007) + x(gdp) + y(life) +
  point + color(continent) +
  line * smooth +
  title("GDP vs Life Expectancy with LOESS Smooth")
(data(gapminder_2007) + x(col.gdp) + y(col.life) +
  point + color(col.continent) +
  line * smooth +
  title("GDP vs Life Expectancy with LOESS Smooth"))
data(gapminder_2007) + x(:gdp) + y(:life) + point + color(:continent) +
  line * smooth + title("GDP vs Life Expectancy with 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 LOESS Smooth"))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 GDP vs Life Expectancy with LOESS Smooth Life Gdp Continent Asia Europe Africa Americas Oceania

“Given gapminder 2007: x is gdp, y is life, points colored by continent, and also a line derived by smooth.”

# Kernel density estimate
data(gapminder_2007) + line * density + x(life) + y(density) +
  x_label("Life expectancy (years)") + y_label("Density") +
  title("Kernel Density: Life Expectancy 2007")
(data(gapminder_2007) + line * density + x(col.life) + y(col.density) +
  x_label("Life expectancy (years)") + y_label("Density") +
  title("Kernel Density: Life Expectancy 2007"))
data(gapminder_2007) + line * density + x(:life) + y(:density) +
  x_label("Life expectancy (years)") + y_label("Density") +
  title("Kernel Density: Life Expectancy 2007")
plot(data(gapminder_2007), layer(line, density), x(col.life),
  y(col.density), x_label("Life expectancy (years)"), y_label("Density"),
  title("Kernel Density: Life Expectancy 2007"))
40 60 80 0.00 0.01 0.02 0.03 0.04 Kernel Density: Life Expectancy 2007 Density Life expectancy (years)

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


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 on one page. | puts things side by side and / puts one above the other. What is written on the right decides which. A facet() splits one plot into panels; 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.

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))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 0K 10K 20K 30K 40K 50K 0K 10K 20K 30K 40K 50K 0K 10K 20K 30K 40K 50K 0K 10K 20K 30K 40K 50K Asia Europe Africa Americas Oceania Life Gdp

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

/ produces small multiples stacked in rows:

gm_two <- gapminder_2007[gapminder_2007$continent %in% c("Africa", "Europe"), ]
data(gm_two) + point + x(gdp) + y(life) / facet(continent)
40 50 60 70 80 0K 10K 20K 30K 40K 50K 40 50 60 70 80 Europe Africa Life Gdp

“Given the gm two table: points, x is gdp, y is life, split into panel rows by continent.”

Write both to cross them into a grid: each operator applies to the facet written after it, read left to right. See Faceting for the grid, what the panels share, and why the facet column must be a category.

With a plot on the right, the same two operators arrange separate plots on one page. Each plot keeps its own coordinate space. Two plots side by side share the vertical axis when they name the same column on it, and two plots one above the other share the horizontal axis the same way:

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

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

One pair of operators, two meanings, and what you write on the right decides which one you get. See Composition.


26.4 Which orders matter

This book writes every sentence the same way: data, mark, positions, refinements. That is a preference rather than a rule, and the engine enforces only part of it. The difference is worth knowing, because it tells you where to be careful. There are three kinds of ordering, and only one of them needs your attention.

Kind What it means Under + Under *
Free Either way draws exactly the same picture. The mark and its positions, in any arrangement. x before y, or y before x. Refinements among themselves. Two transforms after one mark. Write them in the order they happen; the engine works that order out either way.
Refused The engine stops and names what to move. A sentence that does not begin with data(). 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.

Read the bottom right cell first. * is rigid about one thing only, which is that the mark stands on its left. Once the mark is there, no arrangement of what follows changes what you get.

Free is most of it. Every one of these draws the identical plot, and the book writes the first only because a fixed order is easier to read:

(data(gapminder_2007) + point + x(gdp) + y(life)) |
  (data(gapminder_2007) + x(gdp) + point + y(life))
((data(gapminder_2007) + point + x(col.gdp) + y(col.life)) |
  (data(gapminder_2007) + x(col.gdp) + point + y(col.life)))
(data(gapminder_2007) + point + x(:gdp) + y(:life)) |
  (data(gapminder_2007) + x(:gdp) + point + y(:life))
beside(plot(data(gapminder_2007), point, x(col.gdp), y(col.life)),
  plot(data(gapminder_2007), x(col.gdp), point, y(col.life)))
0K 20K 40K 40 50 60 70 80 Life Gdp 0K 20K 40K Gdp

Both name life on y, so the two panels share that axis and it is drawn once, on the left. Each panel still draws its own gdp axis underneath it.

Refused is the engine telling you the sentence has no subject yet. A setting needs a mark to attach to, and a plot needs its table first. + has to have a plot on its left before it can add anything:

data(gapminder_2007) + style(size = 3) + point + x(gdp) + y(life)
data(gapminder_2007) + style(size = 3) + point + x(col.gdp) + y(col.life)
data(gapminder_2007) + style(size = 3) + point + x(:gdp) + y(:life)
plot(data(gapminder_2007), style({ size: 3 }), point, x(col.gdp),
  y(col.life))
Error:
! gog: `style()` has no mark to style. Put it after a mark, e.g. `point + style(color = "tomato")`.

Meaningful is the only one to watch, because it is the only one that stays silent. With two marks in a sentence, a channel written before them belongs to the plot and reaches both, and a channel written after one belongs to that mark. Both are legal, neither warns, and they draw different pictures. That is the subject of Encoding scope. It is also why this book keeps one order: when every sentence looks the same, a different arrangement is a deliberate signal.

The other silent one is the marks themselves. Two marks swapped draw in the order you wrote them, so the second sits on top of the first.

26.4.1 What groups first

The three kinds above are about where you may put a word. Precedence is a different question: how the words you did write get grouped. It is what lets a sentence like this one carry no parentheses at all.

data(gapminder_2007) + bar * bin + x(life) | facet(continent)
data(gapminder_2007) + bar * bin + x(col.life) | facet(col.continent)
data(gapminder_2007) + bar * bin + x(:life) | facet(:continent)
plot(data(gapminder_2007), layer(bar, bin), x(col.life),
  across(col.continent))
50 60 70 80 0 5 10 15 50 60 70 80 50 60 70 80 50 60 70 80 50 60 70 80 Asia Europe Africa Americas Oceania Count Life

* groups first, then +, then |. So the transform joins its mark first, the atoms join the plot next, and the facet splits the finished plot last. / groups before |; the next section has the rule.

Those are R’s own precedences, and Python’s and Julia’s, so one sentence groups the same way in three of the four languages. JavaScript can give no operator a meaning of its own, so it spells the four as words, which makes the grouping something you see instead of something you remember (JavaScript).

26.4.2 What parentheses may group

Parentheses change which operator runs first, and that is the whole of what they do. They group plots, which is why a composed page uses them: / is applied before |, so a | b / c reads as a | (b / c) unless you say otherwise (Composition).

They do not group marks. A second data() applies to the mark written directly after it (Data). Parentheses around several marks do not extend that table to all of them:

actuals: all 5 rows
year sales
2019 120
2020 135
2021 128
2022 152
2023 168
forecast: all 3 rows
year sales
2024 180
2025 195
2026 210
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 worth having, because the alternative is worse than an error. The engine cannot give both marks inside the parentheses the second table. Its only other choice would be to discard both marks and draw a plot missing two layers, in silence. 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.


26.5 Why +, and not a pipe

Readers who know R’s tidyverse packages ask why gog does not use a pipe. 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 would have used a pipe if one had existed. ggplot2 used + instead.

He also answers the question a reader asks next. ggplot2 cannot change now: its functions take the mapping and the data first, while a pipe needs the plot. And he judges that a new package built only to change this would not be worth what it would cost. So for ggplot2 the matter is settled by history rather than by taste, and a grammar starting fresh has to answer it on its own. Three reasons are given for preferring the pipe. Two of them do not apply to gog, and the third is the one that settles the question the other way.

The first is about one language. A pipe is a single idea that works everywhere in R, so a grammar spelling composition differently is one more thing to learn, and 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 spoken by 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. Piping would work in R and Julia and need some other spelling in Python. The languages would then have two different grammars, and 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).

The second is about arithmetic, and it is the strongest. You expect x + y to equal y + x, and ggplot2’s + does not always do that. That objection is answered a few sections up. Here the channels commute, and so does the mark among them: move color ahead of x, or write the positions before the mark, and the engine emits the same bytes. One thing does depend on order: two marks swapped. There the order is the drawing order you asked for, and the second mark draws on top.

The other half of that objection is grouping, whether x + (y + z) means what (x + y) + z means. The question cannot arise here, because x + (y + z) cannot be written at all. + always takes a plot on its left, so two atoms with no table behind them never form a smaller expression to group. They 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) + ...`.

Atoms never collect on their own, so there is no grouping to get wrong.

The third reason is the one that decides it, and it is not an argument against the pipe. It is what a pipe would take away. A pipe applies functions: x |> f means f(x), 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. Nothing at all: 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.

That is the same decision this book opens with, arriving a second time. ㄱ is a letter, not an instruction. You put it in a slot next to other letters, and the block is assembled from its pieces rather than performed in sequence. Hangeul’s atoms are nouns, which is exactly why they compose, and gog’s 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. Wickham’s own pre-ggplot2 experiment was function composition, and it had no * either.

So + here is a choice rather than something inherited. It is the one operator that 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

Operator Reads as Example
+ “…and also…” point + color(continent)
* “…derived by…” bar * bin
| “…split into panel columns by…” plot | facet(continent)
/ “…split into panel rows by…” plot / facet(continent)
| “…beside…” plot_a | plot_b
/ “…above…” plot_a / plot_b

A finite set of rules. An unlimited number of plots.