Composition / Advanced

Sunburst Chart

Follow a hierarchy outward from its root and compare branch shares.

Back to collection

Interactive Chart Preview

JAVASCRIPT

A custom Chart.js plugin draws radial partitions 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 depth = Math.max(...nodes.map(node => path(node.id).length));
    const sectors = [];
    function divide(id, start, end, level, color) {
        if (total(id) <= 0)
            return;
        sectors.push({ id, start, end, inner: level / depth, outer: (level + 1) / depth, color });
        let angle = start;
        children(id).forEach((child, index) => {
            const next = angle + (end - start) * total(child.id) / total(id);
            divide(child.id, angle, next, level + 1, level === 0 ? palette[index % palette.length] : color);
            angle = next;
        });
    }
    divide(root.id, 0, Math.PI * 2, 0, '#334155');
    const geometry = (chart) => ({ x: (chart.chartArea.left + chart.chartArea.right) / 2,
        y: (chart.chartArea.top + chart.chartArea.bottom) / 2,
        radius: Math.min(chart.chartArea.width, chart.chartArea.height) / 2 - 6 });
    return {
        type: 'scatter',
        data: { datasets: [{ data: sectors.map(sector => {
                        const angle = (sector.start + sector.end) / 2 - Math.PI / 2;
                        const radius = sector.inner === 0 ? 0 : (sector.inner + sector.outer) / 2;
                        return { x: Math.cos(angle) * radius, y: Math.sin(angle) * radius };
                    }), pointRadius: 0, pointHoverRadius: 0, pointHitRadius: 0 }] },
        options: {
            responsive: true, maintainAspectRatio: false, animation: false,
            scales: { x: { min: -1, max: 1, display: false }, y: { min: -1, max: 1, reverse: true, display: false } },
            plugins: { legend: { display: false }, tooltip: { callbacks: {
                        title: items => path(sectors[items[0].dataIndex].id),
                        label: item => `${chartData.hierarchyHeaders[3]}: ${total(sectors[item.dataIndex].id)} (${(total(sectors[item.dataIndex].id) / total(root.id) * 100).toFixed(1)}% of total)`,
                    } } },
        },
        // A custom radial partition, with native Chart.js tooltip hit testing.
        plugins: [{ id: 'hierarchy-sectors', afterDatasetsDraw(chart) {
                    const { ctx } = chart, { x, y, radius } = geometry(chart);
                    ctx.save();
                    sectors.forEach(sector => {
                        const start = sector.start - Math.PI / 2, end = sector.end - Math.PI / 2;
                        const inner = sector.inner * radius, outer = sector.outer * radius;
                        ctx.beginPath();
                        ctx.arc(x, y, outer, start, end);
                        if (inner > 0)
                            ctx.arc(x, y, inner, end, start, true);
                        else if (end - start < Math.PI * 2)
                            ctx.lineTo(x, y);
                        ctx.closePath();
                        ctx.fillStyle = sector.color;
                        ctx.fill();
                        ctx.strokeStyle = '#fff';
                        ctx.lineWidth = Math.min(1, (outer - inner) * 0.2);
                        ctx.stroke();
                        const middle = (start + end) / 2, labelRadius = inner === 0 ? 0 : (inner + outer) / 2;
                        if (outer - inner < 28 || (inner > 0 && (end - start) * labelRadius < 28))
                            return;
                        const available = inner === 0 ? outer * 1.6 : outer - inner - 8;
                        const limit = Math.floor(available / 6);
                        const raw = nodes.find(node => node.id === sector.id).label;
                        const label = raw.length > limit ? raw.slice(0, Math.max(0, limit - 1)) + '…' : raw;
                        ctx.save();
                        ctx.clip();
                        ctx.translate(x + Math.cos(middle) * labelRadius, y + Math.sin(middle) * labelRadius);
                        if (inner > 0)
                            ctx.rotate(middle + (Math.cos(middle) < 0 ? Math.PI : 0));
                        ctx.fillStyle = '#fff';
                        ctx.font = '11px sans-serif';
                        ctx.textAlign = 'center';
                        ctx.textBaseline = 'middle';
                        ctx.fillText(label, 0, -6);
                        ctx.fillText(String(total(sector.id)), 0, 8, available);
                        ctx.restore();
                    });
                    ctx.restore();
                }, afterEvent(chart, args) {
                    const { x, y, type } = args.event, center = geometry(chart);
                    const distance = x === null || y === null ? -1 : Math.hypot(x - center.x, y - center.y) / center.radius;
                    const angle = x === null || y === null ? 0 : (Math.atan2(y - center.y, x - center.x) + Math.PI / 2 + Math.PI * 2) % (Math.PI * 2);
                    const index = type === 'mouseout' ? -1 : sectors.findIndex(sector => distance >= sector.inner && distance <= sector.outer && angle >= sector.start && angle <= sector.end);
                    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, matching the Treemap example. Documents uses 70 GB, so its branch spans 70% of the circle. Guides uses 42 GB, which is 42% of the workspace and 60% of Documents. Notes uses 28 GB. Assets uses the remaining 30 GB, split between Images at 18 and Video at 12.

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

Follow Branches From the Center Outward

A sunburst chart puts the root of a hierarchy in the center and its descendants in successive rings. A child occupies an angular part of its parent branch. It helps you follow category membership while seeing which branches contribute most to the whole.

01

Enter Values at the Leaves

Use unique IDs to connect each row to its parent. One root has a blank Parent ID. Enter amounts only for leaves; parent totals are calculated from their descendants. Display labels can repeat because IDs define identity. Parent references must exist and cannot form cycles.

02

Compare Angles Within the Hierarchy

Each parent span is divided among its children in proportion to their values. Compare angles within a ring or follow a branch outward. Do not compare sector areas across rings: outer rings have a larger circumference. Color identifies the top-level branch and does not encode another quantity.

03

Keep the Denominator Clear

Tooltips report each path, value, and share of the original root total. A parent already includes its children, so adding every visible sector double counts the data. Zero-value leaves remain in the table and copied data but have no angular span. At least one leaf must be positive.

04

Allow Space for Labels

Small sectors and deep trees can be difficult to read. Examples omit labels that do not fit; use the table and tooltips for full values. Native layouts can differ in rotation, ring sizing, and label placement. Plotly lets you click into a branch and click the center to return; tooltip shares still use the original total. Use Treemap for a rectangular hierarchy or Bar Chart for precise value comparisons.

Edit Chart Data