2  The grammar

What do you have to say about your data before a plot can be drawn? gog’s whole vocabulary is a short list of ordinary English words. Learn them once and you can read, and write, any plot. This book calls each word an atom, a piece that nothing divides further.

This chapter is a small dictionary, and a dictionary is not read the way a story is. Nobody memorizes one before writing, and these lists ask for no memorizing either. Draw your own data with the few words your question needs, and the plot shows you what each one does. Each plot you write builds the skill, and the skill brings more of these words into use.

2.1 The kernel

Kind The words
Sources data query
Marks point line area bar step interval box ribbon text path rule zone surface edge
Channels x y z color size shape pattern opacity group label play
Transforms bin smooth count density proportion sum mean median max min quantile range confidence deviation bounds partition flow layout cluster, and the four collision modifiers, dodge stack jitter repel
Operators + layers, * derives, | and / arrange
Scales linear log time category order
Spaces flat space polar nest map globe network
Settings style theme palette
Labels title x_label y_label z_label
Facets facet
Selections brush

Every word in it is a common English word: no abbreviations, no acronyms, no silent letters. A common word is often taken already. Several of these name something in R, Python or Julia before gog is loaded, and the last section of this chapter says what happens then.

Read the kernel as two groups: what a plot needs, and what refines or arranges it. Five kinds build a plot. A source names the table that supplies the rows and the columns. A mark is the shape drawn for each row. A channel maps a column to something you can see, such as a position or a color. A transform derives new values from the columns you named. An operator joins the words into one sentence.

The other six refine a plot or arrange it, and none of them is required. A scale decides how a number becomes a position. A space is the surface the plot is drawn on: a flat page, a cube, a circle, a map, a network. A setting changes how the plot looks without saying anything about the data, and a label names it. A facet cuts one plot into panels. A selection picks rows out of a plot that is already drawn.

So the minimum sentence is a source, a mark, and the position channels that mark needs:

data(table) + mark + x(column) + y(column)

Two promises hold across the whole kernel. The engine draws every word in it, though not yet every pairing of words. And gog never accepts a word and then quietly ignores it: a sentence it cannot draw is refused, and the refusal names the rule. See Design laws for the difference between illegal (the grammar forbids it) and unsupported (the grammar allows it; the engine cannot draw it yet).

2.2 The colors of the code

In every specification in this book, each word of the kernel is printed in the color of its kind. These colors belong to the code, never to a plot. They are not decoration: they show the two groups above.

Four of the five kinds that build a plot are words, and each takes a color of its own. The fifth is the operator, which is punctuation in most of these languages, so it is set in heavier type instead of a color. The six kinds that refine or arrange a plot share one muted color, because none of them is required.

Your own names keep the color of the text around them. That is the first thing the coloring tells you: which words belong to gog, and which are yours.

gapminder_2007: first 5 of 142 rows
country continent year life population gdp
Afghanistan Asia 2007 43.828 31889923 974.5803
Albania Europe 2007 76.423 3600523 5937.0295
Algeria Africa 2007 72.301 33333216 6223.3675
Angola Africa 2007 42.731 12420476 4797.2313
Argentina Americas 2007 75.320 40301927 12779.3796
data(gapminder_2007) + bar * bin + x(life) + style(color = "tomato")
data(gapminder_2007) + bar * bin + x(col.life) + style(color = "tomato")
data(gapminder_2007) + bar * bin + x(:life) + style(color = "tomato")
plot(data(gapminder_2007), layer(bar, bin), x(col.life),
  style({ color: "tomato" }))
50 60 70 80 0 10 20 30 Count Life

“Given gapminder 2007: bars derived by bin, x is life, colored tomato.”

data names the table, bar is the mark, bin is the transform, and x is the channel. Those four build the plot. style refines it, so it takes the quiet color, and gapminder_2007, life and "tomato" are yours.

Inside style, the word color stays the color of the text. It names a property here rather than a column, so it is not the color channel: a word you write as an argument name is never one of gog’s words.

An ordinary syntax highlighter could not have drawn these colors. It colors code by the host language’s own categories, and the four languages disagree about gog’s words. R sees data as a function, Python sees a plain name, and none of the four has a category for a mark. So the four spellings of one specification would be colored four different ways. Coloring by kind instead gives a word the same color in all four languages. Your own editor highlights the ordinary way, so do not expect these colors there: on your screen a gog sentence looks like ordinary code.

