{"spec_id":"bar-stacked-percent","library":"d3","language":"javascript","code":"// anyplot.ai\n// bar-stacked-percent: 100% Stacked Bar Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-18\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst muted = t.theme === \"light\" ? \"#6B6A63\" : \"#A8A79F\";\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Product-survey satisfaction breakdown per question, raw response counts.\nconst components = [\n  \"Very Satisfied\",\n  \"Satisfied\",\n  \"Neutral\",\n  \"Dissatisfied\",\n  \"Very Dissatisfied\",\n];\nconst data = [\n  {\n    category: \"Product Quality\",\n    \"Very Satisfied\": 38,\n    Satisfied: 34,\n    Neutral: 14,\n    Dissatisfied: 9,\n    \"Very Dissatisfied\": 5,\n  },\n  {\n    category: \"Customer Support\",\n    \"Very Satisfied\": 22,\n    Satisfied: 31,\n    Neutral: 21,\n    Dissatisfied: 16,\n    \"Very Dissatisfied\": 10,\n  },\n  {\n    category: \"Pricing\",\n    \"Very Satisfied\": 14,\n    Satisfied: 25,\n    Neutral: 26,\n    Dissatisfied: 22,\n    \"Very Dissatisfied\": 13,\n  },\n  {\n    category: \"Ease of Use\",\n    \"Very Satisfied\": 41,\n    Satisfied: 33,\n    Neutral: 15,\n    Dissatisfied: 7,\n    \"Very Dissatisfied\": 4,\n  },\n  {\n    category: \"Delivery Speed\",\n    \"Very Satisfied\": 29,\n    Satisfied: 30,\n    Neutral: 20,\n    Dissatisfied: 13,\n    \"Very Dissatisfied\": 8,\n  },\n];\n\n// Sentiment colors: positive -> green family, neutral -> muted, negative -> amber/red.\nconst color = d3\n  .scaleOrdinal()\n  .domain(components)\n  .range([t.palette[0], t.palette[7], muted, t.amber, t.palette[4]]);\n\n// Text color per segment stays legible against its own fill in both themes.\nconst labelColor = {\n  \"Very Satisfied\": \"#FFFFFF\",\n  Satisfied: \"#1A1A17\",\n  Neutral: t.theme === \"light\" ? \"#FFFDF6\" : \"#1A1A17\",\n  Dissatisfied: \"#1A1A17\",\n  \"Very Dissatisfied\": \"#FFFFFF\",\n};\n\nconst stacked = d3.stack().keys(components).offset(d3.stackOffsetExpand)(data);\n\n// --- Layout -------------------------------------------------------------------\nconst margin = { top: 150, right: 40, bottom: 70, left: 70 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\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.category))\n  .range([0, iw])\n  .padding(0.35);\nconst y = d3.scaleLinear().domain([0, 1]).range([ih, 0]);\n\n// --- Gridlines (y-axis only) ------------------------------------------------\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(4))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Stacked bars ------------------------------------------------------------\nconst layers = g\n  .selectAll(\".layer\")\n  .data(stacked)\n  .join(\"g\")\n  .attr(\"fill\", (d) => color(d.key));\n\nlayers\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\n// --- Percentage labels (only where a segment has room) ----------------------\nlayers.each(function (series) {\n  d3.select(this)\n    .selectAll(\"text\")\n    .data(series.filter((d) => d[1] - d[0] >= 0.06))\n    .join(\"text\")\n    .attr(\"x\", (d) => x(d.data.category) + x.bandwidth() / 2)\n    .attr(\"y\", (d) => (y(d[0]) + y(d[1])) / 2)\n    .attr(\"dy\", \"0.35em\")\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", labelColor[series.key])\n    .style(\"font-size\", \"14px\")\n    .style(\"font-weight\", \"600\")\n    .text((d) => `${Math.round((d[1] - d[0]) * 100)}%`);\n});\n\n// --- Axes ---------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x));\nconst yAxis = g\n  .append(\"g\")\n  .call(d3.axisLeft(y).ticks(4).tickFormat(d3.format(\".0%\")));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\ng.selectAll(\".tick line\").remove();\n\n// --- Legend ---------------------------------------------------------------\nconst legend = svg.append(\"g\").attr(\"text-anchor\", \"start\");\nconst legendItemWidths = components.map((k) => 22 + k.length * 8.2 + 28);\nconst legendTotalWidth = legendItemWidths.reduce((a, b) => a + b, 0);\nlet legendX = (width - legendTotalWidth) / 2;\ncomponents.forEach((key, i) => {\n  const item = legend.append(\"g\").attr(\"transform\", `translate(${legendX},96)`);\n  item\n    .append(\"rect\")\n    .attr(\"width\", 18)\n    .attr(\"height\", 18)\n    .attr(\"rx\", 3)\n    .attr(\"fill\", color(key));\n  item\n    .append(\"text\")\n    .attr(\"x\", 26)\n    .attr(\"y\", 14)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"15px\")\n    .text(key);\n  legendX += legendItemWidths[i];\n});\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\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"bar-stacked-percent · javascript · d3 · anyplot.ai\");\n"}