{"spec_id":"heatmap-correlation","library":"d3","language":"javascript","code":"// anyplot.ai\n// heatmap-correlation: Correlation Matrix Heatmap\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-18\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data: synthetic athletic-performance testing battery -------------------\n// A deterministic LCG stands in for a seeded RNG (the browser has none).\nconst lcg = (seed) => {\n  let state = seed >>> 0;\n  return () => {\n    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n};\nconst rand = lcg(42);\nconst randNormal = () => {\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 labels = [\n  \"Sprint Speed\", \"Vertical Jump\", \"VO2 Max\", \"Body Fat %\",\n  \"Resting HR\", \"Reaction Time\", \"Agility Score\", \"Strength Index\",\n];\n\n// Two latent factors (explosive power, aerobic fitness) drive realistic\n// cross-correlations between the eight test metrics, plus per-metric noise.\nconst athletes = 200;\nconst series = labels.map(() => []);\nfor (let i = 0; i < athletes; i += 1) {\n  const power = randNormal();\n  const fitness = randNormal();\n  series[0].push(0.7 * power + 0.5 * randNormal()); // Sprint Speed\n  series[1].push(0.65 * power + 0.55 * randNormal()); // Vertical Jump\n  series[2].push(0.7 * fitness + 0.5 * randNormal()); // VO2 Max\n  series[3].push(-0.5 * fitness - 0.3 * power + 0.55 * randNormal()); // Body Fat %\n  series[4].push(-0.6 * fitness + 0.55 * randNormal()); // Resting HR\n  series[5].push(-0.2 * power + 0.9 * randNormal()); // Reaction Time\n  series[6].push(0.6 * power + 0.2 * fitness + 0.5 * randNormal()); // Agility Score\n  series[7].push(0.55 * power + 0.55 * randNormal()); // Strength Index\n}\n\nconst mean = (a) => a.reduce((s, v) => s + v, 0) / a.length;\nconst pearson = (a, b) => {\n  const ma = mean(a);\n  const mb = mean(b);\n  let num = 0;\n  let da = 0;\n  let db = 0;\n  for (let i = 0; i < a.length; i += 1) {\n    num += (a[i] - ma) * (b[i] - mb);\n    da += (a[i] - ma) ** 2;\n    db += (b[i] - mb) ** 2;\n  }\n  return num / Math.sqrt(da * db);\n};\n\nconst n = labels.length;\nconst corr = series.map((a) => series.map((b) => pearson(a, b)));\n\n// --- Layout -------------------------------------------------------------\nconst margin = { top: 150, right: 200, bottom: 250, left: 200 };\nconst side = Math.min(width - margin.left - margin.right, height - margin.top - margin.bottom);\nconst cell = side / n;\n\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// --- Color scale: Imprint diverging, fixed -1..1 domain per spec -----------\nconst colorScale = d3.scaleSequential(d3.interpolateRgbBasis(t.div)).domain([-1, 1]);\nconst relLuminance = (hex) => {\n  const c = hex.replace(\"#\", \"\");\n  const r = parseInt(c.slice(0, 2), 16) / 255;\n  const g = parseInt(c.slice(2, 4), 16) / 255;\n  const b = parseInt(c.slice(4, 6), 16) / 255;\n  return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n};\nconst lightText = t.pageBg;\nconst textColorFor = (value) => (relLuminance(colorScale(value)) < 0.5 ? lightText : t.ink);\n\n// --- Matrix (lower triangle only — upper half is redundant) ----------------\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\nconst cells = [];\nfor (let row = 0; row < n; row += 1) {\n  for (let col = 0; col <= row; col += 1) {\n    cells.push({ row, col, value: corr[row][col] });\n  }\n}\n\ng.selectAll(\"rect\").data(cells).join(\"rect\")\n  .attr(\"x\", (d) => d.col * cell)\n  .attr(\"y\", (d) => d.row * cell)\n  .attr(\"width\", cell)\n  .attr(\"height\", cell)\n  .attr(\"fill\", (d) => colorScale(d.value))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\ng.selectAll(\"text.cell-label\").data(cells).join(\"text\")\n  .attr(\"class\", \"cell-label\")\n  .attr(\"x\", (d) => d.col * cell + cell / 2)\n  .attr(\"y\", (d) => d.row * cell + cell / 2)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"central\")\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", 500)\n  .attr(\"fill\", (d) => textColorFor(d.value))\n  .text((d) => d.value.toFixed(2));\n\n// --- Row labels (left) -------------------------------------------------\ng.selectAll(\"text.row-label\").data(labels).join(\"text\")\n  .attr(\"class\", \"row-label\")\n  .attr(\"x\", -14)\n  .attr(\"y\", (_, i) => i * cell + cell / 2)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"dominant-baseline\", \"central\")\n  .style(\"font-size\", \"18px\")\n  .attr(\"fill\", t.inkSoft)\n  .text((d) => d);\n\n// --- Column labels (bottom, rotated) ------------------------------------\ng.selectAll(\"text.col-label\").data(labels).join(\"text\")\n  .attr(\"class\", \"col-label\")\n  .attr(\"transform\", (_, i) => `translate(${i * cell + cell / 2},${side + 16}) rotate(-40)`)\n  .attr(\"text-anchor\", \"end\")\n  .style(\"font-size\", \"18px\")\n  .attr(\"fill\", t.inkSoft)\n  .text((d) => d);\n\n// --- Colorbar legend (fixed -1..1 range) --------------------------------\nconst barX = side + 50;\nconst barWidth = 26;\nconst defs = svg.append(\"defs\");\nconst gradient = defs.append(\"linearGradient\")\n  .attr(\"id\", \"div-gradient\")\n  .attr(\"x1\", \"0\").attr(\"x2\", \"0\").attr(\"y1\", \"1\").attr(\"y2\", \"0\");\nd3.range(0, 1.0001, 0.1).forEach((frac) => {\n  gradient.append(\"stop\")\n    .attr(\"offset\", `${frac * 100}%`)\n    .attr(\"stop-color\", colorScale(-1 + frac * 2));\n});\n\nconst bar = g.append(\"g\").attr(\"transform\", `translate(${barX},0)`);\nbar.append(\"rect\")\n  .attr(\"width\", barWidth)\n  .attr(\"height\", side)\n  .attr(\"fill\", \"url(#div-gradient)\")\n  .attr(\"stroke\", t.inkSoft);\n\nconst barScale = d3.scaleLinear().domain([-1, 1]).range([side, 0]);\nconst barTicks = bar.append(\"g\").attr(\"transform\", `translate(${barWidth},0)`)\n  .call(d3.axisRight(barScale).tickValues([-1, -0.5, 0, 0.5, 1]).tickSize(6));\nbarTicks.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\nbarTicks.selectAll(\"line\").attr(\"stroke\", t.grid);\nbarTicks.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nbar.append(\"text\")\n  .attr(\"x\", barWidth / 2)\n  .attr(\"y\", -18)\n  .attr(\"text-anchor\", \"middle\")\n  .style(\"font-size\", \"16px\")\n  .attr(\"fill\", t.inkSoft)\n  .text(\"r\");\n\n// --- Title ----------------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", 600)\n  .text(\"heatmap-correlation · javascript · d3 · anyplot.ai\");\n"}