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:

iris_flowers: first 5 of 150 rows
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) + 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"))
setosa versicolor virginica 2 4 6 Distribution of petal length per species Petal length (cm) 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:

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
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"))
Asia Europe Africa Americas Oceania 40 50 60 70 80 Life expectancy by continent, 2007 Life expectancy (years) Continent

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"))
40 50 60 70 80 Oceania Americas Africa Europe Asia The same five distributions, horizontal Continent Life expectancy (years)

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:

tenure: first 5 of 150 rows
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"))
0 5 10 15 People & Culture Finance Customer Operations Sales & Marketing Research & Development Team names fit on a vertical axis Team Years at the company

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"))
Asia Europe Africa Americas Oceania 40 50 60 70 80 The same, with whiskers to the full range Life expectancy (years) Continent

“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"))
setosa versicolor virginica 2 4 6 The same distributions, colored by species Petal length (cm) Species Species setosa versicolor virginica

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

gm_eras: first 5 of 284 rows
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"))
Asia Europe Africa Americas Oceania 40 60 80 Life expectancy per continent, 1957 vs 2007 Life expectancy Continent Era 1957 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"))
Asia Europe Africa Americas Oceania 40 50 60 70 80 Whiskers without caps Life Continent

“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"))
Asia Europe Africa Americas Oceania 40 50 60 70 80 Boxes as wide as bars Life Continent

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

gm_continents: first 5 of 1056 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_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"))
Asia Europe Americas 40 60 80 Whiskers to the true extremes, and a border Life expectancy Continent

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