jiehou commited on
Commit
3b0df08
·
verified ·
1 Parent(s): c13d3ce

Upload visualization_multi.py

Browse files
Files changed (1) hide show
  1. visualization_multi.py +827 -0
visualization_multi.py ADDED
@@ -0,0 +1,827 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Multi-Structure 3D Visualization Module
3
+ Based on working pairwise visualization code
4
+ """
5
+
6
+ import numpy as np
7
+ from rmsd_utils import (
8
+ parse_residue_atoms,
9
+ get_backbone_sugar_coords_from_residue,
10
+ get_base_coords_from_residue
11
+ )
12
+
13
+
14
+ def extract_window_pdb(pdb_path, window_indices):
15
+ """
16
+ Extract specific residues from a PDB file based on window indices.
17
+ Uses the WORKING approach from the original code.
18
+ """
19
+ with open(pdb_path) as f:
20
+ lines = f.readlines()
21
+
22
+ # Get all residue numbers from the file
23
+ residues = parse_residue_atoms(pdb_path)
24
+
25
+ if not residues:
26
+ return ''.join(lines)
27
+
28
+ residue_numbers = [res['resnum'] for res in residues]
29
+
30
+ # Map window indices to actual residue numbers
31
+ target_resnums = set()
32
+ for idx in window_indices:
33
+ if idx < len(residue_numbers):
34
+ target_resnums.add(residue_numbers[idx])
35
+
36
+ if not target_resnums:
37
+ return ''.join(lines)
38
+
39
+ # Extract lines for these residues
40
+ window_lines = []
41
+ for line in lines:
42
+ if len(line) < 6:
43
+ continue
44
+
45
+ record = line[0:6].strip()
46
+ if record in ['ATOM', 'HETATM', 'HETAT']:
47
+ try:
48
+ resnum_str = line[22:26].strip()
49
+ if resnum_str:
50
+ resnum = int(resnum_str)
51
+ if resnum in target_resnums:
52
+ window_lines.append(line)
53
+ except (ValueError, IndexError):
54
+ continue
55
+ elif record in ['HEADER', 'TITLE', 'MODEL', 'ENDMDL']:
56
+ window_lines.append(line)
57
+
58
+ # Always add END record
59
+ if window_lines and not any('END' in line for line in window_lines):
60
+ window_lines.append('END\n')
61
+
62
+ result = ''.join(window_lines)
63
+
64
+ if not result or len(result) < 50:
65
+ return ''.join(lines)
66
+
67
+ return result
68
+
69
+
70
+ def transform_pdb_string(pdb_string, rotation_matrix, query_com, ref_com):
71
+ """
72
+ Apply rotation and translation to align query with reference.
73
+ Uses the WORKING transformation from original code.
74
+
75
+ CRITICAL: This is right multiplication (coord @ R), NOT left multiplication (R @ coord)
76
+
77
+ Args:
78
+ pdb_string: PDB format string
79
+ rotation_matrix: 3x3 rotation matrix from RMSD calculation
80
+ query_com: Center of mass of query structure (translate FROM)
81
+ ref_com: Center of mass of reference structure (translate TO)
82
+
83
+ Returns:
84
+ Transformed PDB string with aligned coordinates
85
+ """
86
+ lines = pdb_string.split('\n')
87
+ transformed_lines = []
88
+
89
+ for line in lines:
90
+ if len(line) < 54:
91
+ transformed_lines.append(line)
92
+ continue
93
+
94
+ record = line[0:6].strip()
95
+ if record in ['ATOM', 'HETATM', 'HETAT']:
96
+ try:
97
+ x = float(line[30:38].strip())
98
+ y = float(line[38:46].strip())
99
+ z = float(line[46:54].strip())
100
+
101
+ # Transform: (coord - query_com) @ rotation_matrix + ref_com
102
+ # This is the WORKING approach from original code
103
+ coord = np.array([x, y, z])
104
+ centered = coord - query_com # Move query to origin
105
+ rotated = np.dot(centered, rotation_matrix) # RIGHT multiplication
106
+ new_coord = rotated + ref_com # Move to reference position
107
+
108
+ # Write transformed line
109
+ new_line = (
110
+ line[:30] +
111
+ f"{new_coord[0]:8.3f}" +
112
+ f"{new_coord[1]:8.3f}" +
113
+ f"{new_coord[2]:8.3f}" +
114
+ line[54:]
115
+ )
116
+ transformed_lines.append(new_line)
117
+ except (ValueError, IndexError):
118
+ transformed_lines.append(line)
119
+ else:
120
+ transformed_lines.append(line)
121
+
122
+ return '\n'.join(transformed_lines)
123
+
124
+
125
+ def create_pairwise_visualization(ref_path, query_path, ref_window, query_window,
126
+ rotation_matrix, ref_com, query_com, rmsd,
127
+ ref_name="Reference", query_name="Query"):
128
+ """
129
+ Create interactive 3D visualization of two aligned structures (pairwise).
130
+ Based on working original visualization with enhanced controls.
131
+
132
+ Args:
133
+ ref_path: Path to reference PDB file
134
+ query_path: Path to query PDB file
135
+ ref_window: List of residue indices for reference window
136
+ query_window: List of residue indices for query window
137
+ rotation_matrix: Rotation matrix from RMSD
138
+ ref_com: Center of mass of reference
139
+ query_com: Center of mass of query
140
+ rmsd: RMSD value
141
+ ref_name: Name of reference structure
142
+ query_name: Name of query structure
143
+
144
+ Returns:
145
+ HTML string for py3Dmol visualization
146
+ """
147
+
148
+ # Extract windows
149
+ ref_pdb = extract_window_pdb(ref_path, ref_window)
150
+ query_pdb = extract_window_pdb(query_path, query_window)
151
+
152
+ # Transform query to align with reference
153
+ transformed_query_pdb = transform_pdb_string(
154
+ query_pdb,
155
+ rotation_matrix,
156
+ query_com,
157
+ ref_com
158
+ )
159
+
160
+ # Escape backticks
161
+ ref_pdb_escaped = ref_pdb.replace('`', '\\`')
162
+ transformed_query_pdb_escaped = transformed_query_pdb.replace('`', '\\`')
163
+
164
+ # Create HTML with enhanced controls
165
+ html = f"""
166
+ <!DOCTYPE html>
167
+ <html>
168
+ <head>
169
+ <script src="https://3Dmol.csb.pitt.edu/build/3Dmol-min.js"></script>
170
+ <style>
171
+ #container {{
172
+ width: 100%;
173
+ height: 700px;
174
+ position: relative;
175
+ border: 1px solid #ddd;
176
+ }}
177
+ .control-panel {{
178
+ position: absolute;
179
+ top: 10px;
180
+ right: 10px;
181
+ background: rgba(255, 255, 255, 0.95);
182
+ padding: 15px;
183
+ border-radius: 8px;
184
+ font-family: Arial, sans-serif;
185
+ font-size: 13px;
186
+ z-index: 1000;
187
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
188
+ max-width: 220px;
189
+ }}
190
+ .control-panel h4 {{
191
+ margin: 0 0 10px 0;
192
+ font-size: 14px;
193
+ color: #333;
194
+ }}
195
+ .control-section {{
196
+ margin-bottom: 12px;
197
+ padding-bottom: 12px;
198
+ border-bottom: 1px solid #eee;
199
+ }}
200
+ .control-section:last-child {{
201
+ border-bottom: none;
202
+ margin-bottom: 0;
203
+ }}
204
+ .control-section label {{
205
+ display: block;
206
+ margin: 6px 0;
207
+ cursor: pointer;
208
+ }}
209
+ .control-section input[type="checkbox"] {{
210
+ margin-right: 8px;
211
+ }}
212
+ .control-section select {{
213
+ width: 100%;
214
+ padding: 4px;
215
+ margin-top: 5px;
216
+ border: 1px solid #ccc;
217
+ border-radius: 4px;
218
+ }}
219
+ .legend {{
220
+ position: absolute;
221
+ top: 10px;
222
+ left: 10px;
223
+ background: rgba(255, 255, 255, 0.95);
224
+ padding: 15px;
225
+ border-radius: 8px;
226
+ font-family: Arial, sans-serif;
227
+ font-size: 13px;
228
+ z-index: 1000;
229
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
230
+ }}
231
+ .legend h4 {{
232
+ margin: 0 0 10px 0;
233
+ font-size: 14px;
234
+ color: #333;
235
+ }}
236
+ .legend-item {{
237
+ margin: 6px 0;
238
+ display: flex;
239
+ align-items: center;
240
+ }}
241
+ .color-box {{
242
+ width: 24px;
243
+ height: 16px;
244
+ margin-right: 10px;
245
+ border: 1px solid #333;
246
+ border-radius: 2px;
247
+ }}
248
+ .rmsd-info {{
249
+ position: absolute;
250
+ bottom: 10px;
251
+ left: 10px;
252
+ background: rgba(255, 255, 255, 0.95);
253
+ padding: 10px 15px;
254
+ border-radius: 8px;
255
+ font-family: Arial, sans-serif;
256
+ font-size: 13px;
257
+ z-index: 1000;
258
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
259
+ }}
260
+ .section-title {{
261
+ font-weight: bold;
262
+ color: #555;
263
+ margin-bottom: 5px;
264
+ font-size: 12px;
265
+ text-transform: uppercase;
266
+ }}
267
+ </style>
268
+ </head>
269
+ <body>
270
+ <div id="container"></div>
271
+
272
+ <div class="legend">
273
+ <h4>🧬 Structures</h4>
274
+ <div class="legend-item">
275
+ <div class="color-box" style="background: #4A90E2;"></div>
276
+ <span>{ref_name}</span>
277
+ </div>
278
+ <div class="legend-item">
279
+ <div class="color-box" style="background: #E94B3C;"></div>
280
+ <span>{query_name} (Aligned)</span>
281
+ </div>
282
+ </div>
283
+
284
+ <div class="rmsd-info">
285
+ <strong>RMSD: {rmsd:.3f} Å</strong>
286
+ </div>
287
+
288
+ <div class="control-panel">
289
+ <h4>⚙️ Display Options</h4>
290
+
291
+ <div class="control-section">
292
+ <div class="section-title">Structures</div>
293
+ <label>
294
+ <input type="checkbox" id="showRef" checked onchange="updateDisplay()">
295
+ {ref_name}
296
+ </label>
297
+ <label>
298
+ <input type="checkbox" id="showQuery" checked onchange="updateDisplay()">
299
+ {query_name}
300
+ </label>
301
+ </div>
302
+
303
+ <div class="control-section">
304
+ <div class="section-title">Style</div>
305
+ <select id="styleMode" onchange="updateDisplay()">
306
+ <option value="sticks">Sticks</option>
307
+ <option value="cartoon">Cartoon</option>
308
+ <option value="spheres">Spheres</option>
309
+ <option value="lines">Lines</option>
310
+ </select>
311
+ </div>
312
+
313
+ <div class="control-section">
314
+ <div class="section-title">Components</div>
315
+ <label>
316
+ <input type="checkbox" id="showBackbone" checked onchange="updateDisplay()">
317
+ Backbone/Sugar
318
+ </label>
319
+ <label>
320
+ <input type="checkbox" id="showBases" checked onchange="updateDisplay()">
321
+ Bases
322
+ </label>
323
+ </div>
324
+
325
+ <div class="control-section">
326
+ <div class="section-title">Labels</div>
327
+ <label>
328
+ <input type="checkbox" id="showResidueLabels" onchange="updateDisplay()">
329
+ Residue Labels
330
+ </label>
331
+ <label>
332
+ <input type="checkbox" id="showResidueNumbers" onchange="updateDisplay()">
333
+ Residue Numbers
334
+ </label>
335
+ <label>
336
+ <input type="checkbox" id="showAtomNames" onchange="updateDisplay()">
337
+ Atom Names
338
+ </label>
339
+ <select id="atomLabelMode" onchange="updateDisplay()">
340
+ <option value="all">All Atoms</option>
341
+ <option value="backbone">Backbone Only</option>
342
+ <option value="sidechain">Base Only</option>
343
+ </select>
344
+ </div>
345
+
346
+ <div class="control-section">
347
+ <div class="section-title">Background</div>
348
+ <select id="bgColor" onchange="updateBackground()">
349
+ <option value="white">White</option>
350
+ <option value="black">Black</option>
351
+ <option value="gray">Gray</option>
352
+ </select>
353
+ </div>
354
+ </div>
355
+
356
+ <script>
357
+ let viewer = $3Dmol.createViewer("container", {{backgroundColor: 'white'}});
358
+
359
+ const refPDB = `{ref_pdb_escaped}`;
360
+ const queryPDB = `{transformed_query_pdb_escaped}`;
361
+
362
+ const backboneAtoms = ['P', 'OP1', 'OP2', "O5'", "C5'", "C4'", "O4'", "C3'", "O3'", "C2'", "O2'", "C1'"];
363
+
364
+ function updateDisplay() {{
365
+ viewer.removeAllModels();
366
+ viewer.removeAllLabels();
367
+
368
+ const styleMode = document.getElementById('styleMode').value;
369
+ const showBackbone = document.getElementById('showBackbone').checked;
370
+ const showBases = document.getElementById('showBases').checked;
371
+ const showResidueLabels = document.getElementById('showResidueLabels').checked;
372
+ const showResidueNumbers = document.getElementById('showResidueNumbers').checked;
373
+ const showAtomNames = document.getElementById('showAtomNames').checked;
374
+
375
+ let modelIndex = 0;
376
+
377
+ // Add reference if checked
378
+ if (document.getElementById('showRef').checked) {{
379
+ viewer.addModel(refPDB, "pdb");
380
+ applyStyle(modelIndex, '#4A90E2', styleMode, showBackbone, showBases);
381
+
382
+ if (showResidueLabels || showResidueNumbers) {{
383
+ addResidueLabels(modelIndex, '#4A90E2', showResidueLabels, showResidueNumbers);
384
+ }}
385
+ if (showAtomNames) {{
386
+ addAtomLabels(modelIndex, '#4A90E2');
387
+ }}
388
+
389
+ modelIndex++;
390
+ }}
391
+
392
+ // Add query if checked
393
+ if (document.getElementById('showQuery').checked) {{
394
+ viewer.addModel(queryPDB, "pdb");
395
+ applyStyle(modelIndex, '#E94B3C', styleMode, showBackbone, showBases);
396
+
397
+ if (showResidueLabels || showResidueNumbers) {{
398
+ addResidueLabels(modelIndex, '#E94B3C', showResidueLabels, showResidueNumbers);
399
+ }}
400
+ if (showAtomNames) {{
401
+ addAtomLabels(modelIndex, '#E94B3C');
402
+ }}
403
+
404
+ modelIndex++;
405
+ }}
406
+
407
+ viewer.zoomTo();
408
+ viewer.render();
409
+ }}
410
+
411
+ function applyStyle(modelIndex, baseColor, styleMode, showBackbone, showBases) {{
412
+ const atoms = viewer.selectedAtoms({{model: modelIndex}});
413
+
414
+ atoms.forEach(atom => {{
415
+ const isBackbone = backboneAtoms.includes(atom.atom);
416
+
417
+ // Skip if filtering out this type
418
+ if (!showBackbone && isBackbone) return;
419
+ if (!showBases && !isBackbone) return;
420
+
421
+ if (styleMode === 'sticks') {{
422
+ viewer.setStyle({{model: modelIndex, serial: atom.serial}}, {{
423
+ stick: {{
424
+ color: baseColor,
425
+ radius: 0.15
426
+ }}
427
+ }});
428
+ }} else if (styleMode === 'cartoon') {{
429
+ viewer.setStyle({{model: modelIndex}}, {{
430
+ cartoon: {{
431
+ color: baseColor,
432
+ opacity: 0.8
433
+ }}
434
+ }});
435
+ }} else if (styleMode === 'spheres') {{
436
+ viewer.setStyle({{model: modelIndex, serial: atom.serial}}, {{
437
+ sphere: {{
438
+ color: baseColor,
439
+ radius: 0.25
440
+ }}
441
+ }});
442
+ }} else if (styleMode === 'lines') {{
443
+ viewer.setStyle({{model: modelIndex, serial: atom.serial}}, {{
444
+ line: {{
445
+ color: baseColor
446
+ }}
447
+ }});
448
+ }}
449
+ }});
450
+ }}
451
+
452
+ function addResidueLabels(modelIndex, color, showLabels, showNumbers) {{
453
+ const atoms = viewer.selectedAtoms({{model: modelIndex}});
454
+ const residues = {{}};
455
+
456
+ atoms.forEach(atom => {{
457
+ const key = atom.chain + '_' + atom.resi;
458
+ if (!residues[key]) {{
459
+ residues[key] = atom;
460
+ }}
461
+ }});
462
+
463
+ Object.values(residues).forEach(atom => {{
464
+ let labelText = '';
465
+ if (showLabels && showNumbers) {{
466
+ labelText = atom.resn + atom.resi;
467
+ }} else if (showLabels) {{
468
+ labelText = atom.resn;
469
+ }} else if (showNumbers) {{
470
+ labelText = atom.resi.toString();
471
+ }}
472
+
473
+ if (labelText) {{
474
+ viewer.addLabel(labelText, {{
475
+ position: atom,
476
+ backgroundColor: color,
477
+ backgroundOpacity: 0.7,
478
+ fontColor: 'white',
479
+ fontSize: 11,
480
+ fontWeight: 'bold',
481
+ showBackground: true,
482
+ borderRadius: 3
483
+ }});
484
+ }}
485
+ }});
486
+ }}
487
+
488
+ function addAtomLabels(modelIndex, color) {{
489
+ const atomLabelMode = document.getElementById('atomLabelMode').value;
490
+ const atoms = viewer.selectedAtoms({{model: modelIndex}});
491
+
492
+ let filteredAtoms = atoms;
493
+ if (atomLabelMode === 'backbone') {{
494
+ filteredAtoms = atoms.filter(atom => backboneAtoms.includes(atom.atom));
495
+ }} else if (atomLabelMode === 'sidechain') {{
496
+ filteredAtoms = atoms.filter(atom => !backboneAtoms.includes(atom.atom));
497
+ }}
498
+
499
+ filteredAtoms.forEach(atom => {{
500
+ viewer.addLabel(atom.atom, {{
501
+ position: atom,
502
+ backgroundColor: color,
503
+ backgroundOpacity: 0.6,
504
+ fontColor: 'white',
505
+ fontSize: 9,
506
+ fontWeight: 'normal',
507
+ showBackground: true,
508
+ borderRadius: 2
509
+ }});
510
+ }});
511
+ }}
512
+
513
+ function updateBackground() {{
514
+ const bgColor = document.getElementById('bgColor').value;
515
+ viewer.setBackgroundColor(bgColor);
516
+ viewer.render();
517
+ }}
518
+
519
+ // Initialize
520
+ updateDisplay();
521
+ </script>
522
+ </body>
523
+ </html>
524
+ """
525
+
526
+ return html
527
+
528
+
529
+ def create_multistructure_visualization(ref_path, ref_window, ref_com, query_data_list, ref_name="Reference"):
530
+ """
531
+ Create interactive 3D visualization with multiple structures aligned to reference.
532
+
533
+ Args:
534
+ ref_path: Path to reference PDB file
535
+ ref_window: List of residue indices for reference window
536
+ ref_com: Center of mass of reference window
537
+ query_data_list: List of dicts with keys:
538
+ - name: Structure name
539
+ - path: Path to PDB file
540
+ - window: List of residue indices
541
+ - rotation: Rotation matrix from RMSD
542
+ - query_com: Center of mass
543
+ - rmsd: RMSD value
544
+ - sequence: Sequence string
545
+ ref_name: Name of reference structure
546
+
547
+ Returns:
548
+ HTML string for py3Dmol visualization
549
+ """
550
+
551
+ # Extract reference window
552
+ ref_pdb = extract_window_pdb(ref_path, ref_window)
553
+
554
+ # Color palette
555
+ colors = [
556
+ '#E94B3C', # Red
557
+ '#50C878', # Green
558
+ '#FF9500', # Orange
559
+ '#9B59B6', # Purple
560
+ '#00CED1', # Cyan
561
+ '#FF1493', # Pink
562
+ '#FFD700', # Gold
563
+ '#8B4513', # Brown
564
+ '#708090' # Slate Gray
565
+ ]
566
+
567
+ # Transform all query structures
568
+ transformed_queries = []
569
+ for idx, query_data in enumerate(query_data_list):
570
+ # Extract query window
571
+ query_pdb = extract_window_pdb(query_data['path'], query_data['window'])
572
+
573
+ # Transform query to align with reference
574
+ transformed_pdb = transform_pdb_string(
575
+ query_pdb,
576
+ query_data['rotation'],
577
+ query_data['query_com'],
578
+ ref_com
579
+ )
580
+
581
+ color = colors[idx % len(colors)]
582
+ transformed_queries.append({
583
+ 'pdb': transformed_pdb,
584
+ 'name': query_data['name'],
585
+ 'color': color,
586
+ 'rmsd': query_data['rmsd'],
587
+ 'sequence': query_data['sequence']
588
+ })
589
+
590
+ # Build JavaScript for model data
591
+ models_js = f"const refPDB = `{ref_pdb}`;\n"
592
+ for idx, tq in enumerate(transformed_queries):
593
+ # Escape backticks
594
+ pdb_escaped = tq['pdb'].replace('`', '\\`')
595
+ models_js += f"const queryPDB{idx} = `{pdb_escaped}`;\n"
596
+
597
+ # Build controls HTML
598
+ controls_html = f'<label><input type="checkbox" id="showRef" checked onchange="updateDisplay()"> {ref_name} (Blue)</label>\n'
599
+ for idx, tq in enumerate(transformed_queries):
600
+ controls_html += f'<label><input type="checkbox" id="showQuery{idx}" checked onchange="updateDisplay()"> {tq["name"]} ({tq["color"]})</label>\n'
601
+
602
+ # Build legend HTML
603
+ legend_html = f'<div class="legend-item"><div class="color-box" style="background: #4A90E2;"></div><span>{ref_name}</span></div>\n'
604
+ for tq in transformed_queries:
605
+ legend_html += f'<div class="legend-item"><div class="color-box" style="background: {tq["color"]};"></div><span>{tq["name"]} ({tq["rmsd"]:.3f} Å)</span></div>\n'
606
+
607
+ # Build update function
608
+ update_js = '''
609
+ function updateDisplay() {
610
+ viewer.removeAllModels();
611
+ const styleMode = document.getElementById('styleMode').value;
612
+ const showLabels = document.getElementById('showLabels').checked;
613
+
614
+ let modelIndex = 0;
615
+
616
+ // Add reference if checked
617
+ if (document.getElementById('showRef').checked) {
618
+ viewer.addModel(refPDB, "pdb");
619
+ applyStyle(modelIndex, '#4A90E2', styleMode);
620
+ if (showLabels) {
621
+ addLabels(modelIndex, '#4A90E2');
622
+ }
623
+ modelIndex++;
624
+ }
625
+ '''
626
+
627
+ # Add query structures
628
+ for idx in range(len(transformed_queries)):
629
+ color = transformed_queries[idx]['color']
630
+ update_js += f'''
631
+ if (document.getElementById('showQuery{idx}').checked) {{
632
+ viewer.addModel(queryPDB{idx}, "pdb");
633
+ applyStyle(modelIndex, '{color}', styleMode);
634
+ if (showLabels) {{
635
+ addLabels(modelIndex, '{color}');
636
+ }}
637
+ modelIndex++;
638
+ }}
639
+ '''
640
+
641
+ update_js += '''
642
+ viewer.zoomTo();
643
+ viewer.render();
644
+ }
645
+
646
+ function applyStyle(modelIndex, color, styleMode) {
647
+ if (styleMode === 'sticks') {
648
+ viewer.setStyle({model: modelIndex}, {stick: {color: color, radius: 0.15}});
649
+ } else if (styleMode === 'cartoon') {
650
+ viewer.setStyle({model: modelIndex}, {cartoon: {color: color, opacity: 0.8}});
651
+ } else if (styleMode === 'spheres') {
652
+ viewer.setStyle({model: modelIndex}, {sphere: {color: color, radius: 0.3}});
653
+ } else if (styleMode === 'lines') {
654
+ viewer.setStyle({model: modelIndex}, {line: {color: color}});
655
+ }
656
+ }
657
+
658
+ function addLabels(modelIndex, color) {
659
+ const atoms = viewer.selectedAtoms({model: modelIndex});
660
+ const residues = {};
661
+
662
+ atoms.forEach(atom => {
663
+ const key = atom.chain + '_' + atom.resi;
664
+ if (!residues[key]) {
665
+ residues[key] = atom;
666
+ }
667
+ });
668
+
669
+ Object.values(residues).forEach(atom => {
670
+ viewer.addLabel(atom.resn + atom.resi, {
671
+ position: atom,
672
+ backgroundColor: color,
673
+ backgroundOpacity: 0.7,
674
+ fontColor: 'white',
675
+ fontSize: 10,
676
+ fontWeight: 'bold',
677
+ showBackground: true
678
+ });
679
+ });
680
+ }
681
+ '''
682
+
683
+ # Create HTML
684
+ html = f'''
685
+ <!DOCTYPE html>
686
+ <html>
687
+ <head>
688
+ <script src="https://3Dmol.csb.pitt.edu/build/3Dmol-min.js"></script>
689
+ <style>
690
+ #container {{
691
+ width: 100%;
692
+ height: 700px;
693
+ position: relative;
694
+ border: 1px solid #ddd;
695
+ }}
696
+ .control-panel {{
697
+ position: absolute;
698
+ top: 10px;
699
+ right: 10px;
700
+ background: rgba(255, 255, 255, 0.95);
701
+ padding: 15px;
702
+ border-radius: 8px;
703
+ font-family: Arial, sans-serif;
704
+ font-size: 13px;
705
+ z-index: 1000;
706
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
707
+ max-width: 250px;
708
+ max-height: 600px;
709
+ overflow-y: auto;
710
+ }}
711
+ .control-panel h4 {{
712
+ margin: 0 0 10px 0;
713
+ font-size: 14px;
714
+ color: #333;
715
+ }}
716
+ .control-section {{
717
+ margin-bottom: 12px;
718
+ padding-bottom: 12px;
719
+ border-bottom: 1px solid #eee;
720
+ }}
721
+ .control-section:last-child {{
722
+ border-bottom: none;
723
+ margin-bottom: 0;
724
+ }}
725
+ .control-section label {{
726
+ display: block;
727
+ margin: 6px 0;
728
+ cursor: pointer;
729
+ }}
730
+ .control-section input[type="checkbox"] {{
731
+ margin-right: 8px;
732
+ }}
733
+ .control-section select {{
734
+ width: 100%;
735
+ padding: 4px;
736
+ margin-top: 5px;
737
+ border: 1px solid #ccc;
738
+ border-radius: 4px;
739
+ }}
740
+ .legend {{
741
+ position: absolute;
742
+ top: 10px;
743
+ left: 10px;
744
+ background: rgba(255, 255, 255, 0.95);
745
+ padding: 15px;
746
+ border-radius: 8px;
747
+ font-family: Arial, sans-serif;
748
+ font-size: 13px;
749
+ z-index: 1000;
750
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
751
+ }}
752
+ .legend h4 {{
753
+ margin: 0 0 10px 0;
754
+ font-size: 14px;
755
+ color: #333;
756
+ }}
757
+ .legend-item {{
758
+ margin: 6px 0;
759
+ display: flex;
760
+ align-items: center;
761
+ }}
762
+ .color-box {{
763
+ width: 24px;
764
+ height: 16px;
765
+ margin-right: 10px;
766
+ border: 1px solid #333;
767
+ border-radius: 2px;
768
+ }}
769
+ .section-title {{
770
+ font-weight: bold;
771
+ color: #555;
772
+ margin-bottom: 5px;
773
+ font-size: 12px;
774
+ text-transform: uppercase;
775
+ }}
776
+ </style>
777
+ </head>
778
+ <body>
779
+ <div id="container"></div>
780
+
781
+ <div class="legend">
782
+ <h4>🧬 Structures</h4>
783
+ {legend_html}
784
+ </div>
785
+
786
+ <div class="control-panel">
787
+ <h4>⚙️ Display Options</h4>
788
+
789
+ <div class="control-section">
790
+ <div class="section-title">Structures</div>
791
+ {controls_html}
792
+ </div>
793
+
794
+ <div class="control-section">
795
+ <div class="section-title">Style</div>
796
+ <select id="styleMode" onchange="updateDisplay()">
797
+ <option value="sticks">Sticks</option>
798
+ <option value="cartoon">Cartoon</option>
799
+ <option value="spheres">Spheres</option>
800
+ <option value="lines">Lines</option>
801
+ </select>
802
+ </div>
803
+
804
+ <div class="control-section">
805
+ <div class="section-title">Labels</div>
806
+ <label>
807
+ <input type="checkbox" id="showLabels" onchange="updateDisplay()">
808
+ Show Residue Labels
809
+ </label>
810
+ </div>
811
+ </div>
812
+
813
+ <script>
814
+ let viewer = $3Dmol.createViewer("container", {{backgroundColor: 'white'}});
815
+
816
+ {models_js}
817
+
818
+ {update_js}
819
+
820
+ // Initialize
821
+ updateDisplay();
822
+ </script>
823
+ </body>
824
+ </html>
825
+ '''
826
+
827
+ return html