Appendix E — The ggplot2 phrasebook

ggplot2 proved that a grammar of graphics could be a working tool and not only a theory. Most readers of this book already know it. gog follows the same grammar and decides the details differently. This appendix answers two questions: what to write in gog, and what behaves differently. The ggplot2 calls on this page are those of ggplot2 4.0.3 (Wickham et al., 2026).

The translations assume gog can draw the plot you want. To find out whether it can, read Coverage. That appendix compares ggplot2’s whole reference index with gog’s kernel, in both directions: what gog draws that ggplot2 does not, and what ggplot2 draws that gog does not.

E.1 Side by side

This first table compares the two systems as a whole, rather than term by term.

ggplot2 gog
Languages R R, Python, Julia and JavaScript, all four calling one engine
Naming geom_point, aes, alpha point, channels by plain name, opacity
Where x/y are written aes() at the plot or the layer, and a layer may override the plot’s aes() before any mark, for every layer; after a mark, for that mark. A layer with no column of its own reads the first column written for that axis.
The histogram geom_histogram() or stat_bin(geom = "bar") bar * bin
Log axis scale_x_log10() x(gdp, scale = "log"), written on the binding
3-D external packages one more position: z
Animation gganimate, a separate package play is a channel of the kernel: + play(year)

E.2 Translations

This table is term by term. Find the ggplot2 call on the left; the gog spelling that does the same job is beside it on the right.

You write in ggplot2 You write in gog
ggplot(df, aes(x = a, y = b)) data(df) + x(a) + y(b) + …
geom_point() point
geom_line() line
geom_step() step
geom_area() area
geom_col() bar, one bar per row; with several rows per category, bar * sum gives their total
geom_bar(), which counts the rows for you bar * count, because bar alone counts nothing
geom_histogram(bins = 30) bar * bin(30). The thirty bins are placed differently: gog’s run exactly from the smallest value to the largest, while geom_histogram() makes each bin a little wider and centers the first and last on the smallest and largest values (Where the bin edges fall)
geom_histogram(binwidth = 5) bar * bin(width = 5). The width is the same, but the edges differ: gog puts an edge at zero, so the bins run 35 to 40, while geom_histogram() centers a bin on zero, so they run 37.5 to 42.5 (Choosing a width)
geom_histogram(aes(y = after_stat(density))) bar * bin * proportion. Each bar is its bin’s share of the rows, and ggplot2’s density is that share divided by the bin width. On the same bins, the share histogram and ggplot2’s density histogram have the same shape, and only the numbers on the axis differ. bar * bin + y(density) is refused: there y() could change only the label, so counts would be labeled as a density (The name on a measure axis)
position = "stack", "dodge" or "fill" inside a geom_*() a collision modifier after the statistic: bar * count * stack, bar * count * dodge or bar * count * stack(share = TRUE), and the same after bin for a histogram. With share = TRUE, each segment is divided by its bar’s total, so every bar reaches 1. With no position written, ggplot2 stacks the groups and gog overlays them, so write stack to get ggplot2’s default (The 100% stacked bar)
geom_freqpoly() line * bin. The line starts at the first bin’s count and ends at the last one’s. geom_freqpoly() adds an empty bin at each end, so its line starts and ends at zero. It also uses 30 bins by default, and line * bin(30) gives the same number
geom_density() line * density
geom_histogram(aes(y = after_stat(density))) + geom_density(), a density curve over a histogram two plots, one above the other: histogram / curve, where histogram is bar * bin + x(v) and curve is line * density + x(v) over the same table. They share the x axis, and each keeps its own y axis. The bars stay counts, because gog draws no density histogram. In one panel the pair is refused, because a density estimate is a different kind of number from a count or a share (What is its smooth shape?)
geom_smooth() ribbon * smooth_band + line * smooth: the band and the curve are two layers. smooth fits a straight line locally and geom_smooth() fits a parabola, so the two curves can differ where the relationship is curved (LOESS trend line)
geom_smooth(se = FALSE) line * smooth: the curve of the row above, without its band
geom_smooth(method = "lm") ribbon * straight_band + line * straight: the least-squares line and its confidence band, the same line and band as lm() and predict(interval = "confidence") (The least-squares line). Without the band, line * straight
stat_summary(fun = mean, geom = "bar") bar * mean
geom_boxplot() box, which computes its own summary, as geom_boxplot() does
geom_violin() ribbon * density, with the categories on x. The default shapes differ. gog’s violins run past the data and narrow to a point, weight each area by row count, and share one bandwidth. geom_violin() cuts each end flat, draws equal areas, and gives each violin its own bandwidth. density(compare = "shape") gives equal areas (Compared with ggplot2’s violin)
geom_jitter() point * jitter, on a categorical axis. ggplot2 also moves each point up and down by default, by up to 0.4 of the smallest gap between values, so on rounded data a point is drawn off its value. gog moves it along the category only, as height = 0 does in ggplot2, and computes the spread from the data instead of at random. On two continuous axes gog refuses it and suggests style(opacity = ) instead.
geom_errorbar(), geom_linerange() interval * bounds(lower, upper) for ends you already have, or interval * confidence to compute them; style(caps = FALSE) removes the caps, which gives a linerange
geom_ribbon() ribbon * bounds(lower, upper)
geom_tile() zone + x(a) + y(b) + color(v), with a and b categorical: each category gets its own slot
geom_bin_2d(), geom_bin2d() zone * bin + x(a) + y(b): bin cuts both axes into cells, and the count in each cell becomes its color, with no color() written (Cells cut from the data: the heatmap)
geom_hex() zone * bin(tiling = "hex") + x(a) + y(b): the same count, in hexagonal cells (Hexagons: the other mesh)
stat_summary_2d(aes(z = v), fun = mean) zone * bin * mean + x(a) + y(b) + color(v): the column to summarize goes on color, because a zone shows its value as color. sum, median, max and min work the same way (Cells cut, then summarized: the summary heatmap)
geom_hline(), geom_vline() data(t) + rule + y(v) for horizontal lines, and x(v) for vertical ones, where the table t holds the positions
geom_text() text + label(g)
geom_text(angle = 90) text + label(g) + style(angle = 90): one angle for every label in the layer, in degrees counterclockwise (Turning a label)
geom_path() path, which joins the rows in the table’s order
aes(color = g) color(g)
aes(fill = g) color(g): one channel colors a bar’s fill and a line’s stroke, and there is no fill
aes(size = v) size(v)
aes(shape = g) shape(g)
aes(alpha = v) opacity(v)
aes(linetype = g) pattern(g): a dash on a line, a hatch on a filled region
aes(group = g) group(g)
scale_x_log10() x(v, scale = "log")
xlim(58, 68), scale_x_continuous(limits = c(58, 68)) x(v, limits = c(58, 68)). As in ggplot2, the rows outside the limits are removed, and both report how many. When no row is left, gog refuses, where ggplot2 draws an empty panel (limits: stating the range yourself)
scale_color_viridis_c() color(v) + palette("viridis")
scale_color_manual(values = c(a = "red", b = "blue")) color(g) + palette(c(a = "red", b = "blue")): each color is written beside its category’s name, so sorting the rows cannot change it. gog refuses a name the column does not hold and a category left without a name (Colors by name)
guides(color = "none"), show.legend = FALSE color(g, legend = FALSE), written on the channel whose legend you leave out
guides(x = "none") with labs(x = NULL), or theme(axis.text.x = element_blank(), axis.ticks.x = element_blank(), axis.title.x = element_blank()) x(v, axis = FALSE), written on the position whose axis you leave out. The numbers, the tick marks and the name are left out together, and the scale is unchanged (axis: leaving an axis out)
theme(legend.position = "bottom") theme(legend = "bottom"). The other sides are "top" and "left", and "right" is the default. Above or below the panel, a legend’s entries are placed side by side (Legend placement). ggplot2’s legend.position = "none" removes every legend, and in gog you write legend = FALSE on each channel, as in color(g, legend = FALSE)
facet_grid(r ~ c) | facet(c) / facet(r)
facet_wrap(~ g, ncol = 4) | facet(g, wrap = 4) puts four panels in each row, and / facet(g, wrap = 4) puts four in each column. The operator says the direction, so one number replaces both nrow and ncol.
facet_*(scales = "free_y") y(v, free = TRUE), written on the axis you are freeing. You do not choose the axis a second time, because the binding already says which one.
patchwork’s p1 | p2, p1 / p2 plot_a | plot_b, plot_a / plot_b: the same two operators. What is written on the right decides what happens: a plot is placed beside or below, and a facet() splits the plot into panels.
ggrepel’s geom_text_repel() text * repel, one of the four collision modifiers, with dodge, stack and jitter
ggExtra’s ggMarginal() top / (main | right), three ordinary plots (Composition)
patchwork’s plot_spacer(), plot_layout(widths = ) theme(width = ) on a plot placed beside others, or theme(height = ) on one stacked above or below, in pixels; the plots that ask for nothing share what is left. In the marginal layout above, the empty corner needs no spacer.
coord_flip() swap the x and y bindings: orientation is taken from whichever axis carries the categories, so there is nothing to switch
coord_polar() polar()
labs(title = , subtitle = , caption = , x = , y = ) title(), subtitle(), caption(), x_label(), y_label(), z_label()
color = "red", alpha = 0.5 inside a geom_*() style(color = "red", opacity = 0.5)
theme(…) theme(), for what belongs to the whole plot rather than one layer, such as its size, gridlines and background; style() belongs to one layer
ggsave("plot.svg") save_svg(p, "plot.svg"). gog draws every plot as SVG, so the file name must end in .svg. Functions around the grammar shows the call, which is the same in every language.

