{"spec_id":"heatmap-correlation","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// heatmap-correlation: Correlation Matrix Heatmap\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-08-18\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Financial metrics tracked across a portfolio of companies.\nconst variables = [\n  \"Revenue\",\n  \"Profit Margin\",\n  \"R&D Spend\",\n  \"Debt Ratio\",\n  \"Market Cap\",\n  \"Volatility\",\n];\n\n// Symmetric correlation matrix, hand-authored to reflect plausible\n// relationships (diagonal = 1, values in [-1, 1]).\nconst correlationMatrix = [\n  [1.0, 0.42, 0.58, -0.15, 0.81, -0.35],\n  [0.42, 1.0, 0.25, -0.48, 0.55, -0.52],\n  [0.58, 0.25, 1.0, 0.1, 0.47, 0.18],\n  [-0.15, -0.48, 0.1, 1.0, -0.3, 0.62],\n  [0.81, 0.55, 0.47, -0.3, 1.0, -0.44],\n  [-0.35, -0.52, 0.18, 0.62, -0.44, 1.0],\n];\nconst n = variables.length;\n\n// --- Color helpers (Imprint imprint_div, theme-adaptive midpoint) ----------\nfunction hexToRgb(hex) {\n  const num = parseInt(hex.replace(\"#\", \"\"), 16);\n  return [(num >> 16) & 255, (num >> 8) & 255, num & 255];\n}\nfunction lerp(a, b, f) {\n  return Math.round(a + (b - a) * f);\n}\nfunction lerpColor(hexA, hexB, f) {\n  const a = hexToRgb(hexA);\n  const b = hexToRgb(hexB);\n  return [lerp(a[0], b[0], f), lerp(a[1], b[1], f), lerp(a[2], b[2], f)];\n}\nfunction divergingRgb(value) {\n  const [neg, mid, pos] = t.div;\n  return value <= 0\n    ? lerpColor(neg, mid, value + 1)\n    : lerpColor(mid, pos, value);\n}\nfunction rgbToCss(rgb) {\n  return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;\n}\nfunction relativeLuminance(rgb) {\n  return 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2];\n}\n// Cell fills span the full imprint_div range, so label contrast is judged\n// against the fill itself — not the page theme — using the two Imprint ink\n// tones directly (light-theme ink for pale fills, dark-theme ink for dark\n// saturated fills), independent of ANYPLOT_THEME.\nfunction textColorFor(rgb) {\n  return relativeLuminance(rgb) > 140 ? \"#1A1A17\" : \"#F0EFE8\";\n}\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// Only the lower triangle (incl. diagonal) is drawn — the upper triangle is\n// redundant for a symmetric matrix (see spec \"Consider masking\").\nconst cellData = [];\nfor (let row = 0; row < n; row++) {\n  for (let col = 0; col <= row; col++) {\n    cellData.push({ x: col, y: row });\n  }\n}\n\n// --- Chart --------------------------------------------------------------\n// Chart.js has no native matrix/heatmap type; a scatter chart supplies\n// properly scaled category axes while a custom plugin paints the cells,\n// value labels, and colorbar directly on the canvas.\nconst heatmapPlugin = {\n  id: \"correlationHeatmap\",\n  afterDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    const xScale = scales.x;\n    const yScale = scales.y;\n    const cellW = xScale.getPixelForValue(1) - xScale.getPixelForValue(0);\n    const cellH = yScale.getPixelForValue(1) - yScale.getPixelForValue(0);\n\n    ctx.save();\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    ctx.font = \"600 20px sans-serif\";\n\n    for (const { x: col, y: row } of cellData) {\n      const value = correlationMatrix[row][col];\n      const rgb = divergingRgb(value);\n      const cx = xScale.getPixelForValue(col);\n      const cy = yScale.getPixelForValue(row);\n\n      ctx.fillStyle = rgbToCss(rgb);\n      ctx.fillRect(\n        cx - cellW / 2,\n        cy - Math.abs(cellH) / 2,\n        cellW,\n        Math.abs(cellH),\n      );\n      ctx.strokeStyle = t.pageBg;\n      ctx.lineWidth = 3;\n      ctx.strokeRect(\n        cx - cellW / 2,\n        cy - Math.abs(cellH) / 2,\n        cellW,\n        Math.abs(cellH),\n      );\n\n      ctx.fillStyle = textColorFor(rgb);\n      ctx.fillText(value.toFixed(2), cx, cy);\n    }\n\n    // --- Colorbar (fixed -1..1 range) --------------------------------------\n    const barW = 34;\n    const barX = chartArea.right + 70;\n    const barTop = chartArea.top;\n    const barBottom = chartArea.bottom;\n    const posColor = rgbToCss(divergingRgb(1));\n    const midColor = rgbToCss(divergingRgb(0));\n    const negColor = rgbToCss(divergingRgb(-1));\n\n    const gradient = ctx.createLinearGradient(0, barTop, 0, barBottom);\n    gradient.addColorStop(0, posColor);\n    gradient.addColorStop(0.5, midColor);\n    gradient.addColorStop(1, negColor);\n    ctx.fillStyle = gradient;\n    ctx.fillRect(barX, barTop, barW, barBottom - barTop);\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1.5;\n    ctx.strokeRect(barX, barTop, barW, barBottom - barTop);\n\n    ctx.font = \"14px sans-serif\";\n    ctx.fillStyle = t.inkSoft;\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"middle\";\n    ctx.fillText(\"1\", barX + barW + 10, barTop);\n    ctx.fillText(\"0\", barX + barW + 10, (barTop + barBottom) / 2);\n    ctx.fillText(\"-1\", barX + barW + 10, barBottom);\n\n    ctx.save();\n    ctx.translate(barX + barW + 46, (barTop + barBottom) / 2);\n    ctx.rotate(-Math.PI / 2);\n    ctx.textAlign = \"center\";\n    ctx.font = \"16px sans-serif\";\n    ctx.fillStyle = t.ink;\n    ctx.fillText(\"Correlation\", 0, 0);\n    ctx.restore();\n\n    ctx.restore();\n  },\n};\n\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        data: variables.map((_, i) => ({ x: i, y: i })),\n        pointStyle: false,\n        showLine: false,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: {\n      padding: { top: 10, right: 170, bottom: 10, left: 10 },\n    },\n    plugins: {\n      title: {\n        display: true,\n        text: \"heatmap-correlation · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 24 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"category\",\n        labels: variables,\n        bounds: \"ticks\",\n        offset: true,\n        position: \"bottom\",\n        grid: { display: false, drawTicks: false },\n        border: { display: false },\n        ticks: { color: t.inkSoft, font: { size: 15 } },\n      },\n      y: {\n        type: \"category\",\n        labels: variables,\n        bounds: \"ticks\",\n        offset: true,\n        grid: { display: false, drawTicks: false },\n        border: { display: false },\n        ticks: { color: t.inkSoft, font: { size: 15 } },\n      },\n    },\n  },\n  plugins: [heatmapPlugin],\n});\n"}