The phrasebook answers “how do I spell this in gog”. This appendix answers the question underneath it: can it be spelled. Every entry in ggplot2’s reference index was read against the kernel, and the result is two lists of very different lengths.
The ggplot2 column is dated on purpose, because nothing here can check it: reference index of ggplot2 4.0.3, read 2026-07-26(Wickham et al., 2026). Every gog claim below is either a live plot on this page or a refusal the build re-runs, and ggplot2 is deliberately not a dependency of this book, so its half is the half that will age.
The short list is the one people expect to be long, so it goes first and is not padded. The long list is the interesting one, because every item on it is one of four things, and three of the four were decided on purpose.
F.1 What gog draws that ggplot2 does not
Three things, at the level of pictures.
F.1.1 Anything in the cube
Binding z stands a plot up off the page, and surface draws a height over a plane:
data(maunga_whau) + surface +x(east) +y(north) +z(elevation) +color(elevation) +palette("plasma") +title("Maunga Whau, one row per crossing")
(data(maunga_whau) + surface + x(col.east) + y(col.north) + z(col.elevation) + color(col.elevation) + palette("plasma") + title("Maunga Whau, one row per crossing"))
data(maunga_whau) + surface +x(:east) +y(:north) +z(:elevation) +color(:elevation) +palette("plasma") +title("Maunga Whau, one row per crossing")
plot(data(maunga_whau), surface,x(col.east),y(col.north),z(col.elevation),color(col.elevation),palette("plasma"),title("Maunga Whau, one row per crossing"))
Six marks draw in the cube (point, path, bar, interval, box and surface), the viewing angle is space(turn = , tilt = ), and the cube composes with the two partitions like anything else: | facet(g) gives one projected box per panel and + play(t) runs the sequence inside one. ggplot2 has no depth position and no coordinate system that would read one, and says so outright in its own first chapter, so none of this is expressible in it: in R these plots come from plotly, rgl or lattice, each with a grammar of its own. Here it is one more vowel in the same sentence, and Space is the chapter.
F.1.2 Texture, mapped from a column
pattern maps a categorical column to a hatch on fills and a dash on strokes. Put it on the same column as color and the series are told apart twice over, in one merged key:
data(iris_flowers) + bar * mean +x(species) +y(petal_length) +color(species) +pattern(species) +title("Hue and hatch, one legend")
(data(iris_flowers) + bar * mean + x(col.species) + y(col.petal_length) + color(col.species) + pattern(col.species) + title("Hue and hatch, one legend"))
data(iris_flowers) + bar * mean +x(:species) +y(:petal_length) +color(:species) +pattern(:species) +title("Hue and hatch, one legend")
plot(data(iris_flowers),layer(bar, mean),x(col.species),y(col.petal_length),color(col.species),pattern(col.species),title("Hue and hatch, one legend"))
ggplot2 has linetype for strokes and nothing for the inside of a filled shape; hatching a bar there means the third-party ggpattern. So the print-safe and color-blind-safe bar chart above is a gog sentence with no ggplot2 equivalent in the box.
That is the whole list. Everything else gog draws, ggplot2 draws too, and usually with more knobs on it.
F.1.3 The same plot, without a word for it
Not a picture ggplot2 lacks, but one this grammar draws without being taught it. geom_violin() is a mark of its own there, with stat_ydensity() beside it; here it is ribbon * density, the mark that closes on a second boundary, given the transform that estimates a distribution, over a category:
data(gapminder_2007) + ribbon * density +x(continent) +y(life) +y_label("Life expectancy") +title("ribbon * density: no new word required")
(data(gapminder_2007) + ribbon * density + x(col.continent) + y(col.life) + y_label("Life expectancy") + title("ribbon * density: no new word required"))
data(gapminder_2007) + ribbon * density +x(:continent) +y(:life) +y_label("Life expectancy") +title("ribbon * density: no new word required")
plot(data(gapminder_2007),layer(ribbon, density),x(col.continent),y(col.life),y_label("Life expectancy"),title("ribbon * density: no new word required"))
The half violin comes with it, for nothing, because area and ribbon already differ by where a region closes: area * density + x(continent) + y(life). That is what the orthogonality is for: not that the plot is possible, but that the vocabulary did not grow to make it so.
The donut is the same story arriving from a parameter rather than a geom. ggplot2 spells it coord_radial(inner.radius = ), a number the space is told; here the hole is not a property of the circle at all. It is a stretch of the radial axis with nothing standing on it, so a tree one level deep, drawn on an axis that starts below the first ring, is a donut:
data(spending) + zone *partition(group) +x(amount) +y(depth, limits =c(0, 2)) +color(group) +polar() +title("a donut is a sunburst of a one-level tree")
(data(spending) + zone * partition(col.group) + x(col.amount) + y(col.depth, limits = [0, 2]) + color(col.group) + polar() + title("a donut is a sunburst of a one-level tree"))
data(spending) + zone *partition(:group) +x(:amount) +y(:depth, limits = [0, 2]) +color(:group) +polar() +title("a donut is a sunburst of a one-level tree")
plot(data(spending),layer(zone,partition(col.group)),x(col.amount),y(col.depth, { limits: [0,2] }),color(col.group),polar(),title("a donut is a sunburst of a one-level tree"))
Nothing there is about donuts, and the same limits that hollow this out hollow the three-level sunburst and leave an empty band under the flat icicle, which is what tells you the hole belongs to the scale rather than to the bend. The one reading that cannot state a hole is the plain pie, whose radius is a constant with no domain to widen.
The marginal plot is the same story one level up. ggExtra adds ggMarginal() to ggplot2 and patchwork adds a layout algebra beside it; here a page is | and / between two plots, and the marginals fall out of one rule about what a shared column means (Composition):
(data(gapminder_2007) + bar * bin +x(gdp) +theme(height =130)) / ((data(gapminder_2007) + point +x(gdp) +y(life)) | (data(gapminder_2007) + bar * bin +y(life) +theme(width =130)))
((data(gapminder_2007) + bar *bin+ x(col.gdp) + theme(height =130)) / ((data(gapminder_2007) + point + x(col.gdp) + y(col.life)) | (data(gapminder_2007) + bar *bin+ y(col.life) + theme(width =130))))
(data(gapminder_2007) + bar * bin +x(:gdp) +theme(height =130)) / ((data(gapminder_2007) + point +x(:gdp) +y(:life)) | (data(gapminder_2007) + bar * bin +y(:life) +theme(width =130)))
The rest is not about pictures at all. These rows are differences in how the two packages work, not a scoreboard: each one follows from what its own package set out to be.
ggplot2
gog
Where it runs
R
R, Python, Julia and JavaScript, one engine, the same SVG out of each
What a plot is
R closures, alive in the session
an engine-neutral JSON description you can store, diff, send, or generate
A sentence the package dislikes
a warning, and a plot with those rows removed (Removed 12 rows containing missing values)
no plot, and a message naming the sentence it would accept
What combines with what
documented per geom
one rule table, every pair decided, dumped from the engine into Combinations
Picking out a subset with the mouse
outside the package: the plot is a still image, and the answer is plotly or shiny
brush is a word in the sentence, so the printed page shows the selection too. It dims what you did not pick and removes no rows. Selection is the chapter
Spellings per plot
often two (geom_bar/geom_col, geom_histogram/stat_bin)
one
Saying “log”
scale_x_log10(), a scale function per transformation
x(gdp, scale = "log"), written on the binding
Non-positive values on a log axis
dropped, with a warning
refused, with the fix named
A mean under a log axis
the geometric mean
computed in the data’s own units
Centering a diverging ramp
scale_color_gradient2(midpoint = 0), which pins the midpoint and stretches each arm to the data’s own ends
limits = c(-k, k), the atom that already states a domain. The middle of a domain is a midpoint, so no parameter is added, and the ramp is linear on both arms.
F.3 What ggplot2 draws that gog does not
Sorted by why, because the sort is the useful part. A gap that was argued and lost is a different thing from a gap nobody has looked at.
F.3.1 Refused, and the reason is on record
These will not arrive. Each has a written ruling, and the ruling names the direction to go instead. Design laws is where the laws behind them live.
In ggplot2
The ruling
sec_axis(), dup_axis()
A second y-axis is two coordinate spaces wearing one panel. Facet on the measure, or layer if the units really are shared.
annotate() at page coordinates, annotation_custom(), annotation_raster()
A shape at pixel (120, 340) is a drawing command, and the description has to mean the same thing at another size, in a facet, or on a sphere. Annotation in data space needs no new word: layer a mark over a small table.
ggproto(), user-written geoms and stats
The guarantees rest on a closed set. An atom the rule table never heard of is a hole in the completeness test, in No Exceptions, and in the wire format at once. Compose instead.
coord_flip()
There is no flip atom, because orientation is read off which axis carries the categories. ggplot2 has since superseded it.
gog spells each of these one way. bar draws your numbers, bar * count counts them, bar * bin bins them.
geom_smooth(method = "lm")
A family of fits behind one buried argument is exactly the enumeration this grammar refuses. A least-squares line is a different statistic from a local smoother, so it earns its own plain name when it is built.
scale_x_log10(), scale_x_sqrt() as names
One parameter derives the family: base = 2, base = 10, base = exp(1).
coord_polar(theta = "y")
Which axis is the angle is read off the bindings, not chosen by an argument.
qplot(), and plotting bare vectors
A plot is a mapping from a table. Put the vectors in one.
geom_vline(), geom_hline() as separate marks
Three marks for one idea. They are one rule, which takes a column, so a table of thresholds draws all of them at once, and which is also the rug via style(reach = "edge").
plotmath, label_parsed(), label_bquote()
A TeX engine is one backend’s typesetting. Unicode does the real work: superscript powers on log ticks, Greek, sub- and superscripts in a label.
coord_cartesian(xlim = )
One name here covers two different operations. Narrowing the domain and running the statistics again is limits, which already counts the rows it left out aloud. Magnifying the finished picture changes how you look and not what the plot claims, so it earns no word: it is a button under every plot in the web edition, −, + and fit.
plotly-style interaction: click a point, and read what it is
The kernel reserved the word click for this and gave it up in August 2026, and the survey of what other tools do is what decided it. Plotly hands your application an event; Vega-Lite puts a predicate over named fields in the specification; D3 asks the author for a key function, and uses it for animation rather than for selection. gog already holds all three. Reading a row belongs to the page, so hover does it. A predicate over a column is brush. A key is what an animation needs.
a free-shape selection, drawn around some points with the mouse
The kernel reserved the word lasso for this and gave it up. Every other selection can be written down: brush(gdp, at = c(1200, 45000)) names a range, and the printed page then shows the plot that range asks for. A free shape can only be written as a long list of corners, which nobody writes in a sentence. The act was not refused, only the word: in the web edition you draw the shape with the mouse and the sentence still says brush.
The refusal a ggplot2 reader is most likely to walk into is the first one, so here it is, live. Two series in different units, the second asking for its own scale:
data(quarterly) +x(year) +y(sales) + line + point +y(sales, scale ="log")
Error:
! gog: this layer's `y(sales, …)` gives it its own scale, and the layers of one plot share one y axis — two scales on it would be two coordinate spaces in one panel, where every mark reads against an axis the expression never named. Set the scale once for the plot, before the marks: `y(<column>, scale = "log")`. A layer may name its own *column* for the shared axis — `y(sales)` on its own is fine — but not its own scale. If the two series are genuinely in different units, facet on the measure instead.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The message is the pattern: what the rule is, and what to write instead.
F.3.2 The host language’s job
These are computations, not pictures. gog draws what you hand it, and the fitted value, the theoretical quantile and the reshaped model belong upstream in R, Python, Julia or JavaScript, where you already have better tools for them.
In ggplot2
Where it goes
stat_ecdf()
compute the cumulative shares, then step
stat_qq(), stat_qq_line()
compute the theoretical quantiles, then point and line
stat_ellipse()
compute the ellipse, then path
stat_quantile()
fit the quantile regression, then line
stat_function(), geom_function()
evaluate the function onto a grid, then line
stat_unique()
de-duplicate the table
cut_interval(), cut_number(), cut_width()
cut the column, or use bin
mean_cl_boot(), mean_sdl(), median_hilow()
compute the pair, then ribbon * bounds(lower, upper)
fortify(), autoplot(), map_data()
reach the model or the shapefile in the host, hand over a table
a correlation matrix
cor() where the data lives, reshape it long, then zone + x(var1) + y(var2) + color(r)
The boundary is not as clean as that table makes it look, and pretending otherwise would be dishonest. gog does compute: bin cuts, density estimates, smooth fits a local regression, confidence builds an interval. The working line is that a histogram has no bars until something bins, so the computation is the picture, whereas a quantile regression is a separate answer that happens to be drawn as a line. Where exactly that line falls is one of the design’s open hard parts, not a settled rule.
F.3.3 Designed and owed
Named in the kernel or written down as wanted, with nothing decided against them. These are scheduling, not argument.
In ggplot2
Here
labs(subtitle =, caption =)
more of the label family, and cheap
theme(legend.position =)
it has its home in theme() and is waiting for a case that needs it
geom_smooth(se = TRUE)
the smoother’s band, its missing pair form, exactly as confidence is the mean’s
ggrepel
text * repel, designed, waiting on a layout stage
geom_sf(), geom_map(), coord_sf(), borders()
the map space draws, so a mark at a place on a projected earth is an ordinary sentence (Map). What is still owed is the polygon geometry underneath a choropleth: a filled region whose boundary comes from the data, with holes in it
geom_dotplot()
a cluster-aware mesh, a second cutpoint rule for binning
scale_x_binned(), equal-count bins
the other cutpoint rule, the same parameter
F.3.4 Not decided either way
The useful output of the sweep. Nothing has been argued about these, in either direction, and they are now written down where a future session will find them.
In ggplot2
What is missing here
scale_*_continuous(breaks =, labels =)
tick positions and tick text. tick_count asks for a number of ticks and the engine chooses them, so “tick at 0, 50 and 100” and relabeling a category are both unsayable.
scale_*_manual(values = c(setosa = "red"))
binding a color to a level by name, shown below
scale_y_reverse()
a reversed continuous axis. order(desc = TRUE) reverses a categorical one.
scale_x_sqrt(), coord_trans()
a power scale. The log10-style naming is refused; the capability never was.
guides(color = "none"), show.legend = FALSE
turning a key off. Every mapped channel earns one, always.
linewidth as an aesthetic
a column mapped to stroke width. size can be set on a stroke for the whole layer, not mapped per row.
labs(alt = ), get_alt_text()
alt text on the rendered plot
geom_label()
text in a box, as against bare text
geom_curve()
a curved connector between two points; path joins them straight
after_stat(), after_scale(), stage(), and geom_count() with them
sending a transform’s output to a channel rather than to a position. point * count + size(count) is refused, because the count a transform invented is not a column the sentence can name, so sizing a point by how many rows landed on it has no spelling. One question, and it is the widest one on this list.
geom_contour() over a supplied z
iso-lines of a field you computed. path * density traces a field gog estimated.
geom_text(angle = ), and ggplot2’s polar text generally
a rotated label. text places a name at a point in every space, upright, so a sunburst cannot run its labels along their arcs and small wedges collide. general to polar, not owned by that plot.
theme(axis.text = element_blank()) and friends
saying a plot has no readable axes as a request. theme(grid = ) removes gridlines and tick_count = 0 is refused by design, so the silence has to be earned from what an axis carries rather than asked for: a pie is recognized from its bindings, a packing has no axes at all, and a partition’s radial axis carries a ring index the transform invented and so draws no ticks, no rings and no name. Three recognitions doing a general job, and the residue is the case where a reader wants a real quantity’s axis silent: the measure round a sunburst, say, which today takes x_label("") and keeps its numbers.
The color one is worth seeing, because it is not a missing knob so much as a missing kind of statement. Three species, three colors:
data(iris_flowers) + point +x(petal_length) +y(sepal_length) +color(species) +palette(c("tomato", "steelblue", "seagreen")) +title("Rows in the order the table has them")
(data(iris_flowers) + point + x(col.petal_length) + y(col.sepal_length) + color(col.species) + palette(["tomato", "steelblue", "seagreen"]) + title("Rows in the order the table has them"))
data(iris_flowers) + point +x(:petal_length) +y(:sepal_length) +color(:species) +palette(["tomato", "steelblue", "seagreen"]) +title("Rows in the order the table has them")
plot(data(iris_flowers), point,x(col.petal_length),y(col.sepal_length),color(col.species),palette(["tomato","steelblue","seagreen"]),title("Rows in the order the table has them"))
2465678Rows in the order the table has themSepal LengthPetal LengthSpeciessetosaversicolorvirginica
Now the same sentence and the same palette, over the same rows sorted the other way. Watch the legend:
by_species <- iris_flowers[base::order(iris_flowers$species, decreasing =TRUE), ]data(by_species) + point +x(petal_length) +y(sepal_length) +color(species) +palette(c("tomato", "steelblue", "seagreen")) +title("The same three colors, the other way up")
2465678The same three colors, the other way upSepal LengthPetal LengthSpeciesvirginicaversicolorsetosa
Nothing is wrong here: a palette hands out its colors in the categories’ order, that order is the axis’s, and for a text column the axis order is the order the values first appear (a factor’s levels are honored when you supply one). It is consistent and it is documented. What there is no way to say is setosa is tomato, so a category cannot keep its color across two plots whose category order differs, which is what a report of small multiples usually wants.
F.4 If your plot is not here
The sweep was of ggplot2’s reference index, so a plot that lives in one of its extensions may be missing from both columns. Two families are absent from gog with nothing written down about them at all: networks (nodes and edges, ggraph’s territory) and any shape with a hole in it, which is the same geometry a choropleth and a ring-shaped filled contour both need.
For a plot you can name, Chart names is the dictionary and says which ones are drawn. For a question rather than a name, start at the cookbook.
Wickham, H., Chang, W., Henry, L., Pedersen, T. L., Takahashi, K., Wilke, C., Woo, K., Yutani, H., Dunnington, D., & Brand, T. van den. (2026). ggplot2: Create elegant data visualisations using the grammar of graphics. R package version 4.0.3, published 22 April 2026. https://CRAN.R-project.org/package=ggplot2