33  Polar

Which way does the wind blow most often? Some questions do not fit a flat page. Every plot so far has been drawn on one: an x running across, a y running up. That is one coordinate space, flat, and the kernel names six more. The flat space is also the wrong shape for a whole class of variables. Compass direction, the hour of the day, the month of the year: in each of them the last value is the neighbor of the first. A straight axis has no way to show that.

Two of the oldest statistical graphics were drawn in a circle instead, in the space gog calls polar. Playfair’s Statistical Breviary of 1801 cut a circle into sectors to show the Turkish empire’s territory, and that is the first pie chart (Playfair, 1801). Florence Nightingale’s 1858 diagram of what killed British soldiers in the Crimea (Nightingale, 1858) is a bar chart in polar coordinates. It has three names: a coxcomb, a rose, or, most precisely, a polar area diagram. That third name is the only one that says what the wedges mean. Her wedges put each month’s death rate into the wedge’s area, so their radius grows as the square root of the rate. In gog, a polar bar maps the value directly to the radius. Both are correct drawings, but they are not the same drawing, so check which one you have before you compare them. The section Nightingale’s rose, later in this chapter, draws a rose from her own numbers.

The line version has even more names: radar plot, spider plot, star plot, and Kiviat diagram in engineering. As with the rose and the pie, none of them is an atom. Each is a mark and a channel or two with polar() at the end, which is what the rest of this chapter is about.

33.1 A circular variable on a straight axis

How often the wind blew from each direction is a count per compass point, so start with the plot you know. It shows what a straight axis loses. Here are wind observations from two seasons, summer and winter, drawn as an ordinary bar chart with one bar per compass point:

winds: first 5 of 264 rows
direction bearing speed season
N 19.612260 10.1 Winter
N 357.049499 11.0 Winter
N 349.709737 13.8 Winter
N 2.602499 7.4 Winter
N 338.210715 3.7 Summer
data(winds) + bar * count + x(direction)
data(winds) + bar * count + x(col.direction)
data(winds) + bar * count + x(:direction)
plot(data(winds), layer(bar, count), x(col.direction))
N NE E SE S SW W NW 0 20 40 60 Count Direction

“Given the winds: bars derived by count, x is direction.”

Read the two ends. North is at the far left and north-west at the far right, with the whole compass between them, although the two directions are 45 degrees apart in the world. The chart has cut the circle open and flattened it, and the cut is invisible: nothing in the picture says that the two ends belong together. A reader who did not already know the compass could not tell from this chart that north-west is next to north.

Now add one atom to the same sentence:

data(winds) + bar * count + x(direction) + polar()
data(winds) + bar * count + x(col.direction) + polar()
data(winds) + bar * count + x(:direction) + polar()
plot(data(winds), layer(bar, count), x(col.direction), polar())
N NE E SE S SW W NW 20 40 60 Count Direction

“Given the winds: bars derived by count, x is direction, in polar.”

Same mark, same transform, same binding, same data. The only thing that changed is the space the plot is drawn in, and north-west is beside north because on the compass it is beside north. The west wind, which blew most often, is now a long wedge pointing west, rather than a tall bar you find by reading the labels.

This is what a coordinate space is for, and it is why a pie chart, a rose and a radar plot are not chart types in this grammar. They are marks you already know, read in a circle.

33.2 What polar() changes and what it keeps

One word changed the whole picture, so the next thing to settle is which of the two positions became the angle. x becomes the angle and y becomes the radius. That order is Wilkinson’s (Wilkinson, 2005). His reason is that the first position says where a mark is, and in a circle that is an angle. The second says how much, and that becomes how far out from the center the mark reaches. (That is the rule when both positions are bound. Bind only one and there is nothing to choose, so the one you have becomes the angle: that is the pie, and it has its own section below.)

So x(direction) still says what it always said, that the direction column gives each bar its position. polar() changes one thing only: how positions are read. x now gives a direction around the circle, and y a distance from the center. A bar still runs from its baseline to its value. The baseline is the center of the circle, so the count sets how far out each wedge reaches.

The next question is whether the rest of the grammar still works in a circle: a split by season, for example, with the two seasons side by side. Nothing else in the grammar knows the space changed, so nothing else has to be rewritten for it:

data(winds) + bar * count * dodge + x(direction) + color(season) + polar()
data(winds) + bar * count * dodge + x(col.direction) + color(col.season) + polar()
data(winds) + bar * count * dodge + x(:direction) + color(:season) +
  polar()
plot(data(winds), layer(bar, count, dodge), x(col.direction),
  color(col.season), polar())
N NE E SE S SW W NW 10 20 30 Count Direction Season Summer Winter

“Given the winds: bars derived by count and dodge, x is direction, color by season, in polar.”

color splits the count by season, and dodge sets the two seasons’ bars side by side inside each slot instead of on top of each other. The legend decodes the colors. Every one of those works in a circle exactly as it works on a flat page, because none of them was ever written in terms of pixels.

Change dodge to stack and the seasons pile outward from the center instead, one season outside the other:

data(winds) + bar * count * stack + x(direction) + color(season) + polar()
data(winds) + bar * count * stack + x(col.direction) + color(col.season) + polar()
data(winds) + bar * count * stack + x(:direction) + color(:season) +
  polar()
plot(data(winds), layer(bar, count, stack), x(col.direction),
  color(col.season), polar())
N NE E SE S SW W NW 20 40 60 Count Direction Season Summer Winter

“Given the winds: bars derived by count and stack, x is direction, color by season, in polar.”

Summer starts at the center and winter is stacked outside it, so each wedge now reaches the total of both seasons. West runs out to 63. In the dodged rose above, west stopped at 38, the longer of its two bars. The radial axis now reaches 63 rather than 38. That is why stack is a transform and not a drawing option: a transform runs before the scale is set, so the scale can read the totals stack adds up.

Read the pair together, because each hides what the other shows. Stacked, each wedge shows the total wind from its direction, and west is plainly the most common direction. Dodged, the seasons are side by side, and you can see that the south-west wind comes mostly in summer (30 against 17), and the west wind mostly in winter (38 against 25). Neither is the correct rose. The rose is not a chart with its own options at all. It is bar, with its axis bent.

