| 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 |
14 Box
How is each group spread out? box draws the box-and-whisker of a distribution, one per group. In each category it summarizes the measured column. A box spans the lower quartile to the upper, with a line at the median. Whiskers reach to the most extreme values within 1.5 times the interquartile range (IQR) of the box. Any points beyond that are outliers, drawn individually as dots. bar * mean gives one number per group. box shows how the whole group is spread, and draws the few values far from the rest as separate dots. Boxes stand up by default and lie down when the categories are on y; see Boxes on their side.
The construction is John Tukey’s, from Exploratory Data Analysis (Tukey, 1977), where he called it a schematic plot. The name readers kept was his informal one, box-and-whisker, now usually shortened to box plot. Mary Eleanor Spear’s range bar of 1952 (Spear, 1952) had drawn the box and the extremes a quarter century earlier. The range bar had no rule for the fence, the cutoff that decides which values are outliers. That rule, 1.5 times the IQR, is Tukey’s, and so is the choice of where ordinary values stop and unusual ones begin.
One difference is worth knowing if you know his book. Tukey’s box spans hinges. Sort the values, cut the sorted list in half, and take the median of each half. gog spans the lower and upper quartiles instead, as most software does. The two agree on most samples and differ slightly on small ones. A box drawn by gog is therefore not always the box Tukey’s own rule gives.
14.1 A summary per group
How do the three species differ in petal length, not only on average but in how widely each varies? Put the category that splits the data on x, and put the measured column on y. The mark computes the summary itself:
data(iris_flowers) + box + x(species) + y(petal_length) +
x_label("Species") + y_label("Petal length (cm)") +
title("Distribution of petal length per species")(data(iris_flowers) + box + x(col.species) + y(col.petal_length) +
x_label("Species") + y_label("Petal length (cm)") +
title("Distribution of petal length per species"))data(iris_flowers) + box + x(:species) + y(:petal_length) +
x_label("Species") + y_label("Petal length (cm)") +
title("Distribution of petal length per species")plot(data(iris_flowers), box, x(col.species), y(col.petal_length),
x_label("Species"), y_label("Petal length (cm)"),
title("Distribution of petal length per species"))“Given the iris flowers: boxes, x is species, y is petal length.”
Every box reads the same way. Half the flowers of a species fall inside the box (the middle 50%, from the lower quartile to the upper); the line across it is the median; the whiskers run to the rest of the values inside the fence. The separate dots are the outliers: flowers more than 1.5 IQR from the box. The whisker stops before them, and each one is drawn on its own.
14.2 Distribution across many groups
Which continent varies the most, and which countries fall far below the rest of their own continent? Five boxes answer both at once. The comparison still works with more groups, and here the outliers name particular countries. This is life expectancy across the continents in 2007, five distributions in one panel, with two countries far below the rest of their continent:
| country | continent | year | life | population | gdp |
|---|---|---|---|---|---|
| Afghanistan | Asia | 2007 | 43.828 | 31889923 | 974.5803 |
| Albania | Europe | 2007 | 76.423 | 3600523 | 5937.0295 |
| Algeria | Africa | 2007 | 72.301 | 33333216 | 6223.3675 |
| Angola | Africa | 2007 | 42.731 | 12420476 | 4797.2313 |
| Argentina | Americas | 2007 | 75.320 | 40301927 | 12779.3796 |
data(gapminder_2007) + box + x(continent) + y(life) +
x_label("Continent") + y_label("Life expectancy (years)") +
title("Life expectancy by continent, 2007")(data(gapminder_2007) + box + x(col.continent) + y(col.life) +
x_label("Continent") + y_label("Life expectancy (years)") +
title("Life expectancy by continent, 2007"))data(gapminder_2007) + box + x(:continent) + y(:life) +
x_label("Continent") + y_label("Life expectancy (years)") +
title("Life expectancy by continent, 2007")plot(data(gapminder_2007), box, x(col.continent), y(col.life),
x_label("Continent"), y_label("Life expectancy (years)"),
title("Life expectancy by continent, 2007"))The low dot below Asia is Afghanistan, and the one below the Americas is Haiti. Each sits far enough below its continent’s box that no whisker reaches it. That is what the Tukey construction is for: the whiskers cover the values near the middle, and the dots are the values far from it. When every value in a group’s middle 50% is the same, the box has no height and draws as a flat line. gog does not give it a minimum height to make the spread look larger.
14.3 Boxes on their side
When the category names are long, or there are many of them, the boxes should lie down. Swap the two positions and the same sentence draws the same plot, turned. If you know ggplot2’s coord_flip(), there is no such word here, for the reason there is none on bar. gog reads the two bindings and decides from them which axis carries the numbers. A second way to say one thing is a second thing to learn.
data(gapminder_2007) + box + x(life) + y(continent) +
x_label("Life expectancy (years)") + y_label("Continent") +
title("The same five distributions, lying down")(data(gapminder_2007) + box + x(col.life) + y(col.continent) +
x_label("Life expectancy (years)") + y_label("Continent") +
title("The same five distributions, lying down"))data(gapminder_2007) + box + x(:life) + y(:continent) +
x_label("Life expectancy (years)") + y_label("Continent") +
title("The same five distributions, lying down")plot(data(gapminder_2007), box, x(col.life), y(col.continent),
x_label("Life expectancy (years)"), y_label("Continent"),
title("The same five distributions, lying down"))Compare it with the upright version above. Every box, whisker and outlier dot keeps its place relative to its neighbors, rotated a quarter turn. Afghanistan and Haiti are still the two dots beyond their continents’ whiskers. Everything the rest of this chapter adds behaves the same whichever axis holds the categories: the color split, boxes set side by side, the whisker rule and style().
Use it when the category names are long. The table below is invented: five teams, thirty people in each, and the years each person has been at the company. A vertical axis gives each name a whole line, while an upright plot has to fit it into a slot one box wide:
| team | years |
|---|---|
| Research & Development | 6.7 |
| Research & Development | 4.9 |
| Research & Development | 8.3 |
| Research & Development | 7.6 |
| Research & Development | 10.1 |
set.seed(4)
data(tenure) + box + x(years) + y(team) +
x_label("Years at the company") +
title("Team names fit on a vertical axis")A box needs a number to summarize, so two categorical axes are refused, exactly as they are on a bar. What it refuses shows that refusal.
14.4 Whiskers to the extremes
Sometimes you do not want outliers split off: you want the whiskers to run to the true minimum and maximum, the plain five-number summary. That is the one parameter the box mark carries, box(whiskers = "range"):
data(gapminder_2007) + box(whiskers = "range") + x(continent) + y(life) +
x_label("Continent") + y_label("Life expectancy (years)") +
title("The same, with whiskers to the full range")(data(gapminder_2007) + box(whiskers = "range") + x(col.continent) + y(col.life) +
x_label("Continent") + y_label("Life expectancy (years)") +
title("The same, with whiskers to the full range"))data(gapminder_2007) + box(whiskers = "range") + x(:continent) +
y(:life) + x_label("Continent") + y_label("Life expectancy (years)") +
title("The same, with whiskers to the full range")plot(data(gapminder_2007), box({ whiskers: "range" }), x(col.continent),
y(col.life), x_label("Continent"), y_label("Life expectancy (years)"),
title("The same, with whiskers to the full range"))“Given gapminder 2007: boxes with whiskers to the range, x is continent, y is life.”
Now Afghanistan and Haiti are the ends of their whiskers rather than dots. No point is called an outlier. The default is "tukey" (fences and outliers, the standard box plot); "range" is the whole five-number summary with nothing set apart. whiskers is the only choice a box offers.
14.5 Why box needs no transform
An interval cannot draw without a range transform to give it a low and a high; a box never needs one. interval is pure geometry (“span from here to there”) and which low and high it spans is a genuine choice (a min–max range, a confidence interval, a precomputed pair). A box is a named statistical glyph: the summary is what a box plot means. A statistic that draws as exactly one mark belongs to that mark, not beside it as a transform you compose. So the whole box is box + x + y, and adding a transform to it is refused with direction, as What it refuses shows.
This is the principle that keeps the channel vocabulary small, applied to a mark instead of a channel. Borrowing ymin/ymax/lower/middle/upper as channels would add five channels meaningful to a single mark; instead the summary is invented inside the mark, and the channel set never grows.
14.6 Coloring the boxes
Three boxes in one color read as one series. A color per species lets a reader match each box to the same species in another plot. A color is also how a second category, such as an era, shares each slot. color gives each box its own hue and a legend, the same categorical split bar and interval make. Mapped to the same column that sits on x, it colors one box per group:
data(iris_flowers) + box + x(species) + y(petal_length) + color(species) +
x_label("Species") + y_label("Petal length (cm)") +
title("The same distributions, colored by species")(data(iris_flowers) + box + x(col.species) + y(col.petal_length) + color(col.species) +
x_label("Species") + y_label("Petal length (cm)") +
title("The same distributions, colored by species"))data(iris_flowers) + box + x(:species) + y(:petal_length) +
color(:species) + x_label("Species") + y_label("Petal length (cm)") +
title("The same distributions, colored by species")plot(data(iris_flowers), box, x(col.species), y(col.petal_length),
color(col.species), x_label("Species"), y_label("Petal length (cm)"),
title("The same distributions, colored by species"))“Given the iris flowers: boxes, x is species, y is petal length, color by species.”
Mapping color to a second category splits each x-group into one box per color. Without dodge those boxes share one x and overlap, and their fills turn translucent so each stays readable. dodge sets them side by side, each continent’s two eras next to each other:
| country | continent | year | life | population | gdp | era |
|---|---|---|---|---|---|---|
| Afghanistan | Asia | 1957 | 30.332 | 9240934 | 820.8530 | 1957 |
| Afghanistan | Asia | 2007 | 43.828 | 31889923 | 974.5803 | 2007 |
| Albania | Europe | 1957 | 59.280 | 1476505 | 1942.2842 | 1957 |
| Albania | Europe | 2007 | 76.423 | 3600523 | 5937.0295 | 2007 |
| Algeria | Africa | 1957 | 45.685 | 10270856 | 3013.9760 | 1957 |
data(gm_eras) + box * dodge + x(continent) + y(life) + color(era) +
y_label("Life expectancy") +
title("Life expectancy per continent, 1957 vs 2007")(data(gm_eras) + box * dodge + x(col.continent) + y(col.life) + color(col.era) +
y_label("Life expectancy") +
title("Life expectancy per continent, 1957 vs 2007"))data(gm_eras) + box * dodge + x(:continent) + y(:life) + color(:era) +
y_label("Life expectancy") +
title("Life expectancy per continent, 1957 vs 2007")plot(data(gm_eras), layer(box, dodge), x(col.continent), y(col.life),
color(col.era), y_label("Life expectancy"),
title("Life expectancy per continent, 1957 vs 2007"))“Given the gapminder eras: boxes derived by dodge, x is continent, y is life, color by era.”
Stroke width and opacity stay settings. color and pattern can split one group into several boxes. size and opacity cannot: they would have to change inside a single box, and a box is one shape. style(size = ) sets the width of every line a box draws, for the whole layer: the outline, the whiskers, the median and the end caps.
box joins the other distribution marks. bar * bin draws one variable’s histogram, and interval draws a range or an error bar. box gives the summary per group, and the violin (ribbon * density) gives the whole shape behind that summary. See Transforms for dodge, and for how mean, median and range differ from the box’s own summary.
14.7 What you can set
A setting changes how every box looks without reading a column, and each mark takes its own. These are a box’s, with the values each accepts:
| Setting | Value |
|---|---|
style(color = ) |
any CSS color name or hex |
style(opacity = ) |
0 to 1 |
style(size = ) |
pixels |
style(pattern = ) |
solid, hatch, crosshatch, grid, dots |
style(border_color = ) |
any CSS color name or hex |
style(border_size = ) |
pixels |
A box is the one mark in the kernel that takes a parameter. The other parameters you have met belong to a transform (bin(bins = )), to a position (x(gdp, scale = "log")), or to style(). A box carries its summary inside itself, so the one choice that summary offers belongs to the mark. The plot below reads gm_continents: three continents, every year from 1952 to 2007. Each box therefore covers many more countries than the 2007 plots above:
| 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_continents) + box(whiskers = "range") + x(continent) + y(life) +
style(color = "lightsteelblue", border_color = "navy", border_size = 1.2) +
y_label("Life expectancy") + title("Whiskers to the true extremes, and a border")(data(gm_continents) + box(whiskers = "range") + x(col.continent) + y(col.life) +
style(color = "lightsteelblue", border_color = "navy", border_size = 1.2) +
y_label("Life expectancy") + title("Whiskers to the true extremes, and a border"))data(gm_continents) + box(whiskers = "range") + x(:continent) + y(:life) +
style(color = "lightsteelblue", border_color = "navy", border_size = 1.2) +
y_label("Life expectancy") +
title("Whiskers to the true extremes, and a border")plot(data(gm_continents), box({ whiskers: "range" }), x(col.continent),
y(col.life),
style({ color: "lightsteelblue", border_color: "navy",
border_size: 1.2 }), y_label("Life expectancy"),
title("Whiskers to the true extremes, and a border"))Both values of whiskers still draw a box, which is why it is a parameter and not a transform. It changes the rule the summary follows, not the shape the summary is drawn as.
border_color and border_size are worth using on a box specifically, because a box draws lines and a fill. Both settings apply to every line the box draws: the outline, the whiskers, the median and the caps. A pale fill with a dark edge stays legible at small sizes, and a solid fill does not.
The grid of every mark and every setting shows which other marks share this list. What a mark maps rather than sets is its row on the companion grid.
14.8 What it refuses
A box draws lines and a fill, so its pattern takes a fill’s texture and not a stroke’s dash. "hatch", "crosshatch", "grid" and "dots" are accepted; "dashed" is refused.
data(gapminder_2007) + box + x(continent) + y(life) + style(pattern = "dashed")data(gapminder_2007) + box + x(col.continent) + y(col.life) + style(pattern = "dashed")data(gapminder_2007) + box + x(:continent) + y(:life) +
style(pattern = "dashed")plot(data(gapminder_2007), box, x(col.continent), y(col.life),
style({ pattern: "dashed" }))Error:
! gog: `style(pattern = )` on a `box`: `"dashed"` is a stroke's dash, not a fill texture. Use "solid" (the default), "hatch", "crosshatch", "grid", or "dots".
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
A mapped size is the second. No row can vary a box’s size: the box’s width is the slot it sits in, and the line-work’s width is a layer setting:
data(gapminder_2007) + box + x(continent) + y(life) + size(life)data(gapminder_2007) + box + x(col.continent) + y(col.life) + size(col.life)data(gapminder_2007) + box + x(:continent) + y(:life) + size(:life)plot(data(gapminder_2007), box, x(col.continent), y(col.life),
size(col.life))Error:
! gog: `size` cannot be bound to `box` — a box has no size feature. Remove the `life` mapping from `size`, or use a mark that has one.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
Two categorical axes are the third. A box reads which axis carries the numbers from its two bindings, so one axis has to be continuous. Two categories leave it nothing to summarize, exactly as they do on a bar:
data(gapminder_2007) + box + x(continent) + y(continent)data(gapminder_2007) + box + x(col.continent) + y(col.continent)data(gapminder_2007) + box + x(:continent) + y(:continent)plot(data(gapminder_2007), box, x(col.continent), y(col.continent))Error:
! gog: `box` has categorical columns on both axes — `x(continent)` and `y(continent)` — so there is nothing for it to summarize. One axis must be a number: that is the column the five-number summary reduces.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
A transform is the fourth. A box already summarizes each group into quartiles, a median and whiskers, so it takes none. Why box needs no transform gave the reason:
data(iris_flowers) + box * mean + x(species) + y(petal_length)data(iris_flowers) + box * mean + x(col.species) + y(col.petal_length)data(iris_flowers) + box * mean + x(:species) + y(:petal_length)plot(data(iris_flowers), layer(box, mean), x(col.species),
y(col.petal_length))Error:
! gog: `box` already summarizes each group into a box — quartiles, median, whiskers — so it takes no transform, but `mean` was added. Drop it: `box + x(group) + y(value)` draws the box-and-whisker on its own.
gog: `box * mean * box` measures each cell twice — `mean` and `box` both reduce the column you named, and a cell holds one answer, so drawing both would mean drawing one of them and discarding the other. Keep whichever you meant: `box * mean` or `box * box`. To show both, draw them as two layers: `box * mean + box * box`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The message names the fix: drop the transform, and the box draws its own summary.