{"spec_id":"histogram-2d","library":"d3","language":"javascript","code":"// anyplot.ai\n// histogram-2d: 2D Histogram Heatmap\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 86/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic bivariate normal via Box-Muller) -------\nfunction mulberry32(seed) {\n  return function () {\n    seed |= 0;\n    seed = (seed + 0x6d2b79f5) | 0;\n    let z = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n    z = (z + Math.imul(z ^ (z >>> 7), 61 | z)) ^ z;\n    return ((z ^ (z >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nconst rand = mulberry32(42);\nconst N_POINTS = 9000;\nconst CORRELATION = 0.65;\nconst MEAN_TECH = 0.05;\nconst STD_TECH = 1.8;\nconst MEAN_ENERGY = 0.03;\nconst STD_ENERGY = 2.2;\n\nconst points = [];\nfor (let i = 0; i < N_POINTS; i++) {\n  const u1 = Math.max(rand(), 1e-12);\n  const u2 = rand();\n  const mag = Math.sqrt(-2 * Math.log(u1));\n  const z0 = mag * Math.cos(2 * Math.PI * u2);\n  const z1 = mag * Math.sin(2 * Math.PI * u2);\n  const techReturn = MEAN_TECH + STD_TECH * z0;\n  const energyReturn = MEAN_ENERGY + STD_ENERGY * (CORRELATION * z0 + Math.sqrt(1 - CORRELATION * CORRELATION) * z1);\n  points.push({ x: techReturn, y: energyReturn });\n}\n\n// --- Binning (manual 2D grid — d3-array's bin() only handles 1D) -----------\nconst N_BINS = 26;\nconst [xMin, xMax] = d3.scaleLinear().domain(d3.extent(points, (d) => d.x)).nice().domain();\nconst [yMin, yMax] = d3.scaleLinear().domain(d3.extent(points, (d) => d.y)).nice().domain();\nconst xStep = (xMax - xMin) / N_BINS;\nconst yStep = (yMax - yMin) / N_BINS;\n\nconst counts = Array.from({ length: N_BINS }, () => new Array(N_BINS).fill(0));\nfor (const d of points) {\n  const bx = Math.min(N_BINS - 1, Math.floor((d.x - xMin) / xStep));\n  const by = Math.min(N_BINS - 1, Math.floor((d.y - yMin) / yStep));\n  counts[bx][by] += 1;\n}\n\nconst cells = [];\nlet maxCount = 0;\nfor (let bx = 0; bx < N_BINS; bx++) {\n  for (let by = 0; by < N_BINS; by++) {\n    const count = counts[bx][by];\n    if (count > maxCount) maxCount = count;\n    if (count > 0) cells.push({ bx, by, count });\n  }\n}\n\nconst xMarginal = counts.map((col) => d3.sum(col));\nconst yMarginal = d3.range(N_BINS).map((by) => d3.sum(counts.map((col) => col[by])));\n\n// --- Layout (heatmap + top/right marginal histograms + colorbar) -----------\nconst gap = 8;\nconst mainX0 = 90;\nconst mainX1 = 1376;\nconst mainY0 = 168;\nconst mainY1 = 830;\nconst topMarginY0 = 90;\nconst topMarginY1 = mainY0 - gap;\nconst rightMarginX0 = mainX1 + gap;\nconst rightMarginX1 = rightMarginX0 + 70;\nconst colorbarX0 = rightMarginX1 + 30;\nconst colorbarX1 = colorbarX0 + 26;\nconst binWidthPx = (mainX1 - mainX0) / N_BINS;\nconst binHeightPx = (mainY1 - mainY0) / N_BINS;\n\nconst xScale = d3.scaleLinear().domain([xMin, xMax]).range([mainX0, mainX1]);\nconst yScale = d3.scaleLinear().domain([yMin, yMax]).range([mainY1, mainY0]);\nconst xMarginalScale = d3.scaleLinear().domain([0, d3.max(xMarginal)]).nice().range([topMarginY1, topMarginY0]);\nconst yMarginalScale = d3.scaleLinear().domain([0, d3.max(yMarginal)]).nice().range([rightMarginX0, rightMarginX1]);\n\n// sqrt-compressed density scale: tames the long right tail of a point-count\n// histogram without hitting the log(0) singularity a true log scale would on\n// sparse bins.\nconst colorScale = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain([0, Math.sqrt(maxCount)]);\nconst colorbarScale = d3.scaleLinear().domain([0, maxCount]).range([mainY1, mainY0]);\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// --- Heatmap cells ------------------------------------------------------------\nsvg\n  .selectAll(\"rect.cell\")\n  .data(cells)\n  .join(\"rect\")\n  .attr(\"class\", \"cell\")\n  .attr(\"x\", (d) => mainX0 + d.bx * binWidthPx)\n  .attr(\"y\", (d) => mainY1 - (d.by + 1) * binHeightPx)\n  .attr(\"width\", binWidthPx + 0.5)\n  .attr(\"height\", binHeightPx + 0.5)\n  .attr(\"fill\", (d) => colorScale(Math.sqrt(d.count)));\n\n// --- Marginal histograms (univariate context, per spec's optional note) ----\nsvg\n  .selectAll(\"rect.marginal-x\")\n  .data(xMarginal)\n  .join(\"rect\")\n  .attr(\"class\", \"marginal-x\")\n  .attr(\"x\", (d, i) => mainX0 + i * binWidthPx)\n  .attr(\"width\", binWidthPx + 0.5)\n  .attr(\"y\", (d) => xMarginalScale(d))\n  .attr(\"height\", (d) => topMarginY1 - xMarginalScale(d))\n  .attr(\"fill\", t.palette[0])\n  .attr(\"opacity\", 0.55);\n\nsvg\n  .selectAll(\"rect.marginal-y\")\n  .data(yMarginal)\n  .join(\"rect\")\n  .attr(\"class\", \"marginal-y\")\n  .attr(\"y\", (d, i) => mainY1 - (i + 1) * binHeightPx)\n  .attr(\"height\", binHeightPx + 0.5)\n  .attr(\"x\", rightMarginX0)\n  .attr(\"width\", (d) => yMarginalScale(d) - rightMarginX0)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"opacity\", 0.55);\n\n// --- Axes ----------------------------------------------------------------\nconst xAxisG = svg.append(\"g\").attr(\"transform\", `translate(0,${mainY1})`).call(d3.axisBottom(xScale).ticks(8));\nconst yAxisG = svg.append(\"g\").attr(\"transform\", `translate(${mainX0},0)`).call(d3.axisLeft(yScale).ticks(8));\nfor (const axisG of [xAxisG, yAxisG]) {\n  axisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  axisG.selectAll(\"line\").attr(\"stroke\", t.grid);\n  axisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", (mainX0 + mainX1) / 2)\n  .attr(\"y\", mainY1 + 55)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Tech Stock Daily Return (%)\");\n\nsvg\n  .append(\"text\")\n  .attr(\"transform\", `translate(${mainX0 - 60}, ${(mainY0 + mainY1) / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Energy Stock Daily Return (%)\");\n\n// --- Colorbar (imprint_seq gradient, sqrt-matched to the cell color scale) --\nconst gradientId = \"histogram2dDensityGradient\";\nconst gradient = svg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", gradientId)\n  .attr(\"x1\", \"0%\")\n  .attr(\"y1\", \"100%\")\n  .attr(\"x2\", \"0%\")\n  .attr(\"y2\", \"0%\");\n\nconst STOP_COUNT = 20;\nfor (let i = 0; i <= STOP_COUNT; i++) {\n  const barFraction = i / STOP_COUNT; // linear position along the bar: 0 = count 0, 1 = maxCount\n  gradient\n    .append(\"stop\")\n    .attr(\"offset\", `${barFraction * 100}%`)\n    .attr(\"stop-color\", d3.interpolateRgbBasis(t.seq)(Math.sqrt(barFraction)));\n}\n\nsvg\n  .append(\"rect\")\n  .attr(\"x\", colorbarX0)\n  .attr(\"y\", mainY0)\n  .attr(\"width\", colorbarX1 - colorbarX0)\n  .attr(\"height\", mainY1 - mainY0)\n  .attr(\"fill\", `url(#${gradientId})`);\n\nconst colorbarAxisG = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${colorbarX1},0)`)\n  .call(d3.axisRight(colorbarScale).ticks(5));\ncolorbarAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\ncolorbarAxisG.selectAll(\"line\").attr(\"stroke\", t.grid);\ncolorbarAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", colorbarX0)\n  .attr(\"y\", mainY0 - 16)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"Point count\");\n\n// --- Title -------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"histogram-2d · javascript · d3 · anyplot.ai\");\n"}