import { useMemo, useState } from 'react'; import type { Member, StateInfo, StatePair } from '../lib/api'; import { FIDELITY_INFO, fidelityKey, similarityColor } from '../lib/fidelity'; import { chainLabel, fmtNum } from '../lib/format'; import { Tooltip, useWidth, type TipState } from './charts'; import { Swatch } from './ui'; const MAX_TILES = 40; const MAX_DOTS = 120; /** * One tile per conformational state; each dot is one observation, coloured * by ligand state. Clicking a tile selects the state. */ export function StateLandscape({ states, members, selected, onSelect, highlight, }: { states: StateInfo[]; members: Member[]; selected: number | null; onSelect: (s: number | null) => void; highlight?: { pdb_id: string; auth_asym_id: string } | null; }) { const byState = useMemo(() => { const m = new Map(); for (const mem of members) { const arr = m.get(mem.state_id) ?? []; arr.push(mem); m.set(mem.state_id, arr); } return m; }, [members]); const shown = states.slice(0, MAX_TILES); const rest = states.slice(MAX_TILES); const restObs = rest.reduce((s, x) => s + x.n_members, 0); return (
{shown.map((st) => { const mem = byState.get(st.state_id) ?? []; const holo = mem.filter((m) => m.binding_status === 'holo').length; const isSel = selected === st.state_id; const hasHi = highlight && mem.some((m) => m.pdb_id === highlight.pdb_id && m.auth_asym_id === highlight.auth_asym_id); return ( ); })}
  • Apo observation
  • Holo observation
  • {highlight && (
  • This chain
  • )}
{rest.length > 0 && ( + {rest.length.toLocaleString()} smaller states ({restObs.toLocaleString()} observations) not shown )}
); } /** Mean pair similarity between states (diagonal = within-state). */ export function StateHeatmap({ states, pairs, selected, onSelect, }: { states: StateInfo[]; pairs: StatePair[]; selected: number | null; onSelect: (s: number | null) => void; }) { const [ref, width] = useWidth(); const [tip, setTip] = useState(null); const [hover, setHover] = useState<[number, number] | null>(null); const order = states.slice(0, MAX_TILES).map((s) => s.state_id); const lookup = useMemo(() => { const m = new Map(); for (const p of pairs) { m.set(`${p.a}|${p.b}`, p); m.set(`${p.b}|${p.a}`, p); } return m; }, [pairs]); const n = order.length; const label = 34; const cell = n ? Math.max(8, Math.min(34, Math.floor((Math.min(width, 560) - label - 4) / n))) : 0; const size = label + n * cell; const showLabels = cell >= 14; if (n < 2) return null; return (
{width > 0 && ( {order.map((s, i) => ( {(showLabels || i % 5 === 0) && ( <> {s} {s} )} ))} {order.map((a, i) => order.map((b, j) => { const p = lookup.get(`${a}|${b}`); const dim = (selected != null && a !== selected && b !== selected) || (hover && hover[0] !== i && hover[1] !== j); return ( onSelect(selected === b ? null : b)} onMouseMove={(e) => { const r = (e.currentTarget.ownerSVGElement as SVGSVGElement).getBoundingClientRect(); setHover([i, j]); setTip({ x: e.clientX - r.left, y: e.clientY - r.top, content: ( <>
{a === b ? `Within state ${a}` : `State ${a} ↔ state ${b}`}
Mean similarity {fmtNum(p?.similarity, 3)}
{a !== b && p && (
Fidelity {FIDELITY_INFO[fidelityKey(p.fidelity)].label}
)}
{p ? `${p.n_pairs.toLocaleString()} observation pairs` : 'No pairs'}
), }); }} onMouseLeave={() => { setTip(null); setHover(null); }} /> ); }), )} )}
Mean similarity {[ ['< 0.50', 0.4], ['0.50', 0.6], ['0.70', 0.8], ['0.85', 0.9], ['0.95', 0.96], ['0.98+', 0.99], ].map(([l, v]) => ( {l} ))}
); } export function memberKey(m: { pdb_id: string; auth_asym_id: string }) { return chainLabel(m.pdb_id, m.auth_asym_id); }