Download frontend/src/components/Transitions.tsx from omaib/MuSProt: direct link, hf CLI and curl.
- Browser
- Download file 27.5 kB
-
https://huggingface.co/spaces/omaib/MuSProt/resolve/main/frontend/src/components/Transitions.tsx
- Command line
-
hf download hf://spaces/omaib/MuSProt/frontend/src/components/Transitions.tsx
-
curl -L -o Transitions.tsx https://huggingface.co/spaces/omaib/MuSProt/resolve/main/frontend/src/components/Transitions.tsx
27.5 kB
| import { useMemo, useState } from 'react'; | |
| import { Box } from 'lucide-react'; | |
| import { ENERGY_TERMS, type EnergyTerm, type OrderChange, type PartnerName, type Transition } from '../lib/api'; | |
| import { FIDELITY_INFO, FIDELITY_ORDER, fidelityColor, fidelityKey } from '../lib/fidelity'; | |
| import { chainLabel, fmtNum, fmtSigned, methodOf, methodShort } from '../lib/format'; | |
| import { niceTicks, Tooltip, useWidth, type TipState } from './charts'; | |
| import { ChainLink, EmptyState, FidelityBadge, LigandBadge, Pagination, Segmented, Swatch } from './ui'; | |
| import { PartnerChips, sameSet } from './Partners'; | |
| export type Scope = 'cross' | 'same' | 'all'; | |
| /* βββββββββββββββββββββββββββββ neighbourhood scatter */ | |
| export function TransitionScatter({ | |
| items, | |
| homeState, | |
| selected, | |
| onSelect, | |
| }: { | |
| items: Transition[]; | |
| homeState: number | null; | |
| selected: string | null; | |
| onSelect: (t: Transition) => void; | |
| }) { | |
| const [ref, width] = useWidth<HTMLDivElement>(); | |
| const [tip, setTip] = useState<TipState | null>(null); | |
| const pts = items.filter((t) => t.similarity != null && t.rmsd != null); | |
| const height = 300; | |
| const m = { top: 16, right: 16, bottom: 44, left: 52 }; | |
| const iw = Math.max(0, width - m.left - m.right); | |
| const ih = height - m.top - m.bottom; | |
| const xMin = Math.min(0.3, ...pts.map((p) => p.similarity!)) - 0.02; | |
| const xMax = 1; | |
| const yTicks = niceTicks(Math.max(2, ...pts.map((p) => p.rmsd!))); | |
| const yMax = yTicks[yTicks.length - 1]; | |
| const x = (v: number) => ((v - xMin) / (xMax - xMin)) * iw; | |
| const y = (v: number) => ih - (v / yMax) * ih; | |
| const xTicks = [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1].filter((t) => t >= xMin); | |
| // draw larger changes last so they sit on top | |
| const ordered = [...pts].sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0)); | |
| return ( | |
| <div className="chart scatter" ref={ref}> | |
| {width > 0 && ( | |
| <svg width={width} height={height} role="img" aria-label="Similarity versus RMSD for every alternative"> | |
| <g transform={`translate(${m.left},${m.top})`}> | |
| {[0.5, 0.7, 0.95].filter((t) => t > xMin).map((t) => ( | |
| <g key={t}> | |
| <line x1={x(t)} x2={x(t)} y1={0} y2={ih} className="threshold" /> | |
| </g> | |
| ))} | |
| {yTicks.map((t) => ( | |
| <g key={t}> | |
| <line x1={0} x2={iw} y1={y(t)} y2={y(t)} className={t === 0 ? 'axis-line' : 'grid-line'} /> | |
| <text x={-8} y={y(t)} dy="0.32em" textAnchor="end" className="tick"> | |
| {Number.isInteger(t) ? t : t.toFixed(1)} | |
| </text> | |
| </g> | |
| ))} | |
| {xTicks.map((t) => ( | |
| <text key={t} x={x(t)} y={ih + 16} textAnchor="middle" className="tick"> | |
| {t.toFixed(1)} | |
| </text> | |
| ))} | |
| <text x={iw / 2} y={ih + 36} textAnchor="middle" className="axis-title"> | |
| Pair similarity (aligned core) β more similar | |
| </text> | |
| <text transform={`translate(${-38},${ih / 2}) rotate(-90)`} textAnchor="middle" className="axis-title"> | |
| RMSD (Γ ) | |
| </text> | |
| {ordered.map((p) => { | |
| const key = chainLabel(p.pdb_id, p.auth_asym_id); | |
| const same = p.state_id === homeState; | |
| const isSel = key === selected; | |
| return ( | |
| <circle | |
| key={key} | |
| cx={x(p.similarity!)} | |
| cy={y(p.rmsd!)} | |
| r={isSel ? 7 : 4.5} | |
| fill={same ? 'var(--surface)' : fidelityColor(p.fidelity)} | |
| stroke={same ? 'var(--faint)' : 'var(--surface)'} | |
| strokeWidth={same ? 1.5 : 1.5} | |
| className={`scatter__pt ${isSel ? 'is-sel' : ''}`} | |
| onClick={() => onSelect(p)} | |
| onMouseMove={(e) => { | |
| const r = (e.currentTarget.ownerSVGElement as SVGSVGElement).getBoundingClientRect(); | |
| setTip({ | |
| x: e.clientX - r.left, | |
| y: e.clientY - r.top, | |
| content: ( | |
| <> | |
| <div className="chart-tip__title mono">{key}</div> | |
| <div className="chart-tip__row"> | |
| <span>State</span> | |
| <b>{p.state_id ?? 'β'}{same ? ' (same)' : ''}</b> | |
| </div> | |
| <div className="chart-tip__row"> | |
| <span>Similarity</span> | |
| <b className="tabular">{fmtNum(p.similarity, 3)}</b> | |
| </div> | |
| <div className="chart-tip__row"> | |
| <span>RMSD</span> | |
| <b className="tabular">{fmtNum(p.rmsd, 2)} Γ </b> | |
| </div> | |
| <div className="chart-tip__row"> | |
| <span>Fidelity</span> | |
| <b>{FIDELITY_INFO[fidelityKey(p.fidelity)].label}</b> | |
| </div> | |
| <div className="chart-tip__foot">Click to compare</div> | |
| </> | |
| ), | |
| }); | |
| }} | |
| onMouseLeave={() => setTip(null)} | |
| /> | |
| ); | |
| })} | |
| </g> | |
| </svg> | |
| )} | |
| <Tooltip tip={tip} /> | |
| <ul className="legend"> | |
| <li> | |
| <span className="swatch swatch--dot swatch--ring" /> Same state | |
| </li> | |
| {FIDELITY_ORDER.filter((f) => f !== 'identical').map((f) => ( | |
| <li key={f}> | |
| <Swatch color={fidelityColor(f)} shape="dot" /> {FIDELITY_INFO[f].label} fidelity | |
| </li> | |
| ))} | |
| <li className="muted">Lines at 0.50 / 0.70 / 0.95 mark the fidelity bins</li> | |
| </ul> | |
| </div> | |
| ); | |
| } | |
| /* βββββββββββββββββββββββββββββ table */ | |
| type SortKey = 'similarity' | 'tm_aln' | 'rmsd' | 'coverage' | 'state' | 'resolution' | 'date' | 'order' | EnergyTerm; | |
| type EnergyMode = 'z' | 'delta'; | |
| const PAGE = 25; | |
| /** |z| class: noise (< 1.5), possible (~2), real (>= 3). */ | |
| export function zClass(z: number | null | undefined): string { | |
| if (z == null) return ''; | |
| const a = Math.abs(z); | |
| const dir = z > 0 ? 'up' : 'down'; | |
| return a >= 3 ? `z--${dir} z--strong` : a >= 2 ? `z--${dir} z--weak` : a >= 1.5 ? `z--${dir} z--hint` : 'z--noise'; | |
| } | |
| function sortValue(t: Transition, k: SortKey, mode: EnergyMode = 'delta'): number | string | null { | |
| switch (k) { | |
| case 'similarity': | |
| return t.similarity; | |
| case 'tm_aln': | |
| return t.tm_aln; | |
| case 'rmsd': | |
| return t.rmsd; | |
| case 'coverage': | |
| return t.coverage; | |
| case 'state': | |
| return t.state_id; | |
| case 'resolution': | |
| return t.resolution ?? null; | |
| case 'date': | |
| return t.release_date ?? null; | |
| case 'order': | |
| return t.order.valid ? (t.order.n_ordered ?? 0) + (t.order.n_disordered ?? 0) : null; | |
| default: | |
| return mode === 'z' ? t.z?.[k] ?? null : t.delta[k]; | |
| } | |
| } | |
| export function TransitionsTable({ | |
| items, | |
| homeState, | |
| selected, | |
| onSelect, | |
| scope, | |
| onScope, | |
| onView, | |
| homePartners, | |
| partnerNames, | |
| }: { | |
| items: Transition[]; | |
| homeState: number | null; | |
| selected: string | null; | |
| onSelect: (t: Transition) => void; | |
| scope: Scope; | |
| onScope: (s: Scope) => void; | |
| /** open the superposed structure comparison for this partner */ | |
| onView?: (t: Transition) => void; | |
| /** this chain's biological binding partners, to flag alternatives bound to something else */ | |
| homePartners?: string[]; | |
| partnerNames?: Record<string, PartnerName>; | |
| }) { | |
| const [fid, setFid] = useState<string>('all'); | |
| const hasZ = items.some((t) => t.z != null); | |
| const [energy, setEnergy] = useState<EnergyMode>('z'); | |
| const mode: EnergyMode = hasZ ? energy : 'delta'; | |
| const [bound, setBound] = useState<'all' | 'same' | 'diff'>('all'); | |
| const hasPartners = homePartners != null && items.some((t) => t.partners != null); | |
| const [sort, setSort] = useState<{ k: SortKey; dir: 1 | -1 }>({ k: 'similarity', dir: 1 }); | |
| const [page, setPage] = useState(0); | |
| const nSame = items.filter((t) => t.state_id === homeState).length; | |
| const scoped = useMemo( | |
| () => | |
| items.filter((t) => | |
| scope === 'all' ? true : scope === 'same' ? t.state_id === homeState : t.state_id !== homeState, | |
| ), | |
| [items, scope, homeState], | |
| ); | |
| const nSamePartners = useMemo( | |
| () => scoped.filter((t) => sameSet(t.partners, homePartners)).length, | |
| [scoped, homePartners], | |
| ); | |
| const rows = useMemo(() => { | |
| let r = fid === 'all' ? scoped : scoped.filter((t) => fidelityKey(t.fidelity) === fid); | |
| if (bound !== 'all') r = r.filter((t) => sameSet(t.partners, homePartners) === (bound === 'same')); | |
| return [...r].sort((a, b) => { | |
| const va = sortValue(a, sort.k, mode); | |
| const vb = sortValue(b, sort.k, mode); | |
| if (va == null) return 1; | |
| if (vb == null) return -1; | |
| return (va < vb ? -1 : va > vb ? 1 : 0) * sort.dir; | |
| }); | |
| }, [scoped, fid, sort, bound, homePartners, mode]); | |
| const fidCounts = useMemo(() => { | |
| const c: Record<string, number> = {}; | |
| for (const t of scoped) c[fidelityKey(t.fidelity)] = (c[fidelityKey(t.fidelity)] ?? 0) + 1; | |
| return c; | |
| }, [scoped]); | |
| const th = (k: SortKey, label: string, num = true) => ( | |
| <th className={num ? 'num' : ''} aria-sort={sort.k === k ? (sort.dir === 1 ? 'ascending' : 'descending') : 'none'}> | |
| <button | |
| type="button" | |
| className={`th-sort ${sort.k === k ? 'is-active' : ''}`} | |
| onClick={() => { | |
| setSort((s) => ({ k, dir: s.k === k ? ((-s.dir) as 1 | -1) : 1 })); | |
| setPage(0); | |
| }} | |
| > | |
| {label} | |
| <span className="th-sort__arrow" aria-hidden> | |
| {sort.k === k ? (sort.dir === 1 ? 'β' : 'β') : 'β'} | |
| </span> | |
| </button> | |
| </th> | |
| ); | |
| const fidOptions = [ | |
| { value: 'all', label: 'Any fidelity', count: scoped.length }, | |
| ...[...FIDELITY_ORDER, 'NA'] | |
| .filter((f) => fidCounts[f]) | |
| .map((f) => ({ value: f, label: FIDELITY_INFO[f].label, count: fidCounts[f] })), | |
| ]; | |
| return ( | |
| <> | |
| <div className="table-toolbar table-toolbar--wrap"> | |
| <Segmented | |
| label="Alternative scope" | |
| value={scope} | |
| onChange={(v) => { | |
| onScope(v); | |
| setFid('all'); | |
| setPage(0); | |
| }} | |
| options={[ | |
| { value: 'cross', label: 'Other states', count: items.length - nSame }, | |
| { value: 'same', label: 'Same state', count: nSame }, | |
| { value: 'all', label: 'All', count: items.length }, | |
| ]} | |
| /> | |
| <Segmented | |
| label="Fidelity" | |
| value={fid} | |
| onChange={(v) => { | |
| setFid(v); | |
| setPage(0); | |
| }} | |
| options={fidOptions} | |
| /> | |
| {hasZ && ( | |
| <Segmented<EnergyMode> | |
| label="Energy change" | |
| value={energy} | |
| onChange={setEnergy} | |
| options={[ | |
| { value: 'z', label: 'z-score' }, | |
| { value: 'delta', label: 'Raw Ξ' }, | |
| ]} | |
| /> | |
| )} | |
| {hasPartners && ( | |
| <Segmented | |
| label="Binding partners" | |
| value={bound} | |
| onChange={(v) => { | |
| setBound(v); | |
| setPage(0); | |
| }} | |
| options={[ | |
| { value: 'all', label: 'Any partners', count: scoped.length }, | |
| { value: 'same', label: 'Same as this chain', count: nSamePartners }, | |
| { value: 'diff', label: 'Different', count: scoped.length - nSamePartners }, | |
| ]} | |
| /> | |
| )} | |
| </div> | |
| {rows.length === 0 ? ( | |
| <EmptyState title="No alternatives in this view" /> | |
| ) : ( | |
| <div className="table-wrap"> | |
| <table className="table table--dense table--rows table--sticky-first"> | |
| <thead> | |
| <tr> | |
| <th>Alternative</th> | |
| {th('state', 'State')} | |
| <th>Fidelity</th> | |
| {th('similarity', 'Similarity')} | |
| {th('tm_aln', 'TM (aln)')} | |
| {th('rmsd', 'RMSD Γ ')} | |
| {th('coverage', 'Coverage')} | |
| {th('order', 'Order Ξ')} | |
| {ENERGY_TERMS.map((e) => th(e, mode === 'z' ? `z ${e}` : `Ξ${e}`))} | |
| <th>Method</th> | |
| {th('resolution', 'Res. Γ ')} | |
| <th>Ligand</th> | |
| {hasPartners && <th title="Biological polymer partners of the alternative; outlined = not bound by this chain, βn = partners of this chain missing here">Binding partners</th>} | |
| {th('date', 'Released')} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {rows.slice(page * PAGE, (page + 1) * PAGE).map((t) => { | |
| const key = chainLabel(t.pdb_id, t.auth_asym_id); | |
| return ( | |
| <tr | |
| key={key} | |
| className={`is-clickable ${key === selected ? 'is-selected' : ''}`} | |
| onClick={(e) => { | |
| if ((e.target as HTMLElement).closest('a')) return; | |
| onSelect(t); | |
| }} | |
| > | |
| <td> | |
| <span className="partner-cell"> | |
| <ChainLink pdb={t.pdb_id} chain={t.auth_asym_id} strong /> | |
| {onView && ( | |
| <button | |
| type="button" | |
| className="btn-3d" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| onView(t); | |
| }} | |
| title="View both structures superposed, coloured by per-residue deviation" | |
| > | |
| <Box aria-hidden /> 3D | |
| </button> | |
| )} | |
| </span> | |
| </td> | |
| <td className="num tabular">{t.state_id ?? 'β'}</td> | |
| <td> | |
| <FidelityBadge value={t.fidelity} /> | |
| </td> | |
| <td className="num tabular strong">{fmtNum(t.similarity, 3)}</td> | |
| <td className="num tabular">{fmtNum(t.tm_aln, 3)}</td> | |
| <td className="num tabular">{fmtNum(t.rmsd, 2)}</td> | |
| <td className="num tabular">{fmtNum(t.coverage, 2)}</td> | |
| <td className="num"> | |
| <OrderCell order={t.order} /> | |
| </td> | |
| {ENERGY_TERMS.map((e) => { | |
| const v = t.delta[e]; | |
| if (mode === 'z') { | |
| const z = t.z?.[e] ?? null; | |
| return ( | |
| <td key={e} className="num tabular" title={v == null ? undefined : `Ξ${e} ${fmtSigned(v, 1)}`}> | |
| <span className={`zval ${zClass(z)}`}>{fmtSigned(z, 1)}</span> | |
| </td> | |
| ); | |
| } | |
| return ( | |
| <td key={e} className={`num tabular delta ${v == null ? '' : v > 0 ? 'delta--up' : v < 0 ? 'delta--down' : ''}`}> | |
| {fmtSigned(v, 1)} | |
| </td> | |
| ); | |
| })} | |
| <td>{methodShort(methodOf(t))}</td> | |
| <td className="num tabular">{fmtNum(t.resolution, 2)}</td> | |
| <td> | |
| <LigandBadge status={t.binding_status} /> | |
| </td> | |
| {hasPartners && ( | |
| <td> | |
| <PartnerChips keys={t.partners} names={partnerNames} home={homePartners} /> | |
| </td> | |
| )} | |
| <td className="num tabular">{t.release_date ?? 'β'}</td> | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| <Pagination page={page} pageSize={PAGE} total={rows.length} pageCount={Math.ceil(rows.length / PAGE)} onPage={setPage} /> | |
| </> | |
| ); | |
| } | |
| /* βββββββββββββββββββββββββββββ energy deltas (diverging bars) */ | |
| const Z_AXIS = 6; // bars span |z| <= 6, longer ones are clipped | |
| /** | |
| * Energy change per score. With z scores the bars share one scale (the | |
| * protein's own noise floor), with guides at |z| = 2 and 3; otherwise each raw | |
| * delta is scaled to the largest one. | |
| */ | |
| export function DeltaBars({ delta, z }: { delta: Record<EnergyTerm, number | null>; z?: Record<EnergyTerm, number | null> | null }) { | |
| if (z) { | |
| return ( | |
| <ul className="deltas deltas--z"> | |
| {ENERGY_TERMS.map((e) => { | |
| const v = z[e]; | |
| const w = v == null ? 0 : (Math.min(Math.abs(v), Z_AXIS) / Z_AXIS) * 50; | |
| return ( | |
| <li key={e}> | |
| <span className="deltas__label">{e}</span> | |
| <span className="deltas__track"> | |
| {[-3, -2, 2, 3].map((g) => ( | |
| <span key={g} className={`deltas__guide ${Math.abs(g) === 3 ? 'is-3' : ''}`} style={{ left: `${50 + (g / Z_AXIS) * 50}%` }} /> | |
| ))} | |
| <span className="deltas__mid" /> | |
| {v != null && ( | |
| <span | |
| className={`deltas__bar ${v > 0 ? 'is-up' : 'is-down'} ${Math.abs(v) < 2 ? 'is-faint' : ''}`} | |
| style={v > 0 ? { left: '50%', width: `${w}%` } : { right: '50%', width: `${w}%` }} | |
| /> | |
| )} | |
| </span> | |
| <span className="deltas__val tabular"> | |
| <span className={`zval ${zClass(v)}`}>{v == null ? 'β' : `z ${fmtSigned(v, 1)}`}</span> | |
| <small className="muted">{delta[e] == null ? '' : `Ξ ${fmtSigned(delta[e], 1)}`}</small> | |
| </span> | |
| </li> | |
| ); | |
| })} | |
| </ul> | |
| ); | |
| } | |
| const vals = ENERGY_TERMS.map((e) => delta[e]).filter((v): v is number => v != null); | |
| const max = Math.max(1, ...vals.map(Math.abs)); | |
| return ( | |
| <ul className="deltas"> | |
| {ENERGY_TERMS.map((e) => { | |
| const v = delta[e]; | |
| const w = v == null ? 0 : (Math.abs(v) / max) * 50; | |
| return ( | |
| <li key={e}> | |
| <span className="deltas__label">Ξ{e}</span> | |
| <span className="deltas__track"> | |
| <span className="deltas__mid" /> | |
| {v != null && ( | |
| <span | |
| className={`deltas__bar ${v > 0 ? 'is-up' : 'is-down'}`} | |
| style={v > 0 ? { left: '50%', width: `${w}%` } : { right: '50%', width: `${w}%` }} | |
| /> | |
| )} | |
| </span> | |
| <span className={`deltas__val tabular ${v == null ? '' : v > 0 ? 'delta--up' : 'delta--down'}`}> | |
| {fmtSigned(v, 1)} | |
| </span> | |
| </li> | |
| ); | |
| })} | |
| </ul> | |
| ); | |
| } | |
| /* βββββββββββββββββββββββββββββ observed order / disorder */ | |
| export const ORDER_REASON: Record<string, string> = { | |
| cross_method: 'different experimental methods', | |
| nmr: 'both are NMR ensembles', | |
| no_shared_residues: 'no shared resolved residues', | |
| missing_data: 'missing data', | |
| }; | |
| export const EVIDENCE_LABEL: Record<string, string> = { | |
| high: 'High evidence', | |
| medium: 'Medium evidence', | |
| low: 'Low evidence', | |
| not_applicable: 'No ordering', | |
| }; | |
| function OrderCell({ order }: { order: OrderChange }) { | |
| if (!order.valid) { | |
| return ( | |
| <span className="muted small" title={`Not comparable: ${ORDER_REASON[order.reason ?? ''] ?? order.reason ?? 'n/a'}`}> | |
| n/a | |
| </span> | |
| ); | |
| } | |
| const plus = order.n_ordered ?? 0; | |
| const minus = order.n_disordered ?? 0; | |
| if (!plus && !minus) return <span className="muted">0</span>; | |
| return ( | |
| <span className="order-cell" title={EVIDENCE_LABEL[order.evidence ?? ''] ?? ''}> | |
| {plus > 0 && <span className="order-cell__plus tabular">+{plus}</span>} | |
| {minus > 0 && <span className="order-cell__minus tabular">β{minus}</span>} | |
| {order.evidence && order.evidence !== 'not_applicable' && ( | |
| <span className={`evid evid--${order.evidence}`}>{order.evidence}</span> | |
| )} | |
| </span> | |
| ); | |
| } | |
| function fmtRanges(r: Array<[number, number]>, max = 8): string { | |
| const parts = r.map(([a, b]) => (a === b ? `${a}` : `${a}β${b}`)); | |
| return parts.length > max ? `${parts.slice(0, max).join(', ')} β¦ (+${parts.length - max})` : parts.join(', '); | |
| } | |
| /** | |
| * Residues resolved in the partner but not in this chain ("ordered") and the reverse | |
| * ("disordered"), on the SEQRES axis. Resolved-state sense, not intrinsic disorder. | |
| */ | |
| type ResState = 'both' | 'ord' | 'dis' | 'none'; | |
| /** Contiguous runs of each residue's resolved state in this chain (A) vs the partner (B). */ | |
| function resolvedRuns(L: number, resolvedA: Array<[number, number]>, order: OrderChange) { | |
| const inA = new Uint8Array(L + 2); | |
| for (const [a, b] of resolvedA) for (let i = Math.max(1, a); i <= Math.min(L, b); i++) inA[i] = 1; | |
| const mark = new Array<ResState>(L + 2).fill('none'); | |
| for (let i = 1; i <= L; i++) mark[i] = inA[i] ? 'both' : 'none'; | |
| for (const [a, b] of order.disordered_ranges) for (let i = a; i <= Math.min(L, b); i++) mark[i] = 'dis'; | |
| for (const [a, b] of order.ordered_ranges) for (let i = a; i <= Math.min(L, b); i++) mark[i] = 'ord'; | |
| const runs: Array<{ state: ResState; start: number; end: number }> = []; | |
| for (let i = 1; i <= L; i++) { | |
| const last = runs[runs.length - 1]; | |
| if (last && last.state === mark[i] && last.end === i - 1) last.end = i; | |
| else runs.push({ state: mark[i], start: i, end: i }); | |
| } | |
| return runs; | |
| } | |
| export function OrderChangeView({ | |
| order, | |
| length, | |
| from, | |
| to, | |
| resolvedFrom, | |
| }: { | |
| order: OrderChange; | |
| length: number; | |
| from: string; | |
| to: string; | |
| /** resolved SEQRES ranges of this chain (A); lets the track show every residue's state */ | |
| resolvedFrom?: Array<[number, number]>; | |
| }) { | |
| const L = Math.max(1, length); | |
| const runs = resolvedFrom?.length ? resolvedRuns(L, resolvedFrom, order) : null; | |
| const runTitle = (state: ResState, a: number, b: number) => { | |
| const span = a === b ? `${a}` : `${a}β${b}`; | |
| return state === 'both' | |
| ? `${span}: resolved in both` | |
| : state === 'ord' | |
| ? `${span}: ordered in ${to} (missing in ${from})` | |
| : state === 'dis' | |
| ? `${span}: resolved only in ${from}` | |
| : `${span}: not resolved in either structure`; | |
| }; | |
| return ( | |
| <div className="orderview"> | |
| <h3 className="compare__sub">Order β disorder (resolved residues)</h3> | |
| {!order.valid ? ( | |
| <p className="muted small"> | |
| Not comparable for this pair: {ORDER_REASON[order.reason ?? ''] ?? order.reason ?? 'unknown'}. Resolved-residue | |
| changes are only reported between structures solved with the same method. | |
| </p> | |
| ) : !order.n_ordered && !order.n_disordered ? ( | |
| <p className="muted small"> | |
| Both structures resolve the same residues ({order.n_shared_resolved?.toLocaleString() ?? 'β'} shared). | |
| </p> | |
| ) : ( | |
| <> | |
| <div className="orderview__stats"> | |
| <span> | |
| <span className="swatch" style={{ background: 'var(--ord)' }} /> <b className="tabular">{order.n_ordered ?? 0}</b> residues | |
| ordered in {to} | |
| {order.longest_ordered ? <span className="muted"> (longest {order.longest_ordered})</span> : null} | |
| </span> | |
| <span> | |
| <span className="swatch" style={{ background: 'var(--disord)' }} /> <b className="tabular">{order.n_disordered ?? 0}</b>{' '} | |
| residues resolved only in {from} | |
| {order.longest_disordered ? <span className="muted"> (longest {order.longest_disordered})</span> : null} | |
| </span> | |
| {order.evidence && order.evidence !== 'not_applicable' && ( | |
| <span> | |
| <span className={`evid evid--${order.evidence}`}>{EVIDENCE_LABEL[order.evidence]}</span> | |
| <span className="muted"> | |
| {' '} | |
| {order.n_internal_ordered ?? 0} internal Β· {order.n_terminal_ordered ?? 0} terminal | |
| </span> | |
| </span> | |
| )} | |
| {order.with_ligand_change && <span className="badge badge--outline">Co-occurs with a ligand change</span>} | |
| </div> | |
| <div className={`order-track ${runs ? 'order-track--full' : ''}`} aria-label="Resolved residues along the sequence"> | |
| {runs?.map((r) => ( | |
| <span | |
| key={`${r.state}${r.start}`} | |
| className={`order-track__seg order-track__seg--${r.state}`} | |
| style={{ left: `${((r.start - 1) / L) * 100}%`, width: `${((r.end - r.start + 1) / L) * 100}%` }} | |
| title={runTitle(r.state, r.start, r.end)} | |
| /> | |
| ))} | |
| {!runs && order.ordered_ranges.map(([a, b]) => ( | |
| <span | |
| key={`o${a}`} | |
| className="order-track__seg order-track__seg--ord" | |
| style={{ left: `${((a - 1) / L) * 100}%`, width: `${Math.max(0.4, ((b - a + 1) / L) * 100)}%` }} | |
| title={`Ordered in ${to}: ${a}β${b}`} | |
| /> | |
| ))} | |
| {!runs && order.disordered_ranges.map(([a, b]) => ( | |
| <span | |
| key={`d${a}`} | |
| className="order-track__seg order-track__seg--dis" | |
| style={{ left: `${((a - 1) / L) * 100}%`, width: `${Math.max(0.4, ((b - a + 1) / L) * 100)}%` }} | |
| title={`Resolved only in ${from}: ${a}β${b}`} | |
| /> | |
| ))} | |
| <span className="order-track__labels"> | |
| <span>1</span> | |
| <span>{L}</span> | |
| </span> | |
| </div> | |
| {runs && ( | |
| <ul className="legend order-legend"> | |
| <li> | |
| <span className="swatch order-key order-key--both" /> Resolved in both | |
| </li> | |
| <li> | |
| <span className="swatch" style={{ background: 'var(--ord)' }} /> Ordered in {to} | |
| </li> | |
| <li> | |
| <span className="swatch" style={{ background: 'var(--disord)' }} /> Resolved only in {from} | |
| </li> | |
| <li> | |
| <span className="swatch order-key order-key--none" /> Not resolved in either | |
| </li> | |
| </ul> | |
| )} | |
| <p className="muted small orderview__ranges"> | |
| {order.ordered_ranges.length > 0 && ( | |
| <> | |
| Ordered: <span className="mono">{fmtRanges(order.ordered_ranges)}</span> | |
| </> | |
| )} | |
| {order.ordered_ranges.length > 0 && order.disordered_ranges.length > 0 && ' Β· '} | |
| {order.disordered_ranges.length > 0 && ( | |
| <> | |
| Only in {from}: <span className="mono">{fmtRanges(order.disordered_ranges)}</span> | |
| </> | |
| )} | |
| </p> | |
| </> | |
| )} | |
| </div> | |
| ); | |
| } | |