openms-insight 0.1.9__tar.gz → 0.1.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {openms_insight-0.1.9 → openms_insight-0.1.10}/PKG-INFO +1 -1
  2. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/components/sequenceview.py +20 -0
  3. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/components/table.py +158 -31
  4. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/rendering/bridge.py +155 -12
  5. {openms_insight-0.1.9 → openms_insight-0.1.10}/pyproject.toml +1 -1
  6. {openms_insight-0.1.9 → openms_insight-0.1.10}/.gitignore +0 -0
  7. {openms_insight-0.1.9 → openms_insight-0.1.10}/LICENSE +0 -0
  8. {openms_insight-0.1.9 → openms_insight-0.1.10}/README.md +0 -0
  9. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/__init__.py +0 -0
  10. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/components/__init__.py +0 -0
  11. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/components/heatmap.py +0 -0
  12. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/components/lineplot.py +0 -0
  13. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/components/volcanoplot.py +0 -0
  14. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/core/__init__.py +0 -0
  15. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/core/base.py +0 -0
  16. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/core/cache.py +0 -0
  17. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/core/registry.py +0 -0
  18. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/core/state.py +0 -0
  19. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/core/subprocess_preprocess.py +0 -0
  20. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/js-component/dist/assets/index.css +0 -0
  21. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/js-component/dist/assets/index.js +0 -0
  22. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/js-component/dist/assets/materialdesignicons-webfont.eot +0 -0
  23. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/js-component/dist/assets/materialdesignicons-webfont.ttf +0 -0
  24. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/js-component/dist/assets/materialdesignicons-webfont.woff +0 -0
  25. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/js-component/dist/assets/materialdesignicons-webfont.woff2 +0 -0
  26. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/js-component/dist/index.html +0 -0
  27. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/preprocessing/__init__.py +0 -0
  28. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/preprocessing/compression.py +0 -0
  29. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/preprocessing/filtering.py +0 -0
  30. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/preprocessing/scatter.py +0 -0
  31. {openms_insight-0.1.9 → openms_insight-0.1.10}/openms_insight/rendering/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openms-insight
3
- Version: 0.1.9
3
+ Version: 0.1.10
4
4
  Summary: Interactive visualization components for mass spectrometry data in Streamlit
5
5
  Project-URL: Homepage, https://github.com/t0mdavid-m/OpenMS-Insight
6
6
  Project-URL: Documentation, https://github.com/t0mdavid-m/OpenMS-Insight#readme
@@ -440,6 +440,9 @@ class SequenceView:
440
440
  self._deconvolved = deconvolved
441
441
  self._config = kwargs
442
442
  self._filters = filters or {}
443
+ self._filter_defaults = {}
444
+ for identifier in self._filters.keys():
445
+ self._filter_defaults[identifier] = None
443
446
  self._interactivity = interactivity or {}
444
447
 
445
448
  # Store annotation config with defaults
@@ -534,6 +537,9 @@ class SequenceView:
534
537
 
535
538
  # Restore all configuration
536
539
  self._filters = config.get("filters", {})
540
+ self._filter_defaults = {}
541
+ for identifier in self._filters.keys():
542
+ self._filter_defaults[identifier] = None
537
543
  self._interactivity = config.get("interactivity", {})
538
544
  self._title = config.get("title")
539
545
  self._height = config.get("height", 400)
@@ -650,6 +656,12 @@ class SequenceView:
650
656
  filter_value = state.get(identifier)
651
657
  if filter_value is not None:
652
658
  filtered = filtered.filter(pl.col(column) == filter_value)
659
+ elif (
660
+ identifier in self._filter_defaults
661
+ and self._filter_defaults[identifier] is None
662
+ ):
663
+ # Filter has None default and state is None - return empty intentionally
664
+ return "", 1
653
665
 
654
666
  # Collect and get first row
655
667
  try:
@@ -681,6 +693,14 @@ class SequenceView:
681
693
  filter_value = state.get(identifier)
682
694
  if filter_value is not None:
683
695
  filtered = filtered.filter(pl.col(column) == filter_value)
696
+ elif (
697
+ identifier in self._filter_defaults
698
+ and self._filter_defaults[identifier] is None
699
+ ):
700
+ # Filter has None default and state is None - return empty intentionally
701
+ return pl.DataFrame(
702
+ schema={"peak_id": pl.Int64, "mass": pl.Float64}
703
+ )
684
704
 
685
705
  # Select available columns
