13  Interval

How far does each group reach, from its lowest value to its highest? interval draws a whisker for each group: a span from a low value to a high one, with a short line across each end called a cap. When the whisker shows a statistic with a center, such as a mean, a dot marks that center. interval is the mark that draws error bars and min–max ranges. A bar measures from a baseline up to a value. An interval has no baseline, so it needs both a low value and a high value before it can draw. Whiskers are vertical when the categories are on x and horizontal when they are on y; see Horizontal whiskers.

Almost everyone calls this an error bar, and that name does not say what the whisker measures. Three different quantities are drawn as the same picture: a standard deviation, a standard error, and a confidence interval (CI). On the same data, one can be several times longer than another. Nothing in the drawing says which one it is, and a reader without a caption cannot know.

gog makes the sentence name the statistic instead. interval * range spans the observed extremes. interval * confidence(0.95) computes a confidence interval and states its level in the sentence. interval * bounds(lower, upper) reads two columns you computed yourself. The mark is the same whisker in all three; what differs is the transform, and the transform is written down. That is the law Explicit Over Implicit, applied to one mark. The name error bar never says which statistic was drawn. A written sentence has to say it.

13.1 A range per group

How far do the petal lengths of each species reach, from the shortest to the longest? One whisker per species answers it, and a whisker needs a low value and a high value to span. The low value and the high value come from a transform, not from two extra channels. range reduces y to its minimum and maximum within each x group, and interval spans between them:

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) + interval * range + x(species) + y(petal_length) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("Range of petal length per species")
(data(iris_flowers) + interval * range + x(col.species) + y(col.petal_length) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("Range of petal length per species"))
data(iris_flowers) + interval * range + x(:species) + y(:petal_length) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("Range of petal length per species")
plot(data(iris_flowers), layer(interval, range), x(col.species),
  y(col.petal_length), x_label("Species"), y_label("Petal length (cm)"),
  title("Range of petal length per species"))
setosa versicolor virginica 2 4 6 Range of petal length per species Petal length (cm) Species

“Given the iris flowers: intervals derived by range, x is species, y is petal length.”

interval * range has the same shape as bar * mean. The difference is what the transform returns: mean returns one number per group, and range returns two. You name only the column whose spread you are asking about. The transform computes its two extents, the low value and the high value, and the mark spans them.

range does not belong to interval alone. Three other marks read the same low and high values, and only the shape changes:

Mark derived by range Shape
ribbon * range the band between the two values, filled
line * range the two values as two separate strokes
step * range the same two boundaries, as staircases
interval * range one whisker per group

range is one of six pair transforms. A pair transform supplies a low value and a high value, where most transforms supply a single number. The other five are confidence, deviation, smooth_band, straight_band and bounds, and every mark in that table takes all six.

13.2 The middle half of each group

A minimum and a maximum each come from a single row, so one unusual flower can decide where a whisker ends. To show the middle of a group instead of its extremes, give range two quantiles:

data(iris_flowers) + interval * range(0.25, 0.75) + x(species) + y(petal_length) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("Middle half of petal length per species")
(data(iris_flowers) + interval * range(0.25, 0.75) + x(col.species) + y(col.petal_length) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("Middle half of petal length per species"))
data(iris_flowers) + interval * range(0.25, 0.75) + x(:species) +
  y(:petal_length) + x_label("Species") + y_label("Petal length (cm)") +
  title("Middle half of petal length per species")
plot(data(iris_flowers), layer(interval, range(0.25, 0.75)),
  x(col.species), y(col.petal_length), x_label("Species"),
  y_label("Petal length (cm)"),
  title("Middle half of petal length per species"))
setosa versicolor virginica 2 3 4 5 6 Middle half of petal length per species Petal length (cm) Species

“Given the iris flowers: intervals derived by range from 0.25 to 0.75, x is species, y is petal length.”

This spans the first quartile to the third, which is the interquartile range. range(0.1, 0.9) keeps the middle 80%, and any other pair works the same way. range written with no arguments means range(0, 1), so it still spans from the minimum to the maximum. Transforms explains the two arguments in full.

13.3 Three layers instead of one mark

You now have two ranges of the same groups, the extremes and the middle half. A box draws a similar summary, five numbers in one mark, and you cannot remove one of them. Draw the middle half and the full range as two layers, and the median as a third. You can give any of the three layers its own style, drop one, or replace one:

data(iris_flowers) +
  interval * range + style(color = "darkgray") +
  interval * range(0.25, 0.75) + style(color = "steelblue", size = 8) +
  point * median + style(color = "gold", size = 5, opacity = 1) +
  x(species) + y(petal_length) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("Petal length per species, drawn in three layers")
