11  Area

How large was the quantity at each date? area fills the region between the data and a baseline at zero. Its top edge is the boundary a line would draw. The fill below that edge is what makes the plot read as a quantity rather than as a path.

11.1 Basic usage

How did sales change over five years? Everything in this chapter is added to one sentence: a source, the mark, and its two positions.

actuals: all 5 rows
year sales
2019 120
2020 135
2021 128
2022 152
2023 168
data(actuals) + area + x(year) + y(sales) +
  y_label("Sales") + title("Sales, 2019 to 2023")
(data(actuals) + area + x(col.year) + y(col.sales) +
  y_label("Sales") + title("Sales, 2019 to 2023"))
data(actuals) + area + x(:year) + y(:sales) + y_label("Sales") +
  title("Sales, 2019 to 2023")
plot(data(actuals), area, x(col.year), y(col.sales), y_label("Sales"),
  title("Sales, 2019 to 2023"))
2019 2020 2021 2022 2023 0 50 100 150 Sales, 2019 to 2023 Sales Year

“Given the actuals: an area, x is year, y is sales.”

Notice the fill reaches the left and right edges of the panel. With point, the axis ends a little past the data, because a dot drawn at the last value would otherwise be cut in half by the panel edge. An area has no dots. Its edge is the data itself, so nothing can be cut off, and the axis ends exactly at the first and last year. Scales states the rule in full.

An area’s two axes are not the same kind of thing, and the rest of this chapter follows from that difference. x is the domain, the axis the area is read along. y is the measure, the quantity it fills to. A line has the same two axes, and the profile section of the Line chapter set the rule. A bar turns on its side when you swap its bindings. A line does not, because its measure is always y. An area follows the same rule, so it has no horizontal form either. The one exception is density, whose curve spreads along whichever axis is continuous; With transforms shows both of its forms.

11.2 The baseline has to be zero

A line says where the value was; an area says how much there was. The fill is the quantity, so it has to start from zero. An area drawn from any other baseline would show a height that measures nothing. gog does not stop you from starting the axis somewhere else. y(sales, limits = c(100, NA)) starts it at 100, the fill starts there too, and no warning is printed. That decision is yours to own: the fill then measures from 100 rather than from zero, and the reader of your plot has to know it. Scales explains limits.

So the choice of mark is about the data, not the look. Sales have a true zero, so the first plot is correct: the fill is the sales. Life expectancy in years has a true zero too, and the rest of this chapter fills it. A quantity whose zero is only a convention has no amount for a fill to show. A temperature in degrees Celsius is one, and so is a calendar year. Draw those with line.

An area and a line draw the same top edge. Drawn with line, split by color as the Line chapter showed, that edge says where each country’s value was, and nothing about how much:

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) + line + x(year) + y(life) + color(country) +
  title("Life expectancy as paths, not quantities")
(data(gapminder_asia) + line + x(col.year) + y(col.life) + color(col.country) +
  title("Life expectancy as paths, not quantities"))
data(gapminder_asia) + line + x(:year) + y(:life) + color(:country) +
  title("Life expectancy as paths, not quantities")
plot(data(gapminder_asia), line, x(col.year), y(col.life),
  color(col.country), title("Life expectancy as paths, not quantities"))
1960 1980 2000 40 50 60 70 80 Life expectancy as paths, not quantities Life Year Country China India Indonesia Japan Korea, Rep.

“Given gapminder Asia: lines, x is year, y is life, color by country.”

11.3 Multiple series

The table holds five countries, and the sentence has to say so. Without a split, the boundary runs through all sixty rows as one region. color splits the mark into one region per category and names each one in a legend. It is the same split that gives line one stroke per category:

data(gapminder_asia) + area + x(year) + y(life) + color(country) +
  style(opacity = 0.45) +
  title("One region per country")
(data(gapminder_asia) + area + x(col.year) + y(col.life) + color(col.country) +
  style(opacity = 0.45) +
  title("One region per country"))
data(gapminder_asia) + area + x(:year) + y(:life) + color(:country) +
  style(opacity = 0.45) + title("One region per country")
plot(data(gapminder_asia), area, x(col.year), y(col.life),
  color(col.country), style({ opacity: 0.45 }),
  title("One region per country"))
1960 1980 2000 0 20 40 60 80 One region per country Life Year Country China India Indonesia Japan Korea, Rep.

“Given gapminder Asia: areas, x is year, y is life, color by country.”

