15  Ribbon

How far apart were the lowest and highest values at each date? ribbon draws a filled band from a low boundary to a high one across x: a confidence band, a spread band, the shaded region around a trend. It fills one region, the way an area does. It reads a low value and a high value at each position, the way an interval does. An area fills from the data down to a baseline at zero. A ribbon has no baseline, and both of its edges come from the data, usually through a transform. With a category on one axis and density, the same mark draws a violin plot.

15.1 A band from a range

The gapminder_asia table holds five Asian countries, measured every five years. Did those five move closer together over the years, or further apart? The gap between the lowest and the highest in each year answers that. A band drawn between those two values shows every year at once, as one shape. The two boundaries come from a transform, exactly as an interval’s two extents do. range reduces y to its minimum and maximum within each x, and the ribbon fills between them. Here, the spread of life expectancy across five Asian countries, year by year:

gapminder_asia: first 5 of 60 rows
country continent year life population gdp
China Asia 1952 44.00000 556263527 400.4486
China Asia 1957 50.54896 637408000 575.9870
China Asia 1962 44.50136 665770000 487.6740
China Asia 1967 58.38112 754550000 612.7057
China Asia 1972 63.11888 862030000 676.9001
data(gapminder_asia) + ribbon * range + x(year) + y(life) +
  y_label("Life expectancy") +
  title("The min–max spread across five Asian countries, per year")
(data(gapminder_asia) + ribbon * range + x(col.year) + y(col.life) +
  y_label("Life expectancy") +
  title("The min–max spread across five Asian countries, per year"))
data(gapminder_asia) + ribbon * range + x(:year) + y(:life) +
  y_label("Life expectancy") +
  title("The min–max spread across five Asian countries, per year")
plot(data(gapminder_asia), layer(ribbon, range), x(col.year), y(col.life),
  y_label("Life expectancy"),
  title("The min–max spread across five Asian countries, per year"))
1960 1980 2000 40 50 60 70 80 The min–max spread across five Asian countries, per year Life expectancy Year

“Given gapminder Asia: a ribbon derived by range, x is year, y is life.”

In each year the band’s lower edge is at the lowest life expectancy of the five, and its upper edge is at the highest. The band is the region that holds all five. It narrows from about 26 years wide in 1952 to about 18 in 2007, so the five countries moved closer together.

The low and high pair is the one interval * range draws, filled continuously instead of drawn as a whisker at each x. Only the shape drawn from it changes.

15.2 A band with a mean line

On its own, a band shows the spread but not the center. A band is usually drawn with a line through the middle. You compose the two by layering, not by adding an option to either mark. A ribbon * range for the spread, a line * mean for the trend, sharing one x and y:

data(gapminder_asia) + x(year) + y(life) +
  ribbon * range + style(color = "steelblue", opacity = 0.25) +
  line * mean + style(color = "steelblue", size = 2) +
  y_label("Life expectancy") +
  title("Mean life expectancy, with its min–max band")
(data(gapminder_asia) + x(col.year) + y(col.life) +
  ribbon * range + style(color = "steelblue", opacity = 0.25) +
  line * mean + style(color = "steelblue", size = 2) +
  y_label("Life expectancy") +
  title("Mean life expectancy, with its min–max band"))
data(gapminder_asia) + x(:year) + y(:life) + ribbon * range +
  style(color = "steelblue", opacity = 0.25) + line * mean +
  style(color = "steelblue", size = 2) + y_label("Life expectancy") +
  title("Mean life expectancy, with its min–max band")
plot(data(gapminder_asia), x(col.year), y(col.life), layer(ribbon, range),
  style({ color: "steelblue", opacity: 0.25 }), layer(line, mean),
  style({ color: "steelblue", size: 2 }), y_label("Life expectancy"),
  title("Mean life expectancy, with its min–max band"))
1960 1980 2000 40 50 60 70 80 Mean life expectancy, with its min–max band Life expectancy Year

“Given gapminder Asia: x is year, y is life, a ribbon derived by range and also a line derived by mean.”

Each layer runs its own transform on the same table. mean reduces each year to one point for the line, and range reduces the same year to a low and high pair for the band. This is the most common ribbon plot: a trend with its uncertainty or spread drawn around it. It is two marks the grammar already has, added with +.

15.3 A fan chart of nested bands

Which values of life expectancy do most countries have in each year? gm_all holds all the gapminder years: every country, in every year, 142 countries in all. A min–max band over it spans the full range of life expectancy in each year, but only the two extreme countries set its edges. Naming two quantiles instead gives a band that contains most of the countries. Layer two bands of different widths to draw a fan chart:

gm_all: first 5 of 1704 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_all) + x(year) + y(life) +
  ribbon * range(0.1, 0.9) + style(color = "steelblue", opacity = 0.2) +
  ribbon * range(0.25, 0.75) + style(color = "steelblue", opacity = 0.35) +
  line * median + style(color = "steelblue", size = 2) +
  y_label("Life expectancy") +
  title("Life expectancy across 142 countries, 1952 to 2007")
(data(gm_all) + x(col.year) + y(col.life) +
  ribbon * range(0.1, 0.9) + style(color = "steelblue", opacity = 0.2) +
  ribbon * range(0.25, 0.75) + style(color = "steelblue", opacity = 0.35) +
  line * median + style(color = "steelblue", size = 2) +
  y_label("Life expectancy") +
  title("Life expectancy across 142 countries, 1952 to 2007"))
