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.
@@ -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
- cached_state, vue_data, data_hash = cache[component_id]
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
- 1. Gets current state from StateManager
308
- 2. Calls component._prepare_vue_data() to get filtered data (cached!)
309
- 3. Computes hash for change detection
310
- 4. Only sends data if hash changed from last render (optimization)
311
- 5. Calls the Vue component with data payload
312
- 6. Updates StateManager from Vue response
313
- 7. Triggers st.rerun() if state changed
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
- # Get current state
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
- # Hash tracking key is the component's Streamlit key
394
- # Filter state changes are handled by data_hash comparison (different data = different hash)
395
- hash_tracking_key = key
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 Vue's last-echoed hash for this component
398
- # This is what Vue reported having in its last response
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
- # Send data if Vue's hash doesn't match current hash
402
- # This handles: first render, data change, browser refresh, Vue hot reload
403
- # Vue echoes null/None if it has no data, so mismatch triggers send
404
- # IMPORTANT: Also send data if vue_echoed_hash is None - this means Vue
405
- # hasn't confirmed receipt yet (e.g., after page navigation destroys Vue component)
406
- # NOTE: Hash now correctly reflects annotation state (annotations included in hash),
407
- # so normal comparison works for all components including those with dynamic annotations
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
- # Debug logging for hash tracking issues
411
- if _DEBUG_HASH_TRACKING:
412
- _logger.warning(
413
- f"[HashTrack] {component._cache_id}: "
414
- f"data_changed={data_changed}, "
415
- f"vue_echoed={vue_echoed_hash[:8] if vue_echoed_hash else 'None'}, "
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
- # Only include full data if hash changed
421
- if data_changed:
422
- # Convert any non-pandas data to pandas for Arrow serialization
423
- # pandas DataFrames are passed through (already optimal for Arrow)
424
- # Filter out _hash (internal metadata) but keep _plotConfig (needed by Vue)
425
- converted_data = {}
426
- for data_key, value in vue_data.items():
427
- if data_key == "_hash":
428
- # Skip internal hash metadata
429
- continue
430
- if isinstance(value, pl.LazyFrame):
431
- converted_data[data_key] = value.collect().to_pandas()
432
- elif isinstance(value, pl.DataFrame):
433
- converted_data[data_key] = value.to_pandas()
434
- else:
435
- converted_data[data_key] = value
436
- # pandas DataFrames pass through unchanged (optimal for Arrow)
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
- **converted_data,
439
- "selection_store": state,
440
- "hash": data_hash,
436
+ **cached_data,
437
+ "selection_store": initial_state,
438
+ "hash": cached_hash,
441
439
  "dataChanged": True,
442
- "awaitingFilter": awaiting_filter,
440
+ "awaitingFilter": False,
443
441
  }
444
- # Note: We don't pre-set the hash here anymore. We trust Vue's echo
445
- # at the end of the render cycle. This ensures we detect when Vue
446
- # loses its data (e.g., page navigation) and needs it resent.
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
- # Data unchanged - only send hash and state, Vue will use cached data
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": state,
451
- "hash": data_hash,
455
+ "selection_store": initial_state,
456
+ "hash": "",
452
457
  "dataChanged": False,
453
- "awaitingFilter": awaiting_filter,
458
+ "awaitingFilter": False,
454
459
  }
455
-
456
- # Add height to component args if specified
457
- if height is not None:
458
- component_args["height"] = height
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 component
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
- # Update state from Vue response
487
+ # === PHASE 3: Apply Vue's request FIRST ===
488
+ state_changed = False
477
489
  if result is not None:
478
- # Store Vue's echoed hash for next render comparison
479
- # Only store non-None hashes - Vue echoes None during initialization
480
- # before receiving data, which would corrupt our tracking. Preserving
481
- # the previous valid hash prevents unnecessary data resends.
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][hash_tracking_key] = vue_hash
485
-
486
- # Check if Vue is requesting data due to cache miss (e.g., after page navigation)
487
- # Vue's cache was empty when it received dataChanged=false, so it needs data resent
488
- request_data = result.get("_requestData", False)
489
- if request_data and not data_changed:
490
- # Vue needs data and we didn't just send it - batch for next run
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
- # Capture annotations from Vue (e.g., from SequenceView)
497
- # Use hash-based change detection for robustness
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
- # Update state and rerun if state changed OR annotations changed OR batch resend needed
523
- # Only rerun for requestData if we didn't already send data (to avoid stale triggers)
524
- state_changed = state_manager.update_from_vue(result)
525
- needs_batch_resend = request_data and not data_changed
526
- if state_changed or annotations_changed or needs_batch_resend:
527
- st.rerun()
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.7
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, pagination, CSV export
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 for large tables (default: True)
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=wmq1rjGVe4Ef0SAf5p85pfVCeyLlVevZnxBc9EIg2uk,16458
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=6hxmII90LlbwTy06BJvUdJjXpd8Oqjqdt452AJixJSs,19742
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=_vNYxYHYFgIigbkqYwkIO6cBGFJyF2VN9dr7CBEAQbY,6873
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=CxOcGWXZkR968_6x5-AkfxNYqKBIuuLya6RZVhZyMr0,21494
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=rCYOyEKHcbMbz2V4RflVTQ8UW8fGuvZN6kFVxb-10tI,6075229
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.7.dist-info/METADATA,sha256=NvzVktj8Tyah67T7yAzUwEGuZFqXkiGV5GlTlY8aZyI,16670
28
- openms_insight-0.1.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
29
- openms_insight-0.1.7.dist-info/licenses/LICENSE,sha256=INFF4rOMmpah7Oi14hLqu7NTOsx56KRRNChAAUcfh2E,1823
30
- openms_insight-0.1.7.dist-info/RECORD,,
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,,