Five lines can cross without hiding each other, because a line is a thin stroke. Five areas cannot: a fill covers whatever lies behind it, and at the default opacity only a little shows through. Write the same sentence with no opacity, and gog prints a warning before it draws:

data(gapminder_asia) + area + x(year) + y(life) + color(country) +
  title("Five regions, no opacity set: the last one drawn covers the rest")
(data(gapminder_asia) + area + x(col.year) + y(col.life) + color(col.country) +
  title("Five regions, no opacity set: the last one drawn covers the rest"))
data(gapminder_asia) + area + x(:year) + y(:life) + color(:country) +
  title("Five regions, no opacity set: the last one drawn covers the rest")
plot(data(gapminder_asia), area, x(col.year), y(col.life),
  color(col.country),
  title("Five regions, no opacity set: the last one drawn covers the rest"))
gog: `area` split by `country` draws 5 regions on top of one another, and the last one drawn can hide the rest. Add `style(opacity = 0.5)` to see through them, or use `line` to compare shapes without filling.
1960 1980 2000 0 20 40 60 80 Five regions, no opacity set: the last one drawn covers the rest Life Year Country China India Indonesia Japan Korea, Rep.

Compare the legend with the plot. Every country is named there, and every region but the last is covered by the ones drawn after it. The warning is advice rather than a refusal: the sentence is legal, so gog draws it.

The other way to draw it is to stack the regions instead of overlaying them. That is written area * stack: stack is a collision modifier. Its sibling dodge subdivides a width, and an area has no width. So of the two, only stack applies to an area. Each region starts at the top of the ones below it, so the five form one band and none is covered:

data(gapminder_asia) + area * stack + x(year) + y(population) + color(country) +
  y_label("Population") +
  title("Five Asian countries, population stacked")
(data(gapminder_asia) + area * stack + x(col.year) + y(col.population) + color(col.country) +
  y_label("Population") +
  title("Five Asian countries, population stacked"))
data(gapminder_asia) + area * stack + x(:year) + y(:population) +
  color(:country) + y_label("Population") +
  title("Five Asian countries, population stacked")
plot(data(gapminder_asia), layer(area, stack), x(col.year),
  y(col.population), color(col.country), y_label("Population"),
  title("Five Asian countries, population stacked"))
1960 1980 2000 0M 1000M 2000M Five Asian countries, population stacked Population Year Country China India Indonesia Japan Korea, Rep.

“Given gapminder Asia: areas derived by stack, x is year, y is population, color by country.”

Each country’s region is as thick as its population, and the band’s top edge is the five-country total. This plot measures population instead of life on purpose. You stack quantities that add: populations add up to a total that means something, and life expectancies do not. gog does not stop you from stacking them. area * stack + y(life) draws five bands to a top edge near 370 years, with no warning, and that number means nothing. So use stack with care: you always get a plot, and only you know whether the parts add up.

Which country’s share of the five grew? The stacked plot cannot say, because every region’s baseline moves with the ones below it. stack(share = TRUE) divides each year’s pile by that year’s total, so the band’s top edge is flat at 1 and each region’s thickness is that country’s share:

data(gapminder_asia) + area * stack(share = TRUE) + x(year) + y(population) +
  color(country) +
  y_label("Share of population") +
  title("Five Asian countries, population as shares")
(data(gapminder_asia) + area * stack(share = True) + x(col.year) + y(col.population) +
  color(col.country) +
  y_label("Share of population") +
  title("Five Asian countries, population as shares"))
data(gapminder_asia) + area * stack(share = true) + x(:year) +
  y(:population) + color(:country) + y_label("Share of population") +
  title("Five Asian countries, population as shares")
plot(data(gapminder_asia), layer(area, stack({ share: true })),
  x(col.year), y(col.population), color(col.country),
  y_label("Share of population"),
  title("Five Asian countries, population as shares"))
1960 1980 2000 0.0 0.2 0.4 0.6 0.8 1.0 Five Asian countries, population as shares Share of population Year Country China India Indonesia Japan Korea, Rep.

“Given gapminder Asia: areas derived by stack as shares, x is year, y is population, color by country.”

India’s share rose from 0.33 to 0.39, and Japan’s fell from 0.08 to 0.05. The total is gone from this plot: every year reaches 1, so it answers the share question and not the size question. Draw both when you need both answers. The Transforms chapter covers stack in full, with a section on stacked areas.

11.4 Giving an area an edge

A fill set to a low opacity has a faint top edge, and the top edge is where the data is. A stroke along it makes the boundary easy to read. An area takes no border of its own. The edge on top of a filled region is a line, so you layer one over the other:

data(actuals) + x(year) + y(sales) +
  area + style(color = "steelblue", opacity = 0.25) +
  line + style(color = "steelblue", size = 2.5) +
  title("area + line: the fill and its edge, composed")
(data(actuals) + x(col.year) + y(col.sales) +
  area + style(color = "steelblue", opacity = 0.25) +
  line + style(color = "steelblue", size = 2.5) +
  title("area + line: the fill and its edge, composed"))
data(actuals) + x(:year) + y(:sales) + area +
  style(color = "steelblue", opacity = 0.25) + line +
  style(color = "steelblue", size = 2.5) +
  title("area + line: the fill and its edge, composed")
plot(data(actuals), x(col.year), y(col.sales), area,
  style({ color: "steelblue", opacity: 0.25 }), line,
  style({ color: "steelblue", size: 2.5 }),
  title("area + line: the fill and its edge, composed"))
2019 2020 2021 2022 2023 0 50 100 150 area + line: the fill and its edge, composed Sales Year

“Given the actuals: x is year, y is sales, an area and also a line.”

There is no border setting on area to use instead. Layering a line gives the edge its own color, width and opacity, which one border setting could not.

11.5 One region, one fill

Can you map a column to an area’s opacity? No, and the reason is the shape. An area is a single filled region, so opacity is a setting here and not a channel. Each row of the table is one corner of the top edge, not a piece of the fill. No part of the region belongs to one row, so a column cannot make one row lighter or darker than another. opacity is a setting on line for the same reason.

size is refused both ways: area neither maps it nor sets it. A stroke has a width, so line lets you set one. An area’s extent comes from its x, its y and its baseline, so there is no size to choose. Both marks take style(color = ) and style(opacity = ), and What it refuses shows the two refusals.

11.6 With transforms

Apart from the stack, every plot so far filled to values already in the table. A transform computes new values first, and the area fills to what it computed. A distribution, for one, can be drawn as a filled shape. The question is the one a histogram answers: how many rows fall in each interval? bin, density and the aggregation family mean the same thing on an area as on a bar. bin counts the rows in each interval. The boundary then runs through the bin centers, and the region below it is filled. That shape is the frequency polygon line * bin draws, with its inside filled:

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) + area * bin + x(life) +
  x_label("Life expectancy (years)") +
  title("area * bin: a filled frequency polygon")
(data(gapminder_2007) + area * bin + x(col.life) +
  x_label("Life expectancy (years)") +
  title("area * bin: a filled frequency polygon"))
data(gapminder_2007) + area * bin + x(:life) +
  x_label("Life expectancy (years)") +
  title("area * bin: a filled frequency polygon")
plot(data(gapminder_2007), layer(area, bin), x(col.life),
  x_label("Life expectancy (years)"),
  title("area * bin: a filled frequency polygon"))
50 60 70 80 0 10 20 30 area * bin: a filled frequency polygon Count Life expectancy (years)

“Given gapminder 2007: an area derived by bin, x is life.”

bin(30) and bin(width = 5) set the intervals here exactly as they do for a bar; Choosing the bin count shows both.

density replaces the counts with a smooth curve, and the area fills the region under it:

data(gapminder_2007) + area * density + x(life) +
  x_label("Life expectancy (years)") +
  title("area * density")
(data(gapminder_2007) + area * density + x(col.life) +
  x_label("Life expectancy (years)") +
  title("area * density"))
data(gapminder_2007) + area * density + x(:life) +
  x_label("Life expectancy (years)") + title("area * density")
plot(data(gapminder_2007), layer(area, density), x(col.life),
  x_label("Life expectancy (years)"), title("area * density"))
40 60 80 0.00 0.01 0.02 0.03 0.04 area * density Density Life expectancy (years)

“Given gapminder 2007: an area derived by density, x is life.”

Give that same sentence a category on x and a number on y, and the curve turns. Life now runs along y, and the estimate is drawn across each category’s slot. That plot is the half violin. The domain takes a category as well as a number, which the next section uses in full. ribbon * density draws the mirrored form, and that section says what the width of a violin can mean:

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

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

The two sentences differ in three places: x moved from life to continent, y(life) was added, and so was color(continent). With only x(life), life runs along x and y is free, so y carries the density estimate, as it carried the count for bin. With a category on x and a number on y, no axis is free, so each estimate is drawn across its category’s slot, one distribution per group. color does what it did in Multiple series: a categorical column gives each region its own fill and a legend. On a line the same channel colors the stroke; on an area it colors the fill. The slots already keep the continents apart, so here color changes only the fill of each one.