data(gm_all) + x(:year) + y(:life) + ribbon * range(0.1, 0.9) +
  style(color = "steelblue", opacity = 0.2) + ribbon * range(0.25, 0.75) +
  style(color = "steelblue", opacity = 0.35) + line * median +
  style(color = "steelblue", size = 2) + y_label("Life expectancy") +
  title("Life expectancy across 142 countries, 1952 to 2007")
plot(data(gm_all), x(col.year), y(col.life),
  layer(ribbon, range(0.1, 0.9)),
  style({ color: "steelblue", opacity: 0.2 }),
  layer(ribbon, range(0.25, 0.75)),
  style({ color: "steelblue", opacity: 0.35 }), layer(line, median),
  style({ color: "steelblue", size: 2 }), y_label("Life expectancy"),
  title("Life expectancy across 142 countries, 1952 to 2007"))
1960 1980 2000 40 50 60 70 80 Life expectancy across 142 countries, 1952 to 2007 Life expectancy Year

“Given all the gapminder years: x is year, y is life, a ribbon derived by range from 0.1 to 0.9, and also a ribbon derived by range from 0.25 to 0.75, and also a line derived by median.”

The pale band contains the middle 80% of countries. The darker one contains the middle half, and the line is the median. This plot shows something the min–max version cannot. The median rose from 45 years to 72, and the 90th percentile from 67 to 80, so the distance between them fell from 22 years to 8.

The lower edges of the two bands do not rise steadily. Both lower edges, the 10th percentile and the 25th, decline between 1992 and 2002 while the median rises throughout. A min–max band cannot show this, because its lower edge is one country.

Each band is one more layer, so a third pair of quantiles adds a third band. Write the widest band first, because a later layer is drawn over an earlier one.

15.4 Why a ribbon needs a transform

A low and a high are two numbers, and a plain y() supplies one. An area closes on zero: its region’s other edge is zero, which the grammar supplies. A ribbon has no such second number. Like interval, a ribbon needs a transform that supplies both boundaries, and that transform is part of its minimum syllable. Write a ribbon without one and the engine refuses it, rather than drawing an empty panel. What it refuses shows the message.

Computing the boundaries in a transform also keeps the channel vocabulary small. The alternative would add ymin and ymax as two more channels, useful to only a few marks. A box plot would then need five such channels, one for each number it summarizes. With a transform instead, the same low and high values can be drawn as a whisker, as two lines, or as a filled band. See Transforms for the range, confidence, deviation, smooth_band and straight_band statistics.

Usually that transform is one of the six pair transforms: range, confidence, deviation, smooth_band, straight_band and bounds. density over a category also works. It gives one curve per category, and the ribbon draws that curve and its mirror image as the two boundaries. That is the violin, later in this chapter.

15.5 A confidence band

How well is the mean life expectancy known in each year? That is a different question from how far the five countries spread. range shows the full spread. For the uncertainty of the mean, use confidence. It computes a t-interval for the mean at each x, and the ribbon fills between its two boundaries. Here is the 95% band for the same five countries:

data(gapminder_asia) + ribbon * confidence(0.95) + x(year) + y(life) +
  y_label("Life expectancy") +
  title("95% confidence band for mean life expectancy")
(data(gapminder_asia) + ribbon * confidence(0.95) + x(col.year) + y(col.life) +
  y_label("Life expectancy") +
  title("95% confidence band for mean life expectancy"))
data(gapminder_asia) + ribbon * confidence(0.95) + x(:year) + y(:life) +
  y_label("Life expectancy") +
  title("95% confidence band for mean life expectancy")
plot(data(gapminder_asia), layer(ribbon, confidence(0.95)), x(col.year),
  y(col.life), y_label("Life expectancy"),
  title("95% confidence band for mean life expectancy"))
1960 1980 2000 40 50 60 70 80 95% confidence band for mean life expectancy Life expectancy Year

“Given gapminder Asia: a ribbon derived by confidence at 95%, x is year, y is life.”

The band is wide because each mean comes from only five countries. Those five differ by more than fifteen years, so the interval around their mean is wide. A confidence level above 0.95 draws a wider band still.

Compare it with the first plot in this chapter: the two bands are almost the same width. That comes from having only five values, not from the two statistics agreeing. Over the 142 countries of gm_all, the 95% band in 2007 is about 4 years wide, and their range is 43 years. What the band means is decided by the statistic you name: range draws the observed spread, and confidence draws the uncertainty of the mean. The mark stays the same in both plots, exactly as it does for interval.

15.6 A pre-computed band

range, confidence, deviation, smooth_band and straight_band compute the band from the raw values in the table, at the moment gog draws the plot. Many bands are computed earlier, by the analysis itself. A model’s standard error, a standard error of measurement, a bootstrap interval: each of these is computed before the plot. Each gives a low value and a high one, and those two become two columns in the table. gog draws what you computed, and it does not re-fit the model. The score_band table holds one row per raw score on a test scored from 0 to 20. Each row also holds the expected true score for that raw score, the score a model predicts, and the two ends of a 95% band around it. In this table the expected true score equals the raw score, so a line through it is a diagonal:

score_band: first 5 of 21 rows
score expected lower upper
0 0 -1.610304 1.610304
1 1 -1.865557 3.865557
2 2 -1.944424 5.944424
3 3 -1.694803 7.694803
4 4 -1.259232 9.259232

The ribbon reads those edge columns directly, through bounds(lower, upper):