(data(iris_flowers) +
  interval * range + style(color = "darkgray") +
  interval * range(0.25, 0.75) + style(color = "steelblue", size = 8) +
  point * median + style(color = "gold", size = 5, opacity = 1) +
  x(col.species) + y(col.petal_length) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("Petal length per species, drawn in three layers"))
data(iris_flowers) + interval * range + style(color = "darkgray") +
  interval * range(0.25, 0.75) + style(color = "steelblue", size = 8) +
  point * median + style(color = "gold", size = 5, opacity = 1) +
  x(:species) + y(:petal_length) + x_label("Species") +
  y_label("Petal length (cm)") +
  title("Petal length per species, drawn in three layers")
plot(data(iris_flowers), layer(interval, range),
  style({ color: "darkgray" }), layer(interval, range(0.25, 0.75)),
  style({ color: "steelblue", size: 8 }), layer(point, median),
  style({ color: "gold", size: 5, opacity: 1 }), x(col.species),
  y(col.petal_length), x_label("Species"), y_label("Petal length (cm)"),
  title("Petal length per species, drawn in three layers"))
setosa versicolor virginica 2 4 6 Petal length per species, drawn in three layers Petal length (cm) Species

“Given the iris flowers: intervals derived by range, and also intervals derived by range from 0.25 to 0.75, and also points derived by median, x is species, y is petal length.”

The gray whisker is the full range, the thick blue band is the middle half, and the gold dot is the median. The order of the layers decides what stays visible. A later layer is drawn over an earlier one, so the median is written last. Move it to the front of the sentence and the band draws over it, and almost none of the dot is left. If you have used ggplot2, this is the rule you already know: a geom added later draws on top. The dot is set opaque as well, since a point is slightly translucent by default.

The three layers name the three statistics they draw. A box plot draws similar information and names none of it. It differs from these layers at the ends of the whiskers. A box stops its whiskers at a cutoff computed from the quartiles, and draws every point beyond that cutoff as an outlier. A box on this same data draws outlier dots for setosa and versicolor. They are absent here, because these whiskers run to the true minimum and maximum.

Because each statistic is its own layer, you can change one and leave the others as they are. Widen the band to range(0.1, 0.9) and the plot reports a different statistic, with the same marks. Drop the gray layer and the y axis is scaled to the band rather than to the extremes. Add another layer with a different pair of quantiles, and you have a fan chart: nested spans for each category, instead of nested bands along a continuous axis. A box is still the shorter sentence when you want the five numbers and the outlier rule together.

13.4 A confidence interval, with a center

How well is the average petal length of each species known? That is a different question from how far the flowers spread, and it needs a different statistic. range shows the full spread. For the uncertainty of the mean, use confidence. It computes a t-interval for each group’s mean. That is the confidence interval named at the start of this chapter, and it widens when the group is small. confidence also keeps the mean as the center of the span, and the mark draws a dot there. A whisker with a dot at its center is a pointrange.

data(iris_flowers) + interval * confidence(0.95) + x(species) + y(petal_length) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("95% CI of mean petal length")
(data(iris_flowers) + interval * confidence(0.95) + x(col.species) + y(col.petal_length) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("95% CI of mean petal length"))
data(iris_flowers) + interval * confidence(0.95) + x(:species) +
  y(:petal_length) + x_label("Species") + y_label("Petal length (cm)") +
  title("95% CI of mean petal length")
plot(data(iris_flowers), layer(interval, confidence(0.95)),
  x(col.species), y(col.petal_length), x_label("Species"),
  y_label("Petal length (cm)"), title("95% CI of mean petal length"))
setosa versicolor virginica 2 3 4 5 95% CI of mean petal length Petal length (cm) Species

“Given the iris flowers: intervals derived by confidence at 95%, x is species, y is petal length.”

The level is confidence’s only argument, and it is 0.95 when you leave it out. A higher level draws a longer whisker, so confidence(0.99) is longer than confidence(0.95). The interval is always a t-interval. For a z-interval, a bootstrap, or the standard errors a model already gave you, compute the two numbers yourself and pass them to bounds.

13.5 The spread of the data

How far is a typical flower from the average of its species? That is a question about the spread of the data, not about how well the average is known. The opening of this chapter named three quantities: a standard deviation, a standard error and a confidence interval. confidence draws the confidence interval. The standard deviation has a transform of its own, deviation:

data(iris_flowers) + interval * deviation + x(species) + y(petal_length) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("Mean petal length, with one standard deviation")
(data(iris_flowers) + interval * deviation + x(col.species) + y(col.petal_length) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("Mean petal length, with one standard deviation"))
data(iris_flowers) + interval * deviation + x(:species) +
  y(:petal_length) + x_label("Species") + y_label("Petal length (cm)") +
  title("Mean petal length, with one standard deviation")
