{"spec_id":"histogram-2d","library":"echarts","language":"javascript","code":"// anyplot.ai\n// histogram-2d: 2D Histogram Heatmap\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-05\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: deterministic bivariate-normal sample (LCG-seeded) --------------\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (1103515245 * state + 12345) >>> 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 SAMPLE_SIZE = 16000;\nconst CORRELATION = 0.55;\nconst customerAges = [];\nconst purchaseFrequencies = [];\nfor (let i = 0; i < SAMPLE_SIZE; i++) {\n  const z1 = randNormal();\n  const z2 = randNormal();\n  const zCorrelated = CORRELATION * z1 + Math.sqrt(1 - CORRELATION * CORRELATION) * z2;\n  const age = 42 + z1 * 13;\n  const frequency = 8 + zCorrelated * 3;\n  if (age >= 18 && age <= 75 && frequency >= 0) {\n    customerAges.push(age);\n    purchaseFrequencies.push(frequency);\n  }\n}\n\n// Pearson correlation of the retained sample, surfaced in the subtitle below.\nfunction pearson(xs, ys) {\n  const n = xs.length;\n  const meanX = xs.reduce((a, b) => a + b, 0) / n;\n  const meanY = ys.reduce((a, b) => a + b, 0) / n;\n  let cov = 0;\n  let varX = 0;\n  let varY = 0;\n  for (let i = 0; i < n; i++) {\n    const dx = xs[i] - meanX;\n    const dy = ys[i] - meanY;\n    cov += dx * dy;\n    varX += dx * dx;\n    varY += dy * dy;\n  }\n  return cov / Math.sqrt(varX * varY);\n}\nconst sampleCorrelation = pearson(customerAges, purchaseFrequencies);\n\n// --- Binning: build the 2D histogram grid -----------------------------------\nconst BIN_COUNT = 20;\nconst xMin = Math.min(...customerAges);\nconst xMax = Math.max(...customerAges);\nconst yMin = Math.min(...purchaseFrequencies);\nconst yMax = Math.max(...purchaseFrequencies);\nconst xStep = (xMax - xMin) / BIN_COUNT;\nconst yStep = (yMax - yMin) / BIN_COUNT;\n\nconst binCounts = Array.from({ length: BIN_COUNT }, () => new Array(BIN_COUNT).fill(0));\nfor (let i = 0; i < customerAges.length; i++) {\n  const xi = Math.min(BIN_COUNT - 1, Math.floor((customerAges[i] - xMin) / xStep));\n  const yi = Math.min(BIN_COUNT - 1, Math.floor((purchaseFrequencies[i] - yMin) / yStep));\n  binCounts[yi][xi] += 1;\n}\n\nconst xLabels = Array.from({ length: BIN_COUNT }, (_, i) => (xMin + (i + 0.5) * xStep).toFixed(0));\nconst yLabels = Array.from({ length: BIN_COUNT }, (_, i) => (yMin + (i + 0.5) * yStep).toFixed(1));\n\nconst heatmapData = [];\nlet maxCount = 0;\nfor (let yi = 0; yi < BIN_COUNT; yi++) {\n  for (let xi = 0; xi < BIN_COUNT; xi++) {\n    const count = binCounts[yi][xi];\n    if (count > 0) {\n      heatmapData.push([xi, yi, count]);\n      maxCount = Math.max(maxCount, count);\n    }\n  }\n}\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option --------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"histogram-2d · javascript · echarts · anyplot.ai\",\n    subtext: `Older customers purchase more often — sample correlation r = ${sampleCorrelation.toFixed(2)}`,\n    left: \"center\",\n    top: 20,\n    textStyle: { color: t.ink, fontSize: 22 },\n    subtextStyle: { color: t.inkSoft, fontSize: 15 },\n  },\n  tooltip: {\n    trigger: \"item\",\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    textStyle: { color: t.ink },\n    formatter: (params) => {\n      const [xi, yi, count] = params.value;\n      return `Age: ${xLabels[xi]} yrs<br/>Purchases: ${yLabels[yi]}/yr<br/>Count: ${count}`;\n    },\n  },\n  grid: { left: 120, right: 100, top: 130, bottom: 120 },\n  xAxis: {\n    type: \"category\",\n    data: xLabels,\n    name: \"Customer Age (years)\",\n    nameLocation: \"middle\",\n    nameGap: 50,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14, interval: 3 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitArea: { show: false },\n  },\n  yAxis: {\n    type: \"category\",\n    data: yLabels,\n    name: \"Purchases per Year\",\n    nameLocation: \"middle\",\n    nameGap: 75,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14, interval: 3 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitArea: { show: false },\n  },\n  visualMap: {\n    type: \"continuous\",\n    min: 0,\n    max: maxCount,\n    calculable: true,\n    orient: \"vertical\",\n    right: 10,\n    top: \"middle\",\n    text: [\"Point count\", \"\"],\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n    inRange: { color: t.seq },\n  },\n  series: [\n    {\n      type: \"heatmap\",\n      data: heatmapData,\n      itemStyle: { borderColor: t.pageBg, borderWidth: 1, borderRadius: 2 },\n      emphasis: { itemStyle: { borderColor: t.ink, borderWidth: 2, shadowBlur: 0 } },\n    },\n  ],\n});\n"}