{"spec_id":"histogram-stacked","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// histogram-stacked: Stacked Histogram\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1103515245 + 12345) % 2147483648;\n    return state / 2147483648;\n  };\n}\nconst rand = lcg(42);\n\n// Box-Muller transform driven by the LCG above (browser has no seeded RNG).\nfunction normal(mean, stdDev, count) {\n  const samples = [];\n  for (let i = 0; i < count; i++) {\n    const u1 = Math.max(rand(), 1e-9);\n    const u2 = rand();\n    const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n    samples.push(mean + z * stdDev);\n  }\n  return samples;\n}\n\nconst GROUPS = [\n  { label: \"Engineering\", values: normal(13, 4.5, 380) },\n  { label: \"Sales\", values: normal(21, 5, 420) },\n  { label: \"Support\", values: normal(29, 4.5, 360) },\n];\n\n// Shared bin edges across all groups (0-45 km, 3 km wide bins).\nconst BIN_WIDTH = 3;\nconst BIN_COUNT = 15;\nconst binEdges = Array.from({ length: BIN_COUNT + 1 }, (_, i) => i * BIN_WIDTH);\nconst binLabels = Array.from({ length: BIN_COUNT }, (_, i) => `${binEdges[i]}–${binEdges[i + 1]}`);\n\nfunction toCounts(values) {\n  const counts = new Array(BIN_COUNT).fill(0);\n  values.forEach((v) => {\n    const idx = Math.min(Math.max(Math.floor(v / BIN_WIDTH), 0), BIN_COUNT - 1);\n    counts[idx] += 1;\n  });\n  return counts;\n}\n\nconst counts = GROUPS.map((group) => toCounts(group.values));\nconst stackTotals = binLabels.map((_, binIdx) => counts.reduce((sum, c) => sum + c[binIdx], 0));\nconst maxTotal = Math.max(...stackTotals);\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Storytelling annotations (Chart.js inline plugin API) -------------------\n// Calls out the shortest-/longest-commute groups directly on the canvas,\n// anchored to each group's own peak bin, using the plugin's `afterDatasetsDraw`\n// hook rather than a static caption.\nfunction mean(values) {\n  return values.reduce((sum, v) => sum + v, 0) / values.length;\n}\nfunction peakBinIndex(values) {\n  return Math.min(Math.max(Math.round(mean(values) / BIN_WIDTH), 0), BIN_COUNT - 1);\n}\n\nconst insightCallouts = [\n  { datasetIndex: 0, binIndex: peakBinIndex(GROUPS[0].values), text: \"Engineering: shortest commutes\" },\n  { datasetIndex: 2, binIndex: peakBinIndex(GROUPS[2].values), text: \"Support: longest commutes\" },\n];\n\nconst insightCalloutPlugin = {\n  id: \"insightCallouts\",\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea } = chart;\n    ctx.save();\n    ctx.font = \"600 13px system-ui, sans-serif\";\n    ctx.textAlign = \"center\";\n    insightCallouts.forEach(({ datasetIndex, binIndex, text }) => {\n      const bar = chart.getDatasetMeta(datasetIndex).data[binIndex];\n      if (!bar) return;\n      const labelY = chartArea.top + 14;\n      ctx.strokeStyle = t.ink;\n      ctx.fillStyle = t.ink;\n      ctx.lineWidth = 1;\n      ctx.beginPath();\n      ctx.moveTo(bar.x, bar.y);\n      ctx.lineTo(bar.x, labelY + 8);\n      ctx.stroke();\n      ctx.beginPath();\n      ctx.arc(bar.x, bar.y, 2.5, 0, Math.PI * 2);\n      ctx.fill();\n      ctx.fillText(text, bar.x, labelY);\n    });\n    ctx.restore();\n  },\n};\n\n// --- Chart -------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: binLabels,\n    datasets: GROUPS.map((group, i) => ({\n      label: group.label,\n      data: counts[i],\n      backgroundColor: t.palette[i],\n      borderColor: t.ink,\n      borderWidth: 1,\n      barPercentage: 1.0,\n      categoryPercentage: 1.0,\n    })),\n  },\n  plugins: [insightCalloutPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: {\n      padding: { top: 28 },\n    },\n    plugins: {\n      title: {\n        display: true,\n        text: \"histogram-stacked · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: {\n        display: true,\n        position: \"top\",\n        labels: { color: t.ink, font: { size: 16 } },\n      },\n    },\n    scales: {\n      x: {\n        stacked: true,\n        ticks: { color: t.inkSoft, font: { size: 14 }, maxRotation: 0, autoSkip: true, maxTicksLimit: 10 },\n        grid: { display: false },\n        title: { display: true, text: \"Commute Distance (km)\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        stacked: true,\n        beginAtZero: true,\n        suggestedMax: Math.ceil(maxTotal * 1.25),\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Number of Employees\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n});\n"}