686
706
  cols = ["peak_id", "mass"]
@@ -1,6 +1,7 @@
1
1
  """Table component using Tabulator.js."""
2
2
 
3
3
  import logging
4
+ import re
4
5
  from typing import Any, Dict, List, Optional
5
6
 
6
7
  import polars as pl
@@ -11,6 +12,20 @@ from ..preprocessing.filtering import compute_dataframe_hash
11
12
 
12
13
  logger = logging.getLogger(__name__)
13
14
 
15
+ # Numeric data types for dtype checking
16
+ NUMERIC_DTYPES = (
17
+ pl.Int8,
18
+ pl.Int16,
19
+ pl.Int32,
20
+ pl.Int64,
21
+ pl.UInt8,
22
+ pl.UInt16,
23
+ pl.UInt32,
24
+ pl.UInt64,
25
+ pl.Float32,
26
+ pl.Float64,
27
+ )
28
+
14
29
  # Session state key for tracking last rendered selection per table component
15
30
  _LAST_SELECTION_KEY = "_svc_table_last_selection"
16
31
  # Session state key for tracking last sort/filter state per table component
@@ -419,10 +434,71 @@ class Table(BaseComponent):
419
434
 
420
435
  self._preprocessed_data["column_metadata"] = column_metadata
421
436
 
437
+ # Auto-detect go-to fields if not explicitly provided
438
+ if self._go_to_fields is None:
439
+ self._go_to_fields = self._auto_detect_go_to_fields(data)
440
+ elif self._go_to_fields == []:
441
+ # Explicitly disabled - keep empty list
442
+ pass
443
+ # else: use user-provided list as-is
444
+
422
445
  # Store LazyFrame for streaming to disk (filter happens at render time)
423
446
  # Base class will use sink_parquet() to stream without full materialization
424
447
  self._preprocessed_data["data"] = data # Keep lazy
425
448
 
449
+ def _auto_detect_go_to_fields(self, data: pl.LazyFrame) -> List[str]:
450
+ """
451
+ Auto-detect columns suitable for go-to navigation.
452
+
453
+ Criteria:
454
+ - Integer or String (Utf8) type only (excludes Float)
455
+ - 100% unique values (no duplicates)
456
+ - Samples first 10,000 rows for performance
457
+
458
+ Args:
459
+ data: LazyFrame to analyze for unique columns
460
+
461
+ Returns:
462
+ List of column names in original schema order
463
+ """
464
+ schema = data.collect_schema()
465
+ sample = data.head(10000)
466
+
467
+ candidates = []
468
+ for col_name in schema.names():
469
+ dtype = schema[col_name]
470
+
471
+ # Only Integer and String types (exclude Float)
472
+ if dtype not in (
473
+ pl.Int8,
474
+ pl.Int16,
475
+ pl.Int32,
476
+ pl.Int64,
477
+ pl.UInt8,
478
+ pl.UInt16,
479
+ pl.UInt32,
480
+ pl.UInt64,
481
+ pl.Utf8,
482
+ ):
483
+ continue
484
+
485
+ # Check 100% uniqueness in sample
486
+ stats = sample.select(
487
+ [
488
+ pl.col(col_name).len().alias("count"),
489
+ pl.col(col_name).n_unique().alias("n_unique"),
490
+ ]
491
+ ).collect()
492
+
493
+ count = stats["count"][0]
494
+ n_unique = stats["n_unique"][0]
495
+
496
+ # Must be 100% unique (count == n_unique)
497
+ if count > 0 and count == n_unique:
498
+ candidates.append(col_name)
499
+
500
+ return candidates
501
+
426
502
  def _get_columns_to_select(self) -> Optional[List[str]]:
427
503
  """Get list of columns needed for this table."""
428
504
  if not self._column_definitions:
@@ -527,6 +603,7 @@ class Table(BaseComponent):
527
603
  "total_rows": 0,
528
604
  "total_pages": 0,
529
605
  },
606
+ "_auto_selection": {}, # No data = no auto-selection
530
607
  }
531
608
 
532
609
  # Convert float to int for integer columns (JS numbers come as floats)
@@ -563,8 +640,13 @@ class Table(BaseComponent):
563
640
  elif filter_type == "<=":
564
641
  data = data.filter(pl.col(field) <= value)
565
642
  elif filter_type == "regex":
