{"spec_id":"bar-stacked-labeled","library":"d3","language":"javascript","code":"// anyplot.ai\n// bar-stacked-labeled: Stacked Bar Chart with Total Labels\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 150, right: 50, bottom: 90, left: 120 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: quarterly revenue by product line, in $M ------------------------\nconst components = [\"Hardware\", \"Software\", \"Services\"];\nconst data = [\n  { quarter: \"Q1\", Hardware: 42, Software: 28, Services: 15 },\n  { quarter: \"Q2\", Hardware: 38, Software: 34, Services: 19 },\n  { quarter: \"Q3\", Hardware: 45, Software: 39, Services: 22 },\n  { quarter: \"Q4\", Hardware: 51, Software: 46, Services: 27 },\n];\nconst totals = data.map((d) =>\n  components.reduce((sum, key) => sum + d[key], 0),\n);\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\nconst g = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales -------------------------------------------------------------------\nconst x = d3\n  .scaleBand()\n  .domain(data.map((d) => d.quarter))\n  .range([0, iw])\n  .padding(0.35);\nconst y = d3\n  .scaleLinear()\n  .domain([0, d3.max(totals) * 1.15])\n  .nice()\n  .range([ih, 0]);\nconst color = d3\n  .scaleOrdinal()\n  .domain(components)\n  .range(t.palette.slice(0, components.length));\n\n// --- Y grid (subtle, y-axis only) --------------------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Stacked bars ---------------------------------------------------------\nconst series = d3.stack().keys(components)(data);\n\nconst segments = g\n  .append(\"g\")\n  .selectAll(\"g\")\n  .data(series)\n  .join(\"g\")\n  .attr(\"fill\", (d) => color(d.key));\n\nsegments\n  .selectAll(\"rect\")\n  .data((d) => d)\n  .join(\"rect\")\n  .attr(\"x\", (d) => x(d.data.quarter))\n  .attr(\"y\", (d) => y(d[1]))\n  .attr(\"width\", x.bandwidth())\n  .attr(\"height\", (d) => y(d[0]) - y(d[1]));\n\n// --- In-segment value labels (distinct from the total labels above) ----------\nconst MIN_LABEL_SEGMENT_HEIGHT = 46;\nsegments\n  .selectAll(\".segment-label\")\n  .data((d) => d.filter((v) => y(v[0]) - y(v[1]) >= MIN_LABEL_SEGMENT_HEIGHT))\n  .join(\"text\")\n  .attr(\"class\", \"segment-label\")\n  .attr(\"x\", (d) => x(d.data.quarter) + x.bandwidth() / 2)\n  .attr(\"y\", (d) => (y(d[0]) + y(d[1])) / 2)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"central\")\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .style(\"paint-order\", \"stroke\")\n  .attr(\"stroke\", \"rgba(0,0,0,0.35)\")\n  .attr(\"stroke-width\", 3)\n  .attr(\"fill\", \"#FFFFFF\")\n  .text((d) => `$${d[1] - d[0]}M`);\n\n// --- Total labels above each stack -------------------------------------------\ng.selectAll(\".total-label\")\n  .data(data)\n  .join(\"text\")\n  .attr(\"class\", \"total-label\")\n  .attr(\"x\", (d) => x(d.quarter) + x.bandwidth() / 2)\n  .attr(\"y\", (d, i) => y(totals[i]) - 16)\n  .attr(\"text-anchor\", \"middle\")\n  .style(\"font-size\", \"20px\")\n  .style(\"font-weight\", \"700\")\n  .attr(\"fill\", t.ink)\n  .text((d, i) => `$${totals[i]}M`);\n\n// --- Axes ---------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x));\nconst yAxis = g.append(\"g\").call(\n  d3\n    .axisLeft(y)\n    .ticks(5)\n    .tickFormat((d) => `$${d}M`),\n);\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  axis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  axis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\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\", \"16px\")\n  .text(\"Quarter\");\n\ng.append(\"text\")\n  .attr(\"transform\", `translate(${-88},${ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Revenue ($M)\");\n\n// --- Legend --------------------------------------------------------------\nconst legendItemWidth = 170;\nconst legend = svg\n  .append(\"g\")\n  .attr(\n    \"transform\",\n    `translate(${width - margin.right - components.length * legendItemWidth}, 78)`,\n  );\n\nconst legendItem = legend\n  .selectAll(\"g\")\n  .data(components)\n  .join(\"g\")\n  .attr(\"transform\", (d, i) => `translate(${i * legendItemWidth},0)`);\n\nlegendItem\n  .append(\"rect\")\n  .attr(\"width\", 20)\n  .attr(\"height\", 20)\n  .attr(\"y\", -15)\n  .attr(\"fill\", (d) => color(d));\n\nlegendItem\n  .append(\"text\")\n  .attr(\"x\", 28)\n  .attr(\"y\", 0)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text((d) => d);\n\n// --- Title ---------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"bar-stacked-labeled · javascript · d3 · anyplot.ai\");\n"}