data(score_band) + ribbon * bounds(lower, upper) + x(score) +
  y_label("Expected true score") + x_label("Raw score") +
  title("A pre-computed measurement band")
(data(score_band) + ribbon * bounds(col.lower, col.upper) + x(col.score) +
  y_label("Expected true score") + x_label("Raw score") +
  title("A pre-computed measurement band"))
data(score_band) + ribbon * bounds(:lower, :upper) + x(:score) +
  y_label("Expected true score") + x_label("Raw score") +
  title("A pre-computed measurement band")
plot(data(score_band), layer(ribbon, bounds(col.lower, col.upper)),
  x(col.score), y_label("Expected true score"), x_label("Raw score"),
  title("A pre-computed measurement band"))
0 5 10 15 20 0 10 20 A pre-computed measurement band Expected true score Raw score

“Given the score band: a ribbon from lower to upper, x is score.”

bounds is the pair transform that computes nothing. It reshapes the two columns you give it into the same low and high pair range writes. The band needs no y(), because the two columns are the boundaries. This is the one case range, confidence, deviation, smooth_band and straight_band cannot write: a band that was computed outside the plot, before gog ever read the table.

A band like this is usually drawn together with the values it surrounds. Here those values are the expected true scores. A dashed line draws them, with a point at every raw score. The band, the line and the points are three layers composed with +:

data(score_band) + x(score) + y(expected) +
  ribbon * bounds(lower, upper) + style(color = "steelblue", opacity = 0.18) +
  line + style(color = "steelblue", size = 1.4, pattern = "dashed") +
  point + style(color = "steelblue", size = 2.4) +
  y_label("Expected true score") + x_label("Raw score") +
  title("Expected score with its measurement error band")
(data(score_band) + x(col.score) + y(col.expected) +
  ribbon * bounds(col.lower, col.upper) + style(color = "steelblue", opacity = 0.18) +
  line + style(color = "steelblue", size = 1.4, pattern = "dashed") +
  point + style(color = "steelblue", size = 2.4) +
  y_label("Expected true score") + x_label("Raw score") +
  title("Expected score with its measurement error band"))
data(score_band) + x(:score) + y(:expected) +
  ribbon * bounds(:lower, :upper) +
  style(color = "steelblue", opacity = 0.18) + line +
  style(color = "steelblue", size = 1.4, pattern = "dashed") + point +
  style(color = "steelblue", size = 2.4) +
  y_label("Expected true score") + x_label("Raw score") +
  title("Expected score with its measurement error band")
plot(data(score_band), x(col.score), y(col.expected),
  layer(ribbon, bounds(col.lower, col.upper)),
  style({ color: "steelblue", opacity: 0.18 }), line,
  style({ color: "steelblue", size: 1.4, pattern: "dashed" }), point,
  style({ color: "steelblue", size: 2.4 }),
  y_label("Expected true score"), x_label("Raw score"),
  title("Expected score with its measurement error band"))
0 5 10 15 20 0 10 20 Expected score with its measurement error band Expected true score Raw score

“Given the score band: x is score, y is expected, a ribbon from lower to upper, and also a line and points.”

The dash on that line comes from style(pattern = ). That style() is written after line, so it belongs to that layer, and the band is not dashed. On a stroke the texture is a dash. The band takes the same setting, with different values. On a fill the texture is a hatch: "hatch", "crosshatch", "stripes", "grid" or "dots", never "dashed".

15.7 Filled, or two dashed edges

Some plots need the band without a fill. A band starts as a low and high pair, and the mark decides how that pair is drawn. This follows the No Exceptions law. One bin draws as a bar chart, as a line and as a step histogram, and one pair draws in just as many ways: filled, as two strokes, or as two staircases. ribbon fills the pair. A line traces its two boundaries instead, which is the unfilled band many journals prefer. With style(pattern = ) it becomes the dashed pair of curves those journals often print:

data(score_band) + x(score) + y(expected) +
  line * bounds(lower, upper) + style(color = "gray", pattern = "dashed", size = 1.2) +
  line + style(color = "steelblue", size = 1.5) +
  point + style(color = "steelblue", size = 2.4) +
  y_label("Expected true score") + x_label("Raw score") +
  title("The same band, unfilled: two dashed edges")
(data(score_band) + x(col.score) + y(col.expected) +
  line * bounds(col.lower, col.upper) + style(color = "gray", pattern = "dashed", size = 1.2) +
  line + style(color = "steelblue", size = 1.5) +
  point + style(color = "steelblue", size = 2.4) +
  y_label("Expected true score") + x_label("Raw score") +
  title("The same band, unfilled: two dashed edges"))
data(score_band) + x(:score) + y(:expected) +
  line * bounds(:lower, :upper) +
  style(color = "gray", pattern = "dashed", size = 1.2) + line +
  style(color = "steelblue", size = 1.5) + point +
  style(color = "steelblue", size = 2.4) +
  y_label("Expected true score") + x_label("Raw score") +
  title("The same band, unfilled: two dashed edges")
plot(data(score_band), x(col.score), y(col.expected),
  layer(line, bounds(col.lower, col.upper)),
  style({ color: "gray", pattern: "dashed", size: 1.2 }), line,
  style({ color: "steelblue", size: 1.5 }), point,
  style({ color: "steelblue", size: 2.4 }),
  y_label("Expected true score"), x_label("Raw score"),
  title("The same band, unfilled: two dashed edges"))
0 5 10 15 20 0 10 20 The same band, unfilled: two dashed edges Expected true score Raw score

