Appendix F — Coverage, both directions

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. The result is two lists of very different lengths, and a table of differences between them.

Each table below has a ggplot2 column, and it 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). ggplot2 is deliberately not a dependency of this book, so building the book never runs it. The plots on this page, and the one refusal shown further down, are different: the engine draws them each time the book is built.

The first list is short: what gog does that ggplot2 does not. The second is long: what ggplot2 draws that gog does not. Each gap on it is one of four kinds, and three of the four were decided on purpose. A later section turns to ggplot2’s extension packages, which the reading of the index did not include, and draws five of their plots from tables the host language computes.

F.1 What gog does that ggplot2 does not

The first section below shows a picture ggplot2 cannot draw. The other two show differences in vocabulary, not in what can be drawn.

F.1.1 Anything in the cube

Binding z gives a plot a third position, and surface draws a height over a plane:

maunga_whau: first 5 of 1364 rows
east north elevation
20 20 100
20 40 102
20 60 104
20 80 105
20 100 107
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"))
600 400 200 0 800 600 400 200 0 180 160 140 120 100 East North Elevation Maunga Whau, one row per crossing Elevation 193 143.5 94

“Given Maunga Whau: a surface, x is east, y is north, z is elevation, color by elevation.”

Seven marks draw in the cube: point, text, path, bar, interval, box and surface. space(turn = , tilt = ) sets the viewing angle. facet and play split a cube’s rows as they split any other plot’s: | facet(g) gives one projected box per panel, + play(t) runs the sequence inside a single box. ggplot2 has no depth position, and no coordinate system that would read one. Its own book says it does not support true 3-D surfaces (Wickham, 2016). In R these plots come from plotly, rgl or lattice, each with a grammar of its own. Here z is one more channel in the same sentence, and Space is the chapter.

F.1.2 Texture, a channel of its own

pattern maps a categorical column to a texture: a hatch on the filled regions and a dash on the strokes. Put it on the same column as color and each series is marked twice, by hue and by hatch, in one merged legend:

iris_flowers: first 5 of 150 rows
sepal_length sepal_width petal_length species
5.1 3.5 1.4 setosa
4.9 3.0 1.4 setosa
4.7 3.2 1.3 setosa
4.6 3.1 1.5 setosa
5.0 3.6 1.4 setosa
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"))
setosa versicolor virginica 0 2 4 Hue and hatch, one legend Petal Length Species Species setosa versicolor virginica

“Given the iris flowers: bars derived by mean, x is species, y is petal length, color by species, pattern by species.”

The plot above stays legible in grayscale and for readers with color blindness, because hue and hatch both show the species. ggplot2 can fill a bar with a hatch too. There, you build each hatch yourself with R’s grid package, and it replaces the fill color. In gog, pattern is a channel of its own, separate from color. So hue and hatch can come from two different columns. On a line, the same word draws dashes, where ggplot2 needs a second word, linetype.

F.1.3 The same plot, without a word for it

This is also a picture ggplot2 can draw, and gog draws it without adding a word. geom_violin() is a mark of its own there, with stat_ydensity() beside it. Here it is ribbon * density. A ribbon fills the region between two boundaries, and density estimates one distribution for each category.

gapminder_2007: first 5 of 142 rows
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
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"))
Asia Europe Africa Americas Oceania 40 60 80 ribbon * density: no new word required Life expectancy Continent

“Given gapminder 2007: ribbons derived by density, x is continent, y is life.”

An area fills from its curve to its category’s center line, not to a second boundary. So area * density + x(continent) + y(life) draws half of each violin: the half violin. That is the purpose of orthogonality: gog draws the plot without adding a word to its vocabulary.

The donut is the same case arriving from a parameter rather than a geom. ggplot2 spells it coord_radial(inner.radius = ), a number given to the space; here the hole is not a property of the circle at all. It is a stretch of the radial axis with nothing drawn on it. So a tree one level deep is a donut when its axis starts below the first ring. y(depth) binds the ring number, a column that partition writes (Other columns a transform writes):

