masster 0.3.18__py3-none-any.whl → 0.3.20__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.
Potentially problematic release.
This version of masster might be problematic. Click here for more details.
- masster/__init__.py +2 -0
- masster/_version.py +1 -1
- masster/data/libs/README.md +17 -0
- masster/data/libs/ccm.py +533 -0
- masster/data/libs/central_carbon_README.md +17 -0
- masster/data/libs/central_carbon_metabolites.csv +120 -0
- masster/data/libs/urine.py +333 -0
- masster/data/libs/urine_metabolites.csv +51 -0
- masster/sample/h5.py +1 -1
- masster/sample/helpers.py +3 -7
- masster/sample/lib.py +32 -25
- masster/sample/load.py +9 -3
- masster/sample/plot.py +113 -27
- masster/study/export.py +27 -10
- masster/study/h5.py +58 -40
- masster/study/helpers.py +450 -196
- masster/study/helpers_optimized.py +5 -5
- masster/study/load.py +144 -118
- masster/study/plot.py +691 -277
- masster/study/processing.py +9 -5
- masster/study/study.py +6 -6
- {masster-0.3.18.dist-info → masster-0.3.20.dist-info}/METADATA +1 -1
- {masster-0.3.18.dist-info → masster-0.3.20.dist-info}/RECORD +31 -25
- /masster/data/{examples → wiff}/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.mzML +0 -0
- /masster/data/{examples → wiff}/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.timeseries.data +0 -0
- /masster/data/{examples → wiff}/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.wiff +0 -0
- /masster/data/{examples → wiff}/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.wiff.scan +0 -0
- /masster/data/{examples → wiff}/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.wiff2 +0 -0
- {masster-0.3.18.dist-info → masster-0.3.20.dist-info}/WHEEL +0 -0
- {masster-0.3.18.dist-info → masster-0.3.20.dist-info}/entry_points.txt +0 -0
- {masster-0.3.18.dist-info → masster-0.3.20.dist-info}/licenses/LICENSE +0 -0
masster/study/plot.py
CHANGED
|
@@ -17,7 +17,222 @@ hv.extension("bokeh")
|
|
|
17
17
|
from bokeh.layouts import row as bokeh_row
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def _isolated_save_plot(plot_object, filename, abs_filename, logger, plot_title="Plot"):
|
|
21
|
+
"""
|
|
22
|
+
Save a plot using isolated file saving that doesn't affect global Bokeh state.
|
|
23
|
+
This prevents browser opening issues when mixing file and notebook outputs.
|
|
24
|
+
"""
|
|
25
|
+
if filename.endswith(".html"):
|
|
26
|
+
# Use isolated file saving that doesn't affect global output state
|
|
27
|
+
from bokeh.resources import Resources
|
|
28
|
+
from bokeh.embed import file_html
|
|
29
|
+
|
|
30
|
+
# Create HTML content without affecting global state
|
|
31
|
+
resources = Resources(mode='cdn')
|
|
32
|
+
html = file_html(plot_object, resources, title=plot_title)
|
|
33
|
+
|
|
34
|
+
# Write directly to file
|
|
35
|
+
with open(filename, 'w', encoding='utf-8') as f:
|
|
36
|
+
f.write(html)
|
|
37
|
+
|
|
38
|
+
logger.info(f"Plot saved to: {abs_filename}")
|
|
39
|
+
|
|
40
|
+
elif filename.endswith(".png"):
|
|
41
|
+
try:
|
|
42
|
+
from bokeh.io.export import export_png
|
|
43
|
+
export_png(plot_object, filename=filename)
|
|
44
|
+
logger.info(f"Plot saved to: {abs_filename}")
|
|
45
|
+
except Exception:
|
|
46
|
+
# Fall back to HTML if PNG export not available
|
|
47
|
+
html_filename = filename.replace('.png', '.html')
|
|
48
|
+
from bokeh.resources import Resources
|
|
49
|
+
from bokeh.embed import file_html
|
|
50
|
+
|
|
51
|
+
resources = Resources(mode='cdn')
|
|
52
|
+
html = file_html(plot_object, resources, title=plot_title)
|
|
53
|
+
|
|
54
|
+
with open(html_filename, 'w', encoding='utf-8') as f:
|
|
55
|
+
f.write(html)
|
|
56
|
+
|
|
57
|
+
logger.warning(f"PNG export not available, saved as HTML instead: {html_filename}")
|
|
58
|
+
elif filename.endswith(".pdf"):
|
|
59
|
+
# Try to save as PDF, fall back to HTML if not available
|
|
60
|
+
try:
|
|
61
|
+
from bokeh.io.export import export_pdf
|
|
62
|
+
export_pdf(plot_object, filename=filename)
|
|
63
|
+
logger.info(f"Plot saved to: {abs_filename}")
|
|
64
|
+
except ImportError:
|
|
65
|
+
# Fall back to HTML if PDF export not available
|
|
66
|
+
html_filename = filename.replace('.pdf', '.html')
|
|
67
|
+
from bokeh.resources import Resources
|
|
68
|
+
from bokeh.embed import file_html
|
|
69
|
+
|
|
70
|
+
resources = Resources(mode='cdn')
|
|
71
|
+
html = file_html(plot_object, resources, title=plot_title)
|
|
72
|
+
|
|
73
|
+
with open(html_filename, 'w', encoding='utf-8') as f:
|
|
74
|
+
f.write(html)
|
|
75
|
+
|
|
76
|
+
logger.warning(f"PDF export not available, saved as HTML instead: {html_filename}")
|
|
77
|
+
else:
|
|
78
|
+
# Default to HTML for unknown extensions using isolated approach
|
|
79
|
+
from bokeh.resources import Resources
|
|
80
|
+
from bokeh.embed import file_html
|
|
81
|
+
|
|
82
|
+
resources = Resources(mode='cdn')
|
|
83
|
+
html = file_html(plot_object, resources, title=plot_title)
|
|
84
|
+
|
|
85
|
+
with open(filename, 'w', encoding='utf-8') as f:
|
|
86
|
+
f.write(html)
|
|
87
|
+
|
|
88
|
+
logger.info(f"Plot saved to: {abs_filename}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _isolated_show_notebook(plot_object):
|
|
92
|
+
"""
|
|
93
|
+
Show a plot in notebook using isolated display that resets Bokeh state.
|
|
94
|
+
This prevents browser opening issues when mixing file and notebook outputs.
|
|
95
|
+
"""
|
|
96
|
+
from bokeh.io import reset_output, output_notebook, show
|
|
97
|
+
import holoviews as hv
|
|
98
|
+
import warnings
|
|
99
|
+
import logging
|
|
100
|
+
|
|
101
|
+
# Suppress both warnings and logging messages for the specific Bokeh callback warnings
|
|
102
|
+
# that occur when Panel components with Python callbacks are converted to standalone Bokeh
|
|
103
|
+
bokeh_logger = logging.getLogger('bokeh.embed.util')
|
|
104
|
+
original_level = bokeh_logger.level
|
|
105
|
+
bokeh_logger.setLevel(logging.ERROR) # Suppress WARNING level messages
|
|
106
|
+
|
|
107
|
+
with warnings.catch_warnings():
|
|
108
|
+
warnings.filterwarnings("ignore", message=".*standalone HTML/JS output.*", category=UserWarning)
|
|
109
|
+
warnings.filterwarnings("ignore", message=".*real Python callbacks.*", category=UserWarning)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
# First clear all output state
|
|
113
|
+
reset_output()
|
|
114
|
+
|
|
115
|
+
# Set notebook mode
|
|
116
|
+
output_notebook(hide_banner=True)
|
|
117
|
+
|
|
118
|
+
# Reset Holoviews to notebook mode
|
|
119
|
+
hv.extension('bokeh', logo=False)
|
|
120
|
+
hv.output(backend='bokeh', mode='jupyter')
|
|
121
|
+
|
|
122
|
+
# Show in notebook
|
|
123
|
+
show(plot_object)
|
|
124
|
+
finally:
|
|
125
|
+
# Restore original logging level
|
|
126
|
+
bokeh_logger.setLevel(original_level)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _isolated_save_panel_plot(panel_obj, filename, abs_filename, logger, plot_title):
|
|
130
|
+
"""
|
|
131
|
+
Save a Panel plot using isolated approach that doesn't affect global Bokeh state.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
panel_obj: Panel object to save
|
|
135
|
+
filename: Target filename
|
|
136
|
+
abs_filename: Absolute path for logging
|
|
137
|
+
logger: Logger instance
|
|
138
|
+
plot_title: Title for logging
|
|
139
|
+
"""
|
|
140
|
+
import os # Import os for path operations
|
|
141
|
+
|
|
142
|
+
if filename.endswith(".html"):
|
|
143
|
+
# Panel save method should be isolated but let's be sure
|
|
144
|
+
try:
|
|
145
|
+
# Save directly without affecting global Bokeh state
|
|
146
|
+
panel_obj.save(filename, embed=True)
|
|
147
|
+
logger.info(f"{plot_title} saved to: {abs_filename}")
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error(f"Failed to save {plot_title}: {e}")
|
|
150
|
+
|
|
151
|
+
elif filename.endswith(".png"):
|
|
152
|
+
try:
|
|
153
|
+
from panel.io.save import save_png
|
|
154
|
+
# Convert Panel to Bokeh models before saving
|
|
155
|
+
bokeh_layout = panel_obj.get_root()
|
|
156
|
+
save_png(bokeh_layout, filename=filename)
|
|
157
|
+
logger.info(f"{plot_title} saved to: {abs_filename}")
|
|
158
|
+
except Exception:
|
|
159
|
+
# Fall back to HTML if PNG export not available
|
|
160
|
+
html_filename = filename.replace('.png', '.html')
|
|
161
|
+
abs_html_filename = os.path.abspath(html_filename)
|
|
162
|
+
try:
|
|
163
|
+
panel_obj.save(html_filename, embed=True)
|
|
164
|
+
logger.warning(f"PNG export not available, saved as HTML instead: {abs_html_filename}")
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error(f"Failed to save {plot_title} as HTML fallback: {e}")
|
|
167
|
+
|
|
168
|
+
elif filename.endswith(".pdf"):
|
|
169
|
+
# Try to save as PDF, fall back to HTML if not available
|
|
170
|
+
try:
|
|
171
|
+
from bokeh.io.export import export_pdf
|
|
172
|
+
bokeh_layout = panel_obj.get_root()
|
|
173
|
+
export_pdf(bokeh_layout, filename=filename)
|
|
174
|
+
logger.info(f"{plot_title} saved to: {abs_filename}")
|
|
175
|
+
except ImportError:
|
|
176
|
+
# Fall back to HTML if PDF export not available
|
|
177
|
+
html_filename = filename.replace('.pdf', '.html')
|
|
178
|
+
abs_html_filename = os.path.abspath(html_filename)
|
|
179
|
+
try:
|
|
180
|
+
panel_obj.save(html_filename, embed=True)
|
|
181
|
+
logger.warning(f"PDF export not available, saved as HTML instead: {abs_html_filename}")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error(f"Failed to save {plot_title} as HTML fallback: {e}")
|
|
184
|
+
else:
|
|
185
|
+
# Default to HTML for unknown extensions
|
|
186
|
+
try:
|
|
187
|
+
panel_obj.save(filename, embed=True)
|
|
188
|
+
logger.info(f"{plot_title} saved to: {abs_filename}")
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.error(f"Failed to save {plot_title}: {e}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _isolated_show_panel_notebook(panel_obj):
|
|
194
|
+
"""
|
|
195
|
+
Show a Panel plot in notebook with state isolation to prevent browser opening.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
panel_obj: Panel object to display
|
|
199
|
+
"""
|
|
200
|
+
# Reset Bokeh state completely to prevent browser opening if output_file was called before
|
|
201
|
+
from bokeh.io import reset_output, output_notebook
|
|
202
|
+
import holoviews as hv
|
|
203
|
+
|
|
204
|
+
# First clear all output state
|
|
205
|
+
reset_output()
|
|
206
|
+
|
|
207
|
+
# Set notebook mode
|
|
208
|
+
output_notebook(hide_banner=True)
|
|
209
|
+
|
|
210
|
+
# Reset Holoviews to notebook mode
|
|
211
|
+
hv.extension('bokeh', logo=False)
|
|
212
|
+
hv.output(backend='bokeh', mode='jupyter')
|
|
213
|
+
|
|
214
|
+
# For Panel objects in notebooks, use pn.extension and display inline
|
|
215
|
+
import panel as pn
|
|
216
|
+
try:
|
|
217
|
+
# Configure Panel for notebook display
|
|
218
|
+
pn.extension('bokeh', inline=True, comms='vscode')
|
|
219
|
+
# Use IPython display to show inline instead of show()
|
|
220
|
+
from IPython.display import display
|
|
221
|
+
display(panel_obj)
|
|
222
|
+
except Exception:
|
|
223
|
+
# Fallback to regular Panel show
|
|
224
|
+
panel_obj.show()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def plot_alignment(
|
|
228
|
+
self,
|
|
229
|
+
samples=None,
|
|
230
|
+
maps: bool = True,
|
|
231
|
+
filename: str | None = None,
|
|
232
|
+
width: int = 450,
|
|
233
|
+
height: int = 450,
|
|
234
|
+
markersize: int = 3,
|
|
235
|
+
):
|
|
21
236
|
"""Visualize retention time alignment using two synchronized Bokeh scatter plots.
|
|
22
237
|
|
|
23
238
|
- When ``maps=True`` the function reads ``self.features_maps`` (list of FeatureMap)
|
|
@@ -26,12 +241,8 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
26
241
|
``rt_original`` column (before) and ``rt`` column (after).
|
|
27
242
|
|
|
28
243
|
Parameters
|
|
244
|
+
- samples: List of sample identifiers (sample_uids or sample_names), or single int for random selection, or None for all samples.
|
|
29
245
|
- maps: whether to use feature maps (default True).
|
|
30
|
-
- samples: Sample selection parameter, interpreted like in plot_samples_2d:
|
|
31
|
-
- None: show all samples
|
|
32
|
-
- int: show a random subset of N samples
|
|
33
|
-
- list of ints: show samples with these sample_uids
|
|
34
|
-
- list of strings: show samples with these sample_names
|
|
35
246
|
- filename: optional HTML file path to save the plot.
|
|
36
247
|
- width/height: pixel size of each subplot.
|
|
37
248
|
- markersize: base marker size.
|
|
@@ -44,6 +255,9 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
44
255
|
from bokeh.plotting import figure, show, output_file
|
|
45
256
|
import pandas as pd
|
|
46
257
|
|
|
258
|
+
# Get sample_uids to filter by if specified
|
|
259
|
+
sample_uids = self._get_sample_uids(samples) if samples is not None else None
|
|
260
|
+
|
|
47
261
|
# Build the before/after tabular data used for plotting
|
|
48
262
|
before_data: list[dict[str, Any]] = []
|
|
49
263
|
after_data: list[dict[str, Any]] = []
|
|
@@ -59,31 +273,39 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
59
273
|
self.logger.error("No feature maps available for plotting.")
|
|
60
274
|
return
|
|
61
275
|
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if sample_uids_to_show is not None:
|
|
67
|
-
# Get sample indices for the selected sample_uids
|
|
68
|
-
selected_indices = []
|
|
69
|
-
if hasattr(self, 'samples_df') and self.samples_df is not None and not self.samples_df.is_empty():
|
|
276
|
+
# Filter feature maps by selected samples if specified
|
|
277
|
+
if sample_uids is not None:
|
|
278
|
+
# Create mapping from sample_uid to map_id and filter accordingly
|
|
279
|
+
if hasattr(self, "samples_df") and self.samples_df is not None and not self.samples_df.is_empty():
|
|
70
280
|
samples_info = self.samples_df.to_pandas()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
281
|
+
|
|
282
|
+
# Filter samples_info to only selected sample_uids and get their map_ids
|
|
283
|
+
selected_samples = samples_info[samples_info["sample_uid"].isin(sample_uids)]
|
|
284
|
+
if selected_samples.empty:
|
|
285
|
+
self.logger.error("No matching samples found for the provided sample_uids.")
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
# Get the map_ids for selected samples
|
|
289
|
+
selected_map_ids = selected_samples["map_id"].tolist()
|
|
290
|
+
|
|
291
|
+
# Filter feature maps based on map_ids
|
|
292
|
+
filtered_maps = []
|
|
293
|
+
for map_id in selected_map_ids:
|
|
294
|
+
if 0 <= map_id < len(fmaps):
|
|
295
|
+
filtered_maps.append(fmaps[map_id])
|
|
296
|
+
|
|
297
|
+
fmaps = filtered_maps
|
|
298
|
+
samples_info = selected_samples.reset_index(drop=True)
|
|
299
|
+
|
|
300
|
+
if not fmaps:
|
|
301
|
+
self.logger.error("No feature maps found for the selected samples.")
|
|
302
|
+
return
|
|
74
303
|
else:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# Filter feature maps to only include selected indices
|
|
82
|
-
fmaps = [fmaps[i] for i in selected_indices if i < len(fmaps)]
|
|
83
|
-
|
|
84
|
-
if not fmaps:
|
|
85
|
-
self.logger.error("No feature maps match the selected samples.")
|
|
86
|
-
return
|
|
304
|
+
self.logger.warning("Cannot filter feature maps: no samples_df available")
|
|
305
|
+
|
|
306
|
+
if not fmaps:
|
|
307
|
+
self.logger.error("No feature maps available after filtering.")
|
|
308
|
+
return
|
|
87
309
|
|
|
88
310
|
# Reference (first) sample: use current RT for both before and after
|
|
89
311
|
ref = fmaps[0]
|
|
@@ -92,20 +314,43 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
92
314
|
ref_inty = [f.getIntensity() for f in ref]
|
|
93
315
|
max_ref_inty = max(ref_inty) if ref_inty else 1
|
|
94
316
|
|
|
95
|
-
# sample metadata
|
|
96
|
-
if hasattr(self,
|
|
97
|
-
samples_info
|
|
98
|
-
|
|
99
|
-
|
|
317
|
+
# Get sample metadata for reference (first) sample
|
|
318
|
+
if hasattr(self, "samples_df") and self.samples_df is not None and not self.samples_df.is_empty():
|
|
319
|
+
if 'samples_info' not in locals():
|
|
320
|
+
samples_info = self.samples_df.to_pandas()
|
|
321
|
+
ref_sample_uid = (
|
|
322
|
+
samples_info.iloc[0]["sample_uid"] if "sample_uid" in samples_info.columns else "Reference_UID"
|
|
323
|
+
)
|
|
324
|
+
ref_sample_name = (
|
|
325
|
+
samples_info.iloc[0]["sample_name"] if "sample_name" in samples_info.columns else "Reference"
|
|
326
|
+
)
|
|
100
327
|
else:
|
|
101
|
-
ref_sample_uid =
|
|
102
|
-
ref_sample_name =
|
|
328
|
+
ref_sample_uid = "Reference_UID"
|
|
329
|
+
ref_sample_name = "Reference"
|
|
103
330
|
|
|
104
331
|
for rt, mz, inty in zip(ref_rt, ref_mz, ref_inty):
|
|
105
|
-
before_data.append({
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
332
|
+
before_data.append({
|
|
333
|
+
"rt": rt,
|
|
334
|
+
"mz": mz,
|
|
335
|
+
"inty": inty,
|
|
336
|
+
"alpha": inty / max_ref_inty,
|
|
337
|
+
"sample_idx": 0,
|
|
338
|
+
"sample_name": ref_sample_name,
|
|
339
|
+
"sample_uid": ref_sample_uid,
|
|
340
|
+
"size": markersize + 2,
|
|
341
|
+
})
|
|
342
|
+
after_data.append({
|
|
343
|
+
"rt": rt,
|
|
344
|
+
"mz": mz,
|
|
345
|
+
"inty": inty,
|
|
346
|
+
"alpha": inty / max_ref_inty,
|
|
347
|
+
"sample_idx": 0,
|
|
348
|
+
"sample_name": ref_sample_name,
|
|
349
|
+
"sample_uid": ref_sample_uid,
|
|
350
|
+
"size": markersize + 2,
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
# Remaining samples - now using filtered feature maps and samples_info
|
|
109
354
|
for sample_idx, fm in enumerate(fmaps[1:], start=1):
|
|
110
355
|
mz_vals = []
|
|
111
356
|
inty_vals = []
|
|
@@ -114,7 +359,7 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
114
359
|
|
|
115
360
|
for f in fm:
|
|
116
361
|
try:
|
|
117
|
-
orig = f.getMetaValue(
|
|
362
|
+
orig = f.getMetaValue("original_RT")
|
|
118
363
|
except Exception:
|
|
119
364
|
orig = None
|
|
120
365
|
|
|
@@ -132,23 +377,48 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
132
377
|
|
|
133
378
|
max_inty = max(inty_vals)
|
|
134
379
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if
|
|
138
|
-
|
|
139
|
-
|
|
380
|
+
# Get sample metadata from filtered samples_info
|
|
381
|
+
if hasattr(self, "samples_df") and self.samples_df is not None and not self.samples_df.is_empty():
|
|
382
|
+
# Use filtered samples_info if it exists from the filtering above
|
|
383
|
+
if 'samples_info' in locals() and sample_idx < len(samples_info):
|
|
384
|
+
sample_name = samples_info.iloc[sample_idx].get("sample_name", f"Sample {sample_idx}")
|
|
385
|
+
sample_uid = samples_info.iloc[sample_idx].get("sample_uid", f"Sample_{sample_idx}_UID")
|
|
140
386
|
else:
|
|
141
|
-
|
|
142
|
-
|
|
387
|
+
# Fallback to original samples_df if filtered samples_info is not available
|
|
388
|
+
all_samples_info = self.samples_df.to_pandas()
|
|
389
|
+
if sample_idx < len(all_samples_info):
|
|
390
|
+
sample_name = all_samples_info.iloc[sample_idx].get("sample_name", f"Sample {sample_idx}")
|
|
391
|
+
sample_uid = all_samples_info.iloc[sample_idx].get("sample_uid", f"Sample_{sample_idx}_UID")
|
|
392
|
+
else:
|
|
393
|
+
sample_name = f"Sample {sample_idx}"
|
|
394
|
+
sample_uid = f"Sample_{sample_idx}_UID"
|
|
143
395
|
else:
|
|
144
|
-
sample_name = f
|
|
145
|
-
sample_uid = f
|
|
396
|
+
sample_name = f"Sample {sample_idx}"
|
|
397
|
+
sample_uid = f"Sample_{sample_idx}_UID"
|
|
146
398
|
|
|
147
399
|
for rt, mz, inty in zip(original_rt, mz_vals, inty_vals):
|
|
148
|
-
before_data.append({
|
|
400
|
+
before_data.append({
|
|
401
|
+
"rt": rt,
|
|
402
|
+
"mz": mz,
|
|
403
|
+
"inty": inty,
|
|
404
|
+
"alpha": inty / max_inty,
|
|
405
|
+
"sample_idx": sample_idx,
|
|
406
|
+
"sample_name": sample_name,
|
|
407
|
+
"sample_uid": sample_uid,
|
|
408
|
+
"size": markersize,
|
|
409
|
+
})
|
|
149
410
|
|
|
150
411
|
for rt, mz, inty in zip(aligned_rt, mz_vals, inty_vals):
|
|
151
|
-
after_data.append({
|
|
412
|
+
after_data.append({
|
|
413
|
+
"rt": rt,
|
|
414
|
+
"mz": mz,
|
|
415
|
+
"inty": inty,
|
|
416
|
+
"alpha": inty / max_inty,
|
|
417
|
+
"sample_idx": sample_idx,
|
|
418
|
+
"sample_name": sample_name,
|
|
419
|
+
"sample_uid": sample_uid,
|
|
420
|
+
"size": markersize,
|
|
421
|
+
})
|
|
152
422
|
|
|
153
423
|
else:
|
|
154
424
|
# Use features_df
|
|
@@ -156,88 +426,83 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
156
426
|
self.logger.error("No features_df found. Load features first.")
|
|
157
427
|
return
|
|
158
428
|
|
|
159
|
-
required_cols = [
|
|
429
|
+
required_cols = ["rt", "mz", "inty"]
|
|
160
430
|
missing = [c for c in required_cols if c not in self.features_df.columns]
|
|
161
431
|
if missing:
|
|
162
432
|
self.logger.error(f"Missing required columns in features_df: {missing}")
|
|
163
433
|
return
|
|
164
434
|
|
|
165
|
-
if
|
|
435
|
+
if "rt_original" not in self.features_df.columns:
|
|
166
436
|
self.logger.error("Column 'rt_original' not found in features_df. Alignment may not have been performed.")
|
|
167
437
|
return
|
|
168
438
|
|
|
169
439
|
# Use Polars instead of pandas
|
|
170
440
|
features_df = self.features_df
|
|
441
|
+
|
|
442
|
+
# Filter by selected samples if specified
|
|
443
|
+
if sample_uids is not None:
|
|
444
|
+
features_df = features_df.filter(pl.col("sample_uid").is_in(sample_uids))
|
|
445
|
+
if features_df.is_empty():
|
|
446
|
+
self.logger.error("No features found for the selected samples.")
|
|
447
|
+
return
|
|
171
448
|
|
|
172
|
-
sample_col =
|
|
449
|
+
sample_col = "sample_uid" if "sample_uid" in features_df.columns else "sample_name"
|
|
173
450
|
if sample_col not in features_df.columns:
|
|
174
451
|
self.logger.error("No sample identifier column found in features_df.")
|
|
175
452
|
return
|
|
176
453
|
|
|
177
|
-
# Get sample_uids to limit which samples to show
|
|
178
|
-
sample_uids_to_show = self._get_sample_uids(samples)
|
|
179
|
-
|
|
180
|
-
# Filter features_df based on sample selection if specified
|
|
181
|
-
if sample_uids_to_show is not None:
|
|
182
|
-
if sample_col == 'sample_uid':
|
|
183
|
-
features_df = features_df.filter(pl.col('sample_uid').is_in(sample_uids_to_show))
|
|
184
|
-
else:
|
|
185
|
-
# Need to convert sample names to sample_uids if using sample_name column
|
|
186
|
-
if 'sample_uid' in features_df.columns:
|
|
187
|
-
# Filter by sample_uid even though we're using sample_name as the primary column
|
|
188
|
-
features_df = features_df.filter(pl.col('sample_uid').is_in(sample_uids_to_show))
|
|
189
|
-
else:
|
|
190
|
-
# Convert sample_uids to sample_names and filter
|
|
191
|
-
sample_names_to_show = []
|
|
192
|
-
if hasattr(self, 'samples_df') and self.samples_df is not None:
|
|
193
|
-
for uid in sample_uids_to_show:
|
|
194
|
-
matching_rows = self.samples_df.filter(pl.col("sample_uid") == uid)
|
|
195
|
-
if not matching_rows.is_empty():
|
|
196
|
-
sample_names_to_show.append(matching_rows.row(0, named=True)["sample_name"])
|
|
197
|
-
features_df = features_df.filter(pl.col('sample_name').is_in(sample_names_to_show))
|
|
198
|
-
|
|
199
454
|
# Get unique samples using Polars
|
|
200
455
|
samples = features_df.select(pl.col(sample_col)).unique().to_series().to_list()
|
|
201
456
|
|
|
202
457
|
for sample_idx, sample in enumerate(samples):
|
|
203
458
|
# Filter sample data using Polars
|
|
204
459
|
sample_data = features_df.filter(pl.col(sample_col) == sample)
|
|
205
|
-
|
|
460
|
+
|
|
206
461
|
# Calculate max intensity using Polars
|
|
207
|
-
max_inty = sample_data.select(pl.col(
|
|
462
|
+
max_inty = sample_data.select(pl.col("inty").max()).item()
|
|
208
463
|
max_inty = max_inty if max_inty and max_inty > 0 else 1
|
|
209
|
-
|
|
464
|
+
|
|
210
465
|
sample_name = str(sample)
|
|
211
466
|
# Get sample_uid - if sample_col is 'sample_uid', use sample directly
|
|
212
|
-
if sample_col ==
|
|
467
|
+
if sample_col == "sample_uid":
|
|
213
468
|
sample_uid = sample
|
|
214
469
|
else:
|
|
215
470
|
# Try to get sample_uid from the first row if it exists
|
|
216
|
-
if
|
|
217
|
-
sample_uid = sample_data.select(pl.col(
|
|
471
|
+
if "sample_uid" in sample_data.columns:
|
|
472
|
+
sample_uid = sample_data.select(pl.col("sample_uid")).item()
|
|
218
473
|
else:
|
|
219
474
|
sample_uid = sample
|
|
220
475
|
|
|
221
476
|
# Convert to dict for iteration - more efficient than row-by-row processing
|
|
222
|
-
sample_dict = sample_data.select([
|
|
223
|
-
|
|
477
|
+
sample_dict = sample_data.select(["rt_original", "rt", "mz", "inty"]).to_dicts()
|
|
478
|
+
|
|
224
479
|
for row_dict in sample_dict:
|
|
225
|
-
rt_original = row_dict[
|
|
226
|
-
rt_current = row_dict[
|
|
227
|
-
mz = row_dict[
|
|
228
|
-
inty = row_dict[
|
|
480
|
+
rt_original = row_dict["rt_original"]
|
|
481
|
+
rt_current = row_dict["rt"]
|
|
482
|
+
mz = row_dict["mz"]
|
|
483
|
+
inty = row_dict["inty"]
|
|
229
484
|
alpha = inty / max_inty
|
|
230
485
|
size = markersize + 2 if sample_idx == 0 else markersize
|
|
231
|
-
|
|
486
|
+
|
|
232
487
|
before_data.append({
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
488
|
+
"rt": rt_original,
|
|
489
|
+
"mz": mz,
|
|
490
|
+
"inty": inty,
|
|
491
|
+
"alpha": alpha,
|
|
492
|
+
"sample_idx": sample_idx,
|
|
493
|
+
"sample_name": sample_name,
|
|
494
|
+
"sample_uid": sample_uid,
|
|
495
|
+
"size": size,
|
|
236
496
|
})
|
|
237
497
|
after_data.append({
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
498
|
+
"rt": rt_current,
|
|
499
|
+
"mz": mz,
|
|
500
|
+
"inty": inty,
|
|
501
|
+
"alpha": alpha,
|
|
502
|
+
"sample_idx": sample_idx,
|
|
503
|
+
"sample_name": sample_name,
|
|
504
|
+
"sample_uid": sample_uid,
|
|
505
|
+
"size": size,
|
|
241
506
|
})
|
|
242
507
|
|
|
243
508
|
# Get sample colors from samples_df using sample indices
|
|
@@ -246,17 +511,16 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
246
511
|
# Create mapping from sample_idx to sample_uid more efficiently
|
|
247
512
|
sample_idx_to_uid = {}
|
|
248
513
|
for item in before_data:
|
|
249
|
-
if item[
|
|
250
|
-
sample_idx_to_uid[item[
|
|
514
|
+
if item["sample_idx"] not in sample_idx_to_uid:
|
|
515
|
+
sample_idx_to_uid[item["sample_idx"]] = item["sample_uid"]
|
|
251
516
|
else:
|
|
252
517
|
sample_idx_to_uid = {}
|
|
253
|
-
|
|
518
|
+
|
|
254
519
|
# Get colors from samples_df
|
|
255
520
|
sample_uids_list = list(sample_idx_to_uid.values())
|
|
256
|
-
if sample_uids_list and hasattr(self,
|
|
521
|
+
if sample_uids_list and hasattr(self, "samples_df") and self.samples_df is not None:
|
|
257
522
|
sample_colors = (
|
|
258
|
-
self.samples_df
|
|
259
|
-
.filter(pl.col("sample_uid").is_in(sample_uids_list))
|
|
523
|
+
self.samples_df.filter(pl.col("sample_uid").is_in(sample_uids_list))
|
|
260
524
|
.select(["sample_uid", "sample_color"])
|
|
261
525
|
.to_dict(as_series=False)
|
|
262
526
|
)
|
|
@@ -272,69 +536,112 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
272
536
|
# Add sample_color to data dictionaries before creating DataFrames
|
|
273
537
|
if before_data:
|
|
274
538
|
for item in before_data:
|
|
275
|
-
item[
|
|
276
|
-
|
|
539
|
+
item["sample_color"] = color_map.get(item["sample_idx"], "#1f77b4")
|
|
540
|
+
|
|
277
541
|
if after_data:
|
|
278
542
|
for item in after_data:
|
|
279
|
-
item[
|
|
280
|
-
|
|
543
|
+
item["sample_color"] = color_map.get(item["sample_idx"], "#1f77b4")
|
|
544
|
+
|
|
281
545
|
# Now create DataFrames with the sample_color already included
|
|
282
546
|
before_df = pd.DataFrame(before_data) if before_data else pd.DataFrame()
|
|
283
547
|
after_df = pd.DataFrame(after_data) if after_data else pd.DataFrame()
|
|
284
548
|
|
|
285
549
|
# Create Bokeh figures
|
|
286
|
-
p1 = figure(
|
|
550
|
+
p1 = figure(
|
|
551
|
+
width=width,
|
|
552
|
+
height=height,
|
|
553
|
+
title="Original RT",
|
|
554
|
+
x_axis_label="Retention Time (s)",
|
|
555
|
+
y_axis_label="m/z",
|
|
556
|
+
tools="pan,wheel_zoom,box_zoom,reset,save",
|
|
557
|
+
)
|
|
287
558
|
p1.outline_line_color = None
|
|
288
|
-
p1.background_fill_color =
|
|
289
|
-
p1.border_fill_color =
|
|
559
|
+
p1.background_fill_color = "white"
|
|
560
|
+
p1.border_fill_color = "white"
|
|
290
561
|
p1.min_border = 0
|
|
291
562
|
|
|
292
|
-
p2 = figure(
|
|
563
|
+
p2 = figure(
|
|
564
|
+
width=width,
|
|
565
|
+
height=height,
|
|
566
|
+
title="Current RT",
|
|
567
|
+
x_axis_label="Retention Time (s)",
|
|
568
|
+
y_axis_label="m/z",
|
|
569
|
+
tools="pan,wheel_zoom,box_zoom,reset,save",
|
|
570
|
+
x_range=p1.x_range,
|
|
571
|
+
y_range=p1.y_range,
|
|
572
|
+
)
|
|
293
573
|
p2.outline_line_color = None
|
|
294
|
-
p2.background_fill_color =
|
|
295
|
-
p2.border_fill_color =
|
|
574
|
+
p2.background_fill_color = "white"
|
|
575
|
+
p2.border_fill_color = "white"
|
|
296
576
|
p2.min_border = 0
|
|
297
|
-
|
|
577
|
+
|
|
298
578
|
# Get unique sample indices for iteration
|
|
299
|
-
unique_samples = sorted(list(
|
|
579
|
+
unique_samples = sorted(list({item["sample_idx"] for item in before_data})) if before_data else []
|
|
300
580
|
|
|
301
581
|
renderers_before = []
|
|
302
582
|
renderers_after = []
|
|
303
583
|
|
|
304
584
|
for sample_idx in unique_samples:
|
|
305
|
-
sb = before_df[before_df[
|
|
306
|
-
sa = after_df[after_df[
|
|
307
|
-
color = color_map.get(sample_idx,
|
|
585
|
+
sb = before_df[before_df["sample_idx"] == sample_idx]
|
|
586
|
+
sa = after_df[after_df["sample_idx"] == sample_idx]
|
|
587
|
+
color = color_map.get(sample_idx, "#000000")
|
|
308
588
|
|
|
309
589
|
if not sb.empty:
|
|
310
590
|
src = ColumnDataSource(sb)
|
|
311
|
-
r = p1.scatter(
|
|
591
|
+
r = p1.scatter("rt", "mz", size="size", color=color, alpha="alpha", source=src)
|
|
312
592
|
renderers_before.append(r)
|
|
313
593
|
|
|
314
594
|
if not sa.empty:
|
|
315
595
|
src = ColumnDataSource(sa)
|
|
316
|
-
r = p2.scatter(
|
|
596
|
+
r = p2.scatter("rt", "mz", size="size", color=color, alpha="alpha", source=src)
|
|
317
597
|
renderers_after.append(r)
|
|
318
598
|
|
|
319
599
|
# Add hover tools
|
|
320
|
-
hover1 = HoverTool(
|
|
600
|
+
hover1 = HoverTool(
|
|
601
|
+
tooltips=[
|
|
602
|
+
("Sample UID", "@sample_uid"),
|
|
603
|
+
("Sample Name", "@sample_name"),
|
|
604
|
+
("Sample Color", "$color[swatch]:sample_color"),
|
|
605
|
+
("RT", "@rt{0.00}"),
|
|
606
|
+
("m/z", "@mz{0.0000}"),
|
|
607
|
+
("Intensity", "@inty{0.0e0}"),
|
|
608
|
+
],
|
|
609
|
+
renderers=renderers_before,
|
|
610
|
+
)
|
|
321
611
|
p1.add_tools(hover1)
|
|
322
612
|
|
|
323
|
-
hover2 = HoverTool(
|
|
613
|
+
hover2 = HoverTool(
|
|
614
|
+
tooltips=[
|
|
615
|
+
("Sample UID", "@sample_uid"),
|
|
616
|
+
("Sample Name", "@sample_name"),
|
|
617
|
+
("Sample Color", "$color[swatch]:sample_color"),
|
|
618
|
+
("RT", "@rt{0.00}"),
|
|
619
|
+
("m/z", "@mz{0.0000}"),
|
|
620
|
+
("Intensity", "@inty{0.0e0}"),
|
|
621
|
+
],
|
|
622
|
+
renderers=renderers_after,
|
|
623
|
+
)
|
|
324
624
|
p2.add_tools(hover2)
|
|
325
625
|
|
|
326
626
|
# Create layout with both plots side by side
|
|
327
627
|
# Use the aliased bokeh_row and set sizing_mode, width and height to avoid validation warnings.
|
|
328
|
-
layout = bokeh_row(p1, p2, sizing_mode=
|
|
628
|
+
layout = bokeh_row(p1, p2, sizing_mode="fixed", width=width, height=height)
|
|
329
629
|
|
|
330
|
-
#
|
|
331
|
-
if filename:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
630
|
+
# Apply consistent save/display behavior
|
|
631
|
+
if filename is not None:
|
|
632
|
+
# Convert relative paths to absolute paths using study folder as base
|
|
633
|
+
import os
|
|
634
|
+
if not os.path.isabs(filename):
|
|
635
|
+
filename = os.path.join(self.folder, filename)
|
|
636
|
+
|
|
637
|
+
# Convert to absolute path for logging
|
|
638
|
+
abs_filename = os.path.abspath(filename)
|
|
639
|
+
|
|
640
|
+
# Use isolated file saving
|
|
641
|
+
_isolated_save_plot(layout, filename, abs_filename, self.logger, "Alignment Plot")
|
|
335
642
|
else:
|
|
336
|
-
|
|
337
|
-
|
|
643
|
+
# Show in notebook when no filename provided
|
|
644
|
+
_isolated_show_notebook(layout)
|
|
338
645
|
|
|
339
646
|
return layout
|
|
340
647
|
|
|
@@ -439,20 +746,21 @@ def plot_consensus_2d(
|
|
|
439
746
|
from bokeh.models import ColumnDataSource
|
|
440
747
|
from bokeh.models import HoverTool
|
|
441
748
|
from bokeh.models import LinearColorMapper
|
|
749
|
+
from bokeh.io.export import export_png
|
|
442
750
|
|
|
443
751
|
try:
|
|
444
752
|
from bokeh.models import ColorBar # type: ignore[attr-defined]
|
|
445
753
|
except ImportError:
|
|
446
754
|
from bokeh.models.annotations import ColorBar
|
|
447
755
|
from bokeh.palettes import viridis
|
|
448
|
-
|
|
756
|
+
|
|
449
757
|
# Import cmap for colormap handling
|
|
450
758
|
from cmap import Colormap
|
|
451
759
|
|
|
452
760
|
# Convert Polars DataFrame to pandas for Bokeh compatibility
|
|
453
761
|
data_pd = data.to_pandas()
|
|
454
762
|
source = ColumnDataSource(data_pd)
|
|
455
|
-
|
|
763
|
+
|
|
456
764
|
# Handle colormap using cmap.Colormap
|
|
457
765
|
try:
|
|
458
766
|
# Get colormap palette using cmap
|
|
@@ -461,6 +769,7 @@ def plot_consensus_2d(
|
|
|
461
769
|
# Generate 256 colors and convert to hex
|
|
462
770
|
import numpy as np
|
|
463
771
|
import matplotlib.colors as mcolors
|
|
772
|
+
|
|
464
773
|
colors = colormap(np.linspace(0, 1, 256))
|
|
465
774
|
palette = [mcolors.rgb2hex(color) for color in colors]
|
|
466
775
|
else:
|
|
@@ -473,19 +782,21 @@ def plot_consensus_2d(
|
|
|
473
782
|
# Fall back to generating colors manually
|
|
474
783
|
import numpy as np
|
|
475
784
|
import matplotlib.colors as mcolors
|
|
785
|
+
|
|
476
786
|
colors = colormap(np.linspace(0, 1, 256))
|
|
477
787
|
palette = [mcolors.rgb2hex(color) for color in colors]
|
|
478
788
|
except AttributeError:
|
|
479
789
|
# Fall back to generating colors manually
|
|
480
790
|
import numpy as np
|
|
481
791
|
import matplotlib.colors as mcolors
|
|
792
|
+
|
|
482
793
|
colors = colormap(np.linspace(0, 1, 256))
|
|
483
794
|
palette = [mcolors.rgb2hex(color) for color in colors]
|
|
484
795
|
except (AttributeError, ValueError, TypeError) as e:
|
|
485
796
|
# Fallback to viridis if cmap interpretation fails
|
|
486
797
|
self.logger.warning(f"Could not interpret colormap '{cmap}': {e}, falling back to viridis")
|
|
487
798
|
palette = viridis(256)
|
|
488
|
-
|
|
799
|
+
|
|
489
800
|
color_mapper = LinearColorMapper(
|
|
490
801
|
palette=palette,
|
|
491
802
|
low=data[colorby].min(),
|
|
@@ -549,8 +860,19 @@ def plot_consensus_2d(
|
|
|
549
860
|
p.add_layout(color_bar, "right")
|
|
550
861
|
|
|
551
862
|
if filename is not None:
|
|
552
|
-
|
|
553
|
-
|
|
863
|
+
# Convert relative paths to absolute paths using study folder as base
|
|
864
|
+
import os
|
|
865
|
+
if not os.path.isabs(filename):
|
|
866
|
+
filename = os.path.join(self.folder, filename)
|
|
867
|
+
|
|
868
|
+
# Convert to absolute path for logging
|
|
869
|
+
abs_filename = os.path.abspath(filename)
|
|
870
|
+
|
|
871
|
+
# Use isolated file saving
|
|
872
|
+
_isolated_save_plot(p, filename, abs_filename, self.logger, "Consensus 2D Plot")
|
|
873
|
+
else:
|
|
874
|
+
# Show in notebook when no filename provided
|
|
875
|
+
_isolated_show_notebook(p)
|
|
554
876
|
return p
|
|
555
877
|
|
|
556
878
|
|
|
@@ -603,8 +925,7 @@ def plot_samples_2d(
|
|
|
603
925
|
|
|
604
926
|
# Get sample colors from samples_df
|
|
605
927
|
sample_colors = (
|
|
606
|
-
self.samples_df
|
|
607
|
-
.filter(pl.col("sample_uid").is_in(sample_uids))
|
|
928
|
+
self.samples_df.filter(pl.col("sample_uid").is_in(sample_uids))
|
|
608
929
|
.select(["sample_uid", "sample_color"])
|
|
609
930
|
.to_dict(as_series=False)
|
|
610
931
|
)
|
|
@@ -770,17 +1091,22 @@ def plot_samples_2d(
|
|
|
770
1091
|
# Only set legend properties if a legend was actually created to avoid Bokeh warnings
|
|
771
1092
|
if getattr(p, "legend", None) and len(p.legend) > 0:
|
|
772
1093
|
p.legend.visible = False
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1094
|
+
|
|
1095
|
+
# Apply consistent save/display behavior
|
|
1096
|
+
if filename is not None:
|
|
1097
|
+
# Convert relative paths to absolute paths using study folder as base
|
|
1098
|
+
import os
|
|
1099
|
+
if not os.path.isabs(filename):
|
|
1100
|
+
filename = os.path.join(self.folder, filename)
|
|
1101
|
+
|
|
1102
|
+
# Convert to absolute path for logging
|
|
1103
|
+
abs_filename = os.path.abspath(filename)
|
|
1104
|
+
|
|
1105
|
+
# Use isolated file saving
|
|
1106
|
+
_isolated_save_plot(p, filename, abs_filename, self.logger, "Samples 2D Plot")
|
|
782
1107
|
else:
|
|
783
|
-
|
|
1108
|
+
# Show in notebook when no filename provided
|
|
1109
|
+
_isolated_show_notebook(p)
|
|
784
1110
|
return
|
|
785
1111
|
|
|
786
1112
|
|
|
@@ -794,7 +1120,7 @@ def plot_bpc(
|
|
|
794
1120
|
original: bool = False,
|
|
795
1121
|
):
|
|
796
1122
|
"""
|
|
797
|
-
Plot Base Peak Chromatograms (BPC) for selected samples
|
|
1123
|
+
Plot Base Peak Chromatograms (BPC) for selected samples overlaid using Bokeh.
|
|
798
1124
|
|
|
799
1125
|
This collects per-sample BPCs via `get_bpc(self, sample=uid)` and overlays them.
|
|
800
1126
|
Colors are mapped per-sample using the same Turbo256 palette as `plot_samples_2d`.
|
|
@@ -818,8 +1144,7 @@ def plot_bpc(
|
|
|
818
1144
|
|
|
819
1145
|
# Get sample colors from samples_df
|
|
820
1146
|
sample_colors = (
|
|
821
|
-
self.samples_df
|
|
822
|
-
.filter(pl.col("sample_uid").is_in(sample_uids))
|
|
1147
|
+
self.samples_df.filter(pl.col("sample_uid").is_in(sample_uids))
|
|
823
1148
|
.select(["sample_uid", "sample_color"])
|
|
824
1149
|
.to_dict(as_series=False)
|
|
825
1150
|
)
|
|
@@ -836,7 +1161,7 @@ def plot_bpc(
|
|
|
836
1161
|
for uid in sample_uids:
|
|
837
1162
|
try:
|
|
838
1163
|
first_chrom = get_bpc(self, sample=uid, label=None, original=original)
|
|
839
|
-
if hasattr(first_chrom,
|
|
1164
|
+
if hasattr(first_chrom, "rt_unit"):
|
|
840
1165
|
rt_unit = first_chrom.rt_unit
|
|
841
1166
|
break
|
|
842
1167
|
except Exception:
|
|
@@ -867,7 +1192,11 @@ def plot_bpc(
|
|
|
867
1192
|
# extract arrays
|
|
868
1193
|
try:
|
|
869
1194
|
# prefer Chromatogram API
|
|
870
|
-
chrom_dict =
|
|
1195
|
+
chrom_dict = (
|
|
1196
|
+
chrom.to_dict()
|
|
1197
|
+
if hasattr(chrom, "to_dict")
|
|
1198
|
+
else {"rt": getattr(chrom, "rt"), "inty": getattr(chrom, "inty")}
|
|
1199
|
+
)
|
|
871
1200
|
rt = chrom_dict.get("rt")
|
|
872
1201
|
inty = chrom_dict.get("inty")
|
|
873
1202
|
except Exception:
|
|
@@ -907,7 +1236,7 @@ def plot_bpc(
|
|
|
907
1236
|
|
|
908
1237
|
# Debug: log sample processing details
|
|
909
1238
|
self.logger.debug(
|
|
910
|
-
f"Processing BPC for sample_uid={uid}, sample_name={sample_name}, rt_len={rt.size}, color={color}"
|
|
1239
|
+
f"Processing BPC for sample_uid={uid}, sample_name={sample_name}, rt_len={rt.size}, color={color}",
|
|
911
1240
|
)
|
|
912
1241
|
|
|
913
1242
|
data = {"rt": rt, "inty": inty, "sample": [sample_name] * len(rt), "sample_color": [color] * len(rt)}
|
|
@@ -921,29 +1250,36 @@ def plot_bpc(
|
|
|
921
1250
|
self.logger.warning("No BPC curves to plot for the selected samples.")
|
|
922
1251
|
return
|
|
923
1252
|
|
|
924
|
-
hover = HoverTool(
|
|
1253
|
+
hover = HoverTool(
|
|
1254
|
+
tooltips=[
|
|
1255
|
+
("sample", "@sample"),
|
|
1256
|
+
("sample_color", "$color[swatch]:sample_color"),
|
|
1257
|
+
("rt", "@rt{0.00}"),
|
|
1258
|
+
("inty", "@inty{0.00e0}"),
|
|
1259
|
+
],
|
|
1260
|
+
renderers=renderers,
|
|
1261
|
+
)
|
|
925
1262
|
p.add_tools(hover)
|
|
926
1263
|
|
|
927
1264
|
# Only set legend properties if a legend was actually created to avoid Bokeh warnings
|
|
928
1265
|
if getattr(p, "legend", None) and len(p.legend) > 0:
|
|
929
1266
|
p.legend.visible = False
|
|
930
1267
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
output_file(filename)
|
|
944
|
-
show(p)
|
|
1268
|
+
# Apply consistent save/display behavior
|
|
1269
|
+
if filename is not None:
|
|
1270
|
+
# Convert relative paths to absolute paths using study folder as base
|
|
1271
|
+
import os
|
|
1272
|
+
if not os.path.isabs(filename):
|
|
1273
|
+
filename = os.path.join(self.folder, filename)
|
|
1274
|
+
|
|
1275
|
+
# Convert to absolute path for logging
|
|
1276
|
+
abs_filename = os.path.abspath(filename)
|
|
1277
|
+
|
|
1278
|
+
# Use isolated file saving
|
|
1279
|
+
_isolated_save_plot(p, filename, abs_filename, self.logger, "BPC Plot")
|
|
945
1280
|
else:
|
|
946
|
-
|
|
1281
|
+
# Show in notebook when no filename provided
|
|
1282
|
+
_isolated_show_notebook(p)
|
|
947
1283
|
|
|
948
1284
|
return p
|
|
949
1285
|
|
|
@@ -990,8 +1326,7 @@ def plot_eic(
|
|
|
990
1326
|
|
|
991
1327
|
# Get sample colors from samples_df
|
|
992
1328
|
sample_colors = (
|
|
993
|
-
self.samples_df
|
|
994
|
-
.filter(pl.col("sample_uid").is_in(sample_uids))
|
|
1329
|
+
self.samples_df.filter(pl.col("sample_uid").is_in(sample_uids))
|
|
995
1330
|
.select(["sample_uid", "sample_color"])
|
|
996
1331
|
.to_dict(as_series=False)
|
|
997
1332
|
)
|
|
@@ -1004,7 +1339,7 @@ def plot_eic(
|
|
|
1004
1339
|
for uid in sample_uids:
|
|
1005
1340
|
try:
|
|
1006
1341
|
first_chrom = get_eic(self, sample=uid, mz=mz, mz_tol=mz_tol, label=None)
|
|
1007
|
-
if hasattr(first_chrom,
|
|
1342
|
+
if hasattr(first_chrom, "rt_unit"):
|
|
1008
1343
|
rt_unit = first_chrom.rt_unit
|
|
1009
1344
|
break
|
|
1010
1345
|
except Exception:
|
|
@@ -1035,7 +1370,11 @@ def plot_eic(
|
|
|
1035
1370
|
# extract arrays
|
|
1036
1371
|
try:
|
|
1037
1372
|
# prefer Chromatogram API
|
|
1038
|
-
chrom_dict =
|
|
1373
|
+
chrom_dict = (
|
|
1374
|
+
chrom.to_dict()
|
|
1375
|
+
if hasattr(chrom, "to_dict")
|
|
1376
|
+
else {"rt": getattr(chrom, "rt"), "inty": getattr(chrom, "inty")}
|
|
1377
|
+
)
|
|
1039
1378
|
rt = chrom_dict.get("rt")
|
|
1040
1379
|
inty = chrom_dict.get("inty")
|
|
1041
1380
|
except Exception:
|
|
@@ -1083,27 +1422,35 @@ def plot_eic(
|
|
|
1083
1422
|
self.logger.warning("No EIC curves to plot for the selected samples.")
|
|
1084
1423
|
return
|
|
1085
1424
|
|
|
1086
|
-
hover = HoverTool(
|
|
1425
|
+
hover = HoverTool(
|
|
1426
|
+
tooltips=[
|
|
1427
|
+
("sample", "@sample"),
|
|
1428
|
+
("sample_color", "$color[swatch]:sample_color"),
|
|
1429
|
+
("rt", "@rt{0.00}"),
|
|
1430
|
+
("inty", "@inty{0.0e0}"),
|
|
1431
|
+
],
|
|
1432
|
+
renderers=renderers,
|
|
1433
|
+
)
|
|
1087
1434
|
p.add_tools(hover)
|
|
1088
1435
|
|
|
1089
1436
|
if getattr(p, "legend", None) and len(p.legend) > 0:
|
|
1090
1437
|
p.legend.visible = False
|
|
1091
1438
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
show(p)
|
|
1439
|
+
# Apply consistent save/display behavior
|
|
1440
|
+
if filename is not None:
|
|
1441
|
+
# Convert relative paths to absolute paths using study folder as base
|
|
1442
|
+
import os
|
|
1443
|
+
if not os.path.isabs(filename):
|
|
1444
|
+
filename = os.path.join(self.folder, filename)
|
|
1445
|
+
|
|
1446
|
+
# Convert to absolute path for logging
|
|
1447
|
+
abs_filename = os.path.abspath(filename)
|
|
1448
|
+
|
|
1449
|
+
# Use isolated file saving
|
|
1450
|
+
_isolated_save_plot(p, filename, abs_filename, self.logger, "EIC Plot")
|
|
1105
1451
|
else:
|
|
1106
|
-
|
|
1452
|
+
# Show in notebook when no filename provided
|
|
1453
|
+
_isolated_show_notebook(p)
|
|
1107
1454
|
|
|
1108
1455
|
return p
|
|
1109
1456
|
|
|
@@ -1117,7 +1464,7 @@ def plot_rt_correction(
|
|
|
1117
1464
|
height: int = 300,
|
|
1118
1465
|
):
|
|
1119
1466
|
"""
|
|
1120
|
-
Plot RT correction per sample: (rt - rt_original) vs rt
|
|
1467
|
+
Plot RT correction per sample: (rt - rt_original) vs rt overlaid for selected samples.
|
|
1121
1468
|
|
|
1122
1469
|
This uses the same color mapping as `plot_bpc` so curves for the same samples match.
|
|
1123
1470
|
"""
|
|
@@ -1141,8 +1488,7 @@ def plot_rt_correction(
|
|
|
1141
1488
|
|
|
1142
1489
|
# Get sample colors from samples_df
|
|
1143
1490
|
sample_colors = (
|
|
1144
|
-
self.samples_df
|
|
1145
|
-
.filter(pl.col("sample_uid").is_in(sample_uids))
|
|
1491
|
+
self.samples_df.filter(pl.col("sample_uid").is_in(sample_uids))
|
|
1146
1492
|
.select(["sample_uid", "sample_color"])
|
|
1147
1493
|
.to_dict(as_series=False)
|
|
1148
1494
|
)
|
|
@@ -1228,30 +1574,36 @@ def plot_rt_correction(
|
|
|
1228
1574
|
self.logger.warning("No RT correction curves to plot for the selected samples.")
|
|
1229
1575
|
return
|
|
1230
1576
|
|
|
1231
|
-
hover = HoverTool(
|
|
1577
|
+
hover = HoverTool(
|
|
1578
|
+
tooltips=[
|
|
1579
|
+
("sample", "@sample"),
|
|
1580
|
+
("sample_color", "$color[swatch]:sample_color"),
|
|
1581
|
+
("rt", "@rt{0.00}"),
|
|
1582
|
+
("rt - rt_original", "@delta{0.00}"),
|
|
1583
|
+
],
|
|
1584
|
+
renderers=renderers,
|
|
1585
|
+
)
|
|
1232
1586
|
p.add_tools(hover)
|
|
1233
1587
|
|
|
1234
1588
|
# Only set legend properties if a legend was actually created to avoid Bokeh warnings
|
|
1235
1589
|
if getattr(p, "legend", None) and len(p.legend) > 0:
|
|
1236
1590
|
p.legend.visible = False
|
|
1237
1591
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
else:
|
|
1251
|
-
output_file(filename)
|
|
1252
|
-
show(p)
|
|
1592
|
+
# Apply consistent save/display behavior
|
|
1593
|
+
if filename is not None:
|
|
1594
|
+
# Convert relative paths to absolute paths using study folder as base
|
|
1595
|
+
import os
|
|
1596
|
+
if not os.path.isabs(filename):
|
|
1597
|
+
filename = os.path.join(self.folder, filename)
|
|
1598
|
+
|
|
1599
|
+
# Convert to absolute path for logging
|
|
1600
|
+
abs_filename = os.path.abspath(filename)
|
|
1601
|
+
|
|
1602
|
+
# Use isolated file saving
|
|
1603
|
+
_isolated_save_plot(p, filename, abs_filename, self.logger, "RT Correction Plot")
|
|
1253
1604
|
else:
|
|
1254
|
-
|
|
1605
|
+
# Show in notebook when no filename provided
|
|
1606
|
+
_isolated_show_notebook(p)
|
|
1255
1607
|
|
|
1256
1608
|
return p
|
|
1257
1609
|
|
|
@@ -1280,10 +1632,11 @@ def plot_chrom(
|
|
|
1280
1632
|
if not sample_names:
|
|
1281
1633
|
self.logger.error("No sample names found in chromatogram data.")
|
|
1282
1634
|
return
|
|
1283
|
-
|
|
1635
|
+
|
|
1284
1636
|
# Create color mapping by getting sample_color for each sample_name
|
|
1285
|
-
samples_info = self.samples_df.select(["sample_name", "sample_color"]).to_dict(as_series=False)
|
|
1637
|
+
samples_info = self.samples_df.select(["sample_name", "sample_color", "sample_uid"]).to_dict(as_series=False)
|
|
1286
1638
|
sample_name_to_color = dict(zip(samples_info["sample_name"], samples_info["sample_color"]))
|
|
1639
|
+
sample_name_to_uid = dict(zip(samples_info["sample_name"], samples_info["sample_uid"]))
|
|
1287
1640
|
color_map = {name: sample_name_to_color.get(name, "#1f77b4") for name in sample_names} # fallback to blue
|
|
1288
1641
|
|
|
1289
1642
|
plots = []
|
|
@@ -1339,9 +1692,31 @@ def plot_chrom(
|
|
|
1339
1692
|
sorted_indices = np.argsort(rt)
|
|
1340
1693
|
rt = rt[sorted_indices]
|
|
1341
1694
|
inty = inty[sorted_indices]
|
|
1342
|
-
|
|
1695
|
+
|
|
1696
|
+
# Get sample uid for this sample name
|
|
1697
|
+
sample_uid = sample_name_to_uid.get(sample, None)
|
|
1698
|
+
sample_color = color_map.get(sample, "#1f77b4")
|
|
1699
|
+
|
|
1700
|
+
# Create arrays with sample information for hover tooltips
|
|
1701
|
+
sample_names_array = [sample] * len(rt)
|
|
1702
|
+
sample_uids_array = [sample_uid] * len(rt)
|
|
1703
|
+
sample_colors_array = [sample_color] * len(rt)
|
|
1704
|
+
|
|
1705
|
+
curve = hv.Curve(
|
|
1706
|
+
(rt, inty, sample_names_array, sample_uids_array, sample_colors_array),
|
|
1707
|
+
kdims=["RT"],
|
|
1708
|
+
vdims=["inty", "sample_name", "sample_uid", "sample_color"]
|
|
1709
|
+
).opts(
|
|
1343
1710
|
color=color_map[sample],
|
|
1344
1711
|
line_width=1,
|
|
1712
|
+
tools=["hover"],
|
|
1713
|
+
hover_tooltips=[
|
|
1714
|
+
("RT", "@RT{0.00}"),
|
|
1715
|
+
("Intensity", "@inty{0,0}"),
|
|
1716
|
+
("Sample Name", "@sample_name"),
|
|
1717
|
+
("Sample UID", "@sample_uid"),
|
|
1718
|
+
("Sample Color", "$color[swatch]:sample_color")
|
|
1719
|
+
]
|
|
1345
1720
|
)
|
|
1346
1721
|
curves.append(curve)
|
|
1347
1722
|
|
|
@@ -1400,23 +1775,30 @@ def plot_chrom(
|
|
|
1400
1775
|
# stack vertically.
|
|
1401
1776
|
# Stack all plots vertically in a Panel column
|
|
1402
1777
|
layout = panel.Column(*[panel.panel(plot) for plot in plots])
|
|
1778
|
+
|
|
1779
|
+
# Apply consistent save/display behavior
|
|
1403
1780
|
if filename is not None:
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
else:
|
|
1415
|
-
panel.panel(layout).save(filename, embed=True) # type: ignore[attr-defined]
|
|
1781
|
+
# Convert relative paths to absolute paths using study folder as base
|
|
1782
|
+
import os
|
|
1783
|
+
if not os.path.isabs(filename):
|
|
1784
|
+
filename = os.path.join(self.folder, filename)
|
|
1785
|
+
|
|
1786
|
+
# Convert to absolute path for logging
|
|
1787
|
+
abs_filename = os.path.abspath(filename)
|
|
1788
|
+
|
|
1789
|
+
# Use isolated Panel saving
|
|
1790
|
+
_isolated_save_panel_plot(panel.panel(layout), filename, abs_filename, self.logger, "Chromatogram Plot")
|
|
1416
1791
|
else:
|
|
1417
|
-
#
|
|
1418
|
-
#
|
|
1419
|
-
|
|
1792
|
+
# Show in notebook when no filename provided
|
|
1793
|
+
# Convert Panel layout to Bokeh layout for consistent isolated display
|
|
1794
|
+
try:
|
|
1795
|
+
panel_obj = panel.panel(layout)
|
|
1796
|
+
bokeh_layout = panel_obj.get_root()
|
|
1797
|
+
# Use the regular isolated show method for Bokeh objects
|
|
1798
|
+
_isolated_show_notebook(bokeh_layout)
|
|
1799
|
+
except Exception:
|
|
1800
|
+
# Fallback to Panel display if conversion fails
|
|
1801
|
+
_isolated_show_panel_notebook(panel.panel(layout))
|
|
1420
1802
|
|
|
1421
1803
|
|
|
1422
1804
|
def plot_consensus_stats(
|
|
@@ -1643,11 +2025,21 @@ def plot_consensus_stats(
|
|
|
1643
2025
|
if hasattr(grid, "border_fill_color"):
|
|
1644
2026
|
grid.border_fill_color = "white"
|
|
1645
2027
|
|
|
1646
|
-
#
|
|
1647
|
-
if filename:
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
2028
|
+
# Apply consistent save/display behavior
|
|
2029
|
+
if filename is not None:
|
|
2030
|
+
# Convert relative paths to absolute paths using study folder as base
|
|
2031
|
+
import os
|
|
2032
|
+
if not os.path.isabs(filename):
|
|
2033
|
+
filename = os.path.join(self.folder, filename)
|
|
2034
|
+
|
|
2035
|
+
# Convert to absolute path for logging
|
|
2036
|
+
abs_filename = os.path.abspath(filename)
|
|
2037
|
+
|
|
2038
|
+
# Use isolated file saving
|
|
2039
|
+
_isolated_save_plot(grid, filename, abs_filename, self.logger, "Consensus Stats Plot")
|
|
2040
|
+
else:
|
|
2041
|
+
# Show in notebook when no filename provided
|
|
2042
|
+
_isolated_show_notebook(grid)
|
|
1651
2043
|
return grid
|
|
1652
2044
|
|
|
1653
2045
|
|
|
@@ -1679,6 +2071,7 @@ def plot_pca(
|
|
|
1679
2071
|
from bokeh.plotting import figure, show, output_file
|
|
1680
2072
|
from bokeh.palettes import Category20, viridis
|
|
1681
2073
|
from bokeh.transform import factor_cmap
|
|
2074
|
+
from bokeh.io.export import export_png
|
|
1682
2075
|
from sklearn.decomposition import PCA
|
|
1683
2076
|
from sklearn.preprocessing import StandardScaler
|
|
1684
2077
|
import pandas as pd
|
|
@@ -1702,21 +2095,22 @@ def plot_pca(
|
|
|
1702
2095
|
|
|
1703
2096
|
self.logger.debug(f"Performing PCA on consensus matrix with shape: {consensus_matrix.shape}")
|
|
1704
2097
|
|
|
1705
|
-
#
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
else:
|
|
1713
|
-
# Pandas DataFrame
|
|
1714
|
-
matrix_data = consensus_matrix.to_numpy()
|
|
1715
|
-
elif hasattr(consensus_matrix, "values"):
|
|
1716
|
-
# Pandas DataFrame
|
|
1717
|
-
matrix_data = consensus_matrix.values
|
|
2098
|
+
# Extract only the sample columns (exclude consensus_uid column)
|
|
2099
|
+
sample_cols = [col for col in consensus_matrix.columns if col != "consensus_uid"]
|
|
2100
|
+
|
|
2101
|
+
# Convert consensus matrix to numpy, excluding the consensus_uid column
|
|
2102
|
+
if hasattr(consensus_matrix, "select"):
|
|
2103
|
+
# Polars DataFrame
|
|
2104
|
+
matrix_data = consensus_matrix.select(sample_cols).to_numpy()
|
|
1718
2105
|
else:
|
|
1719
|
-
|
|
2106
|
+
# Pandas DataFrame or other - drop consensus_uid column
|
|
2107
|
+
matrix_sample_data = consensus_matrix.drop(columns=["consensus_uid"], errors="ignore")
|
|
2108
|
+
if hasattr(matrix_sample_data, "values"):
|
|
2109
|
+
matrix_data = matrix_sample_data.values
|
|
2110
|
+
elif hasattr(matrix_sample_data, "to_numpy"):
|
|
2111
|
+
matrix_data = matrix_sample_data.to_numpy()
|
|
2112
|
+
else:
|
|
2113
|
+
matrix_data = np.array(matrix_sample_data)
|
|
1720
2114
|
|
|
1721
2115
|
# Transpose matrix so samples are rows and features are columns
|
|
1722
2116
|
matrix_data = matrix_data.T
|
|
@@ -1753,7 +2147,7 @@ def plot_pca(
|
|
|
1753
2147
|
else:
|
|
1754
2148
|
self.logger.warning(
|
|
1755
2149
|
f"Sample count mismatch: samples_df has {len(samples_pd)} rows, "
|
|
1756
|
-
f"but consensus matrix has {len(pca_df)} samples"
|
|
2150
|
+
f"but consensus matrix has {len(pca_df)} samples",
|
|
1757
2151
|
)
|
|
1758
2152
|
|
|
1759
2153
|
# Prepare color mapping
|
|
@@ -1824,25 +2218,23 @@ def plot_pca(
|
|
|
1824
2218
|
if "sample_uid" in pca_df.columns or "sample_name" in pca_df.columns:
|
|
1825
2219
|
# Choose the identifier to map colors by
|
|
1826
2220
|
id_col = "sample_uid" if "sample_uid" in pca_df.columns else "sample_name"
|
|
1827
|
-
|
|
2221
|
+
|
|
1828
2222
|
# Get colors from samples_df based on the identifier
|
|
1829
2223
|
if id_col == "sample_uid":
|
|
1830
2224
|
sample_colors = (
|
|
1831
|
-
self.samples_df
|
|
1832
|
-
.filter(pl.col("sample_uid").is_in(pca_df[id_col].unique()))
|
|
2225
|
+
self.samples_df.filter(pl.col("sample_uid").is_in(pca_df[id_col].unique()))
|
|
1833
2226
|
.select(["sample_uid", "sample_color"])
|
|
1834
2227
|
.to_dict(as_series=False)
|
|
1835
2228
|
)
|
|
1836
2229
|
color_map = dict(zip(sample_colors["sample_uid"], sample_colors["sample_color"]))
|
|
1837
2230
|
else: # sample_name
|
|
1838
2231
|
sample_colors = (
|
|
1839
|
-
self.samples_df
|
|
1840
|
-
.filter(pl.col("sample_name").is_in(pca_df[id_col].unique()))
|
|
2232
|
+
self.samples_df.filter(pl.col("sample_name").is_in(pca_df[id_col].unique()))
|
|
1841
2233
|
.select(["sample_name", "sample_color"])
|
|
1842
2234
|
.to_dict(as_series=False)
|
|
1843
2235
|
)
|
|
1844
2236
|
color_map = dict(zip(sample_colors["sample_name"], sample_colors["sample_color"]))
|
|
1845
|
-
|
|
2237
|
+
|
|
1846
2238
|
# Map colors into dataframe
|
|
1847
2239
|
pca_df["color"] = [color_map.get(x, "#1f77b4") for x in pca_df[id_col]] # fallback to blue
|
|
1848
2240
|
# Update the ColumnDataSource with new color column
|
|
@@ -1878,7 +2270,7 @@ def plot_pca(
|
|
|
1878
2270
|
if col in pca_df.columns:
|
|
1879
2271
|
if col == "sample_color":
|
|
1880
2272
|
# Display sample_color as a colored swatch
|
|
1881
|
-
tooltip_list.append((
|
|
2273
|
+
tooltip_list.append(("color", "$color[swatch]:sample_color"))
|
|
1882
2274
|
elif pca_df[col].dtype in ["float64", "float32"]:
|
|
1883
2275
|
tooltip_list.append((col, f"@{col}{{0.00}}"))
|
|
1884
2276
|
else:
|
|
@@ -1897,13 +2289,24 @@ def plot_pca(
|
|
|
1897
2289
|
p.legend.location = "top_left"
|
|
1898
2290
|
p.legend.click_policy = "hide"
|
|
1899
2291
|
|
|
1900
|
-
#
|
|
1901
|
-
if filename:
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2292
|
+
# Apply consistent save/display behavior
|
|
2293
|
+
if filename is not None:
|
|
2294
|
+
# Convert relative paths to absolute paths using study folder as base
|
|
2295
|
+
import os
|
|
2296
|
+
if not os.path.isabs(filename):
|
|
2297
|
+
filename = os.path.join(self.folder, filename)
|
|
2298
|
+
|
|
2299
|
+
# Convert to absolute path for logging
|
|
2300
|
+
abs_filename = os.path.abspath(filename)
|
|
2301
|
+
|
|
2302
|
+
# Use isolated file saving
|
|
2303
|
+
_isolated_save_plot(p, filename, abs_filename, self.logger, "PCA Plot")
|
|
2304
|
+
else:
|
|
2305
|
+
# Show in notebook when no filename provided
|
|
2306
|
+
_isolated_show_notebook(p)
|
|
1905
2307
|
return p
|
|
1906
2308
|
|
|
2309
|
+
|
|
1907
2310
|
def plot_tic(
|
|
1908
2311
|
self,
|
|
1909
2312
|
samples=None,
|
|
@@ -1914,7 +2317,7 @@ def plot_tic(
|
|
|
1914
2317
|
original: bool = False,
|
|
1915
2318
|
):
|
|
1916
2319
|
"""
|
|
1917
|
-
Plot Total Ion Chromatograms (TIC) for selected samples
|
|
2320
|
+
Plot Total Ion Chromatograms (TIC) for selected samples overlaid using Bokeh.
|
|
1918
2321
|
|
|
1919
2322
|
Parameters and behavior mirror `plot_bpc` but use per-sample TICs (get_tic).
|
|
1920
2323
|
"""
|
|
@@ -1931,8 +2334,7 @@ def plot_tic(
|
|
|
1931
2334
|
|
|
1932
2335
|
# Get sample colors from samples_df
|
|
1933
2336
|
sample_colors = (
|
|
1934
|
-
self.samples_df
|
|
1935
|
-
.filter(pl.col("sample_uid").is_in(sample_uids))
|
|
2337
|
+
self.samples_df.filter(pl.col("sample_uid").is_in(sample_uids))
|
|
1936
2338
|
.select(["sample_uid", "sample_color"])
|
|
1937
2339
|
.to_dict(as_series=False)
|
|
1938
2340
|
)
|
|
@@ -1945,7 +2347,7 @@ def plot_tic(
|
|
|
1945
2347
|
for uid in sample_uids:
|
|
1946
2348
|
try:
|
|
1947
2349
|
first_chrom = get_tic(self, sample=uid, label=None)
|
|
1948
|
-
if hasattr(first_chrom,
|
|
2350
|
+
if hasattr(first_chrom, "rt_unit"):
|
|
1949
2351
|
rt_unit = first_chrom.rt_unit
|
|
1950
2352
|
break
|
|
1951
2353
|
except Exception:
|
|
@@ -1974,7 +2376,11 @@ def plot_tic(
|
|
|
1974
2376
|
|
|
1975
2377
|
# extract arrays
|
|
1976
2378
|
try:
|
|
1977
|
-
chrom_dict =
|
|
2379
|
+
chrom_dict = (
|
|
2380
|
+
chrom.to_dict()
|
|
2381
|
+
if hasattr(chrom, "to_dict")
|
|
2382
|
+
else {"rt": getattr(chrom, "rt"), "inty": getattr(chrom, "inty")}
|
|
2383
|
+
)
|
|
1978
2384
|
rt = chrom_dict.get("rt")
|
|
1979
2385
|
inty = chrom_dict.get("inty")
|
|
1980
2386
|
except Exception:
|
|
@@ -2022,27 +2428,35 @@ def plot_tic(
|
|
|
2022
2428
|
self.logger.warning("No TIC curves to plot for the selected samples.")
|
|
2023
2429
|
return
|
|
2024
2430
|
|
|
2025
|
-
hover = HoverTool(
|
|
2431
|
+
hover = HoverTool(
|
|
2432
|
+
tooltips=[
|
|
2433
|
+
("sample", "@sample"),
|
|
2434
|
+
("sample_color", "$color[swatch]:sample_color"),
|
|
2435
|
+
("rt", "@rt{0.00}"),
|
|
2436
|
+
("inty", "@inty{0.00e0}"),
|
|
2437
|
+
],
|
|
2438
|
+
renderers=renderers,
|
|
2439
|
+
)
|
|
2026
2440
|
p.add_tools(hover)
|
|
2027
2441
|
|
|
2028
2442
|
# Only set legend properties if a legend was actually created to avoid Bokeh warnings
|
|
2029
2443
|
if getattr(p, "legend", None) and len(p.legend) > 0:
|
|
2030
2444
|
p.legend.visible = False
|
|
2031
2445
|
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
show(p)
|
|
2446
|
+
# Apply consistent save/display behavior
|
|
2447
|
+
if filename is not None:
|
|
2448
|
+
# Convert relative paths to absolute paths using study folder as base
|
|
2449
|
+
import os
|
|
2450
|
+
if not os.path.isabs(filename):
|
|
2451
|
+
filename = os.path.join(self.folder, filename)
|
|
2452
|
+
|
|
2453
|
+
# Convert to absolute path for logging
|
|
2454
|
+
abs_filename = os.path.abspath(filename)
|
|
2455
|
+
|
|
2456
|
+
# Use isolated file saving
|
|
2457
|
+
_isolated_save_plot(p, filename, abs_filename, self.logger, "TIC Plot")
|
|
2045
2458
|
else:
|
|
2046
|
-
|
|
2459
|
+
# Show in notebook when no filename provided
|
|
2460
|
+
_isolated_show_notebook(p)
|
|
2047
2461
|
|
|
2048
2462
|
return p
|