MNutils 1.2.0__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.
- mnutils/GEExam.py +495 -0
- mnutils/GESeries.py +1578 -0
- mnutils/__init__.py +54 -0
- mnutils/_scripts.py +177 -0
- mnutils/fitting/AMARES.py +831 -0
- mnutils/fitting/__init__.py +5 -0
- mnutils/matlab/__init__.py +13 -0
- mnutils/matlab/setup_matlab.py +116 -0
- mnutils/plotting/__init__.py +38 -0
- mnutils/plotting/_widgets/__init__.py +5 -0
- mnutils/plotting/_widgets/_html.py +34 -0
- mnutils/plotting/_widgets/_shared/__init__.py +50 -0
- mnutils/plotting/_widgets/_shared/dom.js +124 -0
- mnutils/plotting/_widgets/_shared/theme.css +140 -0
- mnutils/plotting/_widgets/image_grid.css +202 -0
- mnutils/plotting/_widgets/image_grid.js +212 -0
- mnutils/plotting/_widgets/image_grid.py +86 -0
- mnutils/plotting/_widgets/mrsi_inspector.css +376 -0
- mnutils/plotting/_widgets/mrsi_inspector.js +681 -0
- mnutils/plotting/_widgets/mrsi_inspector.py +70 -0
- mnutils/plotting/_widgets/slice_viewer.css +38 -0
- mnutils/plotting/_widgets/slice_viewer.js +41 -0
- mnutils/plotting/_widgets/slice_viewer.py +35 -0
- mnutils/plotting/images.py +1399 -0
- mnutils/plotting/saving.py +138 -0
- mnutils/plotting/spectra.py +357 -0
- mnutils/py.typed +0 -0
- mnutils/testing/__init__.py +3 -0
- mnutils/testing/_spectra.py +338 -0
- mnutils/testing/synthetic.py +373 -0
- mnutils/utils/__init__.py +25 -0
- mnutils/utils/data_loaders.py +218 -0
- mnutils/utils/file_helpers.py +708 -0
- mnutils/utils/images.py +202 -0
- mnutils/utils/nifti.py +379 -0
- mnutils/utils/spectra.py +149 -0
- mnutils-1.2.0.dist-info/METADATA +161 -0
- mnutils-1.2.0.dist-info/RECORD +41 -0
- mnutils-1.2.0.dist-info/WHEEL +4 -0
- mnutils-1.2.0.dist-info/entry_points.txt +7 -0
- mnutils-1.2.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Saving matplotlib figures with per-context (poster/talk/paper) presets."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
from matplotlib.figure import Figure
|
|
9
|
+
|
|
10
|
+
from ..utils import file_helpers
|
|
11
|
+
|
|
12
|
+
contexts = ["poster", "talk", "paper"]
|
|
13
|
+
|
|
14
|
+
DEFAULT_SAVE_PARAMS = {
|
|
15
|
+
"dpi": 150,
|
|
16
|
+
"format": "png",
|
|
17
|
+
"transparent": False,
|
|
18
|
+
"bbox_inches": "tight",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
POSTER_SAVE_PARAMS = DEFAULT_SAVE_PARAMS.copy()
|
|
22
|
+
POSTER_SAVE_PARAMS.update(
|
|
23
|
+
{
|
|
24
|
+
"dpi": 300,
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
TALK_SAVE_PARAMS = DEFAULT_SAVE_PARAMS.copy()
|
|
29
|
+
TALK_SAVE_PARAMS.update(
|
|
30
|
+
{
|
|
31
|
+
"transparent": True,
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
PAPER_SAVE_PARAMS = DEFAULT_SAVE_PARAMS.copy()
|
|
36
|
+
PAPER_SAVE_PARAMS.update(
|
|
37
|
+
{
|
|
38
|
+
"dpi": 300,
|
|
39
|
+
"pad_inches": 0.1,
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def save_figure(
|
|
45
|
+
fig: Figure,
|
|
46
|
+
folder: str | Path,
|
|
47
|
+
filename: str,
|
|
48
|
+
prepend_date: bool = True,
|
|
49
|
+
move_old: bool = True,
|
|
50
|
+
context: Literal["poster", "talk", "paper"] | None = None,
|
|
51
|
+
modify_folder_path: bool = True,
|
|
52
|
+
**kwargs,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Save a matplotlib figure to a file.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
fig : matplotlib.figure.Figure
|
|
59
|
+
The figure to save.
|
|
60
|
+
folder : str or Path
|
|
61
|
+
The folder to save the figure in.
|
|
62
|
+
filename : str
|
|
63
|
+
The name of the file to save the figure as (without extension).
|
|
64
|
+
prepend_date : bool, optional
|
|
65
|
+
Whether to prepend the current date and time to the filename.
|
|
66
|
+
Defaults to True.
|
|
67
|
+
move_old : bool, optional
|
|
68
|
+
Whether to move old files with the same name to an "old" folder
|
|
69
|
+
first. Defaults to True.
|
|
70
|
+
context : {"poster", "talk", "paper"}, optional
|
|
71
|
+
Preset save parameters (dpi, transparency, padding) to use.
|
|
72
|
+
modify_folder_path : bool, optional
|
|
73
|
+
If True and `context` is given, append `context` to `folder` when
|
|
74
|
+
it isn't already the folder's last component. Defaults to True.
|
|
75
|
+
**kwargs
|
|
76
|
+
Additional keyword arguments passed to `fig.savefig`.
|
|
77
|
+
"""
|
|
78
|
+
folder = Path(folder)
|
|
79
|
+
# If folder does not contain "context" as last directory append it to the folder path
|
|
80
|
+
if modify_folder_path and context is not None and folder.name != context:
|
|
81
|
+
folder = folder / context
|
|
82
|
+
|
|
83
|
+
# Combine default save parameters with any provided kwargs
|
|
84
|
+
match context:
|
|
85
|
+
case "poster":
|
|
86
|
+
save_params = POSTER_SAVE_PARAMS.copy()
|
|
87
|
+
case "talk":
|
|
88
|
+
save_params = TALK_SAVE_PARAMS.copy()
|
|
89
|
+
case "paper":
|
|
90
|
+
save_params = PAPER_SAVE_PARAMS.copy()
|
|
91
|
+
case None:
|
|
92
|
+
save_params = DEFAULT_SAVE_PARAMS.copy()
|
|
93
|
+
save_params.update(kwargs)
|
|
94
|
+
|
|
95
|
+
# Create folder if it doesn't exist
|
|
96
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
if move_old:
|
|
98
|
+
file_helpers.move_files_with_glob(folder, f"*{filename}.*")
|
|
99
|
+
|
|
100
|
+
if prepend_date:
|
|
101
|
+
date_str = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
102
|
+
filename = f"{date_str}_{filename}"
|
|
103
|
+
file_extension = save_params.get("format", DEFAULT_SAVE_PARAMS["format"])
|
|
104
|
+
full_path = folder / f"{filename}.{file_extension}"
|
|
105
|
+
|
|
106
|
+
fig.savefig(full_path, **save_params)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def save_current_figure(
|
|
110
|
+
folder: str | Path, filename: str, prepend_date: bool = True, **kwargs
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Save the current matplotlib figure to a file.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
folder : str or Path
|
|
117
|
+
The folder to save the figure in.
|
|
118
|
+
filename : str
|
|
119
|
+
The name of the file to save the figure as (without extension).
|
|
120
|
+
prepend_date : bool, optional
|
|
121
|
+
Whether to prepend the current date and time to the filename.
|
|
122
|
+
Defaults to True.
|
|
123
|
+
**kwargs
|
|
124
|
+
Additional keyword arguments forwarded to `save_figure`.
|
|
125
|
+
"""
|
|
126
|
+
fig = plt.gcf()
|
|
127
|
+
save_figure(fig, folder, filename, prepend_date, **kwargs)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__all__ = [
|
|
131
|
+
"DEFAULT_SAVE_PARAMS",
|
|
132
|
+
"POSTER_SAVE_PARAMS",
|
|
133
|
+
"TALK_SAVE_PARAMS",
|
|
134
|
+
"PAPER_SAVE_PARAMS",
|
|
135
|
+
"contexts",
|
|
136
|
+
"save_figure",
|
|
137
|
+
"save_current_figure",
|
|
138
|
+
]
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import matplotlib.ticker as ticker
|
|
5
|
+
import numpy as np
|
|
6
|
+
import numpy.typing as npt
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import seaborn as sns
|
|
9
|
+
import xarray as xr
|
|
10
|
+
from matplotlib.axes import Axes
|
|
11
|
+
from xmris import DIMS
|
|
12
|
+
|
|
13
|
+
DEFAULT_SPECTRA_AX_PARAMS = {
|
|
14
|
+
"xlim": (8.1, -2.1),
|
|
15
|
+
"xlabel": "$^2$H chemical shift [ppm]",
|
|
16
|
+
"ylabel": "signal [a.u.]",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DefaultTickerParams:
|
|
21
|
+
"""Default tick locator parameters used by `set_default_ticks`."""
|
|
22
|
+
|
|
23
|
+
yticker_bins: int | Literal["auto"] = "auto"
|
|
24
|
+
xticker_bins: int | Literal["auto"] = "auto"
|
|
25
|
+
ticker_steps: list[int] = [1, 2, 5]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def set_default_ticks(ax: Axes) -> None:
|
|
29
|
+
"""Set default tick parameters for a given Axes object."""
|
|
30
|
+
ax.yaxis.set_major_locator(
|
|
31
|
+
ticker.MaxNLocator(
|
|
32
|
+
nbins=DefaultTickerParams.yticker_bins,
|
|
33
|
+
steps=DefaultTickerParams.ticker_steps,
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
ax.xaxis.set_major_locator(
|
|
37
|
+
ticker.MaxNLocator(
|
|
38
|
+
nbins=DefaultTickerParams.xticker_bins,
|
|
39
|
+
steps=DefaultTickerParams.ticker_steps,
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
# By default override stylesheets behaviour of hiding ticks and set bottom and left to true
|
|
43
|
+
ax.tick_params(bottom=True, left=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def set_default_spectra_ax_params(ax: Axes, **kwargs) -> None:
|
|
47
|
+
"""Set default spectra axis parameters for a given Axes object."""
|
|
48
|
+
spectra_params = DEFAULT_SPECTRA_AX_PARAMS.copy()
|
|
49
|
+
spectra_params.update(kwargs)
|
|
50
|
+
ax.set(**spectra_params)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _resolve_spectra_input(
|
|
54
|
+
data: xr.DataArray | None,
|
|
55
|
+
ppm: npt.NDArray[np.floating] | None,
|
|
56
|
+
spectra: npt.NDArray[np.floating] | None,
|
|
57
|
+
labels: list[str] | None,
|
|
58
|
+
) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating], list[str] | None]:
|
|
59
|
+
"""Resolve ppm/spectra arrays and labels from a DataArray or manual fallback args."""
|
|
60
|
+
if data is not None and DIMS.chemical_shift in data.dims:
|
|
61
|
+
ppm = data.coords[DIMS.chemical_shift].values
|
|
62
|
+
spectra = data.values
|
|
63
|
+
if labels is None:
|
|
64
|
+
other_dims = [d for d in data.dims if d != DIMS.chemical_shift]
|
|
65
|
+
if other_dims:
|
|
66
|
+
labels = [str(v) for v in data.coords[other_dims[0]].values]
|
|
67
|
+
return ppm, spectra, labels
|
|
68
|
+
|
|
69
|
+
if ppm is None or spectra is None:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
"Provide either `data` (an xr.DataArray with a "
|
|
72
|
+
f"'{DIMS.chemical_shift}' dim) or both `ppm` and `spectra`."
|
|
73
|
+
)
|
|
74
|
+
return ppm, spectra, labels
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def plot_spectra(
|
|
78
|
+
data: xr.DataArray | None = None,
|
|
79
|
+
ppm: npt.NDArray[np.floating] | None = None,
|
|
80
|
+
spectra: npt.NDArray[np.floating] | None = None,
|
|
81
|
+
ax: Axes | None = None,
|
|
82
|
+
labels: list[str] | None = None,
|
|
83
|
+
line_kwargs: dict | list[dict] | None = None,
|
|
84
|
+
**kwargs,
|
|
85
|
+
) -> Axes:
|
|
86
|
+
"""Plot one or more spectra against a chemical-shift (ppm) axis.
|
|
87
|
+
|
|
88
|
+
Pass already-phased data — either `data` (an `xr.DataArray` with a
|
|
89
|
+
`chemical_shift` dim/coord, e.g. from `MRSSeries.phase_avg_spec` or
|
|
90
|
+
`.xmr.phase()`/`.xmr.autophase()`) or, as a manual fallback, both `ppm`
|
|
91
|
+
and `spectra` as plain arrays. This function does no phasing itself.
|
|
92
|
+
"""
|
|
93
|
+
resolved_ppm, resolved_spectra, resolved_labels = _resolve_spectra_input(
|
|
94
|
+
data, ppm, spectra, labels
|
|
95
|
+
)
|
|
96
|
+
if resolved_labels is None:
|
|
97
|
+
resolved_labels = []
|
|
98
|
+
|
|
99
|
+
if ax is None:
|
|
100
|
+
_, ax = plt.subplots()
|
|
101
|
+
|
|
102
|
+
spectra_arr = np.asarray(resolved_spectra)
|
|
103
|
+
if spectra_arr.ndim == 1:
|
|
104
|
+
spectra_arr = spectra_arr[np.newaxis, :] # Convert to 2D for consistency
|
|
105
|
+
if np.iscomplexobj(spectra_arr):
|
|
106
|
+
spectra_arr = spectra_arr.real
|
|
107
|
+
spectra_arr = spectra_arr.astype(np.float64)
|
|
108
|
+
|
|
109
|
+
if isinstance(line_kwargs, list):
|
|
110
|
+
if len(line_kwargs) != spectra_arr.shape[0]:
|
|
111
|
+
raise ValueError("Length of line_kwargs list must match number of spectra.")
|
|
112
|
+
elif isinstance(line_kwargs, dict):
|
|
113
|
+
line_kwargs = [line_kwargs] * spectra_arr.shape[0]
|
|
114
|
+
|
|
115
|
+
lines = []
|
|
116
|
+
for spec_idx in range(spectra_arr.shape[0]):
|
|
117
|
+
kwargs_to_use = {} if line_kwargs is None else line_kwargs[spec_idx]
|
|
118
|
+
label = (
|
|
119
|
+
resolved_labels[spec_idx]
|
|
120
|
+
if spec_idx < len(resolved_labels)
|
|
121
|
+
else f"Spectrum {spec_idx + 1}"
|
|
122
|
+
)
|
|
123
|
+
(line,) = ax.plot(
|
|
124
|
+
resolved_ppm,
|
|
125
|
+
spectra_arr[spec_idx],
|
|
126
|
+
label=label,
|
|
127
|
+
**kwargs_to_use,
|
|
128
|
+
)
|
|
129
|
+
lines.append(line)
|
|
130
|
+
|
|
131
|
+
set_default_ticks(ax)
|
|
132
|
+
set_default_spectra_ax_params(ax, **kwargs)
|
|
133
|
+
|
|
134
|
+
if spectra_arr.shape[0] > 1 or resolved_labels:
|
|
135
|
+
ax.legend()
|
|
136
|
+
|
|
137
|
+
return ax
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _resolve_fid_input(
|
|
141
|
+
data: xr.DataArray | None,
|
|
142
|
+
time: npt.NDArray[np.floating] | None,
|
|
143
|
+
fids: npt.NDArray[np.complexfloating | np.floating] | None,
|
|
144
|
+
) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.complexfloating | np.floating], str]:
|
|
145
|
+
"""Resolve a time axis (in ms) and fid values from a DataArray or manual fallback args."""
|
|
146
|
+
if data is not None and DIMS.time in data.dims:
|
|
147
|
+
time_axis = data.coords[DIMS.time].values * 1e3 # s -> ms
|
|
148
|
+
return time_axis, data.values, "Time [ms]"
|
|
149
|
+
|
|
150
|
+
if time is None or fids is None:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"Provide either `data` (an xr.DataArray with a '{DIMS.time}' dim) "
|
|
153
|
+
"or both `time` and `fids`."
|
|
154
|
+
)
|
|
155
|
+
return time, fids, "Time [ms]"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def plot_fid(
|
|
159
|
+
data: xr.DataArray | None = None,
|
|
160
|
+
time: npt.NDArray[np.floating] | None = None,
|
|
161
|
+
fids: npt.NDArray[np.complexfloating | np.floating] | None = None,
|
|
162
|
+
ax: Axes | None = None,
|
|
163
|
+
labels: list[str] | None = None,
|
|
164
|
+
show_real: bool = True,
|
|
165
|
+
show_imag: bool = True,
|
|
166
|
+
**kwargs,
|
|
167
|
+
) -> Axes:
|
|
168
|
+
"""Plot one or more FIDs against a time axis.
|
|
169
|
+
|
|
170
|
+
Pass either `data` (an `xr.DataArray` with a `time` dim/coord, e.g.
|
|
171
|
+
`RawMRISeries.fids`) or, as a manual fallback, both `time` (in ms) and
|
|
172
|
+
`fids` as plain arrays. This function does no processing itself — build
|
|
173
|
+
the time axis you want to see on the DataArray/array before calling.
|
|
174
|
+
"""
|
|
175
|
+
if labels is None:
|
|
176
|
+
labels = []
|
|
177
|
+
|
|
178
|
+
xaxis, fid_values, xaxis_label = _resolve_fid_input(data, time, fids)
|
|
179
|
+
fid_values = np.asarray(np.copy(fid_values))
|
|
180
|
+
if fid_values.ndim == 1:
|
|
181
|
+
fid_values = fid_values[np.newaxis, :] # Convert to 2D for consistency
|
|
182
|
+
|
|
183
|
+
if ax is None:
|
|
184
|
+
_, ax = plt.subplots()
|
|
185
|
+
|
|
186
|
+
for i in range(fid_values.shape[0]):
|
|
187
|
+
label_real = f"{labels[i]} (Real)" if i < len(labels) else f"FID {i + 1} (Real)"
|
|
188
|
+
label_imag = f"{labels[i]} (Imag)" if i < len(labels) else f"FID {i + 1} (Imag)"
|
|
189
|
+
if show_real:
|
|
190
|
+
ax.plot(xaxis, np.real(fid_values[i, :]), label=label_real)
|
|
191
|
+
if show_imag and np.iscomplexobj(fid_values):
|
|
192
|
+
ax.plot(xaxis, np.imag(fid_values[i, :]), label=label_imag, linestyle="--")
|
|
193
|
+
|
|
194
|
+
ax.legend()
|
|
195
|
+
ax.set(**kwargs)
|
|
196
|
+
|
|
197
|
+
ax.set_xlabel(xaxis_label)
|
|
198
|
+
ax.set_ylabel("FID signal [a.u.]")
|
|
199
|
+
|
|
200
|
+
set_default_ticks(ax)
|
|
201
|
+
|
|
202
|
+
sns.set_style("ticks")
|
|
203
|
+
sns.set_palette("colorblind")
|
|
204
|
+
sns.despine()
|
|
205
|
+
|
|
206
|
+
return ax
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def annotate_metabolites(ax: Axes | None = None, freqs: dict | None = None) -> Axes:
|
|
210
|
+
"""Annotate metabolite labels on a plot at specific ppm positions.
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
ax : matplotlib.axes.Axes, optional
|
|
215
|
+
The axes to annotate. Defaults to the current axes.
|
|
216
|
+
freqs : dict, optional
|
|
217
|
+
Mapping of metabolite name to ppm position. Defaults to a predefined
|
|
218
|
+
set of common metabolites (DHO, Glu, Glx, Lac).
|
|
219
|
+
|
|
220
|
+
Returns
|
|
221
|
+
-------
|
|
222
|
+
matplotlib.axes.Axes
|
|
223
|
+
The annotated axes.
|
|
224
|
+
"""
|
|
225
|
+
if ax is None:
|
|
226
|
+
ax = plt.gca()
|
|
227
|
+
|
|
228
|
+
if freqs is None:
|
|
229
|
+
freqs = {
|
|
230
|
+
"DHO": 4.68,
|
|
231
|
+
"Glu": 3.7,
|
|
232
|
+
"Glx": 2.3,
|
|
233
|
+
"Lac": 1.35,
|
|
234
|
+
# "Lip": 0.9,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# Get lines and find max value at specific ppm positions
|
|
238
|
+
data = np.array([line.get_ydata() for line in ax.get_lines()])
|
|
239
|
+
ppm_axis = np.array([line.get_xdata() for line in ax.get_lines()])[0]
|
|
240
|
+
|
|
241
|
+
# Get y limits of plot
|
|
242
|
+
ylims = ax.get_ylim()
|
|
243
|
+
label_offset = (ylims[1] - ylims[0]) * 0.1
|
|
244
|
+
|
|
245
|
+
# Plot labels for all the freq offsets slightly above the max value at that position
|
|
246
|
+
for name, ppm in freqs.items():
|
|
247
|
+
# Get the y-value at the specific ppm position
|
|
248
|
+
ppm_index = np.argmin(np.abs(ppm_axis - ppm))
|
|
249
|
+
max_val = np.max(data[:, ppm_index])
|
|
250
|
+
# Only center 4.68 label, others aligned to left
|
|
251
|
+
if ppm == 4.68:
|
|
252
|
+
ha = "center"
|
|
253
|
+
else:
|
|
254
|
+
ha = "left"
|
|
255
|
+
|
|
256
|
+
ax.text(
|
|
257
|
+
ppm,
|
|
258
|
+
max_val + label_offset, # Slightly above the data line
|
|
259
|
+
name,
|
|
260
|
+
rotation=0,
|
|
261
|
+
verticalalignment="top",
|
|
262
|
+
horizontalalignment=ha,
|
|
263
|
+
color="black",
|
|
264
|
+
fontweight="bold",
|
|
265
|
+
)
|
|
266
|
+
return ax
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def plot_spectra_over_time(
|
|
270
|
+
df: pd.DataFrame,
|
|
271
|
+
labels: list[str] | None = None,
|
|
272
|
+
title: str = "MRS Unlocalized Spectra Over Time",
|
|
273
|
+
**kwargs,
|
|
274
|
+
) -> sns.FacetGrid:
|
|
275
|
+
"""Plot MRS unlocalized spectra over time using a FacetGrid.
|
|
276
|
+
|
|
277
|
+
Parameters
|
|
278
|
+
----------
|
|
279
|
+
df : pandas.DataFrame
|
|
280
|
+
DataFrame containing the spectra data with columns
|
|
281
|
+
`["ppm", "Spectra_ID", "Intensity"]`.
|
|
282
|
+
labels : list of str, optional
|
|
283
|
+
Labels for the spectra. Defaults to an empty list.
|
|
284
|
+
title : str, optional
|
|
285
|
+
Figure title. Defaults to "MRS Unlocalized Spectra Over Time".
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
seaborn.FacetGrid
|
|
290
|
+
The resulting FacetGrid.
|
|
291
|
+
"""
|
|
292
|
+
if labels is None:
|
|
293
|
+
labels = []
|
|
294
|
+
|
|
295
|
+
sns.set_theme(style="white", rc={"axes.facecolor": (0, 0, 0, 0)})
|
|
296
|
+
|
|
297
|
+
# 1. Initialize the FacetGrid
|
|
298
|
+
pal = sns.cubehelix_palette(df["Spectra_ID"].nunique(), rot=-0.25, light=0.7)
|
|
299
|
+
spectra_grid = sns.FacetGrid(
|
|
300
|
+
df,
|
|
301
|
+
row="Spectra_ID", # Maps each spectrum to a column (facet)
|
|
302
|
+
hue="Spectra_ID",
|
|
303
|
+
sharey=True, # Set to True if all spectra have a similar intensity range
|
|
304
|
+
height=1, # Height of each facet
|
|
305
|
+
aspect=5, # Aspect ratio of each facet
|
|
306
|
+
palette=pal,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# 2. Map the plotting function
|
|
310
|
+
spectra_grid.map(
|
|
311
|
+
sns.lineplot,
|
|
312
|
+
"ppm", # Your X-axis
|
|
313
|
+
"Intensity", # Your Y-axis
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Add labels to each subplot
|
|
317
|
+
if labels:
|
|
318
|
+
for ax, label in zip(spectra_grid.axes.flatten(), labels):
|
|
319
|
+
# Check how many lines the label will take
|
|
320
|
+
n_lines = label.count("\n") + 1
|
|
321
|
+
if n_lines > 1:
|
|
322
|
+
ypos = 0.05 + (n_lines - 1) * 0.15
|
|
323
|
+
else:
|
|
324
|
+
ypos = 0.05
|
|
325
|
+
|
|
326
|
+
ax.text(
|
|
327
|
+
0.0,
|
|
328
|
+
ypos,
|
|
329
|
+
label,
|
|
330
|
+
fontweight="bold",
|
|
331
|
+
color=ax.lines[0].get_color(),
|
|
332
|
+
ha="left",
|
|
333
|
+
va="center",
|
|
334
|
+
transform=ax.transAxes,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# 3. Apply standard spectral convention (reverse x-axis)
|
|
338
|
+
spectra_grid.set(**DEFAULT_SPECTRA_AX_PARAMS) # Apply default spectra plotting params
|
|
339
|
+
# Set x limits from 10 to 0 ppm
|
|
340
|
+
spectra_grid.set(xlim=(10, -2))
|
|
341
|
+
spectra_grid.set(**kwargs)
|
|
342
|
+
|
|
343
|
+
spectra_grid.figure.subplots_adjust(hspace=-0.75) # Overlay plots vertically
|
|
344
|
+
|
|
345
|
+
# Remove axes details that don't play well with overlap
|
|
346
|
+
spectra_grid.set_titles("")
|
|
347
|
+
spectra_grid.set(yticks=[], ylabel="")
|
|
348
|
+
spectra_grid.despine(bottom=True, left=True)
|
|
349
|
+
spectra_grid.axes[-1][0].spines["bottom"].set_visible(True)
|
|
350
|
+
|
|
351
|
+
# 4. Final adjustments
|
|
352
|
+
spectra_grid.figure.suptitle(
|
|
353
|
+
f"{title}",
|
|
354
|
+
y=1.0,
|
|
355
|
+
fontsize=12,
|
|
356
|
+
)
|
|
357
|
+
return spectra_grid
|
mnutils/py.typed
ADDED
|
File without changes
|