-
Notifications
You must be signed in to change notification settings - Fork 21
/
plot.py
executable file
·288 lines (254 loc) · 8.35 KB
/
plot.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
import functools
import itertools
import math
from typing import Callable, Dict, Iterable, Optional, Tuple, Union
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import spectrum_utils.fragment_annotation as fa
from spectrum_utils.spectrum import MsmsSpectrum
colors = {
"a": "#388E3C",
"b": "#1976D2",
"c": "#00796B",
"x": "#7B1FA2",
"y": "#D32F2F",
"z": "#F57C00",
"m": "#FBC02D",
"I": "#455A64",
"p": "#512DA8",
"?": "#212121",
"f": "#212121",
None: "#212121",
}
zorders = {
"a": 3,
"b": 4,
"c": 3,
"x": 3,
"y": 4,
"z": 3,
"m": 2,
"I": 3,
"p": 3,
"?": 2,
"f": 5,
None: 1,
}
def _annotate_ion(
mz: float,
intensity: float,
annotation: Optional[fa.FragmentAnnotation],
color_ions: bool,
annot_fmt: Optional[Callable],
annot_kws: Dict[str, object],
ax: plt.Axes,
) -> Tuple[str, int]:
"""
Annotate a specific fragment peak.
Parameters
----------
mz : float
The peak's m/z value (position of the annotation on the x axis).
intensity : float
The peak's intensity (position of the annotation on the y axis).
annotation : Optional[fa.FragmentAnnotation]
The annotation that will be plotted.
color_ions : bool
Flag whether to color the peak annotation or not.
annot_fmt : Optional[Callable]
Function to format the peak annotations. See `FragmentAnnotation` for
supported elements. By default, only canonical b and y peptide fragments
are annotated. If `None`, no peaks are annotated.
annot_kws : Dict[str, object]
Keyword arguments for `ax.text` to customize peak annotations.
ax : plt.Axes
Axes instance on which to plot the annotation.
Returns
-------
Tuple[str, int]
A tuple of the annotation's color as a hex string and the annotation's
zorder.
"""
ion_type = annotation.ion_type[0] if annotation is not None else None
color = colors.get(ion_type if color_ions else None)
zorder = zorders.get(ion_type)
if annot_fmt is not None and annotation is not None:
y = intensity + 0.02 * (intensity > 0)
kws = annot_kws.copy()
kws.update(dict(color=color, zorder=zorder))
ax.text(mz, y, annot_fmt(annotation), **kws)
return color, zorder
def annotate_ion_type(
annotation: fa.FragmentAnnotation, ion_types: Iterable[str]
) -> str:
"""
Convert a `FragmentAnnotation` to a string for annotating peaks in a
spectrum plot.
This function will only annotate singly-charged, mono-isotopic canonical
peaks with the given ion type(s).
Parameters
----------
annotation : fa.FragmentAnnotation
The peak's fragment annotation.
ion_types : Iterable[str]
Accepted ion types to annotate.
Returns
-------
str
The peak's annotation string.
"""
if (
annotation.ion_type[0] in ion_types
and annotation.neutral_loss is None
and annotation.isotope == 0
and annotation.charge == 1
):
return str(annotation.ion_type)
else:
return ""
def spectrum(
spec: MsmsSpectrum,
*,
color_ions: bool = True,
annot_fmt: Optional[Callable] = functools.partial(
annotate_ion_type, ion_types="by"
),
annot_kws: Optional[Dict] = None,
mirror_intensity: bool = False,
grid: Union[bool, str] = True,
ax: Optional[plt.Axes] = None,
) -> plt.Axes:
"""
Plot an MS/MS spectrum.
Parameters
----------
spec : MsmsSpectrum
The spectrum to be plotted.
color_ions : bool, optional
Flag indicating whether or not to color annotated fragment ions. The
default is True.
annot_fmt : Optional[Callable]
Function to format the peak annotations. See `FragmentAnnotation` for
supported elements. By default, only canonical b and y peptide fragments
are annotated. If `None`, no peaks are annotated.
annot_kws : Optional[Dict], optional
Keyword arguments for `ax.text` to customize peak annotations.
mirror_intensity : bool, optional
Flag indicating whether to flip the intensity axis or not.
grid : Union[bool, str], optional
Draw grid lines or not. Either a boolean to enable/disable both major
and minor grid lines or 'major'/'minor' to enable major or minor grid
lines respectively.
ax : Optional[plt.Axes], optional
Axes instance on which to plot the spectrum. If None the current Axes
instance is used.
Returns
-------
plt.Axes
The matplotlib Axes instance on which the spectrum is plotted.
"""
if ax is None:
ax = plt.gca()
ax.yaxis.set_major_formatter(mticker.PercentFormatter(xmax=1.0))
ax.set_ylim(*(0, 1) if not mirror_intensity else (-1, 0))
ax.xaxis.set_minor_locator(mticker.AutoLocator())
ax.yaxis.set_minor_locator(mticker.AutoLocator())
ax.xaxis.set_minor_locator(mticker.AutoMinorLocator())
ax.yaxis.set_minor_locator(mticker.AutoMinorLocator())
if grid in (True, "both", "major"):
ax.grid(b=True, which="major", color="#9E9E9E", linewidth=0.2)
if grid in (True, "both", "minor"):
ax.grid(b=True, which="minor", color="#9E9E9E", linewidth=0.2)
ax.set_axisbelow(True)
ax.tick_params(axis="both", which="both", labelsize="small")
ax.set_xlabel("m/z", style="italic")
ax.set_ylabel("Intensity")
if len(spec.mz) == 0:
return ax
round_mz = 50
max_mz = math.ceil(spec.mz[-1] / round_mz + 1) * round_mz
ax.set_xlim(0, max_mz)
max_intensity = spec.intensity.max()
annotations = (
spec.annotation
if spec.annotation is not None
else itertools.repeat(None)
)
annotation_kws = {
"horizontalalignment": "left" if not mirror_intensity else "right",
"verticalalignment": "center",
"rotation": 90,
"rotation_mode": "anchor",
"zorder": 5,
}
if annot_kws is not None:
annotation_kws.update(annot_kws)
for mz, intensity, annotation in zip(spec.mz, spec.intensity, annotations):
peak_intensity = intensity / max_intensity
if mirror_intensity:
peak_intensity *= -1
color, zorder = _annotate_ion(
mz,
peak_intensity,
# Use the first annotation in case there are multiple options.
annotation[0] if annotation is not None else None,
color_ions,
annot_fmt,
annotation_kws,
ax,
)
ax.plot([mz, mz], [0, peak_intensity], color=color, zorder=zorder)
return ax
def mirror(
spec_top: MsmsSpectrum,
spec_bottom: MsmsSpectrum,
spectrum_kws: Optional[Dict] = None,
ax: Optional[plt.Axes] = None,
) -> plt.Axes:
"""
Mirror plot two MS/MS spectra.
Parameters
----------
spec_top : MsmsSpectrum
The spectrum to be plotted on the top.
spec_bottom : MsmsSpectrum
The spectrum to be plotted on the bottom.
spectrum_kws : Optional[Dict], optional
Keyword arguments for `plot.spectrum`.
ax : Optional[plt.Axes], optional
Axes instance on which to plot the spectrum. If None the current Axes
instance is used.
Returns
-------
plt.Axes
The matplotlib Axes instance on which the spectra are plotted.
"""
if ax is None:
ax = plt.gca()
if spectrum_kws is None:
spectrum_kws = {}
# Top spectrum.
spectrum(spec_top, mirror_intensity=False, ax=ax, **spectrum_kws)
y_max = ax.get_ylim()[1]
# Mirrored bottom spectrum.
spectrum(spec_bottom, mirror_intensity=True, ax=ax, **spectrum_kws)
y_min = ax.get_ylim()[0]
ax.set_ylim(y_min, y_max)
ax.axhline(0, color="#9E9E9E", zorder=10)
max_mz_top = spec_top.mz[-1] if len(spec_top.mz) > 0 else 1
max_mz_bottom = spec_bottom.mz[-1] if len(spec_bottom.mz) > 0 else 1
# Update axes so that both spectra fit.
round_mz = 50
max_mz = max(
[
math.ceil(max_mz_top / round_mz + 1) * round_mz,
math.ceil(max_mz_bottom / round_mz + 1) * round_mz,
]
)
ax.set_xlim(0, max_mz)
ax.yaxis.set_major_locator(mticker.AutoLocator())
ax.yaxis.set_minor_locator(mticker.AutoMinorLocator())
ax.yaxis.set_major_formatter(
mticker.FuncFormatter(lambda x, pos: f"{abs(x):.0%}")
)
return ax