import { useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import type { BindingPartner, Member, PartnerName, StateInfo } from '../lib/api'; import { chainPath, fmtInt } from '../lib/format'; import { EmptyState, ExternalLink } from './ui'; /** Key of an observation's biological binding partners that are not copies of itself. */ export const SELF = 'self'; export const NO_PARTNER = '__none__'; const TYPE_LABEL: Record = { protein: 'Protein', dna: 'DNA', rna: 'RNA', hybrid: 'DNA/RNA', other: 'Polymer' }; /** Display label for a partner key ('self', a UniProt accession, 'dna', 'protein:'). */ export function partnerLabel(key: string, names?: Record): string { if (key === SELF) return 'Same protein'; if (key === NO_PARTNER) return 'No partner'; const n = names?.[key]; if (n?.description) return n.description; const i = key.indexOf(':'); if (i > 0) return key.slice(i + 1); return TYPE_LABEL[key] ?? key; } export function partnerType(key: string, names?: Record): string { if (key === SELF) return 'self'; return names?.[key]?.type ?? (key.includes(':') ? key.split(':')[0] : TYPE_LABEL[key] ? key : 'protein'); } /** Readable name for a partner chain; oligonucleotides described by their sequence become "DNA strand (21 nt)". */ export function polymerName(description: string | null, type: string, entity?: string | null): string { if (!description) return entity ? `Entity ${entity}` : TYPE_LABEL[type] ?? type; if ((type === 'dna' || type === 'rna' || type === 'hybrid') && /\*|^5'|\(5'/.test(description)) { const n = (description.match(/\*/g) ?? []).length; return `${TYPE_LABEL[type]} strand${n ? ` (${n} nt)` : ''}`; } return description; } function short(s: string, max = 28) { return s.length > max ? `${s.slice(0, max - 1)}…` : s; } export function sameSet(a: string[] = [], b: string[] = []) { if (a.length !== b.length) return false; const s = new Set(a); return b.every((k) => s.has(k)); } /** Compact partner chips; with `home`, partners absent from it are marked as gained and missing ones as lost. */ export function PartnerChips({ keys = [], names, home, max = 2, stacked = false, }: { keys?: string[]; names?: Record; home?: string[]; max?: number; /** one chip per line with the full name (for narrow table cells) */ stacked?: boolean; }) { const homeSet = home ? new Set(home) : null; const lost = home ? home.filter((k) => !keys.includes(k)) : []; const title = [ keys.length ? `Partners: ${keys.map((k) => partnerLabel(k, names)).join(', ')}` : 'No biological polymer partner', lost.length ? `Not bound here (bound in this chain): ${lost.map((k) => partnerLabel(k, names)).join(', ')}` : '', ] .filter(Boolean) .join('\n'); if (!keys.length && !lost.length) return —; return ( {keys.slice(0, max).map((k) => ( {stacked ? partnerLabel(k, names) : short(partnerLabel(k, names), 22)} ))} {keys.length > max && +{keys.length - max}} {!keys.length && none} {lost.length > 0 && −{lost.length}} ); } /* ───────────────────────────── chain page: every contacting polymer chain */ function kindOf(p: BindingPartner): { label: string; tone: string; hint: string } { if (p.biological) return p.same_entity ? { label: 'Same protein', tone: 'homo', hint: "Another copy of this chain's protein (same entity), in contact within biological assembly 1" } : { label: 'Bound partner', tone: 'bio', hint: 'A different polymer, in contact within biological assembly 1' }; if (p.n_res_target < 5) return { label: 'Marginal', tone: 'weak', hint: 'Fewer than 5 interface residues on this chain' }; return { label: 'Crystal contact', tone: 'weak', hint: p.via_symmetry ? 'Touches only a symmetry mate and is not part of biological assembly 1 (crystal packing)' : 'In the deposited coordinates but not in biological assembly 1 (likely crystal packing)', }; } function InterfaceStrip({ ranges, length }: { ranges: Array<[number, number]>; length: number }) { if (!length) return null; return ( (a === b ? a : `${a}-${b}`)).join(', ')}`}> {ranges.map(([a, b]) => ( ))} ); } function fmtRanges(r: Array<[number, number]>) { return r.map(([a, b]) => (a === b ? `${a}` : `${a}–${b}`)).join(', '); } export function BindingPartners({ partners, pdb, length, }: { partners: BindingPartner[]; pdb: string; length: number; }) { const nWeak = partners.filter((p) => !p.biological).length; const [showAll, setShowAll] = useState(false); const rows = showAll ? partners : partners.filter((p) => p.biological); if (!partners.length) return This chain touches no other protein, DNA or RNA chain within 4.5 Å.; return ( <> {rows.length === 0 ? (

No biological partner: every contact is a crystal contact or marginal.

) : (
{rows.map((p) => { const k = kindOf(p); return ( ); })}
Partner Chain Kind Interface Interface on this chain (SEQRES)
{TYPE_LABEL[p.type] ?? p.type} {polymerName(p.description, p.type, p.entity)} {p.uniprot && ( {p.uniprot.split(';').map((u) => ( {u} ))} )} {p.observation ? ( {pdb.toUpperCase()}.{p.chain} ) : ( `${pdb.toUpperCase()}.${p.chain}` )} {p.via_symmetry && ( {' '} (sym) )} {k.label} {p.n_res_target} / {p.n_res_partner ?? '—'}
)} {nWeak > 0 && ( )} ); } /* ───────────────────────────── sequence page: partners × states */ const MAX_ROWS = 12; const MAX_COLS = 14; export function PartnersByState({ members, states, names, selectedState, onSelectState, selectedPartner, onSelectPartner, }: { members: Member[]; states: StateInfo[]; names?: Record; selectedState: number | null; onSelectState: (s: number | null) => void; selectedPartner: string | null; onSelectPartner: (k: string | null) => void; }) { const { rows, cols, cell, perState } = useMemo(() => { const total = new Map(); const cell = new Map(); const perState = new Map(); for (const m of members) { const keys = m.partners?.length ? m.partners : [NO_PARTNER]; perState.set(m.state_id, (perState.get(m.state_id) ?? 0) + 1); for (const k of keys) { total.set(k, (total.get(k) ?? 0) + 1); cell.set(`${k}|${m.state_id}`, (cell.get(`${k}|${m.state_id}`) ?? 0) + 1); } } const rows = [...total.entries()].sort((a, b) => b[1] - a[1]).slice(0, MAX_ROWS); const cols = states.slice(0, MAX_COLS); return { rows, cols, cell, perState }; }, [members, states]); if (rows.length === 1 && rows[0][0] === NO_PARTNER) return

No observation of this sequence has a biological polymer partner.

; return (
{cols.map((s) => ( ))} {rows.map(([k, n]) => ( {cols.map((s) => { const c = cell.get(`${k}|${s.state_id}`) ?? 0; const frac = c / (perState.get(s.state_id) || 1); const level = c === 0 ? 0 : Math.min(5, 1 + Math.floor(frac * 5)); return ( ); })} ))}
Partner \ state All
{c > 0 ? c : ''} {fmtInt(n)}
{(states.length > MAX_COLS) && (

Showing the {MAX_COLS} largest of {states.length} states.

)}
); }