Command Palette

Search for a command to run...

Hectal
PHASE 13Advanced ~27 min· topic 4 of 7

Topic 13.4

Trees, Treemaps, Packs and Sunbursts

In one line

Some data is boxes inside boxes: a fleet is split into districts, stations and bike types. d3.hierarchy wraps that data in nodes, sum adds the values up, and a layout (treemap, pack, partition or tree) gives every node a position and a size.

Think of it like this

A family tree, or a company chart. One person at the top, children below, grandchildren below them. Each person has a place in the whole. If every person also has a number (their salary), you can add numbers up the tree: a team's total, a department's total, the company's total.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

hierarchy
Data in which each item can have children, which can have children of their own.
node
One item of the hierarchy, wrapped by D3 with depth, height, parent and children.
leaf
A node with no children. Its value is its own.
treemap
Nested rectangles whose area is the value of each node.
pack
Circles inside circles, each circle's area showing the value.
sunburst
A partition layout drawn as rings: one ring per level, one arc per node.
stratify
Turning a flat list of rows with ids and parent ids into a hierarchy.

Step by step

01Wrap the fleet in a hierarchy

fleet.json is nested: Lumen Bikes, then six districts, then 25 stations, then two bike types with a value each. d3.hierarchy(fleet).sum(...) gives 320 bikes at the root, 82 nodes in all, 50 leaves and height 3.

The first example also shows what sum does with an empty children list, a missing value and text, and builds a tree from a flat table with stratify.

02Watch a treemap place its rectangles

Step through the widget below. It tiles the fleet with four different methods (squarify, binary, slice, dice), one per step, and prints how long and thin the worst rectangle is. Every method splits a parent's space between its children in proportion to their values. Squarify and binary keep the pieces close to square (worst rectangle about 10 times longer than wide). Slice and dice cut strips and give very thin rectangles.

03Draw the treemap, with labels that fit

The second example lays out the fleet in 520 × 330 pixels, with a 16-pixel strip at the top of each district for its name. It draws one rectangle per station, and a label only where it fits. 21 of 25 stations are labelled. The other four have a tooltip.

04The same tree in three more shapes

The third example draws the same hierarchy as packed circles, as a sunburst and as a node-link diagram, and prints what each layout adds to a node. It ends with a deep chain, where padding eats the space: 12 levels, 6 pixels of padding each, and the last three have width 0.

The same tree in three more shapesdiagram
Rendering diagram…

Try it yourself

  1. 1

    Count only the e-bikes

    In the first example, change .sum((d) => d.value) to .sum((d) => (d.name === "ebike" ? d.value : 0)). The root drops from 320 to 120 bikes, the same as the e-bike count in bikes.json. Pedal bikes still exist as nodes with value 0.

  2. 2

    Change the tiling

    In the second example, add .tile(d3.treemapSliceDice) after .size([W, H]). The rectangles become long strips that alternate horizontal and vertical by level. The label count stays 21, but different stations lose their names: Science Park, Station Road, Tram Depot and Lighthouse.

  3. 3

    Turn the sorting off

    In the second example, delete .sort((a, b) => b.value - a.value). The rectangles follow the file's order, not the size order, so big and small stations are mixed in each district, and the labelled count drops from 21 to 20.

Code & diagrams

A hierarchy from the fleet, and what sum does with awkward data New tab

`value` on a node is the sum of its leaves. Quay has 5 e-bikes and 8 pedal bikes, so 13.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

bikes in the fleet: 320 | nodes: 82 | leaves: 50 | height: 3
districts by bikes: Harbour 71, Old Town 62, University 50, Westpark 47, Riverside 46, Hillcrest 44
a leaf's path: Lumen Bikes > Harbour > Quay > ebike | depth 3 | value 5
Quay's children: pedal 8, ebike 5 | descendants of Harbour: 16
odd tree: total 3 | leaves: 5 | empty list=0, no value=0, text=0, zero=0, three=3
a node with children: [] is a leaf: true
stratify: all@0 a@1 b@1 a1@2
multiple roots

Try it: interactive

Compare four ways to tile the fleet

Press step or play. Each step tiles the same fleet with the next method. Change the speed to slow it down.

The fleet as a treemap with labels that fit New tab

Four rectangles are too small for their names. In the picture they keep a tooltip (hover to see it).

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

station rectangles: 25 | labelled: 21 | too small for a label: Rope Walk, Clock Tower, Station Road, West Gate
biggest: Market 102x93 (20 bikes) | smallest: Clock Tower 43x62 (6 bikes)
area ratio 3.6 | bikes ratio 3.3 (rounding to whole pixels moves it a little)
every station inside its district: true | tiling: squarify (the default)
Pack, sunburst, tree: what each layout adds, and a tree that is too deep New tab

Left to right: pack (circles), sunburst (rings), cluster tree (nodes and links). The chain at the end shows how padding can use up the whole box.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

treemap adds : x0, x1, y0, y1
pack adds    : r, x, y | circles drawn: 32
partition adds: y0, y1, x0, x1 | arcs drawn: 81
cluster adds : x, y | tree nodes: 32
widths by level: 100, 88, 76, 64, 52, 40, 28, 16, 4, 0, 0, 0 | the leaf has height 0

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget sum

