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 `
${value}
No rows for the current selection.
"; const head = cols.map((c) => `${p}
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 `Source labels are retained. Contradictions are flagged, not silently rewritten.
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 `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 `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 `MD is reserved for top-ranked complexes after docking / AI gates.
`; const agg = aggregateMd(mdSel); return `No consensus shortlist — variant routed away from small-molecule docking.
`; const goN = rankSel.filter((r) => r.recommendation === "Go").length; return `PROJECT: Lung Cancer | Variant: ${v} | Status: Complete for structure-tractable routes
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 `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.
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 `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}
`; });