Each kind has a section below, in the order of the kernel above: the five that build a plot first, then the six that refine or arrange one. Each section gives the words and what they do, and names the chapter that teaches them in full.

2.3 Sources: where the rows come from

Every sentence begins with a table, and a source is the word that names one. There are two.

Source Where the rows come from
data A table already in memory
query A table a database returns, named by SQL

query() stands exactly where data() stands, and everything after it is unchanged:

query(connection, "SELECT ...") + mark + x(column) + y(column)

The SQL stays inside query(). The columns after it are still bare names, because a database returns a table, and every sentence begins with a table. See Data.

2.4 Marks: the shape drawn for each row

A mark is the geometric form drawn for each row of data. There are fourteen, and you do not need to learn them all now. A few of them draw most of the plots in this book. Each mark has a chapter of its own in Part II, where its plots show what a one-line description cannot.

Mark What it draws
point One glyph (a small drawn symbol, a dot by default) per row
line Rows connected in x order: one value per x, read left to right
bar Rectangle from baseline to value, vertical or horizontal
area Filled region between a line and the baseline
step A line that holds each value until it changes (cumulative curves, histogram outlines)
interval A whisker spanning low→high at each x: error bars, ranges; needs a range transform
box Distribution per group: box, median, whiskers to 1.5 times the interquartile range, outliers as dots; carries its own summary
ribbon A filled band from a low boundary to a high one across x: confidence / spread bands; needs a range transform
text A string at each position: its content comes from the label channel
path Rows connected in the table’s order: a route that may return the way it came; takes style(arrow = )
rule A line at one position, spanning the other axis: reference lines, and rugs via style(reach = "edge")
zone A shaded region: bounded where its sides are known, spanning the whole panel where they are not. Five things can fix the sides: a categorical position (each category gets its own slot: the tile plot), bounds (names them), bin (cuts them: the heatmap), density (cuts or traces them), and a boundary (the data draws them: the choropleth, in map())
surface A sheet above the x-y plane: one row per (x, y) crossing, with a face between each block of four, or one row per cut cell, drawn flat at its own height. The only mark that can be drawn in the cube by itself
edge A stroke between two named things: one row per relation of a graph. Its endpoints come from layout, and it draws in network()

2.5 Channels: a column made visible

The first chapter placed each country by two positions and said nothing more about it. Which continent does each dot belong to? A channel maps a data column to a visual property of the mark. Adding one word to the sentence adds one thing the reader can see:

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

“Given gapminder 2007: points, x is gdp, y is life, color by continent.” You never asked for the legend. A mapped column earns one.

Channels do not all do the same work, and what separates them is what each one earns:

Channel What it does Earns
x Horizontal position, from a continuous or categorical column an axis
y Vertical position, from a continuous or categorical column an axis
z Depth, the third position: it makes a plot 3-D. Drawn by point, bar, interval, box, path and surface an axis
color Fill or stroke color, from a continuous or categorical column a legend
size Radius, on point a legend
shape Glyph (circle, square, triangle) on point a legend
pattern Texture: a fill’s hatch, a stroke’s dash. Every mark but point, text and surface a legend
opacity Transparency, on point, bar, edge and surface a legend
group Splits a mark into one line per category, without encoding it. On line, area, step, interval, box, ribbon, path, zone and surface nothing
label The string a text mark draws, its content nothing
play Cuts the plot into frames and plays them in sequence, from a continuous or categorical column a strip

The Earns column sorts the channels into four groups, and a Korean syllable shows how they fit together. It has three slots: an initial consonant, then a vowel, then an optional final consonant. The mark is the initial consonant, so the channels are the other two.

x, y and z are the vowels: a mark without one of them is silent, so at least one is required, and each earns an axis to be read against.

The five that earn a legend are the final consonants, the optional ㅁ (m) that turns 가 (ga) into 감 (gam). None is ever required, and each changes what the plot says, which is exactly why each needs a key to decode it.

group and label are neither. group splits without encoding: on a line it performs the very same split color does, one line per category, and then stops. Take away color’s visible differences and its legend, and what remains is group. So it works like the space between words rather than like a letter: it separates, but it shows nothing. label supplies a text mark’s content rather than refining it.