plot(data(iris_flowers), layer(interval, deviation), x(col.species),
  y(col.petal_length), x_label("Species"), y_label("Petal length (cm)"),
  title("Mean petal length, with one standard deviation"))
setosa versicolor virginica 2 3 4 5 6 Mean petal length, with one standard deviation Petal length (cm) Species

“Given the iris flowers: intervals derived by deviation, x is species, y is petal length.”

deviation with no argument reaches one standard deviation on each side of the mean. deviation(2) reaches two, so that whisker is four standard deviations long. Like confidence, it computes a mean as its center, so it draws a pointrange rather than a bare error bar.

The standard error has no transform of its own, and it does not need one. It is the standard deviation divided by the square root of the group size. confidence computes it internally and reports the interval built from it, which is what a reader of the plot needs. To draw one standard error on each side of the mean, compute the two ends yourself and pass them to bounds.

deviation and confidence look alike and answer different questions. | puts two plots side by side, and because both name petal_length on y they share that scale, drawn once down the left. Composition explains when two plots share a scale. Here are the two statistics on the same fifty flowers per species:

(data(iris_flowers) + interval * deviation + x(species) + y(petal_length) +
   y_label("Petal length (cm)") + title("Spread of the data")) |
  (data(iris_flowers) + interval * confidence + x(species) + y(petal_length) +
     y_label("") + title("Uncertainty of the mean"))
((data(iris_flowers) + interval * deviation + x(col.species) + y(col.petal_length) +
   y_label("Petal length (cm)") + title("Spread of the data")) |
  (data(iris_flowers) + interval * confidence + x(col.species) + y(col.petal_length) +
     y_label("") + title("Uncertainty of the mean")))
(data(iris_flowers) + interval * deviation + x(:species) +
  y(:petal_length) + y_label("Petal length (cm)") +
  title("Spread of the data")) |
  (data(iris_flowers) + interval * confidence + x(:species) +
  y(:petal_length) + y_label("") + title("Uncertainty of the mean"))
beside(plot(data(iris_flowers), layer(interval, deviation),
  x(col.species), y(col.petal_length), y_label("Petal length (cm)"),
  title("Spread of the data")),
  plot(data(iris_flowers), layer(interval, confidence), x(col.species),
  y(col.petal_length), y_label(""), title("Uncertainty of the mean")))
setosa versicolor virginica 2 3 4 5 6 Spread of the data Petal length (cm) Species setosa versicolor virginica Uncertainty of the mean Species

“Given the iris flowers: intervals derived by deviation, x is species, y is petal length, beside intervals derived by confidence, x is species, y is petal length.”

The whiskers on the right are less than a third as long, and the data is identical. The ratio is the same in all three species. The two statistics do not contradict each other. deviation describes how far the flowers are from their average, and measuring more of them does not make that whisker shorter. confidence describes how precisely the average itself is known. More flowers make its whisker shorter: four times as many flowers make it half as long. So the ratio between the two whiskers comes from the group size. With only a few flowers per species, the confidence whisker can be the longer one, because a t-interval widens when the group is small.

So choose the statistic by asking which caption you would write under the plot. “A versicolor petal is usually within half a centimeter of its species average” describes the left panel. “The average versicolor petal is 4.26 cm, and the interval reaches 0.13 cm on either side” describes the right one. Reporting one and meaning the other is the mistake the name error bar allows, and the reason gog requires the statistic in the sentence.

13.6 Why interval needs a transform

Every plot so far has named a transform. A mark needs one when it draws a number that nothing else supplies. A bar needs no transform, because the grammar already supplies its baseline: zero. bar + x + y draws. An interval has no such default. A low and a high are two numbers, and a plain y() supplies one. So the shortest interval that will draw is a mark, its positions, and a pair transform. That is its minimum syllable. Ask for an interval without a transform and the engine refuses the sentence rather than drawing an empty panel. What it refuses shows that refusal.

This is the design choice that keeps the channel vocabulary small. The obvious alternative would add ymin and ymax as two more channels, useful to only a few marks, and a box plot would then need five. Instead a transform computes the extents, the way mean computes one value per group, so the channel set never grows. ribbon reads the same low and high values, and box computes its five numbers inside the mark. The pair transform bounds(lower, upper) takes a low and a high you already have as two columns of the table, and Horizontal whiskers draws a coefficient plot with it.

13.7 Horizontal whiskers