“Given the score band: x is score, y is expected, lines from lower to upper, and also a line and points.”

line * bounds is one layer that draws both edges, splitting the pair into a low curve and a high one. So you do not write one y() for the low edge and another for the high edge. Two plain line layers can draw two columns you already have. They cannot split a pair that range computes, because that pair never becomes a column you can name. A line draws every pair transform the same way. line * range traces the lowest and the highest values, and line * confidence traces the two ends of a confidence interval. step * bounds draws the same two edges as staircases. Filled or unfilled, solid or dashed, it is one pair and a choice of mark, never a new option added to the band.

Four marks draw a low and high pair: interval draws it as a whisker at each x, line and step trace its two boundaries, and ribbon fills between them. All four marks draw range, confidence, deviation, smooth_band, straight_band and bounds.

15.8 One band per group

How does the spread of one continent compare with another’s, year by year? The gm_continents table holds three continents. One band per continent puts all three spreads in one panel. A categorical column splits a ribbon into one band per group. It is the same split that gives area one region per group and line one stroke per group. Here each continent gets its own band, the range of its countries’ life expectancy over time:

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) + ribbon * range + x(year) + y(life) + color(continent) +
  y_label("Life expectancy") +
  title("Life expectancy spread, three continents")
(data(gm_continents) + ribbon * range + x(col.year) + y(col.life) + color(col.continent) +
  y_label("Life expectancy") +
  title("Life expectancy spread, three continents"))
data(gm_continents) + ribbon * range + x(:year) + y(:life) +
  color(:continent) + y_label("Life expectancy") +
  title("Life expectancy spread, three continents")
plot(data(gm_continents), layer(ribbon, range), x(col.year), y(col.life),
  color(col.continent), y_label("Life expectancy"),
  title("Life expectancy spread, three continents"))
1960 1980 2000 40 60 80 Life expectancy spread, three continents Life expectancy Year Continent Asia Europe Americas

“Given the gapminder continents: ribbons derived by range, x is year, y is life, color by continent.”

Europe’s band narrows from about 29 years wide in 1952 to about 10 in 2007, and the Americas’ from 31 to 20. Asia’s stays about 37 to 40 years wide, except for a dip in its lower edge in 1977.

The bands overlap where two continents cover the same range of life expectancy. A split area draws each region nearly opaque, as it draws a single one, so little of the region behind shows through. For an area, stack solves this: it starts each region at the top of the ones below, so no two regions overlap. A ribbon split into groups draws each band translucent by default instead, so a band behind another stays readable. The difference follows from what each mark measures. An area measures a height from the baseline, so its groups can be piled into a total. A ribbon already spans a low to a high and measures no such height, so there is nothing to stack. Overlapping bands are read through transparency and never stacked, which is why ribbon * stack is refused and the refusal names style(opacity = ).

15.9 One region, one fill

A ribbon is a single filled region, so opacity is a setting here, not a channel, exactly as on area. Its two boundaries pass through the low and high values at each x, and its inside belongs to no single row. So a column cannot make one part of the band lighter or darker than another. What it refuses shows the refusal.

size is refused both ways: a ribbon neither maps it nor sets it. A band’s shape comes entirely from its x values and its two boundaries, so there is no width to choose. To outline the band, layer a line along its boundaries rather than setting an outline on the mark. The plot in What you can set does this with line * bounds.

15.10 A category on the domain

What happens when x holds categories instead of years? The spread of each continent and its center can be drawn as one band across the categories. You then see one shape across the panel, as with an area’s filled profile. A ribbon takes a category on x, the same way an area does, and fills the spread across the categories in axis order:

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) + ribbon * range + line * mean + x(continent) + y(life) +
  y_label("Life expectancy") +
  title("ribbon * range + line * mean: spread and center, per continent")
(data(gapminder_2007) + ribbon * range + line * mean + x(col.continent) + y(col.life) +
  y_label("Life expectancy") +
  title("ribbon * range + line * mean: spread and center, per continent"))
data(gapminder_2007) + ribbon * range + line * mean + x(:continent) +
  y(:life) + y_label("Life expectancy") +
  title("ribbon * range + line * mean: spread and center, per continent")
plot(data(gapminder_2007), layer(ribbon, range), layer(line, mean),
  x(col.continent), y(col.life), y_label("Life expectancy"),
  title("ribbon * range + line * mean: spread and center, per continent"))
Asia Europe Africa Americas Oceania 40 50 60 70 80 ribbon * range + line * mean: spread and center, per continent Life expectancy Continent

The band runs from each continent’s lowest life expectancy to its highest, and the line marks each continent’s mean.

For most such data an interval is the better sentence. It draws a separate whisker at each category and claims nothing about the space between them. A ribbon instead slopes from one category to the next, across positions where no rows exist. The grammar does not decide that for you: both sentences are well formed, and which one fits your data is a question about the data.

data(gapminder_2007) + interval * range + x(continent) + y(life) +
  y_label("Life expectancy") +
  title("interval * range: the same numbers, claiming less")
(data(gapminder_2007) + interval * range + x(col.continent) + y(col.life) +
  y_label("Life expectancy") +
  title("interval * range: the same numbers, claiming less"))
data(gapminder_2007) + interval * range + x(:continent) + y(:life) +
  y_label("Life expectancy") +
  title("interval * range: the same numbers, claiming less")
plot(data(gapminder_2007), layer(interval, range), x(col.continent),
  y(col.life), y_label("Life expectancy"),
  title("interval * range: the same numbers, claiming less"))
