Composition / Intermediate

Treemap

Compare parts of a hierarchy using nested rectangles sized by value.

Back to collection

Interactive Chart Preview

JAVASCRIPT

A custom Chart.js plugin lays out and draws this treemap, with native tooltips.

Live, interactive exampleChart.js

Chart.js Code Example

JAVASCRIPT
import 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
Sample values used in the chart
IDLabelParent IDGB
workspaceWorkspace(Root)100
docsDocumentsworkspace70
assetsAssetsworkspace30
guidesGuidesdocs42
notesNotesdocs28
imagesImagesassets18
videoVideoassets12

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.

01

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.

02

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.

03

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.

04

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.

Edit Chart Data