play is the fourth kind, and it stands outside the syllable the way faceting does. It does not refine one picture. It cuts the plot into frames and plays them in order, so what it earns is a strip naming the frame on screen. See Play.

Not every channel applies to every mark: a line is drawn with a single stroke, so it takes no per-row size or opacity. gog states the rule rather than guessing; see Design laws, and Combinations for the whole mark-by-channel grid on one page.

2.6 Transforms: values the data does not hold

A transform derives new values from the columns you named, and attaches to a mark with *. The mark stays what it is: bar * bin is still a bar, drawn over counted bands.

Eleven of them replace values with a computed statistic.

Transform What it computes
bin Cuts a continuous column into bands and counts the rows in each
count Tallies the rows in each category
proportion The same tally, as a share of the whole
density A smooth curve of where the rows lie
smooth A trend line fitted through the points
sum mean median max min Reduce a column to one value per group
quantile Reduce a column to the value that a given share of the rows fall below, quantile(0.9)

Four produce a low and a high together, which is what a span needs:

Transform What it computes
range A band between two quantiles, the whole group unless you name them
confidence The interval around the mean, which shows how exactly the mean is known
deviation A band of standard deviations around the mean, which is how spread the data is
bounds A low/high pair the table already holds, in two named columns

Four are the collision modifiers: they compute nothing, and they move marks that would otherwise land on top of each other:

Transform What it does
dodge Puts them side by side
stack Piles them on top of one another
jitter Spreads them sideways along a category
repel Moves labels off one another when they overlap

And four transforms compute an entire picture:

Transform What it computes
partition Divides a whole among nested parts: the treemap and the mosaic
flow Carries a magnitude through its stages: the flow diagram
layout Places a graph from an edge table: the network diagram
cluster Joins the closest categories of an axis one pair at a time: the cluster tree. It also puts a tile plot’s slots in a useful order

See Transforms for each one in detail, and Combinations for which marks take which.

2.7 The + operator: layering

A plot often needs more than one mark on one pair of axes, and one operator joins them. It is the same operator that joins every other word. + joins one word to the next. Every sentence in this book is built with it, and the words can be read left to right in the order they were written.

# One layer
data(gapminder_2007) + point + x(gdp) + y(life)
data(gapminder_2007) + point + x(col.gdp) + y(col.life)
data(gapminder_2007) + point + x(:gdp) + y(:life)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

“Given gapminder 2007: points, x is gdp, y is life.” One mark, one layer.

Bars can show the amounts, with a point on top of each bar to mark the exact value:

actuals: all 5 rows
year sales
2019 120
2020 135
2021 128
2022 152
2023 168
# Two layers: bars behind, points on top
data(actuals) + x(year) + y(sales) +
  bar + style(color = "lightsteelblue") +
  point + style(color = "steelblue", size = 6)
(data(actuals) + x(col.year) + y(col.sales) +
  bar + style(color = "lightsteelblue") +
  point + style(color = "steelblue", size = 6))
data(actuals) + x(:year) + y(:sales) + bar +
  style(color = "lightsteelblue") + point +
  style(color = "steelblue", size = 6)
plot(data(actuals), x(col.year), y(col.sales), bar,
  style({ color: "lightsteelblue" }), point,
  style({ color: "steelblue", size: 6 }))
2019 2020 2021 2022 2023 0 50 100 150 Sales Year

“Given the actuals: x is year, y is sales, bars and also points.”

Each mark opens a new layer. Layers are rendered in order: first mark drawn first (at the bottom).

Order appears more than once in this grammar, and it does not mean the same thing every time. Which orders matter gathers all of it in one place: what you may rearrange freely, what the engine refuses, and the two arrangements that change the picture with no warning.

2.8 The * operator: combining

* binds a mark to a transform, creating a derived mark type. The mark is always on the left; the transform is on the right. Where two transforms follow one mark, write them in the order they happen: bar * bin * mean cuts the axis into bands, then averages inside each one.

bar * bin      # histogram
line * smooth  # smoothed trend line
zone * bin     # heatmap: the same `bin`, cutting both axes

The last line, zone * bin, shows the pattern: one transform means one thing, and the mark decides where it applies. In a histogram the bar’s height carries the count, so the count takes one axis and bin cuts the other. A zone carries its count by color instead, so both axes are free, and the same bin cuts both. When one of a zone’s positions is categorical, bin cuts only the other one, because a category is already cut into slots.

