pytractoviz 0.2.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytractoviz/__init__.py +11 -0
- pytractoviz/__main__.py +14 -0
- pytractoviz/_internal/__init__.py +0 -0
- pytractoviz/_internal/cli.py +59 -0
- pytractoviz/_internal/debug.py +110 -0
- pytractoviz/html.py +845 -0
- pytractoviz/py.typed +0 -0
- pytractoviz/utils.py +220 -0
- pytractoviz/viz.py +4272 -0
- pytractoviz-0.2.14.dist-info/METADATA +53 -0
- pytractoviz-0.2.14.dist-info/RECORD +14 -0
- pytractoviz-0.2.14.dist-info/WHEEL +4 -0
- pytractoviz-0.2.14.dist-info/entry_points.txt +5 -0
- pytractoviz-0.2.14.dist-info/licenses/LICENSE +21 -0
pytractoviz/html.py
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
"""HTML report generation for quality checking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_quality_check_html(
|
|
11
|
+
data: dict[str, dict[str, dict[str, str]]],
|
|
12
|
+
output_file: str,
|
|
13
|
+
title: str = "Tractography Quality Check",
|
|
14
|
+
items_per_page: int = 50,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Create an interactive HTML quality check report.
|
|
17
|
+
|
|
18
|
+
This function generates an HTML file with filtering, search, and pagination
|
|
19
|
+
capabilities for efficiently reviewing large datasets of visualizations.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
data : dict
|
|
24
|
+
Nested dictionary structure: {subject_id: {tract_name: {media_type: file_path}}}
|
|
25
|
+
Example:
|
|
26
|
+
{
|
|
27
|
+
"sub-001": {
|
|
28
|
+
"AF_L": {
|
|
29
|
+
"image": "path/to/image.png",
|
|
30
|
+
"plot": "path/to/plot.png",
|
|
31
|
+
"gif": "path/to/animation.gif",
|
|
32
|
+
"video": "path/to/video.mp4"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
output_file : str
|
|
37
|
+
Path where the HTML file will be saved.
|
|
38
|
+
title : str, optional
|
|
39
|
+
Title for the HTML report. Default is "Tractography Quality Check".
|
|
40
|
+
items_per_page : int, optional
|
|
41
|
+
Number of items to display per page. Default is 50.
|
|
42
|
+
|
|
43
|
+
Notes
|
|
44
|
+
-----
|
|
45
|
+
Supported media types in the data dictionary:
|
|
46
|
+
- "image": Static images (PNG, JPG, etc.)
|
|
47
|
+
- "plot": Matplotlib plots or other static plots
|
|
48
|
+
- "gif": Animated GIF files
|
|
49
|
+
- "video": Video files (MP4, WebM, etc.)
|
|
50
|
+
|
|
51
|
+
The HTML report includes:
|
|
52
|
+
- Filtering by subject and tract
|
|
53
|
+
- Search functionality
|
|
54
|
+
- Pagination for efficient loading
|
|
55
|
+
- Thumbnail grid view with expandable detail views
|
|
56
|
+
- Support for images, plots, GIFs, and videos
|
|
57
|
+
"""
|
|
58
|
+
# Convert file paths to relative paths for HTML
|
|
59
|
+
html_dir = Path(output_file).parent
|
|
60
|
+
data_processed: dict[str, dict[str, dict[str, str]]] = {}
|
|
61
|
+
|
|
62
|
+
for subject_id, tracts in data.items():
|
|
63
|
+
data_processed[subject_id] = {}
|
|
64
|
+
for tract_name, media in tracts.items():
|
|
65
|
+
data_processed[subject_id][tract_name] = {}
|
|
66
|
+
for media_type, file_path in media.items():
|
|
67
|
+
# Handle numeric scores (like shape_similarity_score)
|
|
68
|
+
if isinstance(file_path, (int, float)):
|
|
69
|
+
data_processed[subject_id][tract_name][media_type] = str(file_path)
|
|
70
|
+
elif file_path and os.path.exists(file_path):
|
|
71
|
+
# Convert to relative path from HTML file location
|
|
72
|
+
try:
|
|
73
|
+
rel_path = os.path.relpath(file_path, html_dir)
|
|
74
|
+
data_processed[subject_id][tract_name][media_type] = rel_path
|
|
75
|
+
except ValueError:
|
|
76
|
+
# If paths are on different drives (Windows), use absolute
|
|
77
|
+
data_processed[subject_id][tract_name][media_type] = file_path
|
|
78
|
+
elif file_path:
|
|
79
|
+
# Store as-is if it's a string but file doesn't exist (might be a score string)
|
|
80
|
+
data_processed[subject_id][tract_name][media_type] = file_path
|
|
81
|
+
|
|
82
|
+
# Extract unique subjects and tracts for filters
|
|
83
|
+
subjects = sorted(data_processed.keys())
|
|
84
|
+
tract_names: list[str] = sorted({tract for tracts_dict in data_processed.values() for tract in tracts_dict})
|
|
85
|
+
|
|
86
|
+
# Generate HTML
|
|
87
|
+
html_content = f"""<!DOCTYPE html>
|
|
88
|
+
<html lang="en">
|
|
89
|
+
<head>
|
|
90
|
+
<meta charset="UTF-8">
|
|
91
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
92
|
+
<title>{title}</title>
|
|
93
|
+
<style>
|
|
94
|
+
* {{
|
|
95
|
+
margin: 0;
|
|
96
|
+
padding: 0;
|
|
97
|
+
box-sizing: border-box;
|
|
98
|
+
}}
|
|
99
|
+
|
|
100
|
+
body {{
|
|
101
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
102
|
+
background: #f5f5f5;
|
|
103
|
+
color: #333;
|
|
104
|
+
line-height: 1.6;
|
|
105
|
+
}}
|
|
106
|
+
|
|
107
|
+
.header {{
|
|
108
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
109
|
+
color: white;
|
|
110
|
+
padding: 2rem;
|
|
111
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
112
|
+
}}
|
|
113
|
+
|
|
114
|
+
.header h1 {{
|
|
115
|
+
margin-bottom: 0.5rem;
|
|
116
|
+
}}
|
|
117
|
+
|
|
118
|
+
.header .stats {{
|
|
119
|
+
opacity: 0.9;
|
|
120
|
+
font-size: 0.9rem;
|
|
121
|
+
}}
|
|
122
|
+
|
|
123
|
+
.controls {{
|
|
124
|
+
background: white;
|
|
125
|
+
padding: 1.5rem;
|
|
126
|
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
127
|
+
position: sticky;
|
|
128
|
+
top: 0;
|
|
129
|
+
z-index: 100;
|
|
130
|
+
}}
|
|
131
|
+
|
|
132
|
+
.controls-grid {{
|
|
133
|
+
display: grid;
|
|
134
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
135
|
+
gap: 1rem;
|
|
136
|
+
margin-bottom: 1rem;
|
|
137
|
+
}}
|
|
138
|
+
|
|
139
|
+
.control-group {{
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
}}
|
|
143
|
+
|
|
144
|
+
.control-group label {{
|
|
145
|
+
font-weight: 600;
|
|
146
|
+
margin-bottom: 0.5rem;
|
|
147
|
+
font-size: 0.9rem;
|
|
148
|
+
color: #555;
|
|
149
|
+
}}
|
|
150
|
+
|
|
151
|
+
.control-group select,
|
|
152
|
+
.control-group input {{
|
|
153
|
+
padding: 0.6rem;
|
|
154
|
+
border: 2px solid #e0e0e0;
|
|
155
|
+
border-radius: 4px;
|
|
156
|
+
font-size: 0.9rem;
|
|
157
|
+
transition: border-color 0.3s;
|
|
158
|
+
}}
|
|
159
|
+
|
|
160
|
+
.control-group select:focus,
|
|
161
|
+
.control-group input:focus {{
|
|
162
|
+
outline: none;
|
|
163
|
+
border-color: #667eea;
|
|
164
|
+
}}
|
|
165
|
+
|
|
166
|
+
.pagination {{
|
|
167
|
+
display: flex;
|
|
168
|
+
justify-content: center;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 0.5rem;
|
|
171
|
+
margin-top: 1rem;
|
|
172
|
+
}}
|
|
173
|
+
|
|
174
|
+
.pagination button {{
|
|
175
|
+
padding: 0.5rem 1rem;
|
|
176
|
+
border: 2px solid #667eea;
|
|
177
|
+
background: white;
|
|
178
|
+
color: #667eea;
|
|
179
|
+
border-radius: 4px;
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
font-size: 0.9rem;
|
|
182
|
+
transition: all 0.3s;
|
|
183
|
+
}}
|
|
184
|
+
|
|
185
|
+
.pagination button:hover:not(:disabled) {{
|
|
186
|
+
background: #667eea;
|
|
187
|
+
color: white;
|
|
188
|
+
}}
|
|
189
|
+
|
|
190
|
+
.pagination button:disabled {{
|
|
191
|
+
opacity: 0.5;
|
|
192
|
+
cursor: not-allowed;
|
|
193
|
+
}}
|
|
194
|
+
|
|
195
|
+
.pagination .page-info {{
|
|
196
|
+
padding: 0 1rem;
|
|
197
|
+
font-weight: 600;
|
|
198
|
+
}}
|
|
199
|
+
|
|
200
|
+
.grid-container {{
|
|
201
|
+
padding: 2rem;
|
|
202
|
+
max-width: 1800px;
|
|
203
|
+
margin: 0 auto;
|
|
204
|
+
}}
|
|
205
|
+
|
|
206
|
+
.grid {{
|
|
207
|
+
display: grid;
|
|
208
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
209
|
+
gap: 1.5rem;
|
|
210
|
+
}}
|
|
211
|
+
|
|
212
|
+
.item-card {{
|
|
213
|
+
background: white;
|
|
214
|
+
border-radius: 8px;
|
|
215
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
216
|
+
overflow: hidden;
|
|
217
|
+
transition: transform 0.3s, box-shadow 0.3s;
|
|
218
|
+
cursor: pointer;
|
|
219
|
+
}}
|
|
220
|
+
|
|
221
|
+
.item-card:hover {{
|
|
222
|
+
transform: translateY(-4px);
|
|
223
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
|
|
224
|
+
}}
|
|
225
|
+
|
|
226
|
+
.item-header {{
|
|
227
|
+
padding: 1rem;
|
|
228
|
+
background: #f8f9fa;
|
|
229
|
+
border-bottom: 1px solid #e0e0e0;
|
|
230
|
+
}}
|
|
231
|
+
|
|
232
|
+
.item-header h3 {{
|
|
233
|
+
font-size: 1rem;
|
|
234
|
+
margin-bottom: 0.25rem;
|
|
235
|
+
color: #333;
|
|
236
|
+
}}
|
|
237
|
+
|
|
238
|
+
.item-header .tract-name {{
|
|
239
|
+
font-size: 0.85rem;
|
|
240
|
+
color: #667eea;
|
|
241
|
+
font-weight: 600;
|
|
242
|
+
}}
|
|
243
|
+
|
|
244
|
+
.item-media {{
|
|
245
|
+
position: relative;
|
|
246
|
+
width: 100%;
|
|
247
|
+
padding-top: 75%; /* 4:3 aspect ratio */
|
|
248
|
+
background: #f0f0f0;
|
|
249
|
+
overflow: hidden;
|
|
250
|
+
}}
|
|
251
|
+
|
|
252
|
+
.item-media img,
|
|
253
|
+
.item-media video {{
|
|
254
|
+
position: absolute;
|
|
255
|
+
top: 0;
|
|
256
|
+
left: 0;
|
|
257
|
+
width: 100%;
|
|
258
|
+
height: 100%;
|
|
259
|
+
object-fit: contain;
|
|
260
|
+
background: white;
|
|
261
|
+
}}
|
|
262
|
+
|
|
263
|
+
.item-media .media-placeholder {{
|
|
264
|
+
position: absolute;
|
|
265
|
+
top: 50%;
|
|
266
|
+
left: 50%;
|
|
267
|
+
transform: translate(-50%, -50%);
|
|
268
|
+
color: #999;
|
|
269
|
+
font-size: 0.9rem;
|
|
270
|
+
}}
|
|
271
|
+
|
|
272
|
+
.item-footer {{
|
|
273
|
+
padding: 0.75rem 1rem;
|
|
274
|
+
display: flex;
|
|
275
|
+
gap: 0.5rem;
|
|
276
|
+
flex-wrap: wrap;
|
|
277
|
+
}}
|
|
278
|
+
|
|
279
|
+
.media-badge {{
|
|
280
|
+
padding: 0.25rem 0.5rem;
|
|
281
|
+
background: #e3f2fd;
|
|
282
|
+
color: #1976d2;
|
|
283
|
+
border-radius: 12px;
|
|
284
|
+
font-size: 0.75rem;
|
|
285
|
+
font-weight: 600;
|
|
286
|
+
}}
|
|
287
|
+
|
|
288
|
+
.score-badge {{
|
|
289
|
+
padding: 0.5rem 1rem;
|
|
290
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
291
|
+
color: white;
|
|
292
|
+
border-radius: 8px;
|
|
293
|
+
font-size: 1.1rem;
|
|
294
|
+
font-weight: 700;
|
|
295
|
+
text-align: center;
|
|
296
|
+
display: inline-block;
|
|
297
|
+
margin: 0.5rem 0;
|
|
298
|
+
}}
|
|
299
|
+
|
|
300
|
+
.comparison-container {{
|
|
301
|
+
display: grid;
|
|
302
|
+
grid-template-columns: 1fr 1fr;
|
|
303
|
+
gap: 1rem;
|
|
304
|
+
margin: 1rem 0;
|
|
305
|
+
}}
|
|
306
|
+
|
|
307
|
+
.comparison-item {{
|
|
308
|
+
background: #f8f9fa;
|
|
309
|
+
border-radius: 4px;
|
|
310
|
+
padding: 0.5rem;
|
|
311
|
+
text-align: center;
|
|
312
|
+
}}
|
|
313
|
+
|
|
314
|
+
.comparison-item h5 {{
|
|
315
|
+
margin-bottom: 0.5rem;
|
|
316
|
+
color: #555;
|
|
317
|
+
font-size: 0.85rem;
|
|
318
|
+
text-transform: uppercase;
|
|
319
|
+
letter-spacing: 0.5px;
|
|
320
|
+
}}
|
|
321
|
+
|
|
322
|
+
.comparison-item img {{
|
|
323
|
+
width: 100%;
|
|
324
|
+
height: auto;
|
|
325
|
+
border-radius: 4px;
|
|
326
|
+
background: white;
|
|
327
|
+
}}
|
|
328
|
+
|
|
329
|
+
@media (max-width: 768px) {{
|
|
330
|
+
.comparison-container {{
|
|
331
|
+
grid-template-columns: 1fr;
|
|
332
|
+
}}
|
|
333
|
+
}}
|
|
334
|
+
|
|
335
|
+
.modal {{
|
|
336
|
+
display: none;
|
|
337
|
+
position: fixed;
|
|
338
|
+
z-index: 1000;
|
|
339
|
+
left: 0;
|
|
340
|
+
top: 0;
|
|
341
|
+
width: 100%;
|
|
342
|
+
height: 100%;
|
|
343
|
+
background: rgba(0,0,0,0.9);
|
|
344
|
+
overflow: auto;
|
|
345
|
+
}}
|
|
346
|
+
|
|
347
|
+
.modal-content {{
|
|
348
|
+
position: relative;
|
|
349
|
+
margin: 2% auto;
|
|
350
|
+
max-width: 90%;
|
|
351
|
+
max-height: 90vh;
|
|
352
|
+
background: white;
|
|
353
|
+
border-radius: 8px;
|
|
354
|
+
padding: 2rem;
|
|
355
|
+
overflow-y: auto;
|
|
356
|
+
}}
|
|
357
|
+
|
|
358
|
+
.modal-header {{
|
|
359
|
+
display: flex;
|
|
360
|
+
justify-content: space-between;
|
|
361
|
+
align-items: center;
|
|
362
|
+
margin-bottom: 1.5rem;
|
|
363
|
+
padding-bottom: 1rem;
|
|
364
|
+
border-bottom: 2px solid #e0e0e0;
|
|
365
|
+
}}
|
|
366
|
+
|
|
367
|
+
.modal-header h2 {{
|
|
368
|
+
color: #333;
|
|
369
|
+
}}
|
|
370
|
+
|
|
371
|
+
.close {{
|
|
372
|
+
color: #aaa;
|
|
373
|
+
font-size: 2rem;
|
|
374
|
+
font-weight: bold;
|
|
375
|
+
cursor: pointer;
|
|
376
|
+
line-height: 1;
|
|
377
|
+
}}
|
|
378
|
+
|
|
379
|
+
.close:hover {{
|
|
380
|
+
color: #000;
|
|
381
|
+
}}
|
|
382
|
+
|
|
383
|
+
.modal-media-container {{
|
|
384
|
+
display: grid;
|
|
385
|
+
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
386
|
+
gap: 1.5rem;
|
|
387
|
+
}}
|
|
388
|
+
|
|
389
|
+
.modal-media-item {{
|
|
390
|
+
background: #f8f9fa;
|
|
391
|
+
border-radius: 4px;
|
|
392
|
+
padding: 1rem;
|
|
393
|
+
}}
|
|
394
|
+
|
|
395
|
+
.modal-media-item h4 {{
|
|
396
|
+
margin-bottom: 0.5rem;
|
|
397
|
+
color: #555;
|
|
398
|
+
text-transform: uppercase;
|
|
399
|
+
font-size: 0.85rem;
|
|
400
|
+
letter-spacing: 0.5px;
|
|
401
|
+
}}
|
|
402
|
+
|
|
403
|
+
.modal-media-item img,
|
|
404
|
+
.modal-media-item video {{
|
|
405
|
+
width: 100%;
|
|
406
|
+
height: auto;
|
|
407
|
+
border-radius: 4px;
|
|
408
|
+
background: white;
|
|
409
|
+
}}
|
|
410
|
+
|
|
411
|
+
.no-results {{
|
|
412
|
+
text-align: center;
|
|
413
|
+
padding: 4rem 2rem;
|
|
414
|
+
color: #999;
|
|
415
|
+
}}
|
|
416
|
+
|
|
417
|
+
.no-results h3 {{
|
|
418
|
+
margin-bottom: 0.5rem;
|
|
419
|
+
color: #666;
|
|
420
|
+
}}
|
|
421
|
+
|
|
422
|
+
@media (max-width: 768px) {{
|
|
423
|
+
.grid {{
|
|
424
|
+
grid-template-columns: 1fr;
|
|
425
|
+
}}
|
|
426
|
+
|
|
427
|
+
.controls-grid {{
|
|
428
|
+
grid-template-columns: 1fr;
|
|
429
|
+
}}
|
|
430
|
+
|
|
431
|
+
.modal-content {{
|
|
432
|
+
max-width: 95%;
|
|
433
|
+
padding: 1rem;
|
|
434
|
+
}}
|
|
435
|
+
|
|
436
|
+
.modal-media-container {{
|
|
437
|
+
grid-template-columns: 1fr;
|
|
438
|
+
}}
|
|
439
|
+
}}
|
|
440
|
+
</style>
|
|
441
|
+
</head>
|
|
442
|
+
<body>
|
|
443
|
+
<div class="header">
|
|
444
|
+
<h1>{title}</h1>
|
|
445
|
+
<div class="stats">
|
|
446
|
+
<span id="total-items">{len(subjects)} subjects x {len(tract_names)} tracts</span> |
|
|
447
|
+
<span id="filtered-count">Showing all items</span>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
|
|
451
|
+
<div class="controls">
|
|
452
|
+
<div class="controls-grid">
|
|
453
|
+
<div class="control-group">
|
|
454
|
+
<label for="subject-filter">Filter by Subject</label>
|
|
455
|
+
<select id="subject-filter">
|
|
456
|
+
<option value="">All Subjects</option>
|
|
457
|
+
{"".join(f'<option value="{s}">{s}</option>' for s in subjects)}
|
|
458
|
+
</select>
|
|
459
|
+
</div>
|
|
460
|
+
<div class="control-group">
|
|
461
|
+
<label for="tract-filter">Filter by Tract</label>
|
|
462
|
+
<select id="tract-filter">
|
|
463
|
+
<option value="">All Tracts</option>
|
|
464
|
+
{"".join(f'<option value="{t}">{t}</option>' for t in tract_names)}
|
|
465
|
+
</select>
|
|
466
|
+
</div>
|
|
467
|
+
<div class="control-group">
|
|
468
|
+
<label for="search">Search</label>
|
|
469
|
+
<input type="text" id="search" placeholder="Search subject or tract...">
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
<div class="pagination">
|
|
473
|
+
<button id="prev-page" disabled>Previous</button>
|
|
474
|
+
<span class="page-info">
|
|
475
|
+
Page <span id="current-page">1</span> of <span id="total-pages">1</span>
|
|
476
|
+
</span>
|
|
477
|
+
<button id="next-page">Next</button>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
|
|
481
|
+
<div class="grid-container">
|
|
482
|
+
<div class="grid" id="items-grid"></div>
|
|
483
|
+
<div class="no-results" id="no-results" style="display: none;">
|
|
484
|
+
<h3>No items found</h3>
|
|
485
|
+
<p>Try adjusting your filters or search terms</p>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
<div id="modal" class="modal">
|
|
490
|
+
<div class="modal-content">
|
|
491
|
+
<div class="modal-header">
|
|
492
|
+
<h2 id="modal-title"></h2>
|
|
493
|
+
<span class="close">×</span>
|
|
494
|
+
</div>
|
|
495
|
+
<div class="modal-media-container" id="modal-media"></div>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
|
|
499
|
+
<script>
|
|
500
|
+
const data = {json.dumps(data_processed)};
|
|
501
|
+
const itemsPerPage = {items_per_page};
|
|
502
|
+
|
|
503
|
+
let currentPage = 1;
|
|
504
|
+
let filteredData = [];
|
|
505
|
+
|
|
506
|
+
function getMediaType(filePath) {{
|
|
507
|
+
if (!filePath) return null;
|
|
508
|
+
// Check if it's a numeric score (for shape_similarity_score)
|
|
509
|
+
if (typeof filePath === 'number' || (!isNaN(parseFloat(filePath)) && isFinite(filePath))) {{
|
|
510
|
+
return 'score';
|
|
511
|
+
}}
|
|
512
|
+
const ext = filePath.split('.').pop().toLowerCase();
|
|
513
|
+
if (['gif'].includes(ext)) return 'gif';
|
|
514
|
+
if (['mp4', 'webm', 'ogg'].includes(ext)) return 'video';
|
|
515
|
+
if (['png', 'jpg', 'jpeg', 'svg', 'webp'].includes(ext)) return 'image';
|
|
516
|
+
return 'image';
|
|
517
|
+
}}
|
|
518
|
+
|
|
519
|
+
function isComparisonType(mediaType) {{
|
|
520
|
+
return ['before_after_cci', 'atlas_comparison', 'shape_similarity_image'].includes(mediaType);
|
|
521
|
+
}}
|
|
522
|
+
|
|
523
|
+
function flattenData() {{
|
|
524
|
+
const items = [];
|
|
525
|
+
for (const [subject, tracts] of Object.entries(data)) {{
|
|
526
|
+
for (const [tract, media] of Object.entries(tracts)) {{
|
|
527
|
+
items.push({{ subject, tract, media }});
|
|
528
|
+
}}
|
|
529
|
+
}}
|
|
530
|
+
return items;
|
|
531
|
+
}}
|
|
532
|
+
|
|
533
|
+
function filterData() {{
|
|
534
|
+
const subjectFilter = document.getElementById('subject-filter').value;
|
|
535
|
+
const tractFilter = document.getElementById('tract-filter').value;
|
|
536
|
+
const searchTerm = document.getElementById('search').value.toLowerCase();
|
|
537
|
+
|
|
538
|
+
filteredData = flattenData().filter(item => {{
|
|
539
|
+
const matchSubject = !subjectFilter || item.subject === subjectFilter;
|
|
540
|
+
const matchTract = !tractFilter || item.tract === tractFilter;
|
|
541
|
+
const matchSearch = !searchTerm ||
|
|
542
|
+
item.subject.toLowerCase().includes(searchTerm) ||
|
|
543
|
+
item.tract.toLowerCase().includes(searchTerm);
|
|
544
|
+
return matchSubject && matchTract && matchSearch;
|
|
545
|
+
}});
|
|
546
|
+
|
|
547
|
+
currentPage = 1;
|
|
548
|
+
renderItems();
|
|
549
|
+
updatePagination();
|
|
550
|
+
}}
|
|
551
|
+
|
|
552
|
+
function renderItems() {{
|
|
553
|
+
const grid = document.getElementById('items-grid');
|
|
554
|
+
const noResults = document.getElementById('no-results');
|
|
555
|
+
const filteredCount = document.getElementById('filtered-count');
|
|
556
|
+
|
|
557
|
+
if (filteredData.length === 0) {{
|
|
558
|
+
grid.style.display = 'none';
|
|
559
|
+
noResults.style.display = 'block';
|
|
560
|
+
filteredCount.textContent = 'No items found';
|
|
561
|
+
return;
|
|
562
|
+
}}
|
|
563
|
+
|
|
564
|
+
grid.style.display = 'grid';
|
|
565
|
+
noResults.style.display = 'none';
|
|
566
|
+
filteredCount.textContent = `Showing ${{filteredData.length}} item${{filteredData.length !== 1 ? 's' : ''}}`;
|
|
567
|
+
|
|
568
|
+
const start = (currentPage - 1) * itemsPerPage;
|
|
569
|
+
const end = start + itemsPerPage;
|
|
570
|
+
const pageItems = filteredData.slice(start, end);
|
|
571
|
+
|
|
572
|
+
grid.innerHTML = pageItems.map(item => {{
|
|
573
|
+
const mediaTypes = Object.keys(item.media);
|
|
574
|
+
// Find primary media (skip scores)
|
|
575
|
+
const primaryMedia = Object.entries(item.media).find(([type, path]) => {{
|
|
576
|
+
const mt = getMediaType(path);
|
|
577
|
+
return mt !== 'score' && mt !== null;
|
|
578
|
+
}});
|
|
579
|
+
const primaryMediaPath = primaryMedia ? primaryMedia[1] : null;
|
|
580
|
+
const mediaType = primaryMediaPath ? getMediaType(primaryMediaPath) : null;
|
|
581
|
+
|
|
582
|
+
// Find scores to display
|
|
583
|
+
const scores = Object.entries(item.media)
|
|
584
|
+
.filter(([type, path]) => getMediaType(path) === 'score')
|
|
585
|
+
.map(([type, path]) => ({{
|
|
586
|
+
type: type.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase()),
|
|
587
|
+
value: parseFloat(path)
|
|
588
|
+
}}));
|
|
589
|
+
|
|
590
|
+
let mediaHtml = '<div class="media-placeholder">No media available</div>';
|
|
591
|
+
if (primaryMediaPath) {{
|
|
592
|
+
if (mediaType === 'video') {{
|
|
593
|
+
mediaHtml = `<video src="${{primaryMediaPath}}" muted loop></video>`;
|
|
594
|
+
}} else if (mediaType === 'gif' || mediaType === 'image') {{
|
|
595
|
+
mediaHtml = `<img src="${{primaryMediaPath}}" alt="${{item.tract}}" loading="lazy">`;
|
|
596
|
+
}}
|
|
597
|
+
}} else if (scores.length > 0) {{
|
|
598
|
+
// If only scores available, display them
|
|
599
|
+
mediaHtml = scores.map(score =>
|
|
600
|
+
`<div class="score-badge" style="margin: 0.5rem;">${{score.type}}: ${{score.value.toFixed(3)}}</div>`
|
|
601
|
+
).join('');
|
|
602
|
+
}}
|
|
603
|
+
|
|
604
|
+
const badges = mediaTypes.map(type =>
|
|
605
|
+
`<span class="media-badge">${{type}}</span>`
|
|
606
|
+
).join('');
|
|
607
|
+
|
|
608
|
+
return `
|
|
609
|
+
<div class="item-card" onclick="openModal('${{item.subject}}', '${{item.tract}}')">
|
|
610
|
+
<div class="item-header">
|
|
611
|
+
<h3>${{item.subject}}</h3>
|
|
612
|
+
<div class="tract-name">${{item.tract}}</div>
|
|
613
|
+
</div>
|
|
614
|
+
<div class="item-media">
|
|
615
|
+
${{mediaHtml}}
|
|
616
|
+
</div>
|
|
617
|
+
<div class="item-footer">
|
|
618
|
+
${{badges}}
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
`;
|
|
622
|
+
}}).join('');
|
|
623
|
+
}}
|
|
624
|
+
|
|
625
|
+
function updatePagination() {{
|
|
626
|
+
const totalPages = Math.ceil(filteredData.length / itemsPerPage);
|
|
627
|
+
document.getElementById('current-page').textContent = currentPage;
|
|
628
|
+
document.getElementById('total-pages').textContent = totalPages || 1;
|
|
629
|
+
document.getElementById('prev-page').disabled = currentPage === 1;
|
|
630
|
+
document.getElementById('next-page').disabled = currentPage >= totalPages;
|
|
631
|
+
}}
|
|
632
|
+
|
|
633
|
+
function openModal(subject, tract) {{
|
|
634
|
+
const item = filteredData.find(i => i.subject === subject && i.tract === tract);
|
|
635
|
+
if (!item) return;
|
|
636
|
+
|
|
637
|
+
document.getElementById('modal-title').textContent = `${{subject}} - ${{tract}}`;
|
|
638
|
+
const modalMedia = document.getElementById('modal-media');
|
|
639
|
+
|
|
640
|
+
// Separate scores and media files
|
|
641
|
+
const scores = [];
|
|
642
|
+
const mediaFiles = [];
|
|
643
|
+
const comparisons = [];
|
|
644
|
+
|
|
645
|
+
Object.entries(item.media).forEach(([type, path]) => {{
|
|
646
|
+
if (!path) return;
|
|
647
|
+
const mediaType = getMediaType(path);
|
|
648
|
+
|
|
649
|
+
if (mediaType === 'score') {{
|
|
650
|
+
scores.push({{ type, value: parseFloat(path) }});
|
|
651
|
+
}} else if (isComparisonType(type)) {{
|
|
652
|
+
comparisons.push({{ type, path }});
|
|
653
|
+
}} else {{
|
|
654
|
+
mediaFiles.push({{ type, path }});
|
|
655
|
+
}}
|
|
656
|
+
}});
|
|
657
|
+
|
|
658
|
+
let html = '';
|
|
659
|
+
|
|
660
|
+
// Display scores as badges
|
|
661
|
+
if (scores.length > 0) {{
|
|
662
|
+
html += scores.map(score => `
|
|
663
|
+
<div class="modal-media-item">
|
|
664
|
+
<h4>${{score.type.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase())}}</h4>
|
|
665
|
+
<div class="score-badge">${{score.value.toFixed(3)}}</div>
|
|
666
|
+
</div>
|
|
667
|
+
`).join('');
|
|
668
|
+
}}
|
|
669
|
+
|
|
670
|
+
// Display comparisons side-by-side
|
|
671
|
+
if (comparisons.length > 0) {{
|
|
672
|
+
html += comparisons.map(comp => {{
|
|
673
|
+
const mediaType = getMediaType(comp.path);
|
|
674
|
+
let mediaHtml = '';
|
|
675
|
+
|
|
676
|
+
if (mediaType === 'video') {{
|
|
677
|
+
mediaHtml = `<video src="${{comp.path}}" controls autoplay></video>`;
|
|
678
|
+
}} else if (mediaType === 'gif' || mediaType === 'image') {{
|
|
679
|
+
mediaHtml = `<img src="${{comp.path}}" alt="${{comp.type}}">`;
|
|
680
|
+
}}
|
|
681
|
+
|
|
682
|
+
// For before_after_cci, the image is already side-by-side, just add labels
|
|
683
|
+
if (comp.type === 'before_after_cci') {{
|
|
684
|
+
return `
|
|
685
|
+
<div class="modal-media-item">
|
|
686
|
+
<h4>${{comp.type.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase())}}</h4>
|
|
687
|
+
${{mediaHtml}}
|
|
688
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 0.5rem; text-align: center;">
|
|
689
|
+
<div style="font-size: 0.85rem; color: #555; font-weight: 600;">Before CCI</div>
|
|
690
|
+
<div style="font-size: 0.85rem; color: #555; font-weight: 600;">After CCI</div>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
`;
|
|
694
|
+
}} else if (comp.type === 'atlas_comparison') {{
|
|
695
|
+
// Check if we have separate subject and atlas images
|
|
696
|
+
const subjectImg = item.media.subject_image || item.media.atlas_comparison_subject;
|
|
697
|
+
const atlasImg = item.media.atlas_image || item.media.atlas_comparison_atlas;
|
|
698
|
+
|
|
699
|
+
if (subjectImg && atlasImg) {{
|
|
700
|
+
const subjectType = getMediaType(subjectImg);
|
|
701
|
+
const atlasType = getMediaType(atlasImg);
|
|
702
|
+
let subjectHtml = subjectType === 'image' || subjectType === 'gif'
|
|
703
|
+
? `<img src="${{subjectImg}}" alt="Subject">`
|
|
704
|
+
: `<img src="${{comp.path}}" alt="Subject">`;
|
|
705
|
+
let atlasHtml = atlasType === 'image' || atlasType === 'gif'
|
|
706
|
+
? `<img src="${{atlasImg}}" alt="Atlas">`
|
|
707
|
+
: `<img src="${{comp.path}}" alt="Atlas">`;
|
|
708
|
+
|
|
709
|
+
return `
|
|
710
|
+
<div class="modal-media-item">
|
|
711
|
+
<h4>${{comp.type.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase())}}</h4>
|
|
712
|
+
<div class="comparison-container">
|
|
713
|
+
<div class="comparison-item">
|
|
714
|
+
<h5>Subject</h5>
|
|
715
|
+
${{subjectHtml}}
|
|
716
|
+
</div>
|
|
717
|
+
<div class="comparison-item">
|
|
718
|
+
<h5>Atlas</h5>
|
|
719
|
+
${{atlasHtml}}
|
|
720
|
+
</div>
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
723
|
+
`;
|
|
724
|
+
}} else {{
|
|
725
|
+
// Single comparison image
|
|
726
|
+
return `
|
|
727
|
+
<div class="modal-media-item">
|
|
728
|
+
<h4>${{comp.type.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase())}}</h4>
|
|
729
|
+
${{mediaHtml}}
|
|
730
|
+
</div>
|
|
731
|
+
`;
|
|
732
|
+
}}
|
|
733
|
+
}} else if (comp.type === 'shape_similarity_image') {{
|
|
734
|
+
// Check if we have separate subject and atlas images
|
|
735
|
+
const subjectImg = item.media.shape_similarity_subject;
|
|
736
|
+
const atlasImg = item.media.shape_similarity_atlas;
|
|
737
|
+
|
|
738
|
+
if (subjectImg && atlasImg) {{
|
|
739
|
+
const subjectType = getMediaType(subjectImg);
|
|
740
|
+
const atlasType = getMediaType(atlasImg);
|
|
741
|
+
let subjectHtml = subjectType === 'image' || subjectType === 'gif'
|
|
742
|
+
? `<img src="${{subjectImg}}" alt="Subject">`
|
|
743
|
+
: `<img src="${{comp.path}}" alt="Subject">`;
|
|
744
|
+
let atlasHtml = atlasType === 'image' || atlasType === 'gif'
|
|
745
|
+
? `<img src="${{atlasImg}}" alt="Atlas">`
|
|
746
|
+
: `<img src="${{comp.path}}" alt="Atlas">`;
|
|
747
|
+
|
|
748
|
+
return `
|
|
749
|
+
<div class="modal-media-item">
|
|
750
|
+
<h4>${{comp.type.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase())}}</h4>
|
|
751
|
+
<div class="comparison-container">
|
|
752
|
+
<div class="comparison-item">
|
|
753
|
+
<h5>Subject</h5>
|
|
754
|
+
${{subjectHtml}}
|
|
755
|
+
</div>
|
|
756
|
+
<div class="comparison-item">
|
|
757
|
+
<h5>Atlas</h5>
|
|
758
|
+
${{atlasHtml}}
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
</div>
|
|
762
|
+
`;
|
|
763
|
+
}} else {{
|
|
764
|
+
// Single overlay image
|
|
765
|
+
return `
|
|
766
|
+
<div class="modal-media-item">
|
|
767
|
+
<h4>${{comp.type.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase())}}</h4>
|
|
768
|
+
${{mediaHtml}}
|
|
769
|
+
</div>
|
|
770
|
+
`;
|
|
771
|
+
}}
|
|
772
|
+
}} else {{
|
|
773
|
+
return `
|
|
774
|
+
<div class="modal-media-item">
|
|
775
|
+
<h4>${{comp.type.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase())}}</h4>
|
|
776
|
+
${{mediaHtml}}
|
|
777
|
+
</div>
|
|
778
|
+
`;
|
|
779
|
+
}}
|
|
780
|
+
}}).join('');
|
|
781
|
+
}}
|
|
782
|
+
|
|
783
|
+
// Display other media files
|
|
784
|
+
html += mediaFiles.map(media => {{
|
|
785
|
+
const mediaType = getMediaType(media.path);
|
|
786
|
+
let mediaHtml = '';
|
|
787
|
+
|
|
788
|
+
if (mediaType === 'video') {{
|
|
789
|
+
mediaHtml = `<video src="${{media.path}}" controls autoplay></video>`;
|
|
790
|
+
}} else if (mediaType === 'gif' || mediaType === 'image') {{
|
|
791
|
+
mediaHtml = `<img src="${{media.path}}" alt="${{media.type}}">`;
|
|
792
|
+
}}
|
|
793
|
+
|
|
794
|
+
return `
|
|
795
|
+
<div class="modal-media-item">
|
|
796
|
+
<h4>${{media.type.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase())}}</h4>
|
|
797
|
+
${{mediaHtml}}
|
|
798
|
+
</div>
|
|
799
|
+
`;
|
|
800
|
+
}}).join('');
|
|
801
|
+
|
|
802
|
+
modalMedia.innerHTML = html;
|
|
803
|
+
|
|
804
|
+
document.getElementById('modal').style.display = 'block';
|
|
805
|
+
}}
|
|
806
|
+
|
|
807
|
+
function closeModal() {{
|
|
808
|
+
document.getElementById('modal').style.display = 'none';
|
|
809
|
+
}}
|
|
810
|
+
|
|
811
|
+
// Event listeners
|
|
812
|
+
document.getElementById('subject-filter').addEventListener('change', filterData);
|
|
813
|
+
document.getElementById('tract-filter').addEventListener('change', filterData);
|
|
814
|
+
document.getElementById('search').addEventListener('input', filterData);
|
|
815
|
+
document.getElementById('prev-page').addEventListener('click', () => {{
|
|
816
|
+
if (currentPage > 1) {{
|
|
817
|
+
currentPage--;
|
|
818
|
+
renderItems();
|
|
819
|
+
updatePagination();
|
|
820
|
+
window.scrollTo({{ top: 0, behavior: 'smooth' }});
|
|
821
|
+
}}
|
|
822
|
+
}});
|
|
823
|
+
document.getElementById('next-page').addEventListener('click', () => {{
|
|
824
|
+
const totalPages = Math.ceil(filteredData.length / itemsPerPage);
|
|
825
|
+
if (currentPage < totalPages) {{
|
|
826
|
+
currentPage++;
|
|
827
|
+
renderItems();
|
|
828
|
+
updatePagination();
|
|
829
|
+
window.scrollTo({{ top: 0, behavior: 'smooth' }});
|
|
830
|
+
}}
|
|
831
|
+
}});
|
|
832
|
+
document.querySelector('.close').addEventListener('click', closeModal);
|
|
833
|
+
document.getElementById('modal').addEventListener('click', (e) => {{
|
|
834
|
+
if (e.target.id === 'modal') closeModal();
|
|
835
|
+
}});
|
|
836
|
+
|
|
837
|
+
// Initialize
|
|
838
|
+
filterData();
|
|
839
|
+
</script>
|
|
840
|
+
</body>
|
|
841
|
+
</html>"""
|
|
842
|
+
|
|
843
|
+
# Write HTML file
|
|
844
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
845
|
+
f.write(html_content)
|