{"spec_id":"histogram-density","library":"d3","language":"javascript","code":"// anyplot.ai\n// histogram-density: Density 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: 120, right: 70, bottom: 100, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: reaction times from a psychology experiment (ms), fixed-seed LCG -\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\nfunction randomNormal() {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst MEAN = 320;\nconst STD = 45;\nconst N = 700;\nconst reactionTimes = Array.from({ length: N }, () => MEAN + STD * randomNormal());\n\n// --- Histogram binning, normalized to probability density -------------------\n// Built manually with equal-width bins — d3.bin()'s default threshold\n// heuristic can leave a sliver-width edge bin, which spikes the density of a\n// single stray sample.\nconst [dataMin, dataMax] = d3.extent(reactionTimes);\nconst NUM_BINS = 26;\nconst binWidth = (dataMax - dataMin) / NUM_BINS;\nconst counts = new Array(NUM_BINS).fill(0);\nfor (const v of reactionTimes) {\n  const idx = Math.min(NUM_BINS - 1, Math.floor((v - dataMin) / binWidth));\n  counts[idx] += 1;\n}\nconst density = counts.map((count, i) => ({\n  x0: dataMin + i * binWidth,\n  x1: dataMin + (i + 1) * binWidth,\n  y: count / (N * binWidth),\n}));\n\n// --- Theoretical normal PDF, fit from the same mean/std ---------------------\nconst pdf = (x) => Math.exp(-0.5 * ((x - MEAN) / STD) ** 2) / (STD * Math.sqrt(2 * Math.PI));\nconst pdfPoints = d3.range(dataMin, dataMax, (dataMax - dataMin) / 200).map((x) => ({ x, y: pdf(x) }));\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([dataMin, dataMax]).nice().range([0, iw]);\nconst yMax = Math.max(d3.max(density, (d) => d.y), d3.max(pdfPoints, (d) => d.y));\nconst y = d3.scaleLinear().domain([0, yMax * 1.15]).nice().range([ih, 0]);\n\n// --- Y-axis grid (bar chart convention: horizontal only) ---------------------\ng.append(\"g\")\n  .call(d3.axisLeft(y).ticks(6).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Bars: observed density ---------------------------------------------\ng.selectAll(\"rect\")\n  .data(density)\n  .join(\"rect\")\n  .attr(\"x\", (d) => x(d.x0) + 1)\n  .attr(\"y\", (d) => y(d.y))\n  .attr(\"width\", (d) => Math.max(0, x(d.x1) - x(d.x0) - 1))\n  .attr(\"height\", (d) => ih - y(d.y))\n  .attr(\"fill\", t.palette[0])\n  .attr(\"opacity\", 0.9);\n\n// --- Theoretical PDF overlay ----------------------------------------------\n// Subtle area fill under the curve reinforces the empirical-vs-theoretical\n// comparison at a glance, beyond the line alone.\nconst area = d3.area().x((d) => x(d.x)).y0(ih).y1((d) => y(d.y)).curve(d3.curveNatural);\ng.append(\"path\").datum(pdfPoints).attr(\"fill\", t.palette[1]).attr(\"opacity\", 0.12).attr(\"d\", area);\n\nconst line = d3.line().x((d) => x(d.x)).y((d) => y(d.y)).curve(d3.curveNatural);\ng.append(\"path\").datum(pdfPoints).attr(\"fill\", \"none\").attr(\"stroke\", t.palette[1]).attr(\"stroke-width\", 3.5).attr(\"d\", line);\n\n// --- Axes -----------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x).ticks(8).tickSize(0));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6).tickSize(0));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\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 + 70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"19px\")\n  .text(\"Reaction Time (ms)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -78)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"19px\")\n  .text(\"Density\");\n\n// --- Legend -----------------------------------------------------------------\nconst legend = g.append(\"g\").attr(\"transform\", `translate(${iw - 250}, 6)`);\nlegend.append(\"rect\").attr(\"width\", 20).attr(\"height\", 20).attr(\"fill\", t.palette[0]).attr(\"opacity\", 0.9);\nlegend.append(\"text\").attr(\"x\", 30).attr(\"y\", 15).attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\").text(\"Observed density\");\nlegend.append(\"line\").attr(\"x1\", 0).attr(\"x2\", 20).attr(\"y1\", 44).attr(\"y2\", 44).attr(\"stroke\", t.palette[1]).attr(\"stroke-width\", 3.5);\nlegend.append(\"text\").attr(\"x\", 30).attr(\"y\", 49).attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\").text(\"Normal PDF fit\");\n\n// --- Title --------------------------------------------------------------\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\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"histogram-density · javascript · d3 · anyplot.ai\");\n"}