The six summaries (sum, mean, median, max, min, quantile) follow the same rule, read in the other direction. A summary groups the rows by every position the mark does not measure with. It then reduces the named column on the position the mark does measure with: y on a bar, color on a zone, z in space(). So zone * mean + x(a) + y(b) + color(v) averages v inside every cell two categories cross.

See Operators for the full rules, and Transforms for each transform in detail.

2.9 The | and / operators: faceting, or composing

Across the world, life expectancy rises with income. One cloud of 142 countries cannot show whether that holds inside each continent. A second question, how many countries each continent has, needs a plot of its own beside the first. Two operators arrange both. | puts things side by side and / puts one above the other. Whether those are panels or whole plots is decided by what you write on the right.

With facet() on the right they split one plot into panels, one per category of the column it names:

# One panel per continent, side by side
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.”

Every panel shares one scale, that is what makes them comparable, and both operators together cross into a grid. See Faceting for the full rules.

With another plot on the right they arrange the two on one page, each keeping its own coordinate space:

# Two plots, side by side
(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.”

Composed plots share nothing until they name the same column on the same axis, and then they share that axis outright: one scale, one extent, drawn once. That single rule is what makes a marginal plot (a scatter with a small distribution drawn along each edge) two operators rather than a chart type. See Composition.

2.10 Scales: how a number becomes a position

Incomes in gapminder_2007 run from under 300 dollars to nearly 50,000. Most countries therefore crowd the left end of a linear axis, and a log scale spreads them out. A scale is written on the binding, the channel with its column in it. It is not a word of its own, because it belongs to one channel and nothing else can combine with it:

data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
  x_label("GDP per capita")
(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
  x_label("GDP per capita"))
data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
  x_label("GDP per capita")
plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
  y(col.life), x_label("GDP per capita"))
1K 10K 40 50 60 70 80 Life GDP per capita

“Given gapminder 2007: points, x is gdp on a log scale, y is life.”

x_label() writes your own words under the axis in place of the column name. It is a label, not a scale, and Labels, below, covers the four of them.

Scale What it does
linear Equal steps for equal differences. The default
log Equal steps for equal ratios, on x, y, color, size and opacity, with any base
time Positions dates and times, chosen from the column type: Date and POSIXct
category One slot per distinct value, chosen from the column type: columns of text, and columns with a declared order
order Which way a categorical axis runs, or declare it on the column: a factor in R, ordered() in the other three

A scale is not a transform: it produces no new values, only different positions for the ones you have. Where the two meet, what a log scale means for bar * bin or bar * sum, is set out in Scales.

The same binding takes two more words for the same reason. limits says what range the channel runs over when the data is not the authority on that, and tick_count says how densely an axis should be labeled. Both describe the scale, so both are written where the scale is written. Scales has all three.

2.11 Spaces: the surface a plot is drawn on

A space decides what a position means. Every plot so far has been drawn on the plane. That space is flat, and you never have to write it.

Space Where the marks stand
flat The plane: x across, y up. The default
space The cube: x, y and z, projected onto the page
polar The plane bent into a circle: one position becomes an angle, the other a radius
nest Rectangles inside rectangles, each sized by its share of the one holding it
map The earth flattened: longitude across, latitude up
globe The earth itself, viewed: marks stand on the half of the sphere that faces you
network The space of a graph: positions come from layout and their numbers mean nothing, so it draws no axes. State a viewing angle in network() and the layout is computed in the cube instead

One space holds the whole plot. See Space, Polar, Nest, Map, Globe and Network.

2.12 Settings: values you set rather than map

Every channel above answers a question about the data. color(species) asks “which species?”, and earns a legend so the reader can answer it.

Making every point one color asks nothing. It maps no column, needs no scale, and has nothing to decode, so it is not a channel. That is style:

