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.
@@ -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 .components.table import Table
14
- from .components.lineplot import LinePlot
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
  ]
@@ -1,8 +1,8 @@
1
1
  """Visualization components."""
2
2
 
3
- from .table import Table
4
- from .lineplot import LinePlot
5
3
  from .heatmap import Heatmap
4
+ from .lineplot import LinePlot
5
+ from .table import Table
6
6
 
7
7
  __all__ = [
8
8
  "Table",
@@ -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
- ('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]),
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 = 'intensity',
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 = 'heatmap_zoom',
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 = 'Portland',
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
- '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),
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['x_range'] = x_range
246
- self._preprocessed_data['y_range'] = y_range
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['total'] = total
274
+ self._preprocessed_data["total"] = total
251
275
 
252
276
  # Store metadata about categorical filters
253
- self._preprocessed_data['has_categorical_filters'] = True
254
- self._preprocessed_data['categorical_filter_values'] = {}
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(f"[HEATMAP] Warning: categorical_filter '{filter_id}' not in filters, skipping", file=sys.stderr)
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([v for v in unique_values if v is not None and v >= 0])
299
+ unique_values = sorted(
300
+ [v for v in unique_values if v is not None and v >= 0]
301
+ )
274
302
 
275
- print(f"[HEATMAP] Categorical filter '{filter_id}' ({column_name}): {len(unique_values)} unique values", file=sys.stderr)
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['categorical_filter_values'][filter_id] = unique_values
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(pl.col(column_name) == filter_value)
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(self._min_points, filtered_total)
321
+ level_sizes = compute_compression_levels(
322
+ self._min_points, filtered_total
323
+ )
287
324
 
288
- print(f"[HEATMAP] Value {filter_value}: {filtered_total:,} pts → levels {level_sizes}", file=sys.stderr)
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[f'cat_level_sizes_{filter_id}_{filter_value}'] = level_sizes
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'cat_level_{filter_id}_{filter_value}_{level_idx}'
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'cat_level_{filter_id}_{filter_value}_{num_compressed}'
324
- self._preprocessed_data[full_res_key] = filtered_data
325
- self._preprocessed_data[f'cat_num_levels_{filter_id}_{filter_value}'] = num_compressed + 1
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['level_sizes'] = level_sizes
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
- self._preprocessed_data[f'level_{i}'] = level # Keep lazy
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'level_{num_compressed}'] = self._raw_data
359
- self._preprocessed_data['num_levels'] = num_compressed + 1
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['x_range'] = x_range
374
- self._preprocessed_data['y_range'] = y_range
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['total'] = total
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['level_sizes'] = level_sizes
437
+ self._preprocessed_data["level_sizes"] = level_sizes
383
438
 
384
439
  # Build and collect each level
385
- self._preprocessed_data['levels'] = []
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'level_{i}'] = level # Keep lazy
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'level_{num_compressed}'] = self._raw_data
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['num_levels'] = num_compressed + 1
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['x_range'] = x_range
434
- self._preprocessed_data['y_range'] = y_range
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['total'] = total
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['level_sizes'] = level_sizes
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'level_{level_idx}'] = downsampled # Keep lazy
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'level_{level_idx}'] = downsampled.lazy()
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'level_{num_compressed}'] = self._raw_data
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['num_levels'] = num_compressed + 1
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('num_levels', 0)
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'level_{i}')
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'cat_num_levels_{filter_id}_{filter_value}'
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'cat_level_{filter_id}_{filter_value}_{i}'
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(self, state: Dict[str, Any]) -> Tuple[list, Optional[pl.LazyFrame]]:
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('has_categorical_filters'):
549
- cat_filter_values = self._preprocessed_data.get('categorical_filter_values', {})
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(filter_id, selected_value)
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 'PlotlyHeatmap'
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 'heatmapData'
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('xRange', [-1, -1])
585
- y_range = zoom.get('yRange', [-1, -1])
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
- x0, x1 = zoom['xRange']
617
- y0, y1 = zoom['yRange']
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(f"[HEATMAP] Level {level_idx}: {count} pts in zoom range", file=sys.stderr)
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 = [len(l) if isinstance(l, pl.DataFrame) else '?' for l in levels]
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(f"[HEATMAP] No levels available", file=sys.stderr)
751
- return {'heatmapData': pl.DataFrame().to_pandas(), '_hash': ''}
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('has_categorical_filters', False)
755
- print(f"[HEATMAP] No zoom → level 0 ({level_sizes[0]} pts), levels={level_sizes}, categorical={using_cat}", file=sys.stderr)
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(drop=True)
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(f"[HEATMAP] Selected {len(df_polars)} pts for zoom, levels={level_sizes}", file=sys.stderr)
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
- 'heatmapData': df_pandas,
798
- '_hash': data_hash,
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
- 'componentType': self._get_vue_component_name(),
810
- 'xColumn': self._x_column,
811
- 'yColumn': self._y_column,
812
- 'intensityColumn': self._intensity_column,
813
- 'xLabel': self._x_label,
814
- 'yLabel': self._y_label,
815
- 'colorscale': self._colorscale,
816
- 'zoomIdentifier': self._zoom_identifier,
817
- 'interactivity': self._interactivity,
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['title'] = self._title
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
- ) -> 'Heatmap':
923
+ ) -> "Heatmap":
834
924
  """
835
925
  Update heatmap styling.
836
926