{"spec_id":"pie-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// pie-basic: Basic Pie Chart\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 87/100 | Created: 2026-06-02\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data — Streaming subscriber share (illustrative, 2024) ------------------\nconst data = [\n  { category: \"Netflix\", value: 30 },\n  { category: \"Disney+\", value: 22 },\n  { category: \"Amazon Prime\", value: 18 },\n  { category: \"HBO Max\", value: 16 },\n  { category: \"Hulu\", value: 14 },\n];\nconst total = d3.sum(data, (d) => d.value);\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\n\n// --- Layout — title on top, pie centered-left, legend on the right -----------\nconst titleY = 70;\nconst legendW = 280;\nconst pieAreaX0 = 40;\nconst pieAreaY0 = titleY + 60;\nconst pieAreaX1 = width - legendW;\nconst pieAreaY1 = height - 60;\nconst radius =\n  (Math.min(pieAreaX1 - pieAreaX0, pieAreaY1 - pieAreaY0) / 2) * 0.82;\nconst cx = (pieAreaX0 + pieAreaX1) / 2;\nconst cy = (pieAreaY0 + pieAreaY1) / 2;\n\n// --- Colors — Imprint palette positions 1..N in canonical order --------------\nconst color = d3\n  .scaleOrdinal()\n  .domain(data.map((d) => d.category))\n  .range(t.palette);\n\n// --- Pie + arc generators ----------------------------------------------------\nconst pieGen = d3\n  .pie()\n  .value((d) => d.value)\n  .sort(null);\nconst arcs = pieGen(data);\nconst arc = d3.arc().innerRadius(0).outerRadius(radius);\nconst labelArc = d3\n  .arc()\n  .innerRadius(radius * 0.62)\n  .outerRadius(radius * 0.62);\n\n// Slightly explode the largest slice for emphasis (per spec)\nconst maxIdx = d3.maxIndex(data, (d) => d.value);\nconst explodeR = 36;\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${cx},${cy})`);\n\n// --- Slices ------------------------------------------------------------------\ng.selectAll(\"path.slice\")\n  .data(arcs)\n  .join(\"path\")\n  .attr(\"class\", \"slice\")\n  .attr(\"transform\", (a) => {\n    if (a.index !== maxIdx) return \"translate(0,0)\";\n    const mid = (a.startAngle + a.endAngle) / 2 - Math.PI / 2;\n    return `translate(${Math.cos(mid) * explodeR},${Math.sin(mid) * explodeR})`;\n  })\n  .attr(\"d\", arc)\n  .attr(\"fill\", (a) => color(a.data.category))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 3);\n\n// --- Percentage labels inside slices -----------------------------------------\n// Leader slice (Netflix 30%) gets a larger/heavier label so the hierarchy\n// reinforces the explosion focal point.\ng.selectAll(\"text.pct\")\n  .data(arcs)\n  .join(\"text\")\n  .attr(\"class\", \"pct\")\n  .attr(\"transform\", (a) => {\n    const [lx, ly] = labelArc.centroid(a);\n    if (a.index !== maxIdx) return `translate(${lx},${ly})`;\n    const mid = (a.startAngle + a.endAngle) / 2 - Math.PI / 2;\n    return `translate(${lx + Math.cos(mid) * explodeR},${ly + Math.sin(mid) * explodeR})`;\n  })\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"central\")\n  // Always-light label on saturated slices: dark theme's near-black `pageBg`\n  // tanks contrast on the matte-red and blue slots. The Imprint cream reads\n  // cleanly against every palette member in either theme.\n  .attr(\"fill\", \"#FAF8F1\")\n  .style(\"font-size\", (a) => (a.index === maxIdx ? \"44px\" : \"30px\"))\n  .style(\"font-weight\", (a) => (a.index === maxIdx ? \"700\" : \"600\"))\n  .text((a) => `${Math.round((a.data.value / total) * 100)}%`);\n\n// --- Title -------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", titleY)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"34px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"pie-basic · javascript · d3 · anyplot.ai\");\n\n// --- Legend ------------------------------------------------------------------\nconst legendX = width - legendW + 20;\nconst rowH = 56;\nconst legendH = data.length * rowH;\nconst legendY0 = (height - legendH) / 2;\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${legendX},${legendY0})`);\nconst item = legend\n  .selectAll(\"g.item\")\n  .data(data)\n  .join(\"g\")\n  .attr(\"class\", \"item\")\n  .attr(\"transform\", (_d, i) => `translate(0,${i * rowH})`);\nitem\n  .append(\"rect\")\n  .attr(\"width\", 28)\n  .attr(\"height\", 28)\n  .attr(\"rx\", 6)\n  .attr(\"fill\", (d) => color(d.category));\nitem\n  .append(\"text\")\n  .attr(\"x\", 46)\n  .attr(\"y\", 14)\n  .attr(\"dominant-baseline\", \"central\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"24px\")\n  .text((d) => d.category);\n"}