masster 0.3.19__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/lib.py +32 -25
- masster/sample/load.py +7 -1
- masster/sample/plot.py +111 -26
- masster/study/helpers.py +230 -6
- masster/study/plot.py +457 -182
- masster/study/study.py +4 -0
- {masster-0.3.19.dist-info → masster-0.3.20.dist-info}/METADATA +1 -1
- {masster-0.3.19.dist-info → masster-0.3.20.dist-info}/RECORD +24 -18
- /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.19.dist-info → masster-0.3.20.dist-info}/WHEEL +0 -0
- {masster-0.3.19.dist-info → masster-0.3.20.dist-info}/entry_points.txt +0 -0
- {masster-0.3.19.dist-info → masster-0.3.20.dist-info}/licenses/LICENSE +0 -0
masster/study/plot.py
CHANGED
|
@@ -17,18 +17,222 @@ hv.extension("bokeh")
|
|
|
17
17
|
from bokeh.layouts import row as bokeh_row
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
|
|
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
|
+
|
|
21
227
|
def plot_alignment(
|
|
22
228
|
self,
|
|
229
|
+
samples=None,
|
|
23
230
|
maps: bool = True,
|
|
24
231
|
filename: str | None = None,
|
|
25
232
|
width: int = 450,
|
|
26
233
|
height: int = 450,
|
|
27
234
|
markersize: int = 3,
|
|
28
235
|
):
|
|
29
|
-
=======
|
|
30
|
-
def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | None = None, filename: str | None = None, width: int = 450, height: int = 450, markersize: int = 3):
|
|
31
|
-
>>>>>>> Stashed changes
|
|
32
236
|
"""Visualize retention time alignment using two synchronized Bokeh scatter plots.
|
|
33
237
|
|
|
34
238
|
- When ``maps=True`` the function reads ``self.features_maps`` (list of FeatureMap)
|
|
@@ -37,12 +241,8 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
37
241
|
``rt_original`` column (before) and ``rt`` column (after).
|
|
38
242
|
|
|
39
243
|
Parameters
|
|
244
|
+
- samples: List of sample identifiers (sample_uids or sample_names), or single int for random selection, or None for all samples.
|
|
40
245
|
- maps: whether to use feature maps (default True).
|
|
41
|
-
- samples: Sample selection parameter, interpreted like in plot_samples_2d:
|
|
42
|
-
- None: show all samples
|
|
43
|
-
- int: show a random subset of N samples
|
|
44
|
-
- list of ints: show samples with these sample_uids
|
|
45
|
-
- list of strings: show samples with these sample_names
|
|
46
246
|
- filename: optional HTML file path to save the plot.
|
|
47
247
|
- width/height: pixel size of each subplot.
|
|
48
248
|
- markersize: base marker size.
|
|
@@ -55,6 +255,9 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
55
255
|
from bokeh.plotting import figure, show, output_file
|
|
56
256
|
import pandas as pd
|
|
57
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
|
+
|
|
58
261
|
# Build the before/after tabular data used for plotting
|
|
59
262
|
before_data: list[dict[str, Any]] = []
|
|
60
263
|
after_data: list[dict[str, Any]] = []
|
|
@@ -70,31 +273,39 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
70
273
|
self.logger.error("No feature maps available for plotting.")
|
|
71
274
|
return
|
|
72
275
|
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if sample_uids_to_show is not None:
|
|
78
|
-
# Get sample indices for the selected sample_uids
|
|
79
|
-
selected_indices = []
|
|
80
|
-
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():
|
|
81
280
|
samples_info = self.samples_df.to_pandas()
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
85
303
|
else:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
# Filter feature maps to only include selected indices
|
|
93
|
-
fmaps = [fmaps[i] for i in selected_indices if i < len(fmaps)]
|
|
94
|
-
|
|
95
|
-
if not fmaps:
|
|
96
|
-
self.logger.error("No feature maps match the selected samples.")
|
|
97
|
-
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
|
|
98
309
|
|
|
99
310
|
# Reference (first) sample: use current RT for both before and after
|
|
100
311
|
ref = fmaps[0]
|
|
@@ -103,9 +314,10 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
103
314
|
ref_inty = [f.getIntensity() for f in ref]
|
|
104
315
|
max_ref_inty = max(ref_inty) if ref_inty else 1
|
|
105
316
|
|
|
106
|
-
# sample metadata
|
|
317
|
+
# Get sample metadata for reference (first) sample
|
|
107
318
|
if hasattr(self, "samples_df") and self.samples_df is not None and not self.samples_df.is_empty():
|
|
108
|
-
samples_info
|
|
319
|
+
if 'samples_info' not in locals():
|
|
320
|
+
samples_info = self.samples_df.to_pandas()
|
|
109
321
|
ref_sample_uid = (
|
|
110
322
|
samples_info.iloc[0]["sample_uid"] if "sample_uid" in samples_info.columns else "Reference_UID"
|
|
111
323
|
)
|
|
@@ -138,7 +350,7 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
138
350
|
"size": markersize + 2,
|
|
139
351
|
})
|
|
140
352
|
|
|
141
|
-
# Remaining samples
|
|
353
|
+
# Remaining samples - now using filtered feature maps and samples_info
|
|
142
354
|
for sample_idx, fm in enumerate(fmaps[1:], start=1):
|
|
143
355
|
mz_vals = []
|
|
144
356
|
inty_vals = []
|
|
@@ -165,14 +377,21 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
165
377
|
|
|
166
378
|
max_inty = max(inty_vals)
|
|
167
379
|
|
|
380
|
+
# Get sample metadata from filtered samples_info
|
|
168
381
|
if hasattr(self, "samples_df") and self.samples_df is not None and not self.samples_df.is_empty():
|
|
169
|
-
samples_info
|
|
170
|
-
if sample_idx < len(samples_info):
|
|
382
|
+
# Use filtered samples_info if it exists from the filtering above
|
|
383
|
+
if 'samples_info' in locals() and sample_idx < len(samples_info):
|
|
171
384
|
sample_name = samples_info.iloc[sample_idx].get("sample_name", f"Sample {sample_idx}")
|
|
172
385
|
sample_uid = samples_info.iloc[sample_idx].get("sample_uid", f"Sample_{sample_idx}_UID")
|
|
173
386
|
else:
|
|
174
|
-
|
|
175
|
-
|
|
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"
|
|
176
395
|
else:
|
|
177
396
|
sample_name = f"Sample {sample_idx}"
|
|
178
397
|
sample_uid = f"Sample_{sample_idx}_UID"
|
|
@@ -219,34 +438,19 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
219
438
|
|
|
220
439
|
# Use Polars instead of pandas
|
|
221
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
|
|
222
448
|
|
|
223
449
|
sample_col = "sample_uid" if "sample_uid" in features_df.columns else "sample_name"
|
|
224
450
|
if sample_col not in features_df.columns:
|
|
225
451
|
self.logger.error("No sample identifier column found in features_df.")
|
|
226
452
|
return
|
|
227
453
|
|
|
228
|
-
# Get sample_uids to limit which samples to show
|
|
229
|
-
sample_uids_to_show = self._get_sample_uids(samples)
|
|
230
|
-
|
|
231
|
-
# Filter features_df based on sample selection if specified
|
|
232
|
-
if sample_uids_to_show is not None:
|
|
233
|
-
if sample_col == 'sample_uid':
|
|
234
|
-
features_df = features_df.filter(pl.col('sample_uid').is_in(sample_uids_to_show))
|
|
235
|
-
else:
|
|
236
|
-
# Need to convert sample names to sample_uids if using sample_name column
|
|
237
|
-
if 'sample_uid' in features_df.columns:
|
|
238
|
-
# Filter by sample_uid even though we're using sample_name as the primary column
|
|
239
|
-
features_df = features_df.filter(pl.col('sample_uid').is_in(sample_uids_to_show))
|
|
240
|
-
else:
|
|
241
|
-
# Convert sample_uids to sample_names and filter
|
|
242
|
-
sample_names_to_show = []
|
|
243
|
-
if hasattr(self, 'samples_df') and self.samples_df is not None:
|
|
244
|
-
for uid in sample_uids_to_show:
|
|
245
|
-
matching_rows = self.samples_df.filter(pl.col("sample_uid") == uid)
|
|
246
|
-
if not matching_rows.is_empty():
|
|
247
|
-
sample_names_to_show.append(matching_rows.row(0, named=True)["sample_name"])
|
|
248
|
-
features_df = features_df.filter(pl.col('sample_name').is_in(sample_names_to_show))
|
|
249
|
-
|
|
250
454
|
# Get unique samples using Polars
|
|
251
455
|
samples = features_df.select(pl.col(sample_col)).unique().to_series().to_list()
|
|
252
456
|
|
|
@@ -423,16 +627,21 @@ def plot_alignment(self, maps: bool = True, samples: int | list[int | str] | Non
|
|
|
423
627
|
# Use the aliased bokeh_row and set sizing_mode, width and height to avoid validation warnings.
|
|
424
628
|
layout = bokeh_row(p1, p2, sizing_mode="fixed", width=width, height=height)
|
|
425
629
|
|
|
426
|
-
#
|
|
427
|
-
if filename:
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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")
|
|
432
642
|
else:
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
show(layout)
|
|
643
|
+
# Show in notebook when no filename provided
|
|
644
|
+
_isolated_show_notebook(layout)
|
|
436
645
|
|
|
437
646
|
return layout
|
|
438
647
|
|
|
@@ -537,6 +746,7 @@ def plot_consensus_2d(
|
|
|
537
746
|
from bokeh.models import ColumnDataSource
|
|
538
747
|
from bokeh.models import HoverTool
|
|
539
748
|
from bokeh.models import LinearColorMapper
|
|
749
|
+
from bokeh.io.export import export_png
|
|
540
750
|
|
|
541
751
|
try:
|
|
542
752
|
from bokeh.models import ColorBar # type: ignore[attr-defined]
|
|
@@ -650,8 +860,19 @@ def plot_consensus_2d(
|
|
|
650
860
|
p.add_layout(color_bar, "right")
|
|
651
861
|
|
|
652
862
|
if filename is not None:
|
|
653
|
-
|
|
654
|
-
|
|
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)
|
|
655
876
|
return p
|
|
656
877
|
|
|
657
878
|
|
|
@@ -870,17 +1091,22 @@ def plot_samples_2d(
|
|
|
870
1091
|
# Only set legend properties if a legend was actually created to avoid Bokeh warnings
|
|
871
1092
|
if getattr(p, "legend", None) and len(p.legend) > 0:
|
|
872
1093
|
p.legend.visible = False
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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")
|
|
882
1107
|
else:
|
|
883
|
-
|
|
1108
|
+
# Show in notebook when no filename provided
|
|
1109
|
+
_isolated_show_notebook(p)
|
|
884
1110
|
return
|
|
885
1111
|
|
|
886
1112
|
|
|
@@ -1039,22 +1265,21 @@ def plot_bpc(
|
|
|
1039
1265
|
if getattr(p, "legend", None) and len(p.legend) > 0:
|
|
1040
1266
|
p.legend.visible = False
|
|
1041
1267
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
output_file(filename)
|
|
1055
|
-
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")
|
|
1056
1280
|
else:
|
|
1057
|
-
|
|
1281
|
+
# Show in notebook when no filename provided
|
|
1282
|
+
_isolated_show_notebook(p)
|
|
1058
1283
|
|
|
1059
1284
|
return p
|
|
1060
1285
|
|
|
@@ -1211,21 +1436,21 @@ def plot_eic(
|
|
|
1211
1436
|
if getattr(p, "legend", None) and len(p.legend) > 0:
|
|
1212
1437
|
p.legend.visible = False
|
|
1213
1438
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
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")
|
|
1227
1451
|
else:
|
|
1228
|
-
|
|
1452
|
+
# Show in notebook when no filename provided
|
|
1453
|
+
_isolated_show_notebook(p)
|
|
1229
1454
|
|
|
1230
1455
|
return p
|
|
1231
1456
|
|
|
@@ -1364,23 +1589,21 @@ def plot_rt_correction(
|
|
|
1364
1589
|
if getattr(p, "legend", None) and len(p.legend) > 0:
|
|
1365
1590
|
p.legend.visible = False
|
|
1366
1591
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
else:
|
|
1380
|
-
output_file(filename)
|
|
1381
|
-
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")
|
|
1382
1604
|
else:
|
|
1383
|
-
|
|
1605
|
+
# Show in notebook when no filename provided
|
|
1606
|
+
_isolated_show_notebook(p)
|
|
1384
1607
|
|
|
1385
1608
|
return p
|
|
1386
1609
|
|
|
@@ -1411,8 +1634,9 @@ def plot_chrom(
|
|
|
1411
1634
|
return
|
|
1412
1635
|
|
|
1413
1636
|
# Create color mapping by getting sample_color for each sample_name
|
|
1414
|
-
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)
|
|
1415
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"]))
|
|
1416
1640
|
color_map = {name: sample_name_to_color.get(name, "#1f77b4") for name in sample_names} # fallback to blue
|
|
1417
1641
|
|
|
1418
1642
|
plots = []
|
|
@@ -1468,9 +1692,31 @@ def plot_chrom(
|
|
|
1468
1692
|
sorted_indices = np.argsort(rt)
|
|
1469
1693
|
rt = rt[sorted_indices]
|
|
1470
1694
|
inty = inty[sorted_indices]
|
|
1471
|
-
|
|
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(
|
|
1472
1710
|
color=color_map[sample],
|
|
1473
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
|
+
]
|
|
1474
1720
|
)
|
|
1475
1721
|
curves.append(curve)
|
|
1476
1722
|
|
|
@@ -1529,23 +1775,30 @@ def plot_chrom(
|
|
|
1529
1775
|
# stack vertically.
|
|
1530
1776
|
# Stack all plots vertically in a Panel column
|
|
1531
1777
|
layout = panel.Column(*[panel.panel(plot) for plot in plots])
|
|
1778
|
+
|
|
1779
|
+
# Apply consistent save/display behavior
|
|
1532
1780
|
if filename is not None:
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
else:
|
|
1544
|
-
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")
|
|
1545
1791
|
else:
|
|
1546
|
-
#
|
|
1547
|
-
#
|
|
1548
|
-
|
|
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))
|
|
1549
1802
|
|
|
1550
1803
|
|
|
1551
1804
|
def plot_consensus_stats(
|
|
@@ -1772,11 +2025,21 @@ def plot_consensus_stats(
|
|
|
1772
2025
|
if hasattr(grid, "border_fill_color"):
|
|
1773
2026
|
grid.border_fill_color = "white"
|
|
1774
2027
|
|
|
1775
|
-
#
|
|
1776
|
-
if filename:
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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)
|
|
1780
2043
|
return grid
|
|
1781
2044
|
|
|
1782
2045
|
|
|
@@ -1808,6 +2071,7 @@ def plot_pca(
|
|
|
1808
2071
|
from bokeh.plotting import figure, show, output_file
|
|
1809
2072
|
from bokeh.palettes import Category20, viridis
|
|
1810
2073
|
from bokeh.transform import factor_cmap
|
|
2074
|
+
from bokeh.io.export import export_png
|
|
1811
2075
|
from sklearn.decomposition import PCA
|
|
1812
2076
|
from sklearn.preprocessing import StandardScaler
|
|
1813
2077
|
import pandas as pd
|
|
@@ -1831,21 +2095,22 @@ def plot_pca(
|
|
|
1831
2095
|
|
|
1832
2096
|
self.logger.debug(f"Performing PCA on consensus matrix with shape: {consensus_matrix.shape}")
|
|
1833
2097
|
|
|
1834
|
-
#
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
else:
|
|
1842
|
-
# Pandas DataFrame
|
|
1843
|
-
matrix_data = consensus_matrix.to_numpy()
|
|
1844
|
-
elif hasattr(consensus_matrix, "values"):
|
|
1845
|
-
# Pandas DataFrame
|
|
1846
|
-
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()
|
|
1847
2105
|
else:
|
|
1848
|
-
|
|
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)
|
|
1849
2114
|
|
|
1850
2115
|
# Transpose matrix so samples are rows and features are columns
|
|
1851
2116
|
matrix_data = matrix_data.T
|
|
@@ -2024,11 +2289,21 @@ def plot_pca(
|
|
|
2024
2289
|
p.legend.location = "top_left"
|
|
2025
2290
|
p.legend.click_policy = "hide"
|
|
2026
2291
|
|
|
2027
|
-
#
|
|
2028
|
-
if filename:
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
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)
|
|
2032
2307
|
return p
|
|
2033
2308
|
|
|
2034
2309
|
|
|
@@ -2168,20 +2443,20 @@ def plot_tic(
|
|
|
2168
2443
|
if getattr(p, "legend", None) and len(p.legend) > 0:
|
|
2169
2444
|
p.legend.visible = False
|
|
2170
2445
|
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
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")
|
|
2184
2458
|
else:
|
|
2185
|
-
|
|
2459
|
+
# Show in notebook when no filename provided
|
|
2460
|
+
_isolated_show_notebook(p)
|
|
2186
2461
|
|
|
2187
2462
|
return p
|