Composition / Intermediate
Treemap
Compare parts of a hierarchy using nested rectangles sized by value.
Interactive Chart Preview
JAVASCRIPTA custom Chart.js plugin lays out and draws this treemap, with native tooltips.
Edit the column names and values to update your chart. Changes stay in this page until you reload.
| Row# | Unique Node ID | Display Label | Blank for Root | Leaf Value / Calculated Total | Remove row |
|---|---|---|---|---|---|
| 1 | |||||
| 2 | |||||
| 3 | |||||
| 4 | |||||
| 5 | |||||
| 6 | |||||
| 7 |
Use unique IDs and one root with a blank Parent ID. Enter values only for leaves; parent totals are calculated. Parent IDs must exist and must not create a cycle. At least one leaf must be positive.
Chart.js Code Example
JAVASCRIPTimport Chart from 'chart.js/auto';
// Illustrative data used by the live preview.
const chartData = {
"hierarchyHeaders": [
"ID",
"Label",
"Parent ID",
"GB"
],
"nodes": [
{
"id": "workspace",
"label": "Workspace",
"parentId": "",
"value": 0
},
{
"id": "docs",
"label": "Documents",
"parentId": "workspace",
"value": 0
},
{
"id": "assets",
"label": "Assets",
"parentId": "workspace",
"value": 0
},
{
"id": "guides",
"label": "Guides",
"parentId": "docs",
"value": 42
},
{
"id": "notes",
"label": "Notes",
"parentId": "docs",
"value": 28
},
{
"id": "images",
"label": "Images",
"parentId": "assets",
"value": 18
},
{
"id": "video",
"label": "Video",
"parentId": "assets",
"value": 12
}
],
"datasets": []
};
function createConfig(chartData) {
const nodes = chartData.nodes;
const children = (id) => nodes.filter(node => node.parentId === id);
const totals = new Map();
const total = (id) => {
if (totals.has(id))
return totals.get(id);
const items = children(id);
const value = items.length ? items.reduce((sum, child) => sum + total(child.id), 0) : nodes.find(node => node.id === id).value;
totals.set(id, value);
return value;
};
const root = nodes.find(node => !node.parentId);
const palette = ['#2563eb', '#0f766e', '#b45309', '#7c3aed', '#be185d'];
const path = (id) => {
const node = nodes.find(node => node.id === id);
return node.parentId ? [...path(node.parentId), node.label] : [node.label];
};
const rectangles = [];
function tile(id, x, y, width, height, depth, color) {
const items = children(id);
if (!items.length) {
if (total(id) > 0)
rectangles.push({ id, x, y, width, height, color });
return;
}
let offset = 0;
items.forEach((child, index) => {
const share = total(id) ? total(child.id) / total(id) : 0;
const horizontal = depth % 2 === 0;
tile(child.id, x + (horizontal ? offset * width : 0), y + (horizontal ? 0 : offset * height), horizontal ? width * share : width, horizontal ? height : height * share, depth + 1, depth === 0 ? palette[index % palette.length] : color);
offset += share;
});
}
tile(root.id, 0, 0, 1, 1, 0, palette[0]);
return {
type: 'scatter',
data: { datasets: [{ data: rectangles.map(rect => ({ x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 })), pointRadius: 0, pointHoverRadius: 0, pointHitRadius: 0 }] },
options: {
responsive: true, maintainAspectRatio: false, animation: false,
layout: { padding: 10 },
scales: { x: { min: 0, max: 1, display: false }, y: { min: 0, max: 1, reverse: true, display: false } },
plugins: { legend: { display: false }, tooltip: { callbacks: {
title: items => path(rectangles[items[0].dataIndex].id),
label: item => {
const value = total(rectangles[item.dataIndex].id);
return `${chartData.hierarchyHeaders[3]}: ${value} (${(value / total(root.id) * 100).toFixed(1)}% of total)`;
},
} } },
},
// Chart.js has no built-in treemap controller. Draw proportional rectangles
// and route rectangle hit tests through its native tooltip.
plugins: [{
id: 'hierarchy-rectangles',
afterDatasetsDraw(chart) {
const { ctx, scales } = chart;
ctx.save();
rectangles.forEach(rect => {
const x = scales.x.getPixelForValue(rect.x), y = scales.y.getPixelForValue(rect.y);
const width = scales.x.getPixelForValue(rect.x + rect.width) - x;
const height = scales.y.getPixelForValue(rect.y + rect.height) - y;
ctx.fillStyle = rect.color;
ctx.fillRect(x, y, width, height);
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.strokeRect(x, y, width, height);
if (width < 65 || height < 40)
return;
ctx.save();
ctx.beginPath();
ctx.rect(x + 4, y + 4, width - 8, height - 8);
ctx.clip();
ctx.fillStyle = '#fff';
ctx.font = '12px sans-serif';
ctx.textBaseline = 'top';
ctx.fillText(path(rect.id).slice(1).join(' / ') || root.label, x + 9, y + 10);
ctx.fillText(`${total(rect.id)} ${chartData.hierarchyHeaders[3]}`, x + 9, y + 27);
ctx.restore();
});
ctx.restore();
},
afterEvent(chart, args) {
const { x, y, type } = args.event;
const index = x === null || y === null || type === 'mouseout' ? -1 : rectangles.findIndex(rect => {
const px = chart.scales.x.getValueForPixel(x);
const py = chart.scales.y.getValueForPixel(y);
return px >= rect.x && px <= rect.x + rect.width && py >= rect.y && py <= rect.y + rect.height;
});
const active = index < 0 ? [] : [{ datasetIndex: 0, index }];
chart.setActiveElements(active);
chart.tooltip?.setActiveElements(active, { x: x ?? 0, y: y ?? 0 });
args.changed = true;
},
}],
};
}
const chart = new Chart(document.getElementById('myChart'), createConfig(chartData));Preview and code follow the same library selection.
Run This Example
Install the package below in a browser project such as Vite. Put the copied code in a client JavaScript module and add this container to the page before the module runs.
npm install chart.js@4.5.1<div style="height:320px"><canvas id="myChart"></canvas></div>The example includes its sample data. When removing the chart, destroy or dispose of its instance and disconnect any resize observer.
Reading This Example
This illustrative workspace contains 100 GB. Documents accounts for 70 GB, split between Guides at 42 and Notes at 28. Assets accounts for 30 GB, split between Images at 18 and Video at 12. Folder totals are derived from these four leaves.
All library tabs use these illustrative values. Rendering styles and library APIs differ, so use the table to compare exact numbers.
About this example & sample data
| ID | Label | Parent ID | GB |
|---|---|---|---|
| workspace | Workspace | (Root) | 100 |
| docs | Documents | workspace | 70 |
| assets | Assets | workspace | 30 |
| guides | Guides | docs | 42 |
| notes | Notes | docs | 28 |
| images | Images | assets | 18 |
| video | Video | assets | 12 |
READ THE CHART
Compare Values Inside a Hierarchy
A treemap divides a rectangle into nested groups. Each leaf receives an area proportional to its value. It is useful for finding large contributions within a whole, especially when the items belong to folders, families, or other parent groups.
Enter Leaves and Calculate Parents
Each row has a unique ID, a display label, a parent ID, and a value. Exactly one root has a blank parent ID. Enter amounts only for leaves, which have no children. Parent cells show calculated totals, so a folder and its contents are never counted twice. Labels may repeat because IDs define the relationships.
Read Area and Group Membership
Compare rectangle areas, not widths or heights alone. Colors identify branches from the root rather than a second numeric measure. Hover over a leaf to read its path, value, and share of the whole. Small rectangles may hide labels; the data table retains every node and its value.
Keep One Valid Hierarchy
Parent IDs must refer to existing nodes, and parent links cannot form cycles. Values must be nonnegative and the root total must be positive. Zero-value leaves stay in the data but occupy no area. When an edit leaves an incomplete hierarchy, the previous valid preview and code remain available until the problem is fixed.
Allow for Different Layouts
Chart.js, D3, Plotly, and the Python examples alternate horizontal and vertical splits by depth. ECharts uses its own treemap layout, so positions and shapes differ while values remain the same. Chart.js and Python use custom drawing; Seaborn supplies the Matplotlib theme. Use a bar chart when precise comparisons matter more than the hierarchy.