| 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 values beyond that limit are outliers, drawn individually as dots. bar * mean gives only one number per group, while box shows how the whole group is spread. An interval needs a transform to supply its low and high values, while a box computes its whole summary itself. Boxes are vertical when the categories are on x and horizontal when they are on y; see Horizontal boxes.
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: it decides where ordinary values stop and unusual ones begin.
There is one difference from his book. Tukey’s box spans hinges. Sort the values and cut the sorted list in half; when the count is odd, the middle value belongs to both halves. Take the median of each half: those two medians are the hinges. gog spans the lower and upper quartiles instead, as most software does. The two agree when a group has an odd number of values, and can differ slightly when the number is even. 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 categorical column, species, on x, and put the measured column, petal_length, 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 is built 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 the box is the median. Each whisker runs from the box to the most extreme value inside the fence. The separate dots are the outliers: flowers more than 1.5 times the 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, and you read each one exactly as you read the species boxes. In this plot each outlier is one country. This is life expectancy across the continents in 2007, five distributions in one panel:
| 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"))Africa’s box is the tallest, so the middle half of its countries is the most spread out. The low dot below Asia is Afghanistan, and the one below the Americas is Haiti. Each is far enough below its continent’s box that no whisker reaches it. That is what Tukey’s construction is for: the box and whiskers cover the ordinary values, and the dots mark the unusual ones.
One more case can occur in your own data. 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. Oceania’s 1957 box in Coloring the boxes is almost this case: its two countries differ by 0.07 years, so the box draws as a flat line.
14.3 Horizontal boxes
When the category names are long, or there are many of them, the boxes should be horizontal. Swap the two positions, and the plot is drawn with its axes exchanged. If you know ggplot2’s coord_flip(), there is no such word here, because it would be a second way to say one thing. gog reads the two bindings and decides from them which axis carries the numbers, exactly as bar does.
data(gapminder_2007) + box + x(life) + y(continent) +
x_label("Life expectancy (years)") + y_label("Continent") +
title("The same five distributions, horizontal")(data(gapminder_2007) + box + x(col.life) + y(col.continent) +
x_label("Life expectancy (years)") + y_label("Continent") +
title("The same five distributions, horizontal"))data(gapminder_2007) + box + x(:life) + y(:continent) +
x_label("Life expectancy (years)") + y_label("Continent") +
title("The same five distributions, horizontal")plot(data(gapminder_2007), box, x(col.life), y(col.continent),
x_label("Life expectancy (years)"), y_label("Continent"),
title("The same five distributions, horizontal"))Compare it with the vertical version in Distribution across many groups. Every box, whisker and outlier dot keeps its place relative to its neighbors. Only the two axes have changed places. Afghanistan and Haiti are still the two dots beyond their continents’ whiskers. Everything later in this chapter works the same on either axis: the whisker rule, the color split, dodge and style().
The swap matters most when the names are long. The tenure table is invented: five teams, thirty people in each, and the years each person has been at the company. With the categories on x, each name would have to fit into a slot one box wide. With the categories on y, as below, each name is written in full on one line:
| team | years |
|---|---|
| Research & Development | 6.7 |
| Research & Development | 4.9 |
| Research & Development | 8.3 |
| Research & Development | 7.6 |
| Research & Development | 10.1 |
data(tenure) + box + x(years) + y(team) +
x_label("Years at the company") +
title("Team names fit on a vertical axis")(data(tenure) + box + x(col.years) + y(col.team) +
x_label("Years at the company") +
title("Team names fit on a vertical axis"))data(tenure) + box + x(:years) + y(:team) +
x_label("Years at the company") +
title("Team names fit on a vertical axis")plot(data(tenure), box, x(col.years), y(col.team),
x_label("Years at the company"),
title("Team names fit on a vertical axis"))A box needs a continuous column 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 drawn separately. You want the whiskers to run to the true minimum and maximum. The box then shows the five-number summary: the minimum, the lower quartile, the median, the upper quartile and the maximum. The mark takes one parameter for this, a named option inside its parentheses: 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, and no value is drawn as an outlier. The default is "tukey": fences and outliers, the standard box plot. "range" draws the whole five-number summary. whiskers changes only how the summary is computed; the settings that change how a box looks are in What you can set.
14.5 Why box needs no transform
An interval cannot draw without a transform to give it a low and a high. A box never needs one. An interval is only a shape, a span from a low value to a high one. Which low and high it spans is a real choice: a min–max range, a confidence interval, or a pre-computed pair. A box is a shape with one fixed statistical meaning: the summary is what a box plot means. A statistic that is always drawn by the same mark belongs inside that mark, not in a separate transform. So the whole box is box + x + y. Adding any transform other than dodge is refused, and the message says what to write instead, as What it refuses shows.
The same principle keeps the channel vocabulary small: something that only one mark can use belongs inside that mark, not in the shared vocabulary. Taking ggplot2’s ymin, lower, middle, upper and ymax as channels would add five channels meaningful to a single mark. Instead the summary is computed inside the mark, and the channel vocabulary does not grow.
The table below sets the four marks side by side. Three of them read a low and a high value from a transform, and box computes its own summary:
| Mark | The spread it draws |
|---|---|
interval * range |
a whisker from the low value to the high |
ribbon * range |
the band between the two values, filled |
line * range |
the two values as two separate strokes |
box |
quartiles, median and whiskers, with no transform to write |
box is also one of several marks that draw a distribution, and each answers a different question. bar * bin draws one variable’s histogram, and interval draws a range or an error bar. box gives the summary per group. The violin (ribbon * density) draws an estimate of the whole distribution that summary is computed from. See Transforms for how mean, median and range differ from the box’s own summary.
14.6 Coloring the boxes
The three species boxes in A summary per group are all one color, so only the axis labels identify each box. A color per species lets a reader match each box to the same species in another plot. A color also splits each slot by a second category, such as an era. color gives each box its own hue, and adds one legend that names the hues. It is the same categorical split that bar and interval make. Here color is mapped to the same column as x, so 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 category on x into one box per color. Without dodge those boxes are drawn at the same 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.”
In every continent, the 2007 box is entirely above the 1957 box.
On a box, size and opacity are settings rather than channels. color can split one group into several boxes, but size and opacity cannot. A single box is one shape, so it cannot show two sizes or two opacities inside itself. style(size = ) sets the width of every line a box draws, for the whole layer. That covers the outline, the whiskers and the caps (the short lines across the whisker ends). The median is one pixel thicker.
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, stripes, grid, dots |
style(border_color = ) |
any CSS color name or hex |
style(border_size = ) |
pixels |
style(caps = ) |
true or false |
style(gap = ) |
a share of the mark’s place, from 0 to just under 1 |
border_color and border_size are useful on a box in particular, because a box draws lines and a fill. The lines are the outline, the whiskers, the caps and the median. border_color gives them a color of their own; without it, they take the fill’s color, so the median barely shows against the fill. A pale fill with a dark border keeps the median easy to see. border_size sets the same width as size, and when both are given, the box uses border_size. style(caps = FALSE) draws the whiskers without caps, as on an interval:
data(gapminder_2007) + box + x(continent) + y(life) + style(caps = FALSE) +
title("Whiskers without caps")(data(gapminder_2007) + box + x(col.continent) + y(col.life) + style(caps = False) +
title("Whiskers without caps"))data(gapminder_2007) + box + x(:continent) + y(:life) +
style(caps = false) + title("Whiskers without caps")plot(data(gapminder_2007), box, x(col.continent), y(col.life),
style({ caps: false }), title("Whiskers without caps"))“Given gapminder 2007: boxes, x is continent, y is life, with caps off.”
gap sets how much of a box’s slot is left empty. Unless you set it, a box takes about half of its slot, so it is narrower than a bar. style(gap = 0.2) leaves a fifth of the slot empty, as a bar does by default, so each box is as wide as a bar:
data(gapminder_2007) + box + x(continent) + y(life) + style(gap = 0.2) +
title("Boxes as wide as bars")(data(gapminder_2007) + box + x(col.continent) + y(col.life) + style(gap = 0.2) +
title("Boxes as wide as bars"))data(gapminder_2007) + box + x(:continent) + y(:life) + style(gap = 0.2) +
title("Boxes as wide as bars")plot(data(gapminder_2007), box, x(col.continent), y(col.life),
style({ gap: 0.2 }), title("Boxes as wide as bars"))“Given gapminder 2007: boxes, x is continent, y is life, with gap 0.2.”
The boxes and their caps are wider. The quartiles, medians and whiskers mark the same values.
A box is also the one mark in the kernel that takes a parameter. The other parameters you have met belong to a transform, such as bin(width = ), or to a position, such as x(gdp, scale = "log"). A box computes its own summary, so whiskers, which changes how that summary is computed, belongs to the mark. The plot below shows both, whiskers to the range and a pale fill with a navy border. It reads gm_continents: the Americas, Asia and Europe, measured every five years from 1952 to 2007. That is twelve years in each box, so a box here is drawn from twelve times as many values as one drawn from gapminder 2007:
| 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"))“Given the gapminder continents: boxes with whiskers to the range, x is continent, y is life, colored lightsteelblue, with border color navy and border size 1.2.”
Both values of whiskers still draw a box. whiskers changes the rule the summary follows, not the shape the summary is drawn as.
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
Each of the four refusals below has a different reason. The first two concern how a box is drawn, and the last two concern what it summarizes. A dashed pattern is the first. A box’s pattern textures its fill while its lines stay solid, so it takes a fill’s texture and not a stroke’s dash. "hatch", "crosshatch", "stripes", "grid" and "dots" are accepted, and "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", "stripes", "grid", "dots".
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
A mapped size is the second. No column can vary a box’s size. The box’s width comes from its slot and gap. The width of the lines it draws is a setting for the whole layer:
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 takes one size for the whole layer, not one per row. Drop the `life` mapping and set it with `style(size = )`, or use a mark that maps `size`: `point` or `text`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
Two categorical axes are the third. gog reads a box’s two bindings to find the axis that carries the numbers, 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 statistic is the fourth. A box already summarizes each group into quartiles, a median and whiskers, so the only transform it takes is dodge, which computes nothing. 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: 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.
14.9 In short
The summary is inside the mark: a box refuses every statistic, and takes only dodge, which moves boxes and computes nothing. whiskers is its one parameter. A box’s width comes from its slot and gap, so no column can vary it. gog’s box spans the quartiles, and Tukey’s spans hinges, so on a group with an even number of values the two can differ slightly.
14.10 What comes next
A box occupies one position on its axis, and its width carries no value from the table. Ribbon fills the band between a low value and a high one at every position along the axis.