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.

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
# 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"))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Mapped: a legend decodes the color Life Gdp Continent Asia Europe Africa Americas Oceania

“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"))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Set: no legend, because there is nothing to decode Life Gdp

“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 (01) 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:

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
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"))
1960 1980 2000 50 60 70 A dashed trend line Mean life expectancy Year

“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"))
Asia Europe Africa Americas Oceania 0 20 40 60 80 A crosshatched bar Mean life expectancy Continent

“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"))
Asia Europe Africa Americas Oceania 40 50 60 70 80 A box: light fill, dark outline Life expectancy Continent

“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"))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 A point with a white border Life expectancy GDP per capita

“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"))
0K 10K 20K 30K 40 50 60 70 80 Each path ends at 2007 Life expectancy GDP per person Country China India Indonesia Japan Korea, Rep.

“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"))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 point Life Gdp
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"))
1960 1980 2000 40 50 60 70 80 line Life Year

“Given gapminder Asia: lines, x is year, y is life, grouped by country, colored red.”

medals: all 5 rows
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"))
USA China Great Britain Russia Germany 0 10 20 30 40 bar Gold Country

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" }))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

“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"))
USA China Great Britain Russia Germany 0 10 20 30 40 A CSS color name Gold Country

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:

gm_all: first 5 of 1704 rows
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"))
0K 50K 100K 40 60 80 1704 points: transparency reveals the density Life Gdp

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"))
1960 1980 2000 40 50 60 70 80 Five countries in gray; the trend in red Life Year

“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"))
1960 1980 2000 40 50 60 70 80 Set on a line: one width, one opacity Life Year

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"))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Only the points are tomato Life Gdp

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"]))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp Continent Asia Europe Africa Americas Oceania

“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" }))
1K 10K 40 50 60 70 80 Life Gdp Continent Asia Europe Africa Americas Oceania

“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:

winds: first 5 of 264 rows
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" }))
Proportion Season Summer Winter

“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 }))
N NE E SE S SW W NW 20 40 60 Count Direction

“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 }))
Asia Europe Africa Americas Oceania 0 20 40 60 80 Life Continent

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 }))
1K 10K 40 50 60 70 80 Bigger text, one number Life Gdp

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 }))
1K 10K 40 50 60 70 80 Smaller text, same plot Life Gdp

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 }))
1K 10K 40 50 60 70 80 Life Gdp

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"))
1K 10K 40 50 60 70 80 Life Gdp Continent Asia Europe Africa Americas Oceania

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" }))
1K 10K 40 50 60 70 80 Life Gdp Continent Asia Europe Africa Americas Oceania

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))
0K 20K 40K 40 50 60 70 80 0K 20K 40K 0K 20K 40K 0K 20K 40K 0K 20K 40K Asia Europe Africa Americas Oceania Life Gdp

“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))
0K 20K 40K 40 50 60 70 80 0K 20K 40K 0K 20K 40K 0K 20K 40K 0K 20K 40K Asia Europe Africa Americas Oceania Life Gdp

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))
0K 20K 40K 40 50 60 70 80 0K 20K 40K 0K 20K 40K 0K 20K 40K 0K 20K 40K Asia Europe Africa Americas Oceania Life Gdp

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))
0K 20K 40K 40 50 60 70 80 0K 20K 40K 0K 20K 40K 0K 20K 40K 0K 20K 40K Asia Europe Africa Americas Oceania Life Gdp

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))
0K 20K 40K 40 50 60 70 80 0K 20K 40K 0K 20K 40K 0K 20K 40K 0K 20K 40K Asia Europe Africa Americas Oceania Life Gdp

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" }))
Asia Europe Africa Americas Oceania 0 20 40 60 80 Life Continent

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" }))
1K 10K 40 50 60 70 80 Life Gdp

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.