Asia Europe Africa Americas Oceania 40 50 60 70 80 interval * range: the same numbers, claiming less Life expectancy Continent

“Given gapminder 2007: intervals derived by range, x is continent, y is life.”

Each whisker spans the same lowest and highest values as the band, and nothing is drawn between the continents.

15.11 The violin: a distribution per category

A range on a category gives two numbers per group, the lowest and the highest. density gives a whole shape per group, and a ribbon can draw that shape. The curve runs along y, the measure. Its height at each life expectancy is drawn as a width across the continent’s slot, on both sides, so the two halves are the same curve facing opposite ways. That is the violin plot (Hintze & Nelson, 1998), and it is the band across the continents drawn above, with density in place of range and without the line:

data(gapminder_2007) + ribbon * density + x(continent) + y(life) +
  y_label("Life expectancy") +
  title("ribbon * density: one distribution per continent")
(data(gapminder_2007) + ribbon * density + x(col.continent) + y(col.life) +
  y_label("Life expectancy") +
  title("ribbon * density: one distribution per continent"))
data(gapminder_2007) + ribbon * density + x(:continent) + y(:life) +
  y_label("Life expectancy") +
  title("ribbon * density: one distribution per continent")
plot(data(gapminder_2007), layer(ribbon, density), x(col.continent),
  y(col.life), y_label("Life expectancy"),
  title("ribbon * density: one distribution per continent"))
Asia Europe Africa Americas Oceania 40 60 80 ribbon * density: one distribution per continent Life expectancy Continent

“Given gapminder 2007: ribbons derived by density, x is continent, y is life.”

density is not the ribbon’s own transform. Five other marks draw the same estimated curve, and only the shape changes. That is the No Exceptions law: a mark draws whatever rows a transform returns, in the mark’s own way.

Mark derived by density Shape
line * density the curve as a stroke
step * density the curve as a staircase
area * density the curve filled to the baseline
point * density one dot at each point the curve was estimated
bar * density one thin bar at each of those points
ribbon * density the curve mirrored, the violin above

line, step and area also read a category on one axis, exactly as the violin does. point and bar need a continuous axis instead: one dot or one thin bar per estimated point cannot be read inside a category’s slot, so gog refuses both there.

Nothing new was added to the grammar to draw the violin. A ribbon closes on a second boundary the data supplies, and a violin closes on the mirror image of its own curve. density estimates a distribution, and this plot computes one estimate for each group. There is no violin word to learn. Such a word would have to say four things at once: the mark, the statistic, the mirror and the slot. The sentence already says all four: ribbon is the mark and supplies the mirror, density is the statistic, and the category on x gives the slot.

Compare it with the box drawn from the same two columns:

data(gapminder_2007) + box + x(continent) + y(life) +
  y_label("Life expectancy") + title("box + x + y: the same distributions, summarized")
(data(gapminder_2007) + box + x(col.continent) + y(col.life) +
  y_label("Life expectancy") + title("box + x + y: the same distributions, summarized"))
data(gapminder_2007) + box + x(:continent) + y(:life) +
  y_label("Life expectancy") +
  title("box + x + y: the same distributions, summarized")
plot(data(gapminder_2007), box, x(col.continent), y(col.life),
  y_label("Life expectancy"),
  title("box + x + y: the same distributions, summarized"))
Asia Europe Africa Americas Oceania 40 50 60 70 80 box + x + y: the same distributions, summarized Life expectancy Continent

“Given gapminder 2007: boxes, x is continent, y is life.”

A box gives you five numbers you can name. The violin gives you the shape those five numbers summarize. Africa’s violin is widest near 52 years, and it has a second, smaller bulge near 73. Africa’s median is 53, and none of the box’s five numbers shows that second cluster.

15.11.1 What the width means

Every group’s density integrates to 1 on its own. If that were the whole estimate, the widths would say nothing about how many rows each group has. A continent of two countries would be drawn as large as a continent of fifty-two. So by default the estimate is weighted by the group’s row count, and width means the same thing across the whole panel. Oceania holds two countries, so it is the narrowest of the five violins.

The weighting is on the shape’s area, the space it covers, not on its width, which is why Europe’s violin is wider than Africa’s. Europe’s thirty countries differ in life expectancy by no more than ten years, so Europe’s area is packed into a short stretch of the axis and the shape has to be wide. Africa’s fifty-two countries cover thirty-seven years of life expectancy, so its larger area is spread across a much longer stretch of the axis. What the width reports at any height is countries per year of life expectancy, and that reading is the same in every slot.

compare = "shape" removes that weighting and draws every violin to the same area. It is the right choice when your question is about the shapes alone. It is the wrong one when a group whose values are packed together is drawn so wide that every other violin is drawn thin:

data(gapminder_2007) + ribbon * density(compare = "shape") + x(continent) + y(life) +
  y_label("Life expectancy") +
  title('density(compare = "shape"): every violin the same area')
(data(gapminder_2007) + ribbon * density(compare = "shape") + x(col.continent) + y(col.life) +
  y_label("Life expectancy") +
  title('density(compare = "shape"): every violin the same area'))
data(gapminder_2007) + ribbon * density(compare = "shape") +
  x(:continent) + y(:life) + y_label("Life expectancy") +
  title("density(compare = \"shape\"): every violin the same area")
plot(data(gapminder_2007), layer(ribbon, density({ compare: "shape" })),
  x(col.continent), y(col.life), y_label("Life expectancy"),
  title("density(compare = \"shape\"): every violin the same area"))
