openms-insight 0.1.9__py3-none-any.whl → 0.1.10__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/components/sequenceview.py +20 -0
- openms_insight/components/table.py +158 -31
- openms_insight/rendering/bridge.py +155 -12
- {openms_insight-0.1.9.dist-info → openms_insight-0.1.10.dist-info}/METADATA +1 -1
- {openms_insight-0.1.9.dist-info → openms_insight-0.1.10.dist-info}/RECORD +7 -7
- {openms_insight-0.1.9.dist-info → openms_insight-0.1.10.dist-info}/WHEEL +0 -0
- {openms_insight-0.1.9.dist-info → openms_insight-0.1.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
|
|
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
|
-
#
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
go_to_value =
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
#
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
422
|
-
ann_state_matches =
|
|
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(
|
|
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] = (
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openms-insight
|
|
3
|
-
Version: 0.1.
|
|
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
|
|
@@ -2,8 +2,8 @@ openms_insight/__init__.py,sha256=b4H9k0fPVZ6jCiJ4QVSwzlUQnyElF6ZwAlDI8fdPMJE,11
|
|
|
2
2
|
openms_insight/components/__init__.py,sha256=Lcg-D0FILta-YVgMJBlWMKLKC5G5kXOqdy9hBOENABw,233
|
|
3
3
|
openms_insight/components/heatmap.py,sha256=psrdW4gNzZR1jAIox9YS9EHaZaTRrDHFR0t2_0APU9Y,47214
|
|
4
4
|
openms_insight/components/lineplot.py,sha256=I-JPvDzCr3Nu8Boc1V4D8QQ1bHgTqvM6CbeoIe7zJ-s,30896
|
|
5
|
-
openms_insight/components/sequenceview.py,sha256=
|
|
6
|
-
openms_insight/components/table.py,sha256=
|
|
5
|
+
openms_insight/components/sequenceview.py,sha256=ufvtzuU9zaAybsT3pqfqIR0ZHCl7dKJ-jk3kwJALr54,31241
|
|
6
|
+
openms_insight/components/table.py,sha256=0LiJetxECoGLZqhkUhDXp1_92FYgGeajm4zDTWjfc1U,42146
|
|
7
7
|
openms_insight/components/volcanoplot.py,sha256=F-cmYxJMKXVK-aYJpifp8be7nB8hkQd2kLi9DrBElD8,15155
|
|
8
8
|
openms_insight/core/__init__.py,sha256=EPjKX_FFQRgO8mWHs59I-o0BiuzEMzEU1Pfu9YOfLC4,338
|
|
9
9
|
openms_insight/core/base.py,sha256=h0OaubHLky8mk7Yfy3HTIimsz-DfuNRgLfotJu3pZVw,20517
|
|
@@ -16,7 +16,7 @@ openms_insight/preprocessing/compression.py,sha256=T4YbX9PUlfTfPit_kpuLZn8hYpqLY
|
|
|
16
16
|
openms_insight/preprocessing/filtering.py,sha256=fkmaIXfR5hfjyWfaMYqaeybMHaZjvUZYaKCqvxPOWMQ,14152
|
|
17
17
|
openms_insight/preprocessing/scatter.py,sha256=2ifCNTUKHEW9UVpv4z9c5GaLnz5zw9o1119IenzAe9s,4703
|
|
18
18
|
openms_insight/rendering/__init__.py,sha256=ApHvKeh87yY4GTIEai-tCeIXpNbwOXWlmcmIwMMRZYc,198
|
|
19
|
-
openms_insight/rendering/bridge.py,sha256=
|
|
19
|
+
openms_insight/rendering/bridge.py,sha256=yl8QmeGnoaK2ZM4kf-XQW47o4DZ5S2lnkL0Iu7C5CjE,30331
|
|
20
20
|
openms_insight/js-component/dist/index.html,sha256=LSJ3B_YmGUrCCdZ1UaZO2p6Wqsih6nTH62Z_0uZxpD8,430
|
|
21
21
|
openms_insight/js-component/dist/assets/index.css,sha256=wFvo7FbG132LL7uw0slXfrL_oSQ8u2RKL1DW9hD9-kk,884264
|
|
22
22
|
openms_insight/js-component/dist/assets/index.js,sha256=aqGc3g7XLTRr7ptEgoA3XDu5oMS47yxxjUBXgansIo0,6091480
|
|
@@ -24,7 +24,7 @@ openms_insight/js-component/dist/assets/materialdesignicons-webfont.eot,sha256=C
|
|
|
24
24
|
openms_insight/js-component/dist/assets/materialdesignicons-webfont.ttf,sha256=YeirpaTpgf4iz3yOi82-oAR251xiw38Bv37jM2HWhCg,1307660
|
|
25
25
|
openms_insight/js-component/dist/assets/materialdesignicons-webfont.woff,sha256=pZKKDVwvYk5G-Y2bFcL2AEU3f3xZTdeKF1kTLqO0Y-s,587984
|
|
26
26
|
openms_insight/js-component/dist/assets/materialdesignicons-webfont.woff2,sha256=Zi_vqPL4qVwYWI0hd0eJwQfGTnccvmWmmvRikcQxGvw,403216
|
|
27
|
-
openms_insight-0.1.
|
|
28
|
-
openms_insight-0.1.
|
|
29
|
-
openms_insight-0.1.
|
|
30
|
-
openms_insight-0.1.
|
|
27
|
+
openms_insight-0.1.10.dist-info/METADATA,sha256=diBvV5CHNzaJ8RsS4b24ywYnnFFSM_TMkYkLuEpAUnc,16788
|
|
28
|
+
openms_insight-0.1.10.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
29
|
+
openms_insight-0.1.10.dist-info/licenses/LICENSE,sha256=INFF4rOMmpah7Oi14hLqu7NTOsx56KRRNChAAUcfh2E,1823
|
|
30
|
+
openms_insight-0.1.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|