play also works in a circle, with no new atom. The animation chooses the rows for each frame, and polar() bends those rows into a circle, so neither step changes the other. The Play chapter’s “Frames in polar” animates a rose and two pies across half a century.

33.3 Where the circle starts

The angular axis begins at the top and runs clockwise, which is how a clock and a compass are both read. The middle of the first category’s slot is at the top, not the edge of the slot. That is why north points straight up in the rose above: north is the first of the eight directions.

The day_cycle table holds one day of trips, hour by hour:

day_cycle: first 5 of 25 rows
hour trips
0 31
1 18
2 11
3 8
4 9

The axis starts at the top, so midnight is there. A working day reads better with noon at the top. polar(start = ) turns the whole space by a number of degrees:

data(day_cycle) + line + x(hour) + y(trips) + polar(start = -180)
data(day_cycle) + line + x(col.hour) + y(col.trips) + polar(start = -180)
data(day_cycle) + line + x(:hour) + y(:trips) + polar(start = -180)
plot(data(day_cycle), line, x(col.hour), y(col.trips),
  polar({ start: -180 }))
0 10 20 50 100 150 Trips Hour

“Given the day cycle: a line, x is hour, y is trips, in polar, starting at -180.”

Those are the same trips, turned half a circle. Noon is at the top now and midnight at the bottom, which is the better choice if you are reading the working day rather than the day from midnight to midnight. start is a property of the space rather than of the data, the polar counterpart of the space(turn =, tilt =) that sets the angle a 3-D plot is viewed from (see Space). It changes how you look at the plot, never what is drawn.

On a categorical angle, start points at the first category rather than at the scale’s origin, and the reason is in how a categorical scale is built. A categorical scale runs from half a slot before the first category to half a slot after the last, so the scale’s own origin is padding rather than any place in the data. A start angle aimed at the padding would put north at 22.5 degrees, so you would have to write -180/8 to correct it. A flat categorical axis already puts its tick at the category’s center, so the circle agrees with it: start points at the category, not at the gap before it.

33.4 One turn, whatever the data spans

The angular axis is periodic: one full turn is the whole of the axis, so its first value and its last value land on the same spoke, the same line out from the center. That is what closes the circle without a gap or an empty wedge, and it has one consequence: the circle needs the whole cycle, or the two ends will meet in the wrong place.

The day of trips, turned half a circle above, closes, and the reason is in the table’s first and last rows. It runs from hour 0 to hour 24 with both endpoints present, because midnight is where the day ends and also where it begins:

data(day_cycle) + line + x(hour) + y(trips) + polar()
data(day_cycle) + line + x(col.hour) + y(col.trips) + polar()
data(day_cycle) + line + x(:hour) + y(:trips) + polar()
plot(data(day_cycle), line, x(col.hour), y(col.trips), polar())
0 10 20 50 100 150 Trips Hour

The curve closes at the top because the reading at 24 is the reading at 0, and the two are on the same spoke. That is the first of three cases, and in one of them the circle does not close. Most periodic data does not include both ends. A tide gauge reads every three hours and misses both ends of the day, hour 0 and hour 24. Its axis spans 1 to 22, and the failure is easy to see:

tide: first 5 of 8 rows
hour height
1 2.1
4 4.4
7 3.6
10 1.2
13 0.9
data(tide) + line + x(hour) + y(height) + polar()
data(tide) + line + x(col.hour) + y(col.height) + polar()
data(tide) + line + x(:hour) + y(:height) + polar()
plot(data(tide), line, x(col.hour), y(col.height), polar())
5 10 15 20 1 2 3 4 Height Hour

One turn covers 1 to 22, so hour 1 and hour 22 land on the same spoke. The first and last readings are drawn on top of each other, three hours apart in the world and zero degrees apart on the page. Every reading between them is at the wrong angle too, spread over 21 hours instead of 24.

The axis cannot know how long your cycle is: nothing in a column of hours says that a day has 24 of them. So say it:

data(tide) + line + x(hour, limits = c(0, 24)) + y(height) + polar()
data(tide) + line + x(col.hour, limits = [0, 24]) + y(col.height) + polar()
data(tide) + line + x(:hour, limits = [0, 24]) + y(:height) + polar()
plot(data(tide), line, x(col.hour, { limits: [0, 24] }), y(col.height),
  polar())
0 10 20 1 2 3 4 Height Hour

“Given the tide: a line, x is hour from 0 to 24, y is height, in polar.”

Now each reading is where a 24-hour clock would put it. Hour 1 is a twenty-fourth of a turn past midnight, and hour 22 is a twelfth of a turn before it. The two are 45 degrees apart, which matches the three hours between them.

The curve still does not close, which is the second case, and that is the correct picture. The gauge took no reading at midnight, and limits states a domain; it does not invent an observation. The gap you can see between hour 22 and hour 1 is the three-hour gap in the data, drawn at the size it is. Contrast the trips above, which close because a reading at 24 genuinely exists. A categorical angle is the third case, and it closes for a different reason. Every slot is filled there, so the last category is next to the first whatever the data holds.

limits states the domain the axis runs over rather than deriving it from the data. That is the general fix and not a polar one. The same argument, limits, keeps one color ramp fixed across plots built from different tables, which Scales explains in full. Note what limits did not do. No reading was dropped, because 1 through 22 all lie inside 0 through 24. Stating a domain widens an axis as easily as it narrows one.

Fill the day of trips with area. Its baseline is zero, which in a circle is the center, so the area fills everything between the curve and the center:

data(day_cycle) + area + x(hour) + y(trips) + polar()
data(day_cycle) + area + x(col.hour) + y(col.trips) + polar()
data(day_cycle) + area + x(:hour) + y(:trips) + polar()
plot(data(day_cycle), area, x(col.hour), y(col.trips), polar())
0 10 20 50 100 150 Trips Hour

“Given the day cycle: an area, x is hour, y is trips, in polar.”

33.5 The radar: a categorical angle

