Skip to content

Commit 3278096

Browse files
committed
Update deployed CysVis app
1 parent 7dad06f commit 3278096

5 files changed

Lines changed: 187 additions & 1 deletion

File tree

d5sc9ge/cysvis/css/style.css

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@ input {
255255
margin-top: 18px;
256256
}
257257

258+
.table-panel {
259+
margin-top: 18px;
260+
}
261+
258262
.detail-content {
259263
min-height: 120px;
260264
}
@@ -333,6 +337,52 @@ input {
333337
border: 1px solid var(--panel-border);
334338
}
335339

340+
.table-shell {
341+
overflow-x: auto;
342+
}
343+
344+
.table-caption {
345+
margin-bottom: 12px;
346+
color: var(--muted);
347+
font-size: 0.9rem;
348+
}
349+
350+
.table-empty {
351+
margin: 0;
352+
color: var(--muted);
353+
}
354+
355+
.cysteine-table {
356+
width: 100%;
357+
border-collapse: collapse;
358+
min-width: 760px;
359+
}
360+
361+
.cysteine-table th,
362+
.cysteine-table td {
363+
padding: 10px 12px;
364+
border-bottom: 1px solid var(--panel-border);
365+
text-align: left;
366+
vertical-align: top;
367+
font-size: 0.92rem;
368+
}
369+
370+
.cysteine-table th {
371+
color: var(--muted);
372+
font-weight: 600;
373+
font-size: 0.84rem;
374+
text-transform: uppercase;
375+
letter-spacing: 0.04em;
376+
}
377+
378+
.cysteine-row {
379+
cursor: pointer;
380+
}
381+
382+
.cysteine-row:hover {
383+
background: #f3f7f9;
384+
}
385+
336386
@media (max-width: 980px) {
337387
.app-shell {
338388
padding: 16px;

d5sc9ge/cysvis/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ <h2>Legend</h2>
8282
<h2>Residue Detail</h2>
8383
<div id="detail-content" class="detail-content"></div>
8484
</section>
85+
86+
<section class="table-panel panel">
87+
<h2>Cysteine Table</h2>
88+
<div id="cysteine-table" class="table-shell"></div>
89+
</section>
8590
</div>
8691

8792
<script src="./js/data.js"></script>

d5sc9ge/cysvis/js/app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
onLayerToggle: handleLayerToggle,
3030
onExportPng: handleExportPng,
3131
onAnnotationLayerChange: handleAnnotationLayerChange,
32+
onCysteineTableSelect: handleResidueSelected,
3233
});
3334

