diff --git a/.gitignore b/.gitignore index 628e6404..70c11ab7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ dist/ docs/build docs/gallery docs/generated -docs/gallery_scripts +docs/gallery_scripts \ No newline at end of file diff --git a/pyopenms_viz/_bokeh/__init__.py b/pyopenms_viz/_bokeh/__init__.py index 20d87a40..82ca0426 100644 --- a/pyopenms_viz/_bokeh/__init__.py +++ b/pyopenms_viz/_bokeh/__init__.py @@ -8,6 +8,7 @@ BOKEHLinePlot, BOKEHVLinePlot, BOKEHScatterPlot, + BOKEHSequencePlot, BOKEHChromatogramPlot, BOKEHMobilogramPlot, BOKEHSpectrumPlot, @@ -25,6 +26,7 @@ "line": BOKEHLinePlot, "vline": BOKEHVLinePlot, "scatter": BOKEHScatterPlot, + "sequence": BOKEHSequencePlot, "chromatogram": BOKEHChromatogramPlot, "mobilogram": BOKEHMobilogramPlot, "spectrum": BOKEHSpectrumPlot, diff --git a/pyopenms_viz/_bokeh/core.py b/pyopenms_viz/_bokeh/core.py index c5121618..122bd3de 100644 --- a/pyopenms_viz/_bokeh/core.py +++ b/pyopenms_viz/_bokeh/core.py @@ -27,6 +27,7 @@ LinePlot, VLinePlot, ScatterPlot, + SequencePlot, BaseMSPlot, ChromatogramPlot, MobilogramPlot, @@ -412,6 +413,11 @@ def plot(cls, fig, data, x, y, by: str | None = None, plot_3d=False, **kwargs): return fig, legend +class BOKEHSequencePlot(BOKEHPlot, SequencePlot): + def plot(self): + raise NotImplementedError("Sequence plot is not implemented for Plotly") + + class BOKEH_MSPlot(BaseMSPlot, BOKEHPlot, ABC): def get_line_renderer(self, data, x, y, **kwargs) -> None: diff --git a/pyopenms_viz/_core.py b/pyopenms_viz/_core.py index e1547eb0..b45fc709 100644 --- a/pyopenms_viz/_core.py +++ b/pyopenms_viz/_core.py @@ -24,7 +24,7 @@ from .constants import IS_SPHINX_BUILD -_common_kinds = ("line", "vline", "scatter") +_common_kinds = ("line", "vline", "scatter", "sequence") _msdata_kinds = ("chromatogram", "mobilogram", "spectrum", "peakmap") _all_kinds = _common_kinds + _msdata_kinds _entrypoint_backends = ("ms_matplotlib", "ms_bokeh", "ms_plotly") @@ -446,6 +446,65 @@ def _kind(self): return "scatter" +class SequencePlot(BasePlot, ABC): + """ + Plot peptide sequence with matched fragments indicated. + + Plot Specific Parameters + ------------------------ + seq_col : string, optional + The name for sequence column + ion_annotation : string, optional + The name for the ion annotation column + color_annotation : string, optional + The name for the color annotation column + x_pos : float, optional + The center horizontal position of the peptide sequence. + y_pos : float, optional + The center vertical position of the peptide sequence. + spacing : float, optional + The horizontal spacing between amino acids. + seq_fontsize : str, optional + The font size of the amino acids. + frag_len : float, optional + The length of the fragment lines. + """ + + @property + def _kind(self): + return "sequence" + + def __init__( + self, + data: DataFrame, + x, + y, + seq_col="sequence", + ion_annotation: str = "ion_annotation", + color_annotation: str = "color_annotation", + x_pos: float = 0.5, + y_pos: float = 0.5, + spacing: float = 0.06, + seq_fontsize: str | float = "xx-large", + frag_len: float = 0.06, + **kwargs, + ): + self.seq_col = seq_col + self.ion_annotation = ion_annotation + self.color_annotation = color_annotation + self.x_pos = x_pos + self.y_pos = y_pos + self.spacing = spacing + self.frag_len = frag_len + self.seq_fontsize = seq_fontsize + + # Set default config attributes if not passed as keyword arguments + kwargs["_config"] = _BasePlotConfig(kind=self._kind) + + super().__init__(data, x, y, **kwargs) + self.plot() + + class BaseMSPlot(BasePlot, ABC): """ Abstract class for complex plots, such as chromatograms and mobilograms which are made up of simple plots such as ScatterPlots, VLines and LinePlots. diff --git a/pyopenms_viz/_matplotlib/__init__.py b/pyopenms_viz/_matplotlib/__init__.py index 73328b34..88772d7e 100644 --- a/pyopenms_viz/_matplotlib/__init__.py +++ b/pyopenms_viz/_matplotlib/__init__.py @@ -7,6 +7,7 @@ MATPLOTLIBLinePlot, MATPLOTLIBVLinePlot, MATPLOTLIBScatterPlot, + MATPLOTLIBSequencePlot, MATPLOTLIBChromatogramPlot, MATPLOTLIBMobilogramPlot, MATPLOTLIBSpectrumPlot, @@ -23,6 +24,7 @@ "line": MATPLOTLIBLinePlot, "vline": MATPLOTLIBVLinePlot, "scatter": MATPLOTLIBScatterPlot, + "sequence": MATPLOTLIBSequencePlot, "chromatogram": MATPLOTLIBChromatogramPlot, "mobilogram": MATPLOTLIBMobilogramPlot, "spectrum": MATPLOTLIBSpectrumPlot, diff --git a/pyopenms_viz/_matplotlib/core.py b/pyopenms_viz/_matplotlib/core.py index d1225d84..e29e6283 100644 --- a/pyopenms_viz/_matplotlib/core.py +++ b/pyopenms_viz/_matplotlib/core.py @@ -16,6 +16,7 @@ LinePlot, VLinePlot, ScatterPlot, + SequencePlot, BaseMSPlot, ChromatogramPlot, MobilogramPlot, @@ -444,6 +445,73 @@ def plot( return ax, (legend_lines, legend_labels) +class MATPLOTLIBSequencePlot(MATPLOTLIBPlot, SequencePlot): + + def plot(self): + sequence = self.data[self.seq_col].iloc[0] + n_residues = len(sequence) + + # Remap `x` position to be the left edge of the peptide. + self.x_pos = self.x_pos - n_residues * self.spacing / 2 + self.spacing / 2 + + # Plot the amino acids in the peptide. + for i, aa in enumerate(sequence): + self.fig.text( + *(self.x_pos + i * self.spacing, self.y_pos, aa), + fontsize=self.seq_fontsize, + ha="center", + transform=self.fig.transAxes, + va="center", + ) + # Indicate matched fragments. + for annot, color in zip( + self.data[self.ion_annotation], self.data[self.color_annotation] + ): + ion_type = annot[0] + ion_i = int(i) if (i := annot[1:].rstrip("+")) else 1 + x_i = self.x_pos + self.spacing / 2 + (ion_i - 1) * self.spacing + + # Length of the fragment line. + if ion_type in "ax": + y_i = 2 * self.frag_len + elif ion_type in "by": + y_i = self.frag_len + elif ion_type in "cz": + y_i = 3 * self.frag_len + else: + # Ignore unknown ion types. + continue + + # N-terminal fragmentation. + if ion_type in "abc": + xs = [x_i, x_i, x_i - self.spacing / 2] + ys = [self.y_pos, self.y_pos + y_i, self.y_pos + y_i] + nterm = True + # C-terminal fragmentation. + elif ion_type in "xyz": + xs = [x_i + self.spacing / 2, x_i, x_i] + ys = [self.y_pos - y_i, self.y_pos - y_i, self.y_pos] + nterm = False + else: + # Ignore unknown ion types. + continue + + self.fig.plot( + xs, ys, clip_on=False, color=color, transform=self.fig.transAxes + ) + + self.fig.text( + x_i, + self.y_pos + (1.05 if nterm else -1.05) * y_i, + annot, + color=color, + fontsize=self.annotation_font_size, + ha="right" if nterm else "left", + transform=self.fig.transAxes, + va="top" if not nterm else "bottom", + ) + + class MATPLOTLIB_MSPlot(BaseMSPlot, MATPLOTLIBPlot, ABC): def get_line_renderer(self, data, x, y, **kwargs) -> None: diff --git a/pyopenms_viz/_plotly/__init__.py b/pyopenms_viz/_plotly/__init__.py index 7cd7f43c..f237d414 100644 --- a/pyopenms_viz/_plotly/__init__.py +++ b/pyopenms_viz/_plotly/__init__.py @@ -7,6 +7,7 @@ PLOTLYLinePlot, PLOTLYVLinePlot, PLOTLYScatterPlot, + PLOTLYSequencePlot, PLOTLYChromatogramPlot, PLOTLYMobilogramPlot, PLOTLYSpectrumPlot, @@ -23,6 +24,7 @@ "line": PLOTLYLinePlot, "vline": PLOTLYVLinePlot, "scatter": PLOTLYScatterPlot, + "sequence": PLOTLYSequencePlot, "chromatogram": PLOTLYChromatogramPlot, "mobilogram": PLOTLYMobilogramPlot, "spectrum": PLOTLYSpectrumPlot, diff --git a/pyopenms_viz/_plotly/core.py b/pyopenms_viz/_plotly/core.py index b7041624..a9ae460e 100644 --- a/pyopenms_viz/_plotly/core.py +++ b/pyopenms_viz/_plotly/core.py @@ -17,6 +17,7 @@ LinePlot, VLinePlot, ScatterPlot, + SequencePlot, BaseMSPlot, ChromatogramPlot, MobilogramPlot, @@ -534,6 +535,11 @@ def plot( return fig, None +class PLOTLYSequencePlot(PLOTLYPlot, SequencePlot): + def plot(self): + raise NotImplementedError("Sequence plot is not implemented for Plotly") + + class PLOTLY_MSPlot(BaseMSPlot, PLOTLYPlot, ABC): def get_line_renderer(self, data, x, y, **kwargs) -> None: diff --git a/sequenceplot.py b/sequenceplot.py new file mode 100644 index 00000000..fd1c1aed --- /dev/null +++ b/sequenceplot.py @@ -0,0 +1,83 @@ +def plot(ax, data, x=0.5, y=0.5, spacing=0.06, fontsize="xx-large", fontsize_frag="medium", frag_len=0.06): + """ + Plot peptide sequence with matched fragments indicated. + + Parameters + ---------- + ax : matplotlib.axes.Axes + The axes to plot on. + data : pandas.DataFrame + The spectrum dataframe. + x : float, optional + The center horizontal position of the peptide sequence. + y : float, optional + The center vertical position of the peptide sequence. + spacing : float, optional + The horizontal spacing between amino acids. + fontsize : str, optional + The font size of the amino acids. + fontsize_frag : str, optional + The font size of the fragment annotations. + frag_len : float, optional + The length of the fragment lines. + """ + sequence = data["sequence"].iloc[0] + n_residues = len(sequence) + + # Remap `x` position to be the left edge of the peptide. + x = x - n_residues * spacing / 2 + spacing / 2 + + # Plot the amino acids in the peptide. + for i, aa in enumerate(sequence): + ax.text( + *(x + i * spacing, y, aa), + fontsize=fontsize, + ha="center", + transform=ax.transAxes, + va="center", + ) + # Indicate matched fragments. + for annot, color in zip(data["ion_annotation"], data["color_annotation"]): + ion_type = annot[0] + ion_i = int(i) if (i := annot[1:].rstrip("+")) else 1 + x_i = x + spacing / 2 + (ion_i - 1) * spacing + + # Length of the fragment line. + if ion_type in "ax": + y_i = 2 * frag_len + elif ion_type in "by": + y_i = frag_len + elif ion_type in "cz": + y_i = 3 * frag_len + else: + # Ignore unknown ion types. + continue + + # N-terminal fragmentation. + if ion_type in "abc": + xs = [x_i, x_i, x_i - spacing / 2] + ys = [y, y + y_i, y + y_i] + nterm = True + # C-terminal fragmentation. + elif ion_type in "xyz": + xs = [x_i + spacing / 2, x_i, x_i] + ys = [y - y_i, y - y_i, y] + nterm = False + else: + # Ignore unknown ion types. + continue + + ax.plot( + xs, ys, clip_on=False, color=color, transform=ax.transAxes + ) + + ax.text( + x_i, + y + (1.05 if nterm else -1.05) * y_i, + annot, + color=color, + fontsize=fontsize_frag, + ha="right" if nterm else "left", + transform=ax.transAxes, + va="top" if not nterm else "bottom", + )