A categorical angle closes differently. An hour axis closes only when the table holds both ends of the cycle, 0 and 24, which the scale puts on one spoke. Categories cannot do that: each appears exactly once, by definition, so there is no repeated endpoint to land on. Instead the categories together fill the whole turn, which makes the last one genuinely next to the first. So the engine joins the last category back to the first, and a line across categories closes:

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) + line * mean + x(continent) + y(life) + polar() +
  title("line * mean + x(continent) + polar()")
(data(gapminder_2007) + line * mean + x(col.continent) + y(col.life) + polar() +
  title("line * mean + x(continent) + polar()"))
data(gapminder_2007) + line * mean + x(:continent) + y(:life) + polar() +
  title("line * mean + x(continent) + polar()")
plot(data(gapminder_2007), layer(line, mean), x(col.continent),
  y(col.life), polar(), title("line * mean + x(continent) + polar()"))
Asia Europe Africa Americas Oceania 60 70 80 line * mean + x(continent) + polar() Life Continent

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

That is a radar plot, and there is no radar atom in this grammar. The same sentence without polar() is the profile from the Line chapter, minus its point layer. There the axis is straight, so the line has two ends. Bending the space is what joins them. Filled, it is the shape people usually mean by the name:

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

Split it with color and each group is its own outline, and the split reads exactly as it does on a flat profile:

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) + line * mean + x(continent) + y(life) + color(era) + polar() +
  title("half a century apart, two rings")
(data(gm_eras) + line * mean + x(col.continent) + y(col.life) + color(col.era) + polar() +
  title("half a century apart, two rings"))
data(gm_eras) + line * mean + x(:continent) + y(:life) + color(:era) +
  polar() + title("half a century apart, two rings")
plot(data(gm_eras), layer(line, mean), x(col.continent), y(col.life),
  color(col.era), polar(), title("half a century apart, two rings"))
Asia Europe Africa Americas Oceania 40 50 60 70 80 half a century apart, two rings Life Continent Era 1957 2007

The 2007 outline lies outside the 1957 one for every continent, and the gap is widest at Asia, about 21 years.

The radar shares a problem with the rose, explained under What a circle makes harder to read: the eye reads the area the outline encloses, and no number in the table states that area. That area also changes when the categories are put in a different order. Use it when the categories really are a closed set with no first or last member, which is the case a straight axis misreports by having to pick an order. When the question is which continent is highest, the flat bar chart of the same sentence answers it precisely, and is two atoms away.

33.6 An angle that is measured, not named

An angular position does not have to be a category. The eight compass points are names, but an instrument records the same directions as degrees, with a speed beside each reading. bearing is the same wind observation recorded in degrees, a continuous position that happens to run around a circle:

data(winds) + point + x(bearing) + y(speed) + color(season) + polar()
data(winds) + point + x(col.bearing) + y(col.speed) + color(col.season) + polar()
data(winds) + point + x(:bearing) + y(:speed) + color(:season) + polar()
plot(data(winds), point, x(col.bearing), y(col.speed), color(col.season),
  polar())
100 200 300 5 10 15 20 25 Speed Bearing Season Summer Winter

“Given the winds: points, x is bearing, y is speed, color by season, in polar.”

Each dot is at its own bearing. Its distance from the center is its speed. No two readings share a bearing, so a dot can be at any angle, not only at the eight compass points. The dots crowd on the western side, where most of the wind came from. The gridline circles mark speed, read outward from the center. A dot near the rim is a fast wind, whichever way it was blowing.

bin cuts a measured angle into wedges the same way it cuts a flat axis into bars:

data(winds) + bar * bin + x(bearing) + polar()
data(winds) + bar * bin + x(col.bearing) + polar()
data(winds) + bar * bin + x(:bearing) + polar()
plot(data(winds), layer(bar, bin), x(col.bearing), polar())
100 200 300 20 40 Count Bearing

“Given the winds: bars derived by bin, x is bearing, in polar.”

That is a wind rose, one atom different from a histogram: bar * bin in both cases, read on a line or read in a circle. The name comes from the rosa ventorum, Latin for wind rose, which cartographers drew on maps from the 13th century to mark the directions of the winds. The bins touch here as they touch there, because a histogram cuts a continuous axis into adjacent intervals, and that is true whichever shape the axis has been bent into.

A compass turn is 360 degrees, but the axis cannot know that, just as it could not know the length of a day. The axis is fitted to what was observed, so the turn is the span of the bins, from 2.6 to 359.7 degrees. The readings here cover nearly the whole compass, so north lands only 2.6 degrees counterclockwise of the top.

Stating the domain with limits, as with the tide readings, puts north at the top:

data(winds) + bar * bin + x(bearing, limits = c(0, 360)) + polar()
data(winds) + bar * bin + x(col.bearing, limits = [0, 360]) + polar()
data(winds) + bar * bin + x(:bearing, limits = [0, 360]) + polar()
plot(data(winds), layer(bar, bin), x(col.bearing, { limits: [0, 360] }),
  polar())
0 100 200 300 20 40 Count Bearing

“Given the winds: bars derived by bin, x is bearing from 0 to 360, in polar.”

The turn now runs from 0 to 360 degrees, whatever the readings span. bin cuts its wedges on the stated domain as well. It makes ten wedges, as above, so each covers 36 degrees and the first starts at north. The label 0 at the top, missing from the rose above, marks where that wedge begins.

33.7 The pie: where you put the category

The roses so far put the direction on a position: x(direction) gave each compass point its own slot, and the count decided how far out the wedge reached. A pie does the opposite. Its categories are not positions at all; they are a split, and what goes around the circle is the measure itself.

That is the whole difference, and the grammar can say it. Take the category off x and give it to color, then say that the pieces lie end to end:

data(winds) + bar * count * stack + color(direction) + polar()
data(winds) + bar * count * stack + color(col.direction) + polar()
data(winds) + bar * count * stack + color(:direction) + polar()
plot(data(winds), layer(bar, count, stack), color(col.direction), polar())
Count Direction N NE E SE S SW W NW

Read the sentence: a bar, counted, stacked, colored by direction, in polar. Not one word of it is about pies. The slices are the segments of a stacked bar, and the angles are their shares. The legend says which direction each slice is.

