gsMap3D 0.1.0a1__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.
Files changed (74) hide show
  1. gsMap/__init__.py +13 -0
  2. gsMap/__main__.py +4 -0
  3. gsMap/cauchy_combination_test.py +342 -0
  4. gsMap/cli.py +355 -0
  5. gsMap/config/__init__.py +72 -0
  6. gsMap/config/base.py +296 -0
  7. gsMap/config/cauchy_config.py +79 -0
  8. gsMap/config/dataclasses.py +235 -0
  9. gsMap/config/decorators.py +302 -0
  10. gsMap/config/find_latent_config.py +276 -0
  11. gsMap/config/format_sumstats_config.py +54 -0
  12. gsMap/config/latent2gene_config.py +461 -0
  13. gsMap/config/ldscore_config.py +261 -0
  14. gsMap/config/quick_mode_config.py +242 -0
  15. gsMap/config/report_config.py +81 -0
  16. gsMap/config/spatial_ldsc_config.py +334 -0
  17. gsMap/config/utils.py +286 -0
  18. gsMap/find_latent/__init__.py +3 -0
  19. gsMap/find_latent/find_latent_representation.py +312 -0
  20. gsMap/find_latent/gnn/distribution.py +498 -0
  21. gsMap/find_latent/gnn/encoder_decoder.py +186 -0
  22. gsMap/find_latent/gnn/gcn.py +85 -0
  23. gsMap/find_latent/gnn/gene_former.py +164 -0
  24. gsMap/find_latent/gnn/loss.py +18 -0
  25. gsMap/find_latent/gnn/st_model.py +125 -0
  26. gsMap/find_latent/gnn/train_step.py +177 -0
  27. gsMap/find_latent/st_process.py +781 -0
  28. gsMap/format_sumstats.py +446 -0
  29. gsMap/generate_ldscore.py +1018 -0
  30. gsMap/latent2gene/__init__.py +18 -0
  31. gsMap/latent2gene/connectivity.py +781 -0
  32. gsMap/latent2gene/entry_point.py +141 -0
  33. gsMap/latent2gene/marker_scores.py +1265 -0
  34. gsMap/latent2gene/memmap_io.py +766 -0
  35. gsMap/latent2gene/rank_calculator.py +590 -0
  36. gsMap/latent2gene/row_ordering.py +182 -0
  37. gsMap/latent2gene/row_ordering_jax.py +159 -0
  38. gsMap/ldscore/__init__.py +1 -0
  39. gsMap/ldscore/batch_construction.py +163 -0
  40. gsMap/ldscore/compute.py +126 -0
  41. gsMap/ldscore/constants.py +70 -0
  42. gsMap/ldscore/io.py +262 -0
  43. gsMap/ldscore/mapping.py +262 -0
  44. gsMap/ldscore/pipeline.py +615 -0
  45. gsMap/pipeline/quick_mode.py +134 -0
  46. gsMap/report/__init__.py +2 -0
  47. gsMap/report/diagnosis.py +375 -0
  48. gsMap/report/report.py +100 -0
  49. gsMap/report/report_data.py +1832 -0
  50. gsMap/report/static/js_lib/alpine.min.js +5 -0
  51. gsMap/report/static/js_lib/tailwindcss.js +83 -0
  52. gsMap/report/static/template.html +2242 -0
  53. gsMap/report/three_d_combine.py +312 -0
  54. gsMap/report/three_d_plot/three_d_plot_decorate.py +246 -0
  55. gsMap/report/three_d_plot/three_d_plot_prepare.py +202 -0
  56. gsMap/report/three_d_plot/three_d_plots.py +425 -0
  57. gsMap/report/visualize.py +1409 -0
  58. gsMap/setup.py +5 -0
  59. gsMap/spatial_ldsc/__init__.py +0 -0
  60. gsMap/spatial_ldsc/io.py +656 -0
  61. gsMap/spatial_ldsc/ldscore_quick_mode.py +912 -0
  62. gsMap/spatial_ldsc/spatial_ldsc_jax.py +382 -0
  63. gsMap/spatial_ldsc/spatial_ldsc_multiple_sumstats.py +439 -0
  64. gsMap/utils/__init__.py +0 -0
  65. gsMap/utils/generate_r2_matrix.py +610 -0
  66. gsMap/utils/jackknife.py +518 -0
  67. gsMap/utils/manhattan_plot.py +643 -0
  68. gsMap/utils/regression_read.py +177 -0
  69. gsMap/utils/torch_utils.py +23 -0
  70. gsmap3d-0.1.0a1.dist-info/METADATA +168 -0
  71. gsmap3d-0.1.0a1.dist-info/RECORD +74 -0
  72. gsmap3d-0.1.0a1.dist-info/WHEEL +4 -0
  73. gsmap3d-0.1.0a1.dist-info/entry_points.txt +2 -0
  74. gsmap3d-0.1.0a1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2242 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>{{ title }}</title>
