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).gogchose+for two reasons. A pipe cannot be given agogmeaning: Python has no pipe operator, and the pipes of R and Julia already mean something else. A pipe also makes every atom a function, andbar * binhas 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.
bardraws your numbers unchanged.geom_bar()counts rows on its own, andgogcounts only when you write a counting transform, such ascountorbin. The one mark that computes by itself isbox, 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
gogdo 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,legendandaxisare arguments of the binding, as inx(gdp, scale = "log")orcolor(g, legend = FALSE), and there is noscale_*()function. The colors of a color scale come frompalette(), which belongs to the whole plot. - Setting is not mapping. A constant maps no column, so it gets no legend. Write it in
style(). Inggplot2aes(color = "red")is legal and draws a legend with one entry called “red”. Here a channel takes a column, not a value, sogogrefusescolor("red")and namesstyle(color = "red")as the fix. - Refusals name the fix. A sentence
gogcannot draw is refused, and the message says what to change. Whereggplot2warns about an unknown argument and draws anyway,gogstops 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.