The flat form is the same sentence without the last atom, and it is a real chart of its own, the one column that shows how a total divides:

data(winds) + bar * count * stack + color(direction)
data(winds) + bar * count * stack + color(col.direction)
data(winds) + bar * count * stack + color(:direction)
plot(data(winds), layer(bar, count, stack), color(col.direction))
0 100 200 Count Direction N NE E SE S SW W NW

Those two pictures carry identical numbers. Wilkinson’s second chapter is called How To Make a Pie, and his answer is exactly this: a pie chart is a stacked bar in polar coordinates. The column’s height is the pie’s full turn, and each segment’s share of the height is its slice’s share of the circle.

Nothing new was added to the grammar to reach it. There is no pie() atom, no type = "pie", and no argument anywhere that says which chart to draw. Two things happen on their own. First, a bar split only by color is in one slot, so it needs no position axis. Dropping x is how you say that. Second, a plot with one position bound has nothing to choose between: that position becomes the angle, and the radius is left as the constant that sets the pie’s size. Wilkinson’s book has both rules: its one-argument polar function is a separate tool from the two-argument one.

The measure can be any single number that summarizes a group, not just a count. Swap count for sum, name the column it should total, and the same eight directions are weighted by wind speed rather than by how often the wind blew:

data(winds) + bar * sum * stack + y(speed) + color(direction) + polar()
data(winds) + bar * sum * stack + y(col.speed) + color(col.direction) + polar()
data(winds) + bar * sum * stack + y(:speed) + color(:direction) + polar()
plot(data(winds), layer(bar, sum, stack), y(col.speed),
  color(col.direction), polar())
Speed Direction N NE E SE S SW W NW

“Given the winds: bars derived by sum and stack, y is speed, color by direction, in polar.”

Compare it with the pie above: the same categories, a different answer. West is both the most common direction and the strongest, so its slice widens from about a quarter of the circle to a third. Winds from the north and south are the gentlest, so theirs shrink by roughly two fifths, even though exactly the same observations went into them. Swapping one atom changed which question the circle answers.

Swap count for proportion instead, and the angles stay the same, because in a pie each slice’s angle is already its share of the whole. In the pie, only the axis name changes. The flat stacked bar is where the change shows, because its axis now runs from 0 to 1:

data(winds) + bar * proportion * stack + color(direction)
data(winds) + bar * proportion * stack + color(col.direction)
data(winds) + bar * proportion * stack + color(:direction)
plot(data(winds), layer(bar, proportion, stack), color(col.direction))
0.0 0.2 0.4 0.6 0.8 1.0 Proportion Direction N NE E SE S SW W NW

“Given the winds: bars derived by proportion and stack, color by direction.”

Each segment’s height is now its share. West runs from 0.66 to 0.90, a share of 0.24, which is the quarter of the circle its slice takes in the count pie.

The split has to be there. A stack with nothing to pile, or a bar with neither a position nor a split, is refused rather than guessed at. So is a statistic that only means something along an axis:

data(winds) + bar * bin * stack + color(season) + polar()
data(winds) + bar * bin * stack + color(col.season) + polar()
data(winds) + bar * bin * stack + color(:season) + polar()
plot(data(winds), layer(bar, bin, stack), color(col.season), polar())
Error:
! gog: `bin` describes how values are spread along an axis, and this `bar` has no `x()` to spread them along. Add `x(<column>)` to draw the distribution, or use `count`/`sum` for one value per group.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

33.8 Nightingale’s rose

The start of this chapter named Florence Nightingale’s diagram of the deaths in the British army during the Crimean War. The numbers behind it are in the nightingale table: the deaths in each month from April 1854 to March 1856, from three causes:

nightingale: first 5 of 72 rows
period month cause deaths
1854-55 Apr Disease 1
1854-55 Apr Wounds 0
1854-55 Apr Other 5
1854-55 May Disease 12
1854-55 May Wounds 0

Each row is one month and one cause, and deaths counts the soldiers who died of it. The cause Disease means infectious diseases, which she called preventable or mitigable. Wounds means wounds, and Other every other cause. period says which year of the war a month belongs to. Each of her years runs from April to March, so the second year begins in April 1855. A sanitary commission arrived in March 1855 to improve the hygiene of the camps and the hospitals. Her two years compare the deaths before it and after it.

data(nightingale) + bar * stack + x(month) + y(deaths) + color(cause) +
  palette(c(Disease = "lightsteelblue", Wounds = "lightcoral",
            Other = "dimgray")) +
  polar(start = -90) +
  title("Deaths in the British army in the Crimean War, by cause") |
  facet(period)
(data(nightingale) + bar * stack + x(col.month) + y(col.deaths) + color(col.cause) +
  palette({"Disease": "lightsteelblue", "Wounds": "lightcoral", "Other": "dimgray"}) +
  polar(start = -90) +
  title("Deaths in the British army in the Crimean War, by cause") |
  facet(col.period))
data(nightingale) + bar * stack + x(:month) + y(:deaths) + color(:cause) +
  palette(["Disease" => "lightsteelblue", "Wounds" => "lightcoral", "Other" => "dimgray"]) +
  polar(start = -90) +
  title("Deaths in the British army in the Crimean War, by cause") |
  facet(:period)
plot(data(nightingale), layer(bar, stack), x(col.month), y(col.deaths),
  color(col.cause),
  palette({ Disease: "lightsteelblue", Wounds: "lightcoral",
  Other: "dimgray" }), polar({ start: -90 }),
  title("Deaths in the British army in the Crimean War, by cause"),
  across(col.period))
Apr May Jun Jul Aug Sep Oct Nov Dec Jan Feb Mar 1K 2K 3K Apr May Jun Jul Aug Sep Oct Nov Dec Jan Feb Mar 1K 2K 3K 1854-55 1855-56 Deaths in the British army in the Crimean War, by cause Deaths Month Cause Disease Wounds Other

“Given the nightingale table: bars derived by stack, x is month, y is deaths, color by cause, with a palette naming Disease lightsteelblue, Wounds lightcoral and Other dimgray, in polar, starting at -90, split into panel columns by period.”

