Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sequence plot #30

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ dist/
docs/build
docs/gallery
docs/generated
docs/gallery_scripts
docs/gallery_scripts
2 changes: 2 additions & 0 deletions pyopenms_viz/_bokeh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
BOKEHLinePlot,
BOKEHVLinePlot,
BOKEHScatterPlot,
BOKEHSequencePlot,
BOKEHChromatogramPlot,
BOKEHMobilogramPlot,
BOKEHSpectrumPlot,
Expand All @@ -25,6 +26,7 @@
"line": BOKEHLinePlot,
"vline": BOKEHVLinePlot,
"scatter": BOKEHScatterPlot,
"sequence": BOKEHSequencePlot,
"chromatogram": BOKEHChromatogramPlot,
"mobilogram": BOKEHMobilogramPlot,
"spectrum": BOKEHSpectrumPlot,
Expand Down
6 changes: 6 additions & 0 deletions pyopenms_viz/_bokeh/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
LinePlot,
VLinePlot,
ScatterPlot,
SequencePlot,
BaseMSPlot,
ChromatogramPlot,
MobilogramPlot,
Expand Down Expand Up @@ -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:
Expand Down
61 changes: 60 additions & 1 deletion pyopenms_viz/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions pyopenms_viz/_matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
MATPLOTLIBLinePlot,
MATPLOTLIBVLinePlot,
MATPLOTLIBScatterPlot,
MATPLOTLIBSequencePlot,
MATPLOTLIBChromatogramPlot,
MATPLOTLIBMobilogramPlot,
MATPLOTLIBSpectrumPlot,
Expand All @@ -23,6 +24,7 @@
"line": MATPLOTLIBLinePlot,
"vline": MATPLOTLIBVLinePlot,
"scatter": MATPLOTLIBScatterPlot,
"sequence": MATPLOTLIBSequencePlot,
"chromatogram": MATPLOTLIBChromatogramPlot,
"mobilogram": MATPLOTLIBMobilogramPlot,
"spectrum": MATPLOTLIBSpectrumPlot,
Expand Down
68 changes: 68 additions & 0 deletions pyopenms_viz/_matplotlib/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
LinePlot,
VLinePlot,
ScatterPlot,
SequencePlot,
BaseMSPlot,
ChromatogramPlot,
MobilogramPlot,
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions pyopenms_viz/_plotly/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
PLOTLYLinePlot,
PLOTLYVLinePlot,
PLOTLYScatterPlot,
PLOTLYSequencePlot,
PLOTLYChromatogramPlot,
PLOTLYMobilogramPlot,
PLOTLYSpectrumPlot,
Expand All @@ -23,6 +24,7 @@
"line": PLOTLYLinePlot,
"vline": PLOTLYVLinePlot,
"scatter": PLOTLYScatterPlot,
"sequence": PLOTLYSequencePlot,
"chromatogram": PLOTLYChromatogramPlot,
"mobilogram": PLOTLYMobilogramPlot,
"spectrum": PLOTLYSpectrumPlot,
Expand Down
6 changes: 6 additions & 0 deletions pyopenms_viz/_plotly/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
LinePlot,
VLinePlot,
ScatterPlot,
SequencePlot,
BaseMSPlot,
ChromatogramPlot,
MobilogramPlot,
Expand Down Expand Up @@ -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:
Expand Down
83 changes: 83 additions & 0 deletions sequenceplot.py
Original file line number Diff line number Diff line change
@@ -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",
)