E.3 Six differences to expect

The words translate, but six things work differently in gog.

  • + is still +, and on purpose. ggplot2’s author has said the pipe would have been the better interface (Wickham, 2018). gog chose + for two reasons. A pipe cannot be given a gog meaning: Python has no pipe operator, and the pipes of R and Julia already mean something else. A pipe also makes every atom a function, and bar * bin has no meaning once both sides are functions. Operators covers this in full, including which atoms give the same plot in any order.
  • A count is a word you write. bar draws your numbers unchanged. geom_bar() counts rows on its own, and gog counts only when you write a counting transform, such as count or bin. The one mark that computes by itself is box, because a box plot is a summary.
  • There is one spelling. There are no geom/stat pairs, so you never choose between two functions. If two different atoms in gog do the same job, that is a bug: report it on the project’s repository.
  • The scale belongs to the binding. Options such as scale, limits, free, legend and axis are arguments of the binding, as in x(gdp, scale = "log") or color(g, legend = FALSE), and there is no scale_*() function. The colors of a color scale come from palette(), which belongs to the whole plot.
  • Setting is not mapping. A constant maps no column, so it gets no legend. Write it in style(). In ggplot2 aes(color = "red") is legal and draws a legend with one entry called “red”. Here a channel takes a column, not a value, so gog refuses color("red") and names style(color = "red") as the fix.
  • Refusals name the fix. A sentence gog cannot draw is refused, and the message says what to change. Where ggplot2 warns about an unknown argument and draws anyway, gog stops and draws nothing. This book shows refusals on purpose: it runs the refused sentence and prints the message you would see.

If a call you use is in neither table, Coverage says whether gog draws that plot at all.