Whiskers turn horizontal for the reasons bars do: the names are long, or there are many of them. A tall list of horizontal whiskers is then easier to read than a crowded row of vertical ones. An interval takes its orientation from the bindings, the way a bar and a box do. Put the categories on y and the measured column on x:

data(iris_flowers) + interval * confidence(0.95) + x(petal_length) + y(species) +
  x_label("Petal length (cm)") + y_label("Species") +
  title("The same 95% CI, horizontal")
(data(iris_flowers) + interval * confidence(0.95) + x(col.petal_length) + y(col.species) +
  x_label("Petal length (cm)") + y_label("Species") +
  title("The same 95% CI, horizontal"))
data(iris_flowers) + interval * confidence(0.95) + x(:petal_length) +
  y(:species) + x_label("Petal length (cm)") + y_label("Species") +
  title("The same 95% CI, horizontal")
plot(data(iris_flowers), layer(interval, confidence(0.95)),
  x(col.petal_length), y(col.species), x_label("Petal length (cm)"),
  y_label("Species"), title("The same 95% CI, horizontal"))
2 3 4 5 virginica versicolor setosa The same 95% CI, horizontal Species Petal length (cm)

The caps turn too, since a cap always crosses the span at its end, and the center dot stays at the center. The sections after this one work the same way on horizontal whiskers. color, dodge, caps, center and pattern, all introduced below, behave the same whichever axis carries the categories.

This is the usual shape for a coefficient plot, where each row is a named estimate and the reader reads the intervals from top to bottom. Estimates like these normally arrive already computed, from a model rather than from raw observations. That is what bounds is for: it reads two columns you already have as the low and the high, and computes nothing. The term column goes on y. In coefs, lo and hi are the ends of a 95% confidence interval for each term. bounds supplies the values for the other axis, so there is no x() to write:

coefs: all 5 rows
term lo hi
Age 0.02 0.18
Education 0.31 0.55
Experience 0.11 0.29
Region: North -0.24 0.06
Region: South -0.05 0.21
data(coefs) + interval * bounds(lo, hi) + y(term) +
  x_label("Effect on log wage") +
  title("A coefficient plot, from bounds you already have")
(data(coefs) + interval * bounds(col.lo, col.hi) + y(col.term) +
  x_label("Effect on log wage") +
  title("A coefficient plot, from bounds you already have"))
data(coefs) + interval * bounds(:lo, :hi) + y(:term) +
  x_label("Effect on log wage") +
  title("A coefficient plot, from bounds you already have")
plot(data(coefs), layer(interval, bounds(col.lo, col.hi)), y(col.term),
  x_label("Effect on log wage"),
  title("A coefficient plot, from bounds you already have"))
-0.2 0.0 0.2 0.4 Region: South Region: North Experience Education Age A coefficient plot, from bounds you already have Term Effect on log wage

“Given the coefs table: intervals from lo to hi, y is term.”

The two Region intervals, North and South, run from a negative low to a positive high. Each one includes zero, so this data cannot say whether either effect is positive or negative.

An interval needs a number to span, so two categorical axes are refused, as they are on a bar and a box. What it refuses shows that refusal.

13.8 Splitting by color

With color, a reader can see which whisker is which without reading the axis. color splits an interval into one whisker per group, the same categorical split line and bar make. The statistic is computed inside each group, so every group gets its own interval. Here color repeats the species already on x, so each species gets its own hue, and one legend names them all:

data(iris_flowers) + interval * confidence(0.95) + x(species) + y(petal_length) +
  color(species) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("95% CI of mean petal length, by species")
(data(iris_flowers) + interval * confidence(0.95) + x(col.species) + y(col.petal_length) +
  color(col.species) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("95% CI of mean petal length, by species"))
data(iris_flowers) + interval * confidence(0.95) + x(:species) +
  y(:petal_length) + color(:species) + x_label("Species") +
  y_label("Petal length (cm)") +
  title("95% CI of mean petal length, by species")
plot(data(iris_flowers), layer(interval, confidence(0.95)),
  x(col.species), y(col.petal_length), color(col.species),
  x_label("Species"), y_label("Petal length (cm)"),
  title("95% CI of mean petal length, by species"))
setosa versicolor virginica 2 3 4 5 95% CI of mean petal length, by species Petal length (cm) Species Species setosa versicolor virginica

“Given the iris flowers: intervals derived by confidence at 95%, x is species, y is petal length, color by species.”

A split by a second category puts several whiskers on the same category of x, and they overlap. dodge sets them side by side instead. It is the same collision modifier that separates grouped bars and boxes:

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) + interval * range * dodge + x(continent) + y(life) + color(era) +
  y_label("Life expectancy range") +
  title("Life expectancy range per continent, 1957 vs 2007")