8
+
9
+ <!-- Tailwind CSS (Local) -->
10
+ <script src="js_lib/tailwindcss.js"></script>
11
+ <script>
12
+ tailwind.config = {
13
+ theme: {
14
+ extend: {
15
+ colors: {
16
+ primary: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a' }
17
+ }
18
+ }
19
+ }
20
+ }
21
+ </script>
22
+
23
+ <!-- Alpine.js (Local) -->
24
+ <script defer src="js_lib/alpine.min.js"></script>
25
+
26
+ <!-- Plotly.js (Local) - Deferred for faster initial load -->
27
+ <script defer src="js_lib/plotly.min.js"></script>
28
+
29
+ <style>
30
+ [x-cloak] {
31
+ display: none !important;
32
+ }
33
+
34
+ .custom-scrollbar::-webkit-scrollbar {
35
+ width: 8px;
36
+ height: 8px;
37
+ }
38
+
39
+ .custom-scrollbar::-webkit-scrollbar-track {
40
+ background: #f1f1f1;
41
+ border-radius: 4px;
42
+ }
43
+
44
+ .custom-scrollbar::-webkit-scrollbar-thumb {
45
+ background: #c1c1c1;
46
+ border-radius: 4px;
47
+ }
48
+
49
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
50
+ background: #a1a1a1;
51
+ }
52
+
53
+ @keyframes spin {
54
+ to {
55
+ transform: rotate(360deg);
56
+ }
57
+ }
58
+
59
+ .spinner {
60
+ animation: spin 1s linear infinite;
61
+ }
62
+
63
+ .table-row-hover:hover {
64
+ background-color: #f8fafc;
65
+ }
66
+
67
+ .gradient-header {
68
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
69
+ }
70
+
71
+ .plot-container {
72
+ min-height: 450px;
73
+ contain: layout style;
74
+ }
75
+
76
+ /* GPU-accelerated collapse animation */
77
+ .section-content {
78
+ transition: opacity 0.2s ease-out, transform 0.2s ease-out;
79
+ transform-origin: top center;
80
+ overflow: hidden;
81
+ }
82
+
83
+ .section-content.collapsed {
84
+ opacity: 0;
85
+ transform: scaleY(0);
86
+ height: 0;
87
+ pointer-events: none;
88
+ }
89
+
90
+ .section-content.expanded {
91
+ opacity: 1;
92
+ transform: scaleY(1);
93
+ }
94
+ </style>
95
+ </head>
96
+
97
+ <body class="bg-gray-100 text-gray-800 font-sans antialiased" x-data="gsMapApp()" x-init="init()" x-cloak>
98
+
99
+ <!-- Header -->
100
+ <header class="gradient-header text-white shadow-lg sticky top-0 z-50">
101
+ <div class="max-w-full mx-auto px-4 sm:px-6 lg:px-8">
102
+ <div class="flex items-center justify-between h-14">
103
+ <div class="flex items-center gap-3">
104
+ <div class="bg-white/20 backdrop-blur-sm px-3 py-1 rounded-lg">
105
+ <span class="text-xl font-bold">gsMap</span>
106
+ </div>
107
+ <div class="hidden sm:block">
108
+ <h1 class="font-semibold">{{ project_name }}</h1>
109
+ <p class="text-xs text-white/70">{{ generated_at }}</p>
110
+ </div>
111
+ <!-- Dataset Type Badge -->
112
+ <span x-show="reportMeta?.dataset_type" class="ml-2 px-3 py-1 rounded-full text-sm font-medium"
113
+ :class="{
114
+ 'bg-purple-100 text-purple-800': reportMeta?.dataset_type === 'spatial3D',
115
+ 'bg-blue-100 text-blue-800': reportMeta?.dataset_type === 'spatial2D',
116
+ 'bg-green-100 text-green-800': reportMeta?.dataset_type === 'scRNA'
117
+ }" x-text="reportMeta?.dataset_type_label || reportMeta?.dataset_type">
118
+ </span>
119
+ </div>
120
+ <div class="flex items-center gap-4">
121
+ <!-- Loading Indicator in header -->
122
+ <span x-show="isLoading" class="inline-flex items-center text-sm text-white/80">
123
+ <svg class="spinner -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none"
124
+ viewBox="0 0 24 24">
125
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4">
126
+ </circle>
127
+ <path class="opacity-75" fill="currentColor"
128
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
129
+ </svg>
130
+ Loading...
131
+ </span>
132
+ <div class="text-sm text-white/80">v{{ gsmap_version }}</div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </header>
137
+
138
+ <!-- Layout: Sidebar + Main Content -->
139
+ <div class="flex min-h-[calc(100vh-3.5rem)]">
140
+
141
+ <!-- ==================== SIDEBAR ==================== -->
142
+ <aside x-show="dataLoaded"
143
+ class="w-64 bg-white border-r border-gray-200 flex-shrink-0 sticky top-14 h-[calc(100vh-3.5rem)] overflow-y-auto">
144
+ <div class="p-4 space-y-6">
145
+ <!-- Sidebar Header -->
146
+ <div class="pb-3 border-b border-gray-200">
147
+ <h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wider">Controls</h2>
148
+ </div>
149
+
150
+ <!-- Trait Selector -->
151
+ <div>
152
+ <label class="block text-sm font-medium text-gray-700 mb-2">Trait</label>
153
+ <select :value="selectedTrait" @change="handleTraitSelect($event.target.value)"
154
+ class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 text-sm p-2.5 border bg-white">
155
+ <template x-for="t in traits" :key="t">
156
+ <option :value="t" x-text="t"></option>
157
+ </template>
158
+ </select>
159
+ </div>
160
+
161
+ <!-- Sample Selector -->
162
+ <div>
163
+ <label class="block text-sm font-medium text-gray-700 mb-2">Sample</label>
164
+ <select x-model="selectedSample" @change="onSampleChange()"
165
+ class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 text-sm p-2.5 border bg-white">
166
+ <template x-for="s in samples" :key="s">
167
+ <option :value="s" x-text="s"></option>
168
+ </template>
169
+ </select>
170
+ </div>
171
+
172
+ <!-- Annotation Category Selector -->
173
+ <div>
174
+ <label class="block text-sm font-medium text-gray-700 mb-2">Annotation</label>
175
+ <select x-model="selectedAnnotationCategory" @change="onAnnotationChange()"
176
+ class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 text-sm p-2.5 border bg-white">
177
+ <template x-for="anno in annotations" :key="anno">
178
+ <option :value="anno" x-text="anno"></option>
179
+ </template>
180
+ </select>
181
+ </div>
182
+
183
+ <!-- Gene Selector (only top genes) -->
184
+ <div>
185
+ <label class="block text-sm font-medium text-gray-700 mb-2">
186
+ Top Gene <span class="text-gray-400 font-normal">(<span
187
+ x-text="topGenesDropdown.length"></span>)</span>
188
+ </label>
189
+ <select x-model="selectedGene" @change="onGeneChange()"
190
+ class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 text-sm p-2.5 border bg-white">
191
+ <template x-for="g in topGenesDropdown" :key="g.gene">
192
+ <option :value="g.gene" x-text="g.gene + ' (' + (g.PCC ? g.PCC.toFixed(3) : 'N/A') + ')'">
193
+ </option>
194
+ </template>
195
+ </select>
196
+ </div>
197
+
198
+ <!-- Summary Stats -->
199
+ <div class="pt-4 border-t border-gray-200">
200
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Summary</h3>
201
+ <div class="space-y-2 text-sm">
202
+ <div class="flex justify-between">
203
+ <span class="text-gray-500">Traits</span>
204
+ <span class="font-medium text-gray-900" x-text="traits.length"></span>
205
+ </div>
206
+ <div class="flex justify-between">
207
+ <span class="text-gray-500">Samples</span>
208
+ <span class="font-medium text-gray-900" x-text="samples.length"></span>
209
+ </div>
210
+ <div class="flex justify-between">
211
+ <span class="text-gray-500">Annotations</span>
212
+ <span class="font-medium text-gray-900" x-text="annotations.length"></span>
213
+ </div>
214
+ </div>
215
+ </div>
216
+
217
+ <!-- Section Navigation -->
218
+ <div class="pt-4 border-t border-gray-200">
219
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Contents</h3>
220
+ <nav class="space-y-1">
221
+ <template x-for="section in visibleSections" :key="section.id">
222
+ <button
223
+ x-show="(section.id !== 'sec-spatial3d' || has3dWidget) && (section.id !== 'sec-umap' || umapData)"
224
+ @click="sections[section.id.replace('sec-', '')] = true; $nextTick(() => document.getElementById(section.id)?.scrollIntoView({behavior: 'smooth'}))"
225
+ class="w-full text-left px-3 py-2 text-sm rounded-lg hover:bg-gray-100 text-gray-700"
226
+ x-text="`${section.number}. ${section.title}`">
227
+ </button>
228
+ </template>
229
+ </nav>
230
+ </div>
231
+ </div>
232
+ </aside>
233
+
234
+ <!-- ==================== MAIN CONTENT ==================== -->
235
+ <main class="flex-1 p-6 space-y-6 overflow-y-auto">
236
+
237
+ <!-- Debug info (hidden by default, shown if no data) -->
238
+ <div x-show="!dataLoaded" class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
239
+ <p class="text-yellow-800 font-medium">Loading data...</p>
240
+ <p class="text-yellow-600 text-sm mt-1">If this message persists, please check that js_data/ folder
241
+ exists
242
+ with the required .js files.</p>
243
+ </div>
244
+
245
+ <!-- ==================== SECTION 1: 3D SPATIAL VISUALIZATION ==================== -->
246
+ <div id="sec-spatial3d" x-show="dataLoaded && currentSections.show3d && has3dWidget"
247
+ class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
248
+ <div class="p-4 border-b border-gray-100 flex justify-between items-center cursor-pointer"
249
+ @click="sections.spatial3d = !sections.spatial3d">
250
+ <div>
251
+ <h2 class="text-lg font-semibold text-gray-800" x-text="getSectionTitle('sec-spatial3d')"></h2>
252
+ <p class="text-sm text-gray-500">Interactive 3D view of spatial data with -log10(p) values and
253
+ annotations side by side.</p>
254
+ </div>
255
+ <svg class="w-5 h-5 text-gray-500 transition-transform"
256
+ :class="sections.spatial3d ? 'rotate-180' : ''" fill="none" stroke="currentColor"
257
+ viewBox="0 0 24 24">
258
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
259
+ </svg>
260
+ </div>
261
+ <div class="section-content" :class="sections.spatial3d ? 'expanded' : 'collapsed'">
262
+ <!-- 3D Controls -->
263
+ <div class="p-4 bg-gray-50 border-b border-gray-200 flex flex-wrap items-center gap-6">
264
+ <div class="flex items-center gap-2">
265
+ <label class="text-sm font-medium text-gray-700">Trait:</label>
266
+ <span class="text-sm text-gray-600 bg-white px-3 py-2 rounded-lg border border-gray-300"
267
+ x-text="selectedTrait"></span>
268
+ </div>
269
+ <div class="flex items-center gap-2">
270
+ <label class="text-sm font-medium text-gray-700">Annotation:</label>
271
+ <span class="text-sm text-gray-600 bg-white px-3 py-2 rounded-lg border border-gray-300"
272
+ x-text="selectedAnnotationCategory"></span>
273
+ </div>
274
+ </div>
275
+
276
+ <!-- Side-by-side 3D views -->
277
+ <div class="p-2">
278
+ <div class="flex flex-wrap gap-4">
279
+ <!-- Left: Trait -log10(p) -->
280
+ <div class="flex-1 min-w-[500px] relative">
281
+ <h3 class="text-center font-medium text-gray-700 mb-2">
282
+ -log10(p) - <span x-text="selectedTrait"></span>
283
+ </h3>
284
+ <!-- Loading Overlay for Trait -->
285
+ <div x-show="spatial3dTraitLoading"
286
+ class="absolute inset-0 top-8 bg-white/60 backdrop-blur-[1px] z-10 flex items-center justify-center rounded-lg">
287
+ <div class="flex flex-col items-center gap-3">
288
+ <svg class="spinner h-10 w-10 text-primary-600"
289
+ xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
290
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
291
+ stroke-width="4"></circle>
292
+ <path class="opacity-75" fill="currentColor"
293
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
294
+ </svg>
295
+ <span class="text-sm font-medium text-gray-600">Loading 3D Scene...</span>
296
+ </div>
297
+ </div>
298
+ <template x-if="spatial3dTraitPath">
299
+ <iframe :src="spatial3dTraitPath" @load="spatial3dTraitLoading = false"
300
+ class="w-full border-0 rounded-lg" style="height: 800px; min-height: 600px;"
301
+ loading="lazy" title="3D Spatial Visualization - Trait">
302
+ </iframe>
303
+ </template>
304
+ <div x-show="!spatial3dTraitPath" class="p-8 text-center text-gray-500"
305
+ style="height: 800px;">
306
+ No 3D visualization available for this trait.
307
+ </div>
308
+ </div>
309
+ <!-- Right: Annotation -->
310
+ <div class="flex-1 min-w-[500px] relative">
311
+ <h3 class="text-center font-medium text-gray-700 mb-2">
312
+ Annotation - <span x-text="selectedAnnotationCategory"></span>
313
+ </h3>
314
+ <!-- Loading Overlay for Annotation -->
315
+ <div x-show="spatial3dAnnotationLoading"
316
+ class="absolute inset-0 top-8 bg-white/60 backdrop-blur-[1px] z-10 flex items-center justify-center rounded-lg">
317
+ <div class="flex flex-col items-center gap-3">
318
+ <svg class="spinner h-10 w-10 text-primary-600"
319
+ xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
320
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
321
+ stroke-width="4"></circle>
322
+ <path class="opacity-75" fill="currentColor"
323
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
324
+ </svg>
325
+ <span class="text-sm font-medium text-gray-600">Loading 3D Scene...</span>
326
+ </div>
327
+ </div>
328
+ <template x-if="spatial3dAnnotationPath">
329
+ <iframe :src="spatial3dAnnotationPath" @load="spatial3dAnnotationLoading = false"
330
+ class="w-full border-0 rounded-lg" style="height: 800px; min-height: 600px;"
331
+ loading="lazy" title="3D Spatial Visualization - Annotation">
332
+ </iframe>
333
+ </template>
334
+ <div x-show="!spatial3dAnnotationPath" class="p-8 text-center text-gray-500"
335
+ style="height: 800px;">
336
+ No 3D visualization available for this annotation.
337
+ </div>
338
+ </div>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ </div>
343
+
344
+ <!-- ==================== SECTION 2: gsMap 2D Distribution ==================== -->
345
+ <div id="sec-gsmap" x-show="dataLoaded && currentSections.show2dDistribution"
346
+ class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
347
+ <div class="p-4 border-b border-gray-100 flex justify-between items-center cursor-pointer"
348
+ @click="sections.gsmap = !sections.gsmap">
349
+ <div>
350
+ <h2 class="text-lg font-semibold text-gray-800">
351
+ <span x-text="getSectionTitle('sec-gsmap')"></span> -
352
+ <span x-text="selectedSample"></span>
353
+ </h2>
354
+ <p class="text-sm text-gray-500">Spatial distribution of -log10(p) values and annotations side
355
+ by side.</p>
356
+ </div>
357
+ <svg class="w-5 h-5 text-gray-500 transition-transform" :class="sections.gsmap ? 'rotate-180' : ''"
358
+ fill="none" stroke="currentColor" viewBox="0 0 24 24">
359
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
360
+ </svg>
361
+ </div>
362
+ <div class="section-content" :class="sections.gsmap ? 'expanded' : 'collapsed'">
363
+ <!-- Spatial 2D Controls -->
364
+ <div class="p-4 bg-gray-50 border-b border-gray-200 flex flex-wrap items-center gap-6">
365
+ <div class="flex items-center gap-2">
366
+ <label class="text-sm font-medium text-gray-700">Trait:</label>
367
+ <span class="text-sm text-gray-600 bg-white px-3 py-2 rounded-lg border border-gray-300"
368
+ x-text="selectedTrait"></span>
369
+ </div>
370
+ <div class="flex items-center gap-2">
371
+ <label class="text-sm font-medium text-gray-700">Annotation:</label>
372
+ <span class="text-sm text-gray-600 bg-white px-3 py-2 rounded-lg border border-gray-300"
373
+ x-text="selectedAnnotationCategory"></span>
374
+ </div>
375
+ </div>
376
+
377
+ <!-- Plotly Container with Loading Indicator -->
378
+ <div class="p-4">
379
+ <!-- Loading indicator -->
380
+ <div x-show="loadingSampleData" class="flex items-center justify-center py-12">
381
+ <svg class="animate-spin h-8 w-8 text-primary-600 mr-3" xmlns="http://www.w3.org/2000/svg"
382
+ fill="none" viewBox="0 0 24 24">
383
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
384
+ stroke-width="4"></circle>
385
+ <path class="opacity-75" fill="currentColor"
386
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
387
+ </path>
388
+ </svg>
389
+ <span class="text-gray-500 text-lg">Loading sample data...</span>
390
+ </div>
391
+ <!-- Side-by-side plot containers -->
392
+ <div x-show="!loadingSampleData" class="flex flex-wrap gap-4">
393
+ <!-- Left: Trait -log10(p) -->
394
+ <div class="flex-1 min-w-[400px] relative">
395
+ <h3 class="text-center font-medium text-gray-700 mb-2">
396
+ -log10(p) - <span x-text="selectedTrait"></span>
397
+ </h3>
398
+ <!-- Loading Overlay for Trait plot -->
399
+ <div x-show="loadingSpatial2dTraitData"
400
+ class="absolute inset-0 top-8 bg-white/60 backdrop-blur-[1px] z-10 flex items-center justify-center rounded-lg">
401
+ <div class="flex flex-col items-center gap-3">
402
+ <svg class="spinner h-10 w-10 text-primary-600"
403
+ xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
404
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
405
+ stroke-width="4"></circle>
406
+ <path class="opacity-75" fill="currentColor"
407
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
408
+ </svg>
409
+ <span class="text-sm font-medium text-gray-600">Loading trait data...</span>
410
+ </div>
411
+ </div>
412
+ <div id="spatial2dPlotTrait" class="w-full" style="height: 500px;"></div>
413
+ </div>
414
+ <!-- Right: Annotation -->
415
+ <div class="flex-1 min-w-[400px]">
416
+ <h3 class="text-center font-medium text-gray-700 mb-2">
417
+ Annotation - <span x-text="selectedAnnotationCategory"></span>
418
+ </h3>
419
+ <div id="spatial2dPlotAnnotation" class="w-full" style="height: 500px;"></div>
420
+ </div>
421
+ </div>
422
+ </div>
423
+ </div>
424
+ </div>
425
+
426
+ <!-- ==================== SECTION 3: GSS Distribution ==================== -->
427
+ <div id="sec-genes" x-show="dataLoaded && currentSections.showGSS"
428
+ class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
429
+ <div class="p-4 border-b border-gray-100 flex justify-between items-center cursor-pointer"
430
+ @click="sections.genes = !sections.genes">
431
+ <div>
432
+ <h2 class="text-lg font-semibold text-gray-800">
433
+ <span x-text="getSectionTitle('sec-genes')"></span> -
434
+ <span x-text="selectedGene"></span> -
435
+ <span x-text="selectedSample"></span>
436
+ </h2>
437
+ <p class="text-sm text-gray-500">Gene expression and GSS distribution for selected sample.</p>
438
+ </div>
439
+ <svg class="w-5 h-5 text-gray-500 transition-transform" :class="sections.genes ? 'rotate-180' : ''"
440
+ fill="none" stroke="currentColor" viewBox="0 0 24 24">
441
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
442
+ </svg>
443
+ </div>
444
+ <div class="section-content" :class="sections.genes ? 'expanded' : 'collapsed'">
445
+ <!-- Gene Plot Info Bar -->
446
+ <div class="p-4 bg-gray-50 border-b border-gray-200 flex flex-wrap items-center gap-6">
447
+ <div class="flex items-center gap-2">
448
+ <label class="text-sm font-medium text-gray-700">Gene:</label>
449
+ <span class="text-sm text-gray-600 bg-white px-3 py-2 rounded-lg border border-gray-300"
450
+ x-text="selectedGene"></span>
451
+ </div>
452
+ <div class="flex items-center gap-2">
453
+ <label class="text-sm font-medium text-gray-700">Sample:</label>
454
+ <span class="text-sm text-gray-600 bg-white px-3 py-2 rounded-lg border border-gray-300"
455
+ x-text="selectedSample"></span>
456
+ </div>
457
+ </div>
458
+
459
+ <!-- Gene Plots (Side by Side: Expression and GSS) -->
460
+ <div class="p-4">
461
+ <!-- Loading indicator for gene plots -->
462
+ <div x-show="loadingGeneData" class="flex items-center justify-center py-12">
463
+ <svg class="animate-spin h-8 w-8 text-primary-600 mr-3" xmlns="http://www.w3.org/2000/svg"
464
+ fill="none" viewBox="0 0 24 24">
465
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
466
+ stroke-width="4"></circle>
467
+ <path class="opacity-75" fill="currentColor"
468
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
469
+ </path>
470
+ </svg>
471
+ <span class="text-gray-500 text-lg">Loading gene data...</span>
472
+ </div>
473
+ <!-- Side-by-side static image containers - only render when ready -->
474
+ <template x-if="!loadingGeneData && selectedGene && selectedTrait && selectedSample">
475
+ <div class="flex flex-wrap gap-4 justify-center">
476
+ <div class="flex-1 min-w-[400px] max-w-[600px]">
477
+ <h3 class="text-center font-medium text-gray-700 mb-2">Expression</h3>
478
+ <img :src="'gene_diagnostic_plots/gene_' + selectedTrait + '_' + selectedGene + '_' + selectedSample.replace(/[^a-zA-Z0-9]/g, '_') + '_exp.png'"
479
+ :alt="selectedGene + ' Expression'"
480
+ class="w-full h-auto max-h-[500px] object-contain mx-auto"
481
+ @error="$el.style.display='none'; $el.nextElementSibling.style.display='block';"
482
+ @load="$el.style.display=''; $el.nextElementSibling.style.display='none';">
483
+ <p class="text-center text-gray-400 py-8 text-sm" style="display: none;">
484
+ Expression plot not available for this selection.</p>
485
+ </div>
486
+ <div class="flex-1 min-w-[400px] max-w-[600px]">
487
+ <h3 class="text-center font-medium text-gray-700 mb-2">GSS</h3>
488
+ <img :src="'gene_diagnostic_plots/gene_' + selectedTrait + '_' + selectedGene + '_' + selectedSample.replace(/[^a-zA-Z0-9]/g, '_') + '_gss.png'"
489
+ :alt="selectedGene + ' GSS'"
490
+ class="w-full h-auto max-h-[500px] object-contain mx-auto"
491
+ @error="$el.style.display='none'; $el.nextElementSibling.style.display='block';"
492
+ @load="$el.style.display=''; $el.nextElementSibling.style.display='none';">
493
+ <p class="text-center text-gray-400 py-8 text-sm" style="display: none;">
494
+ GSS plot not available for this selection.</p>
495
+ </div>
496
+ </div>
497
+ </template>
498
+ </div>
499
+
500
+ <!-- Gene Diagnostic Table -->
501
+ <div class="border-t border-gray-200">
502
+ <div class="p-4 bg-gray-50 flex justify-between items-center">
503
+ <h3 class="font-medium text-gray-700">Gene Diagnostic Table</h3>
504
+ <input type="text" x-model.debounce.300ms="geneSearchQuery" placeholder="Search genes..."
505
+ class="rounded-lg border-gray-300 shadow-sm text-sm p-2 border w-48">
506
+ </div>
507
+ <div class="overflow-x-auto overflow-y-auto custom-scrollbar max-h-[400px]"
508
+ @scroll="geneTableScroll = $event.target.scrollTop">
509
+ <div :style="{ minHeight: virtualGeneTable.totalHeight + 'px', position: 'relative' }">
510
+ <table class="min-w-full divide-y divide-gray-200"
511
+ :style="{ position: 'absolute', top: virtualGeneTable.offsetY + 'px', left: 0, right: 0 }">
512
+ <thead class="bg-gray-50">
513
+ <tr>
514
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase cursor-pointer hover:bg-gray-100"
515
+ @click="sortGenesBy('gene')">Gene</th>
516
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase cursor-pointer hover:bg-gray-100"
517
+ @click="sortGenesBy('PCC')">PCC</th>
518
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase cursor-pointer hover:bg-gray-100"
519
+ @click="sortGenesBy('Annotation')">Annotation</th>
520
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase cursor-pointer hover:bg-gray-100"
521
+ @click="sortGenesBy('Median_GSS')">Median GSS</th>
522
+ </tr>
523
+ </thead>
524
+ <tbody class="bg-white divide-y divide-gray-200">
525
+ <template x-for="row in virtualGeneTable.items" :key="row.gene">
526
+ <tr class="transition-colors duration-150"
527
+ :style="{ height: geneTableRowHeight + 'px' }" :class="{
528
+ 'bg-primary-50': selectedGene === row.gene,
529
+ 'cursor-pointer hover:bg-gray-50': row.isTop,
530
+ 'cursor-not-allowed opacity-60 bg-gray-50': !row.isTop
531
+ }" @click="if(row.isTop) { selectedGene = row.gene; onGeneChange(); }">
532
+ <td class="px-4 py-2 text-sm text-gray-900"
533
+ :class="{'font-bold': row.isTop, 'font-medium': !row.isTop}"
534
+ x-text="row.gene"></td>
535
+ <td class="px-4 py-2 text-sm text-gray-600 font-mono"
536
+ :class="{'font-bold': row.isTop}"
537
+ x-text="row.PCC ? row.PCC.toFixed(4) : 'N/A'"></td>
538
+ <td class="px-4 py-2 text-sm"><span
539
+ class="px-2 py-0.5 text-xs rounded-full bg-gray-100"
540
+ x-text="row.Annotation || 'N/A'"></span></td>
541
+ <td class="px-4 py-2 text-sm text-gray-600 font-mono"
542
+ x-text="row.Median_GSS ? row.Median_GSS.toFixed(4) : 'N/A'"></td>
543
+ </tr>
544
+ </template>
545
+ </tbody>
546
+ </table>
547
+ </div>
548
+ <div x-show="virtualGeneTable.totalCount > 0"
549
+ class="sticky bottom-0 bg-gray-100 text-xs text-gray-500 p-2 text-center border-t">
550
+ Showing <span x-text="virtualGeneTable.items.length"></span> of <span
551
+ x-text="virtualGeneTable.totalCount"></span> genes
552
+ </div>
553
+ </div>
554
+ </div>
555
+ </div>
556
+ </div>
557
+
558
+ <!-- ==================== SECTION 4: Annotations 2D Distribution (Hidden - merged into sec-gsmap) ==================== -->
559
+ <div id="sec-annotation" x-show="false"
560
+ class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
561
+ <div class="p-4 border-b border-gray-100 flex justify-between items-center cursor-pointer"
562
+ @click="sections.annotation = !sections.annotation">
563
+ <div>
564
+ <h2 class="text-lg font-semibold text-gray-800"><span
565
+ x-text="getSectionTitle('sec-annotation')"></span> - <span
566
+ x-text="selectedAnnotationCategory"></span></h2>
567
+ <p class="text-sm text-gray-500">Spatial distribution of annotations across samples.</p>
568
+ </div>
569
+ <svg class="w-5 h-5 text-gray-500 transition-transform"
570
+ :class="sections.annotation ? 'rotate-180' : ''" fill="none" stroke="currentColor"
571
+ viewBox="0 0 24 24">
572
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
573
+ </svg>
574
+ </div>
575
+ <div class="section-content p-4" :class="sections.annotation ? 'expanded' : 'collapsed'">
576
+ <img x-show="selectedAnnotationCategory"
577
+ :src="'annotation_plots/anno_' + selectedAnnotationCategory + '.png'"
578
+ :alt="selectedAnnotationCategory" class="w-full h-auto max-h-[700px] object-contain mx-auto"
579
+ loading="lazy" onerror="this.style.display='none';">
580
+ <p x-show="!selectedAnnotationCategory" class="text-center text-gray-500 py-8">Select an annotation
581
+ category to view the plot.</p>
582
+ </div>
583
+ </div>
584
+
585
+ <!-- ==================== SECTION 5: UMAP EMBEDDINGS ==================== -->
586
+ <div id="sec-umap" x-show="dataLoaded && currentSections.showUMAP && umapData"
587
+ class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
588
+ <div class="p-4 border-b border-gray-100 flex justify-between items-center cursor-pointer"
589
+ @click="sections.umap = !sections.umap">
590
+ <div>
591
+ <h2 class="text-lg font-semibold text-gray-800" x-text="getSectionTitle('sec-umap')"></h2>
592
+ <p class="text-sm text-gray-500">UMAP visualization of cell and niche embeddings with -log10(p)
593
+ and annotations.</p>
594
+ </div>
595
+ <svg class="w-5 h-5 text-gray-500 transition-transform" :class="sections.umap ? 'rotate-180' : ''"
596
+ fill="none" stroke="currentColor" viewBox="0 0 24 24">
597
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
598
+ </svg>
599
+ </div>
600
+ <div class="section-content" :class="sections.umap ? 'expanded' : 'collapsed'">
601
+ <!-- UMAP Controls -->
602
+ <div class="p-4 bg-gray-50 border-b border-gray-200 flex flex-wrap items-center gap-6">
603
+ <div class="flex items-center gap-2">
604
+ <label class="text-sm font-medium text-gray-700">Trait:</label>
605
+ <span class="text-sm text-gray-600 bg-white px-3 py-2 rounded-lg border border-gray-300"
606
+ x-text="selectedTrait"></span>
607
+ </div>
608
+ <div class="flex items-center gap-2">
609
+ <label class="text-sm font-medium text-gray-700">Annotation:</label>
610
+ <span class="text-sm text-gray-600 bg-white px-3 py-2 rounded-lg border border-gray-300"
611
+ x-text="selectedAnnotationCategory"></span>
612
+ </div>
613
+ <div class="text-sm text-gray-500">
614
+ <span x-text="umapData ? umapData.spot.length : 0"></span> spots
615
+ </div>
616
+ </div>
617
+
618
+ <!-- UMAP Plots Container -->
619
+ <div class="p-4">
620
+ <!-- Row 1: Cell Embedding -->
621
+ <div class="mb-6">
622
+ <h3 class="text-lg font-medium text-gray-800 mb-3 border-b border-gray-200 pb-2">Cell
623
+ Embedding</h3>
624
+ <div class="flex flex-wrap gap-4">
625
+ <!-- Left: Trait -log10(p) -->
626
+ <div class="flex-1 min-w-[400px]">
627
+ <h4 class="text-center font-medium text-gray-600 mb-2 text-sm">
628
+ -log10(p) - <span x-text="selectedTrait"></span>
629
+ </h4>
630
+ <div id="umapCellPlotTrait" class="w-full" style="height: 400px;"></div>
631
+ </div>
632
+ <!-- Right: Annotation -->
633
+ <div class="flex-1 min-w-[400px]">
634
+ <h4 class="text-center font-medium text-gray-600 mb-2 text-sm">
635
+ Annotation - <span x-text="selectedAnnotationCategory"></span>
636
+ </h4>
637
+ <div id="umapCellPlotAnnotation" class="w-full" style="height: 400px;"></div>
638
+ </div>
639
+ </div>
640
+ </div>
641
+
642
+ <!-- Row 2: Niche Embedding (only if available) -->
643
+ <div x-show="umapHasNiche">
644
+ <h3 class="text-lg font-medium text-gray-800 mb-3 border-b border-gray-200 pb-2">Niche
645
+ Embedding</h3>
646
+ <div class="flex flex-wrap gap-4">
647
+ <!-- Left: Trait -log10(p) -->
648
+ <div class="flex-1 min-w-[400px]">
649
+ <h4 class="text-center font-medium text-gray-600 mb-2 text-sm">
650
+ -log10(p) - <span x-text="selectedTrait"></span>
651
+ </h4>
652
+ <div id="umapNichePlotTrait" class="w-full" style="height: 400px;"></div>
653
+ </div>
654
+ <!-- Right: Annotation -->
655
+ <div class="flex-1 min-w-[400px]">
656
+ <h4 class="text-center font-medium text-gray-600 mb-2 text-sm">
657
+ Annotation - <span x-text="selectedAnnotationCategory"></span>
658
+ </h4>
659
+ <div id="umapNichePlotAnnotation" class="w-full" style="height: 400px;"></div>
660
+ </div>
661
+ </div>
662
+ </div>
663
+ </div>
664
+ </div>
665
+ </div>
666
+
667
+ <!-- ==================== SECTION 6: MANHATTAN PLOT ==================== -->
668
+ <div id="sec-manhattan" x-show="dataLoaded && currentSections.showManhattan"
669
+ class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
670
+ <div class="p-4 border-b border-gray-100 flex justify-between items-center cursor-pointer"
671
+ @click="sections.manhattan = !sections.manhattan">
672
+ <div>
673
+ <h2 class="text-lg font-semibold text-gray-800" x-text="getSectionTitle('sec-manhattan')"></h2>
674
+ <p class="text-sm text-gray-500">GWAS results. Red = top PCC genes. Green diamond = selected
675
+ gene.</p>
676
+ </div>
677
+ <svg class="w-5 h-5 text-gray-500 transition-transform"
678
+ :class="sections.manhattan ? 'rotate-180' : ''" fill="none" stroke="currentColor"
679
+ viewBox="0 0 24 24">
680
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
681
+ </svg>
682
+ </div>
683
+ <div class="section-content" :class="sections.manhattan ? 'expanded' : 'collapsed'">
684
+ <div id="manhattanPlot" class="plot-container w-full"></div>
685
+ </div>
686
+ </div>
687
+
688
+ <!-- ==================== SECTION 7: ENRICHMENT ANALYSIS ==================== -->
689
+ <div id="sec-cauchy" x-show="dataLoaded && currentSections.showEnrichment"
690
+ class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
691
+ <div class="p-4 border-b border-gray-100 flex justify-between items-center cursor-pointer"
692
+ @click="sections.cauchy = !sections.cauchy">
693
+ <div>
694
+ <h2 class="text-lg font-semibold text-gray-800" x-text="getSectionTitle('sec-cauchy')"></h2>
695
+ <p class="text-sm text-gray-500">Statistical significance of annotations.</p>
696
+ </div>
697
+ <svg class="w-5 h-5 text-gray-500 transition-transform" :class="sections.cauchy ? 'rotate-180' : ''"
698
+ fill="none" stroke="currentColor" viewBox="0 0 24 24">
699
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
700
+ </svg>
701
+ </div>
702
+ <div class="section-content" :class="sections.cauchy ? 'expanded' : 'collapsed'">
703
+ <!-- Cauchy Controls -->
704
+ <div class="p-4 bg-gray-50 border-b border-gray-200 flex flex-wrap gap-4">
705
+ <div class="flex items-center gap-2">
706
+ <label class="text-sm font-medium text-gray-700">View:</label>
707
+ <select x-model="cauchyViewMode" @change="renderCauchyHeatmap()"
708
+ class="rounded-lg border-gray-300 shadow-sm text-sm p-2 border bg-white">
709
+ <option value="all">All Samples (Aggregated)</option>
710
+ <option value="sample">Per Sample</option>
711
+ </select>
712
+ </div>
713
+ </div>
714
+
715
+ <!-- Cauchy Table -->
716
+ <div class="overflow-x-auto custom-scrollbar max-h-[400px]">
717
+ <table class="min-w-full divide-y divide-gray-200">
718
+ <thead class="bg-gray-50 sticky top-0">
719
+ <tr>
720
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase cursor-pointer hover:bg-gray-100"
721
+ @click="sortCauchyBy('trait')">Trait</th>
722
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase cursor-pointer hover:bg-gray-100"
723
+ @click="sortCauchyBy('annotation')">Annotation</th>
724
+ <th x-show="cauchyViewMode === 'sample'"
725
+ class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase cursor-pointer hover:bg-gray-100"
726
+ @click="sortCauchyBy('sample_name')">Sample</th>
727
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase cursor-pointer hover:bg-gray-100"
728
+ @click="sortCauchyBy('mlog10_p_cauchy')">-log10P (Cauchy)</th>
729
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase cursor-pointer hover:bg-gray-100"
730
+ @click="sortCauchyBy('mlog10_p_median')">-log10P (Median)</th>
731
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase cursor-pointer hover:bg-gray-100"
732
+ @click="sortCauchyBy('top_95_quantile')">95% Quantile (-log10P)</th>
733
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Total
734
+ Spots</th>
735
+ </tr>
736
+ </thead>
737
+ <tbody class="bg-white divide-y divide-gray-200">
738
+ <template x-for="(row, idx) in filteredCauchyTable" :key="idx">
739
+ <tr class="table-row-hover">
740
+ <td class="px-4 py-2 text-sm font-medium text-gray-900" x-text="row.trait"></td>
741
+ <td class="px-4 py-2 text-sm"><span
742
+ class="px-2 py-0.5 text-xs rounded-full bg-primary-100 text-primary-700"
743
+ x-text="row.annotation"></span></td>
744
+ <td x-show="cauchyViewMode === 'sample'" class="px-4 py-2 text-sm text-gray-600"
745
+ x-text="row.sample_name"></td>
746
+ <td class="px-4 py-2 text-sm font-mono text-gray-600"
747
+ x-text="row.mlog10_p_cauchy != null ? row.mlog10_p_cauchy.toFixed(2) : 'N/A'">
748
+ </td>
749
+ <td class="px-4 py-2 text-sm font-mono text-gray-600"
750
+ x-text="row.mlog10_p_median != null ? row.mlog10_p_median.toFixed(2) : 'N/A'">
751
+ </td>
752
+ <td class="px-4 py-2 text-sm font-mono text-gray-600"
753
+ x-text="row.top_95_quantile != null ? row.top_95_quantile.toFixed(2) : 'N/A'">
754
+ </td>
755
+ <td class="px-4 py-2 text-sm text-gray-600" x-text="row.total_spots"></td>
756
+ </tr>
757
+ </template>
758
+ </tbody>
759
+ </table>
760
+ </div>
761
+
762
+ <!-- Cauchy Heatmap -->
763
+ <div class="border-t border-gray-200">
764
+ <div
765
+ class="p-4 bg-gray-50 border-b border-gray-200 flex flex-wrap justify-between items-center gap-4">
766
+ <div>
767
+ <h3 class="font-medium text-gray-700" x-text="'7.2 Cauchy Enrichment Heatmap (' + (
768
+ cauchyHeatmapMetric === 'mlog10_p_cauchy' ? 'Cauchy P-Value' :
769
+ cauchyHeatmapMetric === 'mlog10_p_median' ? 'Median P-Value' : 'Q95'
770
+ ) + ')'"></h3>
771
+ <p class="text-xs text-gray-500 mt-1" x-text="
772
+ cauchyHeatmapMetric === 'mlog10_p_cauchy' ? 'Aggregated significance across all spots.' :
773
+ cauchyHeatmapMetric === 'mlog10_p_median' ? 'Median spot significance in the annotation.' :
774
+ 'The 95th percentile of significance (-log10p) across spots.'
775
+ "></p>
776
+ </div>
777
+ <div class="flex flex-wrap items-center gap-4">
778
+ <div class="flex items-center gap-2">
779
+ <label class="text-sm font-medium text-gray-700">Metric:</label>
780
+ <select x-model="cauchyHeatmapMetric" @change="renderCauchyHeatmap()"
781
+ class="rounded-lg border-gray-300 shadow-sm text-sm p-2 border bg-white">
782
+ <option value="mlog10_p_cauchy">Cauchy P-Value</option>
783
+ <option value="mlog10_p_median">Median P-Value</option>
784
+ <option value="top_95_quantile">Q95 (-log10P)</option>
785
+ </select>
786
+ </div>
787
+ <div class="flex items-center gap-2">
788
+ <label class="inline-flex items-center cursor-pointer">
789
+ <input type="checkbox" x-model="cauchyHeatmapCluster"
790
+ @change="renderCauchyHeatmap()"
791
+ class="rounded border-gray-300 text-primary-600 focus:ring-primary-500 h-4 w-4">
792
+ <span class="ml-2 text-sm text-gray-700">Cluster</span>
793
+ </label>
794
+ </div>
795
+ <div class="flex items-center gap-2">
796
+ <label class="inline-flex items-center cursor-pointer">
797
+ <input type="checkbox" x-model="cauchyHeatmapNormalize"
798
+ @change="renderCauchyHeatmap()"
799
+ class="rounded border-gray-300 text-primary-600 focus:ring-primary-500 h-4 w-4">
800
+ <span class="ml-2 text-sm text-gray-700">0-1 Normalize</span>
801
+ </label>
802
+ </div>
803
+ </div>
804
+ </div>
805
+ <div id="cauchyHeatmap" class="plot-container w-full"></div>
806
+ </div>
807
+ </div>
808
+ </div>
809
+
810
+ <!-- ==================== SECTION 8: REPORT INFORMATION ==================== -->
811
+ <div id="sec-info" x-show="dataLoaded && currentSections.showInfo"
812
+ class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
813
+ <div class="p-4 border-b border-gray-100 flex justify-between items-center cursor-pointer"
814
+ @click="sections.info = !sections.info">
815
+ <div>
816
+ <h2 class="text-lg font-semibold text-gray-800" x-text="getSectionTitle('sec-info')"></h2>
817
+ <p class="text-sm text-gray-500">Configuration and summary statistics.</p>
818
+ </div>
819
+ <svg class="w-5 h-5 text-gray-500 transition-transform" :class="sections.info ? 'rotate-180' : ''"
820
+ fill="none" stroke="currentColor" viewBox="0 0 24 24">
821
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
822
+ </svg>
823
+ </div>
824
+ <div class="section-content" :class="sections.info ? 'expanded' : 'collapsed'">
825
+ <!-- Summary Cards -->
826
+ <div class="p-4 grid grid-cols-1 md:grid-cols-3 gap-4">
827
+ <div class="text-center p-4 bg-primary-50 rounded-lg">
828
+ <p class="text-2xl font-bold text-primary-600" x-text="traits.length"></p>
829
+ <p class="text-sm text-gray-600">Traits</p>
830
+ </div>
831
+ <div class="text-center p-4 bg-green-50 rounded-lg">
832
+ <p class="text-2xl font-bold text-green-600" x-text="samples.length"></p>
833
+ <p class="text-sm text-gray-600">Samples</p>
834
+ </div>
835
+ <div class="text-center p-4 bg-purple-50 rounded-lg">
836
+ <p class="text-2xl font-bold text-purple-600" x-text="annotations.length"></p>
837
+ <p class="text-sm text-gray-600">Annotation Categories</p>
838
+ </div>
839
+ </div>
840
+ <!-- Config JSON -->
841
+ <div class="border-t border-gray-200 p-4 bg-gray-50">
842
+ <h3 class="font-medium text-gray-700 mb-2">Configuration</h3>
843
+ <pre class="text-xs text-gray-600 whitespace-pre-wrap font-mono bg-white p-3 rounded border max-h-64 overflow-auto"
844
+ x-text="JSON.stringify(reportMeta, null, 2)"></pre>
845
+ </div>
846
+ </div>
847
+ </div>
848
+
849
+ </main>
850
+ </div><!-- End of flex layout -->
851
+
852
+ <!-- Footer -->
853
+ <footer class="bg-white border-t border-gray-200">
854
+ <div class="max-w-full mx-auto px-4 py-3">
855
+ <p class="text-center text-sm text-gray-500">gsMap Report | Version {{ gsmap_version }}</p>
856
+ </div>
857
+ </footer>
858
+
859
+ <!-- Data Scripts - MUST load before Alpine initializes -->
860
+ <script src="js_data/report_meta.js"></script>
861
+ <script src="js_data/sample_index.js"></script>
862
+ <script src="js_data/cauchy_results.js"></script>
863
+ <!-- Gene trait correlation data is now loaded per-trait on demand from js_data/gss_stats/ -->
864
+ <script src="js_data/umap_data.js"></script>
865
+
866
+ <script>
867
+ function gsMapApp() {
868
+ return {
869
+ // Data loaded flag
870
+ dataLoaded: false,
871
+ plotlyReady: false,
872
+
873
+ // Global state
874
+ selectedTrait: '',
875
+ selectedAnnotationCategory: '',
876
+ selectedGene: '',
877
+ isLoading: false,
878
+
879
+ // Data arrays (populated from window globals)
880
+ traits: [],
881
+ samples: [],
882
+ annotations: [],
883
+ topCorrGenes: 50,
884
+ chromTickPositions: {},
885
+
886
+ // Section collapse state
887
+ sections: {
888
+ manhattan: true,
889
+ umap: true,
890
+ spatial3d: true,
891
+ gsmap: true,
892
+ annotation: true,
893
+ genes: true,
894
+ cauchy: true,
895
+ info: false
896
+ },
897
+
898
+ // Raw data
899
+ reportMeta: {},
900
+ cauchyData: [],
901
+ topGenesPccCache: {}, // Per-trait cache: { traitName: [geneData] }
902
+ topGenesPccLoadedTraits: {}, // Track loaded traits
903
+ topGenesList: [],
904
+
905
+ // UMAP state
906
+ umapData: null,
907
+ umapHasNiche: false,
908
+ umapAnnotationColumns: [],
909
+ selectedUmapAnnotation: '',
910
+ umapCellPlotTraitInitialized: false,
911
+ umapCellPlotAnnotationInitialized: false,
912
+ umapNichePlotTraitInitialized: false,
913
+ umapNichePlotAnnotationInitialized: false,
914
+
915
+ // 3D visualization state
916
+ is3d: false,
917
+ has3dWidget: false,
918
+ spatial3dTraitLoading: true,
919
+ spatial3dAnnotationLoading: true,
920
+
921
+ // Spatial 2D visualization state
922
+ selectedSample: '',
923
+ spatial2dPlotTraitInitialized: false,
924
+ spatial2dPlotAnnotationInitialized: false,
925
+
926
+ // Per-sample data loading (for efficient on-demand loading)
927
+ sampleDataCache: {}, // { sampleName: data } - cache loaded samples
928
+ loadingSampleData: false, // Loading indicator
929
+ loadingSpatial2dTraitData: false, // Loading indicator for trait switch on 2D plot
930
+ currentSampleData: null, // Currently loaded sample data
931
+ loadingGeneData: false, // Loading indicator for gene PCC data
932
+
933
+ // Section visibility configuration by dataset type
934
+ sectionConfig: {
935
+ 'spatial3D': {
936
+ show3d: true,
937
+ show2dDistribution: true,
938
+ showGSS: true,
939
+ showAnnotations: true,
940
+ showUMAP: true,
941
+ showManhattan: true,
942
+ showEnrichment: true,
943
+ showInfo: true
944
+ },
945
+ 'spatial2D': {
946
+ show3d: false,
947
+ show2dDistribution: true,
948
+ showGSS: true,
949
+ showAnnotations: true,
950
+ showUMAP: true,
951
+ showManhattan: true,
952
+ showEnrichment: true,
953
+ showInfo: true
954
+ },
955
+ 'scRNA': {
956
+ show3d: false,
957
+ show2dDistribution: false,
958
+ showGSS: false,
959
+ showAnnotations: false,
960
+ showUMAP: true,
961
+ showManhattan: true,
962
+ showEnrichment: true,
963
+ showInfo: true
964
+ }
965
+ },
966
+
967
+ // Section definitions for dynamic numbering
968
+ sectionDefs: [
969
+ { key: 'show3d', id: 'sec-spatial3d', title: 'gsMap 3D Exploration' },
970
+ { key: 'show2dDistribution', id: 'sec-gsmap', title: 'gsMap 2D Distribution' },
971
+ { key: 'showGSS', id: 'sec-genes', title: 'GSS Distribution' },
972
+ // Annotation section merged into sec-gsmap, removed from navigation
973
+ // { key: 'showAnnotations', id: 'sec-annotation', title: 'Annotations 2D Distribution' },
974
+ { key: 'showUMAP', id: 'sec-umap', title: 'Embedding UMAP' },
975
+ { key: 'showManhattan', id: 'sec-manhattan', title: 'Manhattan Plot' },
976
+ { key: 'showEnrichment', id: 'sec-cauchy', title: 'Enrichment Analysis' },
977
+ { key: 'showInfo', id: 'sec-info', title: 'Report Information' }
978
+ ],
979
+
980
+ // Computed: current section visibility based on dataset type
981
+ get currentSections() {
982
+ const datasetType = this.reportMeta?.dataset_type || 'spatial3D';
983
+ return this.sectionConfig[datasetType] || this.sectionConfig['spatial3D'];
984
+ },
985
+
986
+ // Computed: visible sections with dynamic numbering
987
+ get visibleSections() {
988
+ const config = this.currentSections;
989
+ const visible = [];
990
+ let num = 1;
991
+ for (const s of this.sectionDefs) {
992
+ if (config[s.key]) {
993
+ visible.push({ ...s, number: num++ });
994
+ }
995
+ }
996
+ return visible;
997
+ },
998
+
999
+ // Helper method to get section title with number
1000
+ getSectionTitle(sectionId) {
1001
+ const section = this.visibleSections.find(s => s.id === sectionId);
1002
+ return section ? `${section.number}. ${section.title}` : '';
1003
+ },
1004
+
1005
+ // Helper to check if a section should be shown
1006
+ isSectionVisible(sectionKey) {
1007
+ return this.currentSections[sectionKey] || false;
1008
+ },
1009
+
1010
+ // Computed property for 3D trait path
1011
+ get spatial3dTraitPath() {
1012
+ if (!this.has3dWidget) return '';
1013
+ const name = this.selectedTrait;
1014
+ if (!name) return '';
1015
+ const safeName = name.replace(/[^a-zA-Z0-9]/g, '_');
1016
+ return `spatial_3d/spatial_3d_trait_${safeName}.html`;
1017
+ },
1018
+
1019
+ // Computed property for 3D annotation path
1020
+ get spatial3dAnnotationPath() {
1021
+ if (!this.has3dWidget) return '';
1022
+ const name = this.selectedAnnotationCategory;
1023
+ if (!name) return '';
1024
+ const safeName = name.replace(/[^a-zA-Z0-9]/g, '_');
1025
+ return `spatial_3d/spatial_3d_anno_${safeName}.html`;
1026
+ },
1027
+
1028
+ // Manhattan state
1029
+ manhattanLoadedTraits: {},
1030
+ manhattanData: null,
1031
+ manhattanPlotInitialized: false,
1032
+ manhattanTraceCache: null,
1033
+ chrColors: ['#1f77b4', '#aec7e8'],
1034
+
1035
+ // Gene table state
1036
+ geneSearchQuery: '',
1037
+ geneSortKey: 'PCC',
1038
+ geneSortAsc: false,
1039
+
1040
+ // Memoization caches
1041
+ _geneFilterCache: null,
1042
+ _geneFilterKey: '',
1043
+ _cauchyFilterCache: null,
1044
+ _cauchyFilterKey: '',
1045
+
1046
+ // Performance optimization caches
1047
+ _umapTraitColorCache: null, // { trait, colors }
1048
+ _umapAnnoColorCache: null, // { anno, traces } for discrete annotations
1049
+ _spatial2dTraitColorCache: null, // { sample, trait, colors }
1050
+ _spatial2dAnnoTracesCache: null, // { sample, anno, traces }
1051
+ _traitChangeDebounceTimer: null, // Debounce timer for trait changes
1052
+
1053
+ // Virtual scrolling state
1054
+ geneTableScroll: 0,
1055
+ geneTableRowHeight: 40,
1056
+ geneTableVisibleCount: 12,
1057
+
1058
+ // Cauchy state
1059
+ cauchyViewMode: 'all',
1060
+ cauchySortKey: 'mlog10_p_median',
1061
+ cauchySortAsc: false,
1062
+ cauchyHeatmapMetric: 'mlog10_p_cauchy',
1063
+ cauchyHeatmapCluster: true,
1064
+ cauchyHeatmapNormalize: false,
1065
+
1066
+ // Memoized: filtered gene table
1067
+ get filteredGeneTable() {
1068
+ const key = `${this.selectedTrait}|${this.geneSearchQuery}|${this.geneSortKey}|${this.geneSortAsc}`;
1069
+ if (this._geneFilterKey === key && this._geneFilterCache) {
1070
+ return this._geneFilterCache;
1071
+ }
1072
+
1073
+ let data = this.topGenesList || [];
1074
+ if (this.geneSearchQuery) {
1075
+ const q = this.geneSearchQuery.toLowerCase();
1076
+ data = data.filter(r => r.gene.toLowerCase().includes(q));
1077
+ }
1078
+ const sortKey = this.geneSortKey, asc = this.geneSortAsc;
1079
+ const result = [...data].sort((a, b) => {
1080
+ let va = a[sortKey], vb = b[sortKey];
1081
+ if (va == null) return 1;
1082
+ if (vb == null) return -1;
1083
+ if (typeof va === 'string') { va = va.toLowerCase(); vb = (vb || '').toLowerCase(); }
1084
+ if (va < vb) return asc ? -1 : 1;
1085
+ if (va > vb) return asc ? 1 : -1;
1086
+ return 0;
1087
+ });
1088
+
1089
+ this._geneFilterKey = key;
1090
+ this._geneFilterCache = result;
1091
+ return result;
1092
+ },
1093
+
1094
+ // Virtual scrolling for gene table
1095
+ get virtualGeneTable() {
1096
+ const allData = this.filteredGeneTable;
1097
+ const start = Math.floor(this.geneTableScroll / this.geneTableRowHeight);
1098
+ const buffer = 5;
1099
+ const startIdx = Math.max(0, start - buffer);
1100
+ const endIdx = Math.min(allData.length, start + this.geneTableVisibleCount + buffer * 2);
1101
+
1102
+ return {
1103
+ items: allData.slice(startIdx, endIdx),
1104
+ totalHeight: allData.length * this.geneTableRowHeight,
1105
+ offsetY: startIdx * this.geneTableRowHeight,
1106
+ totalCount: allData.length
1107
+ };
1108
+ },
1109
+
1110
+ // Dropdown list: only top N genes (for sidebar selector)
1111
+ get topGenesDropdown() {
1112
+ return (this.topGenesList || []).filter(g => g.isTop);
1113
+ },
1114
+
1115
+ // Memoized: filtered cauchy table
1116
+ get filteredCauchyTable() {
1117
+ const key = `${this.selectedTrait}|${this.selectedAnnotationCategory}|${this.cauchyViewMode}|${this.cauchySortKey}|${this.cauchySortAsc}`;
1118
+ if (this._cauchyFilterKey === key && this._cauchyFilterCache) {
1119
+ return this._cauchyFilterCache;
1120
+ }
1121
+
1122
+ let data = this.cauchyData || [];
1123
+ data = data.filter(r =>
1124
+ (this.cauchyViewMode === 'all' ? r.type === 'aggregated' : r.type === 'sample') &&
1125
+ r.trait === this.selectedTrait &&
1126
+ r.annotation_name === this.selectedAnnotationCategory
1127
+ );
1128
+ const sortKey = this.cauchySortKey, asc = this.cauchySortAsc;
1129
+ const result = [...data].sort((a, b) => {
1130
+ let va = a[sortKey], vb = b[sortKey];
1131
+ if (va == null) return 1;
1132
+ if (vb == null) return -1;
1133
+ if (typeof va === 'string') { va = (va || '').toLowerCase(); vb = (vb || '').toLowerCase(); }
1134
+ if (va < vb) return asc ? -1 : 1;
1135
+ if (va > vb) return asc ? 1 : -1;
1136
+ return 0;
1137
+ });
1138
+
1139
+ this._cauchyFilterKey = key;
1140
+ this._cauchyFilterCache = result;
1141
+ return result;
1142
+ },
1143
+
1144
+ // Wait for Plotly to load (deferred)
1145
+ waitForPlotly() {
1146
+ return new Promise(resolve => {
1147
+ if (typeof Plotly !== 'undefined') {
1148
+ this.plotlyReady = true;
1149
+ return resolve();
1150
+ }
1151
+ const check = setInterval(() => {
1152
+ if (typeof Plotly !== 'undefined') {
1153
+ this.plotlyReady = true;
1154
+ clearInterval(check);
1155
+ resolve();
1156
+ }
1157
+ }, 50);
1158
+ });
1159
+ },
1160
+
1161
+ // Initialize
1162
+ async init() {
1163
+ console.log("Initializing gsMap Report...");
1164
+
1165
+ // Load data from window globals
1166
+ if (window.GSMAP_REPORT_META) {
1167
+ this.reportMeta = window.GSMAP_REPORT_META;
1168
+ this.traits = this.reportMeta.traits || [];
1169
+ this.samples = this.reportMeta.samples || [];
1170
+ this.annotations = this.reportMeta.annotations || [];
1171
+ this.topCorrGenes = this.reportMeta.top_corr_genes || 50;
1172
+ this.chromTickPositions = this.reportMeta.chrom_tick_positions || {};
1173
+ console.log("Loaded report meta:", this.traits.length, "traits,", this.samples.length, "samples");
1174
+ } else {
1175
+ console.warn("GSMAP_REPORT_META not found!");
1176
+ }
1177
+
1178
+ if (window.GSMAP_CAUCHY) {
1179
+ this.cauchyData = window.GSMAP_CAUCHY;
1180
+ console.log("Loaded", this.cauchyData.length, "Cauchy results");
1181
+ }
1182
+
1183
+ // Gene trait correlation data is now loaded per-trait on demand
1184
+ // (see loadGenePccData method)
1185
+
1186
+ // Load UMAP data
1187
+ if (window.GSMAP_UMAP) {
1188
+ this.umapData = window.GSMAP_UMAP;
1189
+ this.umapHasNiche = this.umapData.has_niche || false;
1190
+ this.umapAnnotationColumns = this.umapData.annotation_columns || [];
1191
+ if (this.umapAnnotationColumns.length > 0) {
1192
+ this.selectedUmapAnnotation = this.umapAnnotationColumns[0];
1193
+ }
1194
+ console.log("Loaded UMAP data:", this.umapData.spot.length, "points, has_niche:", this.umapHasNiche);
1195
+ }
1196
+
1197
+ // Load 3D visualization state
1198
+ this.is3d = this.reportMeta.is_3d || false;
1199
+ this.has3dWidget = this.reportMeta.has_3d_widget || false;
1200
+ if (this.has3dWidget) {
1201
+ console.log("3D visualization widget available");
1202
+ }
1203
+
1204
+ // Check if data loaded successfully
1205
+ if (this.traits.length > 0) {
1206
+ // Initialize selections
1207
+ this.selectedTrait = this.traits[0];
1208
+ if (this.annotations.length > 0) {
1209
+ this.selectedAnnotationCategory = this.annotations[0];
1210
+ }
1211
+ if (this.samples.length > 0) {
1212
+ this.selectedSample = this.samples[0];
1213
+ }
1214
+
1215
+ // Update gene list (async - loads per-trait data)
1216
+ // Must complete before dataLoaded=true to ensure selectedGene is set
1217
+ await this.updateGeneList();
1218
+
1219
+ // Now safe to show the UI - all selections are initialized
1220
+ this.dataLoaded = true;
1221
+
1222
+ // Wait for Plotly to load, then render plots
1223
+ await this.waitForPlotly();
1224
+ this.loadManhattanData();
1225
+ this.renderCauchyHeatmap();
1226
+ this.renderUmapPlots();
1227
+
1228
+ // Load first sample data then render spatial 2D plot
1229
+ if (this.selectedSample) {
1230
+ await this.loadSampleData(this.selectedSample);
1231
+ this.renderSpatial2DPlot();
1232
+ // Static gene plots load automatically via img src binding
1233
+ }
1234
+ } else {
1235
+ console.error("No traits found - data may not have loaded correctly");
1236
+ }
1237
+ },
1238
+
1239
+ // Event handlers
1240
+ // Handle trait selection - debounced to prevent rapid re-renders
1241
+ handleTraitSelect(newTrait) {
1242
+ if (newTrait === this.selectedTrait) return;
1243
+
1244
+ // Clear any pending debounce
1245
+ if (this._traitChangeDebounceTimer) {
1246
+ clearTimeout(this._traitChangeDebounceTimer);
1247
+ }
1248
+
1249
+ // Immediate UI feedback
1250
+ this.loadingGeneData = true;
1251
+ this.selectedGene = '';
1252
+ this.topGenesList = [];
1253
+ this._geneFilterCache = null;
1254
+ this._geneFilterKey = '';
1255
+ this.selectedTrait = newTrait;
1256
+
1257
+ // Show loading overlay for 2D trait plot
1258
+ this.loadingSpatial2dTraitData = true;
1259
+
1260
+ // Invalidate trait-specific caches
1261
+ this._umapTraitColorCache = null;
1262
+ this._spatial2dTraitColorCache = null;
1263
+
1264
+ // Debounce the heavy operations (50ms)
1265
+ this._traitChangeDebounceTimer = setTimeout(() => {
1266
+ this.onTraitChange();
1267
+ }, 50);
1268
+ },
1269
+
1270
+ async onTraitChange() {
1271
+ // Invalidate caches
1272
+ this._geneFilterCache = null;
1273
+ this._cauchyFilterCache = null;
1274
+ this.manhattanTraceCache = null;
1275
+
1276
+ // Load gene data first (required for gene selection)
1277
+ await this.updateGeneList();
1278
+ this.loadingGeneData = false;
1279
+
1280
+ // Update 3D visualization - trait view needs reload
1281
+ this.spatial3dTraitLoading = true;
1282
+
1283
+ // Parallel render operations using requestAnimationFrame for smooth UI
1284
+ // This prevents blocking the main thread
1285
+ requestAnimationFrame(() => {
1286
+ // Load Manhattan data (async, doesn't block)
1287
+ this.loadManhattanData();
1288
+
1289
+ // Render plots in parallel - only update trait-related views
1290
+ this.renderUmapPlotTrait();
1291
+ this.renderSpatial2DPlotSingle('spatial2dPlotTrait', 'trait');
1292
+
1293
+ // Clear loading state after render
1294
+ this.loadingSpatial2dTraitData = false;
1295
+ });
1296
+ },
1297
+
1298
+ onAnnotationChange() {
1299
+ this._cauchyFilterCache = null;
1300
+
1301
+ // Invalidate annotation-specific caches
1302
+ this._umapAnnoColorCache = null;
1303
+ this._spatial2dAnnoTracesCache = null;
1304
+
1305
+ // Update 3D visualization - annotation view needs reload
1306
+ this.spatial3dAnnotationLoading = true;
1307
+
1308
+ // Parallel render - only update annotation-related views
1309
+ requestAnimationFrame(() => {
1310
+ this.renderUmapPlotAnnotation();
1311
+ this.renderSpatial2DPlotSingle('spatial2dPlotAnnotation', 'annotation');
1312
+ this.renderCauchyHeatmap();
1313
+ });
1314
+ },
1315
+
1316
+ onSampleChange() {
1317
+ // Invalidate sample-specific caches
1318
+ this._spatial2dTraitColorCache = null;
1319
+ this._spatial2dAnnoTracesCache = null;
1320
+
1321
+ // Load sample data on-demand then render
1322
+ this.loadSampleData(this.selectedSample).then(() => {
1323
+ this.renderSpatial2DPlot();
1324
+ }).catch(err => {
1325
+ console.error('Failed to load sample data:', err);
1326
+ });
1327
+ // Static gene plots update automatically via img src binding
1328
+ },
1329
+
1330
+ // Load sample data on-demand via dynamic script injection
1331
+ async loadSampleData(sampleName) {
1332
+ // Return cached data if available
1333
+ if (this.sampleDataCache[sampleName]) {
1334
+ this.currentSampleData = this.sampleDataCache[sampleName];
1335
+ return this.currentSampleData;
1336
+ }
1337
+
1338
+ // Get sample info from index
1339
+ const sampleInfo = window.GSMAP_SAMPLE_INDEX?.[sampleName];
1340
+ if (!sampleInfo) {
1341
+ console.warn(`Sample ${sampleName} not found in index`);
1342
+ return null;
1343
+ }
1344
+
1345
+ this.loadingSampleData = true;
1346
+
1347
+ return new Promise((resolve, reject) => {
1348
+ const script = document.createElement('script');
1349
+ script.src = `js_data/${sampleInfo.file}`;
1350
+
1351
+ script.onload = () => {
1352
+ const data = window[sampleInfo.var_name];
1353
+ if (data) {
1354
+ this.sampleDataCache[sampleName] = data;
1355
+ this.currentSampleData = data;
1356
+ // Show downsampling info if applicable
1357
+ const nSpots = data.spot?.length || 0;
1358
+ const nSpotsOriginal = sampleInfo.n_spots_original || nSpots;
1359
+ if (nSpotsOriginal > nSpots) {
1360
+ console.log(`Loaded sample ${sampleName}: ${nSpots.toLocaleString()} spots (downsampled from ${nSpotsOriginal.toLocaleString()})`);
1361
+ } else {
1362
+ console.log(`Loaded sample ${sampleName}: ${nSpots.toLocaleString()} spots`);
1363
+ }
1364
+ resolve(data);
1365
+ } else {
1366
+ reject(new Error(`Data not found for ${sampleName}`));
1367
+ }
1368
+ this.loadingSampleData = false;
1369
+ };
1370
+
1371
+ script.onerror = () => {
1372
+ console.error(`Failed to load sample data: ${sampleName}`);
1373
+ this.loadingSampleData = false;
1374
+ reject(new Error(`Failed to load ${sampleName}`));
1375
+ };
1376
+
1377
+ document.body.appendChild(script);
1378
+ });
1379
+ },
1380
+
1381
+ onGeneChange() {
1382
+ if (this.manhattanData && this.plotlyReady) {
1383
+ this.renderManhattanPlot();
1384
+ }
1385
+ // Static gene plots update automatically via img src binding
1386
+ },
1387
+
1388
+ // Load gene PCC data for a specific trait on demand
1389
+ async loadGenePccData(trait) {
1390
+ // Return cached data if available
1391
+ if (this.topGenesPccCache[trait]) {
1392
+ return this.topGenesPccCache[trait];
1393
+ }
1394
+
1395
+ const safeTrait = trait.replace(/[^a-zA-Z0-9]/g, '_');
1396
+ const varName = `GSMAP_GENE_TRAIT_CORRELATION_${safeTrait}`;
1397
+
1398
+ // Check if already loaded in window
1399
+ if (this.topGenesPccLoadedTraits[trait] && window[varName]) {
1400
+ this.topGenesPccCache[trait] = window[varName];
1401
+ return this.topGenesPccCache[trait];
1402
+ }
1403
+
1404
+ // Load script dynamically from gss_stats subfolder
1405
+ return new Promise((resolve, reject) => {
1406
+ const script = document.createElement('script');
1407
+ script.src = `js_data/gss_stats/gene_trait_correlation_${trait}.js`;
1408
+
1409
+ script.onload = () => {
1410
+ this.topGenesPccLoadedTraits[trait] = true;
1411
+ if (window[varName]) {
1412
+ this.topGenesPccCache[trait] = window[varName];
1413
+ console.log(`Loaded gene PCC data for ${trait}:`, window[varName].length, 'genes');
1414
+ resolve(this.topGenesPccCache[trait]);
1415
+ } else {
1416
+ console.warn(`Gene PCC variable ${varName} not found after loading script`);
1417
+ resolve([]);
1418
+ }
1419
+ };
1420
+
1421
+ script.onerror = () => {
1422
+ console.warn(`Failed to load gene PCC data for ${trait}`);
1423
+ resolve([]);
1424
+ };
1425
+
1426
+ document.body.appendChild(script);
1427
+ });
1428
+ },
1429
+
1430
+ // Update gene list when trait changes
1431
+ async updateGeneList() {
1432
+ const trait = this.selectedTrait;
1433
+ if (!trait) {
1434
+ this.topGenesList = [];
1435
+ return;
1436
+ }
1437
+
1438
+ // Load trait-specific gene PCC data
1439
+ const traitGeneData = await this.loadGenePccData(trait);
1440
+
1441
+ if (!traitGeneData || traitGeneData.length === 0) {
1442
+ this.topGenesList = [];
1443
+ return;
1444
+ }
1445
+
1446
+ // Data is already for this specific trait, sort by PCC
1447
+ let traitGenes = [...traitGeneData].sort((a, b) => (b.PCC || 0) - (a.PCC || 0));
1448
+
1449
+ // Mark top genes
1450
+ traitGenes.forEach((g, i) => {
1451
+ g.isTop = i < this.topCorrGenes;
1452
+ });
1453
+
1454
+ // Load ALL genes, not just top slice
1455
+ this.topGenesList = traitGenes;
1456
+
1457
+ // Invalidate gene table cache since data changed
1458
+ this._geneFilterCache = null;
1459
+ this._geneFilterKey = '';
1460
+
1461
+ // Select first top gene (matches what's shown in dropdown, has diagnostic plots)
1462
+ const firstTopGene = this.topGenesList.find(g => g.isTop);
1463
+ if (firstTopGene) {
1464
+ this.selectedGene = firstTopGene.gene;
1465
+ } else if (this.topGenesList.length > 0) {
1466
+ // Fallback to first gene if no top genes marked
1467
+ this.selectedGene = this.topGenesList[0].gene;
1468
+ }
1469
+ },
1470
+
1471
+ // Gene plot path (single-sample, takes plot type as parameter)
1472
+ getGenePlotPath(plotType) {
1473
+ // Don't return a path during trait transitions to prevent bad requests
1474
+ if (this.loadingGeneData || !this.selectedGene || !this.selectedTrait || !this.selectedSample) {
1475
+ return '';
1476
+ }
1477
+ const safeSample = this.selectedSample.replace(/[^a-zA-Z0-9]/g, '_');
1478
+ return `gene_diagnostic_plots/gene_${this.selectedTrait}_${this.selectedGene}_${safeSample}_${plotType}.png`;
1479
+ },
1480
+
1481
+ // Sorting
1482
+ sortGenesBy(key) {
1483
+ if (this.geneSortKey === key) this.geneSortAsc = !this.geneSortAsc;
1484
+ else { this.geneSortKey = key; this.geneSortAsc = key === 'gene'; }
1485
+ },
1486
+ sortCauchyBy(key) {
1487
+ if (this.cauchySortKey === key) this.cauchySortAsc = !this.cauchySortAsc;
1488
+ else {
1489
+ this.cauchySortKey = key;
1490
+ this.cauchySortAsc = ['trait', 'annotation', 'sample_name'].includes(key);
1491
+ }
1492
+ },
1493
+
1494
+ // Manhattan Plot
1495
+ loadManhattanData() {
1496
+ const trait = this.selectedTrait;
1497
+ if (!trait) return;
1498
+
1499
+ this.isLoading = true;
1500
+ const safeTrait = trait.replace(/[^a-zA-Z0-9]/g, "_");
1501
+ const varName = `GSMAP_MANHATTAN_${safeTrait}`;
1502
+
1503
+ // Check if already loaded
1504
+ if (this.manhattanLoadedTraits[trait] && window[varName]) {
1505
+ this.manhattanData = window[varName];
1506
+ this.renderManhattanPlot();
1507
+ this.isLoading = false;
1508
+ return;
1509
+ }
1510
+
1511
+ // Load script dynamically
1512
+ const script = document.createElement('script');
1513
+ script.src = `js_data/manhattan_${trait}.js`;
1514
+ script.onload = () => {
1515
+ this.manhattanLoadedTraits[trait] = true;
1516
+ if (window[varName]) {
1517
+ this.manhattanData = window[varName];
1518
+ this.renderManhattanPlot();
1519
+ }
1520
+ this.isLoading = false;
1521
+ };
1522
+ script.onerror = () => {
1523
+ console.error(`Failed to load Manhattan data for ${trait}`);
1524
+ this.isLoading = false;
1525
+ };
1526
+ document.body.appendChild(script);
1527
+ },
1528
+
1529
+ renderManhattanPlot() {
1530
+ const data = this.manhattanData;
1531
+ if (!data || !data.x || data.x.length === 0) return;
1532
+
1533
+ const plotDiv = document.getElementById('manhattanPlot');
1534
+ if (!plotDiv) return;
1535
+
1536
+ const trait = this.selectedTrait;
1537
+ const highlightGene = this.selectedGene ? this.selectedGene.toUpperCase() : '';
1538
+
1539
+ // Build or reuse cached base traces (regular + top PCC SNPs)
1540
+ if (!this.manhattanTraceCache) {
1541
+ const regularX = [], regularY = [], regularColors = [];
1542
+ const topX = [], topY = [], topGenes = [], topSnps = [];
1543
+
1544
+ // Single pass through data to separate regular and top SNPs
1545
+ for (let i = 0; i < data.x.length; i++) {
1546
+ if (data.is_top && data.is_top[i] === 1) {
1547
+ topX.push(data.x[i]);
1548
+ topY.push(data.y[i]);
1549
+ topGenes.push(data.gene[i] || '');
1550
+ topSnps.push(data.snp[i] || '');
1551
+ } else {
1552
+ regularX.push(data.x[i]);
1553
+ regularY.push(data.y[i]);
1554
+ regularColors.push(this.chrColors[data.chr[i] % 2]);
1555
+ }
1556
+ }
1557
+
1558
+ // Calculate maxX
1559
+ let maxX = 0;
1560
+ for (let i = 0; i < data.x.length; i++) {
1561
+ if (data.x[i] > maxX) maxX = data.x[i];
1562
+ }
1563
+
1564
+ this.manhattanTraceCache = {
1565
+ regularX, regularY, regularColors,
1566
+ topX, topY, topGenes, topSnps,
1567
+ maxX
1568
+ };
1569
+ }
1570
+
1571
+ const cache = this.manhattanTraceCache;
1572
+ const traces = [];
1573
+
1574
+ // Regular SNPs - hover disabled for performance
1575
+ if (cache.regularX.length > 0) {
1576
+ traces.push({
1577
+ x: cache.regularX,
1578
+ y: cache.regularY,
1579
+ mode: 'markers',
1580
+ type: 'scattergl',
1581
+ hoverinfo: 'skip', // Disable hover for performance
1582
+ marker: { size: 3, color: cache.regularColors, opacity: 0.6 },
1583
+ name: 'SNPs'
1584
+ });
1585
+ }
1586
+
1587
+ // Top PCC genes (red) - keep hover
1588
+ if (cache.topX.length > 0) {
1589
+ traces.push({
1590
+ x: cache.topX,
1591
+ y: cache.topY,
1592
+ mode: 'markers',
1593
+ type: 'scattergl',
1594
+ text: cache.topGenes.map((g, i) => `<b>${g}</b><br>SNP: ${cache.topSnps[i]}<br>-log10(p): ${cache.topY[i].toFixed(2)}`),
1595
+ hoverinfo: 'text',
1596
+ marker: { size: 6, color: '#e53e3e', opacity: 0.85 },
1597
+ name: 'Top PCC Genes'
1598
+ });
1599
+ }
1600
+
1601
+ // Highlighted gene (green diamond) - always rebuild this trace
1602
+ if (highlightGene) {
1603
+ const highlightX = [], highlightY = [], highlightText = [];
1604
+ for (let i = 0; i < data.x.length; i++) {
1605
+ if ((data.gene[i] || '').toUpperCase() === highlightGene) {
1606
+ highlightX.push(data.x[i]);
1607
+ highlightY.push(data.y[i]);
1608
+ highlightText.push(`<b>${data.gene[i]}</b><br>SNP: ${data.snp[i] || ''}<br>-log10(p): ${data.y[i].toFixed(2)}`);
1609
+ }
1610
+ }
1611
+ if (highlightX.length > 0) {
1612
+ traces.push({
1613
+ x: highlightX,
1614
+ y: highlightY,
1615
+ mode: 'markers',
1616
+ type: 'scattergl',
1617
+ hoverinfo: 'text',
1618
+ hovertext: highlightText,
1619
+ marker: { size: 12, color: '#10b981', symbol: 'diamond', line: { width: 2, color: '#059669' } },
1620
+ name: 'Selected Gene'
1621
+ });
1622
+ }
1623
+ }
1624
+
1625
+ // Get chromosome tick positions
1626
+ const chromTicks = this.chromTickPositions[trait] || {};
1627
+ const tickvals = Object.values(chromTicks);
1628
+ const ticktext = Object.keys(chromTicks);
1629
+
1630
+ const layout = {
1631
+ title: { text: `Manhattan Plot: ${trait}`, font: { size: 16 } },
1632
+ xaxis: {
1633
+ title: 'Chromosome',
1634
+ showgrid: false,
1635
+ zeroline: false,
1636
+ tickvals: tickvals,
1637
+ ticktext: ticktext
1638
+ },
1639
+ yaxis: { title: '-log10(p)', showgrid: true, gridcolor: '#eee', zeroline: false },
1640
+ hovermode: 'closest',
1641
+ margin: { l: 60, r: 30, t: 50, b: 50 },
1642
+ legend: { orientation: 'h', y: -0.15, itemsizing: 'constant' },
1643
+ shapes: [{
1644
+ type: 'line',
1645
+ x0: 0,
1646
+ x1: cache.maxX,
1647
+ y0: -Math.log10(5e-8),
1648
+ y1: -Math.log10(5e-8),
1649
+ line: { color: '#e53e3e', width: 1, dash: 'dash' }
1650
+ }]
1651
+ };
1652
+
1653
+ const config = { responsive: true, displayModeBar: true };
1654
+
1655
+ // Use Plotly.react for efficient updates
1656
+ if (this.manhattanPlotInitialized) {
1657
+ Plotly.react(plotDiv, traces, layout, config);
1658
+ } else {
1659
+ Plotly.newPlot(plotDiv, traces, layout, config);
1660
+ this.manhattanPlotInitialized = true;
1661
+ }
1662
+ },
1663
+
1664
+ // Cauchy Heatmap
1665
+ renderCauchyHeatmap() {
1666
+ const plotDiv = document.getElementById('cauchyHeatmap');
1667
+ if (!plotDiv || !this.cauchyData || this.cauchyData.length === 0) return;
1668
+
1669
+ let data = this.cauchyData.filter(r =>
1670
+ (this.cauchyViewMode === 'all' ? r.type === 'aggregated' : r.type === 'sample') &&
1671
+ r.annotation_name === this.selectedAnnotationCategory
1672
+ );
1673
+ if (data.length === 0) {
1674
+ Plotly.purge(plotDiv);
1675
+ return;
1676
+ }
1677
+
1678
+ const metric = this.cauchyHeatmapMetric;
1679
+ const annotationValues = [...new Set(data.map(r => r.annotation))].sort();
1680
+ const traitValues = [...new Set(data.map(r => r.trait))].sort();
1681
+
1682
+ let zData = annotationValues.map(anno => traitValues.map(trait => {
1683
+ const match = data.find(r => r.annotation === anno && r.trait === trait);
1684
+ return match && match[metric] != null ? match[metric] : 0;
1685
+ }));
1686
+
1687
+ // Deep copy for hover labels
1688
+ let hoverZData = zData.map(row => [...row]);
1689
+
1690
+ // Normalize column-wise (trait-wise)
1691
+ if (this.cauchyHeatmapNormalize) {
1692
+ for (let j = 0; j < traitValues.length; j++) {
1693
+ const colValues = zData.map(row => row[j]);
1694
+ const minVal = Math.min(...colValues);
1695
+ const maxVal = Math.max(...colValues);
1696
+ const range = maxVal - minVal;
1697
+ for (let i = 0; i < annotationValues.length; i++) {
1698
+ if (range > 0) {
1699
+ zData[i][j] = (zData[i][j] - minVal) / range;
1700
+ } else {
1701
+ zData[i][j] = 0;
1702
+ }
1703
+ }
1704
+ }
1705
+ }
1706
+
1707
+ let finalAnnotations = annotationValues;
1708
+ let finalTraits = traitValues;
1709
+
1710
+ if (this.cauchyHeatmapCluster && traitValues.length > 1) {
1711
+ try {
1712
+ // Cluster rows (Annotations)
1713
+ const rowOrder = this.clusterData(zData);
1714
+ zData = rowOrder.map(i => zData[i]);
1715
+ hoverZData = rowOrder.map(i => hoverZData[i]);
1716
+ finalAnnotations = rowOrder.map(i => annotationValues[i]);
1717
+
1718
+ // Cluster columns (Traits)
1719
+ const transposed = traitValues.map((_, j) => zData.map(row => row[j]));
1720
+ const colOrder = this.clusterData(transposed);
1721
+ zData = zData.map(row => colOrder.map(j => row[j]));
1722
+ hoverZData = hoverZData.map(row => colOrder.map(j => row[j]));
1723
+ finalTraits = colOrder.map(j => traitValues[j]);
1724
+ } catch (e) {
1725
+ console.error("Clustering failed, using default order:", e);
1726
+ }
1727
+ }
1728
+
1729
+ const metricLabel = {
1730
+ 'mlog10_p_cauchy': '-log10(p_cauchy)',
1731
+ 'mlog10_p_median': '-log10(p_median)',
1732
+ 'top_95_quantile': 'Q95 (-log10P)'
1733
+ }[metric] || metric;
1734
+
1735
+ const colorscale = ['#313695', '#4575b4', '#74add1', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026'];
1736
+
1737
+ Plotly.react(plotDiv, [{
1738
+ z: zData,
1739
+ x: finalTraits,
1740
+ y: finalAnnotations,
1741
+ type: 'heatmap',
1742
+ colorscale: colorscale.map((c, i) => [i / (colorscale.length - 1), c]),
1743
+ reversescale: false,
1744
+ customdata: hoverZData,
1745
+ hovertemplate: `Annotation: %{y}<br>Trait: %{x}<br>${metricLabel}: %{customdata:.2f}<extra></extra>`
1746
+ }], {
1747
+ title: { text: `Cauchy Enrichment Heatmap: ${metricLabel}`, font: { size: 16 } },
1748
+ xaxis: { title: 'Trait', tickangle: 45, automargin: true },
1749
+ yaxis: { title: 'Annotation', automargin: true },
1750
+ margin: { l: 150, r: 50, t: 80, b: 150 }
1751
+ }, { responsive: true });
1752
+ },
1753
+
1754
+ // Simple Hierarchical Clustering (UPGMA / Average Linkage)
1755
+ clusterData(matrix) {
1756
+ if (matrix.length <= 1) return [0];
1757
+
1758
+ const n = matrix.length;
1759
+ const dist = (a, b) => Math.sqrt(a.reduce((acc, v, i) => acc + Math.pow(v - b[i], 2), 0));
1760
+
1761
+ // Initial clusters
1762
+ let clusters = matrix.map((v, i) => ({
1763
+ indices: [i],
1764
+ vec: v,
1765
+ size: 1
1766
+ }));
1767
+
1768
+ // Distance matrix between clusters
1769
+ const getClusterDist = (c1, c2) => dist(c1.vec, c2.vec);
1770
+
1771
+ while (clusters.length > 1) {
1772
+ let minDist = Infinity;
1773
+ let pair = [0, 1];
1774
+
1775
+ for (let i = 0; i < clusters.length; i++) {
1776
+ for (let j = i + 1; j < clusters.length; j++) {
1777
+ const d = getClusterDist(clusters[i], clusters[j]);
1778
+ if (d < minDist) {
1779
+ minDist = d;
1780
+ pair = [i, j];
1781
+ }
1782
+ }
1783
+ }
1784
+
1785
+ // Merge pair
1786
+ const [i, j] = pair;
1787
+ const c1 = clusters[i];
1788
+ const c2 = clusters[j];
1789
+
1790
+ const mergedVec = c1.vec.map((v, idx) => (v * c1.size + c2.vec[idx] * c2.size) / (c1.size + c2.size));
1791
+ const mergedCluster = {
1792
+ indices: [...c1.indices, ...c2.indices],
1793
+ vec: mergedVec,
1794
+ size: c1.size + c2.size
1795
+ };
1796
+
1797
+ // Update clusters array
1798
+ clusters.splice(j, 1);
1799
+ clusters.splice(i, 1, mergedCluster);
1800
+ }
1801
+
1802
+ return clusters[0].indices;
1803
+ },
1804
+
1805
+ // UMAP Plots - render both trait and annotation views
1806
+ renderUmapPlots() {
1807
+ if (!this.umapData || !this.plotlyReady) return;
1808
+ this.renderUmapPlotTrait();
1809
+ this.renderUmapPlotAnnotation();
1810
+ },
1811
+
1812
+ // Render only trait-colored UMAP plots (called on trait change)
1813
+ renderUmapPlotTrait() {
1814
+ if (!this.umapData || !this.plotlyReady) return;
1815
+
1816
+ const trait = this.selectedTrait;
1817
+ const traitColorMap = ['#313695', '#4575b4', '#74add1', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026'];
1818
+ const traitColorLabel = `-log10(p) ${trait}`;
1819
+
1820
+ // Get or compute trait color values
1821
+ let traitColorValues;
1822
+ if (trait && this.umapData[trait]) {
1823
+ traitColorValues = this.umapData[trait];
1824
+ } else {
1825
+ traitColorValues = new Array(this.umapData.spot.length).fill(0);
1826
+ }
1827
+
1828
+ // Build hover text for trait view (cached implicitly by browser)
1829
+ const anno = this.selectedAnnotationCategory;
1830
+ const traitHoverText = this.umapData.spot.map((spot, i) => {
1831
+ let text = `<b>Sample:</b> ${this.umapData.sample_name[i]}`;
1832
+ // Include annotation value
1833
+ if (anno && this.umapData[anno]) {
1834
+ const annoVal = this.umapData[anno][i];
1835
+ text += `<br><b>${anno}:</b> ${typeof annoVal === 'number' ? annoVal.toFixed(1) : annoVal}`;
1836
+ }
1837
+ // Include trait log10p value
1838
+ if (trait && this.umapData[trait]) {
1839
+ const traitVal = this.umapData[trait][i];
1840
+ text += `<br><b>-log10(p):</b> ${typeof traitVal === 'number' ? traitVal.toFixed(1) : traitVal}`;
1841
+ }
1842
+ return text;
1843
+ });
1844
+
1845
+ const cellPointSize = this.umapData.point_size_cell || 4;
1846
+ const nichePointSize = this.umapData.point_size_niche || 4;
1847
+
1848
+ // Render Cell Embedding - Trait
1849
+ this.renderSingleUmapContinuous(
1850
+ 'umapCellPlotTrait',
1851
+ this.umapData.umap_cell_x,
1852
+ this.umapData.umap_cell_y,
1853
+ traitColorValues,
1854
+ traitColorMap,
1855
+ traitHoverText,
1856
+ 'Cell Embedding',
1857
+ traitColorLabel,
1858
+ cellPointSize
1859
+ );
1860
+
1861
+ // Render Niche Embedding - Trait (if available)
1862
+ if (this.umapHasNiche && this.umapData.umap_niche_x) {
1863
+ this.renderSingleUmapContinuous(
1864
+ 'umapNichePlotTrait',
1865
+ this.umapData.umap_niche_x,
1866
+ this.umapData.umap_niche_y,
1867
+ traitColorValues,
1868
+ traitColorMap,
1869
+ traitHoverText,
1870
+ 'Niche Embedding',
1871
+ traitColorLabel,
1872
+ nichePointSize
1873
+ );
1874
+ }
1875
+ },
1876
+
1877
+ // Render only annotation-colored UMAP plots (called on annotation change)
1878
+ renderUmapPlotAnnotation() {
1879
+ if (!this.umapData || !this.plotlyReady) return;
1880
+
1881
+ const anno = this.selectedAnnotationCategory;
1882
+ if (!anno || !this.umapData[anno]) return;
1883
+
1884
+ const annoColorValues = this.umapData[anno];
1885
+ const annoColorMap = (this.umapData.color_maps && this.umapData.color_maps[anno]) || null;
1886
+
1887
+ // Build hover text for annotation view
1888
+ const annoHoverText = this.umapData.spot.map((spot, i) => {
1889
+ let text = `<b>Sample:</b> ${this.umapData.sample_name[i]}`;
1890
+ const val = this.umapData[anno][i];
1891
+ text += `<br><b>${anno}:</b> ${typeof val === 'number' ? val.toFixed(1) : val}`;
1892
+ return text;
1893
+ });
1894
+
1895
+ const cellPointSize = this.umapData.point_size_cell || 4;
1896
+ const nichePointSize = this.umapData.point_size_niche || 4;
1897
+
1898
+ // Render Cell Embedding - Annotation
1899
+ this.renderSingleUmapDiscrete(
1900
+ 'umapCellPlotAnnotation',
1901
+ this.umapData.umap_cell_x,
1902
+ this.umapData.umap_cell_y,
1903
+ annoColorValues,
1904
+ annoColorMap,
1905
+ annoHoverText,
1906
+ 'Cell Embedding',
1907
+ anno,
1908
+ cellPointSize
1909
+ );
1910
+
1911
+ // Render Niche Embedding - Annotation (if available)
1912
+ if (this.umapHasNiche && this.umapData.umap_niche_x) {
1913
+ this.renderSingleUmapDiscrete(
1914
+ 'umapNichePlotAnnotation',
1915
+ this.umapData.umap_niche_x,
1916
+ this.umapData.umap_niche_y,
1917
+ annoColorValues,
1918
+ annoColorMap,
1919
+ annoHoverText,
1920
+ 'Niche Embedding',
1921
+ anno,
1922
+ nichePointSize
1923
+ );
1924
+ }
1925
+ },
1926
+
1927
+ // Optimized: render continuous (trait) UMAP - single trace, fast update
1928
+ renderSingleUmapContinuous(divId, x, y, colorValues, colorMap, hoverText, title, colorLabel, pointSize) {
1929
+ const plotDiv = document.getElementById(divId);
1930
+ if (!plotDiv) return;
1931
+
1932
+ const opacity = this.umapData.default_opacity || 0.9;
1933
+
1934
+ const trace = {
1935
+ x: x,
1936
+ y: y,
1937
+ mode: 'markers',
1938
+ type: 'scattergl',
1939
+ text: hoverText,
1940
+ hovertemplate: '%{text}<extra></extra>',
1941
+ marker: {
1942
+ size: pointSize,
1943
+ color: colorValues,
1944
+ colorscale: colorMap.map((c, i) => [i / (colorMap.length - 1), c]),
1945
+ opacity: opacity,
1946
+ showscale: true,
1947
+ colorbar: {
1948
+ title: colorLabel,
1949
+ thickness: 15,
1950
+ len: 0.5,
1951
+ y: 0.5
1952
+ }
1953
+ }
1954
+ };
1955
+
1956
+ const layout = {
1957
+ title: { text: title, font: { size: 14 } },
1958
+ xaxis: { title: 'UMAP 1', showgrid: false, zeroline: false },
1959
+ yaxis: { title: 'UMAP 2', showgrid: false, zeroline: false },
1960
+ hovermode: 'closest',
1961
+ margin: { l: 50, r: 20, t: 40, b: 50 },
1962
+ showlegend: false
1963
+ };
1964
+
1965
+ Plotly.react(plotDiv, [trace], layout, { responsive: true, displayModeBar: true });
1966
+ },
1967
+
1968
+ // Optimized: render discrete (annotation) UMAP with cached trace indices
1969
+ renderSingleUmapDiscrete(divId, x, y, annoValues, colorMap, hoverText, title, annoName, pointSize) {
1970
+ const plotDiv = document.getElementById(divId);
1971
+ if (!plotDiv) return;
1972
+
1973
+ const opacity = this.umapData.default_opacity || 0.8;
1974
+ const uniqueValues = [...new Set(annoValues)].sort();
1975
+ const defaultColors = this.getUmapColorScale(uniqueValues.length);
1976
+
1977
+ // Build traces - indices are computed once per annotation
1978
+ const traces = uniqueValues.map((val, i) => {
1979
+ const indices = [];
1980
+ for (let idx = 0; idx < annoValues.length; idx++) {
1981
+ if (annoValues[idx] === val) indices.push(idx);
1982
+ }
1983
+ return {
1984
+ x: indices.map(idx => x[idx]),
1985
+ y: indices.map(idx => y[idx]),
1986
+ mode: 'markers',
1987
+ type: 'scattergl',
1988
+ name: val,
1989
+ text: indices.map(idx => hoverText[idx]),
1990
+ hovertemplate: '%{text}<extra></extra>',
1991
+ marker: {
1992
+ size: pointSize,
1993
+ color: (colorMap && colorMap[val]) || defaultColors[i % defaultColors.length],
1994
+ opacity: opacity
1995
+ }
1996
+ };
1997
+ });
1998
+
1999
+ const layout = {
2000
+ title: { text: title, font: { size: 14 } },
2001
+ xaxis: { title: 'UMAP 1', showgrid: false, zeroline: false },
2002
+ yaxis: { title: 'UMAP 2', showgrid: false, zeroline: false },
2003
+ hovermode: 'closest',
2004
+ margin: { l: 50, r: 20, t: 40, b: 50 },
2005
+ legend: {
2006
+ orientation: 'v',
2007
+ x: 1.02,
2008
+ y: 1,
2009
+ font: { size: 10 },
2010
+ itemsizing: 'constant'
2011
+ },
2012
+ showlegend: uniqueValues.length <= 50
2013
+ };
2014
+
2015
+ Plotly.react(plotDiv, traces, layout, { responsive: true, displayModeBar: true });
2016
+ },
2017
+
2018
+ // Legacy method for backward compatibility (used in init)
2019
+ renderSingleUmap(divId, x, y, annoValues, colorMap, isContinuous, hoverText, title, annoName, pointSize) {
2020
+ if (isContinuous) {
2021
+ this.renderSingleUmapContinuous(divId, x, y, annoValues, colorMap, hoverText, title, annoName, pointSize);
2022
+ } else {
2023
+ this.renderSingleUmapDiscrete(divId, x, y, annoValues, colorMap, hoverText, title, annoName, pointSize);
2024
+ }
2025
+ },
2026
+
2027
+ // Spatial 2D Plot rendering (uses per-sample data loaded on-demand)
2028
+ // Render both 2D spatial plots (trait and annotation side by side)
2029
+ renderSpatial2DPlot() {
2030
+ if (!this.plotlyReady || !this.currentSampleData) return;
2031
+
2032
+ // Render trait plot
2033
+ this.renderSpatial2DPlotSingle('spatial2dPlotTrait', 'trait');
2034
+ // Render annotation plot
2035
+ this.renderSpatial2DPlotSingle('spatial2dPlotAnnotation', 'annotation');
2036
+ },
2037
+
2038
+ // Helper to render a single 2D spatial plot - optimized with caching
2039
+ renderSpatial2DPlotSingle(divId, plotType) {
2040
+ const plotDiv = document.getElementById(divId);
2041
+ if (!plotDiv) return;
2042
+
2043
+ const data = this.currentSampleData;
2044
+ const sample = this.selectedSample;
2045
+
2046
+ // Data is already filtered per sample, no need to filter
2047
+ const x = data.sx;
2048
+ const y = data.sy;
2049
+
2050
+ if (!x || x.length === 0) {
2051
+ console.warn(`No coordinates found for sample: ${sample}`);
2052
+ return;
2053
+ }
2054
+
2055
+ // Use pre-computed point size if available, otherwise calculate
2056
+ const pointSize = data.point_size || this.estimateSpatialPointSize(x, y);
2057
+
2058
+ let traces = [];
2059
+ let layoutExtras = {};
2060
+
2061
+ if (plotType === 'trait') {
2062
+ const trait = this.selectedTrait;
2063
+ // Values already rounded to 1 decimal during export
2064
+ const colorValues = data[trait] || x.map(() => 0);
2065
+ const colorMap = ['#313695', '#4575b4', '#74add1', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026'];
2066
+
2067
+ // Build hover text - include annotation value for trait view
2068
+ const anno = this.selectedAnnotationCategory;
2069
+ const hoverText = data.spot.map((spot, i) => {
2070
+ let text = `<b>Spot:</b> ${spot}`;
2071
+ // Include annotation value
2072
+ if (anno && data[anno]) {
2073
+ const annoVal = data[anno][i];
2074
+ text += `<br><b>${anno}:</b> ${annoVal}`;
2075
+ }
2076
+ // Include trait log10p value
2077
+ const val = data[trait] ? data[trait][i] : 0;
2078
+ const rounded = (val !== null && val !== undefined) ? val.toFixed(1) : 'N/A';
2079
+ text += `<br><b>-log10(p):</b> ${rounded}`;
2080
+ return text;
2081
+ });
2082
+
2083
+ // Single trace with colorbar
2084
+ traces.push({
2085
+ x: x,
2086
+ y: y,
2087
+ mode: 'markers',
2088
+ type: 'scattergl',
2089
+ text: hoverText,
2090
+ hoverinfo: 'text',
2091
+ marker: {
2092
+ size: pointSize,
2093
+ color: colorValues,
2094
+ colorscale: colorMap.map((c, i) => [i / (colorMap.length - 1), c]),
2095
+ // opacity: 0.8,
2096
+ showscale: true,
2097
+ colorbar: {
2098
+ title: {
2099
+ text: '-log10(p)',
2100
+ side: 'top',
2101
+ font: { size: 12 }
2102
+ },
2103
+ thickness: 15,
2104
+ len: 0.5,
2105
+ y: 0.5,
2106
+ x: 1.0,
2107
+ xanchor: 'left'
2108
+ }
2109
+ }
2110
+ });
2111
+ layoutExtras.showlegend = false;
2112
+ } else {
2113
+ // Annotation plot - discrete coloring
2114
+ const anno = this.selectedAnnotationCategory;
2115
+ const colorValues = data[anno] || x.map(() => 'Unknown');
2116
+ const uniqueValues = [...new Set(colorValues)].sort();
2117
+
2118
+ // Use UMAP color maps for consistency
2119
+ const umapColorMap = this.umapData?.color_maps?.[anno];
2120
+ const defaultColors = this.getUmapColorScale(uniqueValues.length);
2121
+
2122
+ // Build hover text - simplified for annotation view
2123
+ const hoverText = data.spot.map((spot, i) => {
2124
+ const val = data[anno] ? data[anno][i] : 'Unknown';
2125
+ return `<b>Spot:</b> ${spot}<br><b>${anno}:</b> ${val}`;
2126
+ });
2127
+
2128
+ // Build traces using optimized loop
2129
+ traces = uniqueValues.map((val, i) => {
2130
+ const indices = [];
2131
+ for (let idx = 0; idx < colorValues.length; idx++) {
2132
+ if (colorValues[idx] === val) indices.push(idx);
2133
+ }
2134
+ const color = (umapColorMap && umapColorMap[val]) || defaultColors[i % defaultColors.length];
2135
+ return {
2136
+ x: indices.map(idx => x[idx]),
2137
+ y: indices.map(idx => y[idx]),
2138
+ mode: 'markers',
2139
+ type: 'scattergl',
2140
+ name: val,
2141
+ text: indices.map(idx => hoverText[idx]),
2142
+ hoverinfo: 'text',
2143
+ marker: {
2144
+ size: pointSize,
2145
+ color: color,
2146
+ // opacity: 0.8
2147
+ }
2148
+ };
2149
+ });
2150
+ layoutExtras.showlegend = uniqueValues.length <= 50;
2151
+ }
2152
+
2153
+ const titleText = plotType === 'trait' ? this.selectedTrait : this.selectedAnnotationCategory;
2154
+ const layout = {
2155
+ title: {
2156
+ text: `${sample} - ${titleText}`,
2157
+ font: { size: 14 }
2158
+ },
2159
+ xaxis: {
2160
+ showgrid: false,
2161
+ zeroline: false,
2162
+ showticklabels: false,
2163
+ scaleanchor: 'y' // Maintain aspect ratio
2164
+ },
2165
+ yaxis: {
2166
+ showgrid: false,
2167
+ zeroline: false,
2168
+ showticklabels: false,
2169
+ autorange: (this.reportMeta.plot_origin === 'upper') ? 'reversed' : true
2170
+ },
2171
+ hovermode: 'closest',
2172
+ margin: { l: 20, r: 20, t: 40, b: 20 },
2173
+ legend: {
2174
+ orientation: 'v',
2175
+ x: 1.02,
2176
+ y: 1,
2177
+ font: { size: 10 },
2178
+ itemsizing: 'constant'
2179
+ },
2180
+ ...layoutExtras
2181
+ };
2182
+
2183
+ const config = { responsive: true, displayModeBar: true };
2184
+
2185
+ // Always use Plotly.react for efficient updates
2186
+ Plotly.react(plotDiv, traces, layout, config);
2187
+ },
2188
+
2189
+ // Helper function to estimate point size for spatial plot
2190
+ estimateSpatialPointSize(x, y) {
2191
+ if (x.length === 0) return 5;
2192
+
2193
+ // Calculate data range using reduce to avoid stack overflow on large arrays
2194
+ // Note: Math.min(...arr) fails when arr.length > ~100k elements
2195
+ let xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity;
2196
+ for (let i = 0; i < x.length; i++) {
2197
+ if (x[i] < xMin) xMin = x[i];
2198
+ if (x[i] > xMax) xMax = x[i];
2199
+ if (y[i] < yMin) yMin = y[i];
2200
+ if (y[i] > yMax) yMax = y[i];
2201
+ }
2202
+
2203
+ const xRange = xMax - xMin;
2204
+ const yRange = yMax - yMin;
2205
+ const dataRange = Math.max(xRange, yRange);
2206
+
2207
+ if (dataRange === 0) return 5;
2208
+
2209
+ // Estimate average point spacing using simple heuristic
2210
+ // For a uniform distribution, avg spacing ~= sqrt(area / n_points)
2211
+ const area = xRange * yRange;
2212
+ const avgSpacing = Math.sqrt(area / x.length);
2213
+
2214
+ // Target: fill ~25-30% of area with points
2215
+ // Point size in pixels, assuming ~600px viewport width
2216
+ const viewportWidth = 600;
2217
+ const pixelsPerDataUnit = viewportWidth / dataRange;
2218
+ const pointSizePixels = avgSpacing * pixelsPerDataUnit * 0.8;
2219
+
2220
+ // Clamp between reasonable bounds
2221
+ return Math.max(2, Math.min(15, pointSizePixels));
2222
+ },
2223
+
2224
+ getUmapColorScale(n) {
2225
+ // Categorical color palette
2226
+ const palette = [
2227
+ '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
2228
+ '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf',
2229
+ '#aec7e8', '#ffbb78', '#98df8a', '#ff9896', '#c5b0d5',
2230
+ '#c49c94', '#f7b6d2', '#c7c7c7', '#dbdb8d', '#9edae5',
2231
+ '#393b79', '#5254a3', '#6b6ecf', '#9c9ede', '#637939',
2232
+ '#8ca252', '#b5cf6b', '#cedb9c', '#8c6d31', '#bd9e39'
2233
+ ];
2234
+ return palette.slice(0, Math.max(n, palette.length));
2235
+ }
2236
+ };
2237
+ }
2238
+ </script>
2239
+
2240
+ </body>
2241
+
2242
+ </html>