{"spec_id":"heatmap-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// heatmap-basic: Basic Heatmap\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 92/100 | Updated: 2026-06-03\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Data — Pearson correlations between 10 macroeconomic indicators.\n// Diverging polarity (−1..+1) → Imprint imprint_div colormap is the right fit.\nconst labels = [\n  \"GDP\",\n  \"Unemploy.\",\n  \"Inflation\",\n  \"Interest\",\n  \"Stocks\",\n  \"Housing\",\n  \"Cons. Conf.\",\n  \"Exports\",\n  \"Imports\",\n  \"Wages\",\n];\nconst N = labels.length;\n\nconst matrix = [\n  [ 1.00, -0.62,  0.18,  0.31,  0.74,  0.66,  0.71,  0.58,  0.45, -0.14],\n  [-0.62,  1.00, -0.22, -0.48, -0.55, -0.59, -0.81, -0.41, -0.35, -0.07],\n  [ 0.18, -0.22,  1.00,  0.69, -0.07, -0.31, -0.42, -0.05,  0.21,  0.46],\n  [ 0.31, -0.48,  0.69,  1.00,  0.12, -0.15, -0.33,  0.07,  0.18,  0.39],\n  [ 0.74, -0.55, -0.07,  0.12,  1.00,  0.58,  0.79,  0.61,  0.40, -0.21],\n  [ 0.66, -0.59, -0.31, -0.15,  0.58,  1.00,  0.66,  0.42,  0.28, -0.18],\n  [ 0.71, -0.81, -0.42, -0.33,  0.79,  0.66,  1.00,  0.48,  0.37,  0.12],\n  [ 0.58, -0.41, -0.05,  0.07,  0.61,  0.42,  0.48,  1.00,  0.73,  0.24],\n  [ 0.45, -0.35,  0.21,  0.18,  0.40,  0.28,  0.37,  0.73,  1.00,  0.31],\n  [-0.14, -0.07,  0.46,  0.39, -0.21, -0.18,  0.12,  0.24,  0.31,  1.00],\n];\n\n// Focal pair — strongest off-diagonal correlation (Cons. Conf. × Unemploy.\n// = -0.81). A heavier t.ink stroke gives the eye a single anchor.\nconst FOCAL = [[1, 6], [6, 1]];\n\n// imprint_div interpolation (red ↔ theme midpoint ↔ blue) — t.div is already\n// theme-adaptive (midpoint = pageBg).\nfunction hexToRgb(hex) {\n  const h = hex.replace(\"#\", \"\");\n  return [\n    parseInt(h.slice(0, 2), 16),\n    parseInt(h.slice(2, 4), 16),\n    parseInt(h.slice(4, 6), 16),\n  ];\n}\nconst C_LO = hexToRgb(t.div[0]);\nconst C_MID = hexToRgb(t.div[1]);\nconst C_HI = hexToRgb(t.div[2]);\nfunction divColor(v) {\n  const u = Math.max(0, Math.min(1, (v + 1) / 2));\n  const [a, b] = u <= 0.5 ? [C_LO, C_MID] : [C_MID, C_HI];\n  const w = u <= 0.5 ? u * 2 : (u - 0.5) * 2;\n  return `rgb(${(a[0] + (b[0] - a[0]) * w) | 0},${(a[1] + (b[1] - a[1]) * w) | 0},${(a[2] + (b[2] - a[2]) * w) | 0})`;\n}\n\nconst FONT_STACK =\n  '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif';\n\n// Custom Chart.js plugin — paints heatmap cells, writes per-cell correlation\n// values, and renders a vertical colorbar in the right-side padding reserved\n// via options.layout.padding.\nconst heatmapPainter = {\n  id: \"heatmap-painter\",\n  beforeDatasetsDraw(chart) {\n    const { ctx, chartArea } = chart;\n    const { left, right, top, bottom } = chartArea;\n    const cellW = (right - left) / N;\n    const cellH = (bottom - top) / N;\n\n    for (let i = 0; i < N; i++) {\n      for (let j = 0; j < N; j++) {\n        ctx.fillStyle = divColor(matrix[i][j]);\n        ctx.fillRect(\n          left + j * cellW,\n          top + i * cellH,\n          cellW + 1,\n          cellH + 1,\n        );\n      }\n    }\n\n    // 1 px t.grid hairline between cells so the matrix structure remains\n    // visible in dark mode even where imprint_div fades into pageBg.\n    ctx.save();\n    ctx.strokeStyle = t.grid;\n    ctx.lineWidth = 1;\n    ctx.beginPath();\n    for (let k = 0; k <= N; k++) {\n      const x = Math.round(left + k * cellW) + 0.5;\n      ctx.moveTo(x, top);\n      ctx.lineTo(x, bottom);\n      const y = Math.round(top + k * cellH) + 0.5;\n      ctx.moveTo(left, y);\n      ctx.lineTo(right, y);\n    }\n    ctx.stroke();\n    ctx.restore();\n\n    // Focal stroke — anchor the eye on the strongest off-diagonal pair.\n    ctx.save();\n    ctx.strokeStyle = t.ink;\n    ctx.lineWidth = 2.5;\n    for (const [i, j] of FOCAL) {\n      ctx.strokeRect(\n        left + j * cellW + 1.5,\n        top + i * cellH + 1.5,\n        cellW - 3,\n        cellH - 3,\n      );\n    }\n    ctx.restore();\n  },\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea, width } = chart;\n    const { left, right, top, bottom } = chartArea;\n    const cellW = (right - left) / N;\n    const cellH = (bottom - top) / N;\n\n    // Per-cell values. Light text on saturated cells, ink on near-midpoint.\n    ctx.save();\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    ctx.font = `500 15px ${FONT_STACK}`;\n    for (let i = 0; i < N; i++) {\n      for (let j = 0; j < N; j++) {\n        const v = matrix[i][j];\n        ctx.fillStyle = Math.abs(v) > 0.55 ? \"#FAF8F1\" : t.ink;\n        ctx.fillText(\n          v.toFixed(2),\n          left + (j + 0.5) * cellW,\n          top + (i + 0.5) * cellH,\n        );\n      }\n    }\n    ctx.restore();\n\n    // Colorbar — vertical gradient in the right-side padding.\n    const cbGap = 32;\n    const cbW = 24;\n    const cbLeft = right + cbGap;\n    const cbTop = top;\n    const cbBottom = bottom;\n    const cbH = cbBottom - cbTop;\n\n    const grad = ctx.createLinearGradient(0, cbTop, 0, cbBottom);\n    grad.addColorStop(0.0, divColor(1));\n    grad.addColorStop(0.5, divColor(0));\n    grad.addColorStop(1.0, divColor(-1));\n    ctx.fillStyle = grad;\n    ctx.fillRect(cbLeft, cbTop, cbW, cbH);\n\n    // Colorbar tick labels.\n    ctx.save();\n    ctx.font = `400 14px ${FONT_STACK}`;\n    ctx.fillStyle = t.inkSoft;\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1;\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"middle\";\n    for (const v of [1, 0.5, 0, -0.5, -1]) {\n      const y = cbTop + (1 - (v + 1) / 2) * cbH;\n      ctx.beginPath();\n      ctx.moveTo(cbLeft + cbW, y);\n      ctx.lineTo(cbLeft + cbW + 5, y);\n      ctx.stroke();\n      ctx.fillText(v.toFixed(1), cbLeft + cbW + 11, y);\n    }\n    ctx.restore();\n\n    // Colorbar title (rotated 90° clockwise, sitting against the canvas edge).\n    ctx.save();\n    ctx.fillStyle = t.ink;\n    ctx.font = `500 16px ${FONT_STACK}`;\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    ctx.translate(width - 14, (cbTop + cbBottom) / 2);\n    ctx.rotate(Math.PI / 2);\n    ctx.fillText(\"Pearson correlation (r)\", 0, 0);\n    ctx.restore();\n  },\n};\n\n// Mount\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// Chart — scatter base purely for axis layout (no data points drawn); cells\n// and colorbar are rendered by heatmapPainter against the resolved chartArea.\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        data: [],\n        pointRadius: 0,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 4, right: 170, bottom: 4, left: 4 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"Macro Indicator Correlations · heatmap-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 18, weight: \"500\" },\n        padding: { top: 4, bottom: 18 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: -0.5,\n        max: N - 0.5,\n        position: \"top\",\n        offset: false,\n        afterBuildTicks: (axis) => {\n          axis.ticks = Array.from({ length: N }, (_, i) => ({ value: i }));\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 15 },\n          autoSkip: false,\n          callback: (v) =>\n            Number.isInteger(v) && v >= 0 && v < N ? labels[v] : \"\",\n          maxRotation: 35,\n          minRotation: 35,\n        },\n        grid: { display: false },\n        border: { display: false },\n      },\n      y: {\n        type: \"linear\",\n        min: -0.5,\n        max: N - 0.5,\n        reverse: true,\n        offset: false,\n        afterBuildTicks: (axis) => {\n          axis.ticks = Array.from({ length: N }, (_, i) => ({ value: i }));\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 15 },\n          autoSkip: false,\n          callback: (v) =>\n            Number.isInteger(v) && v >= 0 && v < N ? labels[v] : \"\",\n        },\n        grid: { display: false },\n        border: { display: false },\n      },\n    },\n  },\n  plugins: [heatmapPainter],\n});\n"}