Skip to content
Open
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
6 changes: 5 additions & 1 deletion pyopenms_viz/_bokeh/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,11 @@ class BOKEHSpectrumPlot(BOKEH_MSPlot, SpectrumPlot):
Class for assembling a Bokeh spectrum plot
"""

pass
def plot_peptide_sequence(self, peptide_sequence: str, matched_fragments=None):
"""
Raises a NotImplementedError because peptide sequence plotting is not supported for Bokeh.
"""
raise NotImplementedError("Peptide sequence plotting is currently unsupported in the Bokeh backend.")


class BOKEHPeakMapPlot(BOKEH_MSPlot, PeakMapPlot):
Expand Down
6 changes: 6 additions & 0 deletions pyopenms_viz/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,12 @@ class SpectrumConfig(VLineConfig):
mirror_spectrum: bool = False
peak_color: str | None = None

# New fields for peptide sequence plotting
display_peptide_sequence: bool = False
peptide_sequence: str = ""
matched_fragments: list[tuple] = field(default_factory=list)


# Binning settings
bin_peaks: Union[Literal["auto"], bool] = False
bin_method: Literal["none", "sturges", "freedman-diaconis", "mz-tol-bin"] = (
Expand Down
15 changes: 15 additions & 0 deletions pyopenms_viz/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,14 @@ def __init__(
super().__init__(data, **kwargs)

self.plot()

@abstractmethod
def plot_peptide_sequence(self, peptide_sequence: str, matched_fragments=None):
"""
Renders a peptide sequence annotation on the spectrum.
Must be implemented by each backend (e.g., matplotlib).
"""
pass

def load_config(self, **kwargs):
if self._config is None:
Expand Down Expand Up @@ -796,6 +804,13 @@ def plot(self):
self.canvas, ann_texts, ann_xs, ann_ys, ann_colors
)

# If config says display_sequence, call the abstract method
if self.display_peptide_sequence and self.peptide_sequence:
self.plot_peptide_sequence(
self.peptide_sequence,
matched_fragments=self.matched_fragments
)

# Mirror spectrum
if self.mirror_spectrum and self.reference_spectrum is not None:
## create a mirror spectrum
Expand Down
97 changes: 96 additions & 1 deletion pyopenms_viz/_matplotlib/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,102 @@ class MATPLOTLIBSpectrumPlot(MATPLOTLIB_MSPlot, SpectrumPlot):
Class for assembling a matplotlib spectrum plot
"""

pass
def plot_peptide_sequence(
self,
peptide_sequence: str,
matched_fragments=None,
x: float = 0.5, # center horizontal position (axes fraction)
y: float = 0.95, # vertical position (axes fraction)
spacing: float = 0.05, # spacing between residues (axes fraction)
fontsize: int = 12,
fontsize_frag: int = 10,
frag_len: float = 0.05 # length for fragment lines (in axes fraction)
):
"""
Plot peptide sequence with matched fragments indicated.

The peptide is displayed in the top corner and the fragmentation pattern
is drawn as lines coming out from the letters -- using the dataframe columns
'ion_annotation' and 'ion_color' (or 'color_annotation'). No extra arguments are needed.
"""
ax = self.ax

# Compute starting x so that the sequence is centered at x.
n_residues = len(peptide_sequence)
start_x = x - n_residues * spacing / 2 + spacing / 2

# Draw each amino acid letter.
for i, aa in enumerate(peptide_sequence):
ax.text(
start_x + i * spacing,
y,
aa,
fontsize=fontsize,
ha="center",
va="center",
transform=ax.transAxes,
)

