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 `

${title}

${value}

`; } function table(rows, cols) { if (!rows.length) return "

No rows for the current selection.

"; const head = cols.map((c) => `${c}`).join(""); const body = rows.map((r) => `${cols.map((c) => `${r[c] ?? ""}`).join("")}`).join(""); return `
${head}${body}
`; } 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 `

Project snapshot — lung cancer TP53 panel

${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())}

Focus variant

${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")}

Nine-stage decision engine

${STAGES.map(([h, p]) => `

${h}

${p}

`).join("")}
${top1 ? `

Preferred reporting language: ${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}.

` : ""} `; } function qcTab(variants, vf) { const flagged = variants.filter((r) => r.qc_status === "Flagged"); return `

Variant intelligence and data-quality gate

Source labels are retained. Contradictions are flagged, not silently rewritten.

${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)}

QC exceptions requiring human review

${table(flagged, ["obs_id", "variant_raw", "hgvs_p", "type_source", "type_inferred", "effect_source", "qc_flags", "allele_frequency"])}

Full observation table

${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 `

Mutation-specific routing — do not send every variant through docking

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.

${table(uniqV, ["hgvs_p", "type_inferred", "domain", "hotspot", "route", "priority_score", "structure_tractable"])} `; } function structureTab(varRow, structSel, v) { const s = structSel[0]; return `

Wild-type vs mutant structure comparison

${s ? `
${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))}
` : ""}
${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 `

Pocket detection and mutation-specific druggability

${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 `

Compound library assembly and AI virtual screening

${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) + "%")}

Reference / control ligands

${table(controls, ["compound_id", "name", "status", "note", "mw", "logp"])}

Top AI-ranked compounds for ${v}

${scr.length ? table(scr, ["name", "status", "ai_score", "ai_confidence", "applicability"]) : "

AI screening is run on missense / structure-tractable variants only.

"} `; } function dockTab(v) { const dsel = DATA.docking.filter((r) => r.variant === v); if (!dsel.length) return `

Matched wild-type vs mutant docking

This variant is not routed to the standard docking workflow.

`; const top = [...dsel].sort((a, b) => num(a.dock_mut) - num(b.dock_mut)).slice(0, 20); return `

Matched wild-type vs mutant docking

${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)}
${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 `

Molecular dynamics

MD is reserved for top-ranked complexes after docking / AI gates.

`; const agg = aggregateMd(mdSel); return `

Molecular dynamics and free-energy proxy (shortlist only)

${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) + "%")}
${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 `

Consensus ranking

No consensus shortlist — variant routed away from small-molecule docking.

`; const goN = rankSel.filter((r) => r.recommendation === "Go").length; return `

Industry dashboard — consensus Rescue / Opportunity Score

PROJECT: Lung Cancer | Variant: ${v} | Status: Complete for structure-tractable routes

${metric("Top candidate", top1.name)} ${metric("Rescue score", num(top1.rescue_score).toFixed(2))} ${metric("Confidence", num(top1.confidence).toFixed(2))} ${metric("Recommendation", `${top1.recommendation}`)} ${metric("Repurposing", top1.status)}

Next action: ${top1.next_experiment}

Go: ${goN} · Hold: ${rankSel.filter((r) => r.recommendation === "Hold").length} · No-go: ${rankSel.filter((r) => r.recommendation === "No-go").length}

${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 `

Validation strategy, success criteria, and risk register

Later-phase experimental readouts

${table(exp, ["Question", "Suggested readout"])}

Key risks and mitigations

${table(risks, ["Risk", "Why it matters", "Mitigation"])}
`; } 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 `

Cloud-native services, data objects, and API contract

Researcher UI → API gateway → variant / structure / compound / evidence services → screening and ranking models → containerized docking → GPU MD jobs → consensus report → object store + warehouse.

Runtime: 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.

Service boundaries

${table(svc, ["Microservice", "Responsibility"])}

Implemented API endpoints (long-running operations return job_id)

${table(api, ["Endpoint", "Input", "Output", "Status"])} ${table(DATA.jobs, ["job_id", "service", "task", "status", "runtime_min", "protocol_version", "container"])} `; } function methodsTab() { return `

Methods, equations, and evaluation contract

Reference sequence: NP_000537.3 / UniProt P04637 (393 aa). Missense WT amino acids are checked against this sequence.

ΔΔG (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·|ΔHKD|·b + 1.15·|Δq|·b + helix/Zn penalties. Volumes are Richards ų; hydrophobicity is Kyte–Doolittle.

Y220C cavity is the published ~200 ų mutation-induced pocket. WT still has the constitutive DNA-cleft (~210 ų).

Thermodynamics. When a published Kd exists, ΔG° = RT ln Kd (298 K). Bmut = clip((−ΔGmut−4)/8, 0, 1). Selectivity = clip((ΔGWT−ΔGmut)/3, 0, 1).

ADMET. Lipinski, Veber, Egan ellipse, Delaney ESOL, QED-like desirability, Gleeson-style hERG vs logP.

Consensus. R(c,m) = 0.22 Bmut + 0.18 S + 0.16 MD + 0.14 Frescue + 0.12 ADMET + 0.10 Evidence − 0.08 Risk.

Enrichment. AUROC = Mann–Whitney P(s+ > s); EF x% = (actives in top x%) / expected.

Submit a variant table and optional docking kcal/mol values through the platform API to score real-world data with the same transforms.

`; } function reportTab(varRow, rankSel, top1) { return `

Downloadable data

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.

${FILES.map((f) => `Download ${f}.csv`).join(" · ")}

${top1 ? `

Minimal candidate record example: ${varRow.hgvs_p} × ${top1.name} | Rescue ${num(top1.rescue_score).toFixed(2)} | ${top1.recommendation} | ${top1.reason_codes}

` : ""} ${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 = `

Failed to load data: ${e}

`; });