{"spec_id":"histogram-2d","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// histogram-2d: 2D Histogram Heatmap\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-05\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Data: correlated daily returns of two assets (deterministic LCG) ------\nlet seed = 42;\nfunction nextUniform() {\n  seed = (seed * 1664525 + 1013904223) >>> 0;\n  return seed / 4294967296;\n}\nfunction nextNormal() {\n  const u1 = Math.max(nextUniform(), 1e-12);\n  const u2 = nextUniform();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst N_POINTS = 4000;\nconst RHO = 0.65;\nconst SIGMA = 2.6; // percent\n\nconst N_BINS = 26;\nconst RANGE_MIN = -10;\nconst RANGE_MAX = 10;\nconst BIN_WIDTH = (RANGE_MAX - RANGE_MIN) / N_BINS;\n\nconst counts = Array.from({ length: N_BINS }, () => new Array(N_BINS).fill(0));\nfor (let i = 0; i < N_POINTS; i++) {\n  const z1 = nextNormal();\n  const z2 = nextNormal();\n  const assetA = z1 * SIGMA;\n  const assetB = (RHO * z1 + Math.sqrt(1 - RHO * RHO) * z2) * SIGMA;\n\n  const ix = Math.min(Math.max(Math.floor((assetA - RANGE_MIN) / BIN_WIDTH), 0), N_BINS - 1);\n  const iy = Math.min(Math.max(Math.floor((assetB - RANGE_MIN) / BIN_WIDTH), 0), N_BINS - 1);\n  counts[iy][ix] += 1;\n}\n\nlet maxCount = 0;\nfor (const row of counts) {\n  for (const c of row) maxCount = Math.max(maxCount, c);\n}\n\n// --- Density → Imprint sequential color (log scale — density is skewed) ---\nfunction hexToRgb(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\nfunction mixHex(hexA, hexB, ratio) {\n  const a = hexToRgb(hexA);\n  const b = hexToRgb(hexB);\n  const rgb = a.map((v, i) => Math.round(v + (b[i] - v) * ratio));\n  return '#' + rgb.map((v) => v.toString(16).padStart(2, '0')).join('');\n}\nfunction colorForCount(count) {\n  const ratio = Math.log1p(count) / Math.log1p(maxCount);\n  return mixHex(t.seq[0], t.seq[1], ratio);\n}\n\nconst seriesData = [];\nfor (let iy = 0; iy < N_BINS; iy++) {\n  for (let ix = 0; ix < N_BINS; ix++) {\n    const count = counts[iy][ix];\n    if (count === 0) continue;\n    seriesData.push({\n      x: RANGE_MIN + (ix + 0.5) * BIN_WIDTH,\n      y: RANGE_MIN + (iy + 0.5) * BIN_WIDTH,\n      count,\n      color: colorForCount(count),\n    });\n  }\n}\n\n// --- Layout: explicit margins so the square bin markers can be sized exactly\nconst MARGIN = { top: 120, right: 190, bottom: 110, left: 120 };\nconst plotWidth = size.width - MARGIN.left - MARGIN.right;\nconst plotHeight = size.height - MARGIN.top - MARGIN.bottom;\nconst pxPerXUnit = plotWidth / (RANGE_MAX - RANGE_MIN);\nconst pxPerYUnit = plotHeight / (RANGE_MAX - RANGE_MIN);\nconst markerRadius = (BIN_WIDTH * Math.min(pxPerXUnit, pxPerYUnit)) / 2 + 0.4;\n\n// --- Custom colorbar (core SVGRenderer — no colorAxis module required) -----\n// Vertical position on the bar is linear in log1p(count), matching colorForCount().\n// Intermediate ticks are placed at their real log-spaced count values so the bar\n// doesn't read as a plain linear count scale.\nfunction colorbarTicks(maxCountValue, barHeight) {\n  const denom = Math.log1p(maxCountValue) || 1;\n  const raw = [0, 1 / 3, 2 / 3, 1].map((r) => {\n    const value = Math.max(1, Math.round(Math.expm1(r * denom)));\n    return { value, ratio: Math.log1p(value) / denom };\n  });\n  const ticks = [];\n  for (const tick of raw) {\n    const y = barHeight * (1 - tick.ratio);\n    const prev = ticks[ticks.length - 1];\n    if (!prev || Math.abs(y - prev.y) >= 16) ticks.push({ ...tick, y });\n  }\n  return ticks;\n}\n\nfunction drawColorbar(chart) {\n  const barWidth = 26;\n  const barX = chart.plotLeft + chart.plotWidth + 30;\n  const barY = chart.plotTop;\n  const barHeight = chart.plotHeight;\n\n  chart.renderer\n    .rect(barX, barY, barWidth, barHeight)\n    .attr({\n      fill: {\n        linearGradient: { x1: 0, y1: 1, x2: 0, y2: 0 },\n        stops: [\n          [0, t.seq[0]],\n          [1, t.seq[1]],\n        ],\n      },\n      stroke: t.inkSoft,\n      'stroke-width': 1,\n    })\n    .add();\n  chart.renderer\n    .text('Count (log scale)', barX, barY - 14)\n    .css({ color: t.inkSoft, fontSize: '14px' })\n    .add();\n\n  for (const tick of colorbarTicks(maxCount, barHeight)) {\n    const y = barY + tick.y;\n    chart.renderer\n      .path(['M', barX + barWidth, y, 'L', barX + barWidth + 6, y])\n      .attr({ stroke: t.inkSoft, 'stroke-width': 1 })\n      .add();\n    chart.renderer\n      .text(String(tick.value), barX + barWidth + 9, y + 4)\n      .css({ color: t.inkSoft, fontSize: '12px' })\n      .add();\n  }\n}\n\n// --- Chart -------------------------------------------------------------\nHighcharts.chart('container', {\n  chart: {\n    type: 'scatter',\n    backgroundColor: 'transparent',\n    animation: false,\n    spacing: [0, 0, 0, 0],\n    margin: [MARGIN.top, MARGIN.right, MARGIN.bottom, MARGIN.left],\n    style: { fontFamily: 'inherit' },\n    events: { load: function () { drawColorbar(this); } },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: 'histogram-2d · javascript · highcharts · anyplot.ai',\n    style: { color: t.ink, fontSize: '22px', fontWeight: '600' },\n  },\n  subtitle: {\n    text: `Simulated daily returns · ρ ≈ ${RHO.toFixed(2)} · n = ${N_POINTS.toLocaleString()}`,\n    style: { color: t.inkSoft, fontSize: '14px' },\n  },\n  xAxis: {\n    min: RANGE_MIN,\n    max: RANGE_MAX,\n    startOnTick: false,\n    endOnTick: false,\n    tickInterval: 5,\n    title: { text: 'Asset A Daily Return (%)', style: { color: t.inkSoft, fontSize: '16px' } },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: '14px' } },\n  },\n  yAxis: {\n    min: RANGE_MIN,\n    max: RANGE_MAX,\n    startOnTick: false,\n    endOnTick: false,\n    tickInterval: 5,\n    title: { text: 'Asset B Daily Return (%)', style: { color: t.inkSoft, fontSize: '16px' } },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: '14px' } },\n  },\n  legend: { enabled: false },\n  tooltip: {\n    backgroundColor: t.elevatedBg,\n    borderColor: t.inkSoft,\n    style: { color: t.ink, fontSize: '14px' },\n    formatter: function () {\n      return `Asset A: ${this.x.toFixed(1)}%<br/>Asset B: ${this.y.toFixed(1)}%<br/>Count: ${this.point.count}`;\n    },\n  },\n  plotOptions: {\n    series: { animation: false },\n    scatter: { marker: { symbol: 'square', radius: markerRadius, lineWidth: 0 } },\n  },\n  series: [{ name: 'Bin count', data: seriesData }],\n});\n"}