| 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 |
24 Setting vs mapping
Should a mark’s color come from your data, or from you? There are two ways to make something red, and gog keeps them apart on purpose.
Mapping lets the data decide a visual property. color(continent) says let the continent decide the color, so gog builds a scale, assigns a color per continent, and draws a legend to decode it.
Setting means you give the property a value yourself. style(color = "tomato") says make them all tomato. No column is read, no scale is built, and no legend appears, because there is nothing to decode.
# MAP: color answers "which continent?"
data(gapminder_2007) + point + x(gdp) + y(life) + color(continent) +
title("Mapped: a legend decodes the color")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) + color(col.continent) +
title("Mapped: a legend decodes the color"))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
title("Mapped: a legend decodes the color")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.continent), title("Mapped: a legend decodes the color"))“Given gapminder 2007: points, x is gdp, y is life, color by continent.”
# SET: color answers nothing; note the missing legend
data(gapminder_2007) + point + x(gdp) + y(life) +
style(color = "tomato") +
title("Set: no legend, because there is nothing to decode")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
style(color = "tomato") +
title("Set: no legend, because there is nothing to decode"))data(gapminder_2007) + point + x(:gdp) + y(:life) +
style(color = "tomato") +
title("Set: no legend, because there is nothing to decode")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
style({ color: "tomato" }),
title("Set: no legend, because there is nothing to decode"))“Given gapminder 2007: points, x is gdp, y is life, colored tomato.”
24.1 Why a constant is not a channel
Every channel in gog maps a data column to a visual property. A constant maps nothing, so it fails the definition. Rotating a 3-D plot is excluded for the same reason: it is a property of the coordinate space rather than a channel.
A practical test agrees: a mapping to color, size, shape, pattern or opacity draws a legend, and a setting draws none. If the plot needs a legend to be read, it was a mapping.
24.2 What you can set
Before you write a setting, you need to know whether the mark can carry it, and the answer differs between a bar and a point.
Every setting is a row and every mark is a column. The grid shows what each mark takes and what it refuses, including combinations you would not have guessed were legal. Legal is not the same as useful. Law 8 says the grammar guarantees a plot is legal, and you judge whether it is useful. The table shows the limit of what can be expressed. Its companion grid, what a mark lets you map rather than set, is Combinations.
| Setting | point |
line |
area |
bar |
step |
interval |
box |
ribbon |
text |
path |
rule |
zone |
surface |
edge |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
color |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
opacity |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
size |
✅ | ✅ | — | — | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | — | — | ✅ |
shape |
✅ | — | — | — | — | — | — | — | — | — | — | — | — | — |
pattern |
— | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | — | ✅ |
border_color |
✅ | — | — | ✅ | — | — | ✅ | — | — | — | — | ✅ | ✅ | — |
border_size |
✅ | — | — | ✅ | — | — | ✅ | — | — | — | — | ✅ | ✅ | — |
caps |
— | — | — | — | — | ✅ | — | — | — | — | — | — | — | — |
center |
— | — | — | — | — | ✅ | — | — | — | — | — | — | — | — |
nudge |
— | — | — | — | — | — | — | — | ✅ | — | — | — | — | — |
arrow |
— | — | — | — | — | — | — | — | — | ✅ | — | — | — | — |
reach |
— | — | — | — | — | — | — | — | — | — | ✅ | — | — | — |
(✅ the setting is available · — the mark’s geometry cannot carry it. Generated live from the engine, like the Combinations grid, so it cannot drift from what style() accepts.)
color (CSS name or hex) and opacity (0–1) are set on every mark. The other rows are not arbitrary permissions: each follows from the mark’s geometry, and a setting is available on every mark of a kind, never on one mark by accident. That is the settable rule: a setting spans its geometry class, the group of marks built the same way.
size is a measurement in pixels: a radius, a stroke width, or a font size. It belongs to every glyph and stroke, and to box, whose width it sets. The grid leaves it blank for bar, area, ribbon, zone and surface, whose positions already decide their size. shape (one of five glyphs) is point’s alone.
pattern is the texture of a mark’s paint: one visual property, realized by geometry the way color is a fill on a bar and a stroke on a line. On the six path strokes (line, step, path, interval, rule, edge) it is the dash: "solid"/"dashed"/"dotted", a dashed error bar included. On the five fills (bar, box, area, ribbon, zone) it is a fill texture: "solid"/"hatch"/"crosshatch"/"grid"/"dots", the hatchings that keep a plot readable in grayscale or in print, where there is no color. It is paint, never geometry: a dashed line and a solid one trace the same path. A step, by contrast, is a different mark because it traces a different path.
A trend drawn through the data is often dashed, so that it reads as a summary rather than as a measurement:
| 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 |
data(gapminder_asia) + line * mean + x(year) + y(life) +
style(pattern = "dashed", size = 1.5) + y_label("Mean life expectancy") +
title("A dashed trend line")(data(gapminder_asia) + line * mean + x(col.year) + y(col.life) +
style(pattern = "dashed", size = 1.5) + y_label("Mean life expectancy") +
title("A dashed trend line"))data(gapminder_asia) + line * mean + x(:year) + y(:life) +
style(pattern = "dashed", size = 1.5) +
y_label("Mean life expectancy") + title("A dashed trend line")plot(data(gapminder_asia), layer(line, mean), x(col.year), y(col.life),
style({ pattern: "dashed", size: 1.5 }),
y_label("Mean life expectancy"), title("A dashed trend line"))“Given gapminder Asia: a line derived by mean, x is year, y is life, with pattern dashed and size 1.5.”
On a fill the same setting hatches the region, a print-ready look that survives a black-and-white photocopy:
data(gapminder_2007) + bar * mean + x(continent) + y(life) +
style(pattern = "crosshatch") +
y_label("Mean life expectancy") +
title("A crosshatched bar")(data(gapminder_2007) + bar * mean + x(col.continent) + y(col.life) +
style(pattern = "crosshatch") +
y_label("Mean life expectancy") +
title("A crosshatched bar"))data(gapminder_2007) + bar * mean + x(:continent) + y(:life) +
style(pattern = "crosshatch") + y_label("Mean life expectancy") +
title("A crosshatched bar")plot(data(gapminder_2007), layer(bar, mean), x(col.continent),
y(col.life), style({ pattern: "crosshatch" }),
y_label("Mean life expectancy"), title("A crosshatched bar"))“Given gapminder 2007: bars derived by mean, x is continent, y is life, with pattern crosshatch.”
The texture is one per layer: style() gives every bar in the layer the same hatch, so it cannot give each category of a color split its own. Doing that is a mapping (a category to a texture), which is the pattern() channel, the texture channel that matches shape().
border_color and border_size draw an outline separate from the fill, and 0 means no border. They belong to the five closed-glyph fills (bar, box, point, zone and surface). On those marks no composition can trace the border, so a setting is the only way to draw it. color fills the shape; border_color outlines it; the two are independent, so a box can be pale inside and dark-edged:
data(gapminder_2007) + box + x(continent) + y(life) +
style(color = "lightsteelblue", border_color = "#26456e", border_size = 1.6) +
y_label("Life expectancy") + title("A box: light fill, dark outline")(data(gapminder_2007) + box + x(col.continent) + y(col.life) +
style(color = "lightsteelblue", border_color = "#26456e", border_size = 1.6) +
y_label("Life expectancy") + title("A box: light fill, dark outline"))data(gapminder_2007) + box + x(:continent) + y(:life) +
style(color = "lightsteelblue", border_color = "#26456e", border_size = 1.6) +
y_label("Life expectancy") + title("A box: light fill, dark outline")plot(data(gapminder_2007), box, x(col.continent), y(col.life),
style({ color: "lightsteelblue", border_color: "#26456e",
border_size: 1.6 }), y_label("Life expectancy"),
title("A box: light fill, dark outline"))“Given gapminder 2007: boxes, x is continent, y is life, colored lightsteelblue, with border color #26456e and border size 1.6.”
The same two settings outline a point, so overlapping points stay separate, exactly as on bar and box:
data(gapminder_2007) + point + x(gdp) + y(life) +
style(color = "steelblue", opacity = 0.7, size = 5, border_color = "white", border_size = 1) +
x_label("GDP per capita") + y_label("Life expectancy") +
title("A point with a white border")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
style(color = "steelblue", opacity = 0.7, size = 5, border_color = "white", border_size = 1) +
x_label("GDP per capita") + y_label("Life expectancy") +
title("A point with a white border"))data(gapminder_2007) + point + x(:gdp) + y(:life) +
style(color = "steelblue", opacity = 0.7, size = 5, border_color = "white", border_size = 1) +
x_label("GDP per capita") + y_label("Life expectancy") +
title("A point with a white border")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
style({ color: "steelblue", opacity: 0.7, size: 5,
border_color: "white", border_size: 1 }), x_label("GDP per capita"),
y_label("Life expectancy"), title("A point with a white border"))“Given gapminder 2007: points, x is gdp, y is life, colored steelblue, with opacity 0.7, size 5, border color white and border size 1.”
A curve fill (area, ribbon) takes no border. Its edge is a data curve, so you draw it as a line layer (area + line, line * bounds), which can do more than a border setting. And a cross point has no fill to outline, so a border on a cross-shaped glyph simply has nothing to draw on.
The last five rows turn one part of a mark’s drawing on or off, each belonging to one mark’s own geometry. caps and center show or hide an interval’s end caps and center dot. nudge moves a text label off its point. arrow puts a head on a path’s end. And reach says how far a rule crosses the axis it does not name. They are single-mark on purpose, because only a whisker has a cap. That is why their rows are nearly empty, and an empty row tells you the setting belongs to one mark.
arrow shows this best: only path is checked in its row, and the reason is geometry. A head marks a direction, and only a path has one: a line sorts its vertices by x, so its last point is the largest x, not the point the data ended on.
data(gapminder_asia) + path + x(gdp) + y(life) + color(country) +
style(arrow = "end") +
x_label("GDP per person") + y_label("Life expectancy") +
title("Each path ends at 2007")(data(gapminder_asia) + path + x(col.gdp) + y(col.life) + color(col.country) +
style(arrow = "end") +
x_label("GDP per person") + y_label("Life expectancy") +
title("Each path ends at 2007"))data(gapminder_asia) + path + x(:gdp) + y(:life) + color(:country) +
style(arrow = "end") + x_label("GDP per person") +
y_label("Life expectancy") + title("Each path ends at 2007")plot(data(gapminder_asia), path, x(col.gdp), y(col.life),
color(col.country), style({ arrow: "end" }), x_label("GDP per person"),
y_label("Life expectancy"), title("Each path ends at 2007"))“Given gapminder Asia: paths, x is gdp, y is life, color by country, with arrow end.”
It takes "end", "start", or "both": a value rather than a TRUE/FALSE, because a double-headed arrow is a common request, and TRUE/FALSE would have needed a second setting.
24.2.1 One color, any mark
Having set a point’s color once, you should not need a second spelling for a line or a bar.
style(color = ) is the same instruction whichever mark it lands on. It fills points, strokes lines, and fills bars:
data(gapminder_2007) + point + x(gdp) + y(life) +
style(color = "red") +
title("point")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
style(color = "red") +
title("point"))data(gapminder_2007) + point + x(:gdp) + y(:life) + style(color = "red") +
title("point")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
style({ color: "red" }), title("point"))data(gapminder_asia) + line + x(year) + y(life) + group(country) +
style(color = "red") +
title("line")(data(gapminder_asia) + line + x(col.year) + y(col.life) + group(col.country) +
style(color = "red") +
title("line"))data(gapminder_asia) + line + x(:year) + y(:life) + group(:country) +
style(color = "red") + title("line")plot(data(gapminder_asia), line, x(col.year), y(col.life),
group(col.country), style({ color: "red" }), title("line"))“Given gapminder Asia: lines, x is year, y is life, grouped by country, colored red.”
| country | gold | silver | bronze |
|---|---|---|---|
| USA | 46 | 37 | 38 |
| China | 38 | 31 | 22 |
| Great Britain | 29 | 17 | 19 |
| Russia | 19 | 18 | 9 |
| Germany | 17 | 10 | 15 |
data(medals) + bar + x(country) + y(gold) +
style(color = "red") +
title("bar")(data(medals) + bar + x(col.country) + y(col.gold) +
style(color = "red") +
title("bar"))data(medals) + bar + x(:country) + y(:gold) + style(color = "red") +
title("bar")plot(data(medals), bar, x(col.country), y(col.gold),
style({ color: "red" }), title("bar"))This next sentence looks like the same request, and it is not:
data(gapminder_2007) + point + x(gdp) + y(life) + color("red")data(gapminder_2007) + point + x(col.gdp) + y(col.life) + color("red")data(gapminder_2007) + point + x(:gdp) + y(:life) + color("red")plot(data(gapminder_2007), point, x(col.gdp), y(col.life), color("red"))Error:
! gog: `color("red")` binds a *value*, and a channel takes a *column*: `color(red)` maps the column called `red`.
To fix one value for the whole layer instead — no legend, nothing to decode — that is a setting: `style(color = "red")`.
color() names a column, so color("red") asks for a column called red. Setting a value and mapping a column are different requests, and gog makes you say which one you mean.
24.2.2 Several at once
A figure for publication rarely needs only one setting. Color, opacity, size and shape are decided together, so they are written together:
data(gapminder_2007) + point + x(gdp) + y(life) +
style(color = "#2a6f97", opacity = 0.45, size = 7, shape = "diamond")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
style(color = "#2a6f97", opacity = 0.45, size = 7, shape = "diamond"))data(gapminder_2007) + point + x(:gdp) + y(:life) +
style(color = "#2a6f97", opacity = 0.45, size = 7, shape = "diamond")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
style({ color: "#2a6f97", opacity: 0.45, size: 7, shape: "diamond" }))“Given gapminder 2007: points, x is gdp, y is life, colored #2a6f97, with opacity 0.45, size 7 and shape diamond.”
One style() call carries as many settings as the mark can take, and every row in the layer is drawn with all of them. The four here are independent: remove any one and the other three are unchanged. Each mark’s chapter lists what you can set on it, under What you can set.
24.2.3 Colors are CSS colors
A color arrives as a hex value from a style guide, or as a name remembered from another tool. Not every name you remember is one gog knows.
A CSS name and a hex value both work anywhere a color is accepted, in style() and in palette():
data(medals) + bar + x(country) + y(gold) +
style(color = "darkslateblue") +
title("A CSS color name")(data(medals) + bar + x(col.country) + y(col.gold) +
style(color = "darkslateblue") +
title("A CSS color name"))data(medals) + bar + x(:country) + y(:gold) +
style(color = "darkslateblue") + title("A CSS color name")plot(data(medals), bar, x(col.country), y(col.gold),
style({ color: "darkslateblue" }), title("A CSS color name"))A name gog does not recognize is refused, and the closest valid name is offered, rather than being passed to the renderer and painted black:
data(medals) + bar + x(country) + y(gold) + style(color = "stelblue")data(medals) + bar + x(col.country) + y(col.gold) + style(color = "stelblue")data(medals) + bar + x(:country) + y(:gold) + style(color = "stelblue")plot(data(medals), bar, x(col.country), y(col.gold),
style({ color: "stelblue" }))Error:
! gog: `style(color = "stelblue")` is not a color. Did you mean "steelblue"? Use a CSS color name, or a hex value like "#4e79a7".
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
R users often assume R color names are CSS color names. They are not. R’s colors() includes numbered shades (gray80, gray50, steelblue3) that CSS does not have. gog uses CSS colors because that is what SVG accepts and what the other three bindings use too, so the vocabulary is the same in every language:
data(medals) + bar + x(country) + y(gold) + style(color = "gray80")data(medals) + bar + x(col.country) + y(col.gold) + style(color = "gray80")data(medals) + bar + x(:country) + y(:gold) + style(color = "gray80")plot(data(medals), bar, x(col.country), y(col.gold),
style({ color: "gray80" }))Error:
! gog: `style(color = "gray80")` is not a color. `gray80` is an R color name. gog uses CSS colors, which have no numbered shades — use "lightgray", "darkgray", or "gray", or a hex value like "#cccccc" for an exact shade.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
For an exact shade, give the hex value.
24.3 What settings are for
Two jobs are constant in practice, and neither is a mapping.
Overplotting. With enough points, the marks cover one another and the shape of the cloud disappears. Lowering opacity is a statement about the rendering, not about the data:
| country | continent | year | life | population | gdp |
|---|---|---|---|---|---|
| Afghanistan | Asia | 1952 | 28.801 | 8425333 | 779.4453 |
| Afghanistan | Asia | 1957 | 30.332 | 9240934 | 820.8530 |
| Afghanistan | Asia | 1962 | 31.997 | 10267083 | 853.1007 |
| Afghanistan | Asia | 1967 | 34.020 | 11537966 | 836.1971 |
| Afghanistan | Asia | 1972 | 36.088 | 13079460 | 739.9811 |
data(gm_all) + point + x(gdp) + y(life) +
style(opacity = 0.2, size = 3) +
title("1704 points: transparency reveals the density")(data(gm_all) + point + x(col.gdp) + y(col.life) +
style(opacity = 0.2, size = 3) +
title("1704 points: transparency reveals the density"))data(gm_all) + point + x(:gdp) + y(:life) +
style(opacity = 0.2, size = 3) +
title("1704 points: transparency reveals the density")plot(data(gm_all), point, x(col.gdp), y(col.life),
style({ opacity: 0.2, size: 3 }),
title("1704 points: transparency reveals the density"))A reference layer. When one layer is what the plot is about, the others should be drawn faintly:
data(gapminder_asia) + x(year) + y(life) +
line + group(country) + style(color = "lightgray") +
line * smooth + style(color = "firebrick", size = 3) +
title("Five countries in gray; the trend in red")(data(gapminder_asia) + x(col.year) + y(col.life) +
line + group(col.country) + style(color = "lightgray") +
line * smooth + style(color = "firebrick", size = 3) +
title("Five countries in gray; the trend in red"))data(gapminder_asia) + x(:year) + y(:life) + line + group(:country) +
style(color = "lightgray") + line * smooth +
style(color = "firebrick", size = 3) +
title("Five countries in gray; the trend in red")plot(data(gapminder_asia), x(col.year), y(col.life), line,
group(col.country), style({ color: "lightgray" }), layer(line, smooth),
style({ color: "firebrick", size: 3 }),
title("Five countries in gray; the trend in red"))“Given gapminder Asia: x is year, y is life, lines, grouped by country, colored lightgray, and also a line derived by smooth, colored firebrick, with size 3.”
24.4 line takes settings it refuses as channels
A mapped opacity works on a point. Try the same binding on a line and gog refuses it.
A line is drawn with one stroke from its first point to its last. Nothing varies from row to row along that stroke, so size and opacity are not channels on line:
data(gapminder_asia) + x(year) + y(life) + line + group(country) + opacity(life)data(gapminder_asia) + x(col.year) + y(col.life) + line + group(col.country) + opacity(col.life)data(gapminder_asia) + x(:year) + y(:life) + line + group(:country) +
opacity(:life)plot(data(gapminder_asia), x(col.year), y(col.life), line,
group(col.country), opacity(col.life))Error:
! gog: `opacity` cannot be bound to `line` — a line has no opacity feature. Remove the `life` mapping from `opacity`, or use a mark that has one.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
But one stroke still has a width and an opacity. Those are settings:
data(gapminder_asia) + line + x(year) + y(life) + group(country) +
style(size = 4, opacity = 0.5) +
title("Set on a line: one width, one opacity")(data(gapminder_asia) + line + x(col.year) + y(col.life) + group(col.country) +
style(size = 4, opacity = 0.5) +
title("Set on a line: one width, one opacity"))data(gapminder_asia) + line + x(:year) + y(:life) + group(:country) +
style(size = 4, opacity = 0.5) +
title("Set on a line: one width, one opacity")plot(data(gapminder_asia), line, x(col.year), y(col.life),
group(col.country), style({ size: 4, opacity: 0.5 }),
title("Set on a line: one width, one opacity"))This is where the distinction matters most. “Can a column be mapped here?” and “Does this mark have this property?” are different questions, and line is where the answers differ.
24.5 You cannot map and set the same property
It is easy to write both by accident: a color(continent) left from an earlier draft, and a style(color = ) added to try one shade.
Doing both would mean one of the two instructions is silently discarded, which gog never does:
data(gapminder_2007) + point + x(gdp) + y(life) +
color(continent) + style(color = "tomato")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
color(col.continent) + style(color = "tomato"))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
style(color = "tomato")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.continent), style({ color: "tomato" }))Error:
! gog: `color(continent)` maps color and `style(color = "tomato")` sets it — one layer cannot do both. Keep `color(continent)` to show the data, or `style(color = "tomato")` to fix every point at one value.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
Pick the one you meant.
24.6 style applies to one layer only
A trend over a cloud of points usually wants the points faint and the line plain, so the two layers cannot look the same.
color() written before any mark applies to every layer that can take it. style() attaches to the nearest mark before it and goes no further, because a decoration written for one layer should not change another:
data(gapminder_2007) + x(gdp) + y(life) +
line * smooth +
point + style(color = "tomato", opacity = 0.4) +
title("Only the points are tomato")(data(gapminder_2007) + x(col.gdp) + y(col.life) +
line * smooth +
point + style(color = "tomato", opacity = 0.4) +
title("Only the points are tomato"))data(gapminder_2007) + x(:gdp) + y(:life) + line * smooth + point +
style(color = "tomato", opacity = 0.4) +
title("Only the points are tomato")plot(data(gapminder_2007), x(col.gdp), y(col.life), layer(line, smooth),
point, style({ color: "tomato", opacity: 0.4 }),
title("Only the points are tomato"))To style both layers, say so on both.
24.7 palette: choosing the colors a mapping uses
You may want both at once: the continents separated by color, and those five colors chosen by you rather than by gog.
style(color = ) is written instead of a mapping. palette() keeps the mapping and chooses which colors the categories receive:
data(gapminder_2007) + point + x(gdp) + y(life) + color(continent) +
palette(c("tomato", "steelblue", "seagreen", "goldenrod", "orchid"))(data(gapminder_2007) + point + x(col.gdp) + y(col.life) + color(col.continent) +
palette(["tomato", "steelblue", "seagreen", "goldenrod", "orchid"]))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
palette(["tomato", "steelblue", "seagreen", "goldenrod", "orchid"])plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.continent),
palette(["tomato", "steelblue", "seagreen", "goldenrod", "orchid"]))“Given gapminder 2007: points, x is gdp, y is life, color by continent, with a palette of tomato, steelblue, seagreen, goldenrod and orchid.”
A palette needs a mapping to give its colors to. With no column bound to color it has nothing to color, so it is refused rather than accepted and then ignored:
data(gapminder_2007) + point + x(gdp) + y(life) + palette("okabe")data(gapminder_2007) + point + x(col.gdp) + y(col.life) + palette("okabe")data(gapminder_2007) + point + x(:gdp) + y(:life) + palette("okabe")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
palette("okabe"))Error:
! gog: `palette()` chooses the colors a `color` mapping hands out, and nothing here maps `color`, so it would have no effect. Bind a column with `color(<column>)` to use it — or, to paint every mark one color, that is a setting rather than a palette: `style(color = "…")`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The message names two fixes, because there are two things you may have meant. To give each category its own color, bind the column with color(continent). To paint every mark one color, that is a setting rather than a palette, and style(color = "steelblue") is where it belongs.
A mapping need not be written to be there. A zone * density colors its cells by a measure it computes itself (Zone), so palette() works on that plot.
One color, though, is not a palette. palette() takes either a palette name ("gog", "okabe") or the colors themselves; a single color name is neither, so it is refused rather than quietly using the default:
data(gapminder_2007) + point + x(gdp) + y(life) + color(continent) +
palette("red")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) + color(col.continent) +
palette("red"))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
palette("red")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.continent), palette("red"))Error:
! gog: `palette("red")` is not a known palette. Named palettes are gog, okabe, soft for categories; blue, viridis, magma, inferno, plasma, cividis, gray for numbers; blue_red, brown_teal for numbers that diverge from a center. `"red"` is a color, not a palette. To paint every mark one color use `style(color = "red")`; to give each category its own color pass a vector: `palette(c("red", ...))`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
Making everything one color is setting, not choosing a palette, so use style(color = "red").
24.8 theme: settings that belong to no mark
The gridlines distract on a slide. The category names overlap along the bottom. Neither problem belongs to any mark.
style() sets a mark’s own appearance. Some settings belong to no mark at all: the gridlines, the shape of the panel, the angle the tick labels are read at. Those belong to the whole plot rather than to any mark, and they have their own word.
data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
color(continent) + theme(grid = "none")(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
color(col.continent) + theme(grid = "none"))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
color(:continent) + theme(grid = "none")plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
y(col.life), color(col.continent), theme({ grid: "none" }))“Given gapminder 2007: points, x is gdp on a log scale, y is life, color by continent.”
grid names its sets by the axis whose ticks they mark, not by the direction they run: "x" keeps the lines standing up from the x axis and removes the others. Naming them that way is what lets the setting survive a bent plane, where the y axis’s gridlines are rings and nothing runs vertically at all:
| direction | bearing | speed | season |
|---|---|---|---|
| N | 19.612260 | 10.1 | Winter |
| N | 357.049499 | 11.0 | Winter |
| N | 349.709737 | 13.8 | Winter |
| N | 2.602499 | 7.4 | Winter |
| N | 338.210715 | 3.7 | Summer |
data(winds) + bar * proportion * stack + color(season) + polar() +
theme(grid = "x")(data(winds) + bar * proportion * stack + color(col.season) + polar() +
theme(grid = "x"))data(winds) + bar * proportion * stack + color(:season) + polar() +
theme(grid = "x")plot(data(winds), layer(bar, proportion, stack), color(col.season),
polar(), theme({ grid: "x" }))“Given the winds: bars derived by proportion and stack, color by season, in polar.”
ratio sets the panel’s width divided by its height, which is how a circle comes out round. The image keeps the size it was given and the panel shrinks inside it, so the plot still takes the same room on a page:
data(winds) + bar * count + x(direction) + polar() + theme(ratio = 1)data(winds) + bar * count + x(col.direction) + polar() + theme(ratio = 1)data(winds) + bar * count + x(:direction) + polar() + theme(ratio = 1)plot(data(winds), layer(bar, count), x(col.direction), polar(),
theme({ ratio: 1 }))“Given the winds: bars derived by count, x is direction, in polar.”
And tick_angle turns the x labels, which is the answer to category names that overlap. The bottom margin grows to hold them:
data(gapminder_2007) + bar * mean + x(continent) + y(life) +
theme(tick_angle = 45)(data(gapminder_2007) + bar * mean + x(col.continent) + y(col.life) +
theme(tick_angle = 45))data(gapminder_2007) + bar * mean + x(:continent) + y(:life) +
theme(tick_angle = 45)plot(data(gapminder_2007), layer(bar, mean), x(col.continent),
y(col.life), theme({ tick_angle: 45 }))font_size sets how big the plot’s text is. It is one number, not three. The number itself is the size of a tick label, in pixels. The axis names and the title are a fixed proportion larger. The default of 11 draws ticks at 11, axis names at 13 and the title at 16. Ask for 16 and those three become 16, 19 and 23:
data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
title("Bigger text, one number") + theme(font_size = 16)(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
title("Bigger text, one number") + theme(font_size = 16))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
title("Bigger text, one number") + theme(font_size = 16)plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
y(col.life), title("Bigger text, one number"), theme({ font_size: 16 }))Look at the margins. They grew with the text, because the room an axis needs is measured from the labels that go in it. Text and the space it sits in are one decision, so larger text never covers the plot.
A smaller number keeps a plot readable when it has to be printed small:
data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
title("Smaller text, same plot") + theme(font_size = 8)(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
title("Smaller text, same plot") + theme(font_size = 8))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
title("Smaller text, same plot") + theme(font_size = 8)plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
y(col.life), title("Smaller text, same plot"), theme({ font_size: 8 }))One number covers all the text because the three sizes were never independent. If the three sizes were set separately, one plot could carry three unrelated text sizes. Asking for a big title alone is not possible here, and that is the design rather than a gap in it.
The number is a measurement in pixels, not a multiplier. font_size = 1.5 is refused, because a number that small is almost always meant as a multiplier:
data(gapminder_2007) + point + x(gdp) + y(life) + theme(font_size = 1.5)data(gapminder_2007) + point + x(col.gdp) + y(col.life) + theme(font_size = 1.5)data(gapminder_2007) + point + x(:gdp) + y(:life) + theme(font_size = 1.5)plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
theme({ font_size: 1.5 }))Error:
! gog: `theme(font_size = )` is how many pixels a tick label is, not a multiplier, so it needs one number of at least 4. The default is 11, and the axis names and the title are derived from it.
font_size does not name a typeface. The engine measures text with its own table of character widths, which is what lets it lay out a plot without a font installed, so there is no font for it to choose. The computer that opens the plot supplies the letters.
A theme can also be asked for by name. A named bundle of these properties is a preset, and you can still adjust it. A preset you could not adjust would leave you setting every property one at a time:
data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
theme("minimal", ratio = 1)(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
theme("minimal", ratio = 1))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
theme("minimal", ratio = 1)plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
y(col.life), theme("minimal", { ratio: 1 }))24.9 The journal look
A journal often asks for a figure on a white background inside a framed panel.
theme("bw") is the one most often wanted for publication: a white panel inside a full rectangle.
data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
color(continent) + theme("bw")(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
color(col.continent) + theme("bw"))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
color(:continent) + theme("bw")plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
y(col.life), color(col.continent), theme("bw"))The name is the misleading part, and it is worth being exact about it. What becomes black and white is everything around the data, never the data itself: the continents above keep every color the palette gave them. In a “bw” plot only the data carries color; everything around it is black, white or gray.
Three settings carry the whole preset. This sentence draws the same picture as theme("bw") above:
data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
color(continent) + theme(background = "white", frame = "full", strip = "white")(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
color(col.continent) + theme(background = "white", frame = "full", strip = "white"))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
color(:continent) +
theme(background = "white", frame = "full", strip = "white")plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
y(col.life), color(col.continent),
theme({ background: "white", frame: "full", strip: "white" }))That is the rule every preset in gog follows: a preset only names settings you could have written yourself. A preset that did more than that would be a second vocabulary inside the first.
24.10 The strip, and how it prints
The third property is strip. It colors the band above each panel, the band that names the category the panel holds. You only see it on a faceted plot, and its color matters more in print than on screen.
By default the band is a light gray. On a screen that is fine, and it ties each name to its own panel:
(data(gapminder_2007) + point + x(gdp, tick_count = 3) + y(life)) |
facet(continent)((data(gapminder_2007) + point + x(col.gdp, tick_count = 3) + y(col.life)) |
facet(col.continent))(data(gapminder_2007) + point + x(:gdp, tick_count = 3) + y(:life)) |
facet(:continent)plot(data(gapminder_2007), point, x(col.gdp, { tick_count: 3 }),
y(col.life), across(col.continent))“Given gapminder 2007: points, x is gdp with 3 ticks, y is life, split into panel columns by continent.”
On paper it is not fine. A light gray is printed as a pattern of tiny dots, and that pattern sits directly under small text. The label gets harder to read, and a photocopy makes it worse. This is the same reason pattern exists for fills: what survives on a screen and what survives on a page are different questions.
So theme("bw") makes the band white as well. The framed panel already separates one panel from the next, so the tint has nothing left to do:
(data(gapminder_2007) + point + x(gdp, tick_count = 3) + y(life) + theme("bw")) |
facet(continent)((data(gapminder_2007) + point + x(col.gdp, tick_count = 3) + y(col.life) + theme("bw")) |
facet(col.continent))(data(gapminder_2007) + point + x(:gdp, tick_count = 3) + y(:life) +
theme("bw")) | facet(:continent)plot(data(gapminder_2007), point, x(col.gdp, { tick_count: 3 }),
y(col.life), theme("bw"), across(col.continent))You can also name the color yourself, for a poster or a slide where a tint makes the name easier to read rather than harder:
(data(gapminder_2007) + point + x(gdp, tick_count = 3) + y(life) +
theme(strip = "#dce8f0", frame = "full")) |
facet(continent)((data(gapminder_2007) + point + x(col.gdp, tick_count = 3) + y(col.life) +
theme(strip = "#dce8f0", frame = "full")) |
facet(col.continent))(data(gapminder_2007) + point + x(:gdp, tick_count = 3) + y(:life) +
theme(strip = "#dce8f0", frame = "full")) | facet(:continent)plot(data(gapminder_2007), point, x(col.gdp, { tick_count: 3 }),
y(col.life), theme({ strip: "#dce8f0", frame: "full" }),
across(col.continent))An animated plot (Play, later in the book) gets the same band, because it names the value it is showing the same way. theme(strip = ) colors that one too. A property that colored one band and not the other would be exactly the kind of exception Law 2 forbids.
24.10.1 The label color follows the band
A dark band is a real choice, and it creates one problem: the default label is dark too.
A dark strip with light text is a common look, and it takes one property:
(data(gapminder_2007) + point + x(gdp, tick_count = 3) + y(life) +
theme(strip = "#22223b", frame = "full")) |
facet(continent)((data(gapminder_2007) + point + x(col.gdp, tick_count = 3) + y(col.life) +
theme(strip = "#22223b", frame = "full")) |
facet(col.continent))(data(gapminder_2007) + point + x(:gdp, tick_count = 3) + y(:life) +
theme(strip = "#22223b", frame = "full")) | facet(:continent)plot(data(gapminder_2007), point, x(col.gdp, { tick_count: 3 }),
y(col.life), theme({ strip: "#22223b", frame: "full" }),
across(col.continent))You did not name a text color, and the text came out white. gog reads the band you asked for and picks whichever of its two default label colors contrasts more against it. A pale band keeps the dark label color, so the plots earlier in this section are unchanged.
gog chooses the label color itself; it does not guess to save you typing. Without that rule, theme(strip = "black") would paint the near-black default label onto a near-black band. Every panel name would be unreadable while the plot still looked finished. gog will not draw a panel name that cannot be read, so it supplies the label color you did not write.
When the label color is a real choice rather than a legibility question, name it. A navy band with gold text is legible, so gog uses the color you name:
(data(gapminder_2007) + point + x(gdp, tick_count = 3) + y(life) +
theme(strip = "navy", strip_text = "gold", frame = "full")) |
facet(continent)((data(gapminder_2007) + point + x(col.gdp, tick_count = 3) + y(col.life) +
theme(strip = "navy", strip_text = "gold", frame = "full")) |
facet(col.continent))(data(gapminder_2007) + point + x(:gdp, tick_count = 3) + y(:life) +
theme(strip = "navy", strip_text = "gold", frame = "full")) |
facet(:continent)plot(data(gapminder_2007), point, x(col.gdp, { tick_count: 3 }),
y(col.life),
theme({ strip: "navy", strip_text: "gold", frame: "full" }),
across(col.continent))Every derivation in gog works this way: it decides what you did not say, and stops deciding as soon as you say it. The engine guarantees the plot is well-formed and leaves taste to you, which is Law 8.
One band gives gog no color to read. "transparent" shows whatever is behind it, and gog cannot know what that is. So the label stays dark, which suits the white page a transparent band usually sits on.
frame says how the panel is bounded, and it takes three values. "full" closes the axis lines into a rectangle, "axes" is the default pair along the bottom and left, and "none" removes them:
data(gapminder_2007) + bar * mean + x(continent) + y(life) +
theme(frame = "none", grid = "y")(data(gapminder_2007) + bar * mean + x(col.continent) + y(col.life) +
theme(frame = "none", grid = "y"))data(gapminder_2007) + bar * mean + x(:continent) + y(:life) +
theme(frame = "none", grid = "y")plot(data(gapminder_2007), layer(bar, mean), x(col.continent),
y(col.life), theme({ frame: "none", grid: "y" }))It is called frame and not border because style(border_color = ) already uses that word, and it means something else: the border of a mark, a bar’s or a point’s. A frame surrounds the picture; a border is what a shape is drawn with. One word, one meaning.
background takes any color the rest of the grammar takes, transparent among them. A journal usually asks for that one, because the figure goes onto a page whose color gog does not know:
data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
theme(background = "transparent", frame = "full")(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
theme(background = "transparent", frame = "full"))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
theme(background = "transparent", frame = "full")plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
y(col.life), theme({ background: "transparent", frame: "full" }))Everything theme() accepts is validated, and a name it does not know is refused rather than quietly ignored:
data(gapminder_2007) + point + x(gdp) + y(life) + theme("dark")data(gapminder_2007) + point + x(col.gdp) + y(col.life) + theme("dark")data(gapminder_2007) + point + x(:gdp) + y(:life) + theme("dark")plot(data(gapminder_2007), point, x(col.gdp), y(col.life), theme("dark"))Error:
! gog: `theme("dark")` is not a theme. gog has `gog`, `minimal`, or `bw`. A theme is a named preset you can then adjust — `theme("minimal", ratio = 1)`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
data(gapminder_2007) + point + x(gdp) + y(life) + theme(grid = "vertical")data(gapminder_2007) + point + x(col.gdp) + y(col.life) + theme(grid = "vertical")data(gapminder_2007) + point + x(:gdp) + y(:life) +
theme(grid = "vertical")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
theme({ grid: "vertical" }))Error:
! gog: `theme(grid = )` is one of "both", "x", "y" or "none".
The difference between the two settings atoms is worth stating once, because it is not about which is more important. style() is the ink: it belongs to a mark, and a plot with three layers can carry three different ones. theme() is the page: there is one of it, and no single layer has gridlines or a background of its own. The two never overlap, and that is why they are two words rather than one.
Text size is where that split is easiest to see, because both atoms can set a size. style(size = 17) on a text layer sets how big those labels are, and they are labels the data asked for. theme(font_size = 16) sets how big the tick labels, the axis names and the title are, and those belong to the page. One is a property of a mark and the other is a property of the plot, so neither can do the other’s job.