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.
- gsMap/__init__.py +13 -0
- gsMap/__main__.py +4 -0
- gsMap/cauchy_combination_test.py +342 -0
- gsMap/cli.py +355 -0
- gsMap/config/__init__.py +72 -0
- gsMap/config/base.py +296 -0
- gsMap/config/cauchy_config.py +79 -0
- gsMap/config/dataclasses.py +235 -0
- gsMap/config/decorators.py +302 -0
- gsMap/config/find_latent_config.py +276 -0
- gsMap/config/format_sumstats_config.py +54 -0
- gsMap/config/latent2gene_config.py +461 -0
- gsMap/config/ldscore_config.py +261 -0
- gsMap/config/quick_mode_config.py +242 -0
- gsMap/config/report_config.py +81 -0
- gsMap/config/spatial_ldsc_config.py +334 -0
- gsMap/config/utils.py +286 -0
- gsMap/find_latent/__init__.py +3 -0
- gsMap/find_latent/find_latent_representation.py +312 -0
- gsMap/find_latent/gnn/distribution.py +498 -0
- gsMap/find_latent/gnn/encoder_decoder.py +186 -0
- gsMap/find_latent/gnn/gcn.py +85 -0
- gsMap/find_latent/gnn/gene_former.py +164 -0
- gsMap/find_latent/gnn/loss.py +18 -0
- gsMap/find_latent/gnn/st_model.py +125 -0
- gsMap/find_latent/gnn/train_step.py +177 -0
- gsMap/find_latent/st_process.py +781 -0
- gsMap/format_sumstats.py +446 -0
- gsMap/generate_ldscore.py +1018 -0
- gsMap/latent2gene/__init__.py +18 -0
- gsMap/latent2gene/connectivity.py +781 -0
- gsMap/latent2gene/entry_point.py +141 -0
- gsMap/latent2gene/marker_scores.py +1265 -0
- gsMap/latent2gene/memmap_io.py +766 -0
- gsMap/latent2gene/rank_calculator.py +590 -0
- gsMap/latent2gene/row_ordering.py +182 -0
- gsMap/latent2gene/row_ordering_jax.py +159 -0
- gsMap/ldscore/__init__.py +1 -0
- gsMap/ldscore/batch_construction.py +163 -0
- gsMap/ldscore/compute.py +126 -0
- gsMap/ldscore/constants.py +70 -0
- gsMap/ldscore/io.py +262 -0
- gsMap/ldscore/mapping.py +262 -0
- gsMap/ldscore/pipeline.py +615 -0
- gsMap/pipeline/quick_mode.py +134 -0
- gsMap/report/__init__.py +2 -0
- gsMap/report/diagnosis.py +375 -0
- gsMap/report/report.py +100 -0
- gsMap/report/report_data.py +1832 -0
- gsMap/report/static/js_lib/alpine.min.js +5 -0
- gsMap/report/static/js_lib/tailwindcss.js +83 -0
- gsMap/report/static/template.html +2242 -0
- gsMap/report/three_d_combine.py +312 -0
- gsMap/report/three_d_plot/three_d_plot_decorate.py +246 -0
- gsMap/report/three_d_plot/three_d_plot_prepare.py +202 -0
- gsMap/report/three_d_plot/three_d_plots.py +425 -0
- gsMap/report/visualize.py +1409 -0
- gsMap/setup.py +5 -0
- gsMap/spatial_ldsc/__init__.py +0 -0
- gsMap/spatial_ldsc/io.py +656 -0
- gsMap/spatial_ldsc/ldscore_quick_mode.py +912 -0
- gsMap/spatial_ldsc/spatial_ldsc_jax.py +382 -0
- gsMap/spatial_ldsc/spatial_ldsc_multiple_sumstats.py +439 -0
- gsMap/utils/__init__.py +0 -0
- gsMap/utils/generate_r2_matrix.py +610 -0
- gsMap/utils/jackknife.py +518 -0
- gsMap/utils/manhattan_plot.py +643 -0
- gsMap/utils/regression_read.py +177 -0
- gsMap/utils/torch_utils.py +23 -0
- gsmap3d-0.1.0a1.dist-info/METADATA +168 -0
- gsmap3d-0.1.0a1.dist-info/RECORD +74 -0
- gsmap3d-0.1.0a1.dist-info/WHEEL +4 -0
- gsmap3d-0.1.0a1.dist-info/entry_points.txt +2 -0
- 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>
|