#!/usr/bin/env python3
#
# __init__.py
"""
Plotting extension for PyMassSpec.
"""
################################################################################
# #
# PyMassSpec software for processing of mass-spectrometry data #
# Copyright (C) 2005-2012 Vladimir Likic #
# Copyright (C) 2019-2021 Dominic Davis-Foster #
# #
# This program is free software; you can redistribute it and/or modify #
# it under the terms of the GNU General Public License version 2 as #
# published by the Free Software Foundation. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program; if not, write to the Free Software #
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. #
# #
################################################################################
# stdlib
from typing import List, Mapping, Optional, Sequence, Tuple
# 3rd party
import matplotlib # type: ignore[import-untyped]
import matplotlib.pyplot as plt # type: ignore[import-untyped]
from matplotlib.axes import Axes # type: ignore[import-untyped]
from matplotlib.backend_bases import MouseEvent # type: ignore[import-untyped]
from matplotlib.container import BarContainer # type: ignore[import-untyped]
from matplotlib.figure import Figure # type: ignore[import-untyped]
from matplotlib.lines import Line2D # type: ignore[import-untyped]
from pyms import Peak
from pyms.IonChromatogram import IonChromatogram
from pyms.Peak.List.Function import is_peak_list
from pyms.Spectrum import MassSpectrum, normalize_mass_spec
# this package
from pymassspec_plot.utils import invert_mass_spec
__author__: str = "Dominic Davis-Foster"
__copyright__: str = "2020-2021 Dominic Davis-Foster"
__license__: str = "GNU General Public License v2 (GPLv2)"
__version__: str = "0.2.0"
__email__: str = "dominic@davis-foster.co.uk"
__all__ = ["plot_ic", "plot_mass_spec", "plot_peaks", "plot_head2tail", "ClickEventHandler"]
default_filetypes = ["png", "pdf", "svg"]
# Ensure that the intersphinx links are correct.
Axes.__module__ = "matplotlib.axes"
Figure.__module__ = "matplotlib.figure"
[docs]def plot_ic(
ax: matplotlib.axes.Axes,
ic: IonChromatogram,
minutes: bool = False,
**kwargs,
) -> List[Line2D]:
"""
Plots an Ion Chromatogram.
:param ax: The axes to plot the IonChromatogram on.
:param ic: Ion chromatogram m/z channels for plotting.
:param minutes: Whether the x-axis should be plotted in minutes. Default :py:obj:`False` (plotted in seconds)
:no-default minutes:
:Other Parameters: :class:`matplotlib.lines.Line2D` properties.
Used to specify properties like a line label (for auto legends),
linewidth, antialiasing, marker face color.
.. code-block:: python
>>> plot_ic(im.get_ic_at_index(5), label='IC @ Index 5', linewidth=2)
See :class:`matplotlib.lines.Line2D` for the list of possible keyword arguments.
:return: A list of Line2D objects representing the plotted data.
"""
if not isinstance(ic, IonChromatogram):
raise TypeError("'ic' must be an IonChromatogram")
time_list = ic.time_list
if minutes:
time_list = [time / 60 for time in time_list]
plot = ax.plot(time_list, ic.intensity_array, **kwargs)
# Set axis ranges
ax.set_xlim(min(time_list), max(time_list))
ax.set_ylim(bottom=0)
return plot
[docs]def plot_mass_spec(ax: Axes, mass_spec: MassSpectrum, **kwargs) -> BarContainer:
"""
Plots a Mass Spectrum.
:param ax: The axes to plot the :class:`~pyms.Spectrum.MassSpectrum` on.
:param mass_spec: The mass spectrum to plot.
:Other Parameters: :class:`matplotlib.lines.Line2D` properties.
Used to specify properties like a line label (for auto legends),
linewidth, antialiasing, marker face color.
Example::
>>> plot_mass_spec(im.get_ms_at_index(5), linewidth=2)
>>> ax.set_title(f"Mass spec for peak at time {im.get_time_at_index(5):5.2f}")
See :class:`matplotlib.lines.Line2D` for the list of possible keyword arguments.
:return: Container with all the bars, and optionally errorbars.
"""
if not isinstance(mass_spec, MassSpectrum):
raise TypeError("'mass_spec' must be a MassSpectrum")
mass_list = mass_spec.mass_list
intensity_list = mass_spec.mass_spec
if "width" not in kwargs:
kwargs["width"] = 0.5
# to set x axis range find minimum and maximum m/z channels
min_mz = mass_list[0]
max_mz = mass_list[-1]
for idx, mass in enumerate(mass_list):
if mass_list[idx] > max_mz:
max_mz = mass_list[idx]
for idx, mass in enumerate(mass_list):
if mass_list[idx] < min_mz:
min_mz = mass_list[idx]
plot = ax.bar(mass_list, intensity_list, **kwargs)
# Set axis ranges
ax.set_xlim(min_mz - 1, max_mz + 1)
ax.set_ylim(bottom=0)
return plot
_spec_quargs_t = "'{0}_spec_kwargs' must be a mapping of keyword arguments for the {0} mass spectrum."
[docs]def plot_head2tail(
ax: Axes,
top_mass_spec: MassSpectrum,
bottom_mass_spec: MassSpectrum,
top_spec_kwargs: Optional[Mapping] = None,
bottom_spec_kwargs: Optional[Mapping] = None,
) -> Tuple[BarContainer, BarContainer]:
"""
Plots two mass spectra head to tail.
:param ax: The axes to plot the MassSpectra on.
:param top_mass_spec: The mass spectrum to plot on top.
:param bottom_mass_spec: The mass spectrum to plot on the bottom.
:param top_spec_kwargs: A dictionary of keyword arguments for the top mass spectrum.
Defaults to red with a line width of ``0.5``.
:no-default top_spec_kwargs:
:param bottom_spec_kwargs: A dictionary of keyword arguments for the bottom mass spectrum.
Defaults to blue with a line width of ``0.5``.
:no-default bottom_spec_kwargs:
``top_spec_kwargs`` and ``bottom_spec_kwargs`` are used to specify properties like a line label
(for auto legends), linewidth, antialiasing, and marker face color.
See :class:`matplotlib.lines.Line2D` for the list of possible keyword arguments.
:return: A tuple of containers with all the bars, and optionally errorbars, for the top and bottom spectra.
.. clearpage::
"""
if not isinstance(top_mass_spec, MassSpectrum):
raise TypeError("'top_mass_spec' must be a MassSpectrum")
if not isinstance(bottom_mass_spec, MassSpectrum):
raise TypeError("'bottom_mass_spec' must be a MassSpectrum")
if top_spec_kwargs is None:
top_spec_kwargs = dict(color="red", width=0.5)
elif not isinstance(top_spec_kwargs, Mapping):
raise TypeError(_spec_quargs_t.format("top"))
if bottom_spec_kwargs is None:
bottom_spec_kwargs = dict(color="blue", width=0.5)
elif not isinstance(bottom_spec_kwargs, Mapping):
raise TypeError(_spec_quargs_t.format("bottom"))
# Plot a line at y=0 with same width and colour as Spines
ax.axhline(
y=0,
color=ax.spines["bottom"].get_edgecolor(),
linewidth=ax.spines["bottom"].get_linewidth(),
)
# Normalize the mass spectra
top_mass_spec = normalize_mass_spec(top_mass_spec)
bottom_mass_spec = normalize_mass_spec(bottom_mass_spec)
# Invert bottom mass spec
invert_mass_spec(bottom_mass_spec, inplace=True)
top_plot = plot_mass_spec(ax, top_mass_spec, **top_spec_kwargs)
bottom_plot = plot_mass_spec(ax, bottom_mass_spec, **bottom_spec_kwargs)
# Set ylim to 1.1 times max/min values
ax.set_ylim(
bottom=min(bottom_mass_spec.intensity_list) * 1.1,
top=max(top_mass_spec.intensity_list) * 1.1,
)
# ax.spines['bottom'].set_position('zero')
return top_plot, bottom_plot
[docs]def plot_peaks(
ax: Axes,
peak_list: Sequence[Peak.Peak],
label: str = "Peaks",
style: str = 'o',
) -> List[Line2D]:
"""
Plots the locations of peaks as found by PyMassSpec.
:param ax: The axes to plot the peaks on.
:param peak_list: List of peaks to plot.
:param label: label for plot legend.
:param style: The marker style. See :mod:`matplotlib.markers` for a complete list
:return: A list of Line2D objects representing the plotted data.
"""
if not is_peak_list(peak_list):
raise TypeError("'peak_list' must be a list of Peak objects")
time_list = []
height_list = []
if "line" in style.lower():
lines = []
for peak in peak_list:
lines.append(ax.axvline(x=peak.rt, color="lightgrey", alpha=0.8, linewidth=0.3))
return lines
else:
for peak in peak_list:
time_list.append(peak.rt)
height_list.append(sum(peak.mass_spectrum.intensity_list))
# height_list.append(peak.height)
# print(peak.height - sum(peak.mass_spectrum.intensity_list))
# print(sum(peak.mass_spectrum.intensity_list))
return ax.plot(time_list, height_list, style, label=label)
# TODO: Change order of arguments and use plt.gca() a la pyplot
[docs]class ClickEventHandler:
"""
Class to enable clicking of a chromatogram to view the intensities top ``n_intensities``
most intense ions at that peak, and viewing of the mass spectrum with a right click.
:param peak_list: The list of peaks identified in the chromatogram.
:param fig: The figure to associate the event handler with.
Defaults to the current figure. (see :func:`plt.gcf() <matplotlib.pyplot.gcf>`.
:no-default fig:
:param ax: The axes to associate the event handler with.
Defaults to the current axes. (see :func:`plt.gca() <matplotlib.pyplot.gca>`.
:no-default ax:
:param tolerance:
:param n_intensities:
""" # noqa: D400
#: The figure to associate the event handler with.
fig: Figure
#: The axes to associate the event handler with.
ax: Axes
#: The figure to plot mass spectra on after right clicking the plot.
ms_fig: Figure
#: The axes to plot mass spectra on after right clicking the plot.
ms_ax: Axes
#: The number of top intensities to show in the terminal when left clicking the plot.
n_intensities: int
#: The callback ID for the button press event.
cid: Optional[int]
def __init__(
self,
peak_list: Sequence[Peak.Peak],
fig: Optional[Figure] = None,
ax: Optional[Axes] = None,
tolerance: float = 0.005,
n_intensities: int = 5,
):
if fig is None:
self.fig = plt.gcf()
else:
self.fig = fig
if ax is None:
self.ax = plt.gca()
else:
self.ax = ax
self.peak_list = peak_list
self.ms_fig: Optional[Figure] = None
self.ms_ax: Optional[Axes] = None
self._min = 1 - tolerance
self._max = 1 + tolerance
self.n_intensities = n_intensities
# If no peak list plot, no mouse click event
if len(self.peak_list):
self.cid = self.fig.canvas.mpl_connect("button_press_event", self.onclick)
else:
self.cid = None
[docs] def onclick(self, event: MouseEvent) -> None:
"""
Finds the n highest intensity m/z channels for the selected peak.
The peak is selected by clicking on it.
If a button other than the left one is clicked, a new plot of the mass spectrum is displayed.
:param event: a mouse click by the user
"""
for peak in self.peak_list:
# if event.xdata > 0.9999*peak.rt and event.xdata < 1.0001*peak.rt:
if self._min * peak.rt < event.xdata < self._max * peak.rt:
intensity_list = peak.mass_spectrum.mass_spec
mass_list = peak.mass_spectrum.mass_list
largest = self.get_n_largest(intensity_list)
print(f"RT: {peak.rt}")
print("Mass\t Intensity")
for i in range(self.n_intensities):
print(f"{mass_list[largest[i]]}\t {intensity_list[largest[i]]}")
# Check if right mouse button pressed, if so plot mass spectrum
# Also check that a peak was selected, not just whitespace
if event.button == 3 and len(intensity_list):
if self.ms_fig is None or self.ms_ax is None:
self.ms_fig, self.ms_ax = plt.subplots(1, 1)
else:
self.ms_ax.clear()
plot_mass_spec(self.ms_ax, peak.mass_spectrum)
self.ms_ax.set_title(f"Mass Spectrum at RT {peak.rt}")
self.ms_fig.show()
# TODO: Add multiple MS to same plot window and add option to close one of them
# TODO: Allow more interaction with MS, e.g. adjusting mass range?
return
# if the selected point is not close enough to peak
print("No Peak at this point")
[docs] def get_n_largest(self, intensity_list: List[float]) -> List[int]:
"""
Computes the indices of the largest n ion intensities for writing to console.
:param intensity_list: List of ion intensities.
:return: Indices of largest :attr:`~.n_intensities` ion intensities.
"""
largest = [0] * self.n_intensities
# Find out largest value
for idx, intensity in enumerate(intensity_list):
if intensity > intensity_list[largest[0]]:
largest[0] = idx
# Now find next four largest values
for j in list(range(1, self.n_intensities)):
for idx, intensity in enumerate(intensity_list):
# if intensity_list[i] > intensity_list[largest[j]] and intensity_list[i] < intensity_list[largest[j-1]]:
if intensity_list[largest[j]] < intensity < intensity_list[largest[j - 1]]:
largest[j] = idx
return largest