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.
Try it yourself
- 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 inbikes.json. Pedal bikes still exist as nodes with value 0. - 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
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
`value` on a node is the sum of its leaves. Quay has 5 e-bikes and 8 pedal bikes, so 13.
Preview
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 rootsTry 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.
Four rectangles are too small for their names. In the picture they keep a tooltip (hover to see it).
Preview
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)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
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 0Break 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.
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.
treemapSquarifyhas aratio(default the golden ratio, 1.618).treemapResquarifykeeps the layout stable when values change slightly, which matters for animations. - ▸
A hierarchy mutates:
treemap(root)setsx0,y0... on the same node objects. Calling a layout twice on one root overwrites the first result. Make a new hierarchy (orroot.copy()) for a second layout, as the third example does withmake(). - ▸
hierarchy.sumis cached on the node: if the underlying data changes, callsumagain. For big trees,root.eachBeforeandeachAfterwalk 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
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
**
d3.hierarchy(data)wraps nested data.** It readsdata.children(an array) and makes a node for every item. Each node hasdata(your object),depth(0 at the root),height(the longest way down),parentandchildren. Methods give you the family:descendants(),leaves(),ancestors(),links(). Fleet: 82 nodes, 50 leaves, height 3. - 3
**
sumandsortprepare the numbers.**root.sum((d) => d.value)sets each node'svalueto its own value plus all its descendants' values. Root: 320 bikes. A node whose value is missing,NaNor text counts as 0. A node withchildren: []is a leaf.root.sort((a, b) => b.value - a.value)puts the largest first, which most layouts need to look good. Withoutsum,valueis undefined and the layout cannot work. - 4
Each layout adds numbers to the nodes.
d3.treemap()addsx0, y0, x1, y1: a rectangle whose area is the value.d3.pack()addsx, y, r: a circle whose area is the value, packed in its parent.d3.partition()addsx0, x1, y0, y1in a unit rectangle: draw them as rectangles (an icicle) or as arcs (a sunburst).d3.tree()andd3.cluster()addx, yfor node-link diagrams, where the shape of the tree matters and the value does not. - 5
Treemap details that matter. The tiling method (
squarifyby default) chooses the rectangles' shapes: squarify makes them nearly square,treemapSliceDicealternates strips by level and keeps order.paddingInner,paddingOuterandpaddingTopleave 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
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
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
What does d3.hierarchy return, and what does sum add?
How is the data of a treemap different from the data of a force layout?
Why is the node's label dropped when a rectangle is small, and what keeps the information reachable?
A chart has twelve levels of nesting with padding at each level. What goes wrong?
Practice
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.
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
sumand why a deep tree disappears.I can build a hierarchy from a flat table with
stratify.