{"spec_id":"histogram-stacked","library":"d3","language":"javascript","code":"// anyplot.ai\n// histogram-stacked: Stacked Histogram\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 100, right: 60, bottom: 90, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Delivery time (days) for three shipping tiers, binned and stacked so the\n// total bar height shows combined order volume with a per-tier breakdown.\nfunction makeNormalSampler(seed) {\n  let state = seed >>> 0;\n  const rng = () => {\n    state = (Math.imul(1103515245, state) + 12345) >>> 0;\n    return state / 4294967296;\n  };\n  return () => {\n    let u = 0;\n    let v = 0;\n    while (u === 0) u = rng();\n    while (v === 0) v = rng();\n    return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);\n  };\n}\nconst randNormal = makeNormalSampler(20260905);\n\nconst tiers = [\n  { key: \"Standard\", n: 600, mean: 5.4, std: 1.6 },\n  { key: \"Express\", n: 420, mean: 3.0, std: 1.0 },\n  { key: \"Same-Day\", n: 260, mean: 1.1, std: 0.4 },\n];\nconst groups = tiers.map((tier) => tier.key);\nconst valuesByGroup = {};\nfor (const tier of tiers) {\n  valuesByGroup[tier.key] = Array.from({ length: tier.n }, () =>\n    Math.max(0.1, tier.mean + tier.std * randNormal()),\n  );\n}\nconst allValues = groups.flatMap((g) => valuesByGroup[g]);\nconst maxValue = d3.max(allValues);\n\n// --- Binning (shared edges across groups via a fixed domain + threshold count)\nconst bin = d3.bin().domain([0, maxValue]).thresholds(18);\nconst binsByGroup = {};\nfor (const g of groups) binsByGroup[g] = bin(valuesByGroup[g]);\n\nconst binData = binsByGroup[groups[0]].map((b, i) => {\n  const row = { x0: b.x0, x1: b.x1 };\n  for (const g of groups) row[g] = binsByGroup[g][i].length;\n  return row;\n});\nconst series = d3.stack().keys(groups)(binData);\nconst maxStack = d3.max(binData, (d) => groups.reduce((sum, g) => sum + d[g], 0));\n\n// Composition-shift insight: the bin where \"Standard\" first overtakes the\n// other tiers as the largest segment, i.e. where the stack flips from\n// same-day-dominated to standard-dominated.\nconst dominantByBin = binData.map((row) => groups.reduce((best, g) => (row[g] > row[best] ? g : best), groups[0]));\nconst crossoverIdx = dominantByBin.findIndex((g, i) => g === \"Standard\" && dominantByBin[i - 1] !== \"Standard\");\n\n// --- Scales -------------------------------------------------------------------\nconst x = d3.scaleLinear().domain([0, maxValue]).nice().range([0, iw]);\nconst y = d3.scaleLinear().domain([0, maxStack]).nice().range([ih, 0]);\nconst color = d3.scaleOrdinal().domain(groups).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// --- Gridlines (y-axis only, subtle) ------------------------------------------\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// --- Bars ----------------------------------------------------------------\nconst barGap = 3;\nconst seriesGroups = g\n  .selectAll(\".series\")\n  .data(series)\n  .join(\"g\")\n  .attr(\"class\", \"series\")\n  .attr(\"fill\", (d) => color(d.key));\nseriesGroups\n  .selectAll(\"rect\")\n  .data((d) => d)\n  .join(\"rect\")\n  .attr(\"x\", (d) => x(d.data.x0) + barGap / 2)\n  .attr(\"width\", (d) => Math.max(0, x(d.data.x1) - x(d.data.x0) - barGap))\n  .attr(\"y\", (d) => y(d[1]))\n  .attr(\"height\", (d) => y(d[0]) - y(d[1]))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1);\n\n// --- Composition-shift callout (visual-hierarchy emphasis) -----------------\nif (crossoverIdx !== -1) {\n  const cx = x(binData[crossoverIdx].x0);\n  const labelOnLeft = cx > iw * 0.6;\n  g.append(\"line\")\n    .attr(\"x1\", cx)\n    .attr(\"x2\", cx)\n    .attr(\"y1\", 0)\n    .attr(\"y2\", ih)\n    .attr(\"stroke\", t.ink)\n    .attr(\"stroke-width\", 2)\n    .attr(\"stroke-dasharray\", \"6,4\")\n    .attr(\"opacity\", 0.55);\n  g.append(\"text\")\n    .attr(\"x\", cx + (labelOnLeft ? -8 : 8))\n    .attr(\"y\", 16)\n    .attr(\"text-anchor\", labelOnLeft ? \"end\" : \"start\")\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"14px\")\n    .style(\"font-weight\", \"600\")\n    .style(\"font-style\", \"italic\")\n    .text(labelOnLeft ? \"← Standard becomes dominant\" : \"Standard becomes dominant →\");\n}\n\n// --- Axes ----------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x).ticks(9));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels -----------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Delivery Time (days)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Number of Orders\");\n\n// --- Legend (top-right, ordered to match stack) -----------------------------\n// Hovering a legend row highlights its segment across every bin (leveraging\n// the interactive HTML export, not just the static screenshot).\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${width - margin.right - 170},${margin.top - 50})`);\ngroups.forEach((name, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(0,${i * 26})`).style(\"cursor\", \"pointer\");\n  row\n    .append(\"rect\")\n    .attr(\"width\", 16)\n    .attr(\"height\", 16)\n    .attr(\"rx\", 3)\n    .attr(\"ry\", 3)\n    .attr(\"fill\", color(name))\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 1);\n  row\n    .append(\"text\")\n    .attr(\"x\", 24)\n    .attr(\"y\", 13)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(name);\n  row\n    .on(\"mouseenter\", () => seriesGroups.attr(\"opacity\", (d) => (d.key === name ? 1 : 0.25)))\n    .on(\"mouseleave\", () => seriesGroups.attr(\"opacity\", 1));\n});\n\n// --- Title -------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"histogram-stacked · javascript · d3 · anyplot.ai\");\n"}