Setting What it fixes
style(color = ) One color for the whole layer
style(opacity = ) One opacity, 01
style(size = ) Point radius, or line stroke width
style(shape = ) One glyph for every point
style(pattern = ) The texture of a mark’s paint: a stroke’s dash (solid/dashed/dotted), a fill’s hatch (hatch/crosshatch/grid/dots). Every mark but point, text and surface
palette( ) Which colors the color channel uses
theme( ) The page around the plot: grid, background, fonts, size
# map: color carries the continent, and a legend decodes it
data(gapminder_2007) + point + x(gdp) + y(life) + color(continent)
data(gapminder_2007) + point + x(col.gdp) + y(col.life) + color(col.continent)
data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  color(col.continent))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp Continent Asia Europe Africa Americas Oceania
# set: one color, no question asked, no legend drawn
data(gapminder_2007) + point + x(gdp) + y(life) +
  style(color = "tomato", opacity = 0.5)
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  style(color = "tomato", opacity = 0.5))
data(gapminder_2007) + point + x(:gdp) + y(:life) +
  style(color = "tomato", opacity = 0.5)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  style({ color: "tomato", opacity: 0.5 }))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

“Given gapminder 2007: points, x is gdp, y is life, colored tomato, with opacity 0.5.”

See Setting vs mapping for the full rules.

2.13 Labels: the words on the plot

An axis carries the name of the column bound to it. A label replaces that name when the column name is not what a reader should see.

Label What it names
title The plot
x_label y_label z_label One axis each

title() is the one that adds words rather than replacing them, because a plot has no other way to say what it is about.

2.14 Facets: one panel per category

Facet What it cuts
facet Splits one plot into panels, one per category of the column it names

A facet is written on the right of | or /, which is why you have already seen it drawn in the operator section: … | facet(continent). See Faceting.

2.15 Selections: choosing rows in a drawn plot

Selection What it does
brush Dims the rows you did not pick, and removes none of them

A selection is the only kind that acts on a plot after it is drawn. Dragging changes what you see, never the sentence. Draw the sentence again and you get the selection it names, not the one you dragged. See Selection.

2.16 Names the host language already uses

Every word in the kernel is an ordinary English word, and ordinary words are already taken. mean names a statistic in every language that has one. data, order, range, text and box all name something in R before gog arrives. That is a consequence of choosing plain names rather than a sign that something has gone wrong, and in ordinary code almost nothing changes.

When a word is both gog’s and the language’s own, the language decides which one your call reaches. gog does not change that, and each language decides differently, so the answer is different in each one:

Language What the language does To reach the other one
R Twenty kernel words collide with R names. Nine are objects, and a call skips them; eleven are functions, and those mask R’s own base::order(x), stats::density(x)
Python A star-import rebinds six builtins, because Python has no rule that protects them from builtins import sum
Julia Two modules exporting one name is an error, never a silent choice Base.sum([1, 2, 3])
JavaScript Nothing happens, because no name reaches your scope unless you import it nothing to do

R’s case is the one to understand, because it looks the worst and matters the least. A mark takes no argument, and neither does a statistic like mean or max, so those words are plain objects rather than functions. When R evaluates a call it looks for a function of that name, and skips any binding that is not one. Base R therefore still answers mean(c(1, 2, 3)), and sapply(df, mean) works for the same reason. The message R prints when the package loads names every collision and cannot show you this distinction, which is why the message looks worse than the problem is.

The other eleven words take arguments, so they are functions, and a function does mask R’s own. Once gog loads, range(0.25, 0.75) is gog’s band; to reach base R’s, write base::range(). Say you forget the masking and write range(heights) for a vector’s smallest and largest. The call reaches gog’s range(), which takes two numbers between 0 and 1, not a vector. For eight of the eleven functions, a wrong argument like that proves the mistake: the call is refused, and the refusal points you to base::range(). Now write data(mtcars), meaning to load R’s example dataset. That call is also correct gog, naming a plot’s table, so gog answers it and cannot warn you. box() and title("...") are the same case. A refusal needs a mistake the engine can detect, and these three calls hold none.

The collision has no good ending. Prefixing the vocabulary, gog_mean and gog_range, would remove the reason for the vocabulary: a reader who has to learn a prefix learns every word twice, and none of the words is English any more. Renaming order to sort_by would take away a word every reader already knows. So the words stay, and the few places that need a qualified name write one.

Each binding chapter carries three things for its language: the full list of collisions, the rule above as code you can run, and the few spellings the rule cannot save. R has five. All five share one shape: a word is handed to one function, which passes it to a second, the way aggregate(v ~ g, df, mean) passes mean on. See R, Python, Julia and JavaScript.

The next two chapters use this vocabulary: first you read ten plots, then you write your own.