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.

2.1 The kernel

Kind The words
Tables data query
Marks point line area bar step interval box ribbon text path rule zone surface
Channels x y z color size shape pattern opacity group label play
Transforms bin smooth count density proportion sum mean median max min range confidence bounds partition, and the three that resolve collisions, dodge stack jitter
Operators + layers, * derives, | and / arrange
Scales linear log time category order
Spaces flat space polar nest map
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.

Read the kernel as two groups: what a plot needs, and what refines or arranges it. Five kinds build a plot. A table 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 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 table, a mark, and the positions that mark needs:

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

Two promises hold across the whole kernel. The engine draws every word in it. 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).

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.2 Tables: where the rows come from

Every sentence begins with a table, and there are two ways to name one.

Table 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.3 Marks: the shape drawn for each row

A mark is the geometric form drawn for each row of data.

Mark What it draws
point One glyph per row
line Rows connected in x order: a function read along a domain
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 (CDFs, 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×IQR, 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 double back; 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 panel where they are not. Five things know them: a categorical position (a category owns a 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 over a floor whose cells tile: one row per (x, y) crossing with a face between each block of four, or one row per cut cell with a flat plateau on each. The only mark that draws in the cube alone

2.4 Channels: a column made visible

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 play the same part, and what tells them apart is what each one earns:

Channel What it does Earns
x Horizontal position, from a numeric or categorical column an axis
y Vertical position, from a numeric 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 categorical or continuous 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 and bar a legend
group Splits a mark into one series 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 numeric or categorical column a strip

The Earns column sorts the channels into four kinds, and a Korean syllable is the shape to hold them against. 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 polyline per category, and then stops. It is what is left of color once the decoding is taken away, which makes it closer to the space between words than to a letter. 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.5 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.

Ten 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

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

Transform What it computes
range A low/high pair computed from the data
confidence The interval around the mean
bounds A low/high pair the table already holds, in two named columns

Three do not compute anything. 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

And partition divides a whole among nested parts, which is what a treemap and a mosaic are built from.

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

2.6 The + operator: layering

+ 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.

# 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

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

Order comes up 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 without saying so.

2.7 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 is the pattern worth noticing: one transform means one thing, and the mark decides how it lands. A bar leaves an axis free to measure the count along, so bin cuts the other one; a zone measures nothing by length and needs an extent on both, so the same word cuts every axis with a width to cut and the count becomes the color. On a zone with one categorical position it cuts the other one only, since a category arrived already cut.

The summaries divide by the same arithmetic, read the other way round. zone * mean + x(a) + y(b) + color(v) averages v within every cell two categories cross, because a summary groups by every position the mark does not measure with and reduces the column named on the one it does, which on a bar is y, on a zone is color, and in space() is z.

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

2.8 The | and / operators: faceting, or composing

| 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

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

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 two operators rather than a chart type. See Composition.

2.9 Scales: how a number becomes a position

A scale is written on the binding, the channel with its column in it. It is not an atom 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.”

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: text and factor columns
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.10 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, which is flat and needs no saying.

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

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

2.11 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 hands out
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

See Setting vs mapping for the full rules.

2.12 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.13 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 it appeared in the operator section above: … | facet(continent). See Faceting.

2.14 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. What it does not change is the sentence, so the printed page still shows the bound that sentence named. See Selection.