Each wedge is one month, and its length from the center is the number of soldiers who died that month. The blue segment shows the deaths from disease, and the pink and gray segments outside it show the deaths from wounds and from other causes. polar(start = -90) turns the circle 90 degrees counterclockwise, so April, the first month, is on the left, where her diagrams start. Two atoms are new to this chapter. The palette gives each cause a lighter form of her blue, red and black, and | facet(period) draws one panel for each year, earliest first (Faceting).

The first winter has the longest wedges. In January 1855, 3,168 soldiers died: 2,761 of disease, 83 of wounds and 324 of other causes. A year later, January 1856 is a small wedge near the center: 92 deaths from all three causes together.

Her diagram differs from this one in four ways. She put her first year on the right. She drew the three causes on top of each other, each measured from the center, while stack piles them, so each wedge here reaches the month’s total. Her wedges show the yearly death rate per 1,000 soldiers, which allows for an army that grew from about 8,600 men to over 40,000. And her radius grows as the square root of that rate, so a wedge’s area is in proportion to it. Here the radius is the number of deaths itself, as for every polar bar in gog. So the first winter looks larger, beside the other months, than it does in her diagram. The next section explains why that matters.

33.9 What a circle makes harder to read

The grammar draws every rose in this chapter, and drawing one is not the same as recommending it. A wedge’s area grows with the square of its radius, so a count that doubles gets four times the area, not twice. Wilkinson says it plainly of the rose: the polar bar chart confounds area with radius, and growing the radius as the square root only partly repairs it. Most readers judge filled shapes by area. A reader who does that will read a stronger west wind than the numbers support.

Law 8 states the limit: the engine guarantees a plot is well formed, never that it is well chosen. Use a circle when the variable really is a cycle, which is the case a flat axis misreports. Use the flat bar chart when the comparison must be precise.

The pie avoids that criticism and has one of its own. Its slices are angles rather than radii, so a share of the total really is the same share of the circle, and readers judge that share well. Angles are poor for ranking: the east and south-west slices of the count pie, 16% and 18%, are hard to order in the circle and easier to order as lengths in the stacked bar. When ranking is the question, drop the last atom and read the bar. Both pictures come from the same sentence.

33.10 The marks in a circle

The rose counted observations per direction. How fast the wind blew from each direction, and how much that varied, is a question for the marks that summarize: box, interval and ribbon. Every mark that draws on a flat page also draws in a circle. These three read here exactly as they read flat: a box still shows the quartiles and the median, a whisker still shows how far the values reach, a band is still a spread. Only the geometry changed.

data(winds) + box + x(direction) + y(speed) + polar()
data(winds) + box + x(col.direction) + y(col.speed) + polar()
data(winds) + box + x(:direction) + y(:speed) + polar()
plot(data(winds), box, x(col.direction), y(col.speed), polar())
N NE E SE S SW W NW 5 10 15 20 25 Speed Direction

“Given the winds: boxes, x is direction, y is speed, in polar.”

Each box is a piece of a ring, from the lower quartile out to the upper. Its median is an arc across it, and its whiskers lie along lines through the center. Drawn flat, the same sentence gives boxes with the same parts.

An interval shows less: one line from the lowest to the highest value, with no box. It is the one to use when the quartiles are more detail than the question needs:

data(winds) + interval * range + x(direction) + y(speed) + polar()
data(winds) + interval * range + x(col.direction) + y(col.speed) + polar()
data(winds) + interval * range + x(:direction) + y(:speed) + polar()
plot(data(winds), layer(interval, range), x(col.direction), y(col.speed),
  polar())
N NE E SE S SW W NW 5 10 15 20 25 Speed Direction

“Given the winds: intervals derived by range, x is direction, y is speed, in polar.”

A ribbon works the same way. range gives each compass point a low and a high value, and polar() turns the ribbon into a radar band, the filled counterpart of the radar that line drew.

data(winds) + ribbon * range + x(direction) + y(speed) + polar()
data(winds) + ribbon * range + x(col.direction) + y(col.speed) + polar()
data(winds) + ribbon * range + x(:direction) + y(:speed) + polar()
plot(data(winds), layer(ribbon, range), x(col.direction), y(col.speed),
  polar())
N NE E SE S SW W NW 5 10 15 20 25 Speed Direction

“Given the winds: a ribbon derived by range, x is direction, y is speed, in polar.”

A staircase holds each value from its own category around to the next rather than sloping between them, so in a circle it is a series of arcs joined by straight jumps. And because the categories fill the whole turn, the last arc carries all the way around to the spoke the first one started from:

data(winds) + step * mean + x(direction) + y(speed) + polar()
data(winds) + step * mean + x(col.direction) + y(col.speed) + polar()
data(winds) + step * mean + x(:direction) + y(:speed) + polar()
plot(data(winds), layer(step, mean), x(col.direction), y(col.speed),
  polar())
N NE E SE S SW W NW 10 15 20 Speed Direction

“Given the winds: a step outline derived by mean, x is direction, y is speed, in polar.”

Look at where the value changes. Every jump is a straight line out from the center, and every held value is a genuine arc rather than a chord cutting across it. That distinction is the only thing the marks in this section needed in order to bend. A segment that holds a value across a span has to follow the circle at its radius. A straight line between the same two points falls inside the circle, and every point along it would be at a radius no reading has.

A zone shades a region, and in this space a region is a wedge. Put a category on each axis and a tile plot bends into a disc, with one cell for every pair:

data(winds) + zone * count + x(direction) + y(season) + polar()
data(winds) + zone * count + x(col.direction) + y(col.season) + polar()
data(winds) + zone * count + x(:direction) + y(:season) + polar()
plot(data(winds), layer(zone, count), x(col.direction), y(col.season),
  polar())
N NE E SE S SW W NW Winter Summer Season Direction Count 38 22 6

“Given the winds: zones derived by count, x is direction, y is season, in polar.”

Two marks do not draw in a circle, and in neither case is the circle the reason. A surface needs three positions, so it needs the cube, the 3-D space that Space describes; it does not draw on a flat page either. An edge needs the network space, which places its nodes. A plot is also drawn in one space and not two, so asking for polar() and z() together is refused rather than one of the two being ignored.