(data(gm_eras) + interval * range * dodge + x(col.continent) + y(col.life) + color(col.era) +
  y_label("Life expectancy range") +
  title("Life expectancy range per continent, 1957 vs 2007"))
data(gm_eras) + interval * range * dodge + x(:continent) + y(:life) +
  color(:era) + y_label("Life expectancy range") +
  title("Life expectancy range per continent, 1957 vs 2007")
plot(data(gm_eras), layer(interval, range, dodge), x(col.continent),
  y(col.life), color(col.era), y_label("Life expectancy range"),
  title("Life expectancy range per continent, 1957 vs 2007"))
Asia Europe Africa Americas Oceania 40 60 80 Life expectancy range per continent, 1957 vs 2007 Life expectancy range Continent Era 1957 2007

“Given the gapminder eras: intervals derived by range and dodge, x is continent, y is life, color by era.”

Oceania’s 1957 whisker looks like a single cap. The table has only two countries there, Australia and New Zealand. In 1957 their life expectancies were within a tenth of a year of each other. That span is shorter than a pixel, so the two caps draw on top of each other. In 2007 the two countries were a year apart, and the caps separate.

Color splits an interval into groups, so each whisker has one color. size and opacity cannot be mapped on an interval, and they stay settings. A whisker is one stroke drawn from a whole group, so no single row can give it a width or an opacity. style() sets them once for the layer:

data(iris_flowers) + interval * range + x(species) + y(petal_length) +
  style(color = "tomato", size = 2.5) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("A heavier, one-color range")
(data(iris_flowers) + interval * range + x(col.species) + y(col.petal_length) +
  style(color = "tomato", size = 2.5) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("A heavier, one-color range"))
data(iris_flowers) + interval * range + x(:species) + y(:petal_length) +
  style(color = "tomato", size = 2.5) + x_label("Species") +
  y_label("Petal length (cm)") + title("A heavier, one-color range")
plot(data(iris_flowers), layer(interval, range), x(col.species),
  y(col.petal_length), style({ color: "tomato", size: 2.5 }),
  x_label("Species"), y_label("Petal length (cm)"),
  title("A heavier, one-color range"))
setosa versicolor virginica 2 4 6 A heavier, one-color range Petal length (cm) Species

“Given the iris flowers: intervals derived by range, x is species, y is petal length, colored tomato, with size 2.5.”

Every whisker is drawn in tomato and 2.5 pixels wide, whatever its species.

13.9 Caps, or a linerange

The caps are the short lines across each end of a whisker. They are decoration rather than data, because they only mark where a whisker stops. A crowded plot is often easier to read without them. So whether to draw them is a setting. style(caps = FALSE) removes them, and a whisker with no caps is a linerange. caps = TRUE, the default, keeps the capped error bar. Here is the confidence interval from Splitting by color with its caps off:

data(iris_flowers) + interval * confidence(0.95) + x(species) + y(petal_length) +
  color(species) + style(caps = FALSE) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("The same CI, drawn as a linerange")
(data(iris_flowers) + interval * confidence(0.95) + x(col.species) + y(col.petal_length) +
  color(col.species) + style(caps = False) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("The same CI, drawn as a linerange"))
data(iris_flowers) + interval * confidence(0.95) + x(:species) +
  y(:petal_length) + color(:species) + style(caps = false) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("The same CI, drawn as a linerange")
plot(data(iris_flowers), layer(interval, confidence(0.95)),
  x(col.species), y(col.petal_length), color(col.species),
  style({ caps: false }), x_label("Species"),
  y_label("Petal length (cm)"),
  title("The same CI, drawn as a linerange"))
setosa versicolor virginica 2 3 4 5 The same CI, drawn as a linerange Petal length (cm) Species Species setosa versicolor virginica

“Given the iris flowers: intervals derived by confidence at 95%, x is species, y is petal length, color by species, with caps off.”

caps belongs to the two marks that draw whiskers, interval and box. On any other mark, style(caps = ) is refused, and the message says what to use instead. A bar’s border_size is refused on a line the same way. caps combines with all six pair transforms. So the error bar, the linerange and the pointrange are all the same mark. Only the transform and one setting change.

A linerange from zero to each value, with a point layer marking the value, is the lollipop chart. A linerange between one category’s two values, such as a country in two years, with a point at each end, is the dumbbell chart. Comparing categories draws both.

13.10 Hiding the center dot

Whether a center exists is decided by the statistic, not by a setting. confidence and deviation compute a mean as their center, and smooth_band and straight_band compute the fitted value. range and bounds compute none. So the first four draw a pointrange, and range and bounds draw a bare error bar. A second layer can add a center where the statistic has none. point * mean over interval * range draws a pointrange from two layers, as Three layers instead of one mark shows with the median.

