11class 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 ( / ^ K E A P 1 - s p e c i f i c n o t e : \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 ( "&" , "&" )
408+ . replaceAll ( "<" , "<" )
409+ . replaceAll ( ">" , ">" )
410+ . replaceAll ( '"' , """ )
411+ . replaceAll ( "'" , "'" ) ;
412+ }
0 commit comments