One sentence is refused here, because a hexagon needs a plane where the same step is the same distance everywhere:

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) + zone * bin(tiling = "hex") + x(gdp) + y(life) + polar()
data(gm_all) + zone * bin(tiling = "hex") + x(col.gdp) + y(col.life) + polar()
data(gm_all) + zone * bin(tiling = "hex") + x(:gdp) + y(:life) + polar()
plot(data(gm_all), layer(zone, bin({ tiling: "hex" })), x(col.gdp),
  y(col.life), polar())
Error:
! gog: `bin(tiling = "hex")` partitions a *plane*, and `polar()` bends the plane into a circle — where a cell is a sector, and a hexagon's six equal sides are equal against a distance the space no longer has (a step of angle is longer at the rim than at the center). Drop the tiling for `rect`, whose cells are the sectors a bent rectangle already is, or drop `polar()` to cut a flat plane.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

bin(tiling = ) says how to divide a plane into cells. It is refused on a one-dimensional bin because an interval is not a plane, and on a categorical axis because slots are an order rather than a distance. Here it is refused because a bent plane is not such a plane either. A hexagon exists to keep every cell the same size and shape. A bent plane has no fixed distance to measure that against, because a step of angle at the rim is longer than the same step near the center. A rectangle still works after the bend, because a bent rectangle is a sector and that is a shape the engine draws.

Which marks draw in which coordinate space is stated in full, on one page, by the Mark × Space grid in Combinations. It is generated from the engine’s own table, so it cannot promise a plot the engine will not draw.

33.11 The sunburst: depth is the radius

A pie divides one whole into parts. The rest of this chapter is about dividing each part again. Where does a household’s money go, and inside each group, which items cost the most? That is a tree of shares, one level inside another. A sunburst draws a tree: one ring per level, and each piece is as wide as its share of the whole. A child is one ring further out, within its parent’s angle. The sunburst has no mark of its own and needs none. A piece of a ring is a rectangle in angle and radius. zone is the mark bounded by a pair of values on each axis. This space is what bends that rectangle into a sector. partition is what turns a hierarchy into those two pairs of values, one rectangle per node, meaning every group, item and detail in the tree.

The hierarchy is stored as columns. spending is a household budget with one row per leaf, meaning the last level of a branch, and the first three columns hold the path down to it:

spending: first 6 of 12 rows
group item detail amount
Housing Rent NA 980
Housing Utilities Energy 140
Housing Utilities Water 35
Housing Repairs NA 120
Food Groceries NA 420
Food Eating out NA 185

In this table, a missing value does not mean an unknown value. It means the branch stops: Rent has no sub-parts, so it reaches the second level and no further.

data(spending) + zone * partition(group, item, detail) + x(amount) +
  color(group) + polar()
(data(spending) + zone * partition(col.group, col.item, col.detail) + x(col.amount) +
  color(col.group) + polar())
data(spending) + zone * partition(:group, :item, :detail) + x(:amount) +
  color(:group) + polar()
plot(data(spending),
  layer(zone, partition(col.group, col.item, col.detail)), x(col.amount),
  color(col.group), polar())
0 1K 2K Amount Group Housing Food Transport Leisure

“Given the spending table: zones derived by partition through group, item and detail, x is amount, color by group, in polar.”

partition(group, item, detail) names the levels from the top of the tree down, so group is the innermost ring and detail the rim. x(amount) is the column that weights each branch: a branch’s share of the circle is its share of the total amount. Apart from polar(), nothing in the sentence is about circles, and nothing in it names a sunburst.

Bind nothing to x and every leaf weighs 1, the same weight count gives every row. The rings are then split by how many leaves each branch has rather than by what they cost:

data(spending) + zone * partition(group, item, detail) + color(group) + polar()
data(spending) + zone * partition(col.group, col.item, col.detail) + color(col.group) + polar()
data(spending) + zone * partition(:group, :item, :detail) +
  color(:group) + polar()
plot(data(spending),
  layer(zone, partition(col.group, col.item, col.detail)),
  color(col.group), polar())
0 5 10 Group Housing Food Transport Leisure

Housing fills half the circle above and a third of it here, because it holds 1,275 of the 2,490 spent, and 4 of the 12 leaves. That is the same swap the pie made between count and sum, this time on a tree.

Read the outer rim. It is solid outside Utilities and Car and blank outside every other item, because those two are the only items with a third level. A ragged rim is what a real hierarchy looks like, and it comes from the missing values rather than from anything in the sentence.

The radial axis carries depth, which says which ring a node is on. The groups fill the ring from 1 to 2, the items the ring from 2 to 3, and the details the ring from 3 to 4. partition computes depth and writes it under that name, so it is a column you can bind like any other, even though your table never held it. Binding it to y is how you say what the radius runs over, and stating a domain that starts below the first ring is what hollows out the middle:

data(spending) + zone * partition(group, item, detail) + x(amount) +
  y(depth, limits = c(0, 4)) + color(group) + polar()
(data(spending) + zone * partition(col.group, col.item, col.detail) + x(col.amount) +
  y(col.depth, limits = [0, 4]) + color(col.group) + polar())
data(spending) + zone * partition(:group, :item, :detail) + x(:amount) +
  y(:depth, limits = [0, 4]) + color(:group) + polar()
plot(data(spending),
  layer(zone, partition(col.group, col.item, col.detail)), x(col.amount),
  y(col.depth, { limits: [0, 4] }), color(col.group), polar())
0 1K 2K Amount Group Housing Food Transport Leisure

The hole is not a setting, and the reason matters. A hole is a stretch of the radial axis with nothing drawn on it. The innermost ring starts at 1, so writing that the axis starts at 0 leaves the first unit of radius empty. Widen the stretch and the hole widens with it, because limits states a domain and the rings keep the places they always had. With c(-1, 4) the hole takes two fifths of the radius rather than a quarter.

Because depth is a column, color() can name it too. A number on color usually gets a ramp. A ring’s depth is a whole number, though, and each ring is one flat color, so the legend has one swatch for each ring, labeled with the ring’s depth: 1 for the groups, 2 for the items, 3 for the details:

data(spending) + zone * partition(group, item, detail) + x(amount) +
  y(depth, limits = c(0, 4)) + color(depth) + polar()
(data(spending) + zone * partition(col.group, col.item, col.detail) + x(col.amount) +
  y(col.depth, limits = [0, 4]) + color(col.depth) + polar())
data(spending) + zone * partition(:group, :item, :detail) + x(:amount) +
  y(:depth, limits = [0, 4]) + color(:depth) + polar()
plot(data(spending),
  layer(zone, partition(col.group, col.item, col.detail)), x(col.amount),
  y(col.depth, { limits: [0, 4] }), color(col.depth), polar())
0 1K 2K Amount Depth 1 2 3

In a sunburst this color repeats what the radius already shows, so it adds no new information, and color(group) stays the usual choice. It works as a reading aid: it shows which ring a node is on, without counting the rings from the center. A text layer can print each node’s ring number with label(depth), as Naming the nodes shows.

33.12 The donut: one level is still a tree

A hierarchy one level deep is still a hierarchy, and everything above is true of it as well. A donut is the hollow sunburst with one level. One level is one ring, that ring runs from 1 to 2, and an axis running from 0 to 2 leaves the first of its two units empty:

data(spending) + zone * partition(group) + x(amount) +
  y(depth, limits = c(0, 2)) + color(group) + polar()
(data(spending) + zone * partition(col.group) + x(col.amount) +
  y(col.depth, limits = [0, 2]) + color(col.group) + polar())
data(spending) + zone * partition(:group) + x(:amount) +
  y(:depth, limits = [0, 2]) + color(:group) + polar()
plot(data(spending), layer(zone, partition(col.group)), x(col.amount),
  y(col.depth, { limits: [0, 2] }), color(col.group), polar())
0 1K 2K Amount Group Housing Food Transport Leisure

There is no donut atom, no hole = setting, and nothing in that sentence about donuts. It is the sunburst’s sentence with one level named instead of three, drawn on a radial axis whose first unit is empty.

The donut you usually see published writes its names in the ring, and that is the text layer explained under Naming the nodes, two sections later, with one level instead of two. In the sentence below, label(name) reads a column that partition writes (Other columns a transform writes):

data(spending) + x(amount) + y(depth, limits = c(0, 2)) +
  zone * partition(group) + color(group) +
  text * partition(group) + label(name) + style(color = "white") +
  polar() + x_label("")
(data(spending) + x(col.amount) + y(col.depth, limits = [0, 2]) +
  zone * partition(col.group) + color(col.group) +
  text * partition(col.group) + label(col.name) + style(color = "white") +
  polar() + x_label(""))
data(spending) + x(:amount) + y(:depth, limits = [0, 2]) +
  zone * partition(:group) + color(:group) + text * partition(:group) +
  label(:name) + style(color = "white") + polar() + x_label("")
plot(data(spending), x(col.amount), y(col.depth, { limits: [0, 2] }),
  layer(zone, partition(col.group)), color(col.group),
  layer(text, partition(col.group)), label(col.name),
  style({ color: "white" }), polar(), x_label(""))
Housing Food Transport Leisure 0 1K 2K Group Housing Food Transport Leisure

“Given the spending table: x is amount, y is depth from 0 to 2, zones derived by partition through group, colored by group, and also text derived by partition through group, label by name, in polar.”

The group names are now written in two places, in the legend and in the ring, so a published plot would keep only one. Both are kept here, so that you can compare the two ways of decoding a wedge.

The pie is the one plot in this space that cannot have a hole, and that follows from what a pie binds. A pie puts its measurement on the angle and leaves the radius constant, so it has no radial domain to widen. No setting adds a hole to a pie. A hole appears only where the radial axis runs below the innermost ring, so only a plot that binds the radius can have one. The donut binds it with y(depth, limits = c(0, 2)).

33.13 The same sentence on a flat page

This chapter has said that the circle is one word, polar(). If that is true, taking the word away should leave a readable plot. Take polar() off the three-level sunburst and change nothing else:

data(spending) + zone * partition(group, item, detail) + x(amount) +
  y(depth, limits = c(0, 4)) + color(group)
(data(spending) + zone * partition(col.group, col.item, col.detail) + x(col.amount) +
  y(col.depth, limits = [0, 4]) + color(col.group))
data(spending) + zone * partition(:group, :item, :detail) + x(:amount) +
  y(:depth, limits = [0, 4]) + color(:group)
plot(data(spending),
  layer(zone, partition(col.group, col.item, col.detail)), x(col.amount),
  y(col.depth, { limits: [0, 4] }), color(col.group))
0 1K 2K Amount Group Housing Food Transport Leisure

That is an icicle chart: depth going up instead of outward, share going across instead of around. The two pictures are one coordinate space apart, and that space is the only difference between two charts with two names. A rose and a bar chart differ in the same way, and so do a pie and a stacked bar.

The empty band along the bottom is the hole, and seeing it flat shows most clearly what a hole is. A hole is not a shape. It is a stretch of the depth axis with nothing on it, and bending the picture wraps that stretch into a disc at the center.

33.14 Naming the nodes

Go back to the three-level sunburst. The legend decodes the groups and nothing inside them, so the items on the middle ring are unnamed. partition writes the center of each node, the middle of its own region, as well as its four edges. That is why a second mark can read the same computation. text takes that center and labels the node:

data(spending) + x(amount) + y(depth, limits = c(0, 4)) +
  zone * partition(group, item, detail) + color(group) +
  text * partition(group, item) + label(name) + style(color = "white") +
  polar() + x_label("")
(data(spending) + x(col.amount) + y(col.depth, limits = [0, 4]) +
  zone * partition(col.group, col.item, col.detail) + color(col.group) +
  text * partition(col.group, col.item) + label(col.name) + style(color = "white") +
  polar() + x_label(""))
data(spending) + x(:amount) + y(:depth, limits = [0, 4]) +
  zone * partition(:group, :item, :detail) + color(:group) +
  text * partition(:group, :item) + label(:name) +
  style(color = "white") + polar() + x_label("")