A setting can only hide a center that exists. A pointrange’s dot is the center the statistic computed. Sometimes you want the whisker without it. style(center = FALSE) drops the dot and leaves a bare error bar. center = TRUE, the default, keeps it:

data(iris_flowers) + interval * confidence(0.95) + x(species) + y(petal_length) +
  color(species) + style(center = FALSE) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("The same CI, without its center dots")
(data(iris_flowers) + interval * confidence(0.95) + x(col.species) + y(col.petal_length) +
  color(col.species) + style(center = False) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("The same CI, without its center dots"))
data(iris_flowers) + interval * confidence(0.95) + x(:species) +
  y(:petal_length) + color(:species) + style(center = false) +
  x_label("Species") + y_label("Petal length (cm)") +
  title("The same CI, without its center dots")
plot(data(iris_flowers), layer(interval, confidence(0.95)),
  x(col.species), y(col.petal_length), color(col.species),
  style({ center: false }), x_label("Species"),
  y_label("Petal length (cm)"),
  title("The same CI, without its center dots"))
setosa versicolor virginica 2 3 4 5 The same CI, without its center dots Petal length (cm) Species Species setosa versicolor virginica

“Given the iris flowers: intervals derived by confidence at 95%, x is species, y is petal length, color by species, with center off.”

center and caps each show or hide one part of an interval: center its middle dot, caps its ends. Each takes two values, so a confidence interval can be drawn in four ways. A capped pointrange is the default. A capped bare error bar is center = FALSE. A linerange with a dot is caps = FALSE. Setting both to FALSE gives a bare linerange. style(center = ) belongs to interval alone, because a box has no center dot: its middle is the median line. On any other mark it is refused, and the message says what to use instead.

Writing center = TRUE is refused when the statistic computes no center. Left out, the setting shows a center only where the statistic computes one, so a plain interval * range still draws. range computes only the two ends of each span, so there is no dot to show:

data(iris_flowers) + interval * range + x(species) + y(petal_length) +
  style(center = TRUE)
(data(iris_flowers) + interval * range + x(col.species) + y(col.petal_length) +
  style(center = True))
data(iris_flowers) + interval * range + x(:species) + y(:petal_length) +
  style(center = true)
plot(data(iris_flowers), layer(interval, range), x(col.species),
  y(col.petal_length), style({ center: true }))
Error:
! gog: `style(center = )` asks this interval for its center dot, but `range` computes none: it gives only the two ends of each span, and the setting can hide a center but not make one. Use `interval * confidence` or `interval * deviation`, which compute the mean as the center, or remove the setting and add `point * mean` to draw the mean as a second layer.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

The message names the two statistics that compute a mean as the center, confidence and deviation. It also names point * mean, a second layer that draws the mean as a dot on each whisker once the setting is removed.

13.11 A dashed whisker

A dash separates one layer from another without a second color, and it still separates them when the plot is printed in black and white. A whisker is a stroke, so it takes style(pattern = ) the same way a line does. This is the settable rule: a dash is the pattern on every path-stroke mark (line, step, interval, path, rule, edge). The dash is drawn on the whisker span only. The caps and the center dot stay solid, since a dashed cap would not look like a cap and a dot has no line to dash:

data(iris_flowers) + interval * confidence(0.95) + x(species) + y(petal_length) +
  color(species) + style(pattern = "dashed") +
  x_label("Species") + y_label("Petal length (cm)") +
  title("A dashed 95% CI")
(data(iris_flowers) + interval * confidence(0.95) + x(col.species) + y(col.petal_length) +
  color(col.species) + style(pattern = "dashed") +
  x_label("Species") + y_label("Petal length (cm)") +
  title("A dashed 95% CI"))
data(iris_flowers) + interval * confidence(0.95) + x(:species) +
  y(:petal_length) + color(:species) + style(pattern = "dashed") +
  x_label("Species") + y_label("Petal length (cm)") +
  title("A dashed 95% CI")
plot(data(iris_flowers), layer(interval, confidence(0.95)),
  x(col.species), y(col.petal_length), color(col.species),
  style({ pattern: "dashed" }), x_label("Species"),
  y_label("Petal length (cm)"), title("A dashed 95% CI"))
setosa versicolor virginica 2 3 4 5 A dashed 95% CI Petal length (cm) Species Species setosa versicolor virginica

“Given the iris flowers: intervals derived by confidence at 95%, x is species, y is petal length, color by species, with pattern dashed.”

pattern can also be mapped to a column. Like color, it splits an interval into one whisker per group, and each group’s whisker gets its own dash. Mapped to a second category, pattern puts several whiskers on one category of x, where they overlap. dodge sets them side by side, as it does for a split by color.

