openms-insight 0.1.7__py3-none-any.whl → 0.1.9__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/table.py +490 -28
- openms_insight/core/base.py +20 -0
- openms_insight/core/state.py +142 -24
- openms_insight/js-component/dist/assets/index.js +113 -113
- openms_insight/rendering/bridge.py +274 -157
- {openms_insight-0.1.7.dist-info → openms_insight-0.1.9.dist-info}/METADATA +3 -3
- {openms_insight-0.1.7.dist-info → openms_insight-0.1.9.dist-info}/RECORD +9 -9
- {openms_insight-0.1.7.dist-info → openms_insight-0.1.9.dist-info}/WHEEL +0 -0
- {openms_insight-0.1.7.dist-info → openms_insight-0.1.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,6 +12,8 @@ import streamlit as st
|
|
|
12
12
|
|
|
13
13
|
# Configure debug logging for hash tracking
|
|
14
14
|
_DEBUG_HASH_TRACKING = os.environ.get("SVC_DEBUG_HASH", "").lower() == "true"
|
|
15
|
+
# Debug logging for page navigation / state sync issues
|
|
16
|
+
_DEBUG_STATE_SYNC = os.environ.get("SVC_DEBUG_STATE", "").lower() == "true"
|
|
15
17
|
_logger = logging.getLogger(__name__)
|
|
16
18
|
|
|
17
19
|
|
|
@@ -128,10 +130,27 @@ def clear_component_annotations() -> None:
|
|
|
128
130
|
st.session_state[_COMPONENT_ANNOTATIONS_KEY].clear()
|
|
129
131
|
|
|
130
132
|
|
|
133
|
+
def _compute_annotation_hash(component: "BaseComponent") -> Optional[str]:
|
|
134
|
+
"""
|
|
135
|
+
Compute hash of component's dynamic annotations, if any.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
component: The component to check for annotations
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Short hash string if annotations exist, None otherwise
|
|
142
|
+
"""
|
|
143
|
+
annotations = getattr(component, "_dynamic_annotations", None)
|
|
144
|
+
if annotations is None:
|
|
145
|
+
return None
|
|
146
|
+
# Hash the sorted keys (sufficient for change detection)
|
|
147
|
+
return hashlib.md5(str(sorted(annotations.keys())).encode()).hexdigest()[:8]
|
|
148
|
+
|
|
149
|
+
|
|
131
150
|
def _get_cached_vue_data(
|
|
132
151
|
component_id: str,
|
|
133
152
|
filter_state_hashable: Tuple[Tuple[str, Any], ...],
|
|
134
|
-
) -> Optional[Tuple[Dict[str, Any], str]]:
|
|
153
|
+
) -> Optional[Tuple[Dict[str, Any], str, Optional[str]]]:
|
|
135
154
|
"""
|
|
136
155
|
Get cached Vue data for component if filter state matches.
|
|
137
156
|
|
|
@@ -143,13 +162,19 @@ def _get_cached_vue_data(
|
|
|
143
162
|
filter_state_hashable: Current filter state (for cache validation)
|
|
144
163
|
|
|
145
164
|
Returns:
|
|
146
|
-
Tuple of (vue_data, data_hash) if cache hit, None otherwise
|
|
165
|
+
Tuple of (vue_data, data_hash, annotation_hash) if cache hit, None otherwise
|
|
147
166
|
"""
|
|
148
167
|
cache = _get_component_cache()
|
|
149
168
|
if component_id in cache:
|
|
150
|
-
|
|
169
|
+
entry = cache[component_id]
|
|
170
|
+
# Support both old (3-tuple) and new (4-tuple) format
|
|
171
|
+
if len(entry) == 4:
|
|
172
|
+
cached_state, vue_data, data_hash, ann_hash = entry
|
|
173
|
+
else:
|
|
174
|
+
cached_state, vue_data, data_hash = entry
|
|
175
|
+
ann_hash = None
|
|
151
176
|
if cached_state == filter_state_hashable:
|
|
152
|
-
return (vue_data, data_hash)
|
|
177
|
+
return (vue_data, data_hash, ann_hash)
|
|
153
178
|
return None
|
|
154
179
|
|
|
155
180
|
|
|
@@ -158,6 +183,7 @@ def _set_cached_vue_data(
|
|
|
158
183
|
filter_state_hashable: Tuple[Tuple[str, Any], ...],
|
|
159
184
|
vue_data: Dict[str, Any],
|
|
160
185
|
data_hash: str,
|
|
186
|
+
ann_hash: Optional[str] = None,
|
|
161
187
|
) -> None:
|
|
162
188
|
"""
|
|
163
189
|
Cache Vue data for component, replacing any previous entry.
|
|
@@ -169,9 +195,10 @@ def _set_cached_vue_data(
|
|
|
169
195
|
filter_state_hashable: Current filter state
|
|
170
196
|
vue_data: Data to cache
|
|
171
197
|
data_hash: Hash of the data
|
|
198
|
+
ann_hash: Hash of dynamic annotations (if any)
|
|
172
199
|
"""
|
|
173
200
|
cache = _get_component_cache()
|
|
174
|
-
cache[component_id] = (filter_state_hashable, vue_data, data_hash)
|
|
201
|
+
cache[component_id] = (filter_state_hashable, vue_data, data_hash, ann_hash)
|
|
175
202
|
|
|
176
203
|
|
|
177
204
|
def _prepare_vue_data_cached(
|
|
@@ -208,8 +235,14 @@ def _prepare_vue_data_cached(
|
|
|
208
235
|
# Try cache first (works for ALL components now)
|
|
209
236
|
cached = _get_cached_vue_data(component_id, filter_state_hashable)
|
|
210
237
|
|
|
238
|
+
if _DEBUG_HASH_TRACKING:
|
|
239
|
+
cache_hit = cached is not None
|
|
240
|
+
_logger.warning(
|
|
241
|
+
f"[CacheDebug] {component._cache_id}: cache_hit={cache_hit}"
|
|
242
|
+
)
|
|
243
|
+
|
|
211
244
|
if cached is not None:
|
|
212
|
-
cached_data, cached_hash = cached
|
|
245
|
+
cached_data, cached_hash, _ = cached # Ignore cached annotation hash here
|
|
213
246
|
|
|
214
247
|
if has_dynamic_annotations:
|
|
215
248
|
# Cache hit but need to re-apply annotations (they may have changed)
|
|
@@ -232,6 +265,9 @@ def _prepare_vue_data_cached(
|
|
|
232
265
|
# Cache miss - compute data
|
|
233
266
|
vue_data = component._prepare_vue_data(state_dict)
|
|
234
267
|
|
|
268
|
+
# Compute annotation hash for cache storage
|
|
269
|
+
ann_hash = _compute_annotation_hash(component)
|
|
270
|
+
|
|
235
271
|
if has_dynamic_annotations:
|
|
236
272
|
# Store BASE data (without dynamic annotation columns) in cache
|
|
237
273
|
if hasattr(component, "_strip_dynamic_columns"):
|
|
@@ -240,7 +276,7 @@ def _prepare_vue_data_cached(
|
|
|
240
276
|
# Fallback: store without _plotConfig (may have stale column refs)
|
|
241
277
|
base_data = {k: v for k, v in vue_data.items() if k != "_plotConfig"}
|
|
242
278
|
base_hash = _hash_data(base_data)
|
|
243
|
-
_set_cached_vue_data(component_id, filter_state_hashable, base_data, base_hash)
|
|
279
|
+
_set_cached_vue_data(component_id, filter_state_hashable, base_data, base_hash, ann_hash)
|
|
244
280
|
|
|
245
281
|
# Return full data with annotations
|
|
246
282
|
data_hash = _hash_data(vue_data)
|
|
@@ -248,7 +284,7 @@ def _prepare_vue_data_cached(
|
|
|
248
284
|
else:
|
|
249
285
|
# Store complete data in cache
|
|
250
286
|
data_hash = _hash_data(vue_data)
|
|
251
|
-
_set_cached_vue_data(component_id, filter_state_hashable, vue_data, data_hash)
|
|
287
|
+
_set_cached_vue_data(component_id, filter_state_hashable, vue_data, data_hash, ann_hash)
|
|
252
288
|
return vue_data, data_hash
|
|
253
289
|
|
|
254
290
|
|
|
@@ -303,14 +339,17 @@ def render_component(
|
|
|
303
339
|
"""
|
|
304
340
|
Render a component in Streamlit.
|
|
305
341
|
|
|
306
|
-
This function:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
342
|
+
This function uses a two-phase approach to handle state synchronization:
|
|
343
|
+
|
|
344
|
+
Phase 1: Call vue_func with CACHED data to get Vue's request
|
|
345
|
+
Phase 2: Apply Vue's request, then prepare UPDATED data for next render
|
|
346
|
+
|
|
347
|
+
This order is critical because Vue's request (e.g., page change, sort) is only
|
|
348
|
+
available after calling vue_func(). By calling it first with cached data, we:
|
|
349
|
+
1. Get Vue's request immediately
|
|
350
|
+
2. Apply it to state BEFORE preparing data
|
|
351
|
+
3. Prepare correct data for the next render
|
|
352
|
+
4. Rerun to send the correct data
|
|
314
353
|
|
|
315
354
|
Args:
|
|
316
355
|
component: The component to render
|
|
@@ -321,146 +360,113 @@ def render_component(
|
|
|
321
360
|
Returns:
|
|
322
361
|
The value returned by the Vue component
|
|
323
362
|
"""
|
|
324
|
-
#
|
|
325
|
-
state = state_manager.get_state_for_vue()
|
|
326
|
-
|
|
327
|
-
# Batch resend: if any component requested data in previous run, clear ALL hashes
|
|
328
|
-
# This ensures all components get data in one rerun instead of O(N) reruns
|
|
329
|
-
if st.session_state.get(_BATCH_RESEND_KEY):
|
|
330
|
-
st.session_state[_VUE_ECHOED_HASH_KEY] = {}
|
|
331
|
-
st.session_state.pop(_BATCH_RESEND_KEY, None)
|
|
332
|
-
|
|
333
|
-
# Generate unique key if not provided (needed for cache)
|
|
334
|
-
# Use cache_id instead of id(component) since components are recreated each rerun
|
|
335
|
-
# Use JSON serialization for deterministic key generation (hash() can vary)
|
|
363
|
+
# === PHASE 0: Generate key and get component info ===
|
|
336
364
|
if key is None:
|
|
337
365
|
interactivity_str = json.dumps(component._interactivity or {}, sort_keys=True)
|
|
338
366
|
key = f"svc_{component._cache_id}_{hashlib.md5(interactivity_str.encode()).hexdigest()[:8]}"
|
|
339
367
|
|
|
340
|
-
# Check if component has required filters without values
|
|
341
|
-
# Don't send potentially huge unfiltered datasets - wait for filter selection
|
|
342
|
-
filters = getattr(component, "_filters", None) or {}
|
|
343
|
-
filter_defaults = getattr(component, "_filter_defaults", None) or {}
|
|
344
|
-
|
|
345
|
-
awaiting_filter = False
|
|
346
|
-
if filters:
|
|
347
|
-
# Check each filter - if no value AND no default, we're waiting
|
|
348
|
-
for identifier in filters.keys():
|
|
349
|
-
filter_value = state.get(identifier)
|
|
350
|
-
has_default = identifier in filter_defaults
|
|
351
|
-
if filter_value is None and not has_default:
|
|
352
|
-
awaiting_filter = True
|
|
353
|
-
break
|
|
354
|
-
|
|
355
|
-
# Extract state keys that affect this component's data for cache key
|
|
356
|
-
# This includes filters and any additional dependencies (e.g., zoom for heatmaps)
|
|
357
|
-
# Uses get_state_dependencies() which can be overridden by subclasses
|
|
358
|
-
state_keys = set(component.get_state_dependencies())
|
|
359
|
-
|
|
360
|
-
# Build hashable version for cache key (converts dicts/lists to JSON strings)
|
|
361
|
-
filter_state_hashable = tuple(
|
|
362
|
-
sorted((k, _make_hashable(state.get(k))) for k in state_keys)
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
# Build original state dict for passing to _prepare_vue_data
|
|
366
|
-
# (contains actual values, not JSON strings)
|
|
367
|
-
relevant_state = {k: state.get(k) for k in state_keys}
|
|
368
|
-
|
|
369
|
-
# Build component ID for cache (includes type to avoid collisions)
|
|
370
368
|
component_type = component._get_vue_component_name()
|
|
371
369
|
component_id = f"{component_type}:{key}"
|
|
372
|
-
|
|
373
|
-
# Skip data preparation if awaiting required filter selection
|
|
374
|
-
# This prevents sending huge unfiltered datasets
|
|
375
|
-
if awaiting_filter:
|
|
376
|
-
vue_data = {}
|
|
377
|
-
data_hash = "awaiting_filter"
|
|
378
|
-
else:
|
|
379
|
-
# Get component data using per-component cache
|
|
380
|
-
# Each component stores exactly one entry (current filter state)
|
|
381
|
-
# - Filterless components: filter_state=() always → always cache hit
|
|
382
|
-
# - Filtered components: cache hit when filter values unchanged
|
|
383
|
-
vue_data, data_hash = _prepare_vue_data_cached(
|
|
384
|
-
component, component_id, filter_state_hashable, relevant_state
|
|
385
|
-
)
|
|
386
|
-
|
|
387
370
|
component_args = component._get_component_args()
|
|
371
|
+
if height is not None:
|
|
372
|
+
component_args["height"] = height
|
|
373
|
+
|
|
374
|
+
# Batch resend: if any component requested data in previous run, clear ALL hashes
|
|
375
|
+
if st.session_state.get(_BATCH_RESEND_KEY):
|
|
376
|
+
st.session_state[_VUE_ECHOED_HASH_KEY] = {}
|
|
377
|
+
st.session_state.pop(_BATCH_RESEND_KEY, None)
|
|
388
378
|
|
|
389
379
|
# Initialize hash cache in session state if needed
|
|
390
380
|
if _VUE_ECHOED_HASH_KEY not in st.session_state:
|
|
391
381
|
st.session_state[_VUE_ECHOED_HASH_KEY] = {}
|
|
392
382
|
|
|
393
|
-
#
|
|
394
|
-
|
|
395
|
-
|
|
383
|
+
# === PHASE 1: Get CACHED data from previous render ===
|
|
384
|
+
cache = _get_component_cache()
|
|
385
|
+
cached_entry = cache.get(component_id)
|
|
396
386
|
|
|
397
|
-
# Get
|
|
398
|
-
|
|
399
|
-
vue_echoed_hash = st.session_state[_VUE_ECHOED_HASH_KEY].get(hash_tracking_key)
|
|
387
|
+
# Get current state for initial render (may be stale until we apply Vue's request)
|
|
388
|
+
initial_state = state_manager.get_state_for_vue()
|
|
400
389
|
|
|
401
|
-
#
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
data_changed = (vue_echoed_hash is None) or (vue_echoed_hash != data_hash)
|
|
390
|
+
# Pre-compute initial selections BEFORE Vue renders (for first render only)
|
|
391
|
+
if hasattr(component, "get_initial_selection"):
|
|
392
|
+
initial_selection = component.get_initial_selection(initial_state)
|
|
393
|
+
if initial_selection:
|
|
394
|
+
for identifier, value in initial_selection.items():
|
|
395
|
+
state_manager.set_selection(identifier, value)
|
|
396
|
+
initial_state = state_manager.get_state_for_vue()
|
|
409
397
|
|
|
410
|
-
#
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
f"data_hash={data_hash[:8] if data_hash else 'None'}, "
|
|
417
|
-
f"key={hash_tracking_key[:50]}..."
|
|
418
|
-
)
|
|
398
|
+
# Compute current filter state for cache validity check
|
|
399
|
+
# This tells us what state the component SHOULD have data for
|
|
400
|
+
state_keys = set(component.get_state_dependencies())
|
|
401
|
+
current_filter_state = tuple(
|
|
402
|
+
sorted((k, _make_hashable(initial_state.get(k))) for k in state_keys)
|
|
403
|
+
)
|
|
419
404
|
|
|
420
|
-
#
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
405
|
+
# Check if cached data is VALID for current state
|
|
406
|
+
# KEY FIX: Only send data when cache matches current state
|
|
407
|
+
# - Before: Always sent cached data, even if stale (page 38 when Vue wants page 1)
|
|
408
|
+
# - Now: Only send if cache matches current state AND annotation state matches
|
|
409
|
+
cache_valid = False
|
|
410
|
+
current_ann_hash = _compute_annotation_hash(component)
|
|
411
|
+
|
|
412
|
+
if cached_entry is not None:
|
|
413
|
+
# Support both old (3-tuple) and new (4-tuple) cache format
|
|
414
|
+
if len(cached_entry) == 4:
|
|
415
|
+
cached_state, cached_data, cached_hash, cached_ann_hash = cached_entry
|
|
416
|
+
else:
|
|
417
|
+
cached_state, cached_data, cached_hash = cached_entry
|
|
418
|
+
cached_ann_hash = None
|
|
419
|
+
|
|
420
|
+
# 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)
|
|
423
|
+
cache_valid = filter_state_matches and ann_state_matches
|
|
424
|
+
|
|
425
|
+
if _DEBUG_STATE_SYNC:
|
|
426
|
+
_logger.warning(
|
|
427
|
+
f"[Bridge:{component._cache_id}] Phase1: cache_valid={cache_valid}, "
|
|
428
|
+
f"filter_match={filter_state_matches}, ann_match={ann_state_matches}, "
|
|
429
|
+
f"cached_ann={cached_ann_hash}, current_ann={current_ann_hash}"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Build payload - only send data if cache is valid for current state
|
|
433
|
+
if cache_valid:
|
|
434
|
+
# Cache HIT - send cached data (it's correct for current state)
|
|
437
435
|
data_payload = {
|
|
438
|
-
**
|
|
439
|
-
"selection_store":
|
|
440
|
-
"hash":
|
|
436
|
+
**cached_data,
|
|
437
|
+
"selection_store": initial_state,
|
|
438
|
+
"hash": cached_hash,
|
|
441
439
|
"dataChanged": True,
|
|
442
|
-
"awaitingFilter":
|
|
440
|
+
"awaitingFilter": False,
|
|
443
441
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
442
|
+
if _DEBUG_STATE_SYNC:
|
|
443
|
+
# Log pagination state for debugging
|
|
444
|
+
pagination_key = next((k for k in state_keys if "page" in k.lower()), None)
|
|
445
|
+
if pagination_key:
|
|
446
|
+
_logger.warning(
|
|
447
|
+
f"[Bridge:{component._cache_id}] Phase1: Cache HIT, "
|
|
448
|
+
f"sending data with hash={cached_hash[:8]}, "
|
|
449
|
+
f"pagination={initial_state.get(pagination_key)}"
|
|
450
|
+
)
|
|
447
451
|
else:
|
|
448
|
-
#
|
|
452
|
+
# Cache MISS (no cache, or state mismatch) - don't send stale data
|
|
453
|
+
# Vue will show loading or use its local cache
|
|
449
454
|
data_payload = {
|
|
450
|
-
"selection_store":
|
|
451
|
-
"hash":
|
|
455
|
+
"selection_store": initial_state,
|
|
456
|
+
"hash": "",
|
|
452
457
|
"dataChanged": False,
|
|
453
|
-
"awaitingFilter":
|
|
458
|
+
"awaitingFilter": False,
|
|
454
459
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
460
|
+
if _DEBUG_STATE_SYNC:
|
|
461
|
+
_logger.warning(
|
|
462
|
+
f"[Bridge:{component._cache_id}] Phase1: Cache MISS, "
|
|
463
|
+
f"not sending data (cached_entry={cached_entry is not None})"
|
|
464
|
+
)
|
|
459
465
|
|
|
460
466
|
# Component layout: [[{componentArgs: {...}}]]
|
|
461
467
|
components = [[{"componentArgs": component_args}]]
|
|
462
468
|
|
|
463
|
-
# Call Vue
|
|
469
|
+
# === PHASE 2: Call vue_func to get Vue's request ===
|
|
464
470
|
vue_func = get_vue_component_function()
|
|
465
471
|
|
|
466
472
|
kwargs = {
|
|
@@ -471,39 +477,141 @@ def render_component(
|
|
|
471
477
|
if height is not None:
|
|
472
478
|
kwargs["height"] = height
|
|
473
479
|
|
|
480
|
+
if _DEBUG_STATE_SYNC:
|
|
481
|
+
_logger.warning(
|
|
482
|
+
f"[Bridge:{component._cache_id}] Phase2: Calling vue_func to get request"
|
|
483
|
+
)
|
|
484
|
+
|
|
474
485
|
result = vue_func(**kwargs)
|
|
475
486
|
|
|
476
|
-
#
|
|
487
|
+
# === PHASE 3: Apply Vue's request FIRST ===
|
|
488
|
+
state_changed = False
|
|
477
489
|
if result is not None:
|
|
478
|
-
#
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
490
|
+
# Debug logging: what we received from Vue
|
|
491
|
+
if _DEBUG_STATE_SYNC:
|
|
492
|
+
vue_counter = result.get("counter")
|
|
493
|
+
vue_keys = [k for k in result.keys() if not k.startswith("_")]
|
|
494
|
+
_logger.warning(
|
|
495
|
+
f"[Bridge:{component._cache_id}] Phase3: Received counter={vue_counter}, "
|
|
496
|
+
f"keys={vue_keys}, _requestData={result.get('_requestData', False)}"
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
# Store Vue's echoed hash
|
|
482
500
|
vue_hash = result.get("_vueDataHash")
|
|
483
501
|
if vue_hash is not None:
|
|
484
|
-
st.session_state[_VUE_ECHOED_HASH_KEY][
|
|
485
|
-
|
|
486
|
-
#
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if
|
|
490
|
-
|
|
491
|
-
# This triggers ONE rerun that clears ALL hashes, so all components
|
|
492
|
-
# get data together instead of O(N) separate reruns
|
|
502
|
+
st.session_state[_VUE_ECHOED_HASH_KEY][key] = vue_hash
|
|
503
|
+
|
|
504
|
+
# Apply Vue's state update FIRST - this is the key fix!
|
|
505
|
+
state_changed = state_manager.update_from_vue(result)
|
|
506
|
+
|
|
507
|
+
# Check if Vue is requesting data resend
|
|
508
|
+
if result.get("_requestData", False):
|
|
493
509
|
st.session_state[_BATCH_RESEND_KEY] = True
|
|
494
|
-
# Note: if data_changed=True, we just sent data so requestData is stale
|
|
495
510
|
|
|
496
|
-
|
|
497
|
-
|
|
511
|
+
# === PHASE 4: Get UPDATED state and prepare data ===
|
|
512
|
+
# Now state reflects Vue's request (e.g., new page number after click)
|
|
513
|
+
state = state_manager.get_state_for_vue()
|
|
514
|
+
|
|
515
|
+
# Check if component has required filters without values
|
|
516
|
+
filters = getattr(component, "_filters", None) or {}
|
|
517
|
+
filter_defaults = getattr(component, "_filter_defaults", None) or {}
|
|
518
|
+
|
|
519
|
+
awaiting_filter = False
|
|
520
|
+
for identifier in filters.keys():
|
|
521
|
+
if state.get(identifier) is None and identifier not in filter_defaults:
|
|
522
|
+
awaiting_filter = True
|
|
523
|
+
break
|
|
524
|
+
|
|
525
|
+
if not awaiting_filter:
|
|
526
|
+
# Extract state keys that affect this component's data
|
|
527
|
+
state_keys = set(component.get_state_dependencies())
|
|
528
|
+
relevant_state = {k: state.get(k) for k in state_keys}
|
|
529
|
+
|
|
530
|
+
# Build hashable version for cache key
|
|
531
|
+
filter_state_hashable = tuple(
|
|
532
|
+
sorted((k, _make_hashable(state.get(k))) for k in state_keys)
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
if _DEBUG_HASH_TRACKING:
|
|
536
|
+
_logger.warning(
|
|
537
|
+
f"[CacheKey] {component._cache_id}: state_keys={list(state_keys)}"
|
|
538
|
+
)
|
|
539
|
+
for k in state_keys:
|
|
540
|
+
if "page" in k.lower():
|
|
541
|
+
_logger.warning(
|
|
542
|
+
f"[CacheKey] {component._cache_id}: {k}={state.get(k)}"
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
# Prepare data with UPDATED state (includes Vue's request)
|
|
546
|
+
# This may also call set_selection() to override (e.g., sort resets page)
|
|
547
|
+
vue_data, data_hash = _prepare_vue_data_cached(
|
|
548
|
+
component, component_id, filter_state_hashable, relevant_state
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# Check if Python overrode state during _prepare_vue_data
|
|
552
|
+
# (e.g., table.py sets page to last page after sort)
|
|
553
|
+
final_state = state_manager.get_state_for_vue()
|
|
554
|
+
if final_state != state:
|
|
555
|
+
state_changed = True
|
|
556
|
+
if _DEBUG_STATE_SYNC:
|
|
557
|
+
_logger.warning(
|
|
558
|
+
f"[Bridge:{component._cache_id}] Phase4: Python overrode state"
|
|
559
|
+
)
|
|
560
|
+
else:
|
|
561
|
+
vue_data = {}
|
|
562
|
+
data_hash = "awaiting_filter"
|
|
563
|
+
filter_state_hashable = ()
|
|
564
|
+
|
|
565
|
+
_logger.info(f"[bridge] Phase4: {component._cache_id} prepared data, hash={data_hash[:8] if data_hash else 'None'}")
|
|
566
|
+
|
|
567
|
+
# === PHASE 5: Cache data for next render ===
|
|
568
|
+
if vue_data:
|
|
569
|
+
# Convert for caching (Arrow serialization requires pandas)
|
|
570
|
+
converted_data = {}
|
|
571
|
+
for data_key, value in vue_data.items():
|
|
572
|
+
if data_key == "_hash":
|
|
573
|
+
continue
|
|
574
|
+
if isinstance(value, pl.LazyFrame):
|
|
575
|
+
converted_data[data_key] = value.collect().to_pandas()
|
|
576
|
+
elif isinstance(value, pl.DataFrame):
|
|
577
|
+
converted_data[data_key] = value.to_pandas()
|
|
578
|
+
else:
|
|
579
|
+
converted_data[data_key] = value
|
|
580
|
+
|
|
581
|
+
# 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)
|
|
583
|
+
|
|
584
|
+
# If cache was invalid at Phase 1, we didn't send data to Vue (dataChanged=False).
|
|
585
|
+
# Trigger a rerun so the newly cached data gets sent on the next render.
|
|
586
|
+
# This handles cross-component filter changes where the affected component
|
|
587
|
+
# needs to receive updated data (e.g., new total_rows/total_pages).
|
|
588
|
+
if not cache_valid:
|
|
589
|
+
state_changed = True
|
|
590
|
+
if _DEBUG_STATE_SYNC:
|
|
591
|
+
_logger.warning(
|
|
592
|
+
f"[Bridge:{component._cache_id}] Phase5: Cache was invalid, "
|
|
593
|
+
f"triggering rerun to send newly cached data"
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
if _DEBUG_STATE_SYNC:
|
|
597
|
+
# 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)
|
|
599
|
+
if pagination_key:
|
|
600
|
+
pagination_val = next((v for k, v in filter_state_hashable if k == pagination_key), None)
|
|
601
|
+
_logger.warning(
|
|
602
|
+
f"[Bridge:{component._cache_id}] Phase5: Cached data with hash={data_hash[:8]}, "
|
|
603
|
+
f"filter_state includes {pagination_key}={pagination_val}"
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
# Handle annotations from Vue (e.g., from SequenceView)
|
|
607
|
+
if result is not None:
|
|
498
608
|
annotations = result.get("_annotations")
|
|
499
609
|
annotations_changed = False
|
|
500
610
|
|
|
501
611
|
if annotations is not None:
|
|
502
|
-
# Compute hash of new annotations
|
|
503
612
|
peak_ids = annotations.get("peak_id", [])
|
|
504
613
|
new_hash = hash(tuple(peak_ids)) if peak_ids else 0
|
|
505
614
|
|
|
506
|
-
# Compare with stored hash
|
|
507
615
|
ann_hash_key = f"_svc_ann_hash_{key}"
|
|
508
616
|
old_hash = st.session_state.get(ann_hash_key)
|
|
509
617
|
|
|
@@ -513,18 +621,27 @@ def render_component(
|
|
|
513
621
|
|
|
514
622
|
_store_component_annotations(key, annotations)
|
|
515
623
|
else:
|
|
516
|
-
# Annotations cleared - check if we had annotations before
|
|
517
624
|
ann_hash_key = f"_svc_ann_hash_{key}"
|
|
518
625
|
if st.session_state.get(ann_hash_key) is not None:
|
|
519
626
|
annotations_changed = True
|
|
520
627
|
st.session_state[ann_hash_key] = None
|
|
521
628
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
629
|
+
if annotations_changed:
|
|
630
|
+
state_changed = True
|
|
631
|
+
|
|
632
|
+
# === PHASE 6: Rerun if state changed ===
|
|
633
|
+
# This will send the UPDATED data (now in cache) to Vue
|
|
634
|
+
if state_changed:
|
|
635
|
+
if _DEBUG_STATE_SYNC:
|
|
636
|
+
_logger.warning(
|
|
637
|
+
f"[Bridge:{component._cache_id}] Phase6: RERUN triggered, "
|
|
638
|
+
f"next render will have cache HIT"
|
|
639
|
+
)
|
|
640
|
+
st.rerun()
|
|
641
|
+
elif _DEBUG_STATE_SYNC:
|
|
642
|
+
_logger.warning(
|
|
643
|
+
f"[Bridge:{component._cache_id}] Phase6: No rerun needed, state_changed=False"
|
|
644
|
+
)
|
|
528
645
|
|
|
529
646
|
return result
|
|
530
647
|
|
|
@@ -546,8 +663,8 @@ def _hash_data(data: Dict[str, Any]) -> str:
|
|
|
546
663
|
|
|
547
664
|
hash_parts = []
|
|
548
665
|
for key, value in sorted(data.items()):
|
|
549
|
-
# Skip internal metadata but NOT dynamic annotation columns
|
|
550
|
-
if key.startswith("_") and not key.startswith("_dynamic"):
|
|
666
|
+
# Skip internal metadata but NOT dynamic annotation columns or pagination
|
|
667
|
+
if key.startswith("_") and not key.startswith("_dynamic") and not key.startswith("_pagination"):
|
|
551
668
|
continue
|
|
552
669
|
if isinstance(value, pd.DataFrame):
|
|
553
670
|
# 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.9
|
|
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
|
|
@@ -47,7 +47,7 @@ Interactive visualization components for mass spectrometry data in Streamlit, ba
|
|
|
47
47
|
- **Memory-efficient preprocessing** via subprocess isolation
|
|
48
48
|
- **Automatic disk caching** with config-based invalidation
|
|
49
49
|
- **Cache reconstruction** - components can be restored from cache without re-specifying configuration
|
|
50
|
-
- **Table component** (Tabulator.js) with filtering, sorting, go-to,
|
|
50
|
+
- **Table component** (Tabulator.js) with server-side pagination, filtering, sorting, go-to, CSV export
|
|
51
51
|
- **Line plot component** (Plotly.js) with highlighting, annotations, zoom
|
|
52
52
|
- **Heatmap component** (Plotly scattergl) with multi-resolution downsampling for millions of points
|
|
53
53
|
- **Volcano plot component** for differential expression visualization with significance thresholds
|
|
@@ -167,7 +167,7 @@ Table(
|
|
|
167
167
|
- `index_field`: Column used as unique row identifier (default: 'id')
|
|
168
168
|
- `go_to_fields`: Columns available in "Go to" navigation
|
|
169
169
|
- `initial_sort`: Default sort configuration
|
|
170
|
-
- `pagination`: Enable pagination
|
|
170
|
+
- `pagination`: Enable server-side pagination (default: True). Only the current page of data is sent to the browser, dramatically reducing memory usage for large datasets.
|
|
171
171
|
- `page_size`: Rows per page (default: 100)
|
|
172
172
|
|
|
173
173
|
**Custom formatters:**
|
|
@@ -3,28 +3,28 @@ openms_insight/components/__init__.py,sha256=Lcg-D0FILta-YVgMJBlWMKLKC5G5kXOqdy9
|
|
|
3
3
|
openms_insight/components/heatmap.py,sha256=psrdW4gNzZR1jAIox9YS9EHaZaTRrDHFR0t2_0APU9Y,47214
|
|
4
4
|
openms_insight/components/lineplot.py,sha256=I-JPvDzCr3Nu8Boc1V4D8QQ1bHgTqvM6CbeoIe7zJ-s,30896
|
|
5
5
|
openms_insight/components/sequenceview.py,sha256=0pDOE0xeoc1-85QZNGdNwwoBwXi-5MFfeb9pCcOi6rc,30274
|
|
6
|
-
openms_insight/components/table.py,sha256=
|
|
6
|
+
openms_insight/components/table.py,sha256=6r0SiWTDSJS6AHNCy4jTxByY9aCt4ussCQFKKrEg77U,36955
|
|
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
|
-
openms_insight/core/base.py,sha256=
|
|
9
|
+
openms_insight/core/base.py,sha256=h0OaubHLky8mk7Yfy3HTIimsz-DfuNRgLfotJu3pZVw,20517
|
|
10
10
|
openms_insight/core/cache.py,sha256=3fnPDWjuWUnxazK2XflcUIeRZZPQ3N45kAKYu-xGBKw,1197
|
|
11
11
|
openms_insight/core/registry.py,sha256=Hak80Jqhx0qa4gbd1YolNZnM6xBrS8I4U_X7zC0bQ8Y,2108
|
|
12
|
-
openms_insight/core/state.py,sha256=
|
|
12
|
+
openms_insight/core/state.py,sha256=CMToxxNyGnqxMccwOcn7FwABNTzjjTsgsMrJCZOZc2o,12438
|
|
13
13
|
openms_insight/core/subprocess_preprocess.py,sha256=m9FbAAFy9Do1Exlh-m4Wo-LDwv6yHlEI4klz5OVwemc,3133
|
|
14
14
|
openms_insight/preprocessing/__init__.py,sha256=xoGdhNVrX8Ty3ywmyaCcWAO3a6QlKceO1xxsy1C8ZTI,596
|
|
15
15
|
openms_insight/preprocessing/compression.py,sha256=T4YbX9PUlfTfPit_kpuLZn8hYpqLYu3xtTme_CG2ymc,12241
|
|
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=a_6lq3jR9tDFxdSxMjeJhyV_Tgw7ALcm-UFEvl_TE_M,25615
|
|
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
|
-
openms_insight/js-component/dist/assets/index.js,sha256=
|
|
22
|
+
openms_insight/js-component/dist/assets/index.js,sha256=aqGc3g7XLTRr7ptEgoA3XDu5oMS47yxxjUBXgansIo0,6091480
|
|
23
23
|
openms_insight/js-component/dist/assets/materialdesignicons-webfont.eot,sha256=CxgxBNL8XyYZbnc8d72vLgVQn9QlnS0V7O3Kebh-hPk,1307880
|
|
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.9.dist-info/METADATA,sha256=--VBuRN6Co39E_BZYFk6OwYkOyWEE9uOq_OBK21PdWE,16787
|
|
28
|
+
openms_insight-0.1.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
29
|
+
openms_insight-0.1.9.dist-info/licenses/LICENSE,sha256=INFF4rOMmpah7Oi14hLqu7NTOsx56KRRNChAAUcfh2E,1823
|
|
30
|
+
openms_insight-0.1.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|