Asia Europe Africa Americas Oceania 40 60 80 density(compare = "shape"): every violin the same area Life expectancy Continent

“Given gapminder 2007: ribbons derived by density compared by shape, x is continent, y is life.”

Oceania is now the widest violin and Africa the thinnest, so two countries are drawn wider than fifty-two. Oceania’s two values are one year apart, so its shape covers the shortest stretch of the axis. To hold the same area as the others in that short stretch, it has to be the widest. The widest shape in a panel sets the scale, so every other violin is drawn in proportion to it. Each plot answers a different question, so gog weights each area by row count unless you write compare = "shape".

15.11.2 Compared with ggplot2’s violin

If you have used ggplot2, you will notice that gog’s violins differ from those geom_violin() draws on the same data. Three choices cause the difference, and gog makes each one on purpose.

First, the ends narrow to a point. A violin is the density curve mirrored, and gog draws the whole curve. The curve runs three bandwidths past each group’s smallest and largest value. The bandwidth is the width of the bell-shaped kernel that density places on each value. No row in the table lies in these tails. Oceania’s two countries lie at 80.2 and 81.2 years, but with the bandwidth of 2.04 years that all five violins share, its violin runs from 74.1 to 87.4. Split by color(continent), line * density draws these same tails, so the violin and the curve show one estimate. geom_violin() stops each violin at its group’s smallest and largest value, so its ends are flat. With trim = FALSE, it draws tails the same way, three bandwidths long.

Second, each violin’s area is weighted by its row count, as What the width means explains. geom_violin() draws every violin to the same area, which is what compare = "shape" draws in gog.

Third, every violin in a plot is smoothed by one bandwidth, so a difference in shape comes from the data and not from the smoothing (One transform, four readings). geom_violin() gives each violin its own bandwidth.

In gog, only the second choice can be changed to match geom_violin(). The ends always narrow to a point, and every violin shares one bandwidth, whether gog chooses it or you set it.

15.11.3 Sideways, and halved

A band cannot turn, because its x is always the domain. A violin can: density makes the category a slot, and gog reads the orientation from the bindings, as it does for a bar or a box. A category on y is the form with room for long category names:

data(gapminder_2007) + ribbon * density + x(life) + y(continent) +
  x_label("Life expectancy") + title("The same violins, turned sideways")
(data(gapminder_2007) + ribbon * density + x(col.life) + y(col.continent) +
  x_label("Life expectancy") + title("The same violins, turned sideways"))
data(gapminder_2007) + ribbon * density + x(:life) + y(:continent) +
  x_label("Life expectancy") + title("The same violins, turned sideways")
plot(data(gapminder_2007), layer(ribbon, density), x(col.life),
  y(col.continent), x_label("Life expectancy"),
  title("The same violins, turned sideways"))
40 60 80 Oceania Americas Africa Europe Asia The same violins, turned sideways Continent Life expectancy

An area draws the same estimate closed on the slot’s center line instead of on its mirror image. So area draws one half of the shape, and ribbon draws both halves:

data(gapminder_2007) + area * density + x(continent) + y(life) +
  y_label("Life expectancy") + title("area * density: the half violin")
(data(gapminder_2007) + area * density + x(col.continent) + y(col.life) +
  y_label("Life expectancy") + title("area * density: the half violin"))
data(gapminder_2007) + area * density + x(:continent) + y(:life) +
  y_label("Life expectancy") + title("area * density: the half violin")
plot(data(gapminder_2007), layer(area, density), x(col.continent),
  y(col.life), y_label("Life expectancy"),
  title("area * density: the half violin"))
Asia Europe Africa Americas Oceania 40 60 80 area * density: the half violin Life expectancy Continent

“Given gapminder 2007: areas derived by density, x is continent, y is life.”

There are two choices here, and they are made independently: which mark closes the region, and which axis carries the category. That gives four plots. The one this chapter has not drawn yet, an area with the category on y, has a name of its own. Halve the violin and turn it sideways. Each category then gets a filled curve rising from its slot’s center line, above one shared measure axis. That is the ridgeline plot:

data(gapminder_2007) + area * density + x(life) + y(continent) +
  x_label("Life expectancy") + title("area * density + x(life): the ridgeline")
(data(gapminder_2007) + area * density + x(col.life) + y(col.continent) +
  x_label("Life expectancy") + title("area * density + x(life): the ridgeline"))
data(gapminder_2007) + area * density + x(:life) + y(:continent) +
  x_label("Life expectancy") +
  title("area * density + x(life): the ridgeline")
plot(data(gapminder_2007), layer(area, density), x(col.life),
  y(col.continent), x_label("Life expectancy"),
  title("area * density + x(life): the ridgeline"))
40 60 80 Oceania Americas Africa Europe Asia area * density + x(life): the ridgeline Continent Life expectancy

Nothing was added for it either. It is the half violin’s sentence with x and y exchanged.

Read the four plots together and you can see what orthogonality gives you here. One idea draws all four plots: density estimated per category, filled on one side of the slot’s center line or on both, with the category on either axis. A word for the violin and a word for the ridgeline would name two of the four. The other two would have no word, and neither would the idea behind all four.

15.11.4 Letting the ridges overlap

The ridgeline above keeps every ridge inside its own slot. That comes from how far a shape reaches by default. The widest shape in a panel reaches 0.4 of a slot from the slot’s center line, and every other shape is drawn in proportion to it. A slot ends 0.5 from that line, so each shape stays inside its own slot, with empty space before the next category. Bars follow the same rule, for the same reason: the gap shows that the categories are separate groups, not pieces of one continuous scale.

