openms-insight 0.1.2__py3-none-any.whl → 0.1.3__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 +11 -7
- openms_insight/components/__init__.py +2 -2
- openms_insight/components/heatmap.py +163 -101
- openms_insight/components/lineplot.py +377 -82
- openms_insight/components/sequenceview.py +677 -213
- openms_insight/components/table.py +86 -58
- openms_insight/core/__init__.py +2 -2
- openms_insight/core/base.py +102 -47
- openms_insight/core/registry.py +6 -5
- openms_insight/core/state.py +33 -31
- openms_insight/core/subprocess_preprocess.py +1 -3
- openms_insight/js-component/dist/assets/index.css +1 -1
- openms_insight/js-component/dist/assets/index.js +105 -105
- openms_insight/preprocessing/__init__.py +5 -6
- openms_insight/preprocessing/compression.py +68 -66
- openms_insight/preprocessing/filtering.py +39 -13
- openms_insight/rendering/__init__.py +1 -1
- openms_insight/rendering/bridge.py +192 -42
- {openms_insight-0.1.2.dist-info → openms_insight-0.1.3.dist-info}/METADATA +163 -20
- openms_insight-0.1.3.dist-info/RECORD +28 -0
- openms_insight-0.1.2.dist-info/RECORD +0 -28
- {openms_insight-0.1.2.dist-info → openms_insight-0.1.3.dist-info}/WHEEL +0 -0
- {openms_insight-0.1.2.dist-info → openms_insight-0.1.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Line plot component using Plotly.js."""
|
|
2
2
|
|
|
3
|
-
from typing import Any, Dict, Optional
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional
|
|
4
4
|
|
|
5
5
|
import polars as pl
|
|
6
6
|
|
|
@@ -8,6 +8,9 @@ from ..core.base import BaseComponent
|
|
|
8
8
|
from ..core.registry import register_component
|
|
9
9
|
from ..preprocessing.filtering import filter_and_collect_cached
|
|
10
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .sequenceview import SequenceView
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
@register_component("lineplot")
|
|
13
16
|
class LinePlot(BaseComponent):
|
|
@@ -51,8 +54,8 @@ class LinePlot(BaseComponent):
|
|
|
51
54
|
interactivity: Optional[Dict[str, str]] = None,
|
|
52
55
|
cache_path: str = ".",
|
|
53
56
|
regenerate_cache: bool = False,
|
|
54
|
-
x_column: str =
|
|
55
|
-
y_column: str =
|
|
57
|
+
x_column: str = "x",
|
|
58
|
+
y_column: str = "y",
|
|
56
59
|
title: Optional[str] = None,
|
|
57
60
|
x_label: Optional[str] = None,
|
|
58
61
|
y_label: Optional[str] = None,
|
|
@@ -60,7 +63,7 @@ class LinePlot(BaseComponent):
|
|
|
60
63
|
annotation_column: Optional[str] = None,
|
|
61
64
|
styling: Optional[Dict[str, Any]] = None,
|
|
62
65
|
config: Optional[Dict[str, Any]] = None,
|
|
63
|
-
**kwargs
|
|
66
|
+
**kwargs,
|
|
64
67
|
):
|
|
65
68
|
"""
|
|
66
69
|
Initialize the LinePlot component.
|
|
@@ -134,7 +137,7 @@ class LinePlot(BaseComponent):
|
|
|
134
137
|
annotation_column=annotation_column,
|
|
135
138
|
styling=styling,
|
|
136
139
|
config=config,
|
|
137
|
-
**kwargs
|
|
140
|
+
**kwargs,
|
|
138
141
|
)
|
|
139
142
|
|
|
140
143
|
def _get_cache_config(self) -> Dict[str, Any]:
|
|
@@ -145,12 +148,32 @@ class LinePlot(BaseComponent):
|
|
|
145
148
|
Dict of config values that affect preprocessing
|
|
146
149
|
"""
|
|
147
150
|
return {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
151
|
+
"x_column": self._x_column,
|
|
152
|
+
"y_column": self._y_column,
|
|
153
|
+
"highlight_column": self._highlight_column,
|
|
154
|
+
"annotation_column": self._annotation_column,
|
|
155
|
+
"title": self._title,
|
|
156
|
+
"x_label": self._x_label,
|
|
157
|
+
"y_label": self._y_label,
|
|
158
|
+
"styling": self._styling,
|
|
159
|
+
"plot_config": self._plot_config,
|
|
152
160
|
}
|
|
153
161
|
|
|
162
|
+
def _restore_cache_config(self, config: Dict[str, Any]) -> None:
|
|
163
|
+
"""Restore component-specific configuration from cached config."""
|
|
164
|
+
self._x_column = config.get("x_column", "x")
|
|
165
|
+
self._y_column = config.get("y_column", "y")
|
|
166
|
+
self._highlight_column = config.get("highlight_column")
|
|
167
|
+
self._annotation_column = config.get("annotation_column")
|
|
168
|
+
self._title = config.get("title")
|
|
169
|
+
self._x_label = config.get("x_label", self._x_column)
|
|
170
|
+
self._y_label = config.get("y_label", self._y_column)
|
|
171
|
+
self._styling = config.get("styling", {})
|
|
172
|
+
self._plot_config = config.get("plot_config", {})
|
|
173
|
+
# Initialize dynamic annotations (not cached)
|
|
174
|
+
self._dynamic_annotations = None
|
|
175
|
+
self._dynamic_title = None
|
|
176
|
+
|
|
154
177
|
def _get_row_group_size(self) -> int:
|
|
155
178
|
"""
|
|
156
179
|
Get optimal row group size for parquet writing.
|
|
@@ -175,8 +198,10 @@ class LinePlot(BaseComponent):
|
|
|
175
198
|
column_names = schema.names()
|
|
176
199
|
|
|
177
200
|
# Validate x and y columns exist
|
|
178
|
-
for col_name, col_label in [
|
|
179
|
-
|
|
201
|
+
for col_name, col_label in [
|
|
202
|
+
(self._x_column, "x_column"),
|
|
203
|
+
(self._y_column, "y_column"),
|
|
204
|
+
]:
|
|
180
205
|
if col_name not in column_names:
|
|
181
206
|
raise ValueError(
|
|
182
207
|
f"{col_label} '{col_name}' not found in data. "
|
|
@@ -214,24 +239,24 @@ class LinePlot(BaseComponent):
|
|
|
214
239
|
data = data.sort(sort_columns)
|
|
215
240
|
|
|
216
241
|
# Store configuration in preprocessed data for serialization
|
|
217
|
-
self._preprocessed_data[
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
242
|
+
self._preprocessed_data["plot_config"] = {
|
|
243
|
+
"x_column": self._x_column,
|
|
244
|
+
"y_column": self._y_column,
|
|
245
|
+
"highlight_column": self._highlight_column,
|
|
246
|
+
"annotation_column": self._annotation_column,
|
|
222
247
|
}
|
|
223
248
|
|
|
224
249
|
# Store LazyFrame for streaming to disk (filter happens at render time)
|
|
225
250
|
# Base class will use sink_parquet() to stream without full materialization
|
|
226
|
-
self._preprocessed_data[
|
|
251
|
+
self._preprocessed_data["data"] = data # Keep lazy
|
|
227
252
|
|
|
228
253
|
def _get_vue_component_name(self) -> str:
|
|
229
254
|
"""Return the Vue component name."""
|
|
230
|
-
return
|
|
255
|
+
return "PlotlyLineplotUnified"
|
|
231
256
|
|
|
232
257
|
def _get_data_key(self) -> str:
|
|
233
258
|
"""Return the key used to send primary data to Vue."""
|
|
234
|
-
return
|
|
259
|
+
return "plotData"
|
|
235
260
|
|
|
236
261
|
def _prepare_vue_data(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
237
262
|
"""
|
|
@@ -266,7 +291,7 @@ class LinePlot(BaseComponent):
|
|
|
266
291
|
columns_to_select.append(col)
|
|
267
292
|
|
|
268
293
|
# Get cached data (DataFrame or LazyFrame)
|
|
269
|
-
data = self._preprocessed_data.get(
|
|
294
|
+
data = self._preprocessed_data.get("data")
|
|
270
295
|
if data is None:
|
|
271
296
|
# Fallback to raw data if available
|
|
272
297
|
data = self._raw_data
|
|
@@ -293,7 +318,7 @@ class LinePlot(BaseComponent):
|
|
|
293
318
|
if self._dynamic_annotations and len(df_pandas) > 0:
|
|
294
319
|
num_rows = len(df_pandas)
|
|
295
320
|
highlights = [False] * num_rows
|
|
296
|
-
annotations = [
|
|
321
|
+
annotations = [""] * num_rows
|
|
297
322
|
|
|
298
323
|
# Get the interactivity column to use for lookup (e.g., 'peak_id')
|
|
299
324
|
# Use the first interactivity column as the ID column for annotation lookup
|
|
@@ -307,44 +332,38 @@ class LinePlot(BaseComponent):
|
|
|
307
332
|
for row_idx, peak_id in enumerate(peak_ids):
|
|
308
333
|
if peak_id in self._dynamic_annotations:
|
|
309
334
|
ann_data = self._dynamic_annotations[peak_id]
|
|
310
|
-
highlights[row_idx] = ann_data.get(
|
|
311
|
-
annotations[row_idx] = ann_data.get(
|
|
335
|
+
highlights[row_idx] = ann_data.get("highlight", False)
|
|
336
|
+
annotations[row_idx] = ann_data.get("annotation", "")
|
|
312
337
|
else:
|
|
313
338
|
# Fallback: use row index as key (legacy behavior)
|
|
314
339
|
for idx, ann_data in self._dynamic_annotations.items():
|
|
315
340
|
if isinstance(idx, int) and 0 <= idx < num_rows:
|
|
316
|
-
highlights[idx] = ann_data.get(
|
|
317
|
-
annotations[idx] = ann_data.get(
|
|
341
|
+
highlights[idx] = ann_data.get("highlight", False)
|
|
342
|
+
annotations[idx] = ann_data.get("annotation", "")
|
|
318
343
|
|
|
319
344
|
# Add dynamic columns to dataframe
|
|
320
345
|
df_pandas = df_pandas.copy()
|
|
321
|
-
df_pandas[
|
|
322
|
-
df_pandas[
|
|
346
|
+
df_pandas["_dynamic_highlight"] = highlights
|
|
347
|
+
df_pandas["_dynamic_annotation"] = annotations
|
|
323
348
|
|
|
324
349
|
# Update column names to use dynamic columns
|
|
325
|
-
highlight_col =
|
|
326
|
-
annotation_col =
|
|
350
|
+
highlight_col = "_dynamic_highlight"
|
|
351
|
+
annotation_col = "_dynamic_annotation"
|
|
327
352
|
|
|
328
353
|
# Update hash to include dynamic annotation state
|
|
329
354
|
import hashlib
|
|
330
|
-
|
|
355
|
+
|
|
356
|
+
ann_hash = hashlib.md5(
|
|
357
|
+
str(sorted(self._dynamic_annotations.keys())).encode()
|
|
358
|
+
).hexdigest()[:8]
|
|
331
359
|
data_hash = f"{data_hash}_{ann_hash}"
|
|
332
360
|
|
|
333
361
|
# Send as DataFrame for Arrow serialization (efficient binary transfer)
|
|
334
362
|
# Vue will parse and extract columns using the config
|
|
335
363
|
return {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
'_plotConfig': {
|
|
340
|
-
'xColumn': self._x_column,
|
|
341
|
-
'yColumn': self._y_column,
|
|
342
|
-
'highlightColumn': highlight_col,
|
|
343
|
-
'annotationColumn': annotation_col,
|
|
344
|
-
'interactivityColumns': {
|
|
345
|
-
col: col for col in (self._interactivity.values() if self._interactivity else [])
|
|
346
|
-
},
|
|
347
|
-
}
|
|
364
|
+
"plotData": df_pandas,
|
|
365
|
+
"_hash": data_hash,
|
|
366
|
+
"_plotConfig": self._build_plot_config(highlight_col, annotation_col),
|
|
348
367
|
}
|
|
349
368
|
|
|
350
369
|
def _get_component_args(self) -> Dict[str, Any]:
|
|
@@ -356,45 +375,45 @@ class LinePlot(BaseComponent):
|
|
|
356
375
|
"""
|
|
357
376
|
# Default styling
|
|
358
377
|
default_styling = {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
378
|
+
"highlightColor": "#E4572E",
|
|
379
|
+
"selectedColor": "#F3A712",
|
|
380
|
+
"unhighlightedColor": "lightblue",
|
|
381
|
+
"highlightHiddenColor": "#1f77b4",
|
|
382
|
+
"annotationColors": {
|
|
383
|
+
"massButton": "#E4572E",
|
|
384
|
+
"selectedMassButton": "#F3A712",
|
|
385
|
+
"sequenceArrow": "#E4572E",
|
|
386
|
+
"selectedSequenceArrow": "#F3A712",
|
|
387
|
+
"background": "#f0f0f0",
|
|
388
|
+
"buttonHover": "#e0e0e0",
|
|
389
|
+
},
|
|
371
390
|
}
|
|
372
391
|
|
|
373
392
|
# Merge user styling with defaults
|
|
374
393
|
styling = {**default_styling, **self._styling}
|
|
375
|
-
if
|
|
376
|
-
styling[
|
|
377
|
-
**default_styling[
|
|
378
|
-
**self._styling[
|
|
394
|
+
if "annotationColors" in self._styling:
|
|
395
|
+
styling["annotationColors"] = {
|
|
396
|
+
**default_styling["annotationColors"],
|
|
397
|
+
**self._styling["annotationColors"],
|
|
379
398
|
}
|
|
380
399
|
|
|
381
400
|
# Use dynamic title if set, otherwise static title
|
|
382
|
-
title = self._dynamic_title if self._dynamic_title else (self._title or
|
|
401
|
+
title = self._dynamic_title if self._dynamic_title else (self._title or "")
|
|
383
402
|
|
|
384
403
|
args: Dict[str, Any] = {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
404
|
+
"componentType": self._get_vue_component_name(),
|
|
405
|
+
"title": title,
|
|
406
|
+
"xLabel": self._x_label,
|
|
407
|
+
"yLabel": self._y_label,
|
|
408
|
+
"styling": styling,
|
|
409
|
+
"config": self._plot_config,
|
|
391
410
|
# Pass interactivity for click handling (sets selection on peak click)
|
|
392
|
-
|
|
411
|
+
"interactivity": self._interactivity,
|
|
393
412
|
# Column mappings for Arrow data parsing in Vue
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
413
|
+
"xColumn": self._x_column,
|
|
414
|
+
"yColumn": self._y_column,
|
|
415
|
+
"highlightColumn": self._highlight_column,
|
|
416
|
+
"annotationColumn": self._annotation_column,
|
|
398
417
|
}
|
|
399
418
|
|
|
400
419
|
# Add any extra config options
|
|
@@ -407,7 +426,7 @@ class LinePlot(BaseComponent):
|
|
|
407
426
|
highlight_color: Optional[str] = None,
|
|
408
427
|
selected_color: Optional[str] = None,
|
|
409
428
|
unhighlighted_color: Optional[str] = None,
|
|
410
|
-
) ->
|
|
429
|
+
) -> "LinePlot":
|
|
411
430
|
"""
|
|
412
431
|
Update plot styling.
|
|
413
432
|
|
|
@@ -420,11 +439,11 @@ class LinePlot(BaseComponent):
|
|
|
420
439
|
Self for method chaining
|
|
421
440
|
"""
|
|
422
441
|
if highlight_color:
|
|
423
|
-
self._styling[
|
|
442
|
+
self._styling["highlightColor"] = highlight_color
|
|
424
443
|
if selected_color:
|
|
425
|
-
self._styling[
|
|
444
|
+
self._styling["selectedColor"] = selected_color
|
|
426
445
|
if unhighlighted_color:
|
|
427
|
-
self._styling[
|
|
446
|
+
self._styling["unhighlightedColor"] = unhighlighted_color
|
|
428
447
|
return self
|
|
429
448
|
|
|
430
449
|
def with_annotations(
|
|
@@ -432,7 +451,7 @@ class LinePlot(BaseComponent):
|
|
|
432
451
|
background_color: Optional[str] = None,
|
|
433
452
|
button_color: Optional[str] = None,
|
|
434
453
|
selected_button_color: Optional[str] = None,
|
|
435
|
-
) ->
|
|
454
|
+
) -> "LinePlot":
|
|
436
455
|
"""
|
|
437
456
|
Configure annotation styling.
|
|
438
457
|
|
|
@@ -444,15 +463,17 @@ class LinePlot(BaseComponent):
|
|
|
444
463
|
Returns:
|
|
445
464
|
Self for method chaining
|
|
446
465
|
"""
|
|
447
|
-
if
|
|
448
|
-
self._styling[
|
|
466
|
+
if "annotationColors" not in self._styling:
|
|
467
|
+
self._styling["annotationColors"] = {}
|
|
449
468
|
|
|
450
469
|
if background_color:
|
|
451
|
-
self._styling[
|
|
470
|
+
self._styling["annotationColors"]["background"] = background_color
|
|
452
471
|
if button_color:
|
|
453
|
-
self._styling[
|
|
472
|
+
self._styling["annotationColors"]["massButton"] = button_color
|
|
454
473
|
if selected_button_color:
|
|
455
|
-
self._styling[
|
|
474
|
+
self._styling["annotationColors"]["selectedMassButton"] = (
|
|
475
|
+
selected_button_color
|
|
476
|
+
)
|
|
456
477
|
|
|
457
478
|
return self
|
|
458
479
|
|
|
@@ -460,7 +481,7 @@ class LinePlot(BaseComponent):
|
|
|
460
481
|
self,
|
|
461
482
|
annotations: Optional[Dict[int, Dict[str, Any]]] = None,
|
|
462
483
|
title: Optional[str] = None,
|
|
463
|
-
) ->
|
|
484
|
+
) -> "LinePlot":
|
|
464
485
|
"""
|
|
465
486
|
Set dynamic annotations to be applied at render time.
|
|
466
487
|
|
|
@@ -493,7 +514,7 @@ class LinePlot(BaseComponent):
|
|
|
493
514
|
self._dynamic_title = title
|
|
494
515
|
return self
|
|
495
516
|
|
|
496
|
-
def clear_dynamic_annotations(self) ->
|
|
517
|
+
def clear_dynamic_annotations(self) -> "LinePlot":
|
|
497
518
|
"""
|
|
498
519
|
Clear any dynamic annotations.
|
|
499
520
|
|
|
@@ -503,3 +524,277 @@ class LinePlot(BaseComponent):
|
|
|
503
524
|
self._dynamic_annotations = None
|
|
504
525
|
self._dynamic_title = None
|
|
505
526
|
return self
|
|
527
|
+
|
|
528
|
+
def _build_plot_config(
|
|
529
|
+
self,
|
|
530
|
+
highlight_col: Optional[str],
|
|
531
|
+
annotation_col: Optional[str],
|
|
532
|
+
) -> Dict[str, Any]:
|
|
533
|
+
"""
|
|
534
|
+
Build _plotConfig dict for Vue component.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
highlight_col: Column name for highlight values
|
|
538
|
+
annotation_col: Column name for annotation text
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
Config dict with column mappings for Vue
|
|
542
|
+
"""
|
|
543
|
+
return {
|
|
544
|
+
"xColumn": self._x_column,
|
|
545
|
+
"yColumn": self._y_column,
|
|
546
|
+
"highlightColumn": highlight_col,
|
|
547
|
+
"annotationColumn": annotation_col,
|
|
548
|
+
"interactivityColumns": {
|
|
549
|
+
col: col
|
|
550
|
+
for col in (self._interactivity.values() if self._interactivity else [])
|
|
551
|
+
},
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
def _strip_dynamic_columns(self, vue_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
555
|
+
"""
|
|
556
|
+
Strip dynamic annotation columns from vue_data for caching.
|
|
557
|
+
|
|
558
|
+
Returns a copy with dynamic columns removed so the cached version
|
|
559
|
+
doesn't contain stale annotation data.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
vue_data: The vue data dict to strip
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
Copy of vue_data without dynamic columns and _plotConfig
|
|
566
|
+
"""
|
|
567
|
+
import pandas as pd
|
|
568
|
+
|
|
569
|
+
vue_data = dict(vue_data)
|
|
570
|
+
df = vue_data.get("plotData")
|
|
571
|
+
|
|
572
|
+
if df is not None and isinstance(df, pd.DataFrame):
|
|
573
|
+
dynamic_cols = ["_dynamic_highlight", "_dynamic_annotation"]
|
|
574
|
+
cols_to_drop = [c for c in dynamic_cols if c in df.columns]
|
|
575
|
+
if cols_to_drop:
|
|
576
|
+
vue_data["plotData"] = df.drop(columns=cols_to_drop)
|
|
577
|
+
|
|
578
|
+
# Remove _plotConfig since it may reference dynamic columns
|
|
579
|
+
vue_data.pop("_plotConfig", None)
|
|
580
|
+
return vue_data
|
|
581
|
+
|
|
582
|
+
def _apply_fresh_annotations(self, vue_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
583
|
+
"""
|
|
584
|
+
Apply current dynamic annotations to cached base vue_data.
|
|
585
|
+
|
|
586
|
+
This is called by bridge.py when there's a cache hit for a component
|
|
587
|
+
with dynamic annotations. Re-applies the current annotation state.
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
vue_data: Cached base vue_data (without annotation columns)
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
vue_data with current annotations applied
|
|
594
|
+
"""
|
|
595
|
+
import pandas as pd
|
|
596
|
+
|
|
597
|
+
df_pandas = vue_data.get("plotData")
|
|
598
|
+
if df_pandas is None:
|
|
599
|
+
return vue_data
|
|
600
|
+
|
|
601
|
+
# Ensure we have a DataFrame
|
|
602
|
+
if not isinstance(df_pandas, pd.DataFrame):
|
|
603
|
+
return vue_data
|
|
604
|
+
|
|
605
|
+
# Determine highlight/annotation columns
|
|
606
|
+
highlight_col = self._highlight_column
|
|
607
|
+
annotation_col = self._annotation_column
|
|
608
|
+
|
|
609
|
+
if self._dynamic_annotations and len(df_pandas) > 0:
|
|
610
|
+
# Apply dynamic annotations
|
|
611
|
+
df_pandas = df_pandas.copy()
|
|
612
|
+
num_rows = len(df_pandas)
|
|
613
|
+
highlights = [False] * num_rows
|
|
614
|
+
annotations = [""] * num_rows
|
|
615
|
+
|
|
616
|
+
# Get the interactivity column for lookup
|
|
617
|
+
id_column = None
|
|
618
|
+
if self._interactivity:
|
|
619
|
+
id_column = list(self._interactivity.values())[0]
|
|
620
|
+
|
|
621
|
+
# Apply annotations by peak_id lookup
|
|
622
|
+
if id_column and id_column in df_pandas.columns:
|
|
623
|
+
peak_ids = df_pandas[id_column].tolist()
|
|
624
|
+
for row_idx, peak_id in enumerate(peak_ids):
|
|
625
|
+
if peak_id in self._dynamic_annotations:
|
|
626
|
+
ann_data = self._dynamic_annotations[peak_id]
|
|
627
|
+
highlights[row_idx] = ann_data.get("highlight", False)
|
|
628
|
+
annotations[row_idx] = ann_data.get("annotation", "")
|
|
629
|
+
else:
|
|
630
|
+
# Fallback: use row index as key
|
|
631
|
+
for idx, ann_data in self._dynamic_annotations.items():
|
|
632
|
+
if isinstance(idx, int) and 0 <= idx < num_rows:
|
|
633
|
+
highlights[idx] = ann_data.get("highlight", False)
|
|
634
|
+
annotations[idx] = ann_data.get("annotation", "")
|
|
635
|
+
|
|
636
|
+
df_pandas["_dynamic_highlight"] = highlights
|
|
637
|
+
df_pandas["_dynamic_annotation"] = annotations
|
|
638
|
+
highlight_col = "_dynamic_highlight"
|
|
639
|
+
annotation_col = "_dynamic_annotation"
|
|
640
|
+
|
|
641
|
+
# Build result
|
|
642
|
+
vue_data = dict(vue_data)
|
|
643
|
+
vue_data["plotData"] = df_pandas
|
|
644
|
+
vue_data["_plotConfig"] = self._build_plot_config(highlight_col, annotation_col)
|
|
645
|
+
return vue_data
|
|
646
|
+
|
|
647
|
+
@classmethod
|
|
648
|
+
def from_sequence_view(
|
|
649
|
+
cls,
|
|
650
|
+
sequence_view: "SequenceView",
|
|
651
|
+
cache_id: str,
|
|
652
|
+
cache_path: str = ".",
|
|
653
|
+
title: Optional[str] = None,
|
|
654
|
+
x_label: str = "m/z",
|
|
655
|
+
y_label: str = "Intensity",
|
|
656
|
+
styling: Optional[Dict[str, Any]] = None,
|
|
657
|
+
**kwargs,
|
|
658
|
+
) -> "LinePlot":
|
|
659
|
+
"""
|
|
660
|
+
Create a LinePlot linked to a SequenceView for annotated spectrum display.
|
|
661
|
+
|
|
662
|
+
The created LinePlot will:
|
|
663
|
+
- Use the same peaks data as the SequenceView
|
|
664
|
+
- Use the same filters (spectrum selection)
|
|
665
|
+
- Use the same interactivity (peak selection)
|
|
666
|
+
- Automatically apply annotations from SequenceView when rendered
|
|
667
|
+
|
|
668
|
+
Args:
|
|
669
|
+
sequence_view: The SequenceView to link to
|
|
670
|
+
cache_id: Unique identifier for this component's cache
|
|
671
|
+
cache_path: Base path for cache storage
|
|
672
|
+
title: Plot title (optional)
|
|
673
|
+
x_label: X-axis label (default: "m/z")
|
|
674
|
+
y_label: Y-axis label (default: "Intensity")
|
|
675
|
+
styling: Style configuration dict
|
|
676
|
+
**kwargs: Additional LinePlot configuration
|
|
677
|
+
|
|
678
|
+
Returns:
|
|
679
|
+
A new LinePlot instance linked to the SequenceView
|
|
680
|
+
|
|
681
|
+
Example:
|
|
682
|
+
sequence_view = SequenceView(
|
|
683
|
+
cache_id="seq",
|
|
684
|
+
sequence_data=sequences_df,
|
|
685
|
+
peaks_data=peaks_df,
|
|
686
|
+
filters={"spectrum": "scan_id"},
|
|
687
|
+
interactivity={"peak": "peak_id"},
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
# Create linked LinePlot
|
|
691
|
+
spectrum_plot = LinePlot.from_sequence_view(
|
|
692
|
+
sequence_view,
|
|
693
|
+
cache_id="spectrum",
|
|
694
|
+
title="Annotated Spectrum",
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
# Render both - annotations flow automatically
|
|
698
|
+
sv_result = sequence_view(key="sv", state_manager=state_manager)
|
|
699
|
+
spectrum_plot(key="plot", state_manager=state_manager, sequence_view_key="sv")
|
|
700
|
+
"""
|
|
701
|
+
# Get peaks data from SequenceView (uses cached data)
|
|
702
|
+
peaks_data = sequence_view.peaks_data
|
|
703
|
+
|
|
704
|
+
if peaks_data is None:
|
|
705
|
+
raise ValueError(
|
|
706
|
+
"SequenceView has no peaks_data. Cannot create linked LinePlot."
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
# Only include filters whose columns exist in peaks_data
|
|
710
|
+
# SequenceView may have filters for both sequence_data and peaks_data,
|
|
711
|
+
# but LinePlot only uses peaks_data
|
|
712
|
+
peaks_columns = peaks_data.collect_schema().names()
|
|
713
|
+
valid_filters = (
|
|
714
|
+
{
|
|
715
|
+
identifier: column
|
|
716
|
+
for identifier, column in sequence_view._filters.items()
|
|
717
|
+
if column in peaks_columns
|
|
718
|
+
}
|
|
719
|
+
if sequence_view._filters
|
|
720
|
+
else None
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
# Create the LinePlot with filtered filters and interactivity
|
|
724
|
+
plot = cls(
|
|
725
|
+
cache_id=cache_id,
|
|
726
|
+
data=peaks_data,
|
|
727
|
+
filters=valid_filters,
|
|
728
|
+
interactivity=sequence_view._interactivity.copy()
|
|
729
|
+
if sequence_view._interactivity
|
|
730
|
+
else None,
|
|
731
|
+
cache_path=cache_path,
|
|
732
|
+
x_column="mass",
|
|
733
|
+
y_column="intensity",
|
|
734
|
+
title=title,
|
|
735
|
+
x_label=x_label,
|
|
736
|
+
y_label=y_label,
|
|
737
|
+
styling=styling,
|
|
738
|
+
**kwargs,
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
# Store reference to sequence view key for annotation lookup
|
|
742
|
+
plot._linked_sequence_view_key: Optional[str] = None
|
|
743
|
+
|
|
744
|
+
return plot
|
|
745
|
+
|
|
746
|
+
def __call__(
|
|
747
|
+
self,
|
|
748
|
+
key: Optional[str] = None,
|
|
749
|
+
state_manager: Optional["StateManager"] = None,
|
|
750
|
+
height: Optional[int] = None,
|
|
751
|
+
sequence_view_key: Optional[str] = None,
|
|
752
|
+
) -> Any:
|
|
753
|
+
"""
|
|
754
|
+
Render the component in Streamlit.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
key: Optional unique key for the Streamlit component
|
|
758
|
+
state_manager: Optional StateManager for cross-component state.
|
|
759
|
+
If not provided, uses a default shared StateManager.
|
|
760
|
+
height: Optional height in pixels for the component
|
|
761
|
+
sequence_view_key: Optional key of a SequenceView component to get
|
|
762
|
+
annotations from. When provided, automatically applies fragment
|
|
763
|
+
annotations from that SequenceView.
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
The value returned by the Vue component (usually selection state)
|
|
767
|
+
"""
|
|
768
|
+
from ..core.state import get_default_state_manager
|
|
769
|
+
from ..rendering.bridge import get_component_annotations, render_component
|
|
770
|
+
|
|
771
|
+
if state_manager is None:
|
|
772
|
+
state_manager = get_default_state_manager()
|
|
773
|
+
|
|
774
|
+
# Apply annotations from linked SequenceView if specified
|
|
775
|
+
if sequence_view_key:
|
|
776
|
+
annotations_df = get_component_annotations(sequence_view_key)
|
|
777
|
+
if annotations_df is not None and annotations_df.height > 0:
|
|
778
|
+
# Convert annotation DataFrame to dynamic annotations dict
|
|
779
|
+
# keyed by peak_id for stable lookup
|
|
780
|
+
dynamic_annotations = {}
|
|
781
|
+
for row in annotations_df.iter_rows(named=True):
|
|
782
|
+
peak_id = row.get("peak_id")
|
|
783
|
+
if peak_id is not None:
|
|
784
|
+
dynamic_annotations[peak_id] = {
|
|
785
|
+
"highlight": True,
|
|
786
|
+
"annotation": row.get("annotation", ""),
|
|
787
|
+
"color": row.get("highlight_color", "#E4572E"),
|
|
788
|
+
}
|
|
789
|
+
self.set_dynamic_annotations(dynamic_annotations)
|
|
790
|
+
else:
|
|
791
|
+
self.clear_dynamic_annotations()
|
|
792
|
+
|
|
793
|
+
return render_component(
|
|
794
|
+
component=self, state_manager=state_manager, key=key, height=height
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
# Type hint import
|
|
799
|
+
if TYPE_CHECKING:
|
|
800
|
+
from ..core.state import StateManager
|