How is each group spread out? box draws the box-and-whisker of a distribution, one per group. At 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×IQR of the box. Any points beyond that are outliers, drawn individually as dots. It is the answer to how is this spread out, within each group? Whereas bar * mean gives one number per group, box gives the shape of all of them, and flags the few that sit apart. 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 that stuck 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, without the fence that makes this version worth drawing: the 1.5×IQR rule is Tukey’s, and so is the decision it encodes.
One difference is worth knowing if you arrived from his book. Tukey’s box spans hinges, the medians of the sorted batch’s two halves; gog spans the lower and upper quartiles, as most software does. They agree on most samples and can differ by a little on small ones, so a box drawn here is not always the box EDA would have drawn.
14.1 A summary per group
Name the category that splits the data on x and the measured column on y. The box does the rest:
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 bulk. The stray dots are the outliers, petals far enough from the box to stand out, which the whisker rule separates out rather than stretching a whisker to reach.
14.2 Distribution across many groups
The comparison scales, and the outliers start to carry information. Here is life expectancy across the continents in 2007. Five distributions read at a glance, and two countries that sit apart from their neighbors:
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 under Asia is Afghanistan; the one under the Americas is Haiti, each far enough below its continent’s box to be flagged rather than swept into a whisker. That is the whole point of the Tukey construction: the whiskers describe the bulk, and the dots say these are unusual. A zero-height box, a group whose middle 50% share almost one value, collapses to a flat line honestly, rather than being nudged open.
14.3 Boxes on their side
Swap the two positions and the same sentence draws the same plot, turned. There is no flip atom, for the reason there is none on bar: gog reads which axis measures off the bindings, and 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"))
Set it beside the upright version above. Every box, whisker and outlier dot sits in the same place relative to its neighbors, rotated a quarter turn, and Afghanistan and Haiti are still the two dots hanging off their continents. Nothing else in the chapter changes with the orientation either: color, dodge, the whiskers knob and style() all behave identically whichever axis holds the categories.
Use it when the category names are long. A vertical axis gives each label a whole line, where an upright chart has to fit it into a slot one box wide:
set.seed(4).teams <-c("Research & Development", "Sales and Marketing","Customer Operations", "Finance", "People & Culture")tenure <-data.frame(team =rep(.teams, each =30),years =round(pmax(rnorm(150, rep(c(6.2, 3.1, 2.4, 7.8, 4.5), each =30),rep(c(2.4, 1.6, 1.2, 3.0, 2.0), each =30)), 0.2), 1))data(tenure) + box +x(years) +y(team) +x_label("Years at the company") +title("Long labels 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:
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.
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 knob 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"))
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. It is a parameter of the mark, the way bin(30) and confidence(0.99) parameterize their statistics, the only choice a box offers.
14.5 Why box needs no transform
An interval cannot draw without a range transform to hand it a low and a high; a box never asks for one. The reason is a small but real distinction. 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 asking for a transform on top is refused with direction:
data(iris_flowers) + box * mean +x(species) +y(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.
This is the same principle that keeps the channel vocabulary small, seen from the other side. 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
color gives each box its own hue and a legend, the same discrete 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")
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"))
Mapping color to a second category splits each x-group into one box per color. Left alone those boxes share an x and overlap (their fills turn translucent to stay readable) and dodge sets them side by side, each continent’s two eras next to each other:
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"))
Width and opacity stay settings: a box is one shape, so there is nothing for a per-row size to vary, and style() sets them for the whole layer.
box completes the distribution tools: bar * bin for one variable’s histogram, interval for a range or an error bar, and box for the summary per group. The same low/high pair drawn as a continuous fill is the ribbon band. See Transforms for dodge and how the summarizing statistics relate.
14.7 What you can set
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
And these vary per row if you map them to a column instead: color() (categories), pattern() (categories), group() (categories), play() (either).
A box is the one mark in the kernel with a parameter of its own. Every other knob in the grammar belongs to a transform (bin(bins = ), confidence(0.99)) or to style(), but a box carries its summary intrinsically, so the one choice that summary offers has nowhere else to live:
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 rim")
(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 rim"))
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 rim")
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 rim"))
box(whiskers = "range") runs the whiskers to the true minimum and maximum with no outliers, against the default "tukey", which stops at 1.5 times the interquartile range and draws the points beyond as dots. Both still draw as a box, which is why it is a parameter and not a transform: it changes the rule the summary follows, not the geometry it becomes.
border_color and border_size are worth using on a box specifically, because a box is line-work and fill. The setting takes the outline, the whiskers and the median together, so a pale fill with a dark edge reads at small sizes where a solid fill does not.
14.8 What it refuses
A box is line-work and fill, which is exactly what its texture refusal turns on: a fill takes a texture, and a dash belongs to a stroke.
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.
Width is the other one. A box has no width of its own to vary, because the width it draws is the slot it sits in:
Error:
! gog: `size` cannot be bound to `box` — a box has no size feature. Remove `size(life)`, or use a mark that has one.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The statistic refusal a box makes is above, under why it needs no transform.
Spear, M. E. (1952). Charting statistics. McGraw-Hill.
Tukey, J. W. (1977). Exploratory data analysis. Addison-Wesley.