{"spec_id":"heatmap-annotated","library":"d3","language":"javascript","code":"// anyplot.ai\n// heatmap-annotated: Annotated Heatmap\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 93/100 | Created: 2026-08-05\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data: deterministic correlation matrix biased toward domain-plausible --\n// magnitudes/signs for well-known weather-variable relationships (LCG jitter\n// within each pair's plausible range keeps it reproducible, not extreme).\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nconst variables = [\n  \"Temperature\",\n  \"Humidity\",\n  \"Wind Speed\",\n  \"Pressure\",\n  \"Rainfall\",\n  \"Cloud Cover\",\n  \"UV Index\",\n  \"Visibility\",\n];\nconst n = variables.length;\n\n// [row, col, sign, minMagnitude, maxMagnitude] for every pair, chosen to\n// match textbook-plausible weather correlations (e.g. temperature/UV index\n// move together, cloud cover/UV index move apart, humidity/rainfall are only\n// moderately linked rather than near-perfect).\nconst PAIR_CORR = [\n  [\"Temperature\", \"Humidity\", -1, 0.3, 0.6],\n  [\"Temperature\", \"Wind Speed\", 1, 0.05, 0.3],\n  [\"Temperature\", \"Pressure\", -1, 0.1, 0.35],\n  [\"Temperature\", \"Rainfall\", -1, 0.2, 0.5],\n  [\"Temperature\", \"Cloud Cover\", -1, 0.15, 0.4],\n  [\"Temperature\", \"UV Index\", 1, 0.5, 0.8],\n  [\"Temperature\", \"Visibility\", 1, 0.1, 0.35],\n  [\"Humidity\", \"Wind Speed\", -1, 0.05, 0.3],\n  [\"Humidity\", \"Pressure\", -1, 0.15, 0.4],\n  [\"Humidity\", \"Rainfall\", 1, 0.35, 0.65],\n  [\"Humidity\", \"Cloud Cover\", 1, 0.3, 0.6],\n  [\"Humidity\", \"UV Index\", -1, 0.2, 0.5],\n  [\"Humidity\", \"Visibility\", -1, 0.3, 0.6],\n  [\"Wind Speed\", \"Pressure\", -1, 0.2, 0.5],\n  [\"Wind Speed\", \"Rainfall\", 1, 0.05, 0.3],\n  [\"Wind Speed\", \"Cloud Cover\", 1, 0.05, 0.3],\n  [\"Wind Speed\", \"UV Index\", -1, 0.0, 0.2],\n  [\"Wind Speed\", \"Visibility\", 1, 0.1, 0.35],\n  [\"Pressure\", \"Rainfall\", -1, 0.25, 0.55],\n  [\"Pressure\", \"Cloud Cover\", -1, 0.2, 0.5],\n  [\"Pressure\", \"UV Index\", 1, 0.1, 0.35],\n  [\"Pressure\", \"Visibility\", 1, 0.15, 0.4],\n  [\"Rainfall\", \"Cloud Cover\", 1, 0.4, 0.7],\n  [\"Rainfall\", \"UV Index\", -1, 0.3, 0.6],\n  [\"Rainfall\", \"Visibility\", -1, 0.35, 0.65],\n  [\"Cloud Cover\", \"UV Index\", -1, 0.4, 0.7],\n  [\"Cloud Cover\", \"Visibility\", -1, 0.25, 0.55],\n  [\"UV Index\", \"Visibility\", 1, 0.1, 0.35],\n];\n\nconst corr = Array.from({ length: n }, () => new Array(n).fill(0));\nfor (let i = 0; i < n; i++) corr[i][i] = 1;\nfor (const [rowVar, colVar, sign, lo, hi] of PAIR_CORR) {\n  const i = variables.indexOf(rowVar);\n  const j = variables.indexOf(colVar);\n  const value = Math.round(sign * (lo + rand() * (hi - lo)) * 100) / 100;\n  corr[i][j] = value;\n  corr[j][i] = value;\n}\n\nconst cells = [];\nfor (let i = 0; i < n; i++) {\n  for (let j = 0; j < n; j++) {\n    cells.push({ row: variables[i], col: variables[j], value: corr[i][j] });\n  }\n}\n\n// --- Layout --------------------------------------------------------------------\n// Size the matrix off min(availW, availH) and center it so cells stay square\n// even though the title/legend need asymmetric top/right space.\nconst margin = { top: 150, right: 250, bottom: 130, left: 190 };\nconst availW = width - margin.left - margin.right;\nconst availH = height - margin.top - margin.bottom;\nconst gridSize = Math.min(availW, availH);\nconst gx = margin.left + (availW - gridSize) / 2;\nconst gy = margin.top + (availH - gridSize) / 2;\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${gx},${gy})`);\n\n// --- Scales --------------------------------------------------------------------\nconst x = d3.scaleBand().domain(variables).range([0, gridSize]).padding(0.04);\nconst y = d3.scaleBand().domain(variables).range([0, gridSize]).padding(0.04);\nconst color = d3.scaleSequential(d3.interpolateRgbBasis(t.div)).domain([-1, 1]);\n\n// --- Contrast text color -------------------------------------------------------\nfunction luminance(hex) {\n  const c = d3.color(hex).rgb();\n  const lin = (v) => {\n    const s = v / 255;\n    return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);\n  };\n  return 0.2126 * lin(c.r) + 0.7152 * lin(c.g) + 0.0722 * lin(c.b);\n}\nconst DARK_TEXT = \"#1A1A17\";\nconst LIGHT_TEXT = \"#F0EFE8\";\nfunction textColor(hex) {\n  return luminance(hex) > 0.45 ? DARK_TEXT : LIGHT_TEXT;\n}\n\n// --- Cells -----------------------------------------------------------------\ng.selectAll(\"rect.cell\")\n  .data(cells)\n  .join(\"rect\")\n  .attr(\"class\", \"cell\")\n  .attr(\"x\", (d) => x(d.col))\n  .attr(\"y\", (d) => y(d.row))\n  .attr(\"width\", x.bandwidth())\n  .attr(\"height\", y.bandwidth())\n  .attr(\"fill\", (d) => color(d.value))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\n// --- Cell value annotations --------------------------------------------------\nconst cellFontSize = Math.max(13, Math.min(22, x.bandwidth() / 3.6));\ng.selectAll(\"text.cell-label\")\n  .data(cells)\n  .join(\"text\")\n  .attr(\"class\", \"cell-label\")\n  .attr(\"x\", (d) => x(d.col) + x.bandwidth() / 2)\n  .attr(\"y\", (d) => y(d.row) + y.bandwidth() / 2)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"central\")\n  .style(\"font-size\", `${cellFontSize}px`)\n  .style(\"font-weight\", (d) => (d.row === d.col || Math.abs(d.value) > 0.6 ? 600 : 400))\n  .attr(\"fill\", (d) => textColor(color(d.value)))\n  .text((d) => d.value.toFixed(2));\n\n// --- Axes ------------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${gridSize})`).call(d3.axisBottom(x).tickSize(0));\nxAxis\n  .selectAll(\"text\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .attr(\"transform\", \"rotate(-35)\")\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"dx\", \"-0.4em\")\n  .attr(\"dy\", \"0.4em\");\nxAxis.select(\".domain\").remove();\n\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickSize(0));\nyAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\nyAxis.select(\".domain\").remove();\n\n// --- Colorbar legend -----------------------------------------------------------\nconst legendW = 34;\nconst legendH = gridSize * 0.62;\nconst legendX = gx + gridSize + 80;\nconst legendY = gy + (gridSize - legendH) / 2;\n\nconst defs = svg.append(\"defs\");\nconst gradient = defs\n  .append(\"linearGradient\")\n  .attr(\"id\", \"corr-gradient\")\n  .attr(\"x1\", \"0\")\n  .attr(\"x2\", \"0\")\n  .attr(\"y1\", \"1\")\n  .attr(\"y2\", \"0\");\ngradient\n  .selectAll(\"stop\")\n  .data(d3.range(0, 1.0001, 0.1))\n  .join(\"stop\")\n  .attr(\"offset\", (d) => `${d * 100}%`)\n  .attr(\"stop-color\", (d) => color(-1 + d * 2));\n\nsvg\n  .append(\"rect\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", legendY)\n  .attr(\"width\", legendW)\n  .attr(\"height\", legendH)\n  .attr(\"fill\", \"url(#corr-gradient)\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\nconst legendScale = d3.scaleLinear().domain([-1, 1]).range([legendY + legendH, legendY]);\nconst legendAxis = d3.axisRight(legendScale).ticks(5).tickSize(6);\nconst legendAxisG = svg.append(\"g\").attr(\"transform\", `translate(${legendX + legendW},0)`).call(legendAxis);\nlegendAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\nlegendAxisG.selectAll(\"line\").attr(\"stroke\", t.grid);\nlegendAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX + legendW / 2)\n  .attr(\"y\", legendY - 22)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"r\");\n\n// --- Title -------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"heatmap-annotated · javascript · d3 · anyplot.ai\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 100)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"17px\")\n  .text(\"Pearson correlation between weather variables\");\n"}