masster 0.4.0__py3-none-any.whl → 0.4.1__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.
- masster/__init__.py +8 -8
- masster/_version.py +1 -1
- masster/chromatogram.py +3 -9
- masster/data/libs/README.md +1 -1
- masster/data/libs/ccm.csv +120 -120
- masster/data/libs/ccm.py +116 -62
- masster/data/libs/central_carbon_README.md +1 -1
- masster/data/libs/urine.py +161 -65
- masster/data/libs/urine_metabolites.csv +4693 -4693
- masster/data/wiff/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.mzML +2 -2
- masster/logger.py +43 -78
- masster/sample/__init__.py +1 -1
- masster/sample/adducts.py +264 -338
- masster/sample/defaults/find_adducts_def.py +8 -21
- masster/sample/defaults/find_features_def.py +1 -6
- masster/sample/defaults/get_spectrum_def.py +1 -5
- masster/sample/defaults/sample_def.py +1 -5
- masster/sample/h5.py +282 -561
- masster/sample/helpers.py +75 -131
- masster/sample/lib.py +17 -42
- masster/sample/load.py +17 -31
- masster/sample/parameters.py +2 -6
- masster/sample/plot.py +27 -88
- masster/sample/processing.py +87 -117
- masster/sample/quant.py +51 -57
- masster/sample/sample.py +90 -103
- masster/sample/sample5_schema.json +44 -44
- masster/sample/save.py +12 -35
- masster/sample/sciex.py +19 -66
- masster/spectrum.py +20 -58
- masster/study/__init__.py +1 -1
- masster/study/defaults/align_def.py +1 -5
- masster/study/defaults/fill_chrom_def.py +1 -5
- masster/study/defaults/fill_def.py +1 -5
- masster/study/defaults/integrate_chrom_def.py +1 -5
- masster/study/defaults/integrate_def.py +1 -5
- masster/study/defaults/study_def.py +25 -58
- masster/study/export.py +207 -233
- masster/study/h5.py +136 -470
- masster/study/helpers.py +202 -495
- masster/study/helpers_optimized.py +13 -40
- masster/study/id.py +110 -213
- masster/study/load.py +143 -230
- masster/study/plot.py +257 -518
- masster/study/processing.py +257 -469
- masster/study/save.py +5 -15
- masster/study/study.py +276 -379
- masster/study/study5_schema.json +96 -96
- {masster-0.4.0.dist-info → masster-0.4.1.dist-info}/METADATA +1 -1
- masster-0.4.1.dist-info/RECORD +67 -0
- masster-0.4.0.dist-info/RECORD +0 -67
- {masster-0.4.0.dist-info → masster-0.4.1.dist-info}/WHEEL +0 -0
- {masster-0.4.0.dist-info → masster-0.4.1.dist-info}/entry_points.txt +0 -0
- {masster-0.4.0.dist-info → masster-0.4.1.dist-info}/licenses/LICENSE +0 -0
masster/study/plot.py
CHANGED
|
@@ -26,71 +26,65 @@ def _isolated_save_plot(plot_object, filename, abs_filename, logger, plot_title=
|
|
|
26
26
|
# Use isolated file saving that doesn't affect global output state
|
|
27
27
|
from bokeh.resources import Resources
|
|
28
28
|
from bokeh.embed import file_html
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
# Create HTML content without affecting global state
|
|
31
|
-
resources = Resources(mode=
|
|
31
|
+
resources = Resources(mode='cdn')
|
|
32
32
|
html = file_html(plot_object, resources, title=plot_title)
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
# Write directly to file
|
|
35
|
-
with open(filename,
|
|
35
|
+
with open(filename, 'w', encoding='utf-8') as f:
|
|
36
36
|
f.write(html)
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
logger.info(f"Plot saved to: {abs_filename}")
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
elif filename.endswith(".png"):
|
|
41
41
|
try:
|
|
42
42
|
from bokeh.io.export import export_png
|
|
43
|
-
|
|
44
43
|
export_png(plot_object, filename=filename)
|
|
45
44
|
logger.info(f"Plot saved to: {abs_filename}")
|
|
46
45
|
except Exception:
|
|
47
46
|
# Fall back to HTML if PNG export not available
|
|
48
|
-
html_filename = filename.replace(
|
|
47
|
+
html_filename = filename.replace('.png', '.html')
|
|
49
48
|
from bokeh.resources import Resources
|
|
50
49
|
from bokeh.embed import file_html
|
|
51
|
-
|
|
52
|
-
resources = Resources(mode=
|
|
50
|
+
|
|
51
|
+
resources = Resources(mode='cdn')
|
|
53
52
|
html = file_html(plot_object, resources, title=plot_title)
|
|
54
|
-
|
|
55
|
-
with open(html_filename,
|
|
53
|
+
|
|
54
|
+
with open(html_filename, 'w', encoding='utf-8') as f:
|
|
56
55
|
f.write(html)
|
|
57
|
-
|
|
58
|
-
logger.warning(
|
|
59
|
-
f"PNG export not available, saved as HTML instead: {html_filename}",
|
|
60
|
-
)
|
|
56
|
+
|
|
57
|
+
logger.warning(f"PNG export not available, saved as HTML instead: {html_filename}")
|
|
61
58
|
elif filename.endswith(".pdf"):
|
|
62
59
|
# Try to save as PDF, fall back to HTML if not available
|
|
63
60
|
try:
|
|
64
61
|
from bokeh.io.export import export_pdf
|
|
65
|
-
|
|
66
62
|
export_pdf(plot_object, filename=filename)
|
|
67
63
|
logger.info(f"Plot saved to: {abs_filename}")
|
|
68
64
|
except ImportError:
|
|
69
65
|
# Fall back to HTML if PDF export not available
|
|
70
|
-
html_filename = filename.replace(
|
|
66
|
+
html_filename = filename.replace('.pdf', '.html')
|
|
71
67
|
from bokeh.resources import Resources
|
|
72
68
|
from bokeh.embed import file_html
|
|
73
|
-
|
|
74
|
-
resources = Resources(mode=
|
|
69
|
+
|
|
70
|
+
resources = Resources(mode='cdn')
|
|
75
71
|
html = file_html(plot_object, resources, title=plot_title)
|
|
76
|
-
|
|
77
|
-
with open(html_filename,
|
|
72
|
+
|
|
73
|
+
with open(html_filename, 'w', encoding='utf-8') as f:
|
|
78
74
|
f.write(html)
|
|
79
|
-
|
|
80
|
-
logger.warning(
|
|
81
|
-
f"PDF export not available, saved as HTML instead: {html_filename}",
|
|
82
|
-
)
|
|
75
|
+
|
|
76
|
+
logger.warning(f"PDF export not available, saved as HTML instead: {html_filename}")
|
|
83
77
|
else:
|
|
84
78
|
# Default to HTML for unknown extensions using isolated approach
|
|
85
79
|
from bokeh.resources import Resources
|
|
86
80
|
from bokeh.embed import file_html
|
|
87
|
-
|
|
88
|
-
resources = Resources(mode=
|
|
81
|
+
|
|
82
|
+
resources = Resources(mode='cdn')
|
|
89
83
|
html = file_html(plot_object, resources, title=plot_title)
|
|
90
|
-
|
|
91
|
-
with open(filename,
|
|
84
|
+
|
|
85
|
+
with open(filename, 'w', encoding='utf-8') as f:
|
|
92
86
|
f.write(html)
|
|
93
|
-
|
|
87
|
+
|
|
94
88
|
logger.info(f"Plot saved to: {abs_filename}")
|
|
95
89
|
|
|
96
90
|
|
|
@@ -103,36 +97,28 @@ def _isolated_show_notebook(plot_object):
|
|
|
103
97
|
import holoviews as hv
|
|
104
98
|
import warnings
|
|
105
99
|
import logging
|
|
106
|
-
|
|
100
|
+
|
|
107
101
|
# Suppress both warnings and logging messages for the specific Bokeh callback warnings
|
|
108
102
|
# that occur when Panel components with Python callbacks are converted to standalone Bokeh
|
|
109
|
-
bokeh_logger = logging.getLogger(
|
|
103
|
+
bokeh_logger = logging.getLogger('bokeh.embed.util')
|
|
110
104
|
original_level = bokeh_logger.level
|
|
111
105
|
bokeh_logger.setLevel(logging.ERROR) # Suppress WARNING level messages
|
|
112
|
-
|
|
106
|
+
|
|
113
107
|
with warnings.catch_warnings():
|
|
114
|
-
warnings.filterwarnings(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
category=UserWarning,
|
|
118
|
-
)
|
|
119
|
-
warnings.filterwarnings(
|
|
120
|
-
"ignore",
|
|
121
|
-
message=".*real Python callbacks.*",
|
|
122
|
-
category=UserWarning,
|
|
123
|
-
)
|
|
124
|
-
|
|
108
|
+
warnings.filterwarnings("ignore", message=".*standalone HTML/JS output.*", category=UserWarning)
|
|
109
|
+
warnings.filterwarnings("ignore", message=".*real Python callbacks.*", category=UserWarning)
|
|
110
|
+
|
|
125
111
|
try:
|
|
126
112
|
# First clear all output state
|
|
127
113
|
reset_output()
|
|
128
|
-
|
|
114
|
+
|
|
129
115
|
# Set notebook mode
|
|
130
116
|
output_notebook(hide_banner=True)
|
|
131
|
-
|
|
132
|
-
# Reset Holoviews to notebook mode
|
|
133
|
-
hv.extension(
|
|
134
|
-
hv.output(backend=
|
|
135
|
-
|
|
117
|
+
|
|
118
|
+
# Reset Holoviews to notebook mode
|
|
119
|
+
hv.extension('bokeh', logo=False)
|
|
120
|
+
hv.output(backend='bokeh', mode='jupyter')
|
|
121
|
+
|
|
136
122
|
# Show in notebook
|
|
137
123
|
show(plot_object)
|
|
138
124
|
finally:
|
|
@@ -143,16 +129,16 @@ def _isolated_show_notebook(plot_object):
|
|
|
143
129
|
def _isolated_save_panel_plot(panel_obj, filename, abs_filename, logger, plot_title):
|
|
144
130
|
"""
|
|
145
131
|
Save a Panel plot using isolated approach that doesn't affect global Bokeh state.
|
|
146
|
-
|
|
132
|
+
|
|
147
133
|
Args:
|
|
148
134
|
panel_obj: Panel object to save
|
|
149
|
-
filename: Target filename
|
|
135
|
+
filename: Target filename
|
|
150
136
|
abs_filename: Absolute path for logging
|
|
151
137
|
logger: Logger instance
|
|
152
138
|
plot_title: Title for logging
|
|
153
139
|
"""
|
|
154
140
|
import os # Import os for path operations
|
|
155
|
-
|
|
141
|
+
|
|
156
142
|
if filename.endswith(".html"):
|
|
157
143
|
# Panel save method should be isolated but let's be sure
|
|
158
144
|
try:
|
|
@@ -161,44 +147,38 @@ def _isolated_save_panel_plot(panel_obj, filename, abs_filename, logger, plot_ti
|
|
|
161
147
|
logger.info(f"{plot_title} saved to: {abs_filename}")
|
|
162
148
|
except Exception as e:
|
|
163
149
|
logger.error(f"Failed to save {plot_title}: {e}")
|
|
164
|
-
|
|
150
|
+
|
|
165
151
|
elif filename.endswith(".png"):
|
|
166
152
|
try:
|
|
167
153
|
from panel.io.save import save_png
|
|
168
|
-
|
|
169
154
|
# Convert Panel to Bokeh models before saving
|
|
170
155
|
bokeh_layout = panel_obj.get_root()
|
|
171
156
|
save_png(bokeh_layout, filename=filename)
|
|
172
157
|
logger.info(f"{plot_title} saved to: {abs_filename}")
|
|
173
158
|
except Exception:
|
|
174
159
|
# Fall back to HTML if PNG export not available
|
|
175
|
-
html_filename = filename.replace(
|
|
160
|
+
html_filename = filename.replace('.png', '.html')
|
|
176
161
|
abs_html_filename = os.path.abspath(html_filename)
|
|
177
162
|
try:
|
|
178
163
|
panel_obj.save(html_filename, embed=True)
|
|
179
|
-
logger.warning(
|
|
180
|
-
f"PNG export not available, saved as HTML instead: {abs_html_filename}",
|
|
181
|
-
)
|
|
164
|
+
logger.warning(f"PNG export not available, saved as HTML instead: {abs_html_filename}")
|
|
182
165
|
except Exception as e:
|
|
183
166
|
logger.error(f"Failed to save {plot_title} as HTML fallback: {e}")
|
|
184
|
-
|
|
167
|
+
|
|
185
168
|
elif filename.endswith(".pdf"):
|
|
186
169
|
# Try to save as PDF, fall back to HTML if not available
|
|
187
170
|
try:
|
|
188
171
|
from bokeh.io.export import export_pdf
|
|
189
|
-
|
|
190
172
|
bokeh_layout = panel_obj.get_root()
|
|
191
173
|
export_pdf(bokeh_layout, filename=filename)
|
|
192
174
|
logger.info(f"{plot_title} saved to: {abs_filename}")
|
|
193
175
|
except ImportError:
|
|
194
176
|
# Fall back to HTML if PDF export not available
|
|
195
|
-
html_filename = filename.replace(
|
|
177
|
+
html_filename = filename.replace('.pdf', '.html')
|
|
196
178
|
abs_html_filename = os.path.abspath(html_filename)
|
|
197
179
|
try:
|
|
198
180
|
panel_obj.save(html_filename, embed=True)
|
|
199
|
-
logger.warning(
|
|
200
|
-
f"PDF export not available, saved as HTML instead: {abs_html_filename}",
|
|
201
|
-
)
|
|
181
|
+
logger.warning(f"PDF export not available, saved as HTML instead: {abs_html_filename}")
|
|
202
182
|
except Exception as e:
|
|
203
183
|
logger.error(f"Failed to save {plot_title} as HTML fallback: {e}")
|
|
204
184
|
else:
|
|
@@ -213,33 +193,31 @@ def _isolated_save_panel_plot(panel_obj, filename, abs_filename, logger, plot_ti
|
|
|
213
193
|
def _isolated_show_panel_notebook(panel_obj):
|
|
214
194
|
"""
|
|
215
195
|
Show a Panel plot in notebook with state isolation to prevent browser opening.
|
|
216
|
-
|
|
196
|
+
|
|
217
197
|
Args:
|
|
218
198
|
panel_obj: Panel object to display
|
|
219
199
|
"""
|
|
220
200
|
# Reset Bokeh state completely to prevent browser opening if output_file was called before
|
|
221
201
|
from bokeh.io import reset_output, output_notebook
|
|
222
202
|
import holoviews as hv
|
|
223
|
-
|
|
203
|
+
|
|
224
204
|
# First clear all output state
|
|
225
205
|
reset_output()
|
|
226
|
-
|
|
206
|
+
|
|
227
207
|
# Set notebook mode
|
|
228
208
|
output_notebook(hide_banner=True)
|
|
229
|
-
|
|
230
|
-
# Reset Holoviews to notebook mode
|
|
231
|
-
hv.extension(
|
|
232
|
-
hv.output(backend=
|
|
233
|
-
|
|
234
|
-
# For Panel objects in notebooks, use
|
|
235
|
-
import panel as
|
|
236
|
-
|
|
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
|
|
237
216
|
try:
|
|
238
217
|
# Configure Panel for notebook display
|
|
239
|
-
|
|
218
|
+
pn.extension('bokeh', inline=True, comms='vscode')
|
|
240
219
|
# Use IPython display to show inline instead of show()
|
|
241
220
|
from IPython.display import display
|
|
242
|
-
|
|
243
221
|
display(panel_obj)
|
|
244
222
|
except Exception:
|
|
245
223
|
# Fallback to regular Panel show
|
|
@@ -274,7 +252,7 @@ def plot_alignment(
|
|
|
274
252
|
"""
|
|
275
253
|
# Local imports so the module can be used even if bokeh isn't needed elsewhere
|
|
276
254
|
from bokeh.models import ColumnDataSource, HoverTool
|
|
277
|
-
from bokeh.plotting import figure
|
|
255
|
+
from bokeh.plotting import figure, show, output_file
|
|
278
256
|
import pandas as pd
|
|
279
257
|
|
|
280
258
|
# Get sample_uids to filter by if specified
|
|
@@ -298,42 +276,32 @@ def plot_alignment(
|
|
|
298
276
|
# Filter feature maps by selected samples if specified
|
|
299
277
|
if sample_uids is not None:
|
|
300
278
|
# Create mapping from sample_uid to map_id and filter accordingly
|
|
301
|
-
if (
|
|
302
|
-
hasattr(self, "samples_df")
|
|
303
|
-
and self.samples_df is not None
|
|
304
|
-
and not self.samples_df.is_empty()
|
|
305
|
-
):
|
|
279
|
+
if hasattr(self, "samples_df") and self.samples_df is not None and not self.samples_df.is_empty():
|
|
306
280
|
samples_info = self.samples_df.to_pandas()
|
|
307
|
-
|
|
281
|
+
|
|
308
282
|
# Filter samples_info to only selected sample_uids and get their map_ids
|
|
309
|
-
selected_samples = samples_info[
|
|
310
|
-
samples_info["sample_uid"].isin(sample_uids)
|
|
311
|
-
]
|
|
283
|
+
selected_samples = samples_info[samples_info["sample_uid"].isin(sample_uids)]
|
|
312
284
|
if selected_samples.empty:
|
|
313
|
-
self.logger.error(
|
|
314
|
-
"No matching samples found for the provided sample_uids.",
|
|
315
|
-
)
|
|
285
|
+
self.logger.error("No matching samples found for the provided sample_uids.")
|
|
316
286
|
return
|
|
317
|
-
|
|
287
|
+
|
|
318
288
|
# Get the map_ids for selected samples
|
|
319
289
|
selected_map_ids = selected_samples["map_id"].tolist()
|
|
320
|
-
|
|
290
|
+
|
|
321
291
|
# Filter feature maps based on map_ids
|
|
322
292
|
filtered_maps = []
|
|
323
293
|
for map_id in selected_map_ids:
|
|
324
294
|
if 0 <= map_id < len(fmaps):
|
|
325
295
|
filtered_maps.append(fmaps[map_id])
|
|
326
|
-
|
|
296
|
+
|
|
327
297
|
fmaps = filtered_maps
|
|
328
298
|
samples_info = selected_samples.reset_index(drop=True)
|
|
329
|
-
|
|
299
|
+
|
|
330
300
|
if not fmaps:
|
|
331
301
|
self.logger.error("No feature maps found for the selected samples.")
|
|
332
302
|
return
|
|
333
303
|
else:
|
|
334
|
-
self.logger.warning(
|
|
335
|
-
"Cannot filter feature maps: no samples_df available",
|
|
336
|
-
)
|
|
304
|
+
self.logger.warning("Cannot filter feature maps: no samples_df available")
|
|
337
305
|
|
|
338
306
|
if not fmaps:
|
|
339
307
|
self.logger.error("No feature maps available after filtering.")
|
|
@@ -347,52 +315,40 @@ def plot_alignment(
|
|
|
347
315
|
max_ref_inty = max(ref_inty) if ref_inty else 1
|
|
348
316
|
|
|
349
317
|
# Get sample metadata for reference (first) sample
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
and self.samples_df is not None
|
|
353
|
-
and not self.samples_df.is_empty()
|
|
354
|
-
):
|
|
355
|
-
if "samples_info" not in locals():
|
|
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():
|
|
356
320
|
samples_info = self.samples_df.to_pandas()
|
|
357
321
|
ref_sample_uid = (
|
|
358
|
-
samples_info.iloc[0]["sample_uid"]
|
|
359
|
-
if "sample_uid" in samples_info.columns
|
|
360
|
-
else "Reference_UID"
|
|
322
|
+
samples_info.iloc[0]["sample_uid"] if "sample_uid" in samples_info.columns else "Reference_UID"
|
|
361
323
|
)
|
|
362
324
|
ref_sample_name = (
|
|
363
|
-
samples_info.iloc[0]["sample_name"]
|
|
364
|
-
if "sample_name" in samples_info.columns
|
|
365
|
-
else "Reference"
|
|
325
|
+
samples_info.iloc[0]["sample_name"] if "sample_name" in samples_info.columns else "Reference"
|
|
366
326
|
)
|
|
367
327
|
else:
|
|
368
328
|
ref_sample_uid = "Reference_UID"
|
|
369
329
|
ref_sample_name = "Reference"
|
|
370
330
|
|
|
371
331
|
for rt, mz, inty in zip(ref_rt, ref_mz, ref_inty):
|
|
372
|
-
before_data.append(
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
"sample_uid": ref_sample_uid,
|
|
393
|
-
"size": markersize + 2,
|
|
394
|
-
},
|
|
395
|
-
)
|
|
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
|
+
})
|
|
396
352
|
|
|
397
353
|
# Remaining samples - now using filtered feature maps and samples_info
|
|
398
354
|
for sample_idx, fm in enumerate(fmaps[1:], start=1):
|
|
@@ -422,33 +378,17 @@ def plot_alignment(
|
|
|
422
378
|
max_inty = max(inty_vals)
|
|
423
379
|
|
|
424
380
|
# Get sample metadata from filtered samples_info
|
|
425
|
-
if (
|
|
426
|
-
hasattr(self, "samples_df")
|
|
427
|
-
and self.samples_df is not None
|
|
428
|
-
and not self.samples_df.is_empty()
|
|
429
|
-
):
|
|
381
|
+
if hasattr(self, "samples_df") and self.samples_df is not None and not self.samples_df.is_empty():
|
|
430
382
|
# Use filtered samples_info if it exists from the filtering above
|
|
431
|
-
if
|
|
432
|
-
sample_name = samples_info.iloc[sample_idx].get(
|
|
433
|
-
|
|
434
|
-
f"Sample {sample_idx}",
|
|
435
|
-
)
|
|
436
|
-
sample_uid = samples_info.iloc[sample_idx].get(
|
|
437
|
-
"sample_uid",
|
|
438
|
-
f"Sample_{sample_idx}_UID",
|
|
439
|
-
)
|
|
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")
|
|
440
386
|
else:
|
|
441
387
|
# Fallback to original samples_df if filtered samples_info is not available
|
|
442
388
|
all_samples_info = self.samples_df.to_pandas()
|
|
443
389
|
if sample_idx < len(all_samples_info):
|
|
444
|
-
sample_name = all_samples_info.iloc[sample_idx].get(
|
|
445
|
-
|
|
446
|
-
f"Sample {sample_idx}",
|
|
447
|
-
)
|
|
448
|
-
sample_uid = all_samples_info.iloc[sample_idx].get(
|
|
449
|
-
"sample_uid",
|
|
450
|
-
f"Sample_{sample_idx}_UID",
|
|
451
|
-
)
|
|
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")
|
|
452
392
|
else:
|
|
453
393
|
sample_name = f"Sample {sample_idx}"
|
|
454
394
|
sample_uid = f"Sample_{sample_idx}_UID"
|
|
@@ -457,32 +397,28 @@ def plot_alignment(
|
|
|
457
397
|
sample_uid = f"Sample_{sample_idx}_UID"
|
|
458
398
|
|
|
459
399
|
for rt, mz, inty in zip(original_rt, mz_vals, inty_vals):
|
|
460
|
-
before_data.append(
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
},
|
|
471
|
-
)
|
|
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
|
+
})
|
|
472
410
|
|
|
473
411
|
for rt, mz, inty in zip(aligned_rt, mz_vals, inty_vals):
|
|
474
|
-
after_data.append(
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
},
|
|
485
|
-
)
|
|
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
|
+
})
|
|
486
422
|
|
|
487
423
|
else:
|
|
488
424
|
# Use features_df
|
|
@@ -497,14 +433,12 @@ def plot_alignment(
|
|
|
497
433
|
return
|
|
498
434
|
|
|
499
435
|
if "rt_original" not in self.features_df.columns:
|
|
500
|
-
self.logger.error(
|
|
501
|
-
"Column 'rt_original' not found in features_df. Alignment may not have been performed.",
|
|
502
|
-
)
|
|
436
|
+
self.logger.error("Column 'rt_original' not found in features_df. Alignment may not have been performed.")
|
|
503
437
|
return
|
|
504
438
|
|
|
505
439
|
# Use Polars instead of pandas
|
|
506
440
|
features_df = self.features_df
|
|
507
|
-
|
|
441
|
+
|
|
508
442
|
# Filter by selected samples if specified
|
|
509
443
|
if sample_uids is not None:
|
|
510
444
|
features_df = features_df.filter(pl.col("sample_uid").is_in(sample_uids))
|
|
@@ -512,9 +446,7 @@ def plot_alignment(
|
|
|
512
446
|
self.logger.error("No features found for the selected samples.")
|
|
513
447
|
return
|
|
514
448
|
|
|
515
|
-
sample_col =
|
|
516
|
-
"sample_uid" if "sample_uid" in features_df.columns else "sample_name"
|
|
517
|
-
)
|
|
449
|
+
sample_col = "sample_uid" if "sample_uid" in features_df.columns else "sample_name"
|
|
518
450
|
if sample_col not in features_df.columns:
|
|
519
451
|
self.logger.error("No sample identifier column found in features_df.")
|
|
520
452
|
return
|
|
@@ -542,9 +474,7 @@ def plot_alignment(
|
|
|
542
474
|
sample_uid = sample
|
|
543
475
|
|
|
544
476
|
# Convert to dict for iteration - more efficient than row-by-row processing
|
|
545
|
-
sample_dict = sample_data.select(
|
|
546
|
-
["rt_original", "rt", "mz", "inty"],
|
|
547
|
-
).to_dicts()
|
|
477
|
+
sample_dict = sample_data.select(["rt_original", "rt", "mz", "inty"]).to_dicts()
|
|
548
478
|
|
|
549
479
|
for row_dict in sample_dict:
|
|
550
480
|
rt_original = row_dict["rt_original"]
|
|
@@ -554,30 +484,26 @@ def plot_alignment(
|
|
|
554
484
|
alpha = inty / max_inty
|
|
555
485
|
size = markersize + 2 if sample_idx == 0 else markersize
|
|
556
486
|
|
|
557
|
-
before_data.append(
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
"sample_uid": sample_uid,
|
|
578
|
-
"size": size,
|
|
579
|
-
},
|
|
580
|
-
)
|
|
487
|
+
before_data.append({
|
|
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,
|
|
496
|
+
})
|
|
497
|
+
after_data.append({
|
|
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,
|
|
506
|
+
})
|
|
581
507
|
|
|
582
508
|
# Get sample colors from samples_df using sample indices
|
|
583
509
|
# Extract unique sample information from the dictionaries we created
|
|
@@ -598,19 +524,14 @@ def plot_alignment(
|
|
|
598
524
|
.select(["sample_uid", "sample_color"])
|
|
599
525
|
.to_dict(as_series=False)
|
|
600
526
|
)
|
|
601
|
-
uid_to_color = dict(
|
|
602
|
-
zip(sample_colors["sample_uid"], sample_colors["sample_color"]),
|
|
603
|
-
)
|
|
527
|
+
uid_to_color = dict(zip(sample_colors["sample_uid"], sample_colors["sample_color"]))
|
|
604
528
|
else:
|
|
605
529
|
uid_to_color = {}
|
|
606
530
|
|
|
607
531
|
# Create color map for sample indices
|
|
608
532
|
color_map: dict[int, str] = {}
|
|
609
533
|
for sample_idx, sample_uid in sample_idx_to_uid.items():
|
|
610
|
-
color_map[sample_idx] = uid_to_color.get(
|
|
611
|
-
sample_uid,
|
|
612
|
-
"#1f77b4",
|
|
613
|
-
) # fallback to blue
|
|
534
|
+
color_map[sample_idx] = uid_to_color.get(sample_uid, "#1f77b4") # fallback to blue
|
|
614
535
|
|
|
615
536
|
# Add sample_color to data dictionaries before creating DataFrames
|
|
616
537
|
if before_data:
|
|
@@ -655,11 +576,7 @@ def plot_alignment(
|
|
|
655
576
|
p2.min_border = 0
|
|
656
577
|
|
|
657
578
|
# Get unique sample indices for iteration
|
|
658
|
-
unique_samples = (
|
|
659
|
-
sorted(list({item["sample_idx"] for item in before_data}))
|
|
660
|
-
if before_data
|
|
661
|
-
else []
|
|
662
|
-
)
|
|
579
|
+
unique_samples = sorted(list({item["sample_idx"] for item in before_data})) if before_data else []
|
|
663
580
|
|
|
664
581
|
renderers_before = []
|
|
665
582
|
renderers_after = []
|
|
@@ -671,26 +588,12 @@ def plot_alignment(
|
|
|
671
588
|
|
|
672
589
|
if not sb.empty:
|
|
673
590
|
src = ColumnDataSource(sb)
|
|
674
|
-
r = p1.scatter(
|
|
675
|
-
"rt",
|
|
676
|
-
"mz",
|
|
677
|
-
size="size",
|
|
678
|
-
color=color,
|
|
679
|
-
alpha="alpha",
|
|
680
|
-
source=src,
|
|
681
|
-
)
|
|
591
|
+
r = p1.scatter("rt", "mz", size="size", color=color, alpha="alpha", source=src)
|
|
682
592
|
renderers_before.append(r)
|
|
683
593
|
|
|
684
594
|
if not sa.empty:
|
|
685
595
|
src = ColumnDataSource(sa)
|
|
686
|
-
r = p2.scatter(
|
|
687
|
-
"rt",
|
|
688
|
-
"mz",
|
|
689
|
-
size="size",
|
|
690
|
-
color=color,
|
|
691
|
-
alpha="alpha",
|
|
692
|
-
source=src,
|
|
693
|
-
)
|
|
596
|
+
r = p2.scatter("rt", "mz", size="size", color=color, alpha="alpha", source=src)
|
|
694
597
|
renderers_after.append(r)
|
|
695
598
|
|
|
696
599
|
# Add hover tools
|
|
@@ -728,21 +631,14 @@ def plot_alignment(
|
|
|
728
631
|
if filename is not None:
|
|
729
632
|
# Convert relative paths to absolute paths using study folder as base
|
|
730
633
|
import os
|
|
731
|
-
|
|
732
634
|
if not os.path.isabs(filename):
|
|
733
635
|
filename = os.path.join(self.folder, filename)
|
|
734
|
-
|
|
636
|
+
|
|
735
637
|
# Convert to absolute path for logging
|
|
736
638
|
abs_filename = os.path.abspath(filename)
|
|
737
|
-
|
|
639
|
+
|
|
738
640
|
# Use isolated file saving
|
|
739
|
-
_isolated_save_plot(
|
|
740
|
-
layout,
|
|
741
|
-
filename,
|
|
742
|
-
abs_filename,
|
|
743
|
-
self.logger,
|
|
744
|
-
"Alignment Plot",
|
|
745
|
-
)
|
|
641
|
+
_isolated_save_plot(layout, filename, abs_filename, self.logger, "Alignment Plot")
|
|
746
642
|
else:
|
|
747
643
|
# Show in notebook when no filename provided
|
|
748
644
|
_isolated_show_notebook(layout)
|
|
@@ -789,13 +685,9 @@ def plot_consensus_2d(
|
|
|
789
685
|
|
|
790
686
|
# Filter by mz_range and rt_range if provided
|
|
791
687
|
if mz_range is not None:
|
|
792
|
-
data = data.filter(
|
|
793
|
-
(pl.col("mz") >= mz_range[0]) & (pl.col("mz") <= mz_range[1]),
|
|
794
|
-
)
|
|
688
|
+
data = data.filter((pl.col("mz") >= mz_range[0]) & (pl.col("mz") <= mz_range[1]))
|
|
795
689
|
if rt_range is not None:
|
|
796
|
-
data = data.filter(
|
|
797
|
-
(pl.col("rt") >= rt_range[0]) & (pl.col("rt") <= rt_range[1]),
|
|
798
|
-
)
|
|
690
|
+
data = data.filter((pl.col("rt") >= rt_range[0]) & (pl.col("rt") <= rt_range[1]))
|
|
799
691
|
|
|
800
692
|
if colorby not in data.columns:
|
|
801
693
|
self.logger.error(f"Column {colorby} not found in consensus_df.")
|
|
@@ -809,25 +701,19 @@ def plot_consensus_2d(
|
|
|
809
701
|
if sizeby in ["inty_mean"]:
|
|
810
702
|
# use log10 of sizeby
|
|
811
703
|
# Filter out empty or all-NA entries before applying np.log10
|
|
812
|
-
data = data.with_columns(
|
|
813
|
-
|
|
814
|
-
pl.
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
.otherwise(markersize)
|
|
821
|
-
.alias("markersize"),
|
|
822
|
-
],
|
|
823
|
-
)
|
|
704
|
+
data = data.with_columns([
|
|
705
|
+
pl.when(
|
|
706
|
+
(pl.col(sizeby).is_not_null()) & (pl.col(sizeby).is_finite()) & (pl.col(sizeby) > 0),
|
|
707
|
+
)
|
|
708
|
+
.then((pl.col(sizeby).log10() * markersize / 12).pow(2))
|
|
709
|
+
.otherwise(markersize)
|
|
710
|
+
.alias("markersize"),
|
|
711
|
+
])
|
|
824
712
|
else:
|
|
825
713
|
max_size = data[sizeby].max()
|
|
826
|
-
data = data.with_columns(
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
],
|
|
830
|
-
)
|
|
714
|
+
data = data.with_columns([
|
|
715
|
+
(pl.col(sizeby) / max_size * markersize).alias("markersize"),
|
|
716
|
+
])
|
|
831
717
|
else:
|
|
832
718
|
data = data.with_columns([pl.lit(markersize).alias("markersize")])
|
|
833
719
|
# sort by ascending colorby
|
|
@@ -835,22 +721,18 @@ def plot_consensus_2d(
|
|
|
835
721
|
# convert consensus_id to string - check if column exists
|
|
836
722
|
if "consensus_id" in data.columns:
|
|
837
723
|
# Handle Object dtype by converting to string first
|
|
838
|
-
data = data.with_columns(
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
],
|
|
847
|
-
)
|
|
724
|
+
data = data.with_columns([
|
|
725
|
+
pl.col("consensus_id")
|
|
726
|
+
.map_elements(
|
|
727
|
+
lambda x: str(x) if x is not None else None,
|
|
728
|
+
return_dtype=pl.Utf8,
|
|
729
|
+
)
|
|
730
|
+
.alias("consensus_id"),
|
|
731
|
+
])
|
|
848
732
|
elif "consensus_uid" in data.columns:
|
|
849
|
-
data = data.with_columns(
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
],
|
|
853
|
-
)
|
|
733
|
+
data = data.with_columns([
|
|
734
|
+
pl.col("consensus_uid").cast(pl.Utf8).alias("consensus_id"),
|
|
735
|
+
])
|
|
854
736
|
|
|
855
737
|
if cmap is None:
|
|
856
738
|
cmap = "viridis"
|
|
@@ -864,6 +746,7 @@ def plot_consensus_2d(
|
|
|
864
746
|
from bokeh.models import ColumnDataSource
|
|
865
747
|
from bokeh.models import HoverTool
|
|
866
748
|
from bokeh.models import LinearColorMapper
|
|
749
|
+
from bokeh.io.export import export_png
|
|
867
750
|
|
|
868
751
|
try:
|
|
869
752
|
from bokeh.models import ColorBar # type: ignore[attr-defined]
|
|
@@ -911,9 +794,7 @@ def plot_consensus_2d(
|
|
|
911
794
|
palette = [mcolors.rgb2hex(color) for color in colors]
|
|
912
795
|
except (AttributeError, ValueError, TypeError) as e:
|
|
913
796
|
# Fallback to viridis if cmap interpretation fails
|
|
914
|
-
self.logger.warning(
|
|
915
|
-
f"Could not interpret colormap '{cmap}': {e}, falling back to viridis",
|
|
916
|
-
)
|
|
797
|
+
self.logger.warning(f"Could not interpret colormap '{cmap}': {e}, falling back to viridis")
|
|
917
798
|
palette = viridis(256)
|
|
918
799
|
|
|
919
800
|
color_mapper = LinearColorMapper(
|
|
@@ -981,13 +862,12 @@ def plot_consensus_2d(
|
|
|
981
862
|
if filename is not None:
|
|
982
863
|
# Convert relative paths to absolute paths using study folder as base
|
|
983
864
|
import os
|
|
984
|
-
|
|
985
865
|
if not os.path.isabs(filename):
|
|
986
866
|
filename = os.path.join(self.folder, filename)
|
|
987
|
-
|
|
867
|
+
|
|
988
868
|
# Convert to absolute path for logging
|
|
989
869
|
abs_filename = os.path.abspath(filename)
|
|
990
|
-
|
|
870
|
+
|
|
991
871
|
# Use isolated file saving
|
|
992
872
|
_isolated_save_plot(p, filename, abs_filename, self.logger, "Consensus 2D Plot")
|
|
993
873
|
else:
|
|
@@ -1033,7 +913,8 @@ def plot_samples_2d(
|
|
|
1033
913
|
"""
|
|
1034
914
|
|
|
1035
915
|
# Local bokeh imports to avoid heavy top-level dependency
|
|
1036
|
-
from bokeh.plotting import figure
|
|
916
|
+
from bokeh.plotting import figure, show, output_file
|
|
917
|
+
from bokeh.io.export import export_png
|
|
1037
918
|
from bokeh.models import ColumnDataSource, HoverTool
|
|
1038
919
|
|
|
1039
920
|
sample_uids = self._get_sample_uids(samples)
|
|
@@ -1063,13 +944,9 @@ def plot_samples_2d(
|
|
|
1063
944
|
|
|
1064
945
|
# Filter by mz_range and rt_range if provided
|
|
1065
946
|
if mz_range is not None:
|
|
1066
|
-
features_batch = features_batch.filter(
|
|
1067
|
-
(pl.col("mz") >= mz_range[0]) & (pl.col("mz") <= mz_range[1]),
|
|
1068
|
-
)
|
|
947
|
+
features_batch = features_batch.filter((pl.col("mz") >= mz_range[0]) & (pl.col("mz") <= mz_range[1]))
|
|
1069
948
|
if rt_range is not None:
|
|
1070
|
-
features_batch = features_batch.filter(
|
|
1071
|
-
(pl.col("rt") >= rt_range[0]) & (pl.col("rt") <= rt_range[1]),
|
|
1072
|
-
)
|
|
949
|
+
features_batch = features_batch.filter((pl.col("rt") >= rt_range[0]) & (pl.col("rt") <= rt_range[1]))
|
|
1073
950
|
|
|
1074
951
|
if features_batch.is_empty():
|
|
1075
952
|
self.logger.error("No features found for the selected samples.")
|
|
@@ -1165,11 +1042,7 @@ def plot_samples_2d(
|
|
|
1165
1042
|
"inty": sample_data["inty"].values,
|
|
1166
1043
|
"alpha": sample_data["alpha"].values,
|
|
1167
1044
|
"sample": np.full(len(sample_data), sample_name, dtype=object),
|
|
1168
|
-
"sample_color": np.full(
|
|
1169
|
-
len(sample_data),
|
|
1170
|
-
color_values[uid],
|
|
1171
|
-
dtype=object,
|
|
1172
|
-
),
|
|
1045
|
+
"sample_color": np.full(len(sample_data), color_values[uid], dtype=object),
|
|
1173
1046
|
},
|
|
1174
1047
|
)
|
|
1175
1048
|
|
|
@@ -1218,18 +1091,17 @@ def plot_samples_2d(
|
|
|
1218
1091
|
# Only set legend properties if a legend was actually created to avoid Bokeh warnings
|
|
1219
1092
|
if getattr(p, "legend", None) and len(p.legend) > 0:
|
|
1220
1093
|
p.legend.visible = False
|
|
1221
|
-
|
|
1094
|
+
|
|
1222
1095
|
# Apply consistent save/display behavior
|
|
1223
1096
|
if filename is not None:
|
|
1224
1097
|
# Convert relative paths to absolute paths using study folder as base
|
|
1225
1098
|
import os
|
|
1226
|
-
|
|
1227
1099
|
if not os.path.isabs(filename):
|
|
1228
1100
|
filename = os.path.join(self.folder, filename)
|
|
1229
|
-
|
|
1101
|
+
|
|
1230
1102
|
# Convert to absolute path for logging
|
|
1231
1103
|
abs_filename = os.path.abspath(filename)
|
|
1232
|
-
|
|
1104
|
+
|
|
1233
1105
|
# Use isolated file saving
|
|
1234
1106
|
_isolated_save_plot(p, filename, abs_filename, self.logger, "Samples 2D Plot")
|
|
1235
1107
|
else:
|
|
@@ -1257,9 +1129,10 @@ def plot_bpc(
|
|
|
1257
1129
|
If False (default), return current/aligned RTs.
|
|
1258
1130
|
"""
|
|
1259
1131
|
# Local imports to avoid heavy top-level deps / circular imports
|
|
1260
|
-
from bokeh.plotting import figure
|
|
1132
|
+
from bokeh.plotting import figure, show, output_file
|
|
1261
1133
|
from bokeh.models import ColumnDataSource, HoverTool
|
|
1262
|
-
from
|
|
1134
|
+
from bokeh.io.export import export_png
|
|
1135
|
+
from masster.study.helpers import get_bpc
|
|
1263
1136
|
|
|
1264
1137
|
sample_uids = self._get_sample_uids(samples)
|
|
1265
1138
|
if not sample_uids:
|
|
@@ -1294,12 +1167,7 @@ def plot_bpc(
|
|
|
1294
1167
|
except Exception:
|
|
1295
1168
|
continue
|
|
1296
1169
|
|
|
1297
|
-
p = figure(
|
|
1298
|
-
width=width,
|
|
1299
|
-
height=height,
|
|
1300
|
-
title=plot_title,
|
|
1301
|
-
tools="pan,wheel_zoom,box_zoom,reset,save",
|
|
1302
|
-
)
|
|
1170
|
+
p = figure(width=width, height=height, title=plot_title, tools="pan,wheel_zoom,box_zoom,reset,save")
|
|
1303
1171
|
p.xaxis.axis_label = f"Retention Time ({rt_unit})"
|
|
1304
1172
|
p.yaxis.axis_label = "Intensity"
|
|
1305
1173
|
|
|
@@ -1371,22 +1239,10 @@ def plot_bpc(
|
|
|
1371
1239
|
f"Processing BPC for sample_uid={uid}, sample_name={sample_name}, rt_len={rt.size}, color={color}",
|
|
1372
1240
|
)
|
|
1373
1241
|
|
|
1374
|
-
data = {
|
|
1375
|
-
"rt": rt,
|
|
1376
|
-
"inty": inty,
|
|
1377
|
-
"sample": [sample_name] * len(rt),
|
|
1378
|
-
"sample_color": [color] * len(rt),
|
|
1379
|
-
}
|
|
1242
|
+
data = {"rt": rt, "inty": inty, "sample": [sample_name] * len(rt), "sample_color": [color] * len(rt)}
|
|
1380
1243
|
src = ColumnDataSource(data)
|
|
1381
1244
|
|
|
1382
|
-
r_line = p.line(
|
|
1383
|
-
"rt",
|
|
1384
|
-
"inty",
|
|
1385
|
-
source=src,
|
|
1386
|
-
line_width=1,
|
|
1387
|
-
color=color,
|
|
1388
|
-
legend_label=str(sample_name),
|
|
1389
|
-
)
|
|
1245
|
+
r_line = p.line("rt", "inty", source=src, line_width=1, color=color, legend_label=str(sample_name))
|
|
1390
1246
|
r_points = p.scatter("rt", "inty", source=src, size=2, color=color, alpha=0.6)
|
|
1391
1247
|
renderers.append(r_line)
|
|
1392
1248
|
|
|
@@ -1413,13 +1269,12 @@ def plot_bpc(
|
|
|
1413
1269
|
if filename is not None:
|
|
1414
1270
|
# Convert relative paths to absolute paths using study folder as base
|
|
1415
1271
|
import os
|
|
1416
|
-
|
|
1417
1272
|
if not os.path.isabs(filename):
|
|
1418
1273
|
filename = os.path.join(self.folder, filename)
|
|
1419
|
-
|
|
1274
|
+
|
|
1420
1275
|
# Convert to absolute path for logging
|
|
1421
1276
|
abs_filename = os.path.abspath(filename)
|
|
1422
|
-
|
|
1277
|
+
|
|
1423
1278
|
# Use isolated file saving
|
|
1424
1279
|
_isolated_save_plot(p, filename, abs_filename, self.logger, "BPC Plot")
|
|
1425
1280
|
else:
|
|
@@ -1451,9 +1306,10 @@ def plot_eic(
|
|
|
1451
1306
|
mz_tol: m/z tolerance in Da. If None, uses study.parameters.eic_mz_tol as default.
|
|
1452
1307
|
"""
|
|
1453
1308
|
# Local imports to avoid heavy top-level deps / circular imports
|
|
1454
|
-
from bokeh.plotting import figure
|
|
1309
|
+
from bokeh.plotting import figure, show, output_file
|
|
1455
1310
|
from bokeh.models import ColumnDataSource, HoverTool
|
|
1456
|
-
from
|
|
1311
|
+
from bokeh.io.export import export_png
|
|
1312
|
+
from masster.study.helpers import get_eic
|
|
1457
1313
|
|
|
1458
1314
|
# Use study's eic_mz_tol parameter as default if not provided
|
|
1459
1315
|
if mz_tol is None:
|
|
@@ -1489,12 +1345,7 @@ def plot_eic(
|
|
|
1489
1345
|
except Exception:
|
|
1490
1346
|
continue
|
|
1491
1347
|
|
|
1492
|
-
p = figure(
|
|
1493
|
-
width=width,
|
|
1494
|
-
height=height,
|
|
1495
|
-
title=plot_title,
|
|
1496
|
-
tools="pan,wheel_zoom,box_zoom,reset,save",
|
|
1497
|
-
)
|
|
1348
|
+
p = figure(width=width, height=height, title=plot_title, tools="pan,wheel_zoom,box_zoom,reset,save")
|
|
1498
1349
|
p.xaxis.axis_label = f"Retention Time ({rt_unit})"
|
|
1499
1350
|
p.yaxis.axis_label = "Intensity"
|
|
1500
1351
|
|
|
@@ -1560,22 +1411,10 @@ def plot_eic(
|
|
|
1560
1411
|
|
|
1561
1412
|
color = color_map.get(uid, "#000000")
|
|
1562
1413
|
|
|
1563
|
-
data = {
|
|
1564
|
-
"rt": rt,
|
|
1565
|
-
"inty": inty,
|
|
1566
|
-
"sample": [sample_name] * len(rt),
|
|
1567
|
-
"sample_color": [color] * len(rt),
|
|
1568
|
-
}
|
|
1414
|
+
data = {"rt": rt, "inty": inty, "sample": [sample_name] * len(rt), "sample_color": [color] * len(rt)}
|
|
1569
1415
|
src = ColumnDataSource(data)
|
|
1570
1416
|
|
|
1571
|
-
r_line = p.line(
|
|
1572
|
-
"rt",
|
|
1573
|
-
"inty",
|
|
1574
|
-
source=src,
|
|
1575
|
-
line_width=1,
|
|
1576
|
-
color=color,
|
|
1577
|
-
legend_label=str(sample_name),
|
|
1578
|
-
)
|
|
1417
|
+
r_line = p.line("rt", "inty", source=src, line_width=1, color=color, legend_label=str(sample_name))
|
|
1579
1418
|
p.scatter("rt", "inty", source=src, size=2, color=color, alpha=0.6)
|
|
1580
1419
|
renderers.append(r_line)
|
|
1581
1420
|
|
|
@@ -1601,13 +1440,12 @@ def plot_eic(
|
|
|
1601
1440
|
if filename is not None:
|
|
1602
1441
|
# Convert relative paths to absolute paths using study folder as base
|
|
1603
1442
|
import os
|
|
1604
|
-
|
|
1605
1443
|
if not os.path.isabs(filename):
|
|
1606
1444
|
filename = os.path.join(self.folder, filename)
|
|
1607
|
-
|
|
1445
|
+
|
|
1608
1446
|
# Convert to absolute path for logging
|
|
1609
1447
|
abs_filename = os.path.abspath(filename)
|
|
1610
|
-
|
|
1448
|
+
|
|
1611
1449
|
# Use isolated file saving
|
|
1612
1450
|
_isolated_save_plot(p, filename, abs_filename, self.logger, "EIC Plot")
|
|
1613
1451
|
else:
|
|
@@ -1630,7 +1468,7 @@ def plot_rt_correction(
|
|
|
1630
1468
|
|
|
1631
1469
|
This uses the same color mapping as `plot_bpc` so curves for the same samples match.
|
|
1632
1470
|
"""
|
|
1633
|
-
from bokeh.plotting import figure
|
|
1471
|
+
from bokeh.plotting import figure, show, output_file
|
|
1634
1472
|
from bokeh.models import ColumnDataSource, HoverTool
|
|
1635
1473
|
import numpy as _np
|
|
1636
1474
|
|
|
@@ -1640,9 +1478,7 @@ def plot_rt_correction(
|
|
|
1640
1478
|
return
|
|
1641
1479
|
|
|
1642
1480
|
if "rt_original" not in self.features_df.columns:
|
|
1643
|
-
self.logger.error(
|
|
1644
|
-
"Column 'rt_original' not found in features_df. Alignment/backup RTs missing.",
|
|
1645
|
-
)
|
|
1481
|
+
self.logger.error("Column 'rt_original' not found in features_df. Alignment/backup RTs missing.")
|
|
1646
1482
|
return
|
|
1647
1483
|
|
|
1648
1484
|
sample_uids = self._get_sample_uids(samples)
|
|
@@ -1661,12 +1497,7 @@ def plot_rt_correction(
|
|
|
1661
1497
|
# For RT correction plots, default to "s" since we're working with features_df directly
|
|
1662
1498
|
rt_unit = "s"
|
|
1663
1499
|
|
|
1664
|
-
p = figure(
|
|
1665
|
-
width=width,
|
|
1666
|
-
height=height,
|
|
1667
|
-
title=title or "RT correction",
|
|
1668
|
-
tools="pan,wheel_zoom,box_zoom,reset,save",
|
|
1669
|
-
)
|
|
1500
|
+
p = figure(width=width, height=height, title=title or "RT correction", tools="pan,wheel_zoom,box_zoom,reset,save")
|
|
1670
1501
|
p.xaxis.axis_label = f"Retention Time ({rt_unit})"
|
|
1671
1502
|
p.yaxis.axis_label = "RT - RT_original (s)"
|
|
1672
1503
|
|
|
@@ -1688,9 +1519,7 @@ def plot_rt_correction(
|
|
|
1688
1519
|
elif "sample_name" in self.features_df.columns:
|
|
1689
1520
|
sample_feats = self.features_df.filter(pl.col("sample_name") == uid)
|
|
1690
1521
|
else:
|
|
1691
|
-
self.logger.debug(
|
|
1692
|
-
"No sample identifier column in features_df; skipping sample filtering",
|
|
1693
|
-
)
|
|
1522
|
+
self.logger.debug("No sample identifier column in features_df; skipping sample filtering")
|
|
1694
1523
|
continue
|
|
1695
1524
|
except Exception as e:
|
|
1696
1525
|
self.logger.debug(f"Error filtering features for sample {uid}: {e}")
|
|
@@ -1734,12 +1563,7 @@ def plot_rt_correction(
|
|
|
1734
1563
|
|
|
1735
1564
|
color = color_map.get(uid, "#000000")
|
|
1736
1565
|
|
|
1737
|
-
data = {
|
|
1738
|
-
"rt": rt,
|
|
1739
|
-
"delta": delta,
|
|
1740
|
-
"sample": [sample_name] * len(rt),
|
|
1741
|
-
"sample_color": [color] * len(rt),
|
|
1742
|
-
}
|
|
1566
|
+
data = {"rt": rt, "delta": delta, "sample": [sample_name] * len(rt), "sample_color": [color] * len(rt)}
|
|
1743
1567
|
src = ColumnDataSource(data)
|
|
1744
1568
|
|
|
1745
1569
|
r_line = p.line("rt", "delta", source=src, line_width=1, color=color)
|
|
@@ -1769,21 +1593,14 @@ def plot_rt_correction(
|
|
|
1769
1593
|
if filename is not None:
|
|
1770
1594
|
# Convert relative paths to absolute paths using study folder as base
|
|
1771
1595
|
import os
|
|
1772
|
-
|
|
1773
1596
|
if not os.path.isabs(filename):
|
|
1774
1597
|
filename = os.path.join(self.folder, filename)
|
|
1775
|
-
|
|
1598
|
+
|
|
1776
1599
|
# Convert to absolute path for logging
|
|
1777
1600
|
abs_filename = os.path.abspath(filename)
|
|
1778
|
-
|
|
1601
|
+
|
|
1779
1602
|
# Use isolated file saving
|
|
1780
|
-
_isolated_save_plot(
|
|
1781
|
-
p,
|
|
1782
|
-
filename,
|
|
1783
|
-
abs_filename,
|
|
1784
|
-
self.logger,
|
|
1785
|
-
"RT Correction Plot",
|
|
1786
|
-
)
|
|
1603
|
+
_isolated_save_plot(p, filename, abs_filename, self.logger, "RT Correction Plot")
|
|
1787
1604
|
else:
|
|
1788
1605
|
# Show in notebook when no filename provided
|
|
1789
1606
|
_isolated_show_notebook(p)
|
|
@@ -1817,18 +1634,10 @@ def plot_chrom(
|
|
|
1817
1634
|
return
|
|
1818
1635
|
|
|
1819
1636
|
# Create color mapping by getting sample_color for each sample_name
|
|
1820
|
-
samples_info = self.samples_df.select(
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
zip(samples_info["sample_name"], samples_info["sample_color"]),
|
|
1825
|
-
)
|
|
1826
|
-
sample_name_to_uid = dict(
|
|
1827
|
-
zip(samples_info["sample_name"], samples_info["sample_uid"]),
|
|
1828
|
-
)
|
|
1829
|
-
color_map = {
|
|
1830
|
-
name: sample_name_to_color.get(name, "#1f77b4") for name in sample_names
|
|
1831
|
-
} # fallback to blue
|
|
1637
|
+
samples_info = self.samples_df.select(["sample_name", "sample_color", "sample_uid"]).to_dict(as_series=False)
|
|
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"]))
|
|
1640
|
+
color_map = {name: sample_name_to_color.get(name, "#1f77b4") for name in sample_names} # fallback to blue
|
|
1832
1641
|
|
|
1833
1642
|
plots = []
|
|
1834
1643
|
self.logger.info(f"Plotting {chroms.shape[0]} chromatograms...")
|
|
@@ -1883,26 +1692,20 @@ def plot_chrom(
|
|
|
1883
1692
|
sorted_indices = np.argsort(rt)
|
|
1884
1693
|
rt = rt[sorted_indices]
|
|
1885
1694
|
inty = inty[sorted_indices]
|
|
1886
|
-
|
|
1695
|
+
|
|
1887
1696
|
# Get sample uid for this sample name
|
|
1888
1697
|
sample_uid = sample_name_to_uid.get(sample, None)
|
|
1889
1698
|
sample_color = color_map.get(sample, "#1f77b4")
|
|
1890
|
-
|
|
1699
|
+
|
|
1891
1700
|
# Create arrays with sample information for hover tooltips
|
|
1892
1701
|
sample_names_array = [sample] * len(rt)
|
|
1893
1702
|
sample_uids_array = [sample_uid] * len(rt)
|
|
1894
1703
|
sample_colors_array = [sample_color] * len(rt)
|
|
1895
|
-
|
|
1704
|
+
|
|
1896
1705
|
curve = hv.Curve(
|
|
1897
|
-
(
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
sample_names_array,
|
|
1901
|
-
sample_uids_array,
|
|
1902
|
-
sample_colors_array,
|
|
1903
|
-
),
|
|
1904
|
-
kdims=["RT"],
|
|
1905
|
-
vdims=["inty", "sample_name", "sample_uid", "sample_color"],
|
|
1706
|
+
(rt, inty, sample_names_array, sample_uids_array, sample_colors_array),
|
|
1707
|
+
kdims=["RT"],
|
|
1708
|
+
vdims=["inty", "sample_name", "sample_uid", "sample_color"]
|
|
1906
1709
|
).opts(
|
|
1907
1710
|
color=color_map[sample],
|
|
1908
1711
|
line_width=1,
|
|
@@ -1912,8 +1715,8 @@ def plot_chrom(
|
|
|
1912
1715
|
("Intensity", "@inty{0,0}"),
|
|
1913
1716
|
("Sample Name", "@sample_name"),
|
|
1914
1717
|
("Sample UID", "@sample_uid"),
|
|
1915
|
-
("Sample Color", "$color[swatch]:sample_color")
|
|
1916
|
-
]
|
|
1718
|
+
("Sample Color", "$color[swatch]:sample_color")
|
|
1719
|
+
]
|
|
1917
1720
|
)
|
|
1918
1721
|
curves.append(curve)
|
|
1919
1722
|
|
|
@@ -1972,26 +1775,19 @@ def plot_chrom(
|
|
|
1972
1775
|
# stack vertically.
|
|
1973
1776
|
# Stack all plots vertically in a Panel column
|
|
1974
1777
|
layout = panel.Column(*[panel.panel(plot) for plot in plots])
|
|
1975
|
-
|
|
1778
|
+
|
|
1976
1779
|
# Apply consistent save/display behavior
|
|
1977
1780
|
if filename is not None:
|
|
1978
1781
|
# Convert relative paths to absolute paths using study folder as base
|
|
1979
1782
|
import os
|
|
1980
|
-
|
|
1981
1783
|
if not os.path.isabs(filename):
|
|
1982
1784
|
filename = os.path.join(self.folder, filename)
|
|
1983
|
-
|
|
1785
|
+
|
|
1984
1786
|
# Convert to absolute path for logging
|
|
1985
1787
|
abs_filename = os.path.abspath(filename)
|
|
1986
|
-
|
|
1788
|
+
|
|
1987
1789
|
# Use isolated Panel saving
|
|
1988
|
-
_isolated_save_panel_plot(
|
|
1989
|
-
panel.panel(layout),
|
|
1990
|
-
filename,
|
|
1991
|
-
abs_filename,
|
|
1992
|
-
self.logger,
|
|
1993
|
-
"Chromatogram Plot",
|
|
1994
|
-
)
|
|
1790
|
+
_isolated_save_panel_plot(panel.panel(layout), filename, abs_filename, self.logger, "Chromatogram Plot")
|
|
1995
1791
|
else:
|
|
1996
1792
|
# Show in notebook when no filename provided
|
|
1997
1793
|
# Convert Panel layout to Bokeh layout for consistent isolated display
|
|
@@ -2025,13 +1821,11 @@ def plot_consensus_stats(
|
|
|
2025
1821
|
"""
|
|
2026
1822
|
from bokeh.layouts import gridplot
|
|
2027
1823
|
from bokeh.models import ColumnDataSource, HoverTool
|
|
2028
|
-
from bokeh.plotting import figure
|
|
1824
|
+
from bokeh.plotting import figure, show, output_file
|
|
2029
1825
|
|
|
2030
1826
|
# Check if consensus_df exists and has data
|
|
2031
1827
|
if self.consensus_df is None or self.consensus_df.is_empty():
|
|
2032
|
-
self.logger.error(
|
|
2033
|
-
"No consensus data available. Run merge/find_consensus first.",
|
|
2034
|
-
)
|
|
1828
|
+
self.logger.error("No consensus data available. Run merge/find_consensus first.")
|
|
2035
1829
|
return
|
|
2036
1830
|
|
|
2037
1831
|
# Define the columns to plot
|
|
@@ -2062,9 +1856,7 @@ def plot_consensus_stats(
|
|
|
2062
1856
|
final_columns = [col for col in columns if col in data_df.columns]
|
|
2063
1857
|
|
|
2064
1858
|
if len(final_columns) < 2:
|
|
2065
|
-
self.logger.error(
|
|
2066
|
-
f"Need at least 2 columns for SPLOM. Available: {final_columns}",
|
|
2067
|
-
)
|
|
1859
|
+
self.logger.error(f"Need at least 2 columns for SPLOM. Available: {final_columns}")
|
|
2068
1860
|
return
|
|
2069
1861
|
|
|
2070
1862
|
self.logger.debug(f"Creating SPLOM with columns: {final_columns}")
|
|
@@ -2237,21 +2029,14 @@ def plot_consensus_stats(
|
|
|
2237
2029
|
if filename is not None:
|
|
2238
2030
|
# Convert relative paths to absolute paths using study folder as base
|
|
2239
2031
|
import os
|
|
2240
|
-
|
|
2241
2032
|
if not os.path.isabs(filename):
|
|
2242
2033
|
filename = os.path.join(self.folder, filename)
|
|
2243
|
-
|
|
2034
|
+
|
|
2244
2035
|
# Convert to absolute path for logging
|
|
2245
2036
|
abs_filename = os.path.abspath(filename)
|
|
2246
|
-
|
|
2037
|
+
|
|
2247
2038
|
# Use isolated file saving
|
|
2248
|
-
_isolated_save_plot(
|
|
2249
|
-
grid,
|
|
2250
|
-
filename,
|
|
2251
|
-
abs_filename,
|
|
2252
|
-
self.logger,
|
|
2253
|
-
"Consensus Stats Plot",
|
|
2254
|
-
)
|
|
2039
|
+
_isolated_save_plot(grid, filename, abs_filename, self.logger, "Consensus Stats Plot")
|
|
2255
2040
|
else:
|
|
2256
2041
|
# Show in notebook when no filename provided
|
|
2257
2042
|
_isolated_show_notebook(grid)
|
|
@@ -2283,9 +2068,10 @@ def plot_pca(
|
|
|
2283
2068
|
title (str): Plot title (default: "PCA of Consensus Matrix")
|
|
2284
2069
|
"""
|
|
2285
2070
|
from bokeh.models import ColumnDataSource, HoverTool, ColorBar, LinearColorMapper
|
|
2286
|
-
from bokeh.plotting import figure
|
|
2071
|
+
from bokeh.plotting import figure, show, output_file
|
|
2287
2072
|
from bokeh.palettes import Category20, viridis
|
|
2288
2073
|
from bokeh.transform import factor_cmap
|
|
2074
|
+
from bokeh.io.export import export_png
|
|
2289
2075
|
from sklearn.decomposition import PCA
|
|
2290
2076
|
from sklearn.preprocessing import StandardScaler
|
|
2291
2077
|
import pandas as pd
|
|
@@ -2300,32 +2086,25 @@ def plot_pca(
|
|
|
2300
2086
|
return
|
|
2301
2087
|
|
|
2302
2088
|
if consensus_matrix is None or consensus_matrix.shape[0] == 0:
|
|
2303
|
-
self.logger.error(
|
|
2304
|
-
"No consensus matrix available. Run merge/find_consensus first.",
|
|
2305
|
-
)
|
|
2089
|
+
self.logger.error("No consensus matrix available. Run merge/find_consensus first.")
|
|
2306
2090
|
return
|
|
2307
2091
|
|
|
2308
2092
|
if samples_df is None or samples_df.is_empty():
|
|
2309
2093
|
self.logger.error("No samples dataframe available.")
|
|
2310
2094
|
return
|
|
2311
2095
|
|
|
2312
|
-
self.logger.debug(
|
|
2313
|
-
f"Performing PCA on consensus matrix with shape: {consensus_matrix.shape}",
|
|
2314
|
-
)
|
|
2096
|
+
self.logger.debug(f"Performing PCA on consensus matrix with shape: {consensus_matrix.shape}")
|
|
2315
2097
|
|
|
2316
2098
|
# Extract only the sample columns (exclude consensus_uid column)
|
|
2317
2099
|
sample_cols = [col for col in consensus_matrix.columns if col != "consensus_uid"]
|
|
2318
|
-
|
|
2100
|
+
|
|
2319
2101
|
# Convert consensus matrix to numpy, excluding the consensus_uid column
|
|
2320
2102
|
if hasattr(consensus_matrix, "select"):
|
|
2321
2103
|
# Polars DataFrame
|
|
2322
2104
|
matrix_data = consensus_matrix.select(sample_cols).to_numpy()
|
|
2323
2105
|
else:
|
|
2324
2106
|
# Pandas DataFrame or other - drop consensus_uid column
|
|
2325
|
-
matrix_sample_data = consensus_matrix.drop(
|
|
2326
|
-
columns=["consensus_uid"],
|
|
2327
|
-
errors="ignore",
|
|
2328
|
-
)
|
|
2107
|
+
matrix_sample_data = consensus_matrix.drop(columns=["consensus_uid"], errors="ignore")
|
|
2329
2108
|
if hasattr(matrix_sample_data, "values"):
|
|
2330
2109
|
matrix_data = matrix_sample_data.values
|
|
2331
2110
|
elif hasattr(matrix_sample_data, "to_numpy"):
|
|
@@ -2356,12 +2135,10 @@ def plot_pca(
|
|
|
2356
2135
|
samples_pd = samples_df.to_pandas()
|
|
2357
2136
|
|
|
2358
2137
|
# Create dataframe with PCA results and sample information
|
|
2359
|
-
pca_df = pd.DataFrame(
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
},
|
|
2364
|
-
)
|
|
2138
|
+
pca_df = pd.DataFrame({
|
|
2139
|
+
"PC1": pca_result[:, 0],
|
|
2140
|
+
"PC2": pca_result[:, 1] if n_components > 1 else np.zeros(len(pca_result)),
|
|
2141
|
+
})
|
|
2365
2142
|
|
|
2366
2143
|
# Add sample information to PCA dataframe
|
|
2367
2144
|
if len(samples_pd) == len(pca_df):
|
|
@@ -2445,31 +2222,21 @@ def plot_pca(
|
|
|
2445
2222
|
# Get colors from samples_df based on the identifier
|
|
2446
2223
|
if id_col == "sample_uid":
|
|
2447
2224
|
sample_colors = (
|
|
2448
|
-
self.samples_df.filter(
|
|
2449
|
-
pl.col("sample_uid").is_in(pca_df[id_col].unique()),
|
|
2450
|
-
)
|
|
2225
|
+
self.samples_df.filter(pl.col("sample_uid").is_in(pca_df[id_col].unique()))
|
|
2451
2226
|
.select(["sample_uid", "sample_color"])
|
|
2452
2227
|
.to_dict(as_series=False)
|
|
2453
2228
|
)
|
|
2454
|
-
color_map = dict(
|
|
2455
|
-
zip(sample_colors["sample_uid"], sample_colors["sample_color"]),
|
|
2456
|
-
)
|
|
2229
|
+
color_map = dict(zip(sample_colors["sample_uid"], sample_colors["sample_color"]))
|
|
2457
2230
|
else: # sample_name
|
|
2458
2231
|
sample_colors = (
|
|
2459
|
-
self.samples_df.filter(
|
|
2460
|
-
pl.col("sample_name").is_in(pca_df[id_col].unique()),
|
|
2461
|
-
)
|
|
2232
|
+
self.samples_df.filter(pl.col("sample_name").is_in(pca_df[id_col].unique()))
|
|
2462
2233
|
.select(["sample_name", "sample_color"])
|
|
2463
2234
|
.to_dict(as_series=False)
|
|
2464
2235
|
)
|
|
2465
|
-
color_map = dict(
|
|
2466
|
-
zip(sample_colors["sample_name"], sample_colors["sample_color"]),
|
|
2467
|
-
)
|
|
2236
|
+
color_map = dict(zip(sample_colors["sample_name"], sample_colors["sample_color"]))
|
|
2468
2237
|
|
|
2469
2238
|
# Map colors into dataframe
|
|
2470
|
-
pca_df["color"] = [
|
|
2471
|
-
color_map.get(x, "#1f77b4") for x in pca_df[id_col]
|
|
2472
|
-
] # fallback to blue
|
|
2239
|
+
pca_df["color"] = [color_map.get(x, "#1f77b4") for x in pca_df[id_col]] # fallback to blue
|
|
2473
2240
|
# Update the ColumnDataSource with new color column
|
|
2474
2241
|
source = ColumnDataSource(pca_df)
|
|
2475
2242
|
scatter = p.scatter(
|
|
@@ -2494,17 +2261,7 @@ def plot_pca(
|
|
|
2494
2261
|
tooltip_list = []
|
|
2495
2262
|
|
|
2496
2263
|
# Columns to exclude from tooltips (file paths and internal/plot fields)
|
|
2497
|
-
excluded_cols = {
|
|
2498
|
-
"file_source",
|
|
2499
|
-
"file_path",
|
|
2500
|
-
"sample_path",
|
|
2501
|
-
"map_id",
|
|
2502
|
-
"PC1",
|
|
2503
|
-
"PC2",
|
|
2504
|
-
"ms1",
|
|
2505
|
-
"ms2",
|
|
2506
|
-
"size",
|
|
2507
|
-
}
|
|
2264
|
+
excluded_cols = {"file_source", "file_path", "sample_path", "map_id", "PC1", "PC2", "ms1", "ms2", "size"}
|
|
2508
2265
|
|
|
2509
2266
|
# Add all sample dataframe columns to tooltips, skipping excluded ones
|
|
2510
2267
|
for col in samples_pd.columns:
|
|
@@ -2536,13 +2293,12 @@ def plot_pca(
|
|
|
2536
2293
|
if filename is not None:
|
|
2537
2294
|
# Convert relative paths to absolute paths using study folder as base
|
|
2538
2295
|
import os
|
|
2539
|
-
|
|
2540
2296
|
if not os.path.isabs(filename):
|
|
2541
2297
|
filename = os.path.join(self.folder, filename)
|
|
2542
|
-
|
|
2298
|
+
|
|
2543
2299
|
# Convert to absolute path for logging
|
|
2544
2300
|
abs_filename = os.path.abspath(filename)
|
|
2545
|
-
|
|
2301
|
+
|
|
2546
2302
|
# Use isolated file saving
|
|
2547
2303
|
_isolated_save_plot(p, filename, abs_filename, self.logger, "PCA Plot")
|
|
2548
2304
|
else:
|
|
@@ -2566,9 +2322,10 @@ def plot_tic(
|
|
|
2566
2322
|
Parameters and behavior mirror `plot_bpc` but use per-sample TICs (get_tic).
|
|
2567
2323
|
"""
|
|
2568
2324
|
# Local imports to avoid heavy top-level deps / circular imports
|
|
2569
|
-
from bokeh.plotting import figure
|
|
2325
|
+
from bokeh.plotting import figure, show, output_file
|
|
2570
2326
|
from bokeh.models import ColumnDataSource, HoverTool
|
|
2571
|
-
from
|
|
2327
|
+
from bokeh.io.export import export_png
|
|
2328
|
+
from masster.study.helpers import get_tic
|
|
2572
2329
|
|
|
2573
2330
|
sample_uids = self._get_sample_uids(samples)
|
|
2574
2331
|
if not sample_uids:
|
|
@@ -2596,12 +2353,7 @@ def plot_tic(
|
|
|
2596
2353
|
except Exception:
|
|
2597
2354
|
continue
|
|
2598
2355
|
|
|
2599
|
-
p = figure(
|
|
2600
|
-
width=width,
|
|
2601
|
-
height=height,
|
|
2602
|
-
title=plot_title,
|
|
2603
|
-
tools="pan,wheel_zoom,box_zoom,reset,save",
|
|
2604
|
-
)
|
|
2356
|
+
p = figure(width=width, height=height, title=plot_title, tools="pan,wheel_zoom,box_zoom,reset,save")
|
|
2605
2357
|
p.xaxis.axis_label = f"Retention Time ({rt_unit})"
|
|
2606
2358
|
p.yaxis.axis_label = "Intensity"
|
|
2607
2359
|
|
|
@@ -2665,22 +2417,10 @@ def plot_tic(
|
|
|
2665
2417
|
|
|
2666
2418
|
color = color_map.get(uid, "#000000")
|
|
2667
2419
|
|
|
2668
|
-
data = {
|
|
2669
|
-
"rt": rt,
|
|
2670
|
-
"inty": inty,
|
|
2671
|
-
"sample": [sample_name] * len(rt),
|
|
2672
|
-
"sample_color": [color] * len(rt),
|
|
2673
|
-
}
|
|
2420
|
+
data = {"rt": rt, "inty": inty, "sample": [sample_name] * len(rt), "sample_color": [color] * len(rt)}
|
|
2674
2421
|
src = ColumnDataSource(data)
|
|
2675
2422
|
|
|
2676
|
-
r_line = p.line(
|
|
2677
|
-
"rt",
|
|
2678
|
-
"inty",
|
|
2679
|
-
source=src,
|
|
2680
|
-
line_width=1,
|
|
2681
|
-
color=color,
|
|
2682
|
-
legend_label=str(sample_name),
|
|
2683
|
-
)
|
|
2423
|
+
r_line = p.line("rt", "inty", source=src, line_width=1, color=color, legend_label=str(sample_name))
|
|
2684
2424
|
p.scatter("rt", "inty", source=src, size=2, color=color, alpha=0.6)
|
|
2685
2425
|
renderers.append(r_line)
|
|
2686
2426
|
|
|
@@ -2707,13 +2447,12 @@ def plot_tic(
|
|
|
2707
2447
|
if filename is not None:
|
|
2708
2448
|
# Convert relative paths to absolute paths using study folder as base
|
|
2709
2449
|
import os
|
|
2710
|
-
|
|
2711
2450
|
if not os.path.isabs(filename):
|
|
2712
2451
|
filename = os.path.join(self.folder, filename)
|
|
2713
|
-
|
|
2452
|
+
|
|
2714
2453
|
# Convert to absolute path for logging
|
|
2715
2454
|
abs_filename = os.path.abspath(filename)
|
|
2716
|
-
|
|
2455
|
+
|
|
2717
2456
|
# Use isolated file saving
|
|
2718
2457
|
_isolated_save_plot(p, filename, abs_filename, self.logger, "TIC Plot")
|
|
2719
2458
|
else:
|