A ridgeline, as it is usually drawn, breaks that rule on purpose: its ridges are tall enough to reach into the slot above, and that overlap is part of the design. density(reach = ) says how far, in slots:

data(gapminder_2007) + area * density(reach = 2.5) + x(life) + y(continent) +
  x_label("Life expectancy") + title("reach = 2.5: each ridge two and a half slots tall")
(data(gapminder_2007) + area * density(reach = 2.5) + x(col.life) + y(col.continent) +
  x_label("Life expectancy") + title("reach = 2.5: each ridge two and a half slots tall"))
data(gapminder_2007) + area * density(reach = 2.5) + x(:life) +
  y(:continent) + x_label("Life expectancy") +
  title("reach = 2.5: each ridge two and a half slots tall")
plot(data(gapminder_2007), layer(area, density({ reach: 2.5 })),
  x(col.life), y(col.continent), x_label("Life expectancy"),
  title("reach = 2.5: each ridge two and a half slots tall"))
40 60 80 Oceania Americas Africa Europe Asia reach = 2.5: each ridge two and a half slots tall Continent Life expectancy

“Given gapminder 2007: areas derived by density reaching 2.5 slots, x is life, y is continent.”

The number is measured from the slot’s center line to the shape’s furthest point. That is why the number means the same thing to both marks. A ribbon reaches that far on each side, an area on one side only. The half violin therefore stays exactly half of the violin, at any reach. Past 0.5 the shapes leave their own slots. gog does not refuse this, because a reach above 0.5 is what your sentence asks for. Where two ridges overlap, the lower one is drawn later and covers the one above it.

The category axis grows to hold the ridges. Normally the axis leaves half a slot of space beyond the first and last categories, which is enough for a bar or a box, since each stays inside its slot. A ridge 2.5 slots tall does not stay in its slot, so the axis adds the room it needs. Without that room, the panel edge would hide the top of the highest ridge.

The edge is a layer here, as it is for every filled band in this chapter. Neither area nor ribbon takes a border setting. A line given the same transform traces the outline of the fill. That is the Filled, or two dashed edges rule again, this time inside a category’s slot:

data(gapminder_2007) + x(life) + y(continent) +
  area * density(reach = 2.5) + style(color = "steelblue", opacity = 0.9) +
  line * density(reach = 2.5) + style(color = "black", size = 1) +
  x_label("Life expectancy") + title("The same ridges, with their edges drawn")
(data(gapminder_2007) + x(col.life) + y(col.continent) +
  area * density(reach = 2.5) + style(color = "steelblue", opacity = 0.9) +
  line * density(reach = 2.5) + style(color = "black", size = 1) +
  x_label("Life expectancy") + title("The same ridges, with their edges drawn"))
data(gapminder_2007) + x(:life) + y(:continent) +
  area * density(reach = 2.5) +
  style(color = "steelblue", opacity = 0.9) +
  line * density(reach = 2.5) + style(color = "black", size = 1) +
  x_label("Life expectancy") +
  title("The same ridges, with their edges drawn")
plot(data(gapminder_2007), x(col.life), y(col.continent),
  layer(area, density({ reach: 2.5 })),
  style({ color: "steelblue", opacity: 0.9 }),
  layer(line, density({ reach: 2.5 })),
  style({ color: "black", size: 1 }), x_label("Life expectancy"),
  title("The same ridges, with their edges drawn"))
40 60 80 Oceania Americas Africa Europe Asia The same ridges, with their edges drawn Continent Life expectancy

Both layers read the same density(reach = 2.5), so the stroke lies exactly on the outline of the fill. That is why no border setting is needed: a layer draws the outline, with its own color, width and dash. Color each ridge by its own category and the plot is complete:

data(gapminder_2007) + x(life) + y(continent) +
  area * density(reach = 2.2) + color(continent) +
  line * density(reach = 2.2) + style(color = "black", size = 0.9) +
  x_label("Life expectancy") + title("One hue per ridge")
(data(gapminder_2007) + x(col.life) + y(col.continent) +
  area * density(reach = 2.2) + color(col.continent) +
  line * density(reach = 2.2) + style(color = "black", size = 0.9) +
  x_label("Life expectancy") + title("One hue per ridge"))
data(gapminder_2007) + x(:life) + y(:continent) +
  area * density(reach = 2.2) + color(:continent) +
  line * density(reach = 2.2) + style(color = "black", size = 0.9) +
  x_label("Life expectancy") + title("One hue per ridge")
plot(data(gapminder_2007), x(col.life), y(col.continent),
  layer(area, density({ reach: 2.2 })), color(col.continent),
  layer(line, density({ reach: 2.2 })),
  style({ color: "black", size: 0.9 }), x_label("Life expectancy"),
  title("One hue per ridge"))
40 60 80 Oceania Americas Africa Europe Asia One hue per ridge Continent Life expectancy Continent Asia Europe Africa Americas Oceania

The color(continent) is written after the area, so it belongs to that layer alone. Written before both marks it would color the line too. A layer cannot both map color and set it, so the sentence would be refused. The place where a channel is written in the sentence decides its scope, and Encoding scope explains that in full.

15.12 What you can set

A setting changes how the band looks without reading a column, and each mark takes its own. These are a ribbon’s, with the values each accepts:

Setting Value
style(color = ) any CSS color name or hex
style(opacity = ) 0 to 1
style(pattern = ) solid, hatch, crosshatch, stripes, grid, dots

A ribbon takes area’s list, and for area’s reasons. There is no size, because the band’s shape comes from the two boundaries its transform supplies. There is no border, because the edge is a line traced along the same pair.

One setting this chapter has not shown on a band is a fill pattern. Here is a band hatched with a grid, with a dashed line along both of its boundaries. The plot uses the score_band table, shown in A pre-computed band:

data(score_band) + x(score) +
  ribbon * bounds(lower, upper) +
    style(color = "slateblue", pattern = "grid", opacity = 0.45) +
  line * bounds(lower, upper) + style(color = "slateblue", pattern = "dashed") +
  y_label("Expected true score") + title("A textured band, and its two edges")
(data(score_band) + x(col.score) +
  ribbon * bounds(col.lower, col.upper) +
    style(color = "slateblue", pattern = "grid", opacity = 0.45) +
  line * bounds(col.lower, col.upper) + style(color = "slateblue", pattern = "dashed") +
  y_label("Expected true score") + title("A textured band, and its two edges"))
data(score_band) + x(:score) + ribbon * bounds(:lower, :upper) +
  style(color = "slateblue", pattern = "grid", opacity = 0.45) +
  line * bounds(:lower, :upper) +
  style(color = "slateblue", pattern = "dashed") +
  y_label("Expected true score") +
  title("A textured band, and its two edges")
plot(data(score_band), x(col.score),
  layer(ribbon, bounds(col.lower, col.upper)),
  style({ color: "slateblue", pattern: "grid", opacity: 0.45 }),
  layer(line, bounds(col.lower, col.upper)),
  style({ color: "slateblue", pattern: "dashed" }),
  y_label("Expected true score"),
  title("A textured band, and its two edges"))
0 5 10 15 20 0 10 20 A textured band, and its two edges Expected true score Score

“Given the score band: x is score, a ribbon from lower to upper, with pattern grid, and also lines from lower to upper, with pattern dashed.”

Both marks read the same bounds(lower, upper) there, exactly as the two ridge layers did, so the edges stay on the band. The two pattern values follow the rule from A pre-computed band: a hatch on the fill, a dash on the stroke.

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.

15.13 What it refuses

The first refusal concerns the measure, which must be continuous. A band from range spans a low value to a high one, and both are quantities, so a category on y is refused. The violins above put a category on y, but there density makes it a slot rather than the measure:

data(gapminder_2007) + ribbon * range + x(life) + y(continent)
data(gapminder_2007) + ribbon * range + x(col.life) + y(col.continent)
data(gapminder_2007) + ribbon * range + x(:life) + y(:continent)
plot(data(gapminder_2007), layer(ribbon, range), x(col.life),
  y(col.continent))
Error:
! gog: `y(continent)` maps a categorical (text) column, but `y` on `ribbon` needs a continuous (numeric) column. On these marks `x` is the domain and `y` the measure, and a category is not a quantity to measure: a mean of category names is not a number, and a region has no categorical baseline to close on. Put the category on `x` instead — `line * mean + x(<category>) + y(<number>)` is the profile plot, and `area * mean` fills it. Unlike `bar`/`box`/`interval`, these marks do not read their orientation off the bindings, because their two axes do not have the same role.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

A bare ribbon is the second refusal. A plain y() supplies one number, and a band needs two boundaries, as Why a ribbon needs a transform explained. The engine says so rather than drawing an empty panel:

data(gapminder_asia) + ribbon + x(year) + y(life)
data(gapminder_asia) + ribbon + x(col.year) + y(col.life)
data(gapminder_asia) + ribbon + x(:year) + y(:life)
plot(data(gapminder_asia), ribbon, x(col.year), y(col.life))
Error:
! gog: `ribbon` draws a span from a low value to a high one, but nothing here produces those extents. Add a range transform — `ribbon * range + x(t) + y(value)` draws a band between the min and max at each x, or `ribbon * 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.

A mapped opacity is the third. The inside of the band belongs to no single row, as One region, one fill explained. Opacity is set with style(opacity = ) and never mapped:

data(gapminder_asia) + ribbon * range + x(year) + y(life) + opacity(gdp)
data(gapminder_asia) + ribbon * range + x(col.year) + y(col.life) + opacity(col.gdp)
data(gapminder_asia) + ribbon * range + x(:year) + y(:life) +
  opacity(:gdp)
plot(data(gapminder_asia), layer(ribbon, range), x(col.year), y(col.life),
  opacity(col.gdp))
Error:
! gog: `opacity` cannot be bound to `ribbon` — a ribbon takes one opacity for the whole layer, not one per row. Drop the `gdp` mapping and set it with `style(opacity = )`, or use a mark that maps `opacity`: `point`, `bar`, `text` or `surface`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

The fourth refusal concerns the positions. A band needs at least two along x in each series, each with a low and a high value. A plot in which every series holds one would draw nothing, so gog refuses it, as Line shows. Its message suggests interval * range, which draws a single position as a whisker.

15.14 In short

A ribbon fills the space between two boundaries, and refuses to draw until a transform supplies them. The transform decides what the band means: the extremes from range, the middle from two quantiles, the mean’s uncertainty from confidence, and a band computed elsewhere from bounds. Over a category, density makes the ribbon a violin. Both edges come from the data, so the band’s shape is never a setting.

15.15 What comes next

A ribbon shows a region and names nothing inside it. Text draws a string at each position, read from a column.