spending: first 5 of 12 rows
group item detail amount
Housing Rent NA 980
Housing Utilities Energy 140
Housing Utilities Water 35
Housing Repairs NA 120
Food Groceries NA 420
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"))
0 1K 2K A donut is a sunburst of a one-level tree Amount Group Housing Food Transport Leisure

“Given the spending table: zones derived by partition through group, x is amount, y is depth from 0 to 2, color by group, in polar.”

No atom in that sentence is about donuts. limits on depth also leaves the middle of the three-level sunburst empty, and leaves an empty band under the flat icicle, which has no polar(). So the hole comes from the scale, not from polar(). Only the plain pie cannot have a hole this way: nothing is bound to its radius, so there is no domain to widen.

The marginal plot is the same again, this time between whole plots. ggExtra adds ggMarginal() to ggplot2 and patchwork adds a layout algebra beside it; here an arrangement is two plots joined by | or /, and the marginals follow from 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)))
below(plot(data(gapminder_2007), layer(bar, bin), x(col.gdp),
  theme({ height: 130 })),
  beside(plot(data(gapminder_2007), point, x(col.gdp), y(col.life)),
  plot(data(gapminder_2007), layer(bar, bin), y(col.life),
  theme({ width: 130 }))))
0 20 40 60 Count 0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp 0 10 20 30 Count

“Given gapminder 2007: bars derived by bin, x is gdp; above points, x is gdp, y is life, beside bars derived by bin, y is life.”

Those are all the differences in this direction. What the extensions add shows more plots that ggplot2 draws only with an extension package. Everything else gog draws, ggplot2 draws too, usually with more settings.

F.2 What is different rather than missing

The rest is not about pictures at all. These rows are differences in how the two packages work: each one follows from what its own package was built for.

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 specification you can store, compare, send or generate
A sentence the package cannot draw 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 pointer 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 selection appears in the PDF as well as on screen. It dims what you did not pick and removes no rows. Selection is the chapter
Moving overlapping labels apart ggrepel, a separate package, with a geom of its own to use instead text * repel, a collision modifier beside dodge, stack and jitter, so it composes with the text mark that was already there. It draws a thin line back to the point when a label has moved far, and where no placement fits, it still draws every label and reports how many overlap
A transform’s column on a second channel after_stat() inside aes(), or geom_count(), a geom for counted points the column the transform writes is named like a column of your table: point * count + size(count) sizes each point by its count, and bar * count + text * count + label(count) prints the count on each bar (Transforms). color(depth) gives each ring of a sunburst its own color (Polar)
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 fixes the midpoint and stretches each side 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 each side of the middle.

F.3 What ggplot2 draws that gog does not

The gaps below are sorted into four tables by reason, because the reason tells you whether a gap is final. A gap with a written ruling behind it is a different thing from a gap nobody has ruled on.

F.3.1 Refused, with the ruling on record

gog will not draw these. 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 drawn in one panel. Facet on the measure, or layer if the units really are shared.
geom_histogram(aes(y = after_stat(density))) with geom_density() over it gog draws no density histogram: its bars are counts of rows or shares of the rows, and a curve’s height is a density. One axis holds one kind of number, so the bars and the curve go in two plots, one above the other, which share the x axis (What is its smooth shape?).
annotate() at page coordinates, annotation_custom(), annotation_raster() A shape at pixel (120, 340) is a drawing command, and the specification 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 Every promise on the Combinations grid depends on the vocabulary being fixed. An atom missing from the rule table has no row on that grid, breaks No Exceptions, and cannot be written into the JSON. Compose instead.
coord_flip() Nothing to write: orientation is read from which axis carries the categories, so swapping the two bindings turns the plot sideways. ggplot2 has since superseded it.
geom_bar() beside geom_col(); geom_histogram() beside stat_bin(geom = "bar") 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 argument is exactly the enumeration this grammar refuses. A least-squares line is a different statistic from a local smoother, so it has its own plain name: line * straight, with its band ribbon * straight_band (The least-squares line).
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 from 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 belongs to one output format, not to the grammar. Unicode covers what readers need: superscript powers on log ticks, Greek, sub- and superscripts in a label.
coord_cartesian(xlim = ) This ggplot2 name covers two different operations. Narrowing the domain and running the statistics again is limits, which already reports how many rows it removed. Magnifying the finished picture changes how you look at it and not what the plot claims. So magnifying needs no word. Under every plot in the web edition, − and + magnify the picture, and the frame button returns to the whole of it.
plotly-style interaction: click a point, and read what it is This needs no word in the kernel, 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 row identity, and uses it for animation rather than for selection. gog answers each of the three elsewhere. Reading a row belongs to the page a plot is shown on, not to the sentence. On a plot that draws each mark at its own row’s values, resting the pointer on a mark shows the values of its row, with or without brush (Pointing at a mark). On a brushed plot, a click keeps them on the plot as a stamp. A predicate over a column is brush. A row identity is what an animation needs, and the grammar has no word for it (Animation).
a free-shape selection, drawn around some points with the pointer There is no lasso word in the kernel. Every other selection can be written down: brush(gdp, at = c(1200, 45000)) names a range, and the PDF 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 pointer and the sentence still says brush.

