{"spec_id":"histogram-overlapping","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// histogram-overlapping: Overlapping Histograms\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-08-18\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Small LCG PRNG + Box-Muller transform — the browser has no seeded RNG.\nlet seed = 42;\nfunction lcgRandom() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\nfunction randomNormal(mean, stdDev) {\n  const u1 = 1 - lcgRandom();\n  const u2 = lcgRandom();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * stdDev;\n}\n\nconst groups = [\n  { name: \"Morning Section\", mean: 72, stdDev: 13, n: 300 },\n  { name: \"Afternoon Section\", mean: 80, stdDev: 6, n: 300 },\n];\ngroups.forEach((group) => {\n  group.scores = Array.from({ length: group.n }, () =>\n    Math.min(100, Math.max(0, randomNormal(group.mean, group.stdDev))),\n  );\n});\n\n// --- Shared bins across both groups (aligned edges for a fair comparison) --\nconst allScores = groups.flatMap((group) => group.scores);\nconst binWidth = 5;\nconst minEdge = Math.floor(Math.min(...allScores) / binWidth) * binWidth;\nconst maxEdge = Math.ceil(Math.max(...allScores) / binWidth) * binWidth;\nconst binCount = (maxEdge - minEdge) / binWidth;\nconst binLabels = Array.from(\n  { length: binCount },\n  (_, i) => `${minEdge + i * binWidth}`,\n);\n\ngroups.forEach((group) => {\n  const counts = new Array(binCount).fill(0);\n  group.scores.forEach((score) => {\n    const idx = Math.min(\n      binCount - 1,\n      Math.max(0, Math.floor((score - minEdge) / binWidth)),\n    );\n    counts[idx] += 1;\n  });\n  group.counts = counts;\n});\n\nfunction withAlpha(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\n// --- Mean-marker plugin --------------------------------------------------\n// Makes the score-gap insight explicit: a dashed vertical line + label at\n// each group's mean, positioned via the shared bin grid (fractional\n// category index) so it lines up with the actual score, not the bin edge.\nconst meanMarkerPlugin = {\n  id: \"meanMarkers\",\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    const xScale = scales.x;\n    ctx.save();\n    groups.forEach((group, i) => {\n      const fracIndex = (group.mean - minEdge) / binWidth - 0.5;\n      const xPixel = xScale.getPixelForValue(fracIndex);\n\n      ctx.strokeStyle = t.palette[i];\n      ctx.lineWidth = 2;\n      ctx.setLineDash([6, 4]);\n      ctx.beginPath();\n      ctx.moveTo(xPixel, chartArea.top);\n      ctx.lineTo(xPixel, chartArea.bottom);\n      ctx.stroke();\n\n      ctx.setLineDash([]);\n      ctx.fillStyle = t.palette[i];\n      ctx.font = \"600 13px sans-serif\";\n      ctx.textAlign = i === 0 ? \"right\" : \"left\";\n      ctx.fillText(\n        `mean ${group.mean.toFixed(0)}`,\n        xPixel + (i === 0 ? -6 : 6),\n        chartArea.top + 16,\n      );\n    });\n    ctx.restore();\n  },\n};\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: binLabels,\n    datasets: groups.map((group, i) => ({\n      label: group.name,\n      data: group.counts,\n      backgroundColor: withAlpha(t.palette[i], 0.55),\n      borderColor: t.palette[i],\n      borderWidth: 1.5,\n      barPercentage: 1.0,\n      categoryPercentage: 1.0,\n      grouped: false,\n    })),\n  },\n  plugins: [meanMarkerPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: {\n      padding: { right: 24, top: 8 },\n    },\n    plugins: {\n      title: {\n        display: true,\n        text: \"histogram-overlapping · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { bottom: 20 },\n      },\n      legend: {\n        labels: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          boxWidth: 14,\n          usePointStyle: true,\n          pointStyle: \"rectRounded\",\n        },\n      },\n    },\n    scales: {\n      x: {\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          maxRotation: 0,\n          autoSkipPadding: 12,\n        },\n        grid: { display: false },\n        title: {\n          display: true,\n          text: \"Exam Score\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n      y: {\n        beginAtZero: true,\n        ticks: { color: t.inkSoft, font: { size: 13 } },\n        grid: { color: t.grid },\n        title: {\n          display: true,\n          text: \"Number of Students\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n    },\n  },\n});\n"}