Built with ApexCharts.js, ApexTree, ApexSankey
Five renderers compete for hierarchical data: a tree, a treemap, a sunburst, an icicle, and a Sankey diagram. The choice is usually made on appearance, which is the wrong axis. Between them they need only three input shapes, and which of those your data already has is what decides what each one is able to say. Work from the data and the answer is usually forced.
Which one does your data allow?
Start with the shape you have, not the picture you want.
| What your data is | Renderer | Because |
|---|---|---|
| Parent and child, no meaningful quantity | Tree (ApexTree) | Its node type has no value field at all. It encodes structure, and nothing else. |
| Nesting plus a quantity per leaf, read as part-to-whole | Treemap, Sunburst or Icicle (ApexCharts) | All three take one nested shape with values. Area, angle or length carries proportion. |
| The above, and leaves must be compared precisely | Icicle or Treemap | Both put proportion on a linear channel. Angles at depth are the hardest of the three to judge. |
| The above, plus a second metric per leaf | Treemap | Only the treemap has colorValue: size says one thing, colour says another. |
| Nodes with more than one parent | Sankey (ApexSankey) | It takes an edge list, so a node may have many inputs. The other four cannot represent this at all. |
| Quantities that move between stages, merging and splitting | Sankey | Edges carry the value. In the other four, value lives on the node. |
Two rows in that table decide most cases, and they are the two people skip.
Is your data actually a tree?
Four of the five require a tree: exactly one parent per node, one root. Only Sankey does not.
So the first question is not aesthetic. It is whether any node has two parents. A customer who buys from two channels, a component used by two assemblies, a person on two teams. If that happens even once, the tree and all three partition charts are structurally wrong, and the only way to force the data in is to duplicate the node, which double-counts its value.
That is a data question you can answer with one query, before you choose a library at all:
// More than one parent anywhere? Then it is a graph, not a hierarchy.
const parents = new Map()
for (const row of rows) {
if (!parents.has(row.id)) parents.set(row.id, new Set())
if (row.parentId != null) parents.get(row.id).add(row.parentId)
}
const multiParent = [...parents].filter(([, set]) => set.size > 1)
console.log(multiParent.length ? 'Not a tree:' : 'A tree.', multiParent)
The demo at the top of this page has that case on a switch. Give one node a second parent and four of the five renderers start reporting a total that is too high by exactly the duplicated value, with nothing thrown and five pictures that all look plausible.
The same dataset, in three input shapes
One flat table, as it comes out of a database, is the honest starting point:
const rows = [
{ id: 'emea', parentId: null, name: 'EMEA', revenue: null },
{ id: 'de', parentId: 'emea', name: 'Germany', revenue: 4200 },
{ id: 'fr', parentId: 'emea', name: 'France', revenue: 3100 },
{ id: 'apac', parentId: null, name: 'APAC', revenue: null },
{ id: 'jp', parentId: 'apac', name: 'Japan', revenue: 5300 },
]
Tree. ApexTree takes nested nodes, and data is required even when you have
nothing to put in it. There is no value field, so revenue has nowhere to go:
{ id: 'emea', name: 'EMEA', data: {}, children: [
{ id: 'de', name: 'Germany', data: { revenue: 4200 }, children: [] },
{ id: 'fr', name: 'France', data: { revenue: 3100 }, children: [] },
]}
Your quantity can ride along in data and be rendered by a node template, but
the layout will not use it. Nothing is sized by revenue.
Treemap, sunburst and icicle. One shape serves all three. The type
definitions call them the partition charts, and a branch may omit its own value
and take the sum of its children, which is exactly what the nulls above want:
series: [{ data: [
{ x: 'EMEA', children: [
{ x: 'Germany', y: 4200 },
{ x: 'France', y: 3100 },
]},
{ x: 'APAC', children: [{ x: 'Japan', y: 5300 }] },
]}]
name is accepted for x and value for y, so either naming works. Because
the input is identical, moving between the three is a one-word change to
chart.type once each type is loaded, and there is a morph demo
that animates between two of them on one dataset.
That "once each type is loaded" is the one practical asterisk, and it is the only place the three diverge. Only the treemap is in the default build. The sunburst left it in ApexCharts 8.0 and the icicle was never in it, so a page that draws all three keeps the default import and adds the other two beside it:
import ApexCharts from 'apexcharts'
import 'apexcharts/sunburst'
import 'apexcharts/icicle'
Keep that first line. import ApexCharts from 'apexcharts/icicle' on its own
hands back the lean core with only the icicle registered, so the treemap, the
sunburst and the default features (legend, toolbar, export and the rest) would
all be missing. With script tags, the two add-on files sit next to the
ApexCharts script:
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<script src="https://cdn.jsdelivr.net/npm/apexcharts/dist/sunburst.js"></script>
<script src="https://cdn.jsdelivr.net/npm/apexcharts/dist/icicle.js"></script>
Miss one and that chart draws nothing, but it does not fail quietly: the console
gets one line, ApexCharts: chart type "sunburst" is not registered, followed by
the import and the script tag that fix it, and render() rejects with the same
message. If one file matters more than its size, apexcharts.full.min.js (or
import ApexCharts from 'apexcharts/full') has all three.
Sankey. A flat edge list, and the hierarchy becomes edges from parent to child:
const nodes = rows.map((r) => ({ id: r.id, title: r.name }))
const edges = rows
.filter((r) => r.parentId)
.map((r) => ({
source: r.parentId,
target: r.id,
value: r.revenue,
type: 'revenue', // required, not optional: it is the grouping/tooltip label
}))
sankey.render({ nodes, edges })
Two things to note. type is a required field on an edge, so a first attempt
with only source, target and value will not type-check. And every edge
needs a value, so intermediate nulls have to be resolved before conversion,
whereas the partition charts sum them for you.
What each one can and cannot say
| Tree | Treemap | Sunburst | Icicle | Sankey | |
|---|---|---|---|---|---|
| Shows structure | Best | Poorly beyond 2 levels | Yes, by ring | Yes, one band per level | Yes, by column |
| Shows proportion | No | Best, by area | Yes, by angle | Yes, by length | Yes, by band width |
| Compares leaf values | No | Yes | Harder: angles at depth are thin | Best: a common linear scale | Yes |
| Second metric per node | Via a template | Yes, colorValue | No | No | No |
| Multiple parents | No | No | No | No | Yes |
| Deep hierarchies | Yes, collapsible | Cramped | Rings get thin fast | Yes, with maxDepth | Wide, not deep |
| Reads exact numbers | Labels | Labels | Rarely fits | Labels, with room for them | Tooltip |
| In the default bundle | n/a, own library | Yes | No, separate entry since 8.0 | No, separate entry | n/a, own library |
Two rows are worth remembering. The treemap's colorValue is the only way in
this set to put two metrics on one picture: revenue as area, growth as colour, in
the same rectangle. The second switch on the demo turns a growth figure on for
every leaf, and only one of the five panels changes, because colorScale is
defined on the treemap's options and on neither of the other two partition
charts, which are handed the identical array.
The other is the icicle's row for comparing leaves. Because every level sits on
one linear axis, two cells on the same band can be compared by length, which is
the reason profiling tools settled on this shape: with sort: 'name' a frame
keeps its place between runs, so two profiles can be read against each other by
eye.
When none of the five is right
| What you want to show | Reach for |
|---|---|
| Ranking twelve items, hierarchy incidental | A bar chart. A treemap makes the reader compare areas to recover an order a bar chart just states. |
| A hierarchy people need to read values off | A data grid with tree data. Indented rows, exact numbers, sortable. |
| How a hierarchy changed over time | Small multiples, or a line chart per branch. All five of these are snapshots. |
| Reporting lines, up to a few hundred people | A tree, but see the org chart guide for the flat-to-nested conversion first. |
| Flows across several categorical dimensions | An alluvial diagram, which is a Sankey whose stages are dimensions rather than a hierarchy. |
| Two levels only, and proportion is the point | A stacked bar or a pie. A treemap of six rectangles is a pie chart that is harder to read. |
Which plans include these?
This is the one place the five options genuinely differ in cost, and it is worth knowing before you get attached to one.
| Renderer | Library | Plan |
|---|---|---|
| Treemap | ApexCharts.js | Community and up, so covered by the under-$2M waiver |
| Sunburst | ApexCharts.js | Community and up, same, but a separate bundle entry |
| Icicle | ApexCharts.js | Community and up, same, but a separate bundle entry |
| Tree | ApexTree | Pro and up |
| Sankey | ApexSankey | Pro and up |
Community is the entry plan and is free for organizations under $2M USD in annual revenue, budget or funding; at or above that it is a paid licence like the others. Nothing in the family is open source, and source published on GitHub is not an open licence. Every one of these renders in full without a licence key, watermarked, so you can try all five against your real data before any of this matters. The pricing page has the matrix.
One dataset, drawn as both a treemap and a sunburstSee the pieces running
Reference documentation
Frequently Asked Questions
What is the difference between a treemap, a sunburst and an icicle?
Only the geometry. All three are partition charts in ApexCharts and take the identical nested input, so moving between them is a one-word change to chart.type once each type is loaded (only the treemap is in the default bundle). A treemap encodes proportion as rectangle area, which packs densely and labels well. A sunburst encodes it as angle on concentric rings, which shows depth clearly but gets thin fast at deeper levels. An icicle is the sunburst unrolled into straight bands, so two cells on the same level can be compared by length rather than by angle, and there is room for labels.
When should I use a Sankey instead of a treemap?
When any node has more than one parent, or when the quantity moves between stages rather than belonging to a node. A Sankey takes a flat edge list, so multiple inputs per node are natural. The tree and all three partition charts require exactly one parent per node, and forcing a multi-parent graph into one means duplicating nodes and double-counting their values. The demo on this page puts that case on a switch: one node with a second parent, and four of the five totals go wrong by the duplicated amount with nothing thrown.
Can a tree diagram show quantities?
Not in its layout. ApexTree's node type has no value field: it encodes structure only. A quantity can ride along in the required data payload and be drawn by a node template, but nothing is sized or ordered by it. If magnitude is the message, use a partition chart instead.
How do I show two metrics on one hierarchy?
Use a treemap with colorValue. It is the only one of the five that separates the two channels: size comes from the value and colour from a second metric, so revenue can be area while growth is colour in the same rectangle. The sunburst and the icicle take the identical input and ignore the field, because colorScale is defined on plotOptions.treemap and on neither of theirs, and the Sankey has one channel, the edge width.
Which of these does the Community plan include?
Treemap, sunburst and icicle, because they are ApexCharts.js chart types and ApexCharts.js is in Community, which is free for organizations under $2M USD in annual revenue, budget or funding. ApexTree and ApexSankey are separate libraries included from the Pro plan upward, so the waiver does not extend to them. All five render in full without a licence key, watermarked, so you can compare them on your own data first.
Why does my sunburst or icicle chart say the chart type "is not registered"?
Because neither is in the default bundle. The icicle has been a separate entry since ApexCharts 7.6, and from 8.0 the sunburst is too, so load each next to the default bundle: import 'apexcharts/sunburst' or import 'apexcharts/icicle' after import ApexCharts from 'apexcharts', or with script tags, dist/sunburst.js or dist/icicle.js alongside the ApexCharts script. Without the entry, the console logs one line naming the import to add, and the chart draws nothing. The treemap is still in the default bundle, so it is the one partition chart that needs nothing extra, and apexcharts.full.min.js (import ApexCharts from 'apexcharts/full' in a bundler) has all three.
Related
Watch a treemap become a sunburst
The morph demo animates one hierarchy between the two geometries without touching the data, which is the clearest way to see that they take the same input.