openms-insight 0.1.6__py3-none-any.whl → 0.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openms_insight/__init__.py +1 -1
- 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 +231 -139
- {openms_insight-0.1.6.dist-info → openms_insight-0.1.8.dist-info}/METADATA +3 -3
- {openms_insight-0.1.6.dist-info → openms_insight-0.1.8.dist-info}/RECORD +10 -10
- {openms_insight-0.1.6.dist-info → openms_insight-0.1.8.dist-info}/WHEEL +0 -0
- {openms_insight-0.1.6.dist-info → openms_insight-0.1.8.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
|
|
|
@@ -56,6 +58,11 @@ _COMPONENT_DATA_CACHE_KEY = "_svc_component_data_cache"
|
|
|
56
58
|
# Stores annotation dataframes returned by components like SequenceView
|
|
57
59
|
_COMPONENT_ANNOTATIONS_KEY = "_svc_component_annotations"
|
|
58
60
|
|
|
61
|
+
# Session state key for batch resend flag
|
|
62
|
+
# When any component requests data (e.g., after page navigation), we clear
|
|
63
|
+
# ALL hashes on the next render so all components get data in one rerun
|
|
64
|
+
_BATCH_RESEND_KEY = "_svc_batch_resend"
|
|
65
|
+
|
|
59
66
|
|
|
60
67
|
def _get_component_cache() -> Dict[str, Any]:
|
|
61
68
|
"""Get per-component data cache from session state."""
|
|
@@ -203,6 +210,12 @@ def _prepare_vue_data_cached(
|
|
|
203
210
|
# Try cache first (works for ALL components now)
|
|
204
211
|
cached = _get_cached_vue_data(component_id, filter_state_hashable)
|
|
205
212
|
|
|
213
|
+
if _DEBUG_HASH_TRACKING:
|
|
214
|
+
cache_hit = cached is not None
|
|
215
|
+
_logger.warning(
|
|
216
|
+
f"[CacheDebug] {component._cache_id}: cache_hit={cache_hit}"
|
|
217
|
+
)
|
|
218
|
+
|
|
206
219
|
if cached is not None:
|
|
207
220
|
cached_data, cached_hash = cached
|
|
208
221
|
|
|
@@ -298,14 +311,17 @@ def render_component(
|
|
|
298
311
|
"""
|
|
299
312
|
Render a component in Streamlit.
|
|
300
313
|
|
|
301
|
-
This function:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
314
|
+
This function uses a two-phase approach to handle state synchronization:
|
|
315
|
+
|
|
316
|
+
Phase 1: Call vue_func with CACHED data to get Vue's request
|
|
317
|
+
Phase 2: Apply Vue's request, then prepare UPDATED data for next render
|
|
318
|
+
|
|
319
|
+
This order is critical because Vue's request (e.g., page change, sort) is only
|
|
320
|
+
available after calling vue_func(). By calling it first with cached data, we:
|
|
321
|
+
1. Get Vue's request immediately
|
|
322
|
+
2. Apply it to state BEFORE preparing data
|
|
323
|
+
3. Prepare correct data for the next render
|
|
324
|
+
4. Rerun to send the correct data
|
|
309
325
|
|
|
310
326
|
Args:
|
|
311
327
|
component: The component to render
|
|
@@ -316,140 +332,101 @@ def render_component(
|
|
|
316
332
|
Returns:
|
|
317
333
|
The value returned by the Vue component
|
|
318
334
|
"""
|
|
319
|
-
#
|
|
320
|
-
state = state_manager.get_state_for_vue()
|
|
321
|
-
|
|
322
|
-
# Generate unique key if not provided (needed for cache)
|
|
323
|
-
# Use cache_id instead of id(component) since components are recreated each rerun
|
|
324
|
-
# Use JSON serialization for deterministic key generation (hash() can vary)
|
|
335
|
+
# === PHASE 0: Generate key and get component info ===
|
|
325
336
|
if key is None:
|
|
326
337
|
interactivity_str = json.dumps(component._interactivity or {}, sort_keys=True)
|
|
327
338
|
key = f"svc_{component._cache_id}_{hashlib.md5(interactivity_str.encode()).hexdigest()[:8]}"
|
|
328
339
|
|
|
329
|
-
# Check if component has required filters without values
|
|
330
|
-
# Don't send potentially huge unfiltered datasets - wait for filter selection
|
|
331
|
-
filters = getattr(component, "_filters", None) or {}
|
|
332
|
-
filter_defaults = getattr(component, "_filter_defaults", None) or {}
|
|
333
|
-
|
|
334
|
-
awaiting_filter = False
|
|
335
|
-
if filters:
|
|
336
|
-
# Check each filter - if no value AND no default, we're waiting
|
|
337
|
-
for identifier in filters.keys():
|
|
338
|
-
filter_value = state.get(identifier)
|
|
339
|
-
has_default = identifier in filter_defaults
|
|
340
|
-
if filter_value is None and not has_default:
|
|
341
|
-
awaiting_filter = True
|
|
342
|
-
break
|
|
343
|
-
|
|
344
|
-
# Extract state keys that affect this component's data for cache key
|
|
345
|
-
# This includes filters and any additional dependencies (e.g., zoom for heatmaps)
|
|
346
|
-
# Uses get_state_dependencies() which can be overridden by subclasses
|
|
347
|
-
state_keys = set(component.get_state_dependencies())
|
|
348
|
-
|
|
349
|
-
# Build hashable version for cache key (converts dicts/lists to JSON strings)
|
|
350
|
-
filter_state_hashable = tuple(
|
|
351
|
-
sorted((k, _make_hashable(state.get(k))) for k in state_keys)
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
# Build original state dict for passing to _prepare_vue_data
|
|
355
|
-
# (contains actual values, not JSON strings)
|
|
356
|
-
relevant_state = {k: state.get(k) for k in state_keys}
|
|
357
|
-
|
|
358
|
-
# Build component ID for cache (includes type to avoid collisions)
|
|
359
340
|
component_type = component._get_vue_component_name()
|
|
360
341
|
component_id = f"{component_type}:{key}"
|
|
361
|
-
|
|
362
|
-
# Skip data preparation if awaiting required filter selection
|
|
363
|
-
# This prevents sending huge unfiltered datasets
|
|
364
|
-
if awaiting_filter:
|
|
365
|
-
vue_data = {}
|
|
366
|
-
data_hash = "awaiting_filter"
|
|
367
|
-
else:
|
|
368
|
-
# Get component data using per-component cache
|
|
369
|
-
# Each component stores exactly one entry (current filter state)
|
|
370
|
-
# - Filterless components: filter_state=() always → always cache hit
|
|
371
|
-
# - Filtered components: cache hit when filter values unchanged
|
|
372
|
-
vue_data, data_hash = _prepare_vue_data_cached(
|
|
373
|
-
component, component_id, filter_state_hashable, relevant_state
|
|
374
|
-
)
|
|
375
|
-
|
|
376
342
|
component_args = component._get_component_args()
|
|
343
|
+
if height is not None:
|
|
344
|
+
component_args["height"] = height
|
|
345
|
+
|
|
346
|
+
# Batch resend: if any component requested data in previous run, clear ALL hashes
|
|
347
|
+
if st.session_state.get(_BATCH_RESEND_KEY):
|
|
348
|
+
st.session_state[_VUE_ECHOED_HASH_KEY] = {}
|
|
349
|
+
st.session_state.pop(_BATCH_RESEND_KEY, None)
|
|
377
350
|
|
|
378
351
|
# Initialize hash cache in session state if needed
|
|
379
352
|
if _VUE_ECHOED_HASH_KEY not in st.session_state:
|
|
380
353
|
st.session_state[_VUE_ECHOED_HASH_KEY] = {}
|
|
381
354
|
|
|
382
|
-
#
|
|
383
|
-
|
|
384
|
-
|
|
355
|
+
# === PHASE 1: Get CACHED data from previous render ===
|
|
356
|
+
cache = _get_component_cache()
|
|
357
|
+
cached_entry = cache.get(component_id)
|
|
385
358
|
|
|
386
|
-
# Get
|
|
387
|
-
|
|
388
|
-
vue_echoed_hash = st.session_state[_VUE_ECHOED_HASH_KEY].get(hash_tracking_key)
|
|
359
|
+
# Get current state for initial render (may be stale until we apply Vue's request)
|
|
360
|
+
initial_state = state_manager.get_state_for_vue()
|
|
389
361
|
|
|
390
|
-
#
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
data_changed = (vue_echoed_hash is None) or (vue_echoed_hash != data_hash)
|
|
362
|
+
# Pre-compute initial selections BEFORE Vue renders (for first render only)
|
|
363
|
+
if hasattr(component, "get_initial_selection"):
|
|
364
|
+
initial_selection = component.get_initial_selection(initial_state)
|
|
365
|
+
if initial_selection:
|
|
366
|
+
for identifier, value in initial_selection.items():
|
|
367
|
+
state_manager.set_selection(identifier, value)
|
|
368
|
+
initial_state = state_manager.get_state_for_vue()
|
|
398
369
|
|
|
399
|
-
#
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
f"data_hash={data_hash[:8] if data_hash else 'None'}, "
|
|
406
|
-
f"key={hash_tracking_key[:50]}..."
|
|
407
|
-
)
|
|
370
|
+
# Compute current filter state for cache validity check
|
|
371
|
+
# This tells us what state the component SHOULD have data for
|
|
372
|
+
state_keys = set(component.get_state_dependencies())
|
|
373
|
+
current_filter_state = tuple(
|
|
374
|
+
sorted((k, _make_hashable(initial_state.get(k))) for k in state_keys)
|
|
375
|
+
)
|
|
408
376
|
|
|
409
|
-
#
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
377
|
+
# Check if cached data is VALID for current state
|
|
378
|
+
# KEY FIX: Only send data when cache matches current state
|
|
379
|
+
# - Before: Always sent cached data, even if stale (page 38 when Vue wants page 1)
|
|
380
|
+
# - Now: Only send if cache matches current state
|
|
381
|
+
cache_valid = False
|
|
382
|
+
if cached_entry is not None:
|
|
383
|
+
cached_state, cached_data, cached_hash = cached_entry
|
|
384
|
+
cache_valid = (cached_state == current_filter_state)
|
|
385
|
+
if _DEBUG_STATE_SYNC:
|
|
386
|
+
_logger.warning(
|
|
387
|
+
f"[Bridge:{component._cache_id}] Phase1: cache_valid={cache_valid}, "
|
|
388
|
+
f"cached_state={cached_state[:2] if cached_state else None}..., "
|
|
389
|
+
f"current_state={current_filter_state[:2] if current_filter_state else None}..."
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Build payload - only send data if cache is valid for current state
|
|
393
|
+
if cache_valid:
|
|
394
|
+
# Cache HIT - send cached data (it's correct for current state)
|
|
426
395
|
data_payload = {
|
|
427
|
-
**
|
|
428
|
-
"selection_store":
|
|
429
|
-
"hash":
|
|
396
|
+
**cached_data,
|
|
397
|
+
"selection_store": initial_state,
|
|
398
|
+
"hash": cached_hash,
|
|
430
399
|
"dataChanged": True,
|
|
431
|
-
"awaitingFilter":
|
|
400
|
+
"awaitingFilter": False,
|
|
432
401
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
402
|
+
if _DEBUG_STATE_SYNC:
|
|
403
|
+
# Log pagination state for debugging
|
|
404
|
+
pagination_key = next((k for k in state_keys if "page" in k.lower()), None)
|
|
405
|
+
if pagination_key:
|
|
406
|
+
_logger.warning(
|
|
407
|
+
f"[Bridge:{component._cache_id}] Phase1: Cache HIT, "
|
|
408
|
+
f"sending data with hash={cached_hash[:8]}, "
|
|
409
|
+
f"pagination={initial_state.get(pagination_key)}"
|
|
410
|
+
)
|
|
436
411
|
else:
|
|
437
|
-
#
|
|
412
|
+
# Cache MISS (no cache, or state mismatch) - don't send stale data
|
|
413
|
+
# Vue will show loading or use its local cache
|
|
438
414
|
data_payload = {
|
|
439
|
-
"selection_store":
|
|
440
|
-
"hash":
|
|
415
|
+
"selection_store": initial_state,
|
|
416
|
+
"hash": "",
|
|
441
417
|
"dataChanged": False,
|
|
442
|
-
"awaitingFilter":
|
|
418
|
+
"awaitingFilter": False,
|
|
443
419
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
420
|
+
if _DEBUG_STATE_SYNC:
|
|
421
|
+
_logger.warning(
|
|
422
|
+
f"[Bridge:{component._cache_id}] Phase1: Cache MISS, "
|
|
423
|
+
f"not sending data (cached_entry={cached_entry is not None})"
|
|
424
|
+
)
|
|
448
425
|
|
|
449
426
|
# Component layout: [[{componentArgs: {...}}]]
|
|
450
427
|
components = [[{"componentArgs": component_args}]]
|
|
451
428
|
|
|
452
|
-
# Call Vue
|
|
429
|
+
# === PHASE 2: Call vue_func to get Vue's request ===
|
|
453
430
|
vue_func = get_vue_component_function()
|
|
454
431
|
|
|
455
432
|
kwargs = {
|
|
@@ -460,36 +437,141 @@ def render_component(
|
|
|
460
437
|
if height is not None:
|
|
461
438
|
kwargs["height"] = height
|
|
462
439
|
|
|
440
|
+
if _DEBUG_STATE_SYNC:
|
|
441
|
+
_logger.warning(
|
|
442
|
+
f"[Bridge:{component._cache_id}] Phase2: Calling vue_func to get request"
|
|
443
|
+
)
|
|
444
|
+
|
|
463
445
|
result = vue_func(**kwargs)
|
|
464
446
|
|
|
465
|
-
#
|
|
447
|
+
# === PHASE 3: Apply Vue's request FIRST ===
|
|
448
|
+
state_changed = False
|
|
466
449
|
if result is not None:
|
|
467
|
-
#
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
450
|
+
# Debug logging: what we received from Vue
|
|
451
|
+
if _DEBUG_STATE_SYNC:
|
|
452
|
+
vue_counter = result.get("counter")
|
|
453
|
+
vue_keys = [k for k in result.keys() if not k.startswith("_")]
|
|
454
|
+
_logger.warning(
|
|
455
|
+
f"[Bridge:{component._cache_id}] Phase3: Received counter={vue_counter}, "
|
|
456
|
+
f"keys={vue_keys}, _requestData={result.get('_requestData', False)}"
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Store Vue's echoed hash
|
|
471
460
|
vue_hash = result.get("_vueDataHash")
|
|
472
461
|
if vue_hash is not None:
|
|
473
|
-
st.session_state[_VUE_ECHOED_HASH_KEY][
|
|
462
|
+
st.session_state[_VUE_ECHOED_HASH_KEY][key] = vue_hash
|
|
463
|
+
|
|
464
|
+
# Apply Vue's state update FIRST - this is the key fix!
|
|
465
|
+
state_changed = state_manager.update_from_vue(result)
|
|
466
|
+
|
|
467
|
+
# Check if Vue is requesting data resend
|
|
468
|
+
if result.get("_requestData", False):
|
|
469
|
+
st.session_state[_BATCH_RESEND_KEY] = True
|
|
470
|
+
|
|
471
|
+
# === PHASE 4: Get UPDATED state and prepare data ===
|
|
472
|
+
# Now state reflects Vue's request (e.g., new page number after click)
|
|
473
|
+
state = state_manager.get_state_for_vue()
|
|
474
474
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if request_data:
|
|
479
|
-
# Clear our stored hash to force data resend on next render
|
|
480
|
-
st.session_state[_VUE_ECHOED_HASH_KEY].pop(hash_tracking_key, None)
|
|
475
|
+
# Check if component has required filters without values
|
|
476
|
+
filters = getattr(component, "_filters", None) or {}
|
|
477
|
+
filter_defaults = getattr(component, "_filter_defaults", None) or {}
|
|
481
478
|
|
|
482
|
-
|
|
483
|
-
|
|
479
|
+
awaiting_filter = False
|
|
480
|
+
for identifier in filters.keys():
|
|
481
|
+
if state.get(identifier) is None and identifier not in filter_defaults:
|
|
482
|
+
awaiting_filter = True
|
|
483
|
+
break
|
|
484
|
+
|
|
485
|
+
if not awaiting_filter:
|
|
486
|
+
# Extract state keys that affect this component's data
|
|
487
|
+
state_keys = set(component.get_state_dependencies())
|
|
488
|
+
relevant_state = {k: state.get(k) for k in state_keys}
|
|
489
|
+
|
|
490
|
+
# Build hashable version for cache key
|
|
491
|
+
filter_state_hashable = tuple(
|
|
492
|
+
sorted((k, _make_hashable(state.get(k))) for k in state_keys)
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
if _DEBUG_HASH_TRACKING:
|
|
496
|
+
_logger.warning(
|
|
497
|
+
f"[CacheKey] {component._cache_id}: state_keys={list(state_keys)}"
|
|
498
|
+
)
|
|
499
|
+
for k in state_keys:
|
|
500
|
+
if "page" in k.lower():
|
|
501
|
+
_logger.warning(
|
|
502
|
+
f"[CacheKey] {component._cache_id}: {k}={state.get(k)}"
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# Prepare data with UPDATED state (includes Vue's request)
|
|
506
|
+
# This may also call set_selection() to override (e.g., sort resets page)
|
|
507
|
+
vue_data, data_hash = _prepare_vue_data_cached(
|
|
508
|
+
component, component_id, filter_state_hashable, relevant_state
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
# Check if Python overrode state during _prepare_vue_data
|
|
512
|
+
# (e.g., table.py sets page to last page after sort)
|
|
513
|
+
final_state = state_manager.get_state_for_vue()
|
|
514
|
+
if final_state != state:
|
|
515
|
+
state_changed = True
|
|
516
|
+
if _DEBUG_STATE_SYNC:
|
|
517
|
+
_logger.warning(
|
|
518
|
+
f"[Bridge:{component._cache_id}] Phase4: Python overrode state"
|
|
519
|
+
)
|
|
520
|
+
else:
|
|
521
|
+
vue_data = {}
|
|
522
|
+
data_hash = "awaiting_filter"
|
|
523
|
+
filter_state_hashable = ()
|
|
524
|
+
|
|
525
|
+
_logger.info(f"[bridge] Phase4: {component._cache_id} prepared data, hash={data_hash[:8] if data_hash else 'None'}")
|
|
526
|
+
|
|
527
|
+
# === PHASE 5: Cache data for next render ===
|
|
528
|
+
if vue_data:
|
|
529
|
+
# Convert for caching (Arrow serialization requires pandas)
|
|
530
|
+
converted_data = {}
|
|
531
|
+
for data_key, value in vue_data.items():
|
|
532
|
+
if data_key == "_hash":
|
|
533
|
+
continue
|
|
534
|
+
if isinstance(value, pl.LazyFrame):
|
|
535
|
+
converted_data[data_key] = value.collect().to_pandas()
|
|
536
|
+
elif isinstance(value, pl.DataFrame):
|
|
537
|
+
converted_data[data_key] = value.to_pandas()
|
|
538
|
+
else:
|
|
539
|
+
converted_data[data_key] = value
|
|
540
|
+
|
|
541
|
+
# Store in cache for next render
|
|
542
|
+
cache[component_id] = (filter_state_hashable, converted_data, data_hash)
|
|
543
|
+
|
|
544
|
+
# If cache was invalid at Phase 1, we didn't send data to Vue (dataChanged=False).
|
|
545
|
+
# Trigger a rerun so the newly cached data gets sent on the next render.
|
|
546
|
+
# This handles cross-component filter changes where the affected component
|
|
547
|
+
# needs to receive updated data (e.g., new total_rows/total_pages).
|
|
548
|
+
if not cache_valid:
|
|
549
|
+
state_changed = True
|
|
550
|
+
if _DEBUG_STATE_SYNC:
|
|
551
|
+
_logger.warning(
|
|
552
|
+
f"[Bridge:{component._cache_id}] Phase5: Cache was invalid, "
|
|
553
|
+
f"triggering rerun to send newly cached data"
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
if _DEBUG_STATE_SYNC:
|
|
557
|
+
# Log what we're caching for debugging
|
|
558
|
+
pagination_key = next((k for k, v in filter_state_hashable if "page" in k.lower()), None)
|
|
559
|
+
if pagination_key:
|
|
560
|
+
pagination_val = next((v for k, v in filter_state_hashable if k == pagination_key), None)
|
|
561
|
+
_logger.warning(
|
|
562
|
+
f"[Bridge:{component._cache_id}] Phase5: Cached data with hash={data_hash[:8]}, "
|
|
563
|
+
f"filter_state includes {pagination_key}={pagination_val}"
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
# Handle annotations from Vue (e.g., from SequenceView)
|
|
567
|
+
if result is not None:
|
|
484
568
|
annotations = result.get("_annotations")
|
|
485
569
|
annotations_changed = False
|
|
486
570
|
|
|
487
571
|
if annotations is not None:
|
|
488
|
-
# Compute hash of new annotations
|
|
489
572
|
peak_ids = annotations.get("peak_id", [])
|
|
490
573
|
new_hash = hash(tuple(peak_ids)) if peak_ids else 0
|
|
491
574
|
|
|
492
|
-
# Compare with stored hash
|
|
493
575
|
ann_hash_key = f"_svc_ann_hash_{key}"
|
|
494
576
|
old_hash = st.session_state.get(ann_hash_key)
|
|
495
577
|
|
|
@@ -499,17 +581,27 @@ def render_component(
|
|
|
499
581
|
|
|
500
582
|
_store_component_annotations(key, annotations)
|
|
501
583
|
else:
|
|
502
|
-
# Annotations cleared - check if we had annotations before
|
|
503
584
|
ann_hash_key = f"_svc_ann_hash_{key}"
|
|
504
585
|
if st.session_state.get(ann_hash_key) is not None:
|
|
505
586
|
annotations_changed = True
|
|
506
587
|
st.session_state[ann_hash_key] = None
|
|
507
588
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
589
|
+
if annotations_changed:
|
|
590
|
+
state_changed = True
|
|
591
|
+
|
|
592
|
+
# === PHASE 6: Rerun if state changed ===
|
|
593
|
+
# This will send the UPDATED data (now in cache) to Vue
|
|
594
|
+
if state_changed:
|
|
595
|
+
if _DEBUG_STATE_SYNC:
|
|
596
|
+
_logger.warning(
|
|
597
|
+
f"[Bridge:{component._cache_id}] Phase6: RERUN triggered, "
|
|
598
|
+
f"next render will have cache HIT"
|
|
599
|
+
)
|
|
600
|
+
st.rerun()
|
|
601
|
+
elif _DEBUG_STATE_SYNC:
|
|
602
|
+
_logger.warning(
|
|
603
|
+
f"[Bridge:{component._cache_id}] Phase6: No rerun needed, state_changed=False"
|
|
604
|
+
)
|
|
513
605
|
|
|
514
606
|
return result
|
|
515
607
|
|
|
@@ -531,8 +623,8 @@ def _hash_data(data: Dict[str, Any]) -> str:
|
|
|
531
623
|
|
|
532
624
|
hash_parts = []
|
|
533
625
|
for key, value in sorted(data.items()):
|
|
534
|
-
# Skip internal metadata but NOT dynamic annotation columns
|
|
535
|
-
if key.startswith("_") and not key.startswith("_dynamic"):
|
|
626
|
+
# Skip internal metadata but NOT dynamic annotation columns or pagination
|
|
627
|
+
if key.startswith("_") and not key.startswith("_dynamic") and not key.startswith("_pagination"):
|
|
536
628
|
continue
|
|
537
629
|
if isinstance(value, pd.DataFrame):
|
|
538
630
|
# 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.8
|
|
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:**
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
openms_insight/__init__.py,sha256=
|
|
1
|
+
openms_insight/__init__.py,sha256=b4H9k0fPVZ6jCiJ4QVSwzlUQnyElF6ZwAlDI8fdPMJE,1111
|
|
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
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=3FiMlNL3hBHue28-0RWP9pt7GaNeczZbQwZl4cupXlc,23935
|
|
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.8.dist-info/METADATA,sha256=l5aHKdSHf5z046vRxq8xJLDN7UywXFfLEcLTxfEEn4Q,16787
|
|
28
|
+
openms_insight-0.1.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
29
|
+
openms_insight-0.1.8.dist-info/licenses/LICENSE,sha256=INFF4rOMmpah7Oi14hLqu7NTOsx56KRRNChAAUcfh2E,1823
|
|
30
|
+
openms_insight-0.1.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|