openms-insight 0.1.1__py3-none-any.whl → 0.1.3__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.
- openms_insight/__init__.py +11 -7
- openms_insight/components/__init__.py +2 -2
- openms_insight/components/heatmap.py +192 -102
- openms_insight/components/lineplot.py +377 -82
- openms_insight/components/sequenceview.py +677 -213
- openms_insight/components/table.py +86 -58
- openms_insight/core/__init__.py +2 -2
- openms_insight/core/base.py +113 -49
- openms_insight/core/registry.py +6 -5
- openms_insight/core/state.py +33 -31
- openms_insight/core/subprocess_preprocess.py +1 -3
- openms_insight/js-component/dist/assets/index.css +1 -1
- openms_insight/js-component/dist/assets/index.js +113 -113
- openms_insight/preprocessing/__init__.py +5 -6
- openms_insight/preprocessing/compression.py +68 -66
- openms_insight/preprocessing/filtering.py +119 -9
- openms_insight/rendering/__init__.py +1 -1
- openms_insight/rendering/bridge.py +192 -42
- {openms_insight-0.1.1.dist-info → openms_insight-0.1.3.dist-info}/METADATA +163 -20
- openms_insight-0.1.3.dist-info/RECORD +28 -0
- openms_insight-0.1.1.dist-info/RECORD +0 -28
- {openms_insight-0.1.1.dist-info → openms_insight-0.1.3.dist-info}/WHEEL +0 -0
- {openms_insight-0.1.1.dist-info → openms_insight-0.1.3.dist-info}/licenses/LICENSE +0 -0
openms_insight/__init__.py
CHANGED
|
@@ -5,15 +5,15 @@ This package provides reusable, interactive Streamlit components backed by Vue.j
|
|
|
5
5
|
visualizations with cross-component selection state management.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from .components.heatmap import Heatmap
|
|
9
|
+
from .components.lineplot import LinePlot
|
|
10
|
+
from .components.sequenceview import SequenceView, SequenceViewResult
|
|
11
|
+
from .components.table import Table
|
|
8
12
|
from .core.base import BaseComponent
|
|
9
|
-
from .core.state import StateManager
|
|
10
|
-
from .core.registry import register_component, get_component_class
|
|
11
13
|
from .core.cache import CacheMissError
|
|
12
|
-
|
|
13
|
-
from .
|
|
14
|
-
from .
|
|
15
|
-
from .components.heatmap import Heatmap
|
|
16
|
-
from .components.sequenceview import SequenceView
|
|
14
|
+
from .core.registry import get_component_class, register_component
|
|
15
|
+
from .core.state import StateManager
|
|
16
|
+
from .rendering.bridge import clear_component_annotations, get_component_annotations
|
|
17
17
|
|
|
18
18
|
__version__ = "0.1.0"
|
|
19
19
|
|
|
@@ -29,4 +29,8 @@ __all__ = [
|
|
|
29
29
|
"LinePlot",
|
|
30
30
|
"Heatmap",
|
|
31
31
|
"SequenceView",
|
|
32
|
+
"SequenceViewResult",
|
|
33
|
+
# Utilities
|
|
34
|
+
"get_component_annotations",
|
|
35
|
+
"clear_component_annotations",
|
|
32
36
|
]
|
|
@@ -22,10 +22,10 @@ def _make_zoom_cache_key(zoom: Optional[Dict[str, Any]]) -> tuple:
|
|
|
22
22
|
if zoom is None:
|
|
23
23
|
return (None,)
|
|
24
24
|
return (
|
|
25
|
-
(
|
|
26
|
-
(
|
|
27
|
-
(
|
|
28
|
-
(
|
|
25
|
+
("x0", zoom.get("xRange", [-1, -1])[0]),
|
|
26
|
+
("x1", zoom.get("xRange", [-1, -1])[1]),
|
|
27
|
+
("y0", zoom.get("yRange", [-1, -1])[0]),
|
|
28
|
+
("y1", zoom.get("yRange", [-1, -1])[1]),
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
|
|
@@ -66,11 +66,11 @@ class Heatmap(BaseComponent):
|
|
|
66
66
|
def __init__(
|
|
67
67
|
self,
|
|
68
68
|
cache_id: str,
|
|
69
|
-
x_column: str,
|
|
70
|
-
y_column: str,
|
|
69
|
+
x_column: Optional[str] = None,
|
|
70
|
+
y_column: Optional[str] = None,
|
|
71
71
|
data: Optional[pl.LazyFrame] = None,
|
|
72
72
|
data_path: Optional[str] = None,
|
|
73
|
-
intensity_column: str =
|
|
73
|
+
intensity_column: Optional[str] = None,
|
|
74
74
|
filters: Optional[Dict[str, str]] = None,
|
|
75
75
|
filter_defaults: Optional[Dict[str, Any]] = None,
|
|
76
76
|
interactivity: Optional[Dict[str, str]] = None,
|
|
@@ -79,15 +79,15 @@ class Heatmap(BaseComponent):
|
|
|
79
79
|
min_points: int = 20000,
|
|
80
80
|
x_bins: int = 400,
|
|
81
81
|
y_bins: int = 50,
|
|
82
|
-
zoom_identifier: str =
|
|
82
|
+
zoom_identifier: str = "heatmap_zoom",
|
|
83
83
|
title: Optional[str] = None,
|
|
84
84
|
x_label: Optional[str] = None,
|
|
85
85
|
y_label: Optional[str] = None,
|
|
86
|
-
colorscale: str =
|
|
86
|
+
colorscale: str = "Portland",
|
|
87
87
|
use_simple_downsample: bool = False,
|
|
88
88
|
use_streaming: bool = True,
|
|
89
89
|
categorical_filters: Optional[List[str]] = None,
|
|
90
|
-
**kwargs
|
|
90
|
+
**kwargs,
|
|
91
91
|
):
|
|
92
92
|
"""
|
|
93
93
|
Initialize the Heatmap component.
|
|
@@ -165,7 +165,7 @@ class Heatmap(BaseComponent):
|
|
|
165
165
|
use_simple_downsample=use_simple_downsample,
|
|
166
166
|
use_streaming=use_streaming,
|
|
167
167
|
categorical_filters=categorical_filters,
|
|
168
|
-
**kwargs
|
|
168
|
+
**kwargs,
|
|
169
169
|
)
|
|
170
170
|
|
|
171
171
|
def _get_cache_config(self) -> Dict[str, Any]:
|
|
@@ -176,17 +176,39 @@ class Heatmap(BaseComponent):
|
|
|
176
176
|
Dict of config values that affect preprocessing
|
|
177
177
|
"""
|
|
178
178
|
return {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
179
|
+
"x_column": self._x_column,
|
|
180
|
+
"y_column": self._y_column,
|
|
181
|
+
"intensity_column": self._intensity_column,
|
|
182
|
+
"min_points": self._min_points,
|
|
183
|
+
"x_bins": self._x_bins,
|
|
184
|
+
"y_bins": self._y_bins,
|
|
185
|
+
"use_simple_downsample": self._use_simple_downsample,
|
|
186
|
+
"use_streaming": self._use_streaming,
|
|
187
|
+
"categorical_filters": sorted(self._categorical_filters),
|
|
188
|
+
"zoom_identifier": self._zoom_identifier,
|
|
189
|
+
"title": self._title,
|
|
190
|
+
"x_label": self._x_label,
|
|
191
|
+
"y_label": self._y_label,
|
|
192
|
+
"colorscale": self._colorscale,
|
|
188
193
|
}
|
|
189
194
|
|
|
195
|
+
def _restore_cache_config(self, config: Dict[str, Any]) -> None:
|
|
196
|
+
"""Restore component-specific configuration from cached config."""
|
|
197
|
+
self._x_column = config.get("x_column")
|
|
198
|
+
self._y_column = config.get("y_column")
|
|
199
|
+
self._intensity_column = config.get("intensity_column", "intensity")
|
|
200
|
+
self._min_points = config.get("min_points", 20000)
|
|
201
|
+
self._x_bins = config.get("x_bins", 400)
|
|
202
|
+
self._y_bins = config.get("y_bins", 50)
|
|
203
|
+
self._use_simple_downsample = config.get("use_simple_downsample", False)
|
|
204
|
+
self._use_streaming = config.get("use_streaming", True)
|
|
205
|
+
self._categorical_filters = config.get("categorical_filters", [])
|
|
206
|
+
self._zoom_identifier = config.get("zoom_identifier", "heatmap_zoom")
|
|
207
|
+
self._title = config.get("title")
|
|
208
|
+
self._x_label = config.get("x_label", self._x_column)
|
|
209
|
+
self._y_label = config.get("y_label", self._y_column)
|
|
210
|
+
self._colorscale = config.get("colorscale", "Portland")
|
|
211
|
+
|
|
190
212
|
def get_state_dependencies(self) -> list:
|
|
191
213
|
"""
|
|
192
214
|
Return list of state keys that affect this component's data.
|
|
@@ -229,6 +251,8 @@ class Heatmap(BaseComponent):
|
|
|
229
251
|
render time, the resulting data has ~min_points regardless of the
|
|
230
252
|
filter value selected.
|
|
231
253
|
|
|
254
|
+
Data is sorted by x, y columns for efficient range query predicate pushdown.
|
|
255
|
+
|
|
232
256
|
Example: For im_dimension with values [0, 1, 2, 3], creates:
|
|
233
257
|
- cat_level_im_dimension_0_0: 20K points with im_id=0
|
|
234
258
|
- cat_level_im_dimension_0_1: 20K points with im_id=1
|
|
@@ -242,53 +266,71 @@ class Heatmap(BaseComponent):
|
|
|
242
266
|
self._x_column,
|
|
243
267
|
self._y_column,
|
|
244
268
|
)
|
|
245
|
-
self._preprocessed_data[
|
|
246
|
-
self._preprocessed_data[
|
|
269
|
+
self._preprocessed_data["x_range"] = x_range
|
|
270
|
+
self._preprocessed_data["y_range"] = y_range
|
|
247
271
|
|
|
248
272
|
# Get total count
|
|
249
273
|
total = self._raw_data.select(pl.len()).collect().item()
|
|
250
|
-
self._preprocessed_data[
|
|
274
|
+
self._preprocessed_data["total"] = total
|
|
251
275
|
|
|
252
276
|
# Store metadata about categorical filters
|
|
253
|
-
self._preprocessed_data[
|
|
254
|
-
self._preprocessed_data[
|
|
277
|
+
self._preprocessed_data["has_categorical_filters"] = True
|
|
278
|
+
self._preprocessed_data["categorical_filter_values"] = {}
|
|
255
279
|
|
|
256
280
|
# Process each categorical filter
|
|
257
281
|
for filter_id in self._categorical_filters:
|
|
258
282
|
if filter_id not in self._filters:
|
|
259
|
-
print(
|
|
283
|
+
print(
|
|
284
|
+
f"[HEATMAP] Warning: categorical_filter '{filter_id}' not in filters, skipping",
|
|
285
|
+
file=sys.stderr,
|
|
286
|
+
)
|
|
260
287
|
continue
|
|
261
288
|
|
|
262
289
|
column_name = self._filters[filter_id]
|
|
263
290
|
|
|
264
291
|
# Get unique values for this filter
|
|
265
292
|
unique_values = (
|
|
266
|
-
self._raw_data
|
|
267
|
-
.select(pl.col(column_name))
|
|
293
|
+
self._raw_data.select(pl.col(column_name))
|
|
268
294
|
.unique()
|
|
269
295
|
.collect()
|
|
270
296
|
.to_series()
|
|
271
297
|
.to_list()
|
|
272
298
|
)
|
|
273
|
-
unique_values = sorted(
|
|
299
|
+
unique_values = sorted(
|
|
300
|
+
[v for v in unique_values if v is not None and v >= 0]
|
|
301
|
+
)
|
|
274
302
|
|
|
275
|
-
print(
|
|
303
|
+
print(
|
|
304
|
+
f"[HEATMAP] Categorical filter '{filter_id}' ({column_name}): {len(unique_values)} unique values",
|
|
305
|
+
file=sys.stderr,
|
|
306
|
+
)
|
|
276
307
|
|
|
277
|
-
self._preprocessed_data[
|
|
308
|
+
self._preprocessed_data["categorical_filter_values"][filter_id] = (
|
|
309
|
+
unique_values
|
|
310
|
+
)
|
|
278
311
|
|
|
279
312
|
# Create compression levels for each filter value
|
|
280
313
|
for filter_value in unique_values:
|
|
281
314
|
# Filter data to this value
|
|
282
|
-
filtered_data = self._raw_data.filter(
|
|
315
|
+
filtered_data = self._raw_data.filter(
|
|
316
|
+
pl.col(column_name) == filter_value
|
|
317
|
+
)
|
|
283
318
|
filtered_total = filtered_data.select(pl.len()).collect().item()
|
|
284
319
|
|
|
285
320
|
# Compute level sizes for this filtered subset
|
|
286
|
-
level_sizes = compute_compression_levels(
|
|
321
|
+
level_sizes = compute_compression_levels(
|
|
322
|
+
self._min_points, filtered_total
|
|
323
|
+
)
|
|
287
324
|
|
|
288
|
-
print(
|
|
325
|
+
print(
|
|
326
|
+
f"[HEATMAP] Value {filter_value}: {filtered_total:,} pts → levels {level_sizes}",
|
|
327
|
+
file=sys.stderr,
|
|
328
|
+
)
|
|
289
329
|
|
|
290
330
|
# Store level sizes for this filter value
|
|
291
|
-
self._preprocessed_data[
|
|
331
|
+
self._preprocessed_data[
|
|
332
|
+
f"cat_level_sizes_{filter_id}_{filter_value}"
|
|
333
|
+
] = level_sizes
|
|
292
334
|
|
|
293
335
|
# Build each compressed level
|
|
294
336
|
for level_idx, target_size in enumerate(level_sizes):
|
|
@@ -314,20 +356,27 @@ class Heatmap(BaseComponent):
|
|
|
314
356
|
y_range=y_range,
|
|
315
357
|
)
|
|
316
358
|
|
|
359
|
+
# Sort by x, y for efficient range query predicate pushdown
|
|
360
|
+
level = level.sort([self._x_column, self._y_column])
|
|
317
361
|
# Store LazyFrame for streaming to disk
|
|
318
|
-
level_key = f
|
|
362
|
+
level_key = f"cat_level_{filter_id}_{filter_value}_{level_idx}"
|
|
319
363
|
self._preprocessed_data[level_key] = level # Keep lazy
|
|
320
364
|
|
|
321
365
|
# Add full resolution as final level (for zoom fallback)
|
|
366
|
+
# Also sorted for consistent predicate pushdown behavior
|
|
322
367
|
num_compressed = len(level_sizes)
|
|
323
|
-
full_res_key = f
|
|
324
|
-
self._preprocessed_data[full_res_key] = filtered_data
|
|
325
|
-
|
|
368
|
+
full_res_key = f"cat_level_{filter_id}_{filter_value}_{num_compressed}"
|
|
369
|
+
self._preprocessed_data[full_res_key] = filtered_data.sort(
|
|
370
|
+
[self._x_column, self._y_column]
|
|
371
|
+
)
|
|
372
|
+
self._preprocessed_data[
|
|
373
|
+
f"cat_num_levels_{filter_id}_{filter_value}"
|
|
374
|
+
] = num_compressed + 1
|
|
326
375
|
|
|
327
376
|
# Also create global levels for when no categorical filter is selected
|
|
328
377
|
# (fallback to standard behavior)
|
|
329
378
|
level_sizes = compute_compression_levels(self._min_points, total)
|
|
330
|
-
self._preprocessed_data[
|
|
379
|
+
self._preprocessed_data["level_sizes"] = level_sizes
|
|
331
380
|
|
|
332
381
|
for i, size in enumerate(level_sizes):
|
|
333
382
|
# If target size equals total, skip downsampling - use all data
|
|
@@ -351,18 +400,24 @@ class Heatmap(BaseComponent):
|
|
|
351
400
|
x_range=x_range,
|
|
352
401
|
y_range=y_range,
|
|
353
402
|
)
|
|
354
|
-
|
|
403
|
+
# Sort by x, y for efficient range query predicate pushdown
|
|
404
|
+
level = level.sort([self._x_column, self._y_column])
|
|
405
|
+
self._preprocessed_data[f"level_{i}"] = level # Keep lazy
|
|
355
406
|
|
|
356
407
|
# Add full resolution as final level (for zoom fallback)
|
|
408
|
+
# Also sorted for consistent predicate pushdown behavior
|
|
357
409
|
num_compressed = len(level_sizes)
|
|
358
|
-
self._preprocessed_data[f
|
|
359
|
-
|
|
410
|
+
self._preprocessed_data[f"level_{num_compressed}"] = self._raw_data.sort(
|
|
411
|
+
[self._x_column, self._y_column]
|
|
412
|
+
)
|
|
413
|
+
self._preprocessed_data["num_levels"] = num_compressed + 1
|
|
360
414
|
|
|
361
415
|
def _preprocess_streaming(self) -> None:
|
|
362
416
|
"""
|
|
363
417
|
Streaming preprocessing - levels stay lazy through caching.
|
|
364
418
|
|
|
365
419
|
Builds lazy query plans that are streamed to disk via sink_parquet().
|
|
420
|
+
Data is sorted by x, y columns for efficient range query predicate pushdown.
|
|
366
421
|
"""
|
|
367
422
|
# Get data ranges (minimal collect - just 4 values)
|
|
368
423
|
x_range, y_range = get_data_range(
|
|
@@ -370,19 +425,19 @@ class Heatmap(BaseComponent):
|
|
|
370
425
|
self._x_column,
|
|
371
426
|
self._y_column,
|
|
372
427
|
)
|
|
373
|
-
self._preprocessed_data[
|
|
374
|
-
self._preprocessed_data[
|
|
428
|
+
self._preprocessed_data["x_range"] = x_range
|
|
429
|
+
self._preprocessed_data["y_range"] = y_range
|
|
375
430
|
|
|
376
431
|
# Get total count
|
|
377
432
|
total = self._raw_data.select(pl.len()).collect().item()
|
|
378
|
-
self._preprocessed_data[
|
|
433
|
+
self._preprocessed_data["total"] = total
|
|
379
434
|
|
|
380
435
|
# Compute target sizes for levels
|
|
381
436
|
level_sizes = compute_compression_levels(self._min_points, total)
|
|
382
|
-
self._preprocessed_data[
|
|
437
|
+
self._preprocessed_data["level_sizes"] = level_sizes
|
|
383
438
|
|
|
384
439
|
# Build and collect each level
|
|
385
|
-
self._preprocessed_data[
|
|
440
|
+
self._preprocessed_data["levels"] = []
|
|
386
441
|
|
|
387
442
|
for i, size in enumerate(level_sizes):
|
|
388
443
|
# If target size equals total, skip downsampling - use all data
|
|
@@ -406,16 +461,22 @@ class Heatmap(BaseComponent):
|
|
|
406
461
|
x_range=x_range,
|
|
407
462
|
y_range=y_range,
|
|
408
463
|
)
|
|
464
|
+
# Sort by x, y for efficient range query predicate pushdown
|
|
465
|
+
# This clusters spatially close points together in row groups
|
|
466
|
+
level = level.sort([self._x_column, self._y_column])
|
|
409
467
|
# Store LazyFrame for streaming to disk
|
|
410
468
|
# Base class will use sink_parquet() to stream without full materialization
|
|
411
|
-
self._preprocessed_data[f
|
|
469
|
+
self._preprocessed_data[f"level_{i}"] = level # Keep lazy
|
|
412
470
|
|
|
413
471
|
# Add full resolution as final level (for zoom fallback)
|
|
472
|
+
# Also sorted for consistent predicate pushdown behavior
|
|
414
473
|
num_compressed = len(level_sizes)
|
|
415
|
-
self._preprocessed_data[f
|
|
474
|
+
self._preprocessed_data[f"level_{num_compressed}"] = self._raw_data.sort(
|
|
475
|
+
[self._x_column, self._y_column]
|
|
476
|
+
)
|
|
416
477
|
|
|
417
478
|
# Store number of levels for reconstruction (includes full resolution)
|
|
418
|
-
self._preprocessed_data[
|
|
479
|
+
self._preprocessed_data["num_levels"] = num_compressed + 1
|
|
419
480
|
|
|
420
481
|
def _preprocess_eager(self) -> None:
|
|
421
482
|
"""
|
|
@@ -423,6 +484,7 @@ class Heatmap(BaseComponent):
|
|
|
423
484
|
|
|
424
485
|
Uses more memory at init but faster rendering. Uses scipy-based
|
|
425
486
|
downsampling for better spatial distribution.
|
|
487
|
+
Data is sorted by x, y columns for efficient range query predicate pushdown.
|
|
426
488
|
"""
|
|
427
489
|
# Get data ranges
|
|
428
490
|
x_range, y_range = get_data_range(
|
|
@@ -430,16 +492,16 @@ class Heatmap(BaseComponent):
|
|
|
430
492
|
self._x_column,
|
|
431
493
|
self._y_column,
|
|
432
494
|
)
|
|
433
|
-
self._preprocessed_data[
|
|
434
|
-
self._preprocessed_data[
|
|
495
|
+
self._preprocessed_data["x_range"] = x_range
|
|
496
|
+
self._preprocessed_data["y_range"] = y_range
|
|
435
497
|
|
|
436
498
|
# Get total count
|
|
437
499
|
total = self._raw_data.select(pl.len()).collect().item()
|
|
438
|
-
self._preprocessed_data[
|
|
500
|
+
self._preprocessed_data["total"] = total
|
|
439
501
|
|
|
440
502
|
# Compute compression level target sizes
|
|
441
503
|
level_sizes = compute_compression_levels(self._min_points, total)
|
|
442
|
-
self._preprocessed_data[
|
|
504
|
+
self._preprocessed_data["level_sizes"] = level_sizes
|
|
443
505
|
|
|
444
506
|
# Build levels from largest to smallest
|
|
445
507
|
if level_sizes:
|
|
@@ -465,21 +527,31 @@ class Heatmap(BaseComponent):
|
|
|
465
527
|
x_bins=self._x_bins,
|
|
466
528
|
y_bins=self._y_bins,
|
|
467
529
|
)
|
|
530
|
+
# Sort by x, y for efficient range query predicate pushdown
|
|
531
|
+
if isinstance(downsampled, pl.LazyFrame):
|
|
532
|
+
downsampled = downsampled.sort([self._x_column, self._y_column])
|
|
533
|
+
else:
|
|
534
|
+
downsampled = downsampled.sort([self._x_column, self._y_column])
|
|
468
535
|
# Store LazyFrame for streaming to disk
|
|
469
536
|
level_idx = len(level_sizes) - 1 - i
|
|
470
537
|
if isinstance(downsampled, pl.LazyFrame):
|
|
471
|
-
self._preprocessed_data[f
|
|
538
|
+
self._preprocessed_data[f"level_{level_idx}"] = (
|
|
539
|
+
downsampled # Keep lazy
|
|
540
|
+
)
|
|
472
541
|
else:
|
|
473
542
|
# DataFrame from downsample_2d - convert back to lazy
|
|
474
|
-
self._preprocessed_data[f
|
|
543
|
+
self._preprocessed_data[f"level_{level_idx}"] = downsampled.lazy()
|
|
475
544
|
current = downsampled
|
|
476
545
|
|
|
477
546
|
# Add full resolution as final level (for zoom fallback)
|
|
547
|
+
# Also sorted for consistent predicate pushdown behavior
|
|
478
548
|
num_compressed = len(level_sizes)
|
|
479
|
-
self._preprocessed_data[f
|
|
549
|
+
self._preprocessed_data[f"level_{num_compressed}"] = self._raw_data.sort(
|
|
550
|
+
[self._x_column, self._y_column]
|
|
551
|
+
)
|
|
480
552
|
|
|
481
553
|
# Store number of levels for reconstruction (includes full resolution)
|
|
482
|
-
self._preprocessed_data[
|
|
554
|
+
self._preprocessed_data["num_levels"] = num_compressed + 1
|
|
483
555
|
|
|
484
556
|
def _get_levels(self) -> list:
|
|
485
557
|
"""
|
|
@@ -488,11 +560,11 @@ class Heatmap(BaseComponent):
|
|
|
488
560
|
Reconstructs the levels list from preprocessed data,
|
|
489
561
|
adding full resolution at the end.
|
|
490
562
|
"""
|
|
491
|
-
num_levels = self._preprocessed_data.get(
|
|
563
|
+
num_levels = self._preprocessed_data.get("num_levels", 0)
|
|
492
564
|
levels = []
|
|
493
565
|
|
|
494
566
|
for i in range(num_levels):
|
|
495
|
-
level_data = self._preprocessed_data.get(f
|
|
567
|
+
level_data = self._preprocessed_data.get(f"level_{i}")
|
|
496
568
|
if level_data is not None:
|
|
497
569
|
levels.append(level_data)
|
|
498
570
|
|
|
@@ -515,7 +587,7 @@ class Heatmap(BaseComponent):
|
|
|
515
587
|
Returns ([], None) if no categorical levels exist for this filter
|
|
516
588
|
"""
|
|
517
589
|
# Check if we have categorical levels for this filter/value
|
|
518
|
-
num_levels_key = f
|
|
590
|
+
num_levels_key = f"cat_num_levels_{filter_id}_{filter_value}"
|
|
519
591
|
num_levels = self._preprocessed_data.get(num_levels_key, 0)
|
|
520
592
|
|
|
521
593
|
if num_levels == 0:
|
|
@@ -523,14 +595,16 @@ class Heatmap(BaseComponent):
|
|
|
523
595
|
|
|
524
596
|
levels = []
|
|
525
597
|
for i in range(num_levels):
|
|
526
|
-
level_key = f
|
|
598
|
+
level_key = f"cat_level_{filter_id}_{filter_value}_{i}"
|
|
527
599
|
level_data = self._preprocessed_data.get(level_key)
|
|
528
600
|
if level_data is not None:
|
|
529
601
|
levels.append(level_data)
|
|
530
602
|
|
|
531
603
|
return levels, None # Full resolution included in cached levels
|
|
532
604
|
|
|
533
|
-
def _get_levels_for_state(
|
|
605
|
+
def _get_levels_for_state(
|
|
606
|
+
self, state: Dict[str, Any]
|
|
607
|
+
) -> Tuple[list, Optional[pl.LazyFrame]]:
|
|
534
608
|
"""
|
|
535
609
|
Get appropriate compression levels based on current filter state.
|
|
536
610
|
|
|
@@ -545,8 +619,10 @@ class Heatmap(BaseComponent):
|
|
|
545
619
|
Tuple of (levels list, raw data for full resolution)
|
|
546
620
|
"""
|
|
547
621
|
# Check if we have categorical filters and a selected value
|
|
548
|
-
if self._preprocessed_data.get(
|
|
549
|
-
cat_filter_values = self._preprocessed_data.get(
|
|
622
|
+
if self._preprocessed_data.get("has_categorical_filters"):
|
|
623
|
+
cat_filter_values = self._preprocessed_data.get(
|
|
624
|
+
"categorical_filter_values", {}
|
|
625
|
+
)
|
|
550
626
|
|
|
551
627
|
for filter_id in self._categorical_filters:
|
|
552
628
|
if filter_id not in cat_filter_values:
|
|
@@ -562,7 +638,9 @@ class Heatmap(BaseComponent):
|
|
|
562
638
|
|
|
563
639
|
# Check if this value has per-filter levels
|
|
564
640
|
if selected_value in cat_filter_values[filter_id]:
|
|
565
|
-
levels, filtered_raw = self._get_categorical_levels(
|
|
641
|
+
levels, filtered_raw = self._get_categorical_levels(
|
|
642
|
+
filter_id, selected_value
|
|
643
|
+
)
|
|
566
644
|
if levels:
|
|
567
645
|
return levels, filtered_raw
|
|
568
646
|
|
|
@@ -571,22 +649,19 @@ class Heatmap(BaseComponent):
|
|
|
571
649
|
|
|
572
650
|
def _get_vue_component_name(self) -> str:
|
|
573
651
|
"""Return the Vue component name."""
|
|
574
|
-
return
|
|
652
|
+
return "PlotlyHeatmap"
|
|
575
653
|
|
|
576
654
|
def _get_data_key(self) -> str:
|
|
577
655
|
"""Return the key used to send primary data to Vue."""
|
|
578
|
-
return
|
|
656
|
+
return "heatmapData"
|
|
579
657
|
|
|
580
658
|
def _is_no_zoom(self, zoom: Optional[Dict[str, Any]]) -> bool:
|
|
581
659
|
"""Check if zoom state represents no zoom (full view)."""
|
|
582
660
|
if zoom is None:
|
|
583
661
|
return True
|
|
584
|
-
x_range = zoom.get(
|
|
585
|
-
y_range = zoom.get(
|
|
586
|
-
return
|
|
587
|
-
x_range[0] < 0 and x_range[1] < 0 and
|
|
588
|
-
y_range[0] < 0 and y_range[1] < 0
|
|
589
|
-
)
|
|
662
|
+
x_range = zoom.get("xRange", [-1, -1])
|
|
663
|
+
y_range = zoom.get("yRange", [-1, -1])
|
|
664
|
+
return x_range[0] < 0 and x_range[1] < 0 and y_range[0] < 0 and y_range[1] < 0
|
|
590
665
|
|
|
591
666
|
def _select_level_for_zoom(
|
|
592
667
|
self,
|
|
@@ -613,8 +688,9 @@ class Heatmap(BaseComponent):
|
|
|
613
688
|
Filtered Polars DataFrame at appropriate resolution
|
|
614
689
|
"""
|
|
615
690
|
import sys
|
|
616
|
-
|
|
617
|
-
|
|
691
|
+
|
|
692
|
+
x0, x1 = zoom["xRange"]
|
|
693
|
+
y0, y1 = zoom["yRange"]
|
|
618
694
|
|
|
619
695
|
# Add raw data as final level if available
|
|
620
696
|
all_levels = list(levels)
|
|
@@ -630,10 +706,10 @@ class Heatmap(BaseComponent):
|
|
|
630
706
|
|
|
631
707
|
# Filter to zoom range
|
|
632
708
|
filtered_lazy = level_data.filter(
|
|
633
|
-
(pl.col(self._x_column) >= x0)
|
|
634
|
-
(pl.col(self._x_column) <= x1)
|
|
635
|
-
(pl.col(self._y_column) >= y0)
|
|
636
|
-
(pl.col(self._y_column) <= y1)
|
|
709
|
+
(pl.col(self._x_column) >= x0)
|
|
710
|
+
& (pl.col(self._x_column) <= x1)
|
|
711
|
+
& (pl.col(self._y_column) >= y0)
|
|
712
|
+
& (pl.col(self._y_column) <= y1)
|
|
637
713
|
)
|
|
638
714
|
|
|
639
715
|
# Apply non-categorical filters if any
|
|
@@ -652,7 +728,10 @@ class Heatmap(BaseComponent):
|
|
|
652
728
|
|
|
653
729
|
count = len(filtered)
|
|
654
730
|
last_filtered = filtered
|
|
655
|
-
print(
|
|
731
|
+
print(
|
|
732
|
+
f"[HEATMAP] Level {level_idx}: {count} pts in zoom range",
|
|
733
|
+
file=sys.stderr,
|
|
734
|
+
)
|
|
656
735
|
|
|
657
736
|
if count >= self._min_points:
|
|
658
737
|
# This level has enough detail
|
|
@@ -712,6 +791,7 @@ class Heatmap(BaseComponent):
|
|
|
712
791
|
Dict with heatmapData (pandas DataFrame) and _hash for change detection
|
|
713
792
|
"""
|
|
714
793
|
import sys
|
|
794
|
+
|
|
715
795
|
zoom = state.get(self._zoom_identifier)
|
|
716
796
|
|
|
717
797
|
# Build columns to select
|
|
@@ -733,7 +813,9 @@ class Heatmap(BaseComponent):
|
|
|
733
813
|
|
|
734
814
|
# Get levels based on current state (may use per-filter levels)
|
|
735
815
|
levels, filtered_raw = self._get_levels_for_state(state)
|
|
736
|
-
level_sizes = [
|
|
816
|
+
level_sizes = [
|
|
817
|
+
len(lvl) if isinstance(lvl, pl.DataFrame) else "?" for lvl in levels
|
|
818
|
+
]
|
|
737
819
|
|
|
738
820
|
# Determine which filters still need to be applied at render time
|
|
739
821
|
# (filters not in categorical_filters need runtime application)
|
|
@@ -747,12 +829,15 @@ class Heatmap(BaseComponent):
|
|
|
747
829
|
# No zoom - use smallest level
|
|
748
830
|
if not levels:
|
|
749
831
|
# No levels available
|
|
750
|
-
print(
|
|
751
|
-
return {
|
|
832
|
+
print("[HEATMAP] No levels available", file=sys.stderr)
|
|
833
|
+
return {"heatmapData": pl.DataFrame().to_pandas(), "_hash": ""}
|
|
752
834
|
|
|
753
835
|
data = levels[0]
|
|
754
|
-
using_cat = self._preprocessed_data.get(
|
|
755
|
-
print(
|
|
836
|
+
using_cat = self._preprocessed_data.get("has_categorical_filters", False)
|
|
837
|
+
print(
|
|
838
|
+
f"[HEATMAP] No zoom → level 0 ({level_sizes[0]} pts), levels={level_sizes}, categorical={using_cat}",
|
|
839
|
+
file=sys.stderr,
|
|
840
|
+
)
|
|
756
841
|
|
|
757
842
|
# Ensure we have a LazyFrame
|
|
758
843
|
if isinstance(data, pl.DataFrame):
|
|
@@ -768,7 +853,9 @@ class Heatmap(BaseComponent):
|
|
|
768
853
|
filter_defaults=self._filter_defaults,
|
|
769
854
|
)
|
|
770
855
|
# Sort by intensity ascending so high-intensity points are drawn on top
|
|
771
|
-
df_pandas = df_pandas.sort_values(self._intensity_column).reset_index(
|
|
856
|
+
df_pandas = df_pandas.sort_values(self._intensity_column).reset_index(
|
|
857
|
+
drop=True
|
|
858
|
+
)
|
|
772
859
|
else:
|
|
773
860
|
# No filters to apply - levels already filtered by categorical filter
|
|
774
861
|
schema_names = data.collect_schema().names()
|
|
@@ -789,13 +876,16 @@ class Heatmap(BaseComponent):
|
|
|
789
876
|
df_polars = df_polars.select(available_cols)
|
|
790
877
|
# Sort by intensity ascending so high-intensity points are drawn on top
|
|
791
878
|
df_polars = df_polars.sort(self._intensity_column)
|
|
792
|
-
print(
|
|
879
|
+
print(
|
|
880
|
+
f"[HEATMAP] Selected {len(df_polars)} pts for zoom, levels={level_sizes}",
|
|
881
|
+
file=sys.stderr,
|
|
882
|
+
)
|
|
793
883
|
data_hash = compute_dataframe_hash(df_polars)
|
|
794
884
|
df_pandas = df_polars.to_pandas()
|
|
795
885
|
|
|
796
886
|
return {
|
|
797
|
-
|
|
798
|
-
|
|
887
|
+
"heatmapData": df_pandas,
|
|
888
|
+
"_hash": data_hash,
|
|
799
889
|
}
|
|
800
890
|
|
|
801
891
|
def _get_component_args(self) -> Dict[str, Any]:
|
|
@@ -806,19 +896,19 @@ class Heatmap(BaseComponent):
|
|
|
806
896
|
Dict with all heatmap configuration for Vue
|
|
807
897
|
"""
|
|
808
898
|
args: Dict[str, Any] = {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
899
|
+
"componentType": self._get_vue_component_name(),
|
|
900
|
+
"xColumn": self._x_column,
|
|
901
|
+
"yColumn": self._y_column,
|
|
902
|
+
"intensityColumn": self._intensity_column,
|
|
903
|
+
"xLabel": self._x_label,
|
|
904
|
+
"yLabel": self._y_label,
|
|
905
|
+
"colorscale": self._colorscale,
|
|
906
|
+
"zoomIdentifier": self._zoom_identifier,
|
|
907
|
+
"interactivity": self._interactivity,
|
|
818
908
|
}
|
|
819
909
|
|
|
820
910
|
if self._title:
|
|
821
|
-
args[
|
|
911
|
+
args["title"] = self._title
|
|
822
912
|
|
|
823
913
|
# Add any extra config options
|
|
824
914
|
args.update(self._config)
|
|
@@ -830,7 +920,7 @@ class Heatmap(BaseComponent):
|
|
|
830
920
|
colorscale: Optional[str] = None,
|
|
831
921
|
x_label: Optional[str] = None,
|
|
832
922
|
y_label: Optional[str] = None,
|
|
833
|
-
) ->
|
|
923
|
+
) -> "Heatmap":
|
|
834
924
|
"""
|
|
835
925
|
Update heatmap styling.
|
|
836
926
|
|