In the second example, delete .sum((d) => d.value) from the line that makes root.

terminal
$ Console
── what you'll see ──
Error: <rect> attribute y: Expected length, "NaN".

Myth vs fact

Myth

A treemap and a pie chart are the same: both show parts of a whole.

Fact

A pie shows one level. A treemap shows several levels at once, in the same space, with the parts inside the parts.

Myth

A bigger rectangle always means a bigger value.

Fact

Only before padding and rounding. With paddings and round(true) a small rectangle can lose pixels, and a level with deep padding can shrink to zero, as the chain shows.

Myth

Sunburst and treemap are two different data formats.

Fact

Both use the same hierarchy. Only the layout function and the drawing code change.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    A treemap's rectangles are laid out by an algorithm that tries to keep them square. treemapSquarify has a ratio (default the golden ratio, 1.618). treemapResquarify keeps the layout stable when values change slightly, which matters for animations.

  • ▸

    A hierarchy mutates: treemap(root) sets x0, y0... on the same node objects. Calling a layout twice on one root overwrites the first result. Make a new hierarchy (or root.copy()) for a second layout, as the third example does with make().

  • ▸

    hierarchy.sum is cached on the node: if the underlying data changes, call sum again. For big trees, root.eachBefore and eachAfter walk the nodes without building arrays.

  • ▸

    Rendering thousands of leaves as SVG rectangles is fine up to a few thousand (Topic 12.1). For bigger trees (a whole file system), draw to canvas, or aggregate deep levels into an "other" node.

Remember this

  1. 1

    The problem this solves: a table of 50 rows (station, bike type, count) cannot show that Harbour holds more bikes than Westpark, or that one station holds a quarter of its district. When the data has parts inside parts, a chart should show the parts inside parts. A treemap, a pack or a sunburst does.

  2. 2

    **d3.hierarchy(data) wraps nested data.** It reads data.children (an array) and makes a node for every item. Each node has data (your object), depth (0 at the root), height (the longest way down), parent and children. Methods give you the family: descendants(), leaves(), ancestors(), links(). Fleet: 82 nodes, 50 leaves, height 3.

  3. 3

    **sum and sort prepare the numbers.** root.sum((d) => d.value) sets each node's value to its own value plus all its descendants' values. Root: 320 bikes. A node whose value is missing, NaN or text counts as 0. A node with children: [] is a leaf. root.sort((a, b) => b.value - a.value) puts the largest first, which most layouts need to look good. Without sum, value is undefined and the layout cannot work.

  4. 4

    Each layout adds numbers to the nodes. d3.treemap() adds x0, y0, x1, y1: a rectangle whose area is the value. d3.pack() adds x, y, r: a circle whose area is the value, packed in its parent. d3.partition() adds x0, x1, y0, y1 in a unit rectangle: draw them as rectangles (an icicle) or as arcs (a sunburst). d3.tree() and d3.cluster() add x, y for node-link diagrams, where the shape of the tree matters and the value does not.

  5. 5

    Treemap details that matter. The tiling method (squarify by default) chooses the rectangles' shapes: squarify makes them nearly square, treemapSliceDice alternates strips by level and keeps order. paddingInner, paddingOuter and paddingTop leave room for borders and parent titles. round(true) snaps to whole pixels. Rectangles' areas are exact before padding, and a rectangle with too much padding shrinks to zero.

  6. 6

    Area is a weak channel (Topic 2.2). Treemaps pack hundreds of values into little space, but people compare area less accurately than length. Use them for "which parts are big and which are tiny", not for exact comparison. Labels are the other problem: a rectangle 20 pixels wide cannot hold the name Clock Tower. Show a label only if it fits, and always give the rest a tooltip (<title>) or an accessible list.

  7. 7

    A flat table can be a tree too. d3.stratify().id(...).parentId(...) turns rows of { id, parent } into a hierarchy, and throws "multiple roots" if two rows have no parent. Use it for data from a database, where the nested form is not the natural one.

Explain it without notes

01

What does d3.hierarchy return, and what does sum add?

02

How is the data of a treemap different from the data of a force layout?

03

Why is the node's label dropped when a rectangle is small, and what keeps the information reachable?

04

A chart has twelve levels of nesting with padding at each level. What goes wrong?

Practice

01

Make the treemap show the e-bike share of each station as a second colour: a darker rectangle at the bottom of each cell, with a height proportional to the e-bikes.

02

Write labelFits(node, fontSize) that returns true only if the node's rectangle is wide and tall enough for its name, using getComputedTextLength of a temporary text element.

Trade-offs

  • ↔

    Treemaps use space well but make exact comparison hard. Sunbursts show depth clearly but waste the corners. Packs look friendly but waste space between circles.

  • ↔

    Showing every level at once keeps context but is crowded. Zoomable treemaps and sunbursts show one level at a time and keep text readable.

Done when you can

  • I can turn nested data into a hierarchy and sum its values up the tree.

  • I can say what a treemap, a pack, a partition and a tree each add to a node.

  • I can draw a treemap with parent titles and labels that only appear when they fit.

  • I can explain why a layout fails without sum and why a deep tree disappears.

  • I can build a hierarchy from a flat table with stratify.