{"spec_id":"heatmap-correlation","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// heatmap-correlation: Correlation Matrix Heatmap\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-08-18\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: correlation matrix across outdoor air-quality sensor readings ---\n// The core Highcharts bundle has no heatmap/colorAxis-mapping module loaded,\n// so the grid is drawn cell-by-cell with the SVG renderer instead.\nconst LABELS = ['Temperature', 'Humidity', 'CO₂ Level', 'PM2.5', 'Noise Level', 'Wind Speed'];\nconst N = LABELS.length;\n\n// Fixed chart geometry (square canvas, harness-guaranteed 1200x1200 CSS px) —\n// a single source of truth for both the chart margin and the scatter-marker\n// radius below, so the invisible hover layer lines up with the drawn grid\n// without needing a runtime resync.\nconst CHART_MARGIN = [100, 60, 145, 185]; // [top, right, bottom, left]\nconst CELL_W = (window.ANYPLOT_SIZE.width - CHART_MARGIN[1] - CHART_MARGIN[3]) / N;\nconst CELL_H = (window.ANYPLOT_SIZE.height - CHART_MARGIN[0] - CHART_MARGIN[2]) / N;\nconst MARKER_RADIUS = Math.max(Math.min(CELL_W, CELL_H) / 2 - 3, 4);\n\n// Symmetric Pearson correlation matrix, diagonal = 1.00. Only the lower\n// triangle (col <= row) is drawn — masking the upper mirror image reduces\n// redundancy, per the spec's \"consider masking upper or lower triangle\" note.\nconst CORR = [\n  [1.00, -0.45, 0.15, 0.30, 0.05, -0.25],\n  [-0.45, 1.00, 0.20, -0.35, -0.05, 0.10],\n  [0.15, 0.20, 1.00, 0.55, 0.40, -0.50],\n  [0.30, -0.35, 0.55, 1.00, 0.35, -0.60],\n  [0.05, -0.05, 0.40, 0.35, 1.00, -0.15],\n  [-0.25, 0.10, -0.50, -0.60, -0.15, 1.00],\n];\n\n// Strongest links (off-diagonal, lower triangle) — the most positive and most\n// negative correlations get a heavier border so the matrix has a point of\n// emphasis beyond the raw grid of numbers.\nlet maxPos = { value: -Infinity, row: -1, col: -1 };\nlet maxNeg = { value: Infinity, row: -1, col: -1 };\nfor (let row = 0; row < N; row++) {\n  for (let col = 0; col < row; col++) {\n    const value = CORR[row][col];\n    if (value > maxPos.value) maxPos = { value, row, col };\n    if (value < maxNeg.value) maxNeg = { value, row, col };\n  }\n}\nfunction isStrongest(row, col) {\n  return (row === maxPos.row && col === maxPos.col) || (row === maxNeg.row && col === maxNeg.col);\n}\n\n// --- Color: imprint_div — negative -1 (red) .. 0 (page bg) .. +1 (blue) ----\nfunction hexToRgb(hex) {\n  return [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];\n}\n\nconst DIV_NEG = hexToRgb(t.div[0]); // #AE3030\nconst DIV_MID = hexToRgb(t.div[1]); // page bg, theme-adaptive\nconst DIV_POS = hexToRgb(t.div[2]); // #4467A3\n\nfunction lerp(a, b, f) {\n  return a + (b - a) * f;\n}\n\nfunction corrRgb(value) {\n  const a = value <= 0 ? DIV_NEG : DIV_MID;\n  const b = value <= 0 ? DIV_MID : DIV_POS;\n  const f = value <= 0 ? value + 1 : value;\n  return [Math.round(lerp(a[0], b[0], f)), Math.round(lerp(a[1], b[1], f)), Math.round(lerp(a[2], b[2], f))];\n}\n\nfunction corrFill(value) {\n  const [red, green, blue] = corrRgb(value);\n  return `rgb(${red},${green},${blue})`;\n}\n\n// Relative luminance — pick text color for maximum contrast against the fill\nfunction corrTextColor(value) {\n  const [red, green, blue] = corrRgb(value);\n  const lin = (c) => {\n    const v = c / 255;\n    return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n  };\n  const luminance = 0.2126 * lin(red) + 0.7152 * lin(green) + 0.0722 * lin(blue);\n  return luminance > 0.4 ? '#1A1A17' : '#F0EFE8';\n}\n\n// --- Title (fontsize scaled off the 67-char baseline) ----------------------\nconst TITLE_TEXT = 'Air-Quality Sensor Correlations · heatmap-correlation · javascript · highcharts · anyplot.ai';\nconst TITLE_FS = Math.max(Math.round(22 * Math.min(1, 67 / TITLE_TEXT.length)), 14);\n\nconst drawn = [];\n\nfunction clearDrawn() {\n  drawn.forEach((el) => {\n    try {\n      el.destroy();\n    } catch (_err) {\n      // already removed\n    }\n  });\n  drawn.length = 0;\n}\n\nfunction drawAll() {\n  const chart = this;\n  clearDrawn();\n  const r = chart.renderer;\n\n  const cellW = chart.plotWidth / N;\n  const cellH = chart.plotHeight / N;\n\n  // Lower-triangle + diagonal cells only.\n  for (let row = 0; row < N; row++) {\n    for (let col = 0; col <= row; col++) {\n      const value = CORR[row][col];\n      const x = chart.plotLeft + col * cellW;\n      const y = chart.plotTop + row * cellH;\n      const onDiagonal = row === col;\n      const emphasize = isStrongest(row, col);\n      const fill = onDiagonal ? t.elevatedBg : corrFill(value);\n      const textColor = onDiagonal ? t.inkSoft : corrTextColor(value);\n\n      drawn.push(\n        r\n          .rect(x + 1, y + 1, cellW - 2, cellH - 2, 3)\n          .attr({\n            fill,\n            stroke: emphasize ? t.ink : onDiagonal ? t.inkSoft : 'none',\n            'stroke-width': emphasize ? 2 : onDiagonal ? 1 : 0,\n            zIndex: 2,\n          })\n          .add()\n      );\n      drawn.push(\n        r\n          .text(value.toFixed(2), x + cellW / 2, y + cellH / 2 + 6)\n          .attr({ align: 'center', zIndex: 2 })\n          .css({ color: textColor, fontSize: '18px', fontWeight: onDiagonal ? '500' : '600' })\n          .add()\n      );\n    }\n  }\n\n  // Row labels (left, right-aligned against the grid).\n  LABELS.forEach((lbl, row) => {\n    const cy = chart.plotTop + (row + 0.5) * cellH + 5;\n    drawn.push(\n      r\n        .text(lbl, chart.plotLeft - 12, cy)\n        .attr({ align: 'right', zIndex: 2 })\n        .css({ color: t.inkSoft, fontSize: '15px' })\n        .add()\n    );\n  });\n\n  // Column labels (bottom, rotated) — every column keeps at least its\n  // diagonal cell, so all N labels stay anchored under real grid content.\n  LABELS.forEach((lbl, col) => {\n    const cx = chart.plotLeft + (col + 0.5) * cellW;\n    drawn.push(\n      r\n        .text(lbl, cx, chart.plotTop + chart.plotHeight + 16)\n        .attr({ align: 'right', rotation: -40, zIndex: 2 })\n        .css({ color: t.inkSoft, fontSize: '15px' })\n        .add()\n    );\n  });\n\n  // Diverging colorbar, placed inside the freed upper-triangle void. For\n  // N=6, rows {0,1} x cols {3,4,5} is always empty (minCol 3 > maxRow 1).\n  const barLeft = chart.plotLeft + 3 * cellW + cellW * 0.3;\n  const barWidth = 3 * cellW * 0.7;\n  const barTop = chart.plotTop + 0.55 * cellH;\n  const barHeight = 22;\n  const segments = 60;\n  const segW = barWidth / segments;\n\n  for (let i = 0; i < segments; i++) {\n    const value = -1 + (2 * i) / (segments - 1);\n    drawn.push(\n      r\n        .rect(barLeft + i * segW, barTop, segW + 0.5, barHeight)\n        .attr({ fill: corrFill(value), zIndex: 2 })\n        .add()\n    );\n  }\n  drawn.push(\n    r\n      .rect(barLeft, barTop, barWidth, barHeight)\n      .attr({ fill: 'none', stroke: t.inkSoft, 'stroke-width': 1, zIndex: 2 })\n      .add()\n  );\n  [\n    [-1, 0],\n    [0, 0.5],\n    [1, 1],\n  ].forEach(([value, frac]) => {\n    drawn.push(\n      r\n        .text(value.toFixed(0), barLeft + frac * barWidth, barTop + barHeight + 20)\n        .attr({ align: 'center', zIndex: 2 })\n        .css({ color: t.inkSoft, fontSize: '13px' })\n        .add()\n    );\n  });\n  drawn.push(\n    r\n      .text('Correlation (r)', barLeft + barWidth / 2, barTop - 12)\n      .attr({ align: 'center', zIndex: 2 })\n      .css({ color: t.inkSoft, fontSize: '14px', fontWeight: '500' })\n      .add()\n  );\n}\n\n// Invisible scatter layer aligned to each drawn lower-triangle cell so\n// hovering exposes a real Highcharts tooltip — the core bundle has no\n// heatmap/colorAxis module, but a matched-axis scatter series recovers\n// native hover interactivity without disturbing the hand-drawn grid above it.\nconst cellPoints = [];\nfor (let row = 0; row < N; row++) {\n  for (let col = 0; col <= row; col++) {\n    cellPoints.push({ x: col, y: row, corr: CORR[row][col], rowLabel: LABELS[row], colLabel: LABELS[col] });\n  }\n}\n\nHighcharts.chart('container', {\n  chart: {\n    backgroundColor: 'transparent',\n    animation: false,\n    style: { fontFamily: 'inherit' },\n    margin: CHART_MARGIN,\n    events: { load: drawAll, redraw: drawAll },\n  },\n  credits: { enabled: false },\n  title: {\n    text: TITLE_TEXT,\n    style: { color: t.ink, fontSize: TITLE_FS + 'px', fontWeight: '600' },\n  },\n  subtitle: {\n    text: 'Pairwise Pearson correlation across 6 outdoor sensor readings (lower triangle shown)',\n    style: { color: t.inkSoft, fontSize: '14px' },\n  },\n  xAxis: { visible: false, min: -0.5, max: N - 0.5 },\n  yAxis: { visible: false, gridLineWidth: 0, min: -0.5, max: N - 0.5, reversed: true },\n  legend: { enabled: false },\n  tooltip: {\n    enabled: true,\n    backgroundColor: t.elevatedBg,\n    borderColor: t.inkSoft,\n    borderRadius: 6,\n    style: { color: t.ink, fontSize: '13px' },\n    formatter: function () {\n      const p = this.point;\n      return `<b>${p.rowLabel} × ${p.colLabel}</b><br/>r = ${p.corr.toFixed(2)}`;\n    },\n  },\n  plotOptions: {\n    series: { animation: false },\n    scatter: {\n      enableMouseTracking: true,\n      stickyTracking: false,\n      marker: {\n        enabled: true,\n        symbol: 'circle',\n        radius: MARKER_RADIUS,\n        fillColor: 'rgba(0,0,0,0.001)',\n        lineWidth: 0,\n        states: { hover: { enabled: false } },\n      },\n    },\n  },\n  series: [\n    {\n      type: 'scatter',\n      name: 'Correlation',\n      data: cellPoints,\n    },\n  ],\n});\n"}