566
- # Text search with regex
567
- data = data.filter(pl.col(field).str.contains(value, literal=False))
643
+ # Text search with regex - invalid patterns match nothing
644
+ try:
645
+ re.compile(value)
646
+ data = data.filter(pl.col(field).str.contains(value, literal=False))
647
+ except re.error:
648
+ # Invalid regex pattern - filter to empty result
649
+ data = data.filter(pl.lit(False))
568
650
 
569
651
  # Apply server-side sort
570
652
  if sort_column:
@@ -578,34 +660,44 @@ class Table(BaseComponent):
578
660
  # Handle go-to request (server-side search for row by field value)
579
661
  navigate_to_page = None
580
662
  target_row_index = None
663
+ go_to_not_found = False
581
664
 
582
665
  if go_to_request:
583
666
  go_to_field = go_to_request.get("field")
584
667
  go_to_value = go_to_request.get("value")
585
668
  if go_to_field and go_to_value is not None:
586
- # Try to convert to number if applicable
587
- try:
588
- go_to_value = float(go_to_value)
589
- if go_to_value.is_integer():
590
- go_to_value = int(go_to_value)
591
- except (ValueError, TypeError):
592
- pass
593
-
594
- # Find the row with row_number
595
- search_result = (
596
- data.with_row_index("_row_num")
597
- .filter(pl.col(go_to_field) == go_to_value)
598
- .select("_row_num")
599
- .head(1)
600
- .collect()
601
- )
602
-
603
- if len(search_result) > 0:
604
- row_num = search_result["_row_num"][0]
605
- target_page = (row_num // page_size) + 1
606
- navigate_to_page = target_page
607
- target_row_index = row_num % page_size
608
- page = target_page # Jump to target page
669
+ # Only convert to numeric if the target column is numeric
670
+ schema = data.collect_schema()
671
+ if go_to_field in schema and schema[go_to_field] in NUMERIC_DTYPES:
672
+ try:
673
+ go_to_value = float(go_to_value)
674
+ if go_to_value.is_integer():
675
+ go_to_value = int(go_to_value)
676
+ except (ValueError, TypeError):
677
+ # Non-numeric string for numeric column - mark as not found
678
+ go_to_not_found = True
679
+ # If column is string (Utf8), keep go_to_value as-is
680
+
681
+ # Only search if we have a valid value (not already marked as not found)
682
+ if not go_to_not_found:
683
+ # Find the row with row_number
684
+ search_result = (
685
+ data.with_row_index("_row_num")
686
+ .filter(pl.col(go_to_field) == go_to_value)
687
+ .select("_row_num")
688
+ .head(1)
689
+ .collect()
690
+ )
691
+
692
+ if len(search_result) > 0:
693
+ row_num = search_result["_row_num"][0]
694
+ target_page = (row_num // page_size) + 1
695
+ navigate_to_page = target_page
696
+ target_row_index = row_num % page_size
697
+ page = target_page # Jump to target page
698
+ else:
699
+ # Row not found - set flag for Vue to show "not found" feedback
700
+ go_to_not_found = True
609
701
 
610
702
  # === Selection and Sort/Filter based navigation ===
611
703
  # PURPOSE: When user sorts/filters, find where the selected row ended up and navigate there
@@ -669,12 +761,28 @@ class Table(BaseComponent):
669
761
  for identifier, column in self._interactivity.items():
670
762
  selected_value = state.get(identifier)
671
763
  if selected_value is not None:
672
- # Convert float to int if needed (JS numbers come as floats)
673
- if (
674
- isinstance(selected_value, float)
675
- and selected_value.is_integer()
676
- ):
677
- selected_value = int(selected_value)
764
+ # Type conversion based on column dtype (same logic as go-to)
765
+ schema = data.collect_schema()
766
+ if column in schema:
767
+ col_dtype = schema[column]
768
+ if col_dtype in NUMERIC_DTYPES:
769
+ # Column is numeric - convert value to numeric if possible
770
+ if isinstance(selected_value, str):
771
+ try:
772
+ selected_value = float(selected_value)
773
+ if selected_value.is_integer():
774
+ selected_value = int(selected_value)
775
+ except (ValueError, TypeError):
776
+ pass
777
+ elif (
778
+ isinstance(selected_value, float)
779
+ and selected_value.is_integer()
780
+ ):
781
+ selected_value = int(selected_value)
782
+ else:
783
+ # Column is string - convert value to string
784
+ if not isinstance(selected_value, str):
785
+ selected_value = str(selected_value)
678
786
 
679
787
  # SEARCH for the selected row in the sorted/filtered data
680
788
  # with_row_index adds position so we know which page it's on
@@ -742,6 +850,22 @@ class Table(BaseComponent):
742
850
  # Clamp page to valid range
743
851
  page = max(1, min(page, total_pages))
744
852
 
853
+ # Compute auto-selection from first row (before pagination)
854
+ # This provides the first row's values for interactivity columns
855
+ # so downstream components can receive initial data when filters change
856
+ auto_selection: Dict[str, Any] = {}
857
+ if self._interactivity and total_rows > 0:
858
+ # Get the first row of sorted/filtered data
859
+ first_row = data.head(1).collect()
860
+ if first_row.height > 0:
861
+ for identifier, column in self._interactivity.items():
862
+ if column in first_row.columns:
863
+ value = first_row[column][0]
864
+ # Convert numpy/polars types to Python types for JSON
865
+ if hasattr(value, "item"):
866
+ value = value.item()
867
+ auto_selection[identifier] = value
868
+
745
869
  # Slice to current page
746
870
  offset = (page - 1) * page_size
747
871
  df_polars = data.slice(offset, page_size).collect()
@@ -761,12 +885,15 @@ class Table(BaseComponent):
761
885
  "sort_column": sort_column,
762
886
  "sort_dir": sort_dir,
763
887
  },
888
+ "_auto_selection": auto_selection,
764
889
  }