plot(data(spending), x(col.amount), y(col.depth, { limits: [0, 4] }),
  layer(zone, partition(col.group, col.item, col.detail)),
  color(col.group), layer(text, partition(col.group, col.item)),
  label(col.name), style({ color: "white" }), polar(), x_label(""))
Housing Food Transport Leisure Rent Utilities Repairs Groceries Eating out Car Season ticket Holidays Hobbies Subscriptions 0 1K 2K Group Housing Food Transport Leisure

label(name) reads a column the transform wrote. The text layer names two levels where the zone draws three. The third level gets no label, which is why the rim is bare. A shallower partition of the same table puts its nodes in exactly the same places, because each node’s width is decided by the leaves underneath it, and those did not change.

Most of the names are upright. A name too wide for its sector turns along the sector’s radius or along its arc, whichever fits more of it. So Hobbies and Subscriptions, the two thinnest items, each run along its own radius, and Transport and Eating out run along their arcs. A name that still does not fit is drawn smaller, at the size that fits. Subscriptions and Season ticket are the clearest case, and Utilities and Eating out are a little smaller too. So every name is drawn, and none crosses the edge of its sector. style(angle = 0) keeps every name upright and at full size, and any other angle turns every name by that angle.

The sunburst’s sentence carries an x_label("") and no y_label("") beside it, because only one of the two axes has a label to remove. The radial axis carries the depth the transform computed. Nobody needs a scale to tell the first ring from the second, so that axis draws no ticks, no gridlines and no name of its own, and nothing in the sentence has to ask for that. The engine reads what the axis carries. It never knows that the plot is a sunburst, in the same way that it never knows a pie is a pie: in both, what is drawn follows from what the sentence binds. The amount around the circle keeps its ticks and its name, because it is a quantity in the data’s own units. x_label("") removes the name and leaves the ticks. To leave out both, drop x_label("") and write x(amount, axis = FALSE), as Scales explains. gog refuses the two together, because the name is part of the axis.

label(depth) prints each node’s ring number in place of its name: 1 on the groups, 2 on the items and 3 on the details.

data(spending) + x(amount) + y(depth, limits = c(0, 4)) +
  zone * partition(group, item, detail) + color(group) +
  text * partition(group, item, detail) + label(depth) + style(color = "white") +
  polar() + x_label("")
(data(spending) + x(col.amount) + y(col.depth, limits = [0, 4]) +
  zone * partition(col.group, col.item, col.detail) + color(col.group) +
  text * partition(col.group, col.item, col.detail) + label(col.depth) + style(color = "white") +
  polar() + x_label(""))
data(spending) + x(:amount) + y(:depth, limits = [0, 4]) +
  zone * partition(:group, :item, :detail) + color(:group) +
  text * partition(:group, :item, :detail) + label(:depth) +
  style(color = "white") + polar() + x_label("")
plot(data(spending), x(col.amount), y(col.depth, { limits: [0, 4] }),
  layer(zone, partition(col.group, col.item, col.detail)),
  color(col.group),
  layer(text, partition(col.group, col.item, col.detail)),
  label(col.depth), style({ color: "white" }), polar(), x_label(""))
1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 0 1K 2K Group Housing Food Transport Leisure

33.15 What a tree lets the grammar check

A hierarchy has two rules that rectangles do not: every child lies inside its parent, and children of one parent do not overlap. Give the engine four edge numbers per node and it cannot check either rule. It will draw a wrong sunburst and say nothing. Give it the tree and it can check both. One case is shown here because it is a real ambiguity rather than a mistake:

mixed <- data.frame(group = c("Housing", "Housing"),
                    item  = c(NA, "Rent"),
                    amount = c(50, 980))
data(mixed) + x(amount) + zone * partition(group, item) + polar()
mixed = {
  "group": ["Housing", "Housing"],
  "item": [None, "Rent"],
  "amount": [50, 980],
}
data(mixed) + x(col.amount) + zone * partition(col.group, col.item) + polar()
mixed = (
  group = ["Housing", "Housing"],
  item = [missing, "Rent"],
  amount = [50, 980],
)
data(mixed) + x(:amount) + zone * partition(:group, :item) + polar()
const mixed = {
  group: ["Housing", "Housing"],
  item: [null, "Rent"],
  amount: [50, 980],
};
plot(data(mixed), x(col.amount),
  layer(zone, partition(col.group, col.item)), polar())
Error:
! gog: `Housing` has a value of its own *and* children with values, and those two readings draw different pictures: either its arc is the children's total (its own number already counted among them), or its own number sits beside them and widens it. gog will not pick — the arithmetic is the accounts', not the grammar's. Put every number on a **leaf** and let the parents be the sums, which is what a partition computes.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

Housing has a number of its own and a child with a number. That can be read two ways, and the two draw different pictures: either Housing is as wide as its child’s 980, or Housing’s own 50 is added to the 980 and Housing is wider. Plotly answers it with a setting called branchvalues. gog does not choose for you, because that sum is a decision about your data, not about the plot. Put every number on a leaf and let the parents be the sums, which is what a partition computes.

Three other refusals are smaller. A level must be a name that a branch shares, so a continuous column is refused and sent to x(), where a measurement belongs. A branch may stop early but may not skip a level: a row with a detail and no item is refused, because a detail must be inside an item, and this row has none. And a mark that cannot read a region, such as point or bar, is refused. The message sends you to zone, which draws the region, or to text, which labels its center.

partition deliberately does not read a tree stored as a parent column and a child column. It reads a hierarchy written as columns, one per level, which is what faceting reads too. A parent column and a child column can describe a tree of any depth, and that is the shape a network has. Network is where that shape is read: layout(parent, child) places it, in a space with its own rules. Each transform reads the shape it is built for.

33.16 In short

x becomes the angle and y the radius, because the first position says where and the second says how much. Bind only one position and it becomes the angle: a stacked bar then draws a pie. A categorical angle closes, so the last category is beside the first. A cycle whose table lacks its ends needs limits to fill the turn. partition draws a hierarchy as rings of depth, and a domain below the first ring makes the hole.

33.17 What comes next

Polar bends the axes, and it still has axes. Nest has none: it packs a panel into regions whose areas are the measure, which is how a treemap shows a share of a whole.