| east | north | elevation |
|---|---|---|
| 20 | 20 | 100 |
| 20 | 40 | 102 |
| 20 | 60 | 104 |
| 20 | 80 | 105 |
| 20 | 100 | 107 |
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:
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"))“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:
| 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"))“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.
| 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"))“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):
| 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"))“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 }))))“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:
| 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:
| pair | species | petal_length |
|---|---|---|
| versicolor | setosa | 5.5 |
| versicolor | setosa | 5.7 |
| versicolor | versicolor | 5.7 |
| versicolor | versicolor | 5.5 |
| virginica | setosa | 7.2 |
| 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"))“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:
| days | surviving | sex |
|---|---|---|
| 0 | 1.0000 | Female |
| 5 | 0.9889 | Female |
| 60 | 0.9778 | Female |
| 61 | 0.9667 | Female |
| 62 | 0.9556 | Female |
| 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")))“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:
| 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 |
| 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"))“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:
| combination | people |
|---|---|
| Web | 40 |
| Paper | 12 |
| Paper and web | 10 |
| Radio | 8 |
| Radio and web | 7 |
| combination | source | member |
|---|---|---|
| Web | Web | yes |
| Paper | Web | no |
| Paper and web | Web | yes |
| Radio | Web | no |
| Radio and web | Web | yes |
| 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("")))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:
| 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))“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.