| 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 |
34 Nest
What share of the whole is each part? A plot has to show that share with something you can see. On a flat panel the share is a length: a segment’s height as a fraction of the column it is in. On a panel bent into a circle it is an angle, and the column becomes a pie. There is a third way, and it fits the most categories into the least space: area.
That is what nest() does. Each row’s measure becomes the size of a region, and the regions fill the panel with no gaps. Dividing a panel that way is a packing. The plot has a name, treemap, and as with the pie there is no atom that spells it.
The treemap began with a real and ordinary problem. In 1990 the 80-megabyte hard disk in Ben Shneiderman’s lab, shared by 14 people, was full, and a directory listing could not show what was filling it. So he worked out a way to draw the disk as a rectangle packed with nested rectangles, each sized by what it holds (Shneiderman, 1992). The treemap is now the usual way to draw a share of a whole when there are more categories than a pie has room for. This chapter teaches that plot.
34.1 One sentence, three spaces
How is the world’s population divided among the continents? You already know one plot that shows a share: the stacked bar. Start there, because the same sentence will answer the question three times. Here is the world’s population as one stacked bar, a single column divided into one part for each continent:
data(gapminder_2007) + bar * sum * stack + y(population) + color(continent) +
title("A column divided")(data(gapminder_2007) + bar * sum * stack + y(col.population) + color(col.continent) +
title("A column divided"))data(gapminder_2007) + bar * sum * stack + y(:population) +
color(:continent) + title("A column divided")plot(data(gapminder_2007), layer(bar, sum, stack), y(col.population),
color(col.continent), title("A column divided"))“Given gapminder 2007: bars derived by sum and stack, y is population, color by continent.”
Now the same sentence in the circle. The column’s height becomes the whole turn, and each segment’s share of the height becomes its slice’s share of the circle:
data(gapminder_2007) + bar * sum * stack + y(population) + color(continent) +
polar() + title("The same shares, as angles")(data(gapminder_2007) + bar * sum * stack + y(col.population) + color(col.continent) +
polar() + title("The same shares, as angles"))data(gapminder_2007) + bar * sum * stack + y(:population) +
color(:continent) + polar() + title("The same shares, as angles")plot(data(gapminder_2007), layer(bar, sum, stack), y(col.population),
color(col.continent), polar(), title("The same shares, as angles"))“Given gapminder 2007: bars derived by sum and stack, y is population, color by continent, in polar.”
And the same sentence again, packed into areas. One word has gone from it, and the end of this section says which word and why:
data(gapminder_2007) + bar * sum + y(population) + color(continent) +
nest() + title("The same shares, as areas")(data(gapminder_2007) + bar * sum + y(col.population) + color(col.continent) +
nest() + title("The same shares, as areas"))data(gapminder_2007) + bar * sum + y(:population) + color(:continent) +
nest() + title("The same shares, as areas")plot(data(gapminder_2007), layer(bar, sum), y(col.population),
color(col.continent), nest(), title("The same shares, as areas"))“Given gapminder 2007: bars derived by sum, y is population, color by continent, in the nest.”
Three pictures, one grammar. No sentence names a chart. Each says a bar, summed and colored by continent, and then its space: polar(), nest(), or no word at all for the flat page. Asia is three fifths of the world’s population in all three, and you can check that against the column, the pie and the treemap in turn.
The word missing from the third sentence is stack: the flat and polar forms carry it, and the packed form does not. stack decides what happens when two marks are drawn at the same place, so it assumes two marks can share a place. A packing gives every region a place of its own, so two marks never collide. stack would have nothing to resolve, and gog refuses it here.
34.2 Why area, when a column already works
Read the column again and look for Oceania. It holds 0.4% of the world’s population, so its segment is about two pixels tall. The border drawn around it takes about a quarter of that height. In the packing the same share is a region about six pixels wide, because a column has only its length to divide, and a packing divides both width and height.
Oceania’s region is three times as wide as its segment was tall, and it still reports the same 0.4%. With only five parts, that extra width is all a packing adds. The advantage grows with the number of parts. A packing is for the case where there are many shares and the question is which of them are large. In a column, every new category makes the segments thinner, and small ones become too thin to see long before they would in a packing.
Area has one weakness, the same one the pie has: the eye compares two lengths more accurately than two areas. If the comparison has to be precise, draw the flat column.
34.3 Two levels, from a position
How much did each continent grow between 1957 and 2007? A packing can show that by nesting one level of regions inside another. Binding a position is what adds the outer level. Each category gets a region sized by its own total, and the rows inside it are packed into that region. Here each continent’s region is divided between its two years:
| 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) + bar * sum + x(continent) + y(population) + color(era) +
nest() + title("Two eras, inside each continent")(data(gm_eras) + bar * sum + x(col.continent) + y(col.population) + color(col.era) +
nest() + title("Two eras, inside each continent"))data(gm_eras) + bar * sum + x(:continent) + y(:population) + color(:era) +
nest() + title("Two eras, inside each continent")plot(data(gm_eras), layer(bar, sum), x(col.continent), y(col.population),
color(col.era), nest(), title("Two eras, inside each continent"))“Given the gapminder eras: bars derived by sum, x is continent, y is population, color by era, in the nest.”
Each continent’s region is sized by its two years added together. No continent ever had that population at one time, so the region’s size is only a frame, and its two parts are what the plot compares. The thicker light lines mark the continents, and the thinner ones divide each continent between the two eras. The legend decodes only the eras, and no continent is named in the plot. Asia fills the left side. Europe, Africa and the Americas fill the right side, from top to bottom, and Oceania is a thin strip, about seven pixels wide, at the bottom right. Africa’s region is the most uneven: more than three quarters of it is the 2007 part, because its population more than tripled in those fifty years.
The nesting needs no new word. Levels inside levels are called a dimension hierarchy, and Wilkinson shows that a dimension hierarchy is a nested facet (Wilkinson, 2005). gog’s facets always cross, so here the nesting happens inside one panel. x divides that panel as an outer facet would, but by share, not into panels of equal size.
The inner level comes from color, and the legend is what decodes it. So the inner level needs few enough values to fit in the legend beside the plot. A common treemap puts countries inside continents, and its legend would need 142 entries. The section Naming the regions, later in this chapter, shows how to write each name inside its own region instead.
34.4 What a packed panel does not have
Look for the axes. There are none, and no setting adds them.
A flat panel’s horizontal direction means something: it stands for a variable, and a tick is a place on it. In a packing neither direction is a variable. Wilkinson states it exactly: the two dimensions of a treemap “have no intrinsic meaning related to the data because they can be reordered without changing the metric properties of the tree” (Wilkinson, 2005). That two regions touch says nothing about how close they are in the data. Moving one to the other side of the panel would not change any share the plot reports.
So there is no tick to place and no axis label to print, and gog refuses x_label():
data(gapminder_2007) + bar * sum + y(population) + color(continent) + nest() +
x_label("Population")(data(gapminder_2007) + bar * sum + y(col.population) + color(col.continent) + nest() +
x_label("Population"))data(gapminder_2007) + bar * sum + y(:population) + color(:continent) +
nest() + x_label("Population")plot(data(gapminder_2007), layer(bar, sum), y(col.population),
color(col.continent), nest(), x_label("Population"))Error:
! gog: `x_label()` names an axis, and a `nest()` plot has none — its two directions carry no variable and can be reordered without changing what the plot says. Use `title()` for the plot's own name, and the color legend to say what the regions are.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The color legend decodes a packed panel, exactly as it decodes a pie.
34.5 Naming the regions
A legend works while it has few enough entries to read. With many entries, the reader has to search the legend for each color, and 142 countries are far too many to search. So put the name where the region is. This sentence has no sum: without it, each row is its own region, so every country gets one inside its continent.
data(gapminder_2007) + bar + x(continent) + y(population) + color(continent) +
text + label(country) + style(color = "white") + nest() +
title("Countries inside continents")(data(gapminder_2007) + bar + x(col.continent) + y(col.population) + color(col.continent) +
text + label(col.country) + style(color = "white") + nest() +
title("Countries inside continents"))data(gapminder_2007) + bar + x(:continent) + y(:population) +
color(:continent) + text + label(:country) + style(color = "white") +
nest() + title("Countries inside continents")plot(data(gapminder_2007), bar, x(col.continent), y(col.population),
color(col.continent), text, label(col.country),
style({ color: "white" }), nest(), title("Countries inside continents"))“Given gapminder 2007: bars, x is continent, y is population, colored by continent, and also text, label by country, in the nest.”
The sentence has two layers, and the second is the ordinary text mark: label says which column supplies the string, exactly as it does on a scatter. label does not say where to put the name. In a nest, the packing decides. A label goes to the center of the region its own row was packed into. That is the same region the bar drew. Both marks read one packing, so no name is drawn inside another country’s region.
Notice what is missing from that sentence: the text layer carries no x and no y of its own. The sentence’s x(continent) and y(population) place the names instead. x chooses the continent’s region, and y sizes each country’s region inside it. Once a region has a size and a place, the name inside it has a place too. On a flat panel a text mark needs two positions before it can be drawn at all. Here it needs a measure and a string, and the packing places it.
All 142 countries are named. A name that does not fit its region is drawn smaller, at the size that fits. Most of the small names can be read when you zoom in on the plot, and the few smallest stay too small even then. The largest countries come first inside each continent, so their regions are close to square and their names keep their full size. A sunburst names its sectors by the same rule (Naming the nodes).
A share too small to have a region at all has nowhere to put its name. Only then is a name left out, and a message above the plot says how many names were drawn. Without that message, a treemap with a missing name would read as a complete list.
Two things make the small names larger: fewer categories, and a larger plot with theme(width =, height =). Each one gives every region more room. style(size = ) sets the size a name is drawn at when it fits, and a name never grows past its region. gog has no separate setting for fitting names into regions.
34.6 The other refusals
x_label() is one of several refusals in a nest, and the three below come from one fact: a region has an area, and nothing else. A point is drawn at the coordinates it is given, and a packing has no coordinates to give:
data(gapminder_2007) + point + x(gdp) + y(life) + nest()data(gapminder_2007) + point + x(col.gdp) + y(col.life) + nest()data(gapminder_2007) + point + x(:gdp) + y(:life) + nest()plot(data(gapminder_2007), point, x(col.gdp), y(col.life), nest())Error:
! gog: `point` is placed by a position, and a packing has none to give it — its two directions are not axes, and two neighboring regions are not near each other in the data (Wilkinson §13.3.4.1). Drop `nest()` to draw `point` flat, or use `bar` or `text`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
An area cannot be negative. A measure with values below zero cannot be packed into regions. On the flat page, a bar below the baseline points down, and the reader can see the sign. A region has no direction to point in, so nothing in the picture could show the sign. The parts would also no longer fit the whole: in the cashflow table below, the positive parts add up to 165, and the whole is only 101.
| step | delta | total |
|---|---|---|
| Opening | 120 | TRUE |
| Sales | 45 | FALSE |
| Refunds | -18 | FALSE |
| Costs | -32 | FALSE |
| Tax | -14 | FALSE |
data(cashflow) + bar * sum + y(delta) + color(step) + nest()data(cashflow) + bar * sum + y(col.delta) + color(col.step) + nest()data(cashflow) + bar * sum + y(:delta) + color(:step) + nest()plot(data(cashflow), layer(bar, sum), y(col.delta), color(col.step),
nest())Error:
! gog: `delta` has negative values, and a `nest()` plot turns a measure into an **area**, which cannot be negative. Flat, a bar below the baseline reads as a loss; a region has no direction to run in. Filter or offset the column, or draw it flat where the sign is readable.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
A log scale is refused too. A region’s size is its share of the total, and that share is computed from the values as they are. A log scale places the values so that equal distances stand for equal ratios. A packing draws no distances, so a log scale has nothing to change, and gog refuses it.
data(gapminder_2007) + bar * sum + y(population, scale = "log") +
color(continent) + nest()(data(gapminder_2007) + bar * sum + y(col.population, scale = "log") +
color(col.continent) + nest())data(gapminder_2007) + bar * sum + y(:population, scale = "log") +
color(:continent) + nest()plot(data(gapminder_2007), layer(bar, sum),
y(col.population, { scale: "log" }), color(col.continent), nest())Error:
! gog: `y(scale = "log")` with `nest()` — a packed region's size is its **share of the total**, which is arithmetic on the values themselves, so a log scale would change nothing about the picture. Drop the scale, or drop `nest()` to read the values against a log axis.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
34.7 Sorting the regions
Which region goes where? Two things decide: the packing method, which is fixed, and the order of the categories, which you can change. The method is the squarified one of Bruls, Huizing and van Wijk (Bruls et al., 2000): regions are placed in rows, and each row is chosen so its rectangles are as close to square as they can be. A simpler method divides one direction at a time, which makes long thin regions, and a long thin area is the hardest kind to judge. gog uses the squarified method for every packing.
Squarified packing gives the squarest regions when the rows arrive sorted by size, which is why published treemaps sort largest first. Inside a region, the rows have no position on any axis, so nothing else gives them an order, and gog packs them largest first. The regions themselves are different. They are the categories of a position, and they follow the order in which the continents first appear in the table, or a declared order if the column has one. To sort them, you add order():
data(gapminder_2007) + bar * sum + x(continent) + y(population) +
color(continent) + order(population, desc = TRUE) + nest() +
title("Largest continent first")(data(gapminder_2007) + bar * sum + x(col.continent) + y(col.population) +
color(col.continent) + order(col.population, desc = True) + nest() +
title("Largest continent first"))data(gapminder_2007) + bar * sum + x(:continent) + y(:population) +
color(:continent) + order(:population, desc = true) + nest() +
title("Largest continent first")plot(data(gapminder_2007), layer(bar, sum), x(col.continent),
y(col.population), color(col.continent),
order(col.population, { desc: true }), nest(),
title("Largest continent first"))“Given gapminder 2007: bars derived by sum, x is continent, y is population, color by continent, ordered by population, largest first, in the nest.”
That is the same order() that reorders a bar chart, and it does the same work here. If the packing sorted on its own, order() would change nothing in this space. The same word would then mean one thing on a bar chart and nothing in a nest.
order() sorts the categories on a position, x or y, so a plot needs a categorical position before it can be sorted. In a nest that position draws no axis, but its categories still have an order. A one-level treemap binds no position at all, so there the atom has nothing to sort:
data(gapminder_2007) + bar * sum + y(population) + color(continent) +
order(population, desc = TRUE) + nest()(data(gapminder_2007) + bar * sum + y(col.population) + color(col.continent) +
order(col.population, desc = True) + nest())data(gapminder_2007) + bar * sum + y(:population) + color(:continent) +
order(:population, desc = true) + nest()plot(data(gapminder_2007), layer(bar, sum), y(col.population),
color(col.continent), order(col.population, { desc: true }), nest())Error:
! gog: `order(population)` sorts a **categorical position axis**, and this plot has none — so there is nothing for it to put in order. Bind a category to `x` or `y`, or, if what you meant was the order of a color split, declare the column's levels in the table: a factor in R, `ordered()` in Python, Julia and JavaScript.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
That refusal does not come from the packed space. It comes from order(), and it applies in every space.
34.8 What comes next
Regions in a nest are sized by the measure, and the packing decides where each one goes. Map places a mark at the point its own row names instead: longitude and latitude, projected onto the page.