| 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
How much of the whole does each part take? A share of a whole has to be carried by something you can see. Flat, it is a length: a segment’s height as a fraction of the column it sits in. Bent into a circle, it is an angle, and the column becomes a pie. There is a third answer, and it is the one that fits the most categories into the least space: area.
That is what nest() draws. 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.
That name has an exact and ordinary origin. Ben Shneiderman’s 80-megabyte hard disk was full in 1990, and a directory listing could not tell him what was filling it. So he wrote a display that packs a rectangle with nested rectangles, each sized by what it holds (Shneiderman, 1992). It is now the usual way to draw a share of a whole when there are more categories than a pie has room for. That display is the one this chapter teaches.
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 divided column. Start there. Here is the population of each continent as a single column, divided into its parts:
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 (one word is gone, and the paragraph below says 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. Nothing in any of them names a chart: a bar, summed, colored by continent, and then a word for the space. 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.
That missing word is stack: the flat and polar forms carry it and the packed form does not. stack decides what happens when two marks land in the same place, so it assumes two marks can share a place. A packing gives every region a place of its own, so there is no collision left to resolve, and gog refuses stack here rather than accepting a word with nothing to do.
34.2 Why area, when a column already works
Read the column again and look for Oceania. At 0.4% of the world it is about two pixels tall, so a quarter of that height is the border drawn around it. 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 the bar was tall, and the share it reports is unchanged. With five parts a packing adds little room. It adds more as the parts multiply, because a column divides in one direction and a panel divides in two. A packing is for the case where there are many shares and the question is which of them are large. A column has one fixed length to divide, so it stops showing new categories long before a packing does.
Area has one weakness, the same one the pie has: two lengths can be compared exactly, and two areas cannot. If the comparison has to be precise, draw the flat column.
34.3 Two levels, from a position
A share often has a share inside it: each continent’s population, and within it how much belongs to 1957 and how much to 2007. A packing can nest one split inside another, and binding a position is what adds that second level. Each category gets a region sized by its own total, and the rows inside it are packed into that region:
| 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 is a region sized by its total across both eras, and inside it the two eras are packed against each other. The heavier lines are the continents; the thinner ones divide what is inside them. Africa’s region is the most uneven: more than three quarters of it is the 2007 part.
The nesting is not new vocabulary. Wilkinson makes the point directly (Wilkinson, 2005): a dimension hierarchy (his name for levels inside levels) is a nested facet. So x here is doing what the outer level of a facet does, with the panel divided by share instead of into equal boxes.
The inner level comes from color, and the legend is what decodes it. So the second level needs few enough values to fit in the legend beside the plot. Countries inside continents, the treemap most often printed, would be a legend of 142 entries, and Naming the regions writes each name inside its own region instead.
34.4 What a packed panel does not have
Look for the axes. There are none, and that is what defines the space rather than a setting you can change.
A flat panel’s horizontal direction means something: it is 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). Two regions that touch are not near each other in the data. Moving one to the other side of the panel would not change a single share the plot reports.
So there is nothing to tick and nothing to label, and gog refuses the atom that would say otherwise:
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.
What decodes a packed panel is the color legend, exactly as it decodes a pie.
34.5 Naming the regions
A legend works while there are few enough entries to scan. With more entries it becomes a second thing to read, and 142 countries are far too many. So put the name where the region is. The sentence drops sum as well: 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"))gog: 25 of 142 labels are drawn — 116 are wider than the region they name, and one share is too small to have a region at all. The packing drew every share, so the names are what is missing. Fewer categories, a larger plot (`theme(width =, height =)`) or a smaller `style(size = )` fits more of them in.
“Given gapminder 2007: bars, x is continent, y is population, color by continent, and also text, label by country, in the nest.”
Two layers, and the second is the ordinary text mark: label says which column supplies the string, exactly as it does on a scatter. What it does not say is where to put it, because in this space nothing says where a mark goes. A label goes to the center of the region its own row was packed into. That is the region the bar drew, so both marks read one packing and no name is drawn in another country’s region.
Notice what is missing from that sentence: the text layer carries no x and no y of its own. The plot’s own x(continent) and y(population) do that work for it. They size the regions, and sizing a region is what places its name. 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.
Count the names and there are far fewer than 142, which is what the message above the plot is for. Most of the world’s countries get a region narrower than their own name, so a label that does not fit its rectangle is not drawn. The engine says how many names it drew and why the rest are missing, rather than letting you read the named countries as though they were all of them. A few African countries are named, and most of Africa is not. Every one of those unnamed countries still has a region whose area is exactly its share of the world. The picture is complete and the labeling is not, and the count above the plot is the one you can check against the names.
That message is why the missing names do not mislead you: a treemap that printed only the names that fit, and said nothing, would read as a complete list.
Three things change how many names are drawn, and you have used each of them already: fewer categories, a larger plot with theme(width =, height =), or smaller text with style(size = ). None of them is a setting for labels in particular, and none of them will make a name fit a region narrower than the word.
34.6 The other refusals
A packing cannot hold a mark that is placed by a position, because it has no positions to place one at:
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, so a measure that runs below zero has no packing. 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, and its parts would no longer sum to the whole.
| 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, and the reason is the one worth remembering: a region’s size is its share of the total, which is arithmetic on the values themselves. Re-spacing them changes what a distance means, and a packing has no distances. Accepting the scale would draw exactly the same picture, and gog refuses a word that changes nothing.
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.
The packing method is not a choice you make. It is squarified, after Bruls, Huizing and van Wijk (Bruls et al., 2000): each group of regions is placed so its rectangles are as close to square as they can be. The 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 offers no choice here, because a second layout would add a word that draws nothing new.
34.7 Sorting the regions
Published treemaps usually sort by size, largest first, because the packing produces squarer rectangles that way. gog does not sort them for you. The regions follow the order the categorical axis already has, so the sort is something you write.
data(gm_eras) + bar * sum + x(continent) + y(population) + color(era) +
order(population, desc = TRUE) + nest() + title("Largest continent first")(data(gm_eras) + bar * sum + x(col.continent) + y(col.population) + color(col.era) +
order(col.population, desc = True) + nest() + title("Largest continent first"))data(gm_eras) + bar * sum + x(:continent) + y(:population) + color(:era) +
order(:population, desc = true) + nest() +
title("Largest continent first")plot(data(gm_eras), layer(bar, sum), x(col.continent), y(col.population),
color(col.era), order(col.population, { desc: true }), nest(),
title("Largest continent first"))“Given the gapminder eras: bars derived by sum, x is continent, y is population, color by era, ordered by population, largest first, in the nest.”
That is the same order() that reorders a bar chart, doing the same job here. If the packing sorted on its own, order() would change nothing in this space, and the atom would promise more than it does.
order() sorts a categorical position axis, so a plot needs one before it can be sorted. The one-level treemap above binds no position at all, and 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, set the column's factor levels where the data lives.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
That refusal is not the packed space’s. It is order()’s, and it applies in every space.