765
890
 
766
891
  if navigate_to_page is not None:
767
892
  result["_navigate_to_page"] = navigate_to_page
768
893
  if target_row_index is not None:
769
894
  result["_target_row_index"] = target_row_index
895
+ if go_to_not_found:
896
+ result["_go_to_not_found"] = True
770
897
 
771
898
  logger.info(
772
899
  f"[Table._prepare_vue_data] Returning: page={page}, total_rows={total_rows}, data_rows={len(df_polars)}"
@@ -16,6 +16,20 @@ _DEBUG_HASH_TRACKING = os.environ.get("SVC_DEBUG_HASH", "").lower() == "true"
16
16
  _DEBUG_STATE_SYNC = os.environ.get("SVC_DEBUG_STATE", "").lower() == "true"
17
17
  _logger = logging.getLogger(__name__)
18
18
 
19
+ # Numeric data types for type conversion during selection validation
20
+ NUMERIC_DTYPES = (
21
+ pl.Int8,
22
+ pl.Int16,
23
+ pl.Int32,
24
+ pl.Int64,
25
+ pl.UInt8,
26
+ pl.UInt16,
27
+ pl.UInt32,
28
+ pl.UInt64,
29
+ pl.Float32,
30
+ pl.Float64,
31
+ )
32
+
19
33
 
20
34
  def _make_hashable(value: Any) -> Any:
21
35
  """
@@ -237,9 +251,7 @@ def _prepare_vue_data_cached(
237
251
 
238
252
  if _DEBUG_HASH_TRACKING:
239
253
  cache_hit = cached is not None
240
- _logger.warning(
241
- f"[CacheDebug] {component._cache_id}: cache_hit={cache_hit}"
242
- )
254
+ _logger.warning(f"[CacheDebug] {component._cache_id}: cache_hit={cache_hit}")
243
255
 
244
256
  if cached is not None:
245
257
  cached_data, cached_hash, _ = cached # Ignore cached annotation hash here
@@ -276,7 +288,9 @@ def _prepare_vue_data_cached(
276
288
  # Fallback: store without _plotConfig (may have stale column refs)
277
289
  base_data = {k: v for k, v in vue_data.items() if k != "_plotConfig"}
278
290
  base_hash = _hash_data(base_data)
279
- _set_cached_vue_data(component_id, filter_state_hashable, base_data, base_hash, ann_hash)
291
+ _set_cached_vue_data(
292
+ component_id, filter_state_hashable, base_data, base_hash, ann_hash
293
+ )
280
294
 
281
295
  # Return full data with annotations
282
296
  data_hash = _hash_data(vue_data)
@@ -284,7 +298,9 @@ def _prepare_vue_data_cached(
284
298
  else:
285
299
  # Store complete data in cache
286
300
  data_hash = _hash_data(vue_data)
287
- _set_cached_vue_data(component_id, filter_state_hashable, vue_data, data_hash, ann_hash)
301
+ _set_cached_vue_data(
302
+ component_id, filter_state_hashable, vue_data, data_hash, ann_hash
303
+ )
288
304
  return vue_data, data_hash
289
305
 
290
306
 
@@ -330,6 +346,101 @@ def get_vue_component_function():
330
346
  return _vue_component_func
331
347
 
332
348
 
349
+ def _validate_interactivity_selections(
350
+ component: "BaseComponent",
351
+ state_manager: "StateManager",
352
+ state: Dict[str, Any],
353
+ ) -> bool:
354
+ """
355
+ Validate that interactivity selections still exist in filtered data.
356
+
357
+ When an external filter changes, the currently selected value may no longer
358
+ exist in the filtered dataset. This function checks each interactivity
359
+ selection and clears it if invalid.
360
+
361
+ Args:
362
+ component: The component to validate selections for
363
+ state_manager: StateManager to clear invalid selections
364
+ state: Current state dict (already has filter values)
365
+
366
+ Returns:
367
+ True if any selection was cleared (state changed), False otherwise
368
+ """
369
+ interactivity = getattr(component, "_interactivity", None)
370
+ if not interactivity:
371
+ return False
372
+
373
+ filters = getattr(component, "_filters", None) or {}
374
+ filter_defaults = getattr(component, "_filter_defaults", None) or {}
375
+
376
+ # Get the preprocessed data
377
+ preprocessed = getattr(component, "_preprocessed_data", None)
378
+ if preprocessed is not None:
379
+ data = preprocessed.get("data")
380
+ else:
381
+ data = None
382
+ if data is None:
383
+ data = getattr(component, "_raw_data", None)
384
+ if data is None:
385
+ # Component doesn't have data we can validate against
386
+ return False
387
+
388
+ # Ensure LazyFrame
389
+ if isinstance(data, pl.DataFrame):
390
+ data = data.lazy()
391
+
392
+ # Apply filters to get the filtered dataset
393
+ for identifier, column in filters.items():
394
+ selected_value = state.get(identifier)
395
+ if selected_value is None and identifier in filter_defaults:
396
+ selected_value = filter_defaults[identifier]
397
+
398
+ if selected_value is None:
399
+ # Awaiting filter - no data to validate against
400
+ return False
401
+
402
+ # Convert float to int for integer columns (type mismatch handling)
403
+ if isinstance(selected_value, float) and selected_value.is_integer():
404
+ selected_value = int(selected_value)
405
+
406
+ data = data.filter(pl.col(column) == selected_value)
407
+
408
+ # Validate each interactivity selection against filtered data
409
+ state_changed = False
410
+ schema = data.collect_schema()
411
+
412
+ for identifier, column in interactivity.items():
413
+ selected_value = state.get(identifier)
414
+ if selected_value is None:
415
+ continue # No selection to validate
416
+
417
+ if column not in schema.names():
418
+ continue # Column doesn't exist
419
+
420
+ # Type conversion for numeric columns
421
+ col_dtype = schema[column]
422
+ if col_dtype in NUMERIC_DTYPES:
423
+ if isinstance(selected_value, float) and selected_value.is_integer():
424
+ selected_value = int(selected_value)
425
+
426
+ # Check if value exists in filtered data (efficient: only fetch 1 row)
427
+ exists = (
428
+ data.filter(pl.col(column) == selected_value).head(1).collect().height > 0
429
+ )
430
+
431
+ if not exists:
432
+ # Selection is invalid - clear it
433
+ state_manager.set_selection(identifier, None)
434
+ state_changed = True
435
+ if _DEBUG_STATE_SYNC:
436
+ _logger.warning(
437
+ f"[Bridge:{component._cache_id}] Cleared invalid selection: "
438
+ f"{identifier}={selected_value} not in filtered data"
439
+ )
440
+
441
+ return state_changed
442
+
443
+
333
444
  def render_component(
334
445
  component: "BaseComponent",
335
446
  state_manager: "StateManager",
@@ -418,8 +529,8 @@ def render_component(
418
529
  cached_ann_hash = None
419
530
 
420
531
  # Cache valid only if BOTH filter state AND annotation state match
421
- filter_state_matches = (cached_state == current_filter_state)
422
- ann_state_matches = (cached_ann_hash == current_ann_hash)
532
+ filter_state_matches = cached_state == current_filter_state
533
+ ann_state_matches = cached_ann_hash == current_ann_hash
423
534
  cache_valid = filter_state_matches and ann_state_matches
424
535
 
425
536
  if _DEBUG_STATE_SYNC:
@@ -522,6 +633,15 @@ def render_component(
522
633
  awaiting_filter = True
523
634
  break
524
635
 
636
+ # === Validate interactivity selections BEFORE preparing data ===
637
+ # When filter changes, the current selection may no longer exist
638
+ # in the filtered data. Clear it so auto-selection can kick in.
639
+ if not awaiting_filter:
640
+ if _validate_interactivity_selections(component, state_manager, state):
641
+ state_changed = True
642
+ # Refresh state after clearing invalid selections
643
+ state = state_manager.get_state_for_vue()
644
+
525
645
  if not awaiting_filter:
526
646
  # Extract state keys that affect this component's data
527
647
  state_keys = set(component.get_state_dependencies())
@@ -548,6 +668,14 @@ def render_component(
548
668
  component, component_id, filter_state_hashable, relevant_state
549
669
  )
550
670
 
671
+ # Apply auto-selection from first row (for tables with interactivity)
672
+ # This ensures downstream components receive data when a filter changes
673
+ auto_selection = vue_data.pop("_auto_selection", {})
674
+ for identifier, value in auto_selection.items():
675
+ if state_manager.get_selection(identifier) is None:
676
+ state_manager.set_selection(identifier, value)
677
+ state_changed = True
678
+
551
679
  # Check if Python overrode state during _prepare_vue_data
552
680
  # (e.g., table.py sets page to last page after sort)
553
681
  final_state = state_manager.get_state_for_vue()
@@ -562,7 +690,9 @@ def render_component(
562
690
  data_hash = "awaiting_filter"
563
691
  filter_state_hashable = ()
564
692
 
565
- _logger.info(f"[bridge] Phase4: {component._cache_id} prepared data, hash={data_hash[:8] if data_hash else 'None'}")
693
+ _logger.info(
694
+ f"[bridge] Phase4: {component._cache_id} prepared data, hash={data_hash[:8] if data_hash else 'None'}"
695
+ )
566
696
 
567
697
  # === PHASE 5: Cache data for next render ===
568
698
  if vue_data:
@@ -579,7 +709,12 @@ def render_component(
579
709
  converted_data[data_key] = value
580
710
 
581
711
  # Store in cache for next render (include annotation hash for validity check)
582
- cache[component_id] = (filter_state_hashable, converted_data, data_hash, current_ann_hash)
712
+ cache[component_id] = (
713
+ filter_state_hashable,
714
+ converted_data,
715
+ data_hash,
716
+ current_ann_hash,
717
+ )
583
718
 
584
719
  # If cache was invalid at Phase 1, we didn't send data to Vue (dataChanged=False).
585
720
  # Trigger a rerun so the newly cached data gets sent on the next render.
@@ -595,9 +730,13 @@ def render_component(
595
730
 
596
731
  if _DEBUG_STATE_SYNC:
597
732
  # Log what we're caching for debugging
598
- pagination_key = next((k for k, v in filter_state_hashable if "page" in k.lower()), None)
733
+ pagination_key = next(
734
+ (k for k, v in filter_state_hashable if "page" in k.lower()), None
735
+ )
599
736
  if pagination_key:
600
- pagination_val = next((v for k, v in filter_state_hashable if k == pagination_key), None)
737
+ pagination_val = next(
738
+ (v for k, v in filter_state_hashable if k == pagination_key), None
739
+ )
601
740
  _logger.warning(
602
741
  f"[Bridge:{component._cache_id}] Phase5: Cached data with hash={data_hash[:8]}, "
603
742
  f"filter_state includes {pagination_key}={pagination_val}"
@@ -664,7 +803,11 @@ def _hash_data(data: Dict[str, Any]) -> str:
664
803
  hash_parts = []
665
804
  for key, value in sorted(data.items()):
666
805
  # Skip internal metadata but NOT dynamic annotation columns or pagination
667
- if key.startswith("_") and not key.startswith("_dynamic") and not key.startswith("_pagination"):
806
+ if (
807
+ key.startswith("_")
808
+ and not key.startswith("_dynamic")
809
+ and not key.startswith("_pagination")
810
+ ):
668
811
  continue
669
812
  if isinstance(value, pd.DataFrame):
670
813
  # Efficient hash for DataFrames
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openms-insight"
7
- version = "0.1.9"
7
+ version = "0.1.10"
8
8
  description = "Interactive visualization components for mass spectrometry data in Streamlit"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
File without changes