Fix all sidebar filters and add live Rescue score reweighting
Browse files- assets/app.js +80 -18
- assets/style.css +13 -0
- docs/WORKFLOW.md +1 -0
- index.html +25 -0
assets/app.js
CHANGED
|
@@ -34,6 +34,10 @@ const STAGES = [
|
|
| 34 |
|
| 35 |
let DATA = {};
|
| 36 |
let activeTab = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
const layoutBase = {
|
| 38 |
font: { color: "#111827", family: "Inter, Arial, sans-serif", size: 12 },
|
| 39 |
paper_bgcolor: "white",
|
|
@@ -91,6 +95,42 @@ function filteredVariants() {
|
|
| 91 |
);
|
| 92 |
}
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
function plot(id, traces, extra = {}) {
|
| 95 |
Plotly.react(id, traces, { ...layoutBase, ...extra }, { responsive: true, displayModeBar: false });
|
| 96 |
}
|
|
@@ -100,16 +140,16 @@ function render() {
|
|
| 100 |
const variants = DATA.variants;
|
| 101 |
const vf = filteredVariants();
|
| 102 |
const varRow = variants.find((r) => r.hgvs_p === v) || variants[0];
|
| 103 |
-
const rankSel = DATA.rankings.filter((r) => r.variant === v)
|
| 104 |
const top1 = rankSel[0];
|
| 105 |
const structSel = DATA.structures.filter((r) => r.variant === v);
|
| 106 |
const pocketSel = DATA.pockets.filter((r) => r.variant === v);
|
| 107 |
const content = document.getElementById("content");
|
| 108 |
|
| 109 |
const views = [
|
| 110 |
-
() => overview(
|
| 111 |
() => qcTab(variants, vf),
|
| 112 |
-
() => routingTab(
|
| 113 |
() => structureTab(varRow, structSel, v),
|
| 114 |
() => pocketTab(pocketSel, v),
|
| 115 |
() => libraryTab(v),
|
|
@@ -126,6 +166,9 @@ function render() {
|
|
| 126 |
}
|
| 127 |
|
| 128 |
function overview(variants, varRow, top1, v) {
|
|
|
|
|
|
|
|
|
|
| 129 |
return `
|
| 130 |
<h2 class="section">Project snapshot — lung cancer TP53 panel</h2>
|
| 131 |
<div class="metrics">
|
|
@@ -133,8 +176,8 @@ function overview(variants, varRow, top1, v) {
|
|
| 133 |
${metric("Unique HGVS", uniq(variants, "hgvs_p").length)}
|
| 134 |
${metric("QC flags", variants.filter((r) => r.qc_status === "Flagged").length)}
|
| 135 |
${metric("Library size", DATA.compounds.length.toLocaleString())}
|
| 136 |
-
${metric("Docked pairs",
|
| 137 |
-
${metric("MD replicas",
|
| 138 |
</div>
|
| 139 |
<h2 class="section">Focus variant</h2>
|
| 140 |
<div class="metrics">
|
|
@@ -451,30 +494,30 @@ function counts(rows, key) {
|
|
| 451 |
|
| 452 |
function drawPlots(tab, v, varRow, rankSel, top1, structSel, pocketSel, vf) {
|
| 453 |
if (tab === 0) {
|
| 454 |
-
const exons = uniq(
|
| 455 |
plot("p_sun", exons.map((e, i) => {
|
| 456 |
-
const sub =
|
| 457 |
const types = counts(sub, "type_inferred");
|
| 458 |
return { type: "bar", name: e, x: Object.keys(types), y: Object.values(types), marker: { color: PALETTE[i % PALETTE.length] } };
|
| 459 |
}), { barmode: "stack", title: "Observations by exon and inferred class" });
|
| 460 |
-
const ordered = [...
|
| 461 |
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 } });
|
| 462 |
}
|
| 463 |
if (tab === 1) {
|
| 464 |
-
const types = uniq(
|
| 465 |
plot("p_af", types.map((t, i) => ({
|
| 466 |
-
type: "histogram", name: t, x:
|
| 467 |
})), { barmode: "overlay", title: "Allele frequency by inferred class" });
|
| 468 |
plot("p_map", [{
|
| 469 |
type: "scatter", mode: "markers",
|
| 470 |
-
x:
|
| 471 |
-
y:
|
| 472 |
-
text:
|
| 473 |
-
marker: { size:
|
| 474 |
}], { title: "Residue map — size = priority, orange = QC flag", xaxis: { title: "Residue" }, yaxis: { title: "Allele frequency" } });
|
| 475 |
}
|
| 476 |
if (tab === 2) {
|
| 477 |
-
const c = counts(
|
| 478 |
plot("p_route", [{ type: "bar", x: Object.values(c), y: Object.keys(c), orientation: "h", marker: { color: "#1B365D" } }], { title: "Observations per route" });
|
| 479 |
}
|
| 480 |
if (tab === 3) {
|
|
@@ -599,7 +642,7 @@ function drawPlots(tab, v, varRow, rankSel, top1, structSel, pocketSel, vf) {
|
|
| 599 |
}], { title: "Selectivity × ADMET × Rescue", scene: { xaxis: { title: "Sel" }, yaxis: { title: "ADMET" }, zaxis: { title: "Score" } } });
|
| 600 |
const top15 = rankSel.slice(0, 15);
|
| 601 |
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 } });
|
| 602 |
-
const w =
|
| 603 |
const terms = [
|
| 604 |
["Bmut", w.Bmut * num(top1.Bmut)],
|
| 605 |
["Selectivity", w.Sselectivity * num(top1.Sselectivity)],
|
|
@@ -661,8 +704,27 @@ async function init() {
|
|
| 661 |
fillSelect("exonSelect", ["All", ...uniq(DATA.variants, "exon").sort()]);
|
| 662 |
fillSelect("typeSelect", ["All", ...uniq(DATA.variants, "type_inferred")]);
|
| 663 |
fillSelect("variantSelect", uniq(DATA.variants, "hgvs_p"), "p.Y220C");
|
| 664 |
-
|
| 665 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
});
|
| 667 |
render();
|
| 668 |
}
|
|
|
|
| 34 |
|
| 35 |
let DATA = {};
|
| 36 |
let activeTab = 0;
|
| 37 |
+
const DEFAULT_WEIGHTS = {
|
| 38 |
+
Bmut: 0.22, Sselectivity: 0.18, MDstability: 0.16,
|
| 39 |
+
Frescue: 0.14, ADMET: 0.12, Evidence: 0.10, Risk: 0.08,
|
| 40 |
+
};
|
| 41 |
const layoutBase = {
|
| 42 |
font: { color: "#111827", family: "Inter, Arial, sans-serif", size: 12 },
|
| 43 |
paper_bgcolor: "white",
|
|
|
|
| 95 |
);
|
| 96 |
}
|
| 97 |
|
| 98 |
+
function weights() {
|
| 99 |
+
const out = {};
|
| 100 |
+
Object.keys(DEFAULT_WEIGHTS).forEach((key) => {
|
| 101 |
+
out[key] = num(document.getElementById(`w_${key}`).value);
|
| 102 |
+
});
|
| 103 |
+
return out;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
function rescore(rows) {
|
| 107 |
+
const w = weights();
|
| 108 |
+
const rescored = rows.map((r) => {
|
| 109 |
+
const score = Math.max(0, Math.min(1,
|
| 110 |
+
w.Bmut * num(r.Bmut)
|
| 111 |
+
+ w.Sselectivity * num(r.Sselectivity)
|
| 112 |
+
+ w.MDstability * num(r.MDstability)
|
| 113 |
+
+ w.Frescue * num(r.Frescue)
|
| 114 |
+
+ w.ADMET * num(r.ADMET)
|
| 115 |
+
+ w.Evidence * num(r.Evidence)
|
| 116 |
+
- w.Risk * num(r.Risk)
|
| 117 |
+
));
|
| 118 |
+
const recommendation = r.admet_flag === "Severe liability"
|
| 119 |
+
? "No-go"
|
| 120 |
+
: (score >= 0.74 && num(r.confidence) >= 0.68 ? "Go" : (score >= 0.58 ? "Hold" : "No-go"));
|
| 121 |
+
return { ...r, rescue_score: +score.toFixed(3), recommendation };
|
| 122 |
+
});
|
| 123 |
+
rescored.sort((a, b) => b.rescue_score - a.rescue_score);
|
| 124 |
+
return rescored.map((r, i) => ({ ...r, rank: i + 1 }));
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
function syncVariantOptions() {
|
| 128 |
+
const allowed = uniq(filteredVariants(), "hgvs_p");
|
| 129 |
+
const current = currentVariant();
|
| 130 |
+
fillSelect("variantSelect", allowed, allowed.includes("p.Y220C") ? "p.Y220C" : allowed[0]);
|
| 131 |
+
if (allowed.includes(current)) document.getElementById("variantSelect").value = current;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
function plot(id, traces, extra = {}) {
|
| 135 |
Plotly.react(id, traces, { ...layoutBase, ...extra }, { responsive: true, displayModeBar: false });
|
| 136 |
}
|
|
|
|
| 140 |
const variants = DATA.variants;
|
| 141 |
const vf = filteredVariants();
|
| 142 |
const varRow = variants.find((r) => r.hgvs_p === v) || variants[0];
|
| 143 |
+
const rankSel = rescore(DATA.rankings.filter((r) => r.variant === v));
|
| 144 |
const top1 = rankSel[0];
|
| 145 |
const structSel = DATA.structures.filter((r) => r.variant === v);
|
| 146 |
const pocketSel = DATA.pockets.filter((r) => r.variant === v);
|
| 147 |
const content = document.getElementById("content");
|
| 148 |
|
| 149 |
const views = [
|
| 150 |
+
() => overview(vf, varRow, top1, v),
|
| 151 |
() => qcTab(variants, vf),
|
| 152 |
+
() => routingTab(vf),
|
| 153 |
() => structureTab(varRow, structSel, v),
|
| 154 |
() => pocketTab(pocketSel, v),
|
| 155 |
() => libraryTab(v),
|
|
|
|
| 166 |
}
|
| 167 |
|
| 168 |
function overview(variants, varRow, top1, v) {
|
| 169 |
+
const allowed = new Set(variants.map((r) => r.hgvs_p));
|
| 170 |
+
const docked = DATA.docking.filter((r) => allowed.has(r.variant));
|
| 171 |
+
const mdRows = DATA.md.filter((r) => allowed.has(r.variant));
|
| 172 |
return `
|
| 173 |
<h2 class="section">Project snapshot — lung cancer TP53 panel</h2>
|
| 174 |
<div class="metrics">
|
|
|
|
| 176 |
${metric("Unique HGVS", uniq(variants, "hgvs_p").length)}
|
| 177 |
${metric("QC flags", variants.filter((r) => r.qc_status === "Flagged").length)}
|
| 178 |
${metric("Library size", DATA.compounds.length.toLocaleString())}
|
| 179 |
+
${metric("Docked pairs", docked.length.toLocaleString())}
|
| 180 |
+
${metric("MD replicas", mdRows.length.toLocaleString())}
|
| 181 |
</div>
|
| 182 |
<h2 class="section">Focus variant</h2>
|
| 183 |
<div class="metrics">
|
|
|
|
| 494 |
|
| 495 |
function drawPlots(tab, v, varRow, rankSel, top1, structSel, pocketSel, vf) {
|
| 496 |
if (tab === 0) {
|
| 497 |
+
const exons = uniq(vf, "exon");
|
| 498 |
plot("p_sun", exons.map((e, i) => {
|
| 499 |
+
const sub = vf.filter((r) => r.exon === e);
|
| 500 |
const types = counts(sub, "type_inferred");
|
| 501 |
return { type: "bar", name: e, x: Object.keys(types), y: Object.values(types), marker: { color: PALETTE[i % PALETTE.length] } };
|
| 502 |
}), { barmode: "stack", title: "Observations by exon and inferred class" });
|
| 503 |
+
const ordered = [...vf].sort((a, b) => num(b.priority_score) - num(a.priority_score));
|
| 504 |
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 } });
|
| 505 |
}
|
| 506 |
if (tab === 1) {
|
| 507 |
+
const types = uniq(vf, "type_inferred");
|
| 508 |
plot("p_af", types.map((t, i) => ({
|
| 509 |
+
type: "histogram", name: t, x: vf.filter((r) => r.type_inferred === t).map((r) => num(r.allele_frequency)), marker: { color: PALETTE[i] },
|
| 510 |
})), { barmode: "overlay", title: "Allele frequency by inferred class" });
|
| 511 |
plot("p_map", [{
|
| 512 |
type: "scatter", mode: "markers",
|
| 513 |
+
x: vf.map((r) => num(r.position)),
|
| 514 |
+
y: vf.map((r) => num(r.allele_frequency)),
|
| 515 |
+
text: vf.map((r) => r.hgvs_p),
|
| 516 |
+
marker: { size: vf.map((r) => 8 + 18 * num(r.priority_score)), color: vf.map((r) => r.qc_status === "Flagged" ? "#C45C26" : "#0E7C7B") },
|
| 517 |
}], { title: "Residue map — size = priority, orange = QC flag", xaxis: { title: "Residue" }, yaxis: { title: "Allele frequency" } });
|
| 518 |
}
|
| 519 |
if (tab === 2) {
|
| 520 |
+
const c = counts(vf, "route");
|
| 521 |
plot("p_route", [{ type: "bar", x: Object.values(c), y: Object.keys(c), orientation: "h", marker: { color: "#1B365D" } }], { title: "Observations per route" });
|
| 522 |
}
|
| 523 |
if (tab === 3) {
|
|
|
|
| 642 |
}], { title: "Selectivity × ADMET × Rescue", scene: { xaxis: { title: "Sel" }, yaxis: { title: "ADMET" }, zaxis: { title: "Score" } } });
|
| 643 |
const top15 = rankSel.slice(0, 15);
|
| 644 |
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 } });
|
| 645 |
+
const w = weights();
|
| 646 |
const terms = [
|
| 647 |
["Bmut", w.Bmut * num(top1.Bmut)],
|
| 648 |
["Selectivity", w.Sselectivity * num(top1.Sselectivity)],
|
|
|
|
| 704 |
fillSelect("exonSelect", ["All", ...uniq(DATA.variants, "exon").sort()]);
|
| 705 |
fillSelect("typeSelect", ["All", ...uniq(DATA.variants, "type_inferred")]);
|
| 706 |
fillSelect("variantSelect", uniq(DATA.variants, "hgvs_p"), "p.Y220C");
|
| 707 |
+
document.getElementById("variantSelect").addEventListener("change", render);
|
| 708 |
+
["exonSelect", "typeSelect"].forEach((id) => {
|
| 709 |
+
document.getElementById(id).addEventListener("change", () => {
|
| 710 |
+
syncVariantOptions();
|
| 711 |
+
render();
|
| 712 |
+
});
|
| 713 |
+
});
|
| 714 |
+
Object.keys(DEFAULT_WEIGHTS).forEach((key) => {
|
| 715 |
+
const input = document.getElementById(`w_${key}`);
|
| 716 |
+
const output = document.getElementById(`out_${key}`);
|
| 717 |
+
input.addEventListener("input", () => {
|
| 718 |
+
output.value = num(input.value).toFixed(2);
|
| 719 |
+
render();
|
| 720 |
+
});
|
| 721 |
+
});
|
| 722 |
+
document.getElementById("resetWeights").addEventListener("click", () => {
|
| 723 |
+
Object.entries(DEFAULT_WEIGHTS).forEach(([key, value]) => {
|
| 724 |
+
document.getElementById(`w_${key}`).value = value;
|
| 725 |
+
document.getElementById(`out_${key}`).value = value.toFixed(2);
|
| 726 |
+
});
|
| 727 |
+
render();
|
| 728 |
});
|
| 729 |
render();
|
| 730 |
}
|
assets/style.css
CHANGED
|
@@ -31,6 +31,19 @@ body {
|
|
| 31 |
border-radius: 6px;
|
| 32 |
border: none;
|
| 33 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
.muted { color: #c5d0e3; font-size: 13px; }
|
| 35 |
.small { font-size: 12px; line-height: 1.45; }
|
| 36 |
main { flex: 1; padding: 18px 22px 40px; overflow-x: hidden; }
|
|
|
|
| 31 |
border-radius: 6px;
|
| 32 |
border: none;
|
| 33 |
}
|
| 34 |
+
.weight-panel {
|
| 35 |
+
margin-top: 18px;
|
| 36 |
+
padding-top: 14px;
|
| 37 |
+
border-top: 1px solid rgba(255,255,255,0.22);
|
| 38 |
+
}
|
| 39 |
+
.weight-panel h3 { margin: 0 0 10px; font-size: 14px; }
|
| 40 |
+
.weight-panel label { margin: 9px 0; }
|
| 41 |
+
.weight-panel output { float: right; font-variant-numeric: tabular-nums; }
|
| 42 |
+
.weight-panel input[type="range"] { width: 100%; margin-top: 5px; accent-color: #35b8b5; }
|
| 43 |
+
.weight-panel button {
|
| 44 |
+
width: 100%; margin-top: 8px; padding: 7px; border: 1px solid #9fb2cf;
|
| 45 |
+
border-radius: 6px; color: white; background: transparent; cursor: pointer;
|
| 46 |
+
}
|
| 47 |
.muted { color: #c5d0e3; font-size: 13px; }
|
| 48 |
.small { font-size: 12px; line-height: 1.45; }
|
| 49 |
main { flex: 1; padding: 18px 22px 40px; overflow-x: hidden; }
|
docs/WORKFLOW.md
CHANGED
|
@@ -19,3 +19,4 @@
|
|
| 19 |
|
| 20 |
- 2026-08-13: Created `tp53-discovery-platform` covering variant QC, routing, structures, pockets, AI screen, docking, MD, ranking, validation, architecture and reports.
|
| 21 |
- 2026-08-14: Physics/literature calculation engine (NP_000537.3 QC, published ΔΔG, ΔG=RT ln Kd, Lipinski/Veber/Egan/QED, consensus R(c,m)). Variant and docking CSV ingest for real-world evaluation. Methods tab and experimental top-10 plan.
|
|
|
|
|
|
| 19 |
|
| 20 |
- 2026-08-13: Created `tp53-discovery-platform` covering variant QC, routing, structures, pockets, AI screen, docking, MD, ranking, validation, architecture and reports.
|
| 21 |
- 2026-08-14: Physics/literature calculation engine (NP_000537.3 QC, published ΔΔG, ΔG=RT ln Kd, Lipinski/Veber/Egan/QED, consensus R(c,m)). Variant and docking CSV ingest for real-world evaluation. Methods tab and experimental top-10 plan.
|
| 22 |
+
- 2026-08-14: Fixed hosted sidebar reactivity. Exon/class filters now cascade into focus variants, KPIs, plots, routing, docking/MD counts, and tables. Added seven live Rescue/Opportunity weight sliders with immediate re-ranking and recommendation updates.
|
index.html
CHANGED
|
@@ -21,6 +21,31 @@
|
|
| 21 |
<label>Inferred class
|
| 22 |
<select id="typeSelect"><option>All</option></select>
|
| 23 |
</label>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
<p class="muted small">Rescue / Opportunity Score uses the documented multi-objective weights. Computational estimates only.</p>
|
| 25 |
</aside>
|
| 26 |
<main>
|
|
|
|
| 21 |
<label>Inferred class
|
| 22 |
<select id="typeSelect"><option>All</option></select>
|
| 23 |
</label>
|
| 24 |
+
<div class="weight-panel">
|
| 25 |
+
<h3>Rescue score weights</h3>
|
| 26 |
+
<label>Mutant binding <output id="out_Bmut">0.22</output>
|
| 27 |
+
<input id="w_Bmut" type="range" min="0" max="0.40" step="0.01" value="0.22" />
|
| 28 |
+
</label>
|
| 29 |
+
<label>Mutant selectivity <output id="out_Sselectivity">0.18</output>
|
| 30 |
+
<input id="w_Sselectivity" type="range" min="0" max="0.40" step="0.01" value="0.18" />
|
| 31 |
+
</label>
|
| 32 |
+
<label>MD stability <output id="out_MDstability">0.16</output>
|
| 33 |
+
<input id="w_MDstability" type="range" min="0" max="0.40" step="0.01" value="0.16" />
|
| 34 |
+
</label>
|
| 35 |
+
<label>Rescue relevance <output id="out_Frescue">0.14</output>
|
| 36 |
+
<input id="w_Frescue" type="range" min="0" max="0.40" step="0.01" value="0.14" />
|
| 37 |
+
</label>
|
| 38 |
+
<label>ADMET / exposure <output id="out_ADMET">0.12</output>
|
| 39 |
+
<input id="w_ADMET" type="range" min="0" max="0.40" step="0.01" value="0.12" />
|
| 40 |
+
</label>
|
| 41 |
+
<label>Evidence <output id="out_Evidence">0.10</output>
|
| 42 |
+
<input id="w_Evidence" type="range" min="0" max="0.40" step="0.01" value="0.10" />
|
| 43 |
+
</label>
|
| 44 |
+
<label>Risk penalty <output id="out_Risk">0.08</output>
|
| 45 |
+
<input id="w_Risk" type="range" min="0" max="0.30" step="0.01" value="0.08" />
|
| 46 |
+
</label>
|
| 47 |
+
<button id="resetWeights" type="button">Reset weights</button>
|
| 48 |
+
</div>
|
| 49 |
<p class="muted small">Rescue / Opportunity Score uses the documented multi-objective weights. Computational estimates only.</p>
|
| 50 |
</aside>
|
| 51 |
<main>
|