| 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 |
23 Channels and legends
Which visual property should carry which column? A channel binds a data column to a visual property of a mark, and the channels do not all do the same work. x, y and z are the vowels: without one a mark does not render. The five below that earn a legend are the final consonants, the optional ㅁ (m) that turns 가 (ga) into 감 (gam): never required, and each one changes what the plot says. Every channel atom follows the same pattern:
channel(field)
No exceptions. x and y are channels too: the same rule applies everywhere.
The word for this is not settled outside gog, so you may know it as something else. Bertin wrote the vocabulary down first, in Sémiologie graphique (Bertin, 1967). He separated the plane’s two dimensions from six retinal variables: size, value, texture, color, orientation, and shape. The eye does not read the two groups the same way. Wilkinson and ggplot2 both say aesthetic, which is why you write aes() there and nothing like it here. The visualization literature settled instead on channel, paired with the mark it modifies (Munzner, 2014), and that is the word gog takes. A channel is a path a signal travels down, which is what a color or a size is here. gog takes the everyday word for the job, as its Plain Names law asks. Encoding is the same idea named as an act rather than a thing, and this book keeps both words. A channel is what you write; an encoding is what it does. Encoding scope is about the second.
| Atom | Visual property | Variable type |
|---|---|---|
x(field) |
horizontal position | any |
y(field) |
vertical position | any |
z(field) |
depth: the third position, which makes a plot 3-D | continuous |
color(field) |
fill / stroke color | continuous or categorical |
size(field) |
point radius | continuous |
shape(field) |
glyph shape | categorical |
pattern(field) |
fill hatch / stroke dash | categorical |
opacity(field) |
transparency | continuous |
group(field) |
splits a mark into one series per category, without encoding it | categorical |
label(field) |
the string a text mark draws |
any |
play(field) |
cuts the plot into frames and plays them in sequence | any |
A legend is generated automatically whenever one of the five refining channels (color, size, shape, pattern, opacity) is bound to a column, with no configuration required. x and y earn an axis instead, and group earns neither: it splits a mark without encoding anything, so there is nothing for a key to decode.
23.1 color
Which points belong together? A plain scatter cannot say, so the grouping has to come from a column.
color(field) maps a column to color, and a legend appears automatically. It takes either kind of column, and the kind decides what color means.
It is spelled color, and only color. gog writes American English throughout and accepts no second spelling, so there is no colour here as there is in ggplot2; writing one earns a refusal that names the fix (see Law 3).
A categorical column asks “which one?” Each category gets its own hue from the palette:
data(gapminder_2007) + point + x(gdp) + y(life) +
color(continent)(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
color(col.continent))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent)plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.continent))“Given gapminder 2007: points, x is gdp, y is life, color by continent.”
A continuous column asks “how much?” Its values run along a ramp. A ramp is a range of colors that a number is placed along, the way an axis is a range of positions. This one runs light to dark, and the legend is the ramp itself, labeled at its minimum, midpoint, and maximum:
data(gapminder_2007) + point + x(gdp) + y(life) +
color(life) + style(size = 6)(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
color(col.life) + style(size = 6))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:life) +
style(size = 6)plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.life), style({ size: 6 }))Notice the legend: color gets one continuous strip while size and opacity get three sampled examples. A legend should show the scale, and color is the only continuous channel whose whole range fits in a fixed space; there is no way to draw a continuum of circles. The rule is the same for every channel; only the shape of the legend changes. The difference matters most for a multi-hue ramp. Three sampled colors from viridis say nothing about the colors between them, while three circles of growing size do.
The default ramp is one hue, not a rainbow, and that too is deliberate. A sequential ramp encodes magnitude, and lightness is what a reader perceives as more. One hue running light to dark is read quickly, stays readable in grayscale and under color blindness, and stays distinct from the categorical palette when both appear in one figure. A rainbow encodes magnitude as hue, which has no intrinsic order, so the reader has to consult the legend for every point. gog’s default is a single blue built on the same hue as the first categorical color, so the two scales read as one system. See Ramps below for the multi-hue alternatives, the diverging ramps, and custom stops.
color also implies grouping for line marks. Each unique value gets its own connected line:
| 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) + x(year) + y(life) + color(country) +
line + point(data(gapminder_asia) + x(col.year) + y(col.life) + color(col.country) +
line + point)data(gapminder_asia) + x(:year) + y(:life) + color(:country) + line +
pointplot(data(gapminder_asia), x(col.year), y(col.life), color(col.country),
line, point)“Given gapminder Asia: x is year, y is life, color by country, lines and also points.”
Position decides which layers a channel reaches. color(country) is written before any mark above, so it belongs to the plot and colors both the lines and the points. Written after a mark it binds to that mark alone:
data(gapminder_asia) + x(year) + y(life) +
line + color(country) +
point + size(population)(data(gapminder_asia) + x(col.year) + y(col.life) +
line + color(col.country) +
point + size(col.population))data(gapminder_asia) + x(:year) + y(:life) + line + color(:country) +
point + size(:population)plot(data(gapminder_asia), x(col.year), y(col.life), line,
color(col.country), point, size(col.population))“Given gapminder Asia: x is year, y is life, lines, color by country, and also points, size by population.”
Channels bind forward, never backward. See Encoding scope.
23.2 size
With x and y spent on income and life expectancy, a third number such as population still needs a place on the plot.
size(field) scales point radius linearly from 3px (min) to 12px (max). A size legend shows three representative circles.
data(gapminder_2007) + point + x(gdp) + y(life) +
size(population)(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
size(col.population))data(gapminder_2007) + point + x(:gdp) + y(:life) + size(:population)plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
size(col.population))Combine color and size for a classic bubble chart:
data(gapminder_2007) + point + x(gdp) + y(life) +
color(continent) + size(population)(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
color(col.continent) + size(col.population))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
size(:population)plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.continent), size(col.population))23.3 shape
shape(field) maps a categorical column to one of five glyphs: circle, square, triangle, diamond, cross. This is what separates groups in a figure printed in black and white. Five is the whole set. Do not map shape to a column with more categories: the sixth starts over at the circle, and two categories then share a glyph. The Shape section of the point chapter lists the ways past five.
data(gapminder_2007) + point + x(gdp) + y(life) +
shape(continent)(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
shape(col.continent))data(gapminder_2007) + point + x(:gdp) + y(:life) + shape(:continent)plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
shape(col.continent))“Given gapminder 2007: points, x is gdp, y is life, shape by continent.”
color and shape can be bound to the same column. Saying one column twice is redundant encoding, and it makes the plot easier to read:
data(gapminder_2007) + point + x(gdp) + y(life) +
color(continent) + shape(continent)(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
color(col.continent) + shape(col.continent))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
shape(:continent)plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.continent), shape(col.continent))23.4 pattern
pattern(field) does for textures what shape does for glyphs: it maps a categorical column to a texture. The mark’s geometry decides which one: a hatch on the fills (bar, box, area, ribbon, zone), a dash on the strokes (line, step, interval, path, rule). Textures read without hue, so a plot mapped by pattern stays legible in grayscale or with color blindness. The first category is always the plain one (solid), the rest textured:
| 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) +
pattern(species) + y_label("Mean petal length (cm)")(data(iris_flowers) + bar * mean + x(col.species) + y(col.petal_length) +
pattern(col.species) + y_label("Mean petal length (cm)"))data(iris_flowers) + bar * mean + x(:species) + y(:petal_length) +
pattern(:species) + y_label("Mean petal length (cm)")plot(data(iris_flowers), layer(bar, mean), x(col.species),
y(col.petal_length), pattern(col.species),
y_label("Mean petal length (cm)"))“Given the iris flowers: bars derived by mean, x is species, y is petal length, pattern by species.”
The point of the channel is redundant encoding: the same column on color and pattern, so a reader separates the series by hue and by texture at once. gog draws these as one merged legend, not two:
| quarter | channel | revenue |
|---|---|---|
| Q1 | Retail | 4 |
| Q1 | Online | 3 |
| Q2 | Retail | 5 |
| Q2 | Online | 6 |
| Q3 | Retail | 4 |
data(channel_sales) + bar * sum * dodge + x(quarter) + y(revenue) + color(channel) +
pattern(channel) + title("Grouped bars, separated by hue and hatch")(data(channel_sales) + bar * sum * dodge + x(col.quarter) + y(col.revenue) + color(col.channel) +
pattern(col.channel) + title("Grouped bars, separated by hue and hatch"))data(channel_sales) + bar * sum * dodge + x(:quarter) + y(:revenue) +
color(:channel) + pattern(:channel) +
title("Grouped bars, separated by hue and hatch")plot(data(channel_sales), layer(bar, sum, dodge), x(col.quarter),
y(col.revenue), color(col.channel), pattern(col.channel),
title("Grouped bars, separated by hue and hatch"))“Given the channel sales table: bars derived by sum and dodge, x is quarter, y is revenue, color by channel, pattern by channel.”
On a stroke the same channel dashes each series instead, so these two lines are separated by hue and by dash:
| month | team | sales |
|---|---|---|
| 1 | North | 2 |
| 2 | North | 3 |
| 3 | North | 4 |
| 4 | North | 5 |
| 5 | North | 6 |
data(team_trend) + line + x(month) + y(sales) + color(team) + pattern(team) +
title("Two series, by hue and dash")(data(team_trend) + line + x(col.month) + y(col.sales) + color(col.team) + pattern(col.team) +
title("Two series, by hue and dash"))data(team_trend) + line + x(:month) + y(:sales) + color(:team) +
pattern(:team) + title("Two series, by hue and dash")plot(data(team_trend), line, x(col.month), y(col.sales), color(col.team),
pattern(col.team), title("Two series, by hue and dash"))The texture palette is deliberately small: four hatches beside solid on a fill, and two dashes beside solid on a stroke. So pattern suits a few categories rather than dozens, and past that count the textures repeat. To fix one texture for a whole layer instead of mapping a column, use style(pattern = ).
23.5 opacity
Sometimes a number should decide how faint a mark is drawn, so that small values fade and large ones stay solid.
opacity(field) maps a continuous column to transparency. Use it when marks overlap, so the crowded regions look darker than the empty ones.
data(gapminder_2007) + point + x(gdp) + y(life) + opacity(population) +
title("Opacity: larger populations drawn more solidly")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) + opacity(col.population) +
title("Opacity: larger populations drawn more solidly"))data(gapminder_2007) + point + x(:gdp) + y(:life) + opacity(:population) +
title("Opacity: larger populations drawn more solidly")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
opacity(col.population),
title("Opacity: larger populations drawn more solidly"))“Given gapminder 2007: points, x is gdp, y is life, opacity by population.”
Mapped values run from 0.15 to 0.95 rather than 0 to 1: a fully transparent mark is invisible, which is never what a reader wants. Marks with no opacity binding are drawn at 0.82, unless style(opacity = ) sets one.
Like size, opacity produces a legend showing the minimum, midpoint, and maximum of the column.
On line, opacity cannot be mapped: a stroke has one opacity, and one value cannot carry a column. This is the same rule that refuses a per-row size on a line, not a special case for opacity:
data(gapminder_asia) + line + x(year) + y(life) + opacity(population)data(gapminder_asia) + line + x(col.year) + y(col.life) + opacity(col.population)data(gapminder_asia) + line + x(:year) + y(:life) + opacity(:population)plot(data(gapminder_asia), line, x(col.year), y(col.life),
opacity(col.population))Error:
! gog: `opacity` cannot be bound to `line` — a line has no opacity feature. Remove the `population` 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.
That one stroke still has an opacity, so you can set it for the whole line:
data(gapminder_asia) + line + x(year) + y(life) + group(country) +
style(opacity = 0.4)(data(gapminder_asia) + line + x(col.year) + y(col.life) + group(col.country) +
style(opacity = 0.4))data(gapminder_asia) + line + x(:year) + y(:life) + group(:country) +
style(opacity = 0.4)plot(data(gapminder_asia), line, x(col.year), y(col.life),
group(col.country), style({ opacity: 0.4 }))“Given gapminder Asia: lines, x is year, y is life, grouped by country, with opacity 0.4.”
To distinguish lines by data, use color or group.
color behaves differently from size and opacity here. The difference looks like an exception and is not, so it is worth stating exactly. A width, an opacity and a dash are properties of the whole stroke: ask where along the line the width is 3px and the question has no answer. A color is not like that. You can point at one place on a line and ask what color it is, so a measure can vary along it, and color on a stroke mark accepts either kind of column:
data(gapminder_asia) + line + x(year) + y(life) + group(country) +
color(gdp, scale = "log") + palette("viridis") +
y_label("Life expectancy") + title("One line per country, colored by income")(data(gapminder_asia) + line + x(col.year) + y(col.life) + group(col.country) +
color(col.gdp, scale = "log") + palette("viridis") +
y_label("Life expectancy") + title("One line per country, colored by income"))data(gapminder_asia) + line + x(:year) + y(:life) + group(:country) +
color(:gdp, scale = "log") + palette("viridis") +
y_label("Life expectancy") +
title("One line per country, colored by income")plot(data(gapminder_asia), line, x(col.year), y(col.life),
group(col.country), color(col.gdp, { scale: "log" }),
palette("viridis"), y_label("Life expectancy"),
title("One line per country, colored by income"))“Given gapminder Asia: lines, x is year, y is life, grouped by country, color by gdp on a log scale, with the viridis palette.”
A category on color splits the mark into one stroke per group. A measure varies the color along each stroke and earns the ramp legend instead of a swatch per series. One channel, one mark, two behaviors, and the column’s type decides which one you get. group still splits the mark either way, which is why the sentence above draws one line per country and runs the ramp along each line. See Setting vs mapping.
23.6 group
group(field) connects line segments within the same group without adding a color distinction. Use it when you want a single color for all lines but still need them separated by category.
data(gapminder_asia) + line + x(year) + y(life) +
group(country)(data(gapminder_asia) + line + x(col.year) + y(col.life) +
group(col.country))data(gapminder_asia) + line + x(:year) + y(:life) + group(:country)plot(data(gapminder_asia), line, x(col.year), y(col.life),
group(col.country))When you want color and grouping, use color(field) alone: it implies grouping automatically.
23.7 Multiple channels
Two positions and one refining channel carry three columns. A table usually has a fourth you want on the page.
Channels compose freely, so you can bind several of them on one mark:
# Color + size + shape on the same plot
data(gapminder_2007) + point + x(gdp) + y(life) +
color(continent) + size(population) + shape(continent) +
title("GDP and life expectancy, 2007")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
color(col.continent) + size(col.population) + shape(col.continent) +
title("GDP and life expectancy, 2007"))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
size(:population) + shape(:continent) +
title("GDP and life expectancy, 2007")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.continent), size(col.population), shape(col.continent),
title("GDP and life expectancy, 2007"))23.8 Palettes
palette() chooses the colors the color channel assigns. There is no separate atom for ramps, which ggplot2 calls gradients. One word covers both, because the column decides. A categorical column reads a set of colors as one color per category. A continuous column reads the same set as stops, the fixed colors a ramp passes through.
There are three kinds of named palette, and they say three different things: a categorical palette gives each category its own color, a sequential ramp runs from low values to high, and a diverging ramp measures distance from a center in either direction. The type of the column decides which of the three kinds you may name, so the sections below follow that order.
gog uses a 20-color default palette, so most tables never use them all. When a column has more categories than that, gog builds the extra colors from the HSL color wheel, and no two categories share a color.
23.8.1 Categorical palettes
| Name | Colors | Best for |
|---|---|---|
"gog" |
20 (default) | General use |
"okabe" |
8 base + 8 light variants | Colorblind accessibility |
"soft" |
8, muted | Filled marks: bars, areas, zones |
"okabe" is Okabe and Ito’s set (Okabe & Ito, 2008), chosen so that its eight colors stay distinct for readers with the common forms of color blindness:
data(gapminder_2007) + point + x(gdp) + y(life) +
color(continent) + palette("okabe")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
color(col.continent) + palette("okabe"))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
palette("okabe")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.continent), palette("okabe"))The default is tuned for points and lines, where a color occupies a few pixels and must stay visible from across the page. A fill is large, so it is already easy to see, and the saturation that makes a small point findable is too strong on a bar. A row of saturated bars draws more attention than the numbers the bars report. soft is the same eight colors at lower saturation. Compare these two, which differ only in the palette:
data(gapminder_2007) + bar * count + x(continent) + color(continent) +
title("The default, over filled bars")(data(gapminder_2007) + bar * count + x(col.continent) + color(col.continent) +
title("The default, over filled bars"))data(gapminder_2007) + bar * count + x(:continent) + color(:continent) +
title("The default, over filled bars")plot(data(gapminder_2007), layer(bar, count), x(col.continent),
color(col.continent), title("The default, over filled bars"))“Given gapminder 2007: bars derived by count, x is continent, color by continent.”
data(gapminder_2007) + bar * count + x(continent) + color(continent) +
palette("soft") + title("palette(\"soft\")")(data(gapminder_2007) + bar * count + x(col.continent) + color(col.continent) +
palette("soft") + title("palette(\"soft\")"))data(gapminder_2007) + bar * count + x(:continent) + color(:continent) +
palette("soft") + title("palette(\"soft\")")plot(data(gapminder_2007), layer(bar, count), x(col.continent),
color(col.continent), palette("soft"), title("palette(\"soft\")"))Nothing is wrong with the first one, and on a scatter of the same data it is the better choice. The difference is what the color is doing: a point has to be findable, while a bar is already visible and only has to differ from its neighbors.
23.8.2 Ramps
For a continuous color column, palette() names a ramp instead. These run one way, from low values to high, and lightness is what carries the ordering.
| Name | What it is | Use it when |
|---|---|---|
"blue" |
the default: one hue, light→dark | almost always |
"viridis" |
many hues, with equal color steps for equal value steps | dense data where fine levels must stay distinct |
"magma" |
viridis’s family: black through purple to cream | a dark plot with strong contrast |
"inferno" |
the same family, hotter | as above, warmer |
"plasma" |
the same family, blue through magenta to yellow | as above, cooler at the low end |
"cividis" |
the family’s colorblind-optimized member | the ramp itself has to survive two-color vision (dichromacy) |
"gray" |
no hue at all: light gray to near-black | figures that will be printed in black and white |
A table is a poor way to choose a ramp. Here are six of the seven, all but the default blue, on the Maunga Whau crater. The crater is cut into cells and colored by mean height, the dense continuous surface a ramp is picked for. palette() is a property of the plot, so six ramps means six plots, and | and / put them on one page:
| east | north | elevation |
|---|---|---|
| 20 | 20 | 100 |
| 20 | 40 | 102 |
| 20 | 60 | 104 |
| 20 | 80 | 105 |
| 20 | 100 | 107 |
((data(maunga_whau) + zone * bin(16) * mean + x(east) + y(north) +
color(elevation) + palette("viridis") + title("viridis")) |
(data(maunga_whau) + zone * bin(16) * mean + x(east) + y(north) +
color(elevation) + palette("magma") + title("magma"))) /
((data(maunga_whau) + zone * bin(16) * mean + x(east) + y(north) +
color(elevation) + palette("inferno") + title("inferno")) |
(data(maunga_whau) + zone * bin(16) * mean + x(east) + y(north) +
color(elevation) + palette("plasma") + title("plasma"))) /
((data(maunga_whau) + zone * bin(16) * mean + x(east) + y(north) +
color(elevation) + palette("cividis") + title("cividis")) |
(data(maunga_whau) + zone * bin(16) * mean + x(east) + y(north) +
color(elevation) + palette("gray") + title("gray")))(((data(maunga_whau) + zone * bin(16) * mean + x(col.east) + y(col.north) +
color(col.elevation) + palette("viridis") + title("viridis")) |
(data(maunga_whau) + zone * bin(16) * mean + x(col.east) + y(col.north) +
color(col.elevation) + palette("magma") + title("magma"))) /
((data(maunga_whau) + zone * bin(16) * mean + x(col.east) + y(col.north) +
color(col.elevation) + palette("inferno") + title("inferno")) |
(data(maunga_whau) + zone * bin(16) * mean + x(col.east) + y(col.north) +
color(col.elevation) + palette("plasma") + title("plasma"))) /
((data(maunga_whau) + zone * bin(16) * mean + x(col.east) + y(col.north) +
color(col.elevation) + palette("cividis") + title("cividis")) |
(data(maunga_whau) + zone * bin(16) * mean + x(col.east) + y(col.north) +
color(col.elevation) + palette("gray") + title("gray"))))((data(maunga_whau) + zone * bin(16) * mean + x(:east) + y(:north) +
color(:elevation) + palette("viridis") + title("viridis")) |
(data(maunga_whau) + zone * bin(16) * mean + x(:east) + y(:north) +
color(:elevation) + palette("magma") + title("magma"))) /
((data(maunga_whau) + zone * bin(16) * mean + x(:east) + y(:north) +
color(:elevation) + palette("inferno") + title("inferno")) |
(data(maunga_whau) + zone * bin(16) * mean + x(:east) + y(:north) +
color(:elevation) + palette("plasma") + title("plasma"))) /
((data(maunga_whau) + zone * bin(16) * mean + x(:east) + y(:north) +
color(:elevation) + palette("cividis") + title("cividis")) |
(data(maunga_whau) + zone * bin(16) * mean + x(:east) + y(:north) +
color(:elevation) + palette("gray") + title("gray")))below(below(beside(plot(data(maunga_whau), layer(zone, bin(16), mean),
x(col.east), y(col.north), color(col.elevation), palette("viridis"),
title("viridis")),
plot(data(maunga_whau), layer(zone, bin(16), mean), x(col.east),
y(col.north), color(col.elevation), palette("magma"), title("magma"))),
beside(plot(data(maunga_whau), layer(zone, bin(16), mean), x(col.east),
y(col.north), color(col.elevation), palette("inferno"),
title("inferno")),
plot(data(maunga_whau), layer(zone, bin(16), mean), x(col.east),
y(col.north), color(col.elevation), palette("plasma"),
title("plasma")))),
beside(plot(data(maunga_whau), layer(zone, bin(16), mean), x(col.east),
y(col.north), color(col.elevation), palette("cividis"),
title("cividis")),
plot(data(maunga_whau), layer(zone, bin(16), mean), x(col.east),
y(col.north), color(col.elevation), palette("gray"), title("gray"))))“Given Maunga Whau: zones derived by bin into 16 and mean, x is east, y is north, color by elevation, with the viridis palette, beside the same with magma; above the same pair with inferno and plasma; above the same pair with cividis and gray.”
That is the same sentence six times with one word changed. It is written out in full because a shortcut here would be R code, and every sentence in this book is drawn in four languages.
Four things are worth noticing in those six plots.
gray runs the other way. Its crater is dark at the summit while the other five are bright there. That is not a mistake in either: gog’s own ramps (gray and the default blue) run light to dark, because with one hue a darker color reads as a larger value, and that also holds after photocopying. The viridis family runs dark to light, because that is what those ramps are, and reversing them would give a ramp named magma that no longer looks like magma. The legend is what settles it in any given plot, and it is the reason a mapped channel always gets one: read the top label and you know which end holds the largest values.
Of the five imported ramps, magma and inferno end on a near-white. magma’s last stop holds barely any contrast against the panel, so the very top of the scale nearly disappears, which you can see in the pale flat summits above. A ramp with equal color steps ends pale by design, so that is not an oversight, and it is why blue is the default rather than one of these. blue and gray stop short of white, so their pale end stays visible.
cividis has the least contrast, and deliberately: it uses only the two hues that survive the common forms of color blindness. Less contrast is what that costs, and it is worth accepting whenever the ramp itself has to stay readable.
gray is for the figure that has to survive photocopying. It is a ramp only, and there is no categorical gray palette. Readers cannot tell more than about four grays apart, and the grammar has a better answer for categories in a print figure. That answer is pattern, which separates them without spending color at all, and asking for palette("gray") on a categorical column says so:
data(gapminder_2007) + point + x(gdp) + y(life) +
color(continent) + palette("gray")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
color(col.continent) + palette("gray"))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
palette("gray")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.continent), palette("gray"))Error:
! gog: `palette("gray")` is a sequential ramp for numbers, but `color` is bound to a text column. Use a categorical palette — `gog`, `okabe`, or `soft` — or list one color per category. For categories in a figure that has to print in black and white, reach for `pattern(<column>)` instead — it tells them apart without spending color.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
23.8.3 Diverging ramps
A sequential ramp means more. A diverging ramp means away from a center, in two directions, which is what a change, a residual, a correlation and an anomaly all are. gog provides two:
| Name | What it is | Use it when |
|---|---|---|
"blue_red" |
blue → neutral → red | the usual choice |
"brown_teal" |
brown → neutral → teal | the sign of the value is what matters, and readers may be colorblind |
The name reads low to high, so blue_red paints the smallest values blue and the largest red. Both turn through the same gray, and that gray marks the center of the scale.
Which value gets the neutral is not a property of the palette. It is the middle of the scale’s domain, so limits is what puts zero in the middle. Without limits, the middle falls on the data’s own midpoint. Here the cash flow runs from -32 to 120, so the midpoint is 44: that is what gets painted gray, and zero comes out blue.
| step | delta | total |
|---|---|---|
| Opening | 120 | TRUE |
| Sales | 45 | FALSE |
| Refunds | -18 | FALSE |
| Costs | -32 | FALSE |
| Tax | -14 | FALSE |
data(cashflow) + point + x(step) + y(delta) +
color(delta) + palette("blue_red") + style(size = 9) +
title("The neutral lands at 44, not at 0")(data(cashflow) + point + x(col.step) + y(col.delta) +
color(col.delta) + palette("blue_red") + style(size = 9) +
title("The neutral lands at 44, not at 0"))data(cashflow) + point + x(:step) + y(:delta) + color(:delta) +
palette("blue_red") + style(size = 9) +
title("The neutral lands at 44, not at 0")plot(data(cashflow), point, x(col.step), y(col.delta), color(col.delta),
palette("blue_red"), style({ size: 9 }),
title("The neutral lands at 44, not at 0"))State a symmetric domain and the neutral lands on zero. Read the “Closing” point, which is exactly zero:
data(cashflow) + point + x(step) + y(delta) +
color(delta, limits = c(-120, 120)) + palette("blue_red") + style(size = 9) +
title("limits = c(-120, 120) puts zero on the neutral")(data(cashflow) + point + x(col.step) + y(col.delta) +
color(col.delta, limits = [-120, 120]) + palette("blue_red") + style(size = 9) +
title("limits = c(-120, 120) puts zero on the neutral"))data(cashflow) + point + x(:step) + y(:delta) +
color(:delta, limits = [-120, 120]) + palette("blue_red") +
style(size = 9) +
title("limits = c(-120, 120) puts zero on the neutral")plot(data(cashflow), point, x(col.step), y(col.delta),
color(col.delta, { limits: [-120, 120] }), palette("blue_red"),
style({ size: 9 }),
title("limits = c(-120, 120) puts zero on the neutral"))“Given the cashflow table: points, x is step, y is delta, color by delta from -120 to 120, with the blue red palette.”
Two things follow from doing it this way. The ramp stays linear: equal distance in color is equal distance in the data on both sides of the center. A scale that fixes a midpoint and then rescales each side to the data’s own ends loses that. And limits removes rows outside it, so the domain has to be wide enough to hold the data: c(-50, 50) here would drop the opening balance and say so. Symmetric limits have one drawback, worth knowing before you set them. If a signed column runs from -2 to 100, almost none of the blue half is used, because the data reaches only a little way below zero.
The colors you pass become the stops, and gog interpolates between them:
data(gapminder_2007) + point + x(gdp) + y(life) +
color(life) + palette(c("white", "navy")) + style(size = 6) +
title("Custom stops")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
color(col.life) + palette(["white", "navy"]) + style(size = 6) +
title("Custom stops"))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:life) +
palette(["white", "navy"]) + style(size = 6) + title("Custom stops")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.life), palette(["white", "navy"]), style({ size: 6 }),
title("Custom stops"))“Given gapminder 2007: points, x is gdp, y is life, color by life, with a palette of white and navy.”
Asking for a palette of the wrong kind is refused rather than guessed at. A categorical palette cannot serve a continuous column:
data(gapminder_2007) + point + x(gdp) + y(life) +
color(life) + palette("okabe")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
color(col.life) + palette("okabe"))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:life) +
palette("okabe")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.life), palette("okabe"))Error:
! gog: `palette("okabe")` hands out one color per category, but `color` is bound to a numeric column. Use a ramp — `blue`, `viridis`, `magma`, `inferno`, `plasma`, `cividis`, or `gray` run one way, `blue_red` or `brown_teal` diverge from a center — or give your own stops, e.g. `palette(c("white", "navy"))`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
No ramp survives a skewed column, and swapping palettes will not help. A linear ramp gives most of its colors to the range where most rows sit. Color population and nearly every country falls at the light end, because two countries are several times larger than any other. That is the data, not the palette, so the fix is a log scale on the channel:
data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
color(population, scale = "log") +
x_label("GDP per capita")(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
color(col.population, scale = "log") +
x_label("GDP per capita"))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
color(:population, scale = "log") + x_label("GDP per capita")plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
y(col.life), color(col.population, { scale: "log" }),
x_label("GDP per capita"))color, size and opacity all take scale =, because all three answer “how far along?”. shape, pattern and group do not: they answer “which one?”.
23.8.4 Custom colors
Often the colors are decided before you begin: a journal asks for them, or an earlier figure already used them.
Pass the colors yourself: hex values, CSS color names such as firebrick, or a mix of the two:
data(gapminder_2007) + point + x(gdp) + y(life) +
color(continent) +
palette(c("#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00"))(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
color(col.continent) +
palette(["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00"]))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
palette(["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00"])plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.continent),
palette(["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00"]))data(gapminder_2007) + point + x(gdp) + y(life) +
color(continent) +
palette(c("firebrick", "steelblue", "seagreen", "rebeccapurple", "darkorange"))(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
color(col.continent) +
palette(["firebrick", "steelblue", "seagreen", "rebeccapurple", "darkorange"]))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
palette(["firebrick", "steelblue", "seagreen", "rebeccapurple", "darkorange"])plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.continent),
palette(["firebrick", "steelblue", "seagreen", "rebeccapurple",
"darkorange"]))A color gog cannot recognize is refused with the nearest match offered, rather than handed to the renderer to be painted black:
data(gapminder_2007) + point + x(gdp) + y(life) +
color(continent) + palette(c("firebrick", "stelblue"))(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
color(col.continent) + palette(["firebrick", "stelblue"]))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
palette(["firebrick", "stelblue"])plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.continent), palette(["firebrick", "stelblue"]))Error:
! gog: `palette()` entry "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.
Colors are assigned in first-appearance order in the data (the first unique value gets the first palette color, the second unique value gets the second, and so on), so the same table always gets the same colors. (Unless the column declares its order; then that order sets the sequence.)
One mistake to know about: a single color is not a palette. palette() takes a palette name or the colors themselves, and palette("red") is neither; it is an attempt to set one constant color, which is style(color = "red"):
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.
23.9 Legend placement
gog places every legend to the right of the plot area. The plot area shrinks by itself to make room, so there is no margin to set, no position to choose, and no way to hide a legend.