// Cross-layer heatmap — hero element. Plotly under the hood. // Click any cell -> onCellClick({layer, component}) const { useRef, useEffect, useMemo, useState } = React; function Heatmap({ matrix, behaviorMatrix, behaviorLabel, onCellClick, selectedCell, palette }) { const ref = useRef(null); const [hover, setHover] = useState(null); const componentLabels = AtlasData.COMPONENTS.map(c => c.label); const layerLabels = Array.from({ length: AtlasData.N_LAYERS }, (_, i) => `L${i}`); // Build z matrix — if a behavior overlay is active, blend const z = useMemo(() => { if (!behaviorMatrix) return matrix; // emphasize behavior — multiplicative blend, then re-normalize const out = []; let max = 0; for (let l = 0; l < matrix.length; l++) { const row = []; for (let c = 0; c < (matrix[0] || []).length; c++) { const a = matrix[l][c]; const b = behaviorMatrix[l] ? behaviorMatrix[l][c] : 0; if (!Number.isFinite(a) && !Number.isFinite(b)) { row.push(NaN); continue; } const v = 0.25 * (Number.isFinite(a) ? a : 0) + 0.95 * (Number.isFinite(b) ? b : 0); row.push(v); if (v > max) max = v; } out.push(row); } return out.map(r => r.map(v => Number.isFinite(v) ? v / (max || 1) : NaN)); }, [matrix, behaviorMatrix]); useEffect(() => { if (!ref.current || !window.Plotly) return; const colorscale = palette; const data = [{ z, x: componentLabels, y: layerLabels, type: 'heatmap', colorscale, zmin: 0, zmax: 1, xgap: 3, ygap: 3, hoverongaps: false, hovertemplate: '%{y} · %{x}
F-stat %{z:.2f}', showscale: false, connectgaps: false, }]; const layout = { margin: { l: 56, r: 18, t: 8, b: 60 }, paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', font: { family: 'Fraunces, Georgia, serif', color: '#5b454c', size: 12 }, xaxis: { side: 'bottom', tickangle: -32, automargin: false, tickfont: { family: 'JetBrains Mono, ui-monospace', size: 11, color: '#84686c' }, showgrid: false, zeroline: false, ticks: '', }, yaxis: { autorange: 'reversed', automargin: false, tickfont: { family: 'JetBrains Mono, ui-monospace', size: 10, color: '#84686c' }, tickmode: 'array', tickvals: layerLabels.filter((_, i) => i % 2 === 0), showgrid: false, zeroline: false, ticks: '', }, }; const config = { displayModeBar: false, responsive: true }; Plotly.react(ref.current, data, layout, config); const node = ref.current; const clickHandler = (ev) => { const p = ev.points && ev.points[0]; if (!p) return; const li = layerLabels.indexOf(p.y); const ci = componentLabels.indexOf(p.x); onCellClick && onCellClick({ layer: li, component: AtlasData.COMPONENTS[ci].id }); }; const hoverHandler = (ev) => { const p = ev.points && ev.points[0]; if (!p) return; setHover({ layer: layerLabels.indexOf(p.y), component: AtlasData.COMPONENTS[componentLabels.indexOf(p.x)].id, v: p.z }); }; const unhoverHandler = () => setHover(null); node.on('plotly_click', clickHandler); node.on('plotly_hover', hoverHandler); node.on('plotly_unhover', unhoverHandler); return () => { try { node.removeAllListeners && node.removeAllListeners('plotly_click'); } catch {} }; }, [z, palette]); // Selected-cell outline overlay const cellOverlay = useMemo(() => { if (!selectedCell) return null; const ci = AtlasData.COMPONENTS.findIndex(c => c.id === selectedCell.component); if (ci < 0) return null; const cols = AtlasData.COMPONENTS.length; const rows = AtlasData.N_LAYERS; // approximate cell position based on plot area return { ci, li: selectedCell.layer, cols, rows }; }, [selectedCell]); return (
{cellOverlay && (
)}
{hover ? ( <> L{hover.layer} · {hover.component} · F-stat {Number.isFinite(hover.v) ? hover.v.toFixed(2) : '—'} {behaviorLabel && (<>·{behaviorLabel} overlay)} ) : ( Hover a cell — click to drill into a layer · component )}
); } window.Heatmap = Heatmap;