| 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> > 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>`; |
| }); |
|
|