13.12 A bar with its error bar

Two marks can answer two questions about the same groups. A bar shows where each group’s mean is. An interval shows how well that mean is known. Layer them and you have a plot that appears throughout the social sciences, usually with a bare error bar rather than a pointrange:

data(iris_flowers) + bar * mean + style(color = "lightgray") +
  interval * confidence(0.95) + style(center = FALSE, color = "black") +
  x(species) + y(petal_length) +
  x_label("Species") + y_label("Mean petal length (cm)") +
  title("Mean petal length, with a 95% confidence interval")
(data(iris_flowers) + bar * mean + style(color = "lightgray") +
  interval * confidence(0.95) + style(center = False, color = "black") +
  x(col.species) + y(col.petal_length) +
  x_label("Species") + y_label("Mean petal length (cm)") +
  title("Mean petal length, with a 95% confidence interval"))
data(iris_flowers) + bar * mean + style(color = "lightgray") +
  interval * confidence(0.95) + style(center = false, color = "black") +
  x(:species) + y(:petal_length) + x_label("Species") +
  y_label("Mean petal length (cm)") +
  title("Mean petal length, with a 95% confidence interval")
plot(data(iris_flowers), layer(bar, mean), style({ color: "lightgray" }),
  layer(interval, confidence(0.95)),
  style({ center: false, color: "black" }), x(col.species),
  y(col.petal_length), x_label("Species"),
  y_label("Mean petal length (cm)"),
  title("Mean petal length, with a 95% confidence interval"))
setosa versicolor virginica 0 2 4 Mean petal length, with a 95% confidence interval Mean petal length (cm) Species

“Given the iris flowers: bars derived by mean, colored lightgray, and also intervals derived by confidence at 95%, with center off, colored black, x is species, y is petal length.”

Each layer names its own statistic. bar * mean reduces each group to one number, and interval * confidence(0.95) computes the interval around that number. Both read the same column and report different things, and the sentence says which is which. The dot is turned off because it would be exactly on the top of the bar, which already marks the mean.

A bar with an error bar is where the three quantities named at the start of this chapter are most often confused, and nothing in the picture separates them. In gog the sentence names the statistic: this one says confidence(0.95), and deviation in its place would give a different sentence and a much longer whisker.

The groups of a second category go side by side, with the same dodge that separates grouped bars. Bind color to the bars and group to the whiskers. group splits the whiskers into the same slots as the bars but draws no legend of its own, so the plot keeps one:

data(gm_eras) +
  bar * mean * dodge + color(era) +
  interval * confidence(0.95) * dodge + group(era) +
  style(center = FALSE, color = "black") +
  x(continent) + y(life) +
  y_label("Mean life expectancy (years)") +
  title("Mean life expectancy by continent, 1957 and 2007")
(data(gm_eras) +
  bar * mean * dodge + color(col.era) +
  interval * confidence(0.95) * dodge + group(col.era) +
  style(center = False, color = "black") +
  x(col.continent) + y(col.life) +
  y_label("Mean life expectancy (years)") +
  title("Mean life expectancy by continent, 1957 and 2007"))
data(gm_eras) + bar * mean * dodge + color(:era) +
  interval * confidence(0.95) * dodge + group(:era) +
  style(center = false, color = "black") + x(:continent) + y(:life) +
  y_label("Mean life expectancy (years)") +
  title("Mean life expectancy by continent, 1957 and 2007")
plot(data(gm_eras), layer(bar, mean, dodge), color(col.era),
  layer(interval, confidence(0.95), dodge), group(col.era),
  style({ center: false, color: "black" }), x(col.continent), y(col.life),
  y_label("Mean life expectancy (years)"),
  title("Mean life expectancy by continent, 1957 and 2007"))
Asia Europe Africa Americas Oceania 0 20 40 60 80 Mean life expectancy by continent, 1957 and 2007 Mean life expectancy (years) Continent Era 1957 2007

“Given the gapminder eras: bars derived by mean and dodge, colored by era, and also intervals derived by confidence at 95% and dodge, grouped by era, with center off, colored black, x is continent, y is life.”

Oceania has both the shortest whisker and the longest, from the same two countries. A confidence interval from two values is wide compared with the distance between them, because two values say little about the spread. In 2007 Australia and New Zealand were a year apart, and the interval is thirteen years wide. In 1957 the two countries were 0.07 years apart, and the confidence interval is still nine tenths of a year wide. That is more than twelve times the 1957 range, which showed as a single cap in Splitting by color.