The refusal a ggplot2 reader is most likely to meet is the first one, so here it is, live. Two series in different units, the second asking for its own scale:

quarterly: first 5 of 19 rows
year sales
2005 95
2006 104
2007 118
2008 112
2009 88
data(quarterly) + x(year) + y(sales) + line +
  point + y(sales, scale = "log")
(data(quarterly) + x(col.year) + y(col.sales) + line +
  point + y(col.sales, scale = "log"))
data(quarterly) + x(:year) + y(:sales) + line + point +
  y(:sales, scale = "log")
plot(data(quarterly), x(col.year), y(col.sales), line, point,
  y(col.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.

Every refusal in gog has that shape: it says what the rule is, and what to write instead.

F.3.2 The host language’s job

These are computations, not pictures. gog draws the table you give it. Compute the fitted value, the theoretical quantile or a model turned into a table before the plot, in R, Python, Julia or JavaScript. Those languages already have better tools for them.

In ggplot2 Where it goes
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() bootstrap a lower and an upper bound, then ribbon * bounds(lower, upper). Two helpers documented beside it are gog atoms instead: mean_sdl() is deviation, and median_hilow() is range(0.025, 0.975) with point * median
fortify(), autoplot(), map_data() get the model or the map boundaries in the host, then give gog a table
a correlation matrix cor() where the data lives, reshape it long, then zone + x(var1) + y(var2) + color(r)

The table makes the split between gog and the host language look simpler than it is. gog does compute. bin cuts, density estimates, smooth fits a local regression, and straight fits a least-squares line. confidence builds an interval, deviation measures spread, quantile cuts a distribution, and cumulative adds up a running total, so step * count * cumulative(share = TRUE) draws the curve stat_ecdf() draws. The test is whether the picture can exist without the computation. A histogram has no bars until something bins. A quantile regression is a separate answer that is then drawn as a line. How the test applies to a new case is not settled.

F.3.3 Missing, with the direction already settled

gog does not draw these. Each one belongs to a family the grammar already has, and nothing about them is disputed.

In ggplot2 Here
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, and zone with group(country) fills each region from a boundary the data carries, holes included (Map). gog reads no spatial file. A boundary arrives as an ordinary table of points, which is the shape map_data() returns
geom_dotplot() gog would need a mesh that follows the data’s clusters, and a second rule for choosing bin edges
scale_x_binned(), equal-count bins the other cutpoint rule, the same parameter

F.3.4 Not decided either way

None of these has a ruling in either direction. Many groups gives reasons against custom tick text, but that is an argument, not a ruling.

In ggplot2 What is missing here
scale_*_continuous(breaks =, labels =) tick positions and tick text of your own. tick_count asks for a number of ticks and the engine chooses them, so “tick at 0, 50 and 100” and relabeling a category cannot be written. You can choose how the numbers are written: percent, short and decimals cover scales::percent, the K, M, B and T labels and a fixed number of places (Scales).
scale_y_reverse() a reversed continuous axis. x(continent) + order(continent, desc = TRUE) sorts a categorical axis from Z to A, or a declared order from last to first.
scale_x_sqrt(), coord_trans() a power scale. The log10-style naming is refused; the capability never was.
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
after_scale(), stage() one channel’s value computed from another’s after the scale has turned it into a color, such as a border drawn darker than the bar’s own color
geom_label() text in a box, rather than bare text
geom_curve() a curved connector between two points; path joins them straight
geom_contour() over a supplied z iso-lines of a field you computed. path * density traces a field gog estimated.
aes(angle = ) on geom_text(), and ggplot2’s polar text generally a separate angle for each label, read from a column. style(angle = ) turns every label in a layer by the same angle. A sunburst is the one plot where gog turns labels by itself: each name too wide for its sector turns along the sector’s radius or its arc, and one that still does not fit is drawn smaller. Other labels in polar, such as names around a rose, take one angle for the whole layer.

F.4 What the extensions add

Every table above compares gog with ggplot2 itself. Many plots drawn with ggplot2 come instead from its extension packages: separate packages that add new plots to ggplot2. gog already draws several of these plots: the node-link diagram in Network, the alluvial diagram in Flow, the beeswarm, ridgelines, a halo behind a label, and animation with play. Other extension plots need a computation first, then an ordinary drawing. The host language computes the tables, and gog draws them with words it already has. Five of these plots follow.

F.4.1 Stars from a significance test

Packages such as ggsignif and ggpubr mark a plot with the result of a statistical test. The test is a computation, so it belongs to the host language, which builds two small tables from its result. One holds the corners of each bracket: a line that joins the two groups compared, with a short leg down at each end. The other holds the stars that stand for each p-value. Here a t-test compares the petal length of versicolor, and then of virginica, with that of setosa, the group the other two are compared with:

petal_brackets: first 5 of 8 rows
pair species petal_length
versicolor setosa 5.5
versicolor setosa 5.7
versicolor versicolor 5.7
versicolor versicolor 5.5
virginica setosa 7.2
petal_stars: all 2 rows
species petal_length stars
versicolor 5.7 ***
virginica 7.4 ***
data(iris_flowers) + box + x(species) + y(petal_length) +
  data(petal_brackets) + path + group(pair) +
  data(petal_stars) + text + label(stars) + style(nudge = "up") +
  y_label("Petal length") + title("Each species against setosa")
(data(iris_flowers) + box + x(col.species) + y(col.petal_length) +
  data(petal_brackets) + path + group(col.pair) +
  data(petal_stars) + text + label(col.stars) + style(nudge = "up") +
  y_label("Petal length") + title("Each species against setosa"))
data(iris_flowers) + box + x(:species) + y(:petal_length) +
  data(petal_brackets) + path + group(:pair) + data(petal_stars) + text +
  label(:stars) + style(nudge = "up") + y_label("Petal length") +
  title("Each species against setosa")
plot(data(iris_flowers), box, x(col.species), y(col.petal_length),
  data(petal_brackets), path, group(col.pair), data(petal_stars), text,
  label(col.stars), style({ nudge: "up" }), y_label("Petal length"),
  title("Each species against setosa"))
setosa versicolor virginica 2 4 6 Each species against setosa Petal length Species

“Given the iris flowers: boxes, x is species, y is petal length, and also paths from the petal brackets, grouped by pair, and also text from the petal stars, label by stars.”

Each bracket is four rows, which path joins in order: up from setosa, across, and down to the species compared. group(pair) keeps the two brackets apart. The stars are above the species compared, and three stars mean a p-value below 0.001.

F.4.2 A survival curve with the numbers at risk

A Kaplan–Meier curve estimates, for each day, the share of patients still alive: the survival probability that Step draws. Packages such as survminer add a table beneath the curve. It gives the number at risk every 200 days: the patients still alive and still followed by the study. The host language fits the curve and counts those at risk. These two tables were computed with R’s survival package, from its data on 228 patients with advanced lung cancer:

lung_survival: first 5 of 208 rows
days surviving sex
0 1.0000 Female
5 0.9889 Female
60 0.9778 Female
61 0.9667 Female
62 0.9556 Female
lung_at_risk: first 5 of 12 rows
days sex at_risk
0 Female 90
200 Female 66
400 Female 26
600 Female 11
800 Female 2

gog draws the curve as a step and the counts as text, in a second plot below the first. The two plots share the days axis, and both ask for 8 ticks, so each count has a tick above it:

(data(lung_survival) + step + x(days, tick_count = 8) + y(surviving) +
   color(sex) + palette(c(Female = "#e15759", Male = "#4e79a7")) +
   y_label("Share surviving") +
   title("Survival of 228 patients with advanced lung cancer, by sex")) /
  (data(lung_at_risk) + text + x(days, tick_count = 8) + y(sex) +
     label(at_risk) + color(sex, legend = FALSE) +
     palette(c(Female = "#e15759", Male = "#4e79a7")) +
     theme(height = 110) + y_label("At risk"))
((data(lung_survival) + step + x(col.days, tick_count = 8) + y(col.surviving) +
   color(col.sex) + palette({"Female": "#e15759", "Male": "#4e79a7"}) +
   y_label("Share surviving") +
   title("Survival of 228 patients with advanced lung cancer, by sex")) /
  (data(lung_at_risk) + text + x(col.days, tick_count = 8) + y(col.sex) +
     label(col.at_risk) + color(col.sex, legend = False) +
     palette({"Female": "#e15759", "Male": "#4e79a7"}) +
     theme(height = 110) + y_label("At risk")))
(data(lung_survival) + step + x(:days, tick_count = 8) + y(:surviving) +
  color(:sex) + palette(["Female" => "#e15759", "Male" => "#4e79a7"]) +
  y_label("Share surviving") +
  title("Survival of 228 patients with advanced lung cancer, by sex")) /
  (data(lung_at_risk) + text + x(:days, tick_count = 8) + y(:sex) +
  label(:at_risk) + color(:sex, legend = false) +
  palette(["Female" => "#e15759", "Male" => "#4e79a7"]) +
  theme(height = 110) + y_label("At risk"))
below(plot(data(lung_survival), step, x(col.days, { tick_count: 8 }),
  y(col.surviving), color(col.sex),
  palette({ Female: "#e15759", Male: "#4e79a7" }),
  y_label("Share surviving"),
  title("Survival of 228 patients with advanced lung cancer, by sex")),
  plot(data(lung_at_risk), text, x(col.days, { tick_count: 8 }),
  y(col.sex), label(col.at_risk), color(col.sex, { legend: false }),
  palette({ Female: "#e15759", Male: "#4e79a7" }), theme({ height: 110 }),
  y_label("At risk")))
0.0 0.2 0.4 0.6 0.8 1.0 Survival of 228 patients with advanced lung cancer, by sex Share surviving Sex Female Male 90 66 26 11 2 0 138 78 31 13 6 2 0 200 400 600 800 1000 Male Female At risk Days

“Given the lung survival table: step outlines, x is days with 8 ticks, y is surviving, color by sex; above text from the lung at risk table, x is days with 8 ticks, y is sex, label by at risk, color by sex, without a legend.”

Each plot chooses its own colors, so the palette is written in both plots. It ties each sex to one color, whatever the order of the rows, and legend = FALSE leaves the second legend out. Women lived longer in this study: at 400 days the curve shows about half of them alive, against under a third of the men. The counts fall faster than the curves, because 63 patients left the study alive, and a patient counts as at risk only while the study still follows them.

F.4.3 Names at the ends of the lines

Packages such as directlabels write each line’s name at its end, so a reader does not have to match colors against a legend. The lines come from gapminder_asia. The names come from a second table, with one row for each line, at its last year, 2007:

gapminder_asia: first 5 of 60 rows
country continent year life population gdp
China Asia 1952 44.00000 556263527 400.4486
China Asia 1957 50.54896 637408000 575.9870
China Asia 1962 44.50136 665770000 487.6740
China Asia 1967 58.38112 754550000 612.7057
China Asia 1972 63.11888 862030000 676.9001
asia_2007: all 5 rows
country continent year life population gdp
China Asia 2007 72.961 1318683096 4959.115
India Asia 2007 64.698 1110396331 2452.210
Indonesia Asia 2007 70.650 223547000 3540.652
Japan Asia 2007 82.603 127467972 31656.068
Korea, Rep. Asia 2007 78.623 49044790 23348.140
data(gapminder_asia) + line + x(year, limits = c(1952, 2016)) + y(life) +
  color(country, legend = FALSE) +
  data(asia_2007) + text + label(country) + color(country) +
  style(nudge = "right") +
  y_label("Life expectancy") + title("Each line named at its end")
(data(gapminder_asia) + line + x(col.year, limits = [1952, 2016]) + y(col.life) +
  color(col.country, legend = False) +
  data(asia_2007) + text + label(col.country) + color(col.country) +
  style(nudge = "right") +
  y_label("Life expectancy") + title("Each line named at its end"))
data(gapminder_asia) + line + x(:year, limits = [1952, 2016]) + y(:life) +
  color(:country, legend = false) + data(asia_2007) + text +
  label(:country) + color(:country) + style(nudge = "right") +
  y_label("Life expectancy") + title("Each line named at its end")
plot(data(gapminder_asia), line, x(col.year, { limits: [1952, 2016] }),
  y(col.life), color(col.country, { legend: false }), data(asia_2007),
  text, label(col.country), color(col.country), style({ nudge: "right" }),
  y_label("Life expectancy"), title("Each line named at its end"))
China India Indonesia Japan Korea, Rep. 1960 1980 2000 40 50 60 70 80 Each line named at its end Life expectancy Year

“Given gapminder Asia: lines, x is year from 1952 to 2016, y is life, colored by country, without a legend, and also text from the Asia 2007 table, label by country, colored by country.”

The names replace the legend, so legend = FALSE leaves the legend out. color(country) is written again after text, because a channel written after one mark reaches that mark alone (Encoding scope). The limits on year extend the axis to 2016, past the last year of data, so the names fit inside the panel.

F.4.4 How sets overlap

When a person can give several answers, the question is how many gave each combination. An UpSet plot shows this. Packages such as UpSetR and ComplexUpset draw one bar for each combination of sets. Under the bars, a grid of dots shows which sets belong to each combination. Suppose 85 people were asked where they get their news: from a printed newspaper, the radio or the web. The host language counts each combination of answers and writes three tables. news_counts holds the count for each combination, largest first, which is the order of the bars. news_members holds one row for each combination and source, saying whether the source is in it. news_links keeps only the sources that are, for the line that joins them:

news_counts: first 5 of 7 rows
combination people
Web 40
Paper 12
Paper and web 10
Radio 8
Radio and web 7
news_members: first 5 of 21 rows
combination source member
Web Web yes
Paper Web no
Paper and web Web yes
Radio Web no
Radio and web Web yes
news_links: first 5 of 12 rows
combination source
Web Web
Paper Paper
Paper and web Web
Paper and web Paper
Radio Radio
(data(news_counts) + bar + x(combination) + y(people) + x_label("") +
   y_label("People") + title("Where 85 people get their news")) /
  (data(news_links) + path + x(combination) + y(source) + group(combination) +
     style(size = 2, color = "#333333") +
     data(news_members) + point + color(member, legend = FALSE) +
     palette(c(yes = "#333333", no = "#d6d6d6")) + style(size = 7) +
     x_label("") + y_label(""))
((data(news_counts) + bar + x(col.combination) + y(col.people) + x_label("") +
   y_label("People") + title("Where 85 people get their news")) /
  (data(news_links) + path + x(col.combination) + y(col.source) + group(col.combination) +
     style(size = 2, color = "#333333") +
     data(news_members) + point + color(col.member, legend = False) +
     palette({"yes": "#333333", "no": "#d6d6d6"}) + style(size = 7) +
     x_label("") + y_label("")))
(data(news_counts) + bar + x(:combination) + y(:people) + x_label("") +
  y_label("People") + title("Where 85 people get their news")) /
  (data(news_links) + path + x(:combination) + y(:source) +
  group(:combination) + style(size = 2, color = "#333333") +
  data(news_members) + point + color(:member, legend = false) +
  palette(["yes" => "#333333", "no" => "#d6d6d6"]) + style(size = 7) +
  x_label("") + y_label(""))
below(plot(data(news_counts), bar, x(col.combination), y(col.people),
  x_label(""), y_label("People"),
  title("Where 85 people get their news")),
  plot(data(news_links), path, x(col.combination), y(col.source),
  group(col.combination), style({ size: 2, color: "#333333" }),
  data(news_members), point, color(col.member, { legend: false }),
  palette({ yes: "#333333", no: "#d6d6d6" }), style({ size: 7 }),
  x_label(""), y_label("")))
0 10 20 30 40 Where 85 people get their news People Web Paper Paper and web Radio Radio and web Paper and radio All three Radio Paper Web

The two plots share the combination axis, so each bar is above its column of dots. A dark dot marks a source in the combination, and a line joins the dark dots of one combination.

F.4.5 A horizon chart

A horizon chart draws a long time series in a panel of small height. Each value is measured from one chosen value, here the mean. Differences above it rise from the baseline in shades of one color. Differences below it are turned upward too, in shades of a second color. Each band holds a slice of fixed height, and the part of a difference that does not fit is drawn again from the baseline, in a darker shade, over the lighter band. Packages such as ggHoriPlot draw it. Cutting each difference into slices is simple arithmetic, so the host language does it. This table holds New York’s daily high temperature from May to September 1973, in degrees Fahrenheit, from R’s airquality data. Each row holds one day and one band: the part of that day’s distance from the mean that falls in the band, with three bands above the mean and three below:

nyc_horizon: first 5 of 918 rows
month day band degrees
May 1 warm 0
May 2 warm 0
May 3 warm 0
May 4 warm 0
May 5 warm 0

The first rows are 0 because the first days of May were cooler than the mean, so those days have nothing in the warm bands.

data(nyc_horizon) + area + x(day) + y(degrees) + color(band) +
  palette(c(warm = "#fcbba1", warmer = "#fb6a4a", warmest = "#a50f15",
            cool = "#c6dbef", cooler = "#6baed6", coolest = "#08519c")) +
  style(opacity = 1) + y_label("") +
  title("New York's daily highs, May to September 1973") / facet(month)
(data(nyc_horizon) + area + x(col.day) + y(col.degrees) + color(col.band) +
  palette({"warm": "#fcbba1", "warmer": "#fb6a4a", "warmest": "#a50f15", "cool": "#c6dbef", "cooler": "#6baed6", "coolest": "#08519c"}) +
  style(opacity = 1) + y_label("") +
  title("New York's daily highs, May to September 1973") / facet(col.month))
data(nyc_horizon) + area + x(:day) + y(:degrees) + color(:band) +
  palette(["warm" => "#fcbba1", "warmer" => "#fb6a4a", "warmest" => "#a50f15", "cool" => "#c6dbef", "cooler" => "#6baed6", "coolest" => "#08519c"]) +
  style(opacity = 1) + y_label("") +
  title("New York's daily highs, May to September 1973") / facet(:month)
plot(data(nyc_horizon), area, x(col.day), y(col.degrees), color(col.band),
  palette({ warm: "#fcbba1", warmer: "#fb6a4a", warmest: "#a50f15",
  cool: "#c6dbef", cooler: "#6baed6", coolest: "#08519c" }),
  style({ opacity: 1 }), y_label(""),
  title("New York's daily highs, May to September 1973"), down(col.month))
0 2 4 6 0 2 4 6 0 2 4 6 0 2 4 6 10 20 30 0 2 4 6 May June July August September New York's daily highs, May to September 1973 Day Band warm warmer warmest cool cooler coolest

“Given the nyc horizon table: areas, x is day, y is degrees, color by band, split into panel rows by month.”

Each month is one panel. Red shows the days warmer than the mean, blue the cooler days, and a darker band a larger difference. style(opacity = 1) draws each band solid, so a darker band hides the lighter one beneath it. The bands are drawn in the order they first appear in the table, so the table lists each lighter band before the darker one drawn over it.

F.5 If your plot is not here

This chapter compares gog with ggplot2’s reference index, not with its extensions. So a plot from an extension may be absent from this chapter, unless What the extensions add draws it.

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.