{"spec_id":"sunburst-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// sunburst-basic: Basic Sunburst Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.1\n// Quality: 91/100 | Created: 2026-07-26\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: local drive usage, 3-level folder hierarchy (MB) -----------------\n// Each parent's value equals the sum of its children, so every ring shares the\n// same total and matching order — this is what keeps a parent arc's angular\n// span exactly encompassing its children's combined span across rings.\nconst level1 = [\n  { label: \"Documents\", value: 320 },\n  { label: \"Photos\", value: 280 },\n  { label: \"Code\", value: 260 },\n  { label: \"Videos\", value: 140 },\n];\n\nconst level2 = [\n  { parent: 0, label: \"Reports\", value: 140 },\n  { parent: 0, label: \"Invoices\", value: 100 },\n  { parent: 0, label: \"Contracts\", value: 80 },\n  { parent: 1, label: \"Vacation\", value: 160 },\n  { parent: 1, label: \"Family\", value: 120 },\n  { parent: 2, label: \"Frontend\", value: 150 },\n  { parent: 2, label: \"Backend\", value: 110 },\n  { parent: 3, label: \"Tutorials\", value: 90 },\n  { parent: 3, label: \"Clips\", value: 50 },\n];\n\nconst level3 = [\n  { parent: 0, label: \"Q1 Report\", value: 80 },\n  { parent: 0, label: \"Q2 Report\", value: 60 },\n  { parent: 1, label: \"Client A\", value: 55 },\n  { parent: 1, label: \"Client B\", value: 45 },\n  { parent: 2, label: \"Vendor X\", value: 50 },\n  { parent: 2, label: \"Vendor Y\", value: 30 },\n  { parent: 3, label: \"Summer 2024\", value: 95 },\n  { parent: 3, label: \"Winter 2024\", value: 65 },\n  { parent: 4, label: \"Birthdays\", value: 70 },\n  { parent: 4, label: \"Holidays\", value: 50 },\n  { parent: 5, label: \"React App\", value: 90 },\n  { parent: 5, label: \"UI Kit\", value: 60 },\n  { parent: 6, label: \"API Server\", value: 65 },\n  { parent: 6, label: \"Database\", value: 45 },\n  { parent: 7, label: \"Beginner\", value: 55 },\n  { parent: 7, label: \"Advanced\", value: 35 },\n  { parent: 8, label: \"Highlights\", value: 30 },\n  { parent: 8, label: \"Bloopers\", value: 20 },\n];\n\n// --- Branch coloring: same hue per branch, lighter tint per outward ring ----\n// Tints mix toward white (not the page background) so data colors stay\n// identical between light and dark themes — only chrome flips.\nconst branchColors = [t.palette[0], t.palette[1], t.palette[2], t.palette[3]];\n\nfunction tint(hex, amount) {\n  const n = parseInt(hex.slice(1), 16);\n  const mix = (c) => Math.round(c + (255 - c) * amount);\n  const r = mix((n >> 16) & 255);\n  const g = mix((n >> 8) & 255);\n  const b = mix(n & 255);\n  return `#${[r, g, b].map((c) => c.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\nfunction relativeLuminance(r, g, b) {\n  const lin = (c) => {\n    const s = c / 255;\n    return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);\n  };\n  return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);\n}\n\nfunction contrastRatio(l1, l2) {\n  const lighter = Math.max(l1, l2);\n  const darker = Math.min(l1, l2);\n  return (lighter + 0.05) / (darker + 0.05);\n}\n\nfunction labelColorFor(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  const luminance = relativeLuminance((n >> 16) & 255, (n >> 8) & 255, n & 255);\n  const darkContrast = contrastRatio(luminance, relativeLuminance(0x1a, 0x1a, 0x17));\n  const lightContrast = contrastRatio(luminance, relativeLuminance(0xf0, 0xef, 0xe8));\n  return darkContrast >= lightContrast ? \"#1A1A17\" : \"#F0EFE8\";\n}\n\nconst level1Colors = level1.map((d, i) => branchColors[i]);\nconst level2Colors = level2.map((d) => tint(branchColors[d.parent], 0.35));\nconst level3Colors = level3.map((d) => tint(branchColors[level2[d.parent].parent], 0.6));\n\n// --- Breadcrumb paths for tooltips (leaf-level detail is hover-only) --------\nconst level1Paths = level1.map((d) => d.label);\nconst level2Paths = level2.map((d) => `${level1[d.parent].label} ▸ ${d.label}`);\nconst level3Paths = level3.map((d) => {\n  const mid = level2[d.parent];\n  return `${level1[mid.parent].label} ▸ ${mid.label} ▸ ${d.label}`;\n});\n\n// --- Custom plugin: draw labels directly on segments wide enough to hold ----\n// them (the sunburst spec calls for labeling major segments only, leaving\n// smaller ones to the interactive tooltip).\nconst MIN_LABEL_ANGLE = (20 * Math.PI) / 180;\n\nconst radialLabels = {\n  id: \"radialLabels\",\n  afterDatasetsDraw(chart) {\n    const { ctx } = chart;\n    ctx.save();\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    chart.data.datasets.forEach((dataset, dsIndex) => {\n      if (!dataset.sliceLabels) return;\n      const meta = chart.getDatasetMeta(dsIndex);\n      meta.data.forEach((arc, i) => {\n        const { startAngle, endAngle } = arc.getProps([\"startAngle\", \"endAngle\"], true);\n        if (endAngle - startAngle < MIN_LABEL_ANGLE) return;\n        const { x, y } = arc.getCenterPoint(true);\n        ctx.font = dataset.labelFont;\n        ctx.fillStyle = dataset.sliceLabelColors[i];\n        ctx.fillText(dataset.sliceLabels[i], x, y);\n      });\n    });\n    ctx.restore();\n  },\n};\nChart.register(radialLabels);\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart: three concentric doughnut rings = one sunburst ------------------\n// Dataset order matters: Chart.js draws dataset 0 as the outermost ring, so\n// the leaf level (level3) comes first and the root level (level1) comes last.\nnew Chart(canvas, {\n  type: \"doughnut\",\n  data: {\n    datasets: [\n      {\n        data: level3.map((d) => d.value),\n        backgroundColor: level3Colors,\n        borderColor: t.pageBg,\n        borderWidth: 2,\n        spacing: 2,\n        weight: 1,\n        paths: level3Paths,\n      },\n      {\n        data: level2.map((d) => d.value),\n        backgroundColor: level2Colors,\n        borderColor: t.pageBg,\n        borderWidth: 2,\n        spacing: 2,\n        weight: 1,\n        paths: level2Paths,\n        sliceLabels: level2.map((d) => d.label),\n        sliceLabelColors: level2Colors.map(labelColorFor),\n        labelFont: \"500 15px sans-serif\",\n      },\n      {\n        data: level1.map((d) => d.value),\n        backgroundColor: level1Colors,\n        borderColor: t.pageBg,\n        borderWidth: 2,\n        spacing: 2,\n        weight: 1,\n        paths: level1Paths,\n        sliceLabels: level1.map((d) => d.label),\n        sliceLabelColors: level1Colors.map(labelColorFor),\n        labelFont: \"600 20px sans-serif\",\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    cutout: \"8%\",\n    plugins: {\n      title: {\n        display: true,\n        text: \"sunburst-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 24 },\n      },\n      legend: { display: false },\n      tooltip: {\n        callbacks: {\n          title: (items) => items[0].dataset.paths[items[0].dataIndex],\n          label: (item) => `${item.parsed} MB`,\n        },\n      },\n    },\n  },\n});\n"}