{"spec_id":"histogram-overlapping","library":"d3","language":"javascript","code":"// anyplot.ai\n// histogram-overlapping: Overlapping Histograms\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-08-18\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 130, right: 260, bottom: 110, left: 130 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\nfunction makeLcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\n\nfunction randomNormal(rng, mean, sd) {\n  const u1 = Math.max(rng(), 1e-9);\n  const u2 = rng();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * sd;\n}\n\nconst rng = makeLcg(42);\nconst groups = [\n  { label: \"Control group\", mean: 68, sd: 11, n: 260 },\n  { label: \"Treatment group\", mean: 78, sd: 10, n: 260 },\n];\n\nconst records = [];\nfor (const grp of groups) {\n  for (let i = 0; i < grp.n; i++) {\n    const score = Math.min(100, Math.max(0, randomNormal(rng, grp.mean, grp.sd)));\n    records.push({ group: grp.label, value: score });\n  }\n}\n\n// --- Shared bins across both groups, tightened to the actual data range -----\nconst allValues = records.map((d) => d.value);\nconst [dataMin, dataMax] = d3.extent(allValues);\nconst binWidth = 5;\nconst domainMin = Math.max(0, Math.floor(dataMin / binWidth) * binWidth - binWidth);\nconst domainMax = Math.min(100, Math.ceil(dataMax / binWidth) * binWidth);\nconst binCount = Math.round((domainMax - domainMin) / binWidth);\nconst binner = d3\n  .bin()\n  .domain([domainMin, domainMax])\n  .thresholds(d3.range(binCount + 1).map((i) => domainMin + i * binWidth));\n\nconst binsByGroup = groups.map((grp) => ({\n  label: grp.label,\n  bins: binner(records.filter((d) => d.group === grp.label).map((d) => d.value)),\n}));\n\nconst maxCount = d3.max(binsByGroup.flatMap((g) => g.bins.map((b) => b.length)));\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// --- Scales ---------------------------------------------------------------\nconst x = d3.scaleLinear().domain([domainMin, domainMax]).range([0, iw]);\nconst y = d3.scaleLinear().domain([0, maxCount]).nice().range([ih, 0]);\nconst color = d3.scaleOrdinal().domain(groups.map((grp) => grp.label)).range(t.palette);\n\n// --- Gridlines (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// --- Axes -----------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(10));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6));\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.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\nxAxis.selectAll(\".tick line\").remove();\nyAxis.selectAll(\".tick line\").remove();\n\n// --- Overlapping histogram bars --------------------------------------------\nfor (const grp of binsByGroup) {\n  g.append(\"g\")\n    .selectAll(\"rect\")\n    .data(grp.bins)\n    .join(\"rect\")\n    .attr(\"x\", (d) => x(d.x0) + 1)\n    .attr(\"y\", (d) => y(d.length))\n    .attr(\"width\", (d) => Math.max(0, x(d.x1) - x(d.x0) - 2))\n    .attr(\"height\", (d) => ih - y(d.length))\n    .attr(\"fill\", color(grp.label))\n    .attr(\"fill-opacity\", 0.55)\n    .attr(\"stroke\", color(grp.label))\n    .attr(\"stroke-width\", 1.5);\n}\n\n// --- Mean markers (data storytelling: highlights the Treatment-group shift) -\nconst meansByGroup = groups.map((grp) => ({\n  label: grp.label,\n  mean: d3.mean(\n    records.filter((d) => d.group === grp.label),\n    (d) => d.value\n  ),\n}));\nconst meanLine = d3.line();\nmeansByGroup.forEach((gm, i) => {\n  const mx = x(gm.mean);\n  const topY = -(16 + (i % 2) * 24);\n  g.append(\"path\")\n    .attr(\"d\", meanLine([[mx, ih], [mx, topY]]))\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", color(gm.label))\n    .attr(\"stroke-width\", 2)\n    .attr(\"stroke-dasharray\", \"6,4\");\n  g.append(\"text\")\n    .attr(\"x\", mx)\n    .attr(\"y\", topY - 8)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", color(gm.label))\n    .style(\"font-size\", \"13px\")\n    .style(\"font-weight\", \"600\")\n    .text(`${gm.label.replace(\" group\", \"\")} mean: ${gm.mean.toFixed(1)}`);\n});\n\n// --- Axis labels -------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 68)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Test Score\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -92)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Number of Students\");\n\n// --- Legend ------------------------------------------------------------------\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left + iw + 48},${margin.top + 24})`);\n\ngroups.forEach((grp, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(0,${i * 46})`);\n  row\n    .append(\"rect\")\n    .attr(\"width\", 26)\n    .attr(\"height\", 26)\n    .attr(\"fill\", color(grp.label))\n    .attr(\"fill-opacity\", 0.55)\n    .attr(\"stroke\", color(grp.label))\n    .attr(\"stroke-width\", 1.5);\n  row\n    .append(\"text\")\n    .attr(\"x\", 38)\n    .attr(\"y\", 20)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"16px\")\n    .text(grp.label);\n});\n\n// --- Title ------------------------------------------------------------------\nconst title = \"A/B Test Scores · histogram-overlapping · javascript · d3 · anyplot.ai\";\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${Math.round(22 * Math.min(1, 67 / title.length))}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\n"}