{"spec_id":"donut-nested","library":"d3","language":"javascript","code":"// anyplot.ai\n// donut-nested: Nested Donut Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-08-18\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic): annual department budget, $K ---------\nconst departments = [\n  {\n    name: \"Engineering\",\n    children: [\n      { name: \"Salaries\", value: 420 },\n      { name: \"Cloud Infra\", value: 150 },\n      { name: \"Licenses\", value: 70 },\n      { name: \"Training\", value: 25 },\n    ],\n  },\n  {\n    name: \"Marketing\",\n    children: [\n      { name: \"Advertising\", value: 160 },\n      { name: \"Events\", value: 90 },\n      { name: \"Content\", value: 65 },\n      { name: \"Tools\", value: 20 },\n    ],\n  },\n  {\n    name: \"Sales\",\n    children: [\n      { name: \"Commissions\", value: 210 },\n      { name: \"Travel\", value: 95 },\n      { name: \"Software\", value: 30 },\n    ],\n  },\n  {\n    name: \"Operations\",\n    children: [\n      { name: \"Facilities\", value: 140 },\n      { name: \"Logistics\", value: 95 },\n      { name: \"Admin\", value: 65 },\n      { name: \"Insurance\", value: 25 },\n    ],\n  },\n];\n\nconst root = d3.hierarchy({ name: \"Budget\", children: departments }).sum((d) => d.value);\nd3.partition().size([2 * Math.PI, 1])(root);\nconst grandTotal = root.value;\n\n// --- Radii: two rings with a visible gap; angles come straight from the ----\n// --- hierarchy, so child segments align exactly under their parent's arc ---\nconst HOLE = 90;\nconst RING1_OUT = 195;\nconst RING2_IN = 203;\nconst RING2_OUT = 380;\nconst radiusFor = (depth) => (depth === 1 ? { inner: HOLE, outer: RING1_OUT } : { inner: RING2_IN, outer: RING2_OUT });\n\nconst arc = d3\n  .arc()\n  .startAngle((d) => d.x0)\n  .endAngle((d) => d.x1)\n  .padAngle(0.006)\n  .cornerRadius(2)\n  .innerRadius((d) => radiusFor(d.depth).inner)\n  .outerRadius((d) => radiusFor(d.depth).outer);\n\n// --- Color: one Imprint hue per department, lightened per child ------------\nconst deptNames = departments.map((d) => d.name);\nconst deptColor = d3.scaleOrdinal().domain(deptNames).range(t.palette.slice(0, deptNames.length));\n\nconst shade = (hex, i, n) => {\n  const c = d3.hsl(hex);\n  c.l = Math.min(0.86, c.l + ((i + 1) / (n + 1)) * 0.34);\n  return c.formatHex();\n};\n\nconst fillFor = (d) => {\n  if (d.depth === 1) return deptColor(d.data.name);\n  const base = deptColor(d.parent.data.name);\n  const siblings = d.parent.children;\n  return shade(base, siblings.indexOf(d), siblings.length);\n};\n\n// Relative-luminance check so segment labels stay legible on every shade.\nconst luminance = (hex) => {\n  const c = d3.rgb(hex);\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};\nconst labelInkFor = (fill) => (luminance(fill) > 0.42 ? t.ink : t.pageBg);\n\n// --- Mount -------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// Centered lower in the mount (below the title block) so the donut's full\n// vertical extent is available to balance against the legend column.\nconst cx = 420;\nconst cy = 630;\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${cx},${cy})`);\n\nconst nodes = root.descendants().filter((d) => d.depth > 0);\n\ng.selectAll(\"path\")\n  .data(nodes)\n  .join(\"path\")\n  .attr(\"d\", arc)\n  .attr(\"fill\", fillFor)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Radial labels on segments large enough to hold text; smaller ones -----\n// --- fall through to the legend below (per spec: label large, legend small) -\n// Threshold raised alongside the bigger label fontsizes below so labels never\n// crowd a segment too small to hold them.\nconst labelVisible = (d) => (d.x1 - d.x0) * ((radiusFor(d.depth).inner + radiusFor(d.depth).outer) / 2) > 46;\n\nconst labelTransform = (d) => {\n  const deg = ((d.x0 + d.x1) / 2) * (180 / Math.PI);\n  const r = (radiusFor(d.depth).inner + radiusFor(d.depth).outer) / 2;\n  return `rotate(${deg - 90}) translate(${r},0) rotate(${deg < 180 ? 0 : 180})`;\n};\n\ng.selectAll(\"text.segment\")\n  .data(nodes.filter(labelVisible))\n  .join(\"text\")\n  .attr(\"class\", \"segment\")\n  .attr(\"transform\", labelTransform)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dy\", \"0.32em\")\n  .style(\"font-size\", (d) => (d.depth === 1 ? \"18px\" : \"15px\"))\n  .style(\"font-weight\", (d) => (d.depth === 1 ? \"600\" : \"400\"))\n  .attr(\"fill\", (d) => labelInkFor(fillFor(d)))\n  .text((d) => d.data.name);\n\n// --- Center total ------------------------------------------------------\ng.append(\"text\")\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"y\", -8)\n  .style(\"font-size\", \"14px\")\n  .attr(\"fill\", t.inkSoft)\n  .text(\"Total Budget\");\ng.append(\"text\")\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"y\", 22)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"700\")\n  .attr(\"fill\", t.ink)\n  .text(`$${grandTotal.toLocaleString(\"en-US\")}K`);\n\n// --- Legend for the segments too small to carry an inline label ------------\n// Rows are vertically centered on the donut's own center (cy) so the block\n// balances against the ring's full height instead of hugging the top.\nconst legendItems = nodes.filter((d) => d.depth === 2 && !labelVisible(d));\nconst legendX = 850;\nconst rowH = 72;\nconst legendTop = cy - ((legendItems.length - 1) * rowH) / 2;\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", legendTop - 46)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", \"600\")\n  .attr(\"fill\", t.inkSoft)\n  .text(\"Smaller categories\");\n\nconst legend = svg\n  .selectAll(\"g.legend-item\")\n  .data(legendItems)\n  .join(\"g\")\n  .attr(\"class\", \"legend-item\")\n  .attr(\"transform\", (d, i) => `translate(${legendX},${legendTop + i * rowH})`);\n\nlegend.append(\"rect\").attr(\"width\", 20).attr(\"height\", 20).attr(\"rx\", 4).attr(\"fill\", fillFor);\nlegend\n  .append(\"text\")\n  .attr(\"x\", 28)\n  .attr(\"y\", 15)\n  .style(\"font-size\", \"17px\")\n  .attr(\"fill\", t.inkSoft)\n  .text((d) => `${d.parent.data.name} · ${d.data.name} ($${d.data.value}K)`);\n\n// --- Title ---------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"donut-nested · javascript · d3 · anyplot.ai\");\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 90)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(\"Annual budget by department and expense category\");\n"}