shrut27's picture
Implement complete API contract, persistence, jobs, artifacts, and architecture status
ff453a6 verified
Raw
History Blame Contribute Delete
39.9 kB
const PALETTE = ["#1B365D", "#0E7C7B", "#C45C26", "#D4A017", "#2F855A", "#6B46C1", "#2B6CB0", "#C53030"];
const TABS = [
"Overview",
"Variant intelligence & QC",
"Routing strategy",
"Structure engine",
"Pockets & druggability",
"Compound library & AI screen",
"WT vs mutant docking",
"MD & free energy",
"Consensus ranking",
"Validation & risks",
"Platform architecture",
"Methods & formulas",
"Reports & data",
];
const FILES = [
"variants", "structures", "pockets", "compounds", "screening",
"docking", "rankings", "md", "md_timeseries", "rmsf", "admet", "validation", "jobs",
];
const STAGES = [
["1. Variant intelligence", "Normalize HGVS, QC mismatches, map domain, score priority, assign route."],
["2. Structure engine", "Prepare WT and mutant, compute ΔΔG, RMSD, SASA and electrostatic shift."],
["3. Pocket engine", "Detect cavities, WT–mutant volume delta, druggability, docking gate."],
["4. Compound library", "Approved, clinical, screening and Y220C positive-control ligands."],
["5. AI prescreen", "Rapid ranking with confidence and applicability domain before docking."],
["6. Matched docking", "Identical protocol on mutant and WT; mutant preference = Δdock."],
["7. Re-rank + ADMET", "Penalize liabilities, promote mutant-selective, exposure-feasible chemotypes."],
["8. MD + MM/GBSA", "Replica trajectories, contact occupancy, free-energy proxy on the shortlist only."],
["9. Consensus ranking", "Rescue/Opportunity Score, reason codes, Go / Hold / No-go, next experiment."],
];
let DATA = {};
let activeTab = 0;
const DEFAULT_WEIGHTS = {
Bmut: 0.22, Sselectivity: 0.18, MDstability: 0.16,
Frescue: 0.14, ADMET: 0.12, Evidence: 0.10, Risk: 0.08,
};
const layoutBase = {
font: { color: "#111827", family: "Inter, Arial, sans-serif", size: 12 },
paper_bgcolor: "white",
plot_bgcolor: "white",
margin: { t: 40, r: 20, l: 50, b: 60 },
legend: { bgcolor: "rgba(255,255,255,0.9)" },
};
function loadCsv(name) {
return new Promise((resolve, reject) => {
Papa.parse(`data/${name}.csv`, {
download: true,
header: true,
dynamicTyping: true,
skipEmptyLines: true,
complete: (res) => resolve(res.data),
error: reject,
});
});
}
function uniq(arr, key) {
return [...new Set(arr.map((r) => r[key]).filter((v) => v !== undefined && v !== null && v !== ""))];
}
function num(v) { return typeof v === "number" ? v : parseFloat(v) || 0; }
function bool(v) { return v === true || v === "True" || v === "true" || v === 1; }
function metric(title, value) {
return `<div class="card"><h3>${title}</h3><p>${value}</p></div>`;
}
function table(rows, cols) {
if (!rows.length) return "<p class='warn'>No rows for the current selection.</p>";
const head = cols.map((c) => `<th>${c}</th>`).join("");
const body = rows.map((r) => `<tr>${cols.map((c) => `<td>${r[c] ?? ""}</td>`).join("")}</tr>`).join("");
return `<div style="overflow:auto;max-height:420px"><table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
}
function recClass(v) {
if (v === "Go") return "go";
if (v === "Hold") return "hold";
return "nogo";
}
function currentVariant() {
return document.getElementById("variantSelect").value;
}
function filteredVariants() {
const exon = document.getElementById("exonSelect").value;
const typ = document.getElementById("typeSelect").value;
return DATA.variants.filter((r) =>
(exon === "All" || r.exon === exon) && (typ === "All" || r.type_inferred === typ)
);
}
function weights() {
const out = {};
Object.keys(DEFAULT_WEIGHTS).forEach((key) => {
out[key] = num(document.getElementById(`w_${key}`).value);
});
return out;
}
function rescore(rows) {
const w = weights();
const rescored = rows.map((r) => {
const score = Math.max(0, Math.min(1,
w.Bmut * num(r.Bmut)
+ w.Sselectivity * num(r.Sselectivity)
+ w.MDstability * num(r.MDstability)
+ w.Frescue * num(r.Frescue)
+ w.ADMET * num(r.ADMET)
+ w.Evidence * num(r.Evidence)
- w.Risk * num(r.Risk)
));
const recommendation = r.admet_flag === "Severe liability"
? "No-go"
: (score >= 0.74 && num(r.confidence) >= 0.68 ? "Go" : (score >= 0.58 ? "Hold" : "No-go"));
return { ...r, rescue_score: +score.toFixed(3), recommendation };
});
rescored.sort((a, b) => b.rescue_score - a.rescue_score);
return rescored.map((r, i) => ({ ...r, rank: i + 1 }));
}
function syncVariantOptions() {
const allowed = uniq(filteredVariants(), "hgvs_p");
const current = currentVariant();
fillSelect("variantSelect", allowed, allowed.includes("p.Y220C") ? "p.Y220C" : allowed[0]);
if (allowed.includes(current)) document.getElementById("variantSelect").value = current;
}
function plot(id, traces, extra = {}) {
Plotly.react(id, traces, { ...layoutBase, ...extra }, { responsive: true, displayModeBar: false });
}
function render() {
const v = currentVariant();
const variants = DATA.variants;
const vf = filteredVariants();
const varRow = variants.find((r) => r.hgvs_p === v) || variants[0];
const rankSel = rescore(DATA.rankings.filter((r) => r.variant === v));
const top1 = rankSel[0];
const structSel = DATA.structures.filter((r) => r.variant === v);
const pocketSel = DATA.pockets.filter((r) => r.variant === v);
const content = document.getElementById("content");
const views = [
() => overview(vf, varRow, top1, v),
() => qcTab(variants, vf),
() => routingTab(vf),
() => structureTab(varRow, structSel, v),
() => pocketTab(pocketSel, v),
() => libraryTab(v),
() => dockTab(v),
() => mdTab(v),
() => rankTab(rankSel, varRow, top1, v),
() => valTab(),
() => archTab(),
() => methodsTab(),
() => reportTab(varRow, rankSel, top1),
];
content.innerHTML = views[activeTab]();
requestAnimationFrame(() => drawPlots(activeTab, v, varRow, rankSel, top1, structSel, pocketSel, vf));
}
function overview(variants, varRow, top1, v) {
const allowed = new Set(variants.map((r) => r.hgvs_p));
const docked = DATA.docking.filter((r) => allowed.has(r.variant));
const mdRows = DATA.md.filter((r) => allowed.has(r.variant));
return `
<h2 class="section">Project snapshot — lung cancer TP53 panel</h2>
<div class="metrics">
${metric("Observations", variants.length)}
${metric("Unique HGVS", uniq(variants, "hgvs_p").length)}
${metric("QC flags", variants.filter((r) => r.qc_status === "Flagged").length)}
${metric("Library size", DATA.compounds.length.toLocaleString())}
${metric("Docked pairs", docked.length.toLocaleString())}
${metric("MD replicas", mdRows.length.toLocaleString())}
</div>
<h2 class="section">Focus variant</h2>
<div class="metrics">
${metric("Variant", v)}
${metric("Priority", num(varRow.priority_score).toFixed(2))}
${metric("Route", String(varRow.route).split("(")[0].trim())}
${metric("QC", varRow.qc_status)}
${metric("Top Rescue score", top1 ? num(top1.rescue_score).toFixed(2) : "n/a")}
</div>
<div class="grid2">
<div id="p_sun" class="plot"></div>
<div id="p_pri" class="plot"></div>
</div>
<h2 class="section">Nine-stage decision engine</h2>
<div class="stage-grid">${STAGES.map(([h, p]) => `<div class="stage"><h4>${h}</h4><p>${p}</p></div>`).join("")}</div>
${top1 ? `<p class="callout"><b>Preferred reporting language:</b> ${v}${top1.name} | Rescue/Opportunity Score: ${num(top1.rescue_score).toFixed(2)} | Mutant preference: ${num(top1.Sselectivity).toFixed(2)} | MD stability: ${num(top1.MDstability).toFixed(2)} | AI confidence: ${num(top1.confidence).toFixed(2)} | Recommendation: ${top1.recommendation}.</p>` : ""}
`;
}
function qcTab(variants, vf) {
const flagged = variants.filter((r) => r.qc_status === "Flagged");
return `
<h2 class="section">Variant intelligence and data-quality gate</h2>
<p class="muted">Source labels are retained. Contradictions are flagged, not silently rewritten.</p>
<div class="metrics">
${metric("Missense (inferred)", variants.filter((r) => r.type_inferred === "Missense").length)}
${metric("Nonsense / stop", variants.filter((r) => r.type_inferred === "Nonsense").length)}
${metric("Splice-site", variants.filter((r) => r.type_inferred === "Splice Site").length)}
${metric("Type mismatches", variants.filter((r) => r.type_source !== r.type_inferred).length)}
</div>
<div class="grid2">
<div id="p_af" class="plot"></div>
<div id="p_map" class="plot"></div>
</div>
<h3>QC exceptions requiring human review</h3>
${table(flagged, ["obs_id", "variant_raw", "hgvs_p", "type_source", "type_inferred", "effect_source", "qc_flags", "allele_frequency"])}
<h3>Full observation table</h3>
${table(vf, ["obs_id", "exon", "hgvs_p", "canonical_wt", "position", "allele_frequency", "type_source", "type_inferred", "functional_class", "ddg_kcal", "ddg_source", "qc_status", "priority_score", "route"])}
`;
}
function routingTab(variants) {
const uniqV = [];
const seen = new Set();
variants.forEach((r) => { if (!seen.has(r.hgvs_p)) { seen.add(r.hgvs_p); uniqV.push(r); } });
uniqV.sort((a, b) => num(b.priority_score) - num(a.priority_score));
return `
<h2 class="section">Mutation-specific routing — do not send every variant through docking</h2>
<div id="p_route" class="plot"></div>
<p class="callout">Y220C is the platform benchmark: known mutation-induced cavity and ligand-bound structural precedents. Truncating and splice variants are held out of the standard small-molecule pocket workflow.</p>
${table(uniqV, ["hgvs_p", "type_inferred", "domain", "hotspot", "route", "priority_score", "structure_tractable"])}
`;
}
function structureTab(varRow, structSel, v) {
const s = structSel[0];
return `
<h2 class="section">Wild-type vs mutant structure comparison</h2>
${s ? `<div class="metrics">
${metric("ΔΔG proxy", s.ddg_kcal + " kcal/mol")}
${metric("Cα RMSD", s.ca_rmsd_A + " Å")}
${metric("Local RMSD", s.local_rmsd_A + " Å")}
${metric("SASA Δ", s.sasa_delta_A2 + " Ų")}
${metric("Structure quality", num(s.structure_quality).toFixed(2))}
</div>` : ""}
<div class="grid2">
<div id="p_ddg" class="plot"></div>
<div id="p_rmsf" class="plot"></div>
</div>
<div id="p_pvol" class="plot"></div>
${table(DATA.structures, ["variant", "ddg_kcal", "ca_rmsd_A", "local_rmsd_A", "sasa_delta_A2", "pocket_vol_wt", "pocket_vol_mut", "structure_quality", "source_wt"])}
`;
}
function pocketTab(pocketSel, v) {
return `
<h2 class="section">Pocket detection and mutation-specific druggability</h2>
<div class="grid2">
<div id="p_pock" class="plot"></div>
<div id="p_pockv" class="plot"></div>
</div>
<div id="p_radar" class="plot"></div>
${table(pocketSel, ["pocket_id", "pocket_name", "volume_wt", "volume_mut", "volume_delta", "druggability", "mutant_created", "docking_gate"])}
`;
}
function libraryTab(v) {
const controls = DATA.compounds.filter((c) => c.note);
const scr = DATA.screening.filter((r) => r.variant === v).sort((a, b) => num(b.ai_score) - num(a.ai_score)).slice(0, 25);
return `
<h2 class="section">Compound library assembly and AI virtual screening</h2>
<div class="metrics">
${metric("Compounds", DATA.compounds.length.toLocaleString())}
${metric("Approved / clinical", DATA.compounds.filter((c) => c.status === "approved" || c.status === "clinical").length)}
${metric("Y220C controls", DATA.compounds.filter((c) => bool(c.y220c_control)).length)}
${metric("Lipinski pass", Math.round(100 * DATA.compounds.filter((c) => bool(c.lipinski_pass)).length / DATA.compounds.length) + "%")}
</div>
<div class="grid2">
<div id="p_lib" class="plot"></div>
<div id="p_chem" class="plot"></div>
</div>
<div class="grid2">
<div id="p_ai" class="plot"></div>
<div id="p_conf" class="plot"></div>
</div>
<h3>Reference / control ligands</h3>
${table(controls, ["compound_id", "name", "status", "note", "mw", "logp"])}
<h3>Top AI-ranked compounds for ${v}</h3>
${scr.length ? table(scr, ["name", "status", "ai_score", "ai_confidence", "applicability"]) : "<p class='warn'>AI screening is run on missense / structure-tractable variants only.</p>"}
`;
}
function dockTab(v) {
const dsel = DATA.docking.filter((r) => r.variant === v);
if (!dsel.length) return `<h2 class="section">Matched wild-type vs mutant docking</h2><p class="warn">This variant is not routed to the standard docking workflow.</p>`;
const top = [...dsel].sort((a, b) => num(a.dock_mut) - num(b.dock_mut)).slice(0, 20);
return `
<h2 class="section">Matched wild-type vs mutant docking</h2>
<div class="metrics">
${metric("Docked ligands", dsel.length)}
${metric("Median dock mutant", median(dsel.map((r) => num(r.dock_mut))).toFixed(2))}
${metric("Median WT", median(dsel.map((r) => num(r.dock_wt))).toFixed(2))}
${metric("Mutant-preferring", dsel.filter((r) => num(r.mutant_preference) > 0.4).length)}
</div>
<div class="grid2">
<div id="p_dock" class="plot"></div>
<div id="p_ddock" class="plot"></div>
</div>
<div id="p_fp" class="plot" style="height:440px"></div>
${table(top, ["name", "status", "dock_mut", "dock_wt", "delta_dock", "mutant_preference", "pose_qc", "key_contacts"])}
`;
}
function mdTab(v) {
const mdSel = DATA.md.filter((r) => r.variant === v);
if (!mdSel.length) return `<h2 class="section">Molecular dynamics</h2><p class="warn">MD is reserved for top-ranked complexes after docking / AI gates.</p>`;
const agg = aggregateMd(mdSel);
return `
<h2 class="section">Molecular dynamics and free-energy proxy (shortlist only)</h2>
<div class="metrics">
${metric("Complexes", agg.length)}
${metric("Replicas / complex", "3 × 100 ns")}
${metric("Median MM/GBSA", median(agg.map((r) => r.mmgbsa)).toFixed(1))}
${metric("QC pass rate", Math.round(100 * mdSel.filter((r) => r.qc === "Pass").length / mdSel.length) + "%")}
</div>
<div class="grid2">
<div id="p_md" class="plot"></div>
<div id="p_ts" class="plot"></div>
</div>
${table(agg.sort((a, b) => a.mmgbsa - b.mmgbsa).slice(0, 25), ["name", "ligand_rmsd", "protein_rmsd", "contact_occupancy", "mmgbsa", "hbond_mean", "qc_pass"])}
`;
}
function rankTab(rankSel, varRow, top1, v) {
if (!top1) return `<h2 class="section">Consensus ranking</h2><p class="warn">No consensus shortlist — variant routed away from small-molecule docking.</p>`;
const goN = rankSel.filter((r) => r.recommendation === "Go").length;
return `
<h2 class="section">Industry dashboard — consensus Rescue / Opportunity Score</h2>
<p class="muted">PROJECT: Lung Cancer | Variant: ${v} | Status: Complete for structure-tractable routes</p>
<div class="metrics">
${metric("Top candidate", top1.name)}
${metric("Rescue score", num(top1.rescue_score).toFixed(2))}
${metric("Confidence", num(top1.confidence).toFixed(2))}
${metric("Recommendation", `<span class="${recClass(top1.recommendation)}">${top1.recommendation}</span>`)}
${metric("Repurposing", top1.status)}
</div>
<p class="callout"><b>Next action:</b> ${top1.next_experiment}</p>
<div class="grid2">
<div id="p_comp" class="plot"></div>
<div id="p_3" class="plot"></div>
</div>
<div id="p_rankbar" class="plot"></div>
<div id="p_wbar" class="plot"></div>
<p class="muted">Go: ${goN} · Hold: ${rankSel.filter((r) => r.recommendation === "Hold").length} · No-go: ${rankSel.filter((r) => r.recommendation === "No-go").length}</p>
${table(rankSel.slice(0, 25), ["rank", "name", "status", "dock_mut", "dock_wt", "mutant_preference", "MDstability", "ADMET", "rescue_score", "confidence", "recommendation", "next_experiment"])}
`;
}
function valTab() {
const risks = [
["Docking-score overinterpretation", "Scores are noisy and engine-dependent", "Matched WT–mutant protocol, consensus, MD, experiment"],
["Incorrect mutation annotation", "Wrong residue invalidates downstream modeling", "Hard QC gate + human review of flagged rows"],
["Low-quality structures", "Bad loops/protonation distort pockets", "Structure quality score and standardized preparation"],
["No tractable pocket", "Some variants are not small-molecule rescue problems", "Routing gate and alternative-strategy branch"],
["Data leakage in AI", "Random splits inflate performance", "Scaffold / mutation-aware splits"],
["ADMET mismatch", "Binding irrelevant at achievable exposure", "Exposure-aware filter and safety penalties"],
["False certainty", "Confident numbers outside training domain", "Applicability domain, reason codes, no-go thresholds"],
["Compute cost", "MD on every ligand is unaffordable", "AI → docking gate → top-candidate MD only"],
].map((r) => ({ Risk: r[0], "Why it matters": r[1], Mitigation: r[2] }));
const exp = [
["Does the compound bind mutant TP53?", "Biophysical binding assay on purified protein"],
["Does it stabilize mutant TP53?", "Thermal shift / orthogonal stability assay"],
["Does it restore p53 pathway function?", "Reporter / target-gene transcriptional readout"],
["Is the effect mutation-specific?", "Matched WT vs mutant models"],
["Cancer-cell phenotype?", "Viability / apoptosis at exposure-relevant concentrations"],
["Repurposing feasible?", "Effective concentration vs known human exposure and safety margins"],
].map((r) => ({ Question: r[0], "Suggested readout": r[1] }));
return `
<h2 class="section">Validation strategy, success criteria, and risk register</h2>
<div class="grid2">
<div id="p_auroc" class="plot"></div>
<div id="p_ef" class="plot"></div>
</div>
<h3>Later-phase experimental readouts</h3>
${table(exp, ["Question", "Suggested readout"])}
<h3>Key risks and mitigations</h3>
${table(risks, ["Risk", "Why it matters", "Mitigation"])}
<div class="grid2">
<div id="p_herg" class="plot"></div>
<div id="p_exp" class="plot"></div>
</div>
`;
}
function archTab() {
const svc = [
["variant-service", "Upload/normalize variants, annotate type/domain, priority and route"],
["structure-service", "Retrieve/build/prepare WT and mutant structures; structural deltas"],
["pocket-service", "Detect pockets and mutant-specific changes; emit docking grids"],
["compound-service", "Import/standardize libraries and provenance"],
["ai-screen-service", "Rapid compound ranking and confidence"],
["docking-service", "Queue matched mutant and WT docking"],
["md-service", "Replicate MD and free-energy proxy"],
["admet-service", "Physicochemical / ADMET / liability flags"],
["evidence-service", "Public/internal evidence and repurposing metadata"],
["ranking-service", "Consensus score, uncertainty, shortlist"],
["report-service", "Traceable dashboard JSON / PDF artifacts"],
].map((r) => ({ Microservice: r[0], Responsibility: r[1] }));
const api = [
["POST /projects", "Cancer type, project name", "project_id", "Implemented"],
["POST /variants:ingest", "JSON variant records", "job_id → VariantRecords + QC artifact", "Implemented"],
["POST /variants/{id}/analyze", "variant_id", "job_id → priority, class, domain, route, confidence", "Implemented"],
["POST /structures:prepare", "variant_ids, source preference", "job_id → WT/mutant StructureRecords", "Implemented"],
["POST /pockets:detect", "structure_ids", "job_id → pockets, grids, druggability", "Implemented"],
["POST /compounds:screen", "variant_id, library_id, model_id", "job_id → ranked candidates + uncertainty", "Implemented"],
["POST /docking:run", "variant_id, compound properties, protocol_id", "job_id → matched WT/mutant scores", "Implemented"],
["POST /md:run", "complexes, protocol_id", "job_id → trajectory metrics + MM/GBSA + QC", "Implemented"],
["POST /rankings:compute", "variant_id, score components, weights", "job_id → shortlist + reason codes", "Implemented"],
["GET /jobs/{id}", "job_id", "status, logs, output, error", "Implemented"],
["GET /projects/{id}/jobs", "project_id", "all asynchronous jobs", "Implemented"],
["GET /projects/{id}/artifacts", "project_id", "versioned artifact index", "Implemented"],
["GET /artifacts/{id}", "artifact_id", "artifact metadata and payload", "Implemented"],
["GET /projects/{id}/report", "project_id", "dashboard JSON + report artifacts", "Implemented"],
].map((r) => ({ Endpoint: r[0], Input: r[1], Output: r[2], Status: `${r[3]} (API container)` }));
return `
<h2 class="section">Cloud-native services, data objects, and API contract</h2>
<p>Researcher UI → API gateway → variant / structure / compound / evidence services → screening and ranking models → containerized docking → GPU MD jobs → consensus report → object store + warehouse.</p>
<p class="callout"><b>Runtime:</b> all routes are implemented in the versioned FastAPI container. This public static deployment publishes the dashboard and backend source; live HTTP API execution requires the organization’s Docker/CPU runtime to be enabled.</p>
<div id="p_jobs" class="plot"></div>
<h3>Service boundaries</h3>
${table(svc, ["Microservice", "Responsibility"])}
<h3>Implemented API endpoints (long-running operations return job_id)</h3>
${table(api, ["Endpoint", "Input", "Output", "Status"])}
${table(DATA.jobs, ["job_id", "service", "task", "status", "runtime_min", "protocol_version", "container"])}
`;
}
function methodsTab() {
return `
<h2 class="section">Methods, equations, and evaluation contract</h2>
<p>Reference sequence: <b>NP_000537.3</b> / UniProt <b>P04637</b> (393 aa). Missense WT amino acids are checked against this sequence.</p>
<p><b>ΔΔG</b> (kcal/mol, unfolding; positive = destabilizing). Literature thermal-unfolding values when published (Y220C = 3.78 kcal/mol). Otherwise FoldX-inspired:
0.018·|ΔV|·b + 0.35·|ΔH<sub>KD</sub>|·b + 1.15·|Δq|·b + helix/Zn penalties. Volumes are Richards ų; hydrophobicity is Kyte–Doolittle.</p>
<p><b>Y220C cavity</b> is the published ~200 ų mutation-induced pocket. WT still has the constitutive DNA-cleft (~210 ų).</p>
<p><b>Thermodynamics.</b> When a published K<sub>d</sub> exists, ΔG° = RT ln K<sub>d</sub> (298 K). B<sub>mut</sub> = clip((−ΔG<sub>mut</sub>−4)/8, 0, 1). Selectivity = clip((ΔG<sub>WT</sub>−ΔG<sub>mut</sub>)/3, 0, 1).</p>
<p><b>ADMET.</b> Lipinski, Veber, Egan ellipse, Delaney ESOL, QED-like desirability, Gleeson-style hERG vs logP.</p>
<p><b>Consensus.</b> R(c,m) = 0.22 B<sub>mut</sub> + 0.18 S + 0.16 MD + 0.14 F<sub>rescue</sub> + 0.12 ADMET + 0.10 Evidence − 0.08 Risk.</p>
<p><b>Enrichment.</b> AUROC = Mann–Whitney P(s<sub>+</sub> &gt; s<sub>−</sub>); EF x% = (actives in top x%) / expected.</p>
<p class="callout">Submit a variant table and optional docking kcal/mol values through the platform API to score real-world data with the same transforms.</p>
`;
}
function reportTab(varRow, rankSel, top1) {
return `
<h2 class="section">Downloadable data</h2>
<p>Deliverables in this workspace: QC-audited variant table, WT/mutant structure metrics, pocket report, versioned libraries, AI prescreen, matched docking, MD summaries, consensus ranking with uncertainty, service/API map, and experimental next steps.</p>
<p>${FILES.map((f) => `<a href="data/${f}.csv" download>Download ${f}.csv</a>`).join(" · ")}</p>
${top1 ? `<p class="callout">Minimal candidate record example: ${varRow.hgvs_p} × ${top1.name} | Rescue ${num(top1.rescue_score).toFixed(2)} | ${top1.recommendation} | ${top1.reason_codes}</p>` : ""}
${table(rankSel.slice(0, 10), ["rank", "name", "status", "rescue_score", "confidence", "recommendation", "next_experiment"])}
`;
}
function median(arr) {
const a = arr.filter((x) => !Number.isNaN(x)).sort((x, y) => x - y);
if (!a.length) return 0;
const m = Math.floor(a.length / 2);
return a.length % 2 ? a[m] : (a[m - 1] + a[m]) / 2;
}
function aggregateMd(rows) {
const map = {};
rows.forEach((r) => {
const k = r.compound_id;
if (!map[k]) map[k] = { name: r.name, n: 0, ligand_rmsd: 0, protein_rmsd: 0, contact_occupancy: 0, mmgbsa: 0, hbond_mean: 0, qc: 0 };
map[k].n += 1;
map[k].ligand_rmsd += num(r.ligand_rmsd);
map[k].protein_rmsd += num(r.protein_rmsd);
map[k].contact_occupancy += num(r.contact_occupancy);
map[k].mmgbsa += num(r.mmgbsa);
map[k].hbond_mean += num(r.hbond_mean);
map[k].qc += r.qc === "Pass" ? 1 : 0;
});
return Object.values(map).map((x) => ({
name: x.name,
ligand_rmsd: +(x.ligand_rmsd / x.n).toFixed(2),
protein_rmsd: +(x.protein_rmsd / x.n).toFixed(2),
contact_occupancy: +(x.contact_occupancy / x.n).toFixed(3),
mmgbsa: +(x.mmgbsa / x.n).toFixed(2),
hbond_mean: +(x.hbond_mean / x.n).toFixed(2),
qc_pass: +(x.qc / x.n).toFixed(2),
}));
}
function counts(rows, key) {
const m = {};
rows.forEach((r) => { m[r[key]] = (m[r[key]] || 0) + 1; });
return m;
}
function drawPlots(tab, v, varRow, rankSel, top1, structSel, pocketSel, vf) {
if (tab === 0) {
const exons = uniq(vf, "exon");
plot("p_sun", exons.map((e, i) => {
const sub = vf.filter((r) => r.exon === e);
const types = counts(sub, "type_inferred");
return { type: "bar", name: e, x: Object.keys(types), y: Object.values(types), marker: { color: PALETTE[i % PALETTE.length] } };
}), { barmode: "stack", title: "Observations by exon and inferred class" });
const ordered = [...vf].sort((a, b) => num(b.priority_score) - num(a.priority_score));
plot("p_pri", [{ type: "bar", x: ordered.map((r) => r.hgvs_p), y: ordered.map((r) => num(r.priority_score)), marker: { color: "#0E7C7B" } }], { title: "Variant priority score", xaxis: { tickangle: -45 } });
}
if (tab === 1) {
const types = uniq(vf, "type_inferred");
plot("p_af", types.map((t, i) => ({
type: "histogram", name: t, x: vf.filter((r) => r.type_inferred === t).map((r) => num(r.allele_frequency)), marker: { color: PALETTE[i] },
})), { barmode: "overlay", title: "Allele frequency by inferred class" });
plot("p_map", [{
type: "scatter", mode: "markers",
x: vf.map((r) => num(r.position)),
y: vf.map((r) => num(r.allele_frequency)),
text: vf.map((r) => r.hgvs_p),
marker: { size: vf.map((r) => 8 + 18 * num(r.priority_score)), color: vf.map((r) => r.qc_status === "Flagged" ? "#C45C26" : "#0E7C7B") },
}], { title: "Residue map — size = priority, orange = QC flag", xaxis: { title: "Residue" }, yaxis: { title: "Allele frequency" } });
}
if (tab === 2) {
const c = counts(vf, "route");
plot("p_route", [{ type: "bar", x: Object.values(c), y: Object.keys(c), orientation: "h", marker: { color: "#1B365D" } }], { title: "Observations per route" });
}
if (tab === 3) {
plot("p_ddg", [{
type: "scatter", mode: "markers",
x: DATA.structures.map((r) => num(r.ddg_kcal)),
y: DATA.structures.map((r) => num(r.ca_rmsd_A)),
text: DATA.structures.map((r) => r.variant),
marker: { size: DATA.structures.map((r) => 8 + num(r.sasa_delta_A2) / 8), color: "#0E7C7B" },
}], { title: "Stability vs global structural change", xaxis: { title: "ΔΔG" }, yaxis: { title: "Cα RMSD" } });
const sys = uniq(DATA.rmsf, "system");
plot("p_rmsf", sys.map((s, i) => ({
type: "scatter", mode: "lines", name: s,
x: DATA.rmsf.filter((r) => r.system === s).map((r) => r.residue),
y: DATA.rmsf.filter((r) => r.system === s).map((r) => r.rmsf),
line: { color: PALETTE[i] },
})), { title: "DBD RMSF (WT vs selected mutants)", xaxis: { title: "Residue" } });
plot("p_pvol", [
{ type: "bar", name: "WT", x: DATA.structures.map((r) => r.variant), y: DATA.structures.map((r) => num(r.pocket_vol_wt)), marker: { color: "#1B365D" } },
{ type: "bar", name: "Mutant", x: DATA.structures.map((r) => r.variant), y: DATA.structures.map((r) => num(r.pocket_vol_mut)), marker: { color: "#0E7C7B" } },
], { barmode: "group", title: "Pocket volume WT vs mutant", xaxis: { tickangle: -45 } });
}
if (tab === 4) {
plot("p_pock", [{
type: "scatter", mode: "markers",
x: DATA.pockets.map((r) => num(r.volume_delta)),
y: DATA.pockets.map((r) => num(r.druggability)),
text: DATA.pockets.map((r) => r.pocket_id),
marker: { color: DATA.pockets.map((r) => r.docking_gate === "Open" ? "#0E7C7B" : "#C45C26"), size: 9 },
}], { title: "Druggability vs volume delta", xaxis: { title: "Volume Δ" }, yaxis: { title: "Druggability" } });
const ps = DATA.pockets.filter((r) => r.variant === v);
plot("p_pockv", [{ type: "bar", x: ps.map((r) => r.pocket_name), y: ps.map((r) => num(r.druggability)), marker: { color: "#1B365D" } }], { title: `Pockets on ${v}` });
plot("p_radar", ps.map((r, i) => ({
type: "scatterpolar", fill: "toself", name: r.pocket_name,
r: [num(r.druggability), num(r.hydrophobicity), num(r.polarity), Math.min(num(r.volume_mut) / 400, 1), num(r.druggability)],
theta: ["Druggability", "Hydrophobicity", "Polarity", "Volume", "Druggability"],
line: { color: PALETTE[i] },
})), { title: `Pocket chemistry radar — ${v}`, polar: { radialaxis: { range: [0, 1] } } });
}
if (tab === 5) {
const st = counts(DATA.compounds, "status");
plot("p_lib", [{ type: "pie", labels: Object.keys(st), values: Object.values(st), marker: { colors: PALETTE } }], { title: "Library composition" });
const sample = DATA.compounds.filter((_, i) => i % 4 === 0);
plot("p_chem", [{
type: "scattergl", mode: "markers",
x: sample.map((r) => num(r.mw)), y: sample.map((r) => num(r.logp)),
marker: { size: 5, opacity: 0.45, color: "#0E7C7B" },
}], { title: "Physicochemical space (MW vs logP)", xaxis: { title: "MW" }, yaxis: { title: "logP" } });
const scr = DATA.screening.filter((r) => r.variant === v);
if (scr.length && document.getElementById("p_ai")) {
plot("p_ai", [{ type: "histogram", x: scr.map((r) => num(r.ai_score)), marker: { color: "#1B365D" } }], { title: `AI prescreen — ${v}` });
const samp = scr.filter((_, i) => i % 3 === 0);
plot("p_conf", [{
type: "scattergl", mode: "markers",
x: samp.map((r) => num(r.ai_score)), y: samp.map((r) => num(r.ai_confidence)),
marker: { size: 6, opacity: 0.5, color: samp.map((r) => r.applicability === "In-domain" ? "#0E7C7B" : "#C45C26") },
}], { title: "Score vs confidence", xaxis: { title: "AI score" }, yaxis: { title: "Confidence" } });
}
}
if (tab === 6) {
const dsel = DATA.docking.filter((r) => r.variant === v);
if (!dsel.length) return;
const mn = Math.min(...dsel.map((r) => Math.min(num(r.dock_wt), num(r.dock_mut))));
const mx = Math.max(...dsel.map((r) => Math.max(num(r.dock_wt), num(r.dock_mut))));
plot("p_dock", [
{ type: "scatter", mode: "markers", x: dsel.map((r) => num(r.dock_wt)), y: dsel.map((r) => num(r.dock_mut)), text: dsel.map((r) => r.name), marker: { color: dsel.map((r) => num(r.mutant_preference)), colorscale: "Tealgrn", size: 8, colorbar: { title: "Sel." } } },
{ type: "scatter", mode: "lines", x: [mn, mx], y: [mn, mx], line: { dash: "dash", color: "#9CA3AF" }, name: "identity" },
], { title: "WT vs mutant docking (more negative = stronger)", xaxis: { title: "WT" }, yaxis: { title: "Mutant" } });
const statuses = uniq(dsel, "status");
plot("p_ddock", statuses.map((s, i) => ({ type: "box", name: s, y: dsel.filter((r) => r.status === s).map((r) => num(r.delta_dock)), marker: { color: PALETTE[i] } })), { title: "Δ docking (mutant − WT)" });
const topd = [...dsel].sort((a, b) => num(a.dock_mut) - num(b.dock_mut)).slice(0, 12);
const residues = [...new Set(topd.flatMap((r) => String(r.key_contacts).split(",")))];
plot("p_fp", [{
type: "heatmap",
x: residues,
y: topd.map((r) => r.name),
z: topd.map((r) => residues.map((res) => String(r.key_contacts).split(",").includes(res) ? 1 : 0)),
colorscale: "Teal",
}], { title: "Interaction fingerprint — top 12 mutant poses" });
}
if (tab === 7) {
const mdSel = DATA.md.filter((r) => r.variant === v);
if (!mdSel.length) return;
const agg = aggregateMd(mdSel);
plot("p_md", [{
type: "scatter", mode: "markers",
x: agg.map((r) => r.ligand_rmsd), y: agg.map((r) => r.mmgbsa), text: agg.map((r) => r.name),
marker: { size: agg.map((r) => 8 + 20 * r.contact_occupancy), color: "#0E7C7B" },
}], { title: "Ligand RMSD vs MM/GBSA", xaxis: { title: "RMSD" }, yaxis: { title: "MM/GBSA" } });
const ts = DATA.md_timeseries.filter((r) => r.variant === "p.Y220C" || r.variant === v);
if (v === "p.Y220C" && ts.length) {
const names = uniq(ts, "name");
plot("p_ts", names.map((n, i) => {
const pts = {};
ts.filter((r) => r.name === n).forEach((r) => {
pts[r.time_ns] = pts[r.time_ns] || [];
pts[r.time_ns].push(num(r.ligand_rmsd));
});
const xs = Object.keys(pts).map(Number).sort((a, b) => a - b);
return { type: "scatter", mode: "lines", name: n, x: xs, y: xs.map((t) => pts[t].reduce((a, b) => a + b, 0) / pts[t].length), line: { color: PALETTE[i % PALETTE.length] } };
}), { title: "Y220C ligand RMSD vs time (mean of replicas)" });
} else {
plot("p_ts", [1, 2, 3].map((rep, i) => ({ type: "box", name: "Replica " + rep, y: mdSel.filter((r) => num(r.replica) === rep).map((r) => num(r.ligand_rmsd)), marker: { color: PALETTE[i] } })), { title: `Replica RMSD — ${v}` });
}
}
if (tab === 8 && top1) {
const axes = ["Bmut", "Sselectivity", "MDstability", "Frescue", "ADMET", "Evidence"];
plot("p_comp", rankSel.slice(0, 5).map((r, i) => ({
type: "scatterpolar", fill: "toself", name: String(r.name).slice(0, 16),
r: axes.map((a) => num(r[a])).concat([num(r.Bmut)]),
theta: axes.concat(["Bmut"]),
line: { color: PALETTE[i] },
})), { title: "Component scores — top 5", polar: { radialaxis: { range: [0, 1] } } });
const recColor = { Go: "#2F855A", Hold: "#C45C26", "No-go": "#C53030" };
plot("p_3", [{
type: "scatter3d", mode: "markers",
x: rankSel.slice(0, 80).map((r) => num(r.Sselectivity)),
y: rankSel.slice(0, 80).map((r) => num(r.ADMET)),
z: rankSel.slice(0, 80).map((r) => num(r.rescue_score)),
text: rankSel.slice(0, 80).map((r) => r.name),
marker: { size: 4, color: rankSel.slice(0, 80).map((r) => recColor[r.recommendation] || "#4A5568") },
}], { title: "Selectivity × ADMET × Rescue", scene: { xaxis: { title: "Sel" }, yaxis: { title: "ADMET" }, zaxis: { title: "Score" } } });
const top15 = rankSel.slice(0, 15);
plot("p_rankbar", [{ type: "bar", x: top15.map((r) => r.name), y: top15.map((r) => num(r.rescue_score)), marker: { color: top15.map((r) => recColor[r.recommendation] || "#1B365D") } }], { title: "Ranked Rescue / Opportunity Score", xaxis: { tickangle: -40 } });
const w = weights();
const terms = [
["Bmut", w.Bmut * num(top1.Bmut)],
["Selectivity", w.Sselectivity * num(top1.Sselectivity)],
["MD", w.MDstability * num(top1.MDstability)],
["Rescue", w.Frescue * num(top1.Frescue)],
["ADMET", w.ADMET * num(top1.ADMET)],
["Evidence", w.Evidence * num(top1.Evidence)],
["Risk", -w.Risk * num(top1.Risk)],
];
plot("p_wbar", [{ type: "bar", x: terms.map((t) => t[0]), y: terms.map((t) => t[1]), marker: { color: terms.map((t) => t[1] >= 0 ? "#0E7C7B" : "#C53030") } }], { title: `Weighted contributions — ${top1.name}` });
}
if (tab === 9) {
plot("p_auroc", [
{ type: "bar", name: "AUROC", x: DATA.validation.map((r) => r.model), y: DATA.validation.map((r) => num(r.auroc)), marker: { color: "#1B365D" } },
{ type: "bar", name: "PR-AUC", x: DATA.validation.map((r) => r.model), y: DATA.validation.map((r) => num(r.pr_auc)), marker: { color: "#0E7C7B" } },
], { barmode: "group", title: "Y220C enrichment — scaffold-split" });
plot("p_ef", [
{ type: "bar", name: "EF1%", x: DATA.validation.map((r) => r.model), y: DATA.validation.map((r) => num(r.ef1)), marker: { color: "#C45C26" } },
{ type: "bar", name: "EF5%", x: DATA.validation.map((r) => r.model), y: DATA.validation.map((r) => num(r.ef5)), marker: { color: "#D4A017" } },
], { barmode: "group", title: "Early enrichment" });
const samp = DATA.admet.filter((_, i) => i % 3 === 0);
const st = uniq(samp, "status");
plot("p_herg", st.map((s, i) => ({ type: "box", name: s, y: samp.filter((r) => r.status === s).map((r) => num(r.herg_risk)), marker: { color: PALETTE[i] } })), { title: "hERG risk by status" });
plot("p_exp", [{
type: "scattergl", mode: "markers",
x: samp.map((r) => num(r.permeability)), y: samp.map((r) => num(r.solubility)),
marker: { size: 5, opacity: 0.4, color: samp.map((r) => bool(r.exposure_feasible) ? "#0E7C7B" : "#C53030") },
}], { title: "Exposure feasibility", xaxis: { title: "Permeability" }, yaxis: { title: "Solubility" } });
}
if (tab === 10) {
plot("p_jobs", [{ type: "bar", x: DATA.jobs.map((r) => num(r.runtime_min)), y: DATA.jobs.map((r) => r.service), orientation: "h", marker: { color: "#0E7C7B" } }], { title: "Example job runtimes for the Y220C benchmark pass" });
}
}
function fillSelect(id, values, preferred) {
const el = document.getElementById(id);
const current = el.value;
el.innerHTML = "";
values.forEach((v) => {
const o = document.createElement("option");
o.value = v; o.textContent = v;
el.appendChild(o);
});
if (values.includes(current)) el.value = current;
else if (preferred && values.includes(preferred)) el.value = preferred;
}
async function init() {
const loaded = await Promise.all(FILES.map(loadCsv));
FILES.forEach((n, i) => { DATA[n] = loaded[i]; });
const bar = document.getElementById("tabBar");
TABS.forEach((t, i) => {
const b = document.createElement("button");
b.textContent = t;
b.onclick = () => { activeTab = i; [...bar.children].forEach((x, j) => x.classList.toggle("active", j === i)); render(); };
if (i === 0) b.classList.add("active");
bar.appendChild(b);
});
fillSelect("exonSelect", ["All", ...uniq(DATA.variants, "exon").sort()]);
fillSelect("typeSelect", ["All", ...uniq(DATA.variants, "type_inferred")]);
fillSelect("variantSelect", uniq(DATA.variants, "hgvs_p"), "p.Y220C");
document.getElementById("variantSelect").addEventListener("change", render);
["exonSelect", "typeSelect"].forEach((id) => {
document.getElementById(id).addEventListener("change", () => {
syncVariantOptions();
render();
});
});
Object.keys(DEFAULT_WEIGHTS).forEach((key) => {
const input = document.getElementById(`w_${key}`);
const output = document.getElementById(`out_${key}`);
input.addEventListener("input", () => {
output.value = num(input.value).toFixed(2);
render();
});
});
document.getElementById("resetWeights").addEventListener("click", () => {
Object.entries(DEFAULT_WEIGHTS).forEach(([key, value]) => {
document.getElementById(`w_${key}`).value = value;
document.getElementById(`out_${key}`).value = value.toFixed(2);
});
render();
});
render();
}
init().catch((e) => {
document.getElementById("content").innerHTML = `<p class="warn">Failed to load data: ${e}</p>`;
});