| 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 |
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.
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" }))“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))“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))“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:
| 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 }))“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 axesThe 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))“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)))“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"))“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, 0–1 |
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))# 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 }))“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.