{"spec_id":"sunburst-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// sunburst-basic: Basic Sunburst Chart\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 91/100 | Created: 2026-07-26\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Cloud infrastructure spend, thousands USD/month, by business unit -> service -> workload.\nconst data = {\n  name: \"Cloud Spend\",\n  children: [\n    {\n      name: \"Engineering\",\n      children: [\n        {\n          name: \"Compute\",\n          children: [\n            { name: \"Production\", value: 42 },\n            { name: \"Staging\", value: 18 },\n            { name: \"ML Training\", value: 26 },\n          ],\n        },\n        {\n          name: \"Storage\",\n          children: [\n            { name: \"Object Storage\", value: 21 },\n            { name: \"Block Storage\", value: 14 },\n          ],\n        },\n        { name: \"Networking\", value: 19 },\n      ],\n    },\n    {\n      name: \"Data & Analytics\",\n      children: [\n        { name: \"Data Warehouse\", value: 24 },\n        { name: \"Streaming Pipeline\", value: 16 },\n        {\n          name: \"BI Tools\",\n          children: [\n            { name: \"Dashboards\", value: 9 },\n            { name: \"Ad-hoc Queries\", value: 7 },\n          ],\n        },\n      ],\n    },\n    {\n      name: \"Operations\",\n      children: [\n        { name: \"Monitoring\", value: 15 },\n        { name: \"Security\", value: 12 },\n        { name: \"Backup & DR\", value: 11 },\n      ],\n    },\n  ],\n};\n\n// --- Layout -------------------------------------------------------------\nconst margin = { top: 130, right: 20, bottom: 20, left: 20 };\nconst size = Math.min(width - margin.left - margin.right, height - margin.top - margin.bottom);\nconst radius = size / 2;\n\nconst root = d3\n  .hierarchy(data)\n  .sum((d) => d.value)\n  .sort((a, b) => b.value - a.value);\nd3.partition().size([2 * Math.PI, radius])(root);\n\n// --- Color: one hue per top-level branch, lightened per depth -----------\nconst branchNames = root.children.map((d) => d.data.name);\nconst branchColor = d3.scaleOrdinal().domain(branchNames).range(t.palette);\n\nfunction wedgeHsl(d) {\n  let branchNode = d;\n  while (branchNode.depth > 1) branchNode = branchNode.parent;\n  const base = d3.hsl(branchColor(branchNode.data.name));\n  base.l = Math.min(0.82, base.l + (d.depth - 1) * 0.14);\n  return base;\n}\nconst colorFor = (d) => wedgeHsl(d).toString();\n// Label ink is picked by WCAG relative luminance (not HSL lightness, which\n// misjudges green: it's weighted 0.7152 in the WCAG formula vs. 0.0722 for\n// blue, so a green wedge is far brighter than a blue wedge at the same HSL\n// l). Comparing actual contrast ratios against both ink options keeps every\n// label >=4.5:1 regardless of hue, in both themes.\nconst LABEL_DARK = \"#1A1A17\";\nconst LABEL_LIGHT = \"#F0EFE8\";\nfunction relLuminance(color) {\n  const c = d3.rgb(color);\n  const lin = (v) => {\n    v /= 255;\n    return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n  };\n  return 0.2126 * lin(c.r) + 0.7152 * lin(c.g) + 0.0722 * lin(c.b);\n}\nfunction contrastRatio(l1, l2) {\n  const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1];\n  return (hi + 0.05) / (lo + 0.05);\n}\nfunction labelColorFor(d) {\n  const wedgeLum = relLuminance(colorFor(d));\n  const darkContrast = contrastRatio(wedgeLum, relLuminance(LABEL_DARK));\n  const lightContrast = contrastRatio(wedgeLum, relLuminance(LABEL_LIGHT));\n  return darkContrast >= lightContrast ? LABEL_DARK : LABEL_LIGHT;\n}\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${width / 2},${margin.top + radius})`);\n\nconst arc = d3\n  .arc()\n  .startAngle((d) => d.x0)\n  .endAngle((d) => d.x1)\n  .padAngle((d) => Math.min((d.x1 - d.x0) / 2, 0.004))\n  .padRadius(radius / 2)\n  .innerRadius((d) => d.y0)\n  .outerRadius((d) => d.y1 - 1);\n\n// --- Segments (skip the invisible root) ------------------------------------\ng.selectAll(\"path\")\n  .data(root.descendants().filter((d) => d.depth > 0))\n  .join(\"path\")\n  .attr(\"d\", arc)\n  .attr(\"fill\", colorFor)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Labels: only where the angular span leaves room to read ---------------\nfunction labelTransform(d) {\n  const midAngle = ((d.x0 + d.x1) / 2) * (180 / Math.PI);\n  const labelRadius = (d.y0 + d.y1) / 2;\n  const rotate = midAngle - 90;\n  const flip = midAngle < 180 ? 0 : 180;\n  return `rotate(${rotate}) translate(${labelRadius},0) rotate(${flip})`;\n}\n\ng.selectAll(\"text\")\n  .data(root.descendants().filter((d) => d.depth > 0 && d.x1 - d.x0 > 0.11))\n  .join(\"text\")\n  .attr(\"transform\", labelTransform)\n  .attr(\"dy\", \"0.32em\")\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", labelColorFor)\n  .style(\"font-size\", (d) => (d.depth === 1 ? \"16px\" : \"13px\"))\n  .style(\"font-weight\", (d) => (d.depth === 1 ? \"600\" : \"400\"))\n  .style(\"pointer-events\", \"none\")\n  .text((d) => d.data.name);\n\n// --- Title ------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"sunburst-basic · javascript · d3 · anyplot.ai\");\n"}