3435
const viewer = new CysVisViewer({
@@ -49,6 +50,7 @@
4950
ui.renderDomainControls(state.protein, state.renderState.visibleDomains);
5051
ui.renderAnnotationLayerControl(state.renderState.annotationLayer);
5152
ui.renderLegend(state.renderState.encodingScheme, state.renderState.annotationLayer);
53+
ui.renderCysteineTable(state.protein, state.variantsData, state.hotspotData, state.renderState.annotationLayer);
5254
ui.showDefaultDetail(state.protein, state.variantsData, state.hotspotData);
5355
}
5456

d5sc9ge/cysvis/js/ui.js

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
class CysVisUI {
2-
constructor({ onProteinSubmit, onDomainToggle, onLayerToggle, onExportPng, onAnnotationLayerChange }) {
2+
constructor({ onProteinSubmit, onDomainToggle, onLayerToggle, onExportPng, onAnnotationLayerChange, onCysteineTableSelect }) {
33
this.onProteinSubmit = onProteinSubmit;
44
this.onDomainToggle = onDomainToggle;
55
this.onLayerToggle = onLayerToggle;
66
this.onExportPng = onExportPng;
77
this.onAnnotationLayerChange = onAnnotationLayerChange;
8+
this.onCysteineTableSelect = onCysteineTableSelect;
89
this.domainControlsElement = document.getElementById("domain-controls");
910
this.summaryElement = document.getElementById("protein-summary");
1011
this.proteinNoteElement = document.getElementById("protein-note");
@@ -18,6 +19,7 @@ class CysVisUI {
1819
this.cysteineToggle = document.getElementById("toggle-cysteines");
1920
this.variantToggle = document.getElementById("toggle-variants");
2021
this.surfaceToggle = document.getElementById("toggle-surface");
22+
this.cysteineTableElement = document.getElementById("cysteine-table");
2123
}
2224

2325
init() {
@@ -122,6 +124,82 @@ class CysVisUI {
122124
});
123125
}
124126

127+
renderCysteineTable(protein, variantsData, hotspotData, annotationLayer) {
128+
if (!protein.cysteines.length) {
129+
this.cysteineTableElement.innerHTML = "<p class=\"table-empty\">No cysteine residues are available for this protein yet.</p>";
130+
return;
131+
}
132+
133+
const rows = protein.cysteines
134+
.map((cysteine) => {
135+
const exactGroup = variantsData.groupsByResidue[cysteine.resi];
136+
const hotspot = hotspotData.byResidue[cysteine.resi];
137+
const annotationSummary = cysteine.annotations.length
138+
? cysteine.annotations.map((annotation) => CYSVIS_ANNOTATION_LAYERS[annotation]?.label || annotation).join(", ")
139+
: "None";
140+
const hasGenericAutoNote =
141+
typeof cysteine.notes === "string" &&
142+
cysteine.notes.startsWith("Auto-detected cysteine from the UniProt canonical sequence.");
143+
const hotspotPValue = hotspotData.loading
144+
? "..."
145+
: hotspotData.error
146+
? "n/a"
147+
: hotspotData.summary.pathogenicResidueCount === 0
148+
? "n/a"
149+
: hotspot?.empiricalPValue != null
150+
? hotspot.empiricalPValue.toFixed(3)
151+
: "n/a";
152+
const hotspotPercentile =
153+
hotspot?.percentile != null ? `${hotspot.percentile.toFixed(1)}` : hotspotData.loading ? "..." : "n/a";
154+
const directClinVar = exactGroup ? `${exactGroup.count} (${exactGroup.significanceLabel})` : "0";
155+
const note = hasGenericAutoNote ? "" : cysteine.notes ? cysteine.notes.replace(/^KEAP1-specific note:\s*/i, "") : "";
156+
157+
return `
158+
<tr class="cysteine-row" data-resi="${cysteine.resi}">
159+
<td>C${cysteine.resi}</td>
160+
<td>${cysteine.domain}</td>
161+
<td>${annotationSummary}</td>
162+
<td>${directClinVar}</td>
163+
<td>${hotspotPValue}</td>
164+
<td>${hotspotPercentile}</td>
165+
<td title="${escapeHtml(note)}">${note ? escapeHtml(truncateText(note, 120)) : "-"}</td>
166+
</tr>
167+
`;
168+
})
169+
.join("");
170+
171+
this.cysteineTableElement.innerHTML = `
172+
<div class="table-caption">
173+
Showing ${protein.cysteines.length} cysteine residues for ${protein.displayName}.
174+
Active annotation layer: ${CYSVIS_ANNOTATION_LAYERS[annotationLayer].label}.
175+
</div>
176+
<table class="cysteine-table">
177+
<thead>
178+
<tr>
179+
<th>Residue</th>
180+
<th>Domain</th>
181+
<th>Annotations</th>
182+
<th>ClinVar at residue</th>
183+
<th>Hotspot p</th>
184+
<th>Hotspot pct</th>
185+
<th>Notes</th>
186+
</tr>
187+
</thead>
188+
<tbody>${rows}</tbody>
189+
</table>
190+
`;
191+
192+
this.cysteineTableElement.querySelectorAll(".cysteine-row").forEach((row) => {
193+
row.addEventListener("click", () => {
194+
const residue = Number.parseInt(row.dataset.resi, 10);
195+
const cysteine = protein.cysteines.find((entry) => entry.resi === residue);
196+
if (cysteine) {
197+
this.onCysteineTableSelect(cysteine);
198+
}
199+
});
200+
});
201+
}
202+
125203
showDefaultDetail(protein, variantsData, hotspotData) {
126204
const variantLine = variantsData.loading
127205
? "ClinVar variants are loading."
@@ -319,3 +397,16 @@ class CysVisUI {
319397
`;
320398
}
321399
}
400+
401+
function truncateText(text, maxLength) {
402+
return text.length > maxLength ? `${text.slice(0, maxLength - 1)}...` : text;
403+
}
404+
405+
function escapeHtml(text) {
406+
return text
407+
.replaceAll("&", "&amp;")
408+
.replaceAll("<", "&lt;")
409+
.replaceAll(">", "&gt;")
410+
.replaceAll('"', "&quot;")
411+
.replaceAll("'", "&#39;");
412+
}

d5sc9ge/cysvis/js/viewer.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class CysVisViewer {
1111
this.currentProtein = null;
1212
this.currentVariantGroups = [];
1313
this.markerShapes = [];
14+
this.selectionShapes = [];
1415
this.renderState = null;
1516
this.pdbText = null;
1617
this.resizeHandler = () => {
@@ -102,6 +103,13 @@ class CysVisViewer {
102103
selectedCysteineResi: nextSelection.selectedCysteineResi,
103104
selectedVariantResidue: nextSelection.selectedVariantResidue,
104105
};
106+
107+
if (!this.viewer || !this.pdbText) {
108+
return;
109+
}
110+
111+
this.syncSelectionOverlay();
112+
this.viewer.render();
105113
}
106114

107115
exportPngDataUri() {
@@ -126,6 +134,7 @@ class CysVisViewer {
126134
// surface handle becomes invalid and must not be removed afterward.
127135
this.surfaceHandle = null;
128136
this.markerShapes = [];
137+
this.selectionShapes = [];
129138
this.viewer.clear();
130139
this.model = this.viewer.addModel(this.pdbText, "pdb");
131140
this.applyScene();
@@ -223,6 +232,7 @@ class CysVisViewer {
223232
}
224233

225234
this.syncSurface();
235+
this.syncSelectionOverlay();
226236
}
227237

228238
renderVariantMarker(group, scheme, geometryKey) {
@@ -276,6 +286,34 @@ class CysVisViewer {
276286
});
277287
}
278288

289+
syncSelectionOverlay() {
290+
this.selectionShapes.forEach((shape) => {
291+
this.viewer.removeShape(shape);
292+
});
293+
this.selectionShapes = [];
294+
295+
const selectedCysteineResi = this.renderState?.selectedCysteineResi;
296+
if (selectedCysteineResi == null) {
297+
return;
298+
}
299+
300+
const atom =
301+
this.model?.selectedAtoms({ chain: "A", resi: selectedCysteineResi, atom: "SG" })?.[0] ||
302+
this.model?.selectedAtoms({ chain: "A", resi: selectedCysteineResi, atom: "CA" })?.[0];
303+
if (!atom) {
304+
return;
305+
}
306+
307+
this.selectionShapes.push(
308+
this.viewer.addSphere({
309+
center: { x: atom.x, y: atom.y, z: atom.z },
310+
radius: 1.28,
311+
color: "#ffffff",
312+
opacity: 0.78,
313+
})
314+
);
315+
}
316+
279317
syncSurface() {
280318
if (this.surfaceHandle) {
281319
this.viewer.removeSurface(this.surfaceHandle);

0 commit comments

Comments
 (0)