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:

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) + 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"))
0 2B 4B 6B A column divided Population Continent Asia Europe Africa Americas Oceania

“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"))
The same shares, as angles Population Continent Asia Europe Africa Americas Oceania

“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"))
The same shares, as areas Continent Asia Europe Africa Americas Oceania

“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:

gm_eras: first 5 of 284 rows
country continent year life population gdp era
Afghanistan Asia 1957 30.332 9240934 820.8530 1957
Afghanistan Asia 2007 43.828 31889923 974.5803 2007
Albania Europe 1957 59.280 1476505 1942.2842 1957
Albania Europe 2007 76.423 3600523 5937.0295 2007
Algeria Africa 1957 45.685 10270856 3013.9760 1957
data(gm_eras) + 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"))
Two eras, inside each continent Era 1957 2007

“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"))
Afghanistan Albania Algeria Angola Argentina Australia Austria Bahrain Bangladesh Belgium Benin Bolivia Bosnia and Herzegovina Botswana Brazil Bulgaria Burkina Faso Burundi Cambodia Cameroon Canada Central African Republic Chad Chile China Colombia Comoros Congo, Dem. Rep. Congo, Rep. Costa Rica Cote d'Ivoire Croatia Cuba Czech Republic Denmark Djibouti Dominican Republic Ecuador Egypt El Salvador Equatorial Guinea Eritrea Ethiopia Finland France Gabon Gambia Germany Ghana Greece Guatemala Guinea Guinea-Bissau Haiti Honduras Hong Kong, China Hungary Iceland India Indonesia Iran Iraq Ireland Israel Italy Jamaica Japan Jordan Kenya Korea, Dem. Rep. Korea, Rep. Kuwait Lebanon Lesotho Liberia Libya Madagascar Malawi Malaysia Mali Mauritania Mauritius Mexico Mongolia Montenegro Morocco Mozambique Myanmar Namibia Nepal Netherlands New Zealand Nicaragua Niger Nigeria Norway Oman Pakistan Panama Paraguay Peru Philippines Poland Portugal Puerto Rico Reunion Romania Rwanda Sao Tome and Principe Saudi Arabia Senegal Serbia Sierra Leone Singapore Slovak Republic Slovenia Somalia South Africa Spain Sri Lanka Sudan Swaziland Sweden Switzerland Syria Taiwan Tanzania Thailand Togo Trinidad and Tobago Tunisia Turkey Uganda United Kingdom United States Uruguay Venezuela Vietnam West Bank and Gaza Yemen, Rep. Zambia Zimbabwe Countries inside continents Continent Asia Europe Africa Americas Oceania

“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.

cashflow: first 5 of 6 rows
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"))
Largest continent first Continent Asia Europe Africa Americas Oceania

“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.