What this plot does not show is the data. Two groups can have the same mean and the same interval and still hold very different values, and a bar reports only the one number. Three layers instead of one mark draws the spread itself, and box draws five numbers at once.

13.13 What you can set

A setting changes how every whisker looks without reading a column, and each mark takes its own. These are an interval’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, dashed, dotted, dotdash, longdash
style(caps = ) true or false
style(center = ) true or false

An interval takes the four settings of a line, because a whisker is a stroke. It also takes two more, caps and center. The plot below gives several settings at once. It reads gm_continents: the Americas, Asia and Europe, measured every five years from 1952 to 2007, so each interval is computed from every country in that continent, across all twelve years.

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) + interval * confidence(0.99) + x(continent) + y(life) +
  style(caps = FALSE, size = 3, color = "darkslateblue", pattern = "dotted") +
  y_label("Life expectancy") + title("A thick, dotted 99% linerange")
(data(gm_continents) + interval * confidence(0.99) + x(col.continent) + y(col.life) +
  style(caps = False, size = 3, color = "darkslateblue", pattern = "dotted") +
  y_label("Life expectancy") + title("A thick, dotted 99% linerange"))
data(gm_continents) + interval * confidence(0.99) + x(:continent) +
  y(:life) +
  style(caps = false, size = 3, color = "darkslateblue", pattern = "dotted") +
  y_label("Life expectancy") + title("A thick, dotted 99% linerange")
plot(data(gm_continents), layer(interval, confidence(0.99)),
  x(col.continent), y(col.life),
  style({ caps: false, size: 3, color: "darkslateblue",
  pattern: "dotted" }), y_label("Life expectancy"),
  title("A thick, dotted 99% linerange"))
Asia Europe Americas 60 65 70 A thick, dotted 99% linerange Life expectancy Continent

The confidence level is not in the settings table, because it is not a setting. It is written into the transform instead. confidence(0.99) changes what is computed, not how it is drawn. To decide whether something is a setting, ask whether it changes the computed numbers. The level does, so it belongs to the transform.

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.

13.14 What it refuses

An interval is a stroke, not a filled shape, so its pattern is a dash and a hatch is refused:

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) + interval * range + x(continent) + y(life) +
  style(pattern = "hatch")
(data(gapminder_2007) + interval * range + x(col.continent) + y(col.life) +
  style(pattern = "hatch"))
data(gapminder_2007) + interval * range + x(:continent) + y(:life) +
  style(pattern = "hatch")
plot(data(gapminder_2007), layer(interval, range), x(col.continent),
  y(col.life), style({ pattern: "hatch" }))
Error:
! gog: `style(pattern = "hatch")` is not a stroke pattern. Use "solid" (the default), "dashed", "dotted", "dotdash", "longdash".
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

You might expect a hatch to work, because a capped range looks like a shape with an inside. The engine draws it as strokes, and a stroke takes a dash.

An interval with no transform is the second refusal. A plain y() supplies one number, and a span needs two, as Why interval needs a transform explained. The engine says so rather than drawing an empty panel:

data(iris_flowers) + interval + x(species) + y(petal_length)
data(iris_flowers) + interval + x(col.species) + y(col.petal_length)
data(iris_flowers) + interval + x(:species) + y(:petal_length)
plot(data(iris_flowers), interval, x(col.species), y(col.petal_length))
Error:
! gog: `interval` draws a span from a low value to a high one, but nothing here produces those extents. Add a range transform — `interval * range + x(group) + y(value)` draws the min–max range per group, or `interval * bounds(lo, hi)` a pre-computed one.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

The message calls them range transforms. It names two of the four, range and bounds, and writes out a sentence the engine would accept.

Two categorical axes are the third refusal. An interval needs one continuous axis, because that column is where the low and the high come from:

data(gapminder_2007) + interval * range + x(continent) + y(continent)
data(gapminder_2007) + interval * range + x(col.continent) + y(col.continent)
data(gapminder_2007) + interval * range + x(:continent) + y(:continent)
plot(data(gapminder_2007), layer(interval, range), x(col.continent),
  y(col.continent))
Error:
! gog: `interval` has categorical columns on both axes — `x(continent)` and `y(continent)` — so there is nothing for it to span. One axis must be a number: that is the column the low and high extents come from.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

Two categories leave the mark nothing to span.

13.15 In short

An interval computes nothing. Its transform decides which two numbers it spans, and whether there is a center to draw. caps and center are settings, so neither reads a column. A dash can be set for the whole layer, or mapped to a column, which splits the whiskers the way color does.

13.16 What comes next

Which two numbers an interval spans is the transform’s answer, never the mark’s. Box answers it inside the mark: it computes the quartiles and the median itself, and draws them as a box and two whiskers.