{"spec_id":"bar-stacked","library":"d3","language":"javascript","code":"// anyplot.ai\n// bar-stacked: Stacked Bar Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 150, right: 50, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Quarterly software-company revenue ($M) by product line, stacked largest-first.\nconst components = [\"Software\", \"Hardware\", \"Services\", \"Support\"];\nconst data = [\n  { category: \"2023 Q1\", Software: 42, Hardware: 28, Services: 18, Support: 12 },\n  { category: \"2023 Q2\", Software: 45, Hardware: 26, Services: 20, Support: 13 },\n  { category: \"2023 Q3\", Software: 48, Hardware: 30, Services: 19, Support: 14 },\n  { category: \"2023 Q4\", Software: 55, Hardware: 33, Services: 22, Support: 15 },\n  { category: \"2024 Q1\", Software: 51, Hardware: 29, Services: 21, Support: 16 },\n  { category: \"2024 Q2\", Software: 58, Hardware: 31, Services: 24, Support: 17 },\n];\nconst totals = data.map((d) => components.reduce((sum, k) => sum + d[k], 0));\nconst stackedSeries = d3.stack().keys(components)(data);\n\n// --- Scales -------------------------------------------------------------------\nconst x = d3.scaleBand().domain(data.map((d) => d.category)).range([0, iw]).padding(0.32);\nconst y = d3.scaleLinear().domain([0, d3.max(totals) * 1.12]).nice().range([ih, 0]);\nconst color = d3.scaleOrdinal().domain(components).range(t.palette);\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(${margin.left},${margin.top})`);\n\n// --- Y gridlines (behind bars) -----------------------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .call(d3.axisLeft(y).ticks(6).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .call((sel) => sel.selectAll(\"line\").attr(\"stroke\", t.grid));\n\n// --- Stacked bars ---------------------------------------------------------\ng.selectAll(\"g.series\")\n  .data(stackedSeries)\n  .join(\"g\")\n  .attr(\"class\", \"series\")\n  .attr(\"fill\", (d) => color(d.key))\n  .selectAll(\"rect\")\n  .data((d) => d)\n  .join(\"rect\")\n  .attr(\"x\", (d) => x(d.data.category))\n  .attr(\"y\", (d) => y(d[1]))\n  .attr(\"width\", x.bandwidth())\n  .attr(\"height\", (d) => y(d[0]) - y(d[1]))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\n// --- Total labels above each stack -----------------------------------------\ng.selectAll(\"text.total\")\n  .data(data)\n  .join(\"text\")\n  .attr(\"class\", \"total\")\n  .attr(\"x\", (d) => x(d.category) + x.bandwidth() / 2)\n  .attr(\"y\", (d, i) => y(totals[i]) - 14)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .style(\"font-weight\", \"600\")\n  .text((d, i) => `$${totals[i]}M`);\n\n// --- Axes -----------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x).tickSizeOuter(0));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6).tickFormat((d) => `$${d}M`));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\nxAxis.selectAll(\"line\").remove();\n\n// --- Axis titles ------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Quarter\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -80)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Revenue ($M)\");\n\n// --- Legend (measured + centered, horizontal) --------------------------------\nconst legend = svg.append(\"g\");\nconst legendItems = components.map((name) => {\n  const item = legend.append(\"g\");\n  item.append(\"rect\").attr(\"width\", 22).attr(\"height\", 22).attr(\"rx\", 3).attr(\"fill\", color(name));\n  item.append(\"text\").attr(\"x\", 30).attr(\"y\", 17).attr(\"fill\", t.inkSoft).style(\"font-size\", \"17px\").text(name);\n  return item;\n});\nconst gap = 34;\nconst itemWidths = legendItems.map((item) => item.node().getBBox().width);\nconst legendTotalWidth = itemWidths.reduce((a, b) => a + b, 0) + gap * (itemWidths.length - 1);\nlet legendX = (width - legendTotalWidth) / 2;\nlegendItems.forEach((item, i) => {\n  item.attr(\"transform\", `translate(${legendX},96)`);\n  legendX += itemWidths[i] + gap;\n});\n\n// --- Title --------------------------------------------------------------\nconst titleText = \"Quarterly Revenue by Product Line · bar-stacked · javascript · d3 · anyplot.ai\";\nconst titleFontSize = Math.max(16, Math.round(22 * Math.min(1, 67 / titleText.length)));\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleFontSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(titleText);\n"}