Can the two bindings be swapped? For density, yes. Put life on x and the category on y, and each curve lies on its own line instead of standing in its own slot. That is the ridgeline plot, and it is the half violin’s sentence with x and y exchanged:

data(gapminder_2007) + area * density + x(life) + y(continent) +
  color(continent) +
  x_label("Life expectancy") + title("area * density + x(life): the ridgeline")
(data(gapminder_2007) + area * density + x(col.life) + y(col.continent) +
  color(col.continent) +
  x_label("Life expectancy") + title("area * density + x(life): the ridgeline"))
data(gapminder_2007) + area * density + x(:life) + y(:continent) +
  color(: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), color(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 Continent Asia Europe Africa Americas Oceania

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

Ribbon draws all four forms together: one side or two, and the category on either axis.

The aggregation family is the third kind named at the start of this section. An aggregation runs before the fill: it leaves one value per year, and the area fills to those values, exactly as a bar would:

data(gapminder_asia) + area * mean + x(year) + y(life) +
  y_label("Mean life expectancy") +
  title("area * mean: five countries averaged per year")
(data(gapminder_asia) + area * mean + x(col.year) + y(col.life) +
  y_label("Mean life expectancy") +
  title("area * mean: five countries averaged per year"))
data(gapminder_asia) + area * mean + x(:year) + y(:life) +
  y_label("Mean life expectancy") +
  title("area * mean: five countries averaged per year")
plot(data(gapminder_asia), layer(area, mean), x(col.year), y(col.life),
  y_label("Mean life expectancy"),
  title("area * mean: five countries averaged per year"))
1960 1980 2000 0 20 40 60 area * mean: five countries averaged per year Mean life expectancy Year

“Given gapminder Asia: an area derived by mean, x is year, y is life.”

11.7 A category on the domain

That mean ran over years, a continuous domain. Put a category there, and one summary per continent is usually drawn as bars. An area draws the five continent means as one connected shape instead. Use it when the reader should compare the shape rather than five heights, which is what a radar chart shows. Each category gets one slot, the boundary runs from slot to slot in axis order, and the region still fills down to zero:

data(gapminder_2007) + area * mean + x(continent) + y(life) +
  y_label("Mean life expectancy") +
  title("area * mean + x(continent): the filled profile")
(data(gapminder_2007) + area * mean + x(col.continent) + y(col.life) +
  y_label("Mean life expectancy") +
  title("area * mean + x(continent): the filled profile"))
data(gapminder_2007) + area * mean + x(:continent) + y(:life) +
  y_label("Mean life expectancy") +
  title("area * mean + x(continent): the filled profile")
plot(data(gapminder_2007), layer(area, mean), x(col.continent),
  y(col.life), y_label("Mean life expectancy"),
  title("area * mean + x(continent): the filled profile"))
Asia Europe Africa Americas Oceania 0 20 40 60 80 area * mean + x(continent): the filled profile Mean life expectancy Continent

gog draws it. Whether the plot is honest depends on the segments between the categories. Nothing was measured between Africa and the Americas, and a straight segment is drawn there anyway. A line across that gap claims a change. A fill claims a quantity as well, at every point between the two categories. If you read only the vertices, the segments help you compare the five heights. If you read the segments themselves, they claim what was never measured. To compare the five means and nothing else, swap area for bar: one atom away, and no segments to read.

The fill is worth keeping when the shape itself is the message, and a shape can also be drawn on a circular axis. Add polar() and the same sentence draws the filled radar:

data(gapminder_2007) + area * mean + x(continent) + y(life) + polar() +
    title("area * mean + polar(): the filled radar")
(data(gapminder_2007) + area * mean + x(col.continent) + y(col.life) + polar() +
    title("area * mean + polar(): the filled radar"))
data(gapminder_2007) + area * mean + x(:continent) + y(:life) + polar() +
  title("area * mean + polar(): the filled radar")
plot(data(gapminder_2007), layer(area, mean), x(col.continent),
  y(col.life), polar(), title("area * mean + polar(): the filled radar"))
Asia Europe Africa Americas Oceania 20 40 60 80 area * mean + polar(): the filled radar Life Continent

“Given gapminder 2007: an area derived by mean, x is continent, y is life, in polar.”

11.8 What you can set

A setting changes how the region looks without reading a column, and each mark takes its own. These are an area’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, grid, dots

That is the shortest settings list in the book, shared with ribbon. line has one more, size, and bar has two, border_color and border_size. Neither gap is an omission: One region, one fill explains size, and Giving an area an edge explains the border. The one setting this chapter has not shown yet is pattern, a texture on the fill. The edge is still a line:

data(actuals) + x(year) + y(sales) +
  area + style(color = "seagreen", pattern = "hatch", opacity = 0.5) +
  line + style(color = "seagreen", size = 2) +
  y_label("Sales") + title("A textured fill, and a line for the edge")
(data(actuals) + x(col.year) + y(col.sales) +
  area + style(color = "seagreen", pattern = "hatch", opacity = 0.5) +
  line + style(color = "seagreen", size = 2) +
  y_label("Sales") + title("A textured fill, and a line for the edge"))
data(actuals) + x(:year) + y(:sales) + area +
  style(color = "seagreen", pattern = "hatch", opacity = 0.5) + line +
  style(color = "seagreen", size = 2) + y_label("Sales") +
  title("A textured fill, and a line for the edge")
plot(data(actuals), x(col.year), y(col.sales), area,
  style({ color: "seagreen", pattern: "hatch", opacity: 0.5 }), line,
  style({ color: "seagreen", size: 2 }), y_label("Sales"),
  title("A textured fill, and a line for the edge"))
2019 2020 2021 2022 2023 0 50 100 150 A textured fill, and a line for the edge Sales Year

“Given the actuals: x is year, y is sales, an area colored seagreen, with pattern hatch and opacity 0.5, and also a line colored seagreen, with size 2.”

That layered line can be dashed, colored differently, or smoothed, and it traces only the data boundary. A border would also draw a line along the two sides and the baseline.

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.

11.9 What it refuses

The first two refusals come from an axis. The first is the measure. A region fills to a baseline at zero, and a category has none. So a categorical y is refused, and the message asks for the category on x instead:

data(gapminder_2007) + area * mean + x(life) + y(continent)
data(gapminder_2007) + area * mean + x(col.life) + y(col.continent)
data(gapminder_2007) + area * mean + x(:life) + y(:continent)
plot(data(gapminder_2007), layer(area, mean), x(col.life),
  y(col.continent))
Error:
! gog: `y(continent)` maps a categorical (text) column, but `y` on `area` 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.

density is the exception, because its curve spreads along the continuous axis instead of filling to a baseline. With y(continent) it draws the ridgeline in With transforms.

The other refusal is the domain. bin cuts a continuous axis into intervals, and a category is one slot with no width to cut. density needs a continuous axis too, and either x or y can be that axis, so it refuses only when neither is continuous. The message names the transform for categories, count after bin:

data(gapminder_2007) + area * bin + x(continent)
data(gapminder_2007) + area * bin + x(col.continent)
data(gapminder_2007) + area * bin + x(:continent)
plot(data(gapminder_2007), layer(area, bin), x(col.continent))
Error:
! gog: `bin` cuts a continuous axis into intervals, and `x(continent)` is categorical — a category is one slot, with no width to cut. To tally rows per category, `count` is the transform that does it: `bar * count`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

A density on a category names proportion the same way.

The last two refusals come from the fill. A mapped opacity is the third. Each row is one corner of the top edge, so no part of the region belongs to a single row, as One region, one fill explained.

data(gapminder_asia) + area + x(year) + y(life) + opacity(gdp)
data(gapminder_asia) + area + x(col.year) + y(col.life) + opacity(col.gdp)
data(gapminder_asia) + area + x(:year) + y(:life) + opacity(:gdp)
plot(data(gapminder_asia), area, x(col.year), y(col.life),
  opacity(col.gdp))
Error:
! gog: `opacity` cannot be bound to `area` — an area has no opacity feature. Remove the `gdp` mapping from `opacity`, or use a mark that has one.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

A size is the fourth, and it is refused both ways. An area’s extent comes from its x, its y and its baseline, so there is no size to map and none to set:

data(actuals) + area + x(year) + y(sales) + style(size = 4)
data(actuals) + area + x(col.year) + y(col.sales) + style(size = 4)
data(actuals) + area + x(:year) + y(:sales) + style(size = 4)
plot(data(actuals), area, x(col.year), y(col.sales), style({ size: 4 }))
Error:
! gog: `style(size = 4)` — an area has no size to set. Remove it, or use a mark that has one.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

A line accepts a size, because a stroke has a width to choose.