# If matched_fragments is not provided, try to extract them from self.data.
if matched_fragments is None and hasattr(self, "data"):
df = self.data
# Check for column "ion_annotation" and either "ion_color" or "color_annotation"
color_col = "ion_color" if "ion_color" in df.columns else "color_annotation"
if "ion_annotation" in df.columns and color_col in df.columns:
matched_fragments = []
for _, row in df.iterrows():
annot = str(row.get("ion_annotation", "")).strip()
frag_color = row.get(color_col, "blue")
# Ensure frag_color is a string; if not, fallback.
if not isinstance(frag_color, str):
frag_color = "blue"
# Extract numeric index from annotation (e.g. "a3+").
if len(annot) > 1:
ion_type = annot[0].lower()
try:
ion_index = int(annot[1:].rstrip("+"))
except ValueError:
continue
frag_x = start_x + (ion_index - 1) * spacing
if ion_type in "abc":
y_offset = frag_len
elif ion_type in "xyz":
y_offset = -frag_len
else:
y_offset = 0
matched_fragments.append((annot, frag_color, frag_x, y_offset))
# Draw the fragments if any.
if matched_fragments:
for frag in matched_fragments:
if len(frag) == 2:
annot, frag_color = frag
frag_x = x
y_offset = frag_len
elif len(frag) >= 4:
annot, frag_color, frag_x, y_offset = frag
else:
continue

if not isinstance(frag_color, str):
frag_color = "blue"

ax.plot(
[frag_x, frag_x],
[y, y + y_offset],
clip_on=False,
color=frag_color,
transform=ax.transAxes,
)
ax.text(
frag_x,
y + (y_offset * 1.1),
annot,
color=frag_color,
fontsize=fontsize_frag,
ha="center",
va="bottom" if y_offset >= 0 else "top",
transform=ax.transAxes,
)


class MATPLOTLIBPeakMapPlot(MATPLOTLIB_MSPlot, PeakMapPlot):
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 @@ -619,6 +619,12 @@ def _prepare_data(self, df, label_suffix=" (ref)"):
self.reference_spectrum[self.by] + label_suffix
)
return df

def plot_peptide_sequence(self, peptide_sequence: str, matched_fragments=None):
"""
Raises a NotImplementedError because peptide sequence plotting is not supported for Plotly.
"""
raise NotImplementedError("Peptide sequence plotting is currently unsupported in the Plotly backend.")


class PLOTLYPeakMapPlot(PLOTLY_MSPlot, PeakMapPlot):
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions test/test_chromatogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

import pytest
import pandas as pd
import matplotlib

# Force non-interactive backend to avoid Tcl/Tk issues
matplotlib.use("Agg")

@pytest.mark.parametrize(
"kwargs",
Expand Down Expand Up @@ -53,3 +56,5 @@ def test_chromatogram_with_annotation(
fig.tight_layout()

assert snapshot == out


38 changes: 38 additions & 0 deletions test/test_spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import pytest
import pandas as pd

import matplotlib

matplotlib.use("Agg")

@pytest.mark.parametrize(
"kwargs",
Expand Down Expand Up @@ -91,3 +94,38 @@ def test_spectrum_with_annotations(spectrum_data, snapshot, kwargs):
fig.tight_layout()

assert snapshot == out


def test_spectrum_peptide_sequence_matplotlib(spectrum_data, snapshot):
"""
Tests that peptide sequence plotting works for Matplotlib
when plotting a spectrum.
"""
kwargs = {
"display_peptide_sequence": True,
"peptide_sequence": "PEPTIDEK", # Standard dummy peptide
"matched_fragments": [(100, 500), (200, 1000)], # (m/z, intensity) pairs
}
out = spectrum_data.plot(
x="mz",
y="intensity",
kind="spectrum",
show_plot=False,
**kwargs,
)
fig = out.get_figure()
fig.tight_layout()
assert snapshot == out

@pytest.mark.parametrize("backend", ["ms_bokeh", "ms_plotly"])
def test_spectrum_peptide_sequence_unsupported(spectrum_data, backend):
pd.options.plotting.backend = backend
with pytest.raises(NotImplementedError, match="unsupported"):
spectrum_data.plot(
x="mz",
y="intensity",
kind="spectrum",
show_plot=False,
display_peptide_sequence=True,
peptide_sequence="PEPTIDEK",
)