{"spec_id":"histogram-2d","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// histogram-2d: 2D Histogram Heatmap\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: correlated daily returns for two assets, deterministic LCG -------\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\n\nfunction randNormal() {\n  const u1 = Math.max(rand(), 1e-12);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst N_POINTS = 6000;\nconst CORRELATION = 0.65;\nconst assetAReturns = [];\nconst assetBReturns = [];\nfor (let i = 0; i < N_POINTS; i++) {\n  const z1 = randNormal();\n  const z2 = randNormal();\n  assetAReturns.push(z1 * 1.4);\n  assetBReturns.push(CORRELATION * z1 * 1.1 + Math.sqrt(1 - CORRELATION * CORRELATION) * z2 * 1.1);\n}\n\n// --- Bin the joint distribution into a fixed grid ---------------------------\nconst N_BINS = 26;\nconst xMin = Math.min(...assetAReturns);\nconst xMax = Math.max(...assetAReturns);\nconst yMin = Math.min(...assetBReturns);\nconst yMax = Math.max(...assetBReturns);\nconst binWidthX = (xMax - xMin) / N_BINS;\nconst binWidthY = (yMax - yMin) / N_BINS;\n\nconst counts = new Array(N_BINS * N_BINS).fill(0);\nfor (let i = 0; i < N_POINTS; i++) {\n  let bx = Math.floor((assetAReturns[i] - xMin) / binWidthX);\n  let by = Math.floor((assetBReturns[i] - yMin) / binWidthY);\n  if (bx === N_BINS) bx -= 1;\n  if (by === N_BINS) by -= 1;\n  counts[by * N_BINS + bx] += 1;\n}\nconst maxCount = Math.max(...counts);\nconst logMaxCount = Math.log1p(maxCount);\n\nconst bins = [];\nfor (let by = 0; by < N_BINS; by++) {\n  for (let bx = 0; bx < N_BINS; bx++) {\n    const count = counts[by * N_BINS + bx];\n    if (count === 0) continue;\n    const left = xMin + bx * binWidthX;\n    const bottom = yMin + by * binWidthY;\n    bins.push({ left, right: left + binWidthX, bottom, top: bottom + binWidthY, count });\n  }\n}\n\n// --- Density color: imprint_seq, log-scaled (density varies widely) --------\nfunction hexToRgb(hex) {\n  const value = parseInt(hex.slice(1), 16);\n  return [(value >> 16) & 255, (value >> 8) & 255, value & 255];\n}\nconst seqLow = hexToRgb(t.seq[0]);\nconst seqHigh = hexToRgb(t.seq[1]);\nfunction densityColor(count) {\n  const ratio = Math.log1p(count) / logMaxCount;\n  const r = Math.round(seqLow[0] + (seqHigh[0] - seqLow[0]) * ratio);\n  const g = Math.round(seqLow[1] + (seqHigh[1] - seqLow[1]) * ratio);\n  const b = Math.round(seqLow[2] + (seqHigh[2] - seqLow[2]) * ratio);\n  return `rgb(${r}, ${g}, ${b})`;\n}\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Custom plugin: paints binned density rects + a colorbar legend ---------\n// Chart.js has no core heatmap chart type; the plugin hooks below use the\n// scatter chart's own linear scales to convert bin edges to pixel rects,\n// entirely through Chart.js's public plugin API (no chartjs-chart-matrix).\nconst COLORBAR_WIDTH = 28;\nconst COLORBAR_OFFSET = 30;\nconst COLORBAR_RESERVED = 140;\n\nconst heatmapPlugin = {\n  id: \"histogram2dHeatmap\",\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    const { x: xScale, y: yScale } = scales;\n    ctx.save();\n    for (const bin of bins) {\n      const xPixelLeft = xScale.getPixelForValue(bin.left);\n      const xPixelRight = xScale.getPixelForValue(bin.right);\n      const yPixelTop = yScale.getPixelForValue(bin.top);\n      const yPixelBottom = yScale.getPixelForValue(bin.bottom);\n      ctx.fillStyle = densityColor(bin.count);\n      ctx.fillRect(\n        Math.min(xPixelLeft, xPixelRight),\n        Math.min(yPixelTop, yPixelBottom),\n        Math.abs(xPixelRight - xPixelLeft),\n        Math.abs(yPixelBottom - yPixelTop)\n      );\n    }\n    ctx.restore();\n  },\n  afterDraw(chart) {\n    const { ctx, chartArea } = chart;\n    const barX = chartArea.right + COLORBAR_OFFSET;\n    const barTop = chartArea.top;\n    const barHeight = chartArea.bottom - chartArea.top;\n\n    const gradient = ctx.createLinearGradient(0, barTop + barHeight, 0, barTop);\n    gradient.addColorStop(0, t.seq[0]);\n    gradient.addColorStop(1, t.seq[1]);\n\n    ctx.save();\n    ctx.fillStyle = gradient;\n    ctx.fillRect(barX, barTop, COLORBAR_WIDTH, barHeight);\n    ctx.strokeStyle = t.grid;\n    ctx.lineWidth = 1;\n    ctx.strokeRect(barX, barTop, COLORBAR_WIDTH, barHeight);\n\n    ctx.fillStyle = t.inkSoft;\n    ctx.font = \"13px sans-serif\";\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"middle\";\n    ctx.fillText(\"0\", barX + COLORBAR_WIDTH + 8, barTop + barHeight);\n    ctx.fillText(String(maxCount), barX + COLORBAR_WIDTH + 8, barTop);\n\n    ctx.translate(barX + COLORBAR_WIDTH + 34, barTop + barHeight / 2);\n    ctx.rotate(-Math.PI / 2);\n    ctx.textAlign = \"center\";\n    ctx.fillStyle = t.ink;\n    ctx.font = \"14px sans-serif\";\n    ctx.fillText(\"Point count\", 0, 0);\n    ctx.restore();\n  },\n};\n\n// --- Chart ---------------------------------------------------------------\n// A near-invisible dataset drives the scatter chart's linear scale ranges;\n// the plugin above does the actual density painting.\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        label: \"Joint density\",\n        data: [\n          { x: xMin, y: yMin },\n          { x: xMax, y: yMax },\n        ],\n        pointRadius: 0,\n        showLine: false,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { right: COLORBAR_RESERVED } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"histogram-2d · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: xMin,\n        max: xMax,\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { display: false },\n        title: { display: true, text: \"Asset A Daily Return (%)\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        type: \"linear\",\n        min: yMin,\n        max: yMax,\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { display: false },\n        title: { display: true, text: \"Asset B Daily Return (%)\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n  plugins: [heatmapPlugin],\n});\n"}