|
| 1 | +/** |
| 2 | + * Dataset-scale AlphaFold analytics — inspect many models at once and aggregate the |
| 3 | + * confidence science: a combined pLDDT distribution, mean confidence across the set, |
| 4 | + * and a ranked table (mean pLDDT, % disordered). Turns the single-protein tool into |
| 5 | + * a lightweight benchmarking platform. |
| 6 | + */ |
| 7 | +import { useMemo, useState } from "react"; |
| 8 | +import { inspectModel } from "../api/inspect.ts"; |
| 9 | +import { plddtSummary, disorderedRegions } from "../engine/disorder.ts"; |
| 10 | +import { mapWithConcurrency } from "../batch/pool.ts"; |
| 11 | +import { parseIdList } from "../batch/parseIds.ts"; |
| 12 | +import { PlddtHistogram } from "../charts/PlddtHistogram.tsx"; |
| 13 | +import { toCsv } from "../workspace/csv.ts"; |
| 14 | +import { Icon } from "../ui/Icon.tsx"; |
| 15 | +import { useToast } from "../ui/toast.tsx"; |
| 16 | + |
| 17 | +interface Row { |
| 18 | + accession: string; |
| 19 | + name: string; |
| 20 | + n: number; |
| 21 | + meanPlddt: number; |
| 22 | + fractionDisordered: number; |
| 23 | + fractionConfident: number; |
| 24 | + disorderRegions: number; |
| 25 | + plddts: number[]; |
| 26 | + error?: string; |
| 27 | +} |
| 28 | + |
| 29 | +const PLACEHOLDER = `Paste UniProt accessions (one per line or comma-separated): |
| 30 | +P04637 |
| 31 | +P24941 |
| 32 | +P38398 |
| 33 | +P0DTD1`; |
| 34 | + |
| 35 | +export function DatasetView({ onOpen }: { onOpen: (accession: string) => void }) { |
| 36 | + const { toast } = useToast(); |
| 37 | + const [text, setText] = useState(""); |
| 38 | + const [rows, setRows] = useState<Row[]>([]); |
| 39 | + const [running, setRunning] = useState(false); |
| 40 | + const [progress, setProgress] = useState(0); |
| 41 | + |
| 42 | + const ids = useMemo(() => parseIdList(text), [text]); |
| 43 | + const ok = rows.filter((r) => !r.error); |
| 44 | + |
| 45 | + const aggregate = useMemo(() => { |
| 46 | + const all = ok.flatMap((r) => r.plddts); |
| 47 | + return { all, summary: plddtSummary(all) }; |
| 48 | + }, [ok]); |
| 49 | + |
| 50 | + async function run() { |
| 51 | + if (ids.length === 0 || running) return; |
| 52 | + setRunning(true); |
| 53 | + setRows([]); |
| 54 | + setProgress(0); |
| 55 | + let done = 0; |
| 56 | + const results: Row[] = []; |
| 57 | + await mapWithConcurrency(ids, 3, async (id) => { |
| 58 | + try { |
| 59 | + const r = await inspectModel(id); |
| 60 | + const plddts = r.residues.map((x) => x.plddt); |
| 61 | + const s = plddtSummary(plddts); |
| 62 | + results.push({ |
| 63 | + accession: r.accession, |
| 64 | + name: r.name, |
| 65 | + n: s.n, |
| 66 | + meanPlddt: s.mean, |
| 67 | + fractionDisordered: s.fractionDisordered, |
| 68 | + fractionConfident: s.fractionConfident, |
| 69 | + disorderRegions: disorderedRegions(r.residues).length, |
| 70 | + plddts, |
| 71 | + }); |
| 72 | + } catch (e) { |
| 73 | + results.push({ accession: id, name: id, n: 0, meanPlddt: 0, fractionDisordered: 0, fractionConfident: 0, disorderRegions: 0, plddts: [], error: (e as Error).message }); |
| 74 | + } finally { |
| 75 | + done++; |
| 76 | + setProgress(done); |
| 77 | + setRows([...results].sort((a, b) => b.meanPlddt - a.meanPlddt)); |
| 78 | + } |
| 79 | + }); |
| 80 | + setRunning(false); |
| 81 | + toast("Dataset analysis finished.", "success"); |
| 82 | + } |
| 83 | + |
| 84 | + function exportCsv() { |
| 85 | + const csv = toCsv( |
| 86 | + ["accession", "protein", "residues", "mean_plddt", "fraction_disordered", "fraction_confident", "disorder_regions"], |
| 87 | + ok.map((r) => [r.accession, r.name, r.n, r.meanPlddt.toFixed(1), r.fractionDisordered.toFixed(3), r.fractionConfident.toFixed(3), r.disorderRegions]), |
| 88 | + ); |
| 89 | + const blob = new Blob([csv], { type: "text/csv" }); |
| 90 | + const url = URL.createObjectURL(blob); |
| 91 | + const a = document.createElement("a"); |
| 92 | + a.href = url; |
| 93 | + a.download = "alphafold_dataset.csv"; |
| 94 | + a.click(); |
| 95 | + } |
| 96 | + |
| 97 | + return ( |
| 98 | + <section className="dataset-view"> |
| 99 | + <div className="view-head"> |
| 100 | + <div> |
| 101 | + <h2>Dataset analytics</h2> |
| 102 | + <p className="muted">Inspect many AlphaFold models at once and see the confidence landscape across the set.</p> |
| 103 | + </div> |
| 104 | + </div> |
| 105 | + |
| 106 | + <textarea className="batch-input" rows={6} placeholder={PLACEHOLDER} value={text} onChange={(e) => setText(e.target.value)} disabled={running} /> |
| 107 | + <div className="batch-controls"> |
| 108 | + <button className="primary" onClick={() => void run()} disabled={running || ids.length === 0}> |
| 109 | + {running ? `Analyzing… ${progress}/${ids.length}` : `Analyze ${ids.length || ""} model${ids.length === 1 ? "" : "s"}`} |
| 110 | + </button> |
| 111 | + {ok.length > 0 && <button onClick={exportCsv}><Icon name="download" size={14} /> Export CSV</button>} |
| 112 | + </div> |
| 113 | + |
| 114 | + {running && <div className="progress"><div className="progress-bar" style={{ width: `${(progress / ids.length) * 100}%` }} /></div>} |
| 115 | + |
| 116 | + {ok.length > 0 && ( |
| 117 | + <> |
| 118 | + <div className="metrics"> |
| 119 | + <Stat label="Models" value={String(ok.length)} sub={`${aggregate.summary.n.toLocaleString()} residues`} /> |
| 120 | + <Stat label="Mean pLDDT" value={aggregate.summary.mean.toFixed(1)} sub="across the set" /> |
| 121 | + <Stat label="Confident" value={`${(aggregate.summary.fractionConfident * 100).toFixed(0)}%`} sub="pLDDT ≥ 70" /> |
| 122 | + <Stat label="Disordered" value={`${(aggregate.summary.fractionDisordered * 100).toFixed(0)}%`} sub="pLDDT < 50" /> |
| 123 | + </div> |
| 124 | + <div className="charts"><PlddtHistogram plddts={aggregate.all} /></div> |
| 125 | + <div className="dash-scroll"> |
| 126 | + <table> |
| 127 | + <thead><tr><th>Protein</th><th>Residues</th><th>Mean pLDDT</th><th>Confident</th><th>Disordered</th><th>IDRs</th></tr></thead> |
| 128 | + <tbody> |
| 129 | + {rows.map((r) => ( |
| 130 | + <tr key={r.accession} className={r.error ? "row-wrong" : ""}> |
| 131 | + <td>{r.error ? <span className="muted">{r.accession} — {r.error}</span> : <button className="link strong" onClick={() => onOpen(r.accession)}>{r.name}</button>}</td> |
| 132 | + <td>{r.n || ""}</td> |
| 133 | + <td>{r.error ? "" : r.meanPlddt.toFixed(1)}</td> |
| 134 | + <td>{r.error ? "" : `${(r.fractionConfident * 100).toFixed(0)}%`}</td> |
| 135 | + <td>{r.error ? "" : `${(r.fractionDisordered * 100).toFixed(0)}%`}</td> |
| 136 | + <td>{r.error ? "" : r.disorderRegions}</td> |
| 137 | + </tr> |
| 138 | + ))} |
| 139 | + </tbody> |
| 140 | + </table> |
| 141 | + </div> |
| 142 | + </> |
| 143 | + )} |
| 144 | + </section> |
| 145 | + ); |
| 146 | +} |
| 147 | + |
| 148 | +function Stat({ label, value, sub }: { label: string; value: string; sub: string }) { |
| 149 | + return ( |
| 150 | + <div className="metric"> |
| 151 | + <div className="metric-value">{value}</div> |
| 152 | + <div className="metric-label">{label}</div> |
| 153 | + <div className="metric-hint muted">{sub}</div> |
| 154 | + </div> |
| 155 | + ); |
| 156 | +} |
0 commit comments