Skip to content

Commit 64f58ae

Browse files
committed
Simplifying MultiPlot Interface #83
1 parent 40549e8 commit 64f58ae

File tree

3 files changed

+117
-93
lines changed

3 files changed

+117
-93
lines changed
Lines changed: 36 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
"""
2-
Plot Spyogenes subplots ms_matplotlib
3-
=======================================
2+
Plot Spyogenes subplots ms_matplotlib using tile_by
3+
====================================================
44
5-
Here we show how we can plot multiple chromatograms across runs together
5+
This script downloads the Spyogenes data and uses the new tile_by parameter to create subplots automatically.
66
"""
77

88
import pandas as pd
99
import requests
1010
import zipfile
11-
import numpy as np
1211
import matplotlib.pyplot as plt
12+
import sys
1313

14+
# Append the local module path
15+
16+
# Set the plotting backend
1417
pd.options.plotting.backend = "ms_matplotlib"
1518

1619
###### Load Data #######
17-
18-
# URL of the zip file
1920
url = "https://github.com/OpenMS/pyopenms_viz/releases/download/v0.1.3/spyogenes.zip"
2021
zip_filename = "spyogenes.zip"
2122

@@ -24,73 +25,47 @@
2425
print(f"Downloading {zip_filename}...")
2526
response = requests.get(url)
2627
response.raise_for_status() # Check for any HTTP errors
27-
28-
# Save the zip file to the current directory
2928
with open(zip_filename, "wb") as out:
3029
out.write(response.content)
3130
print(f"Downloaded {zip_filename} successfully.")
32-
except requests.RequestException as e:
31+
except Exception as e:
3332
print(f"Error downloading zip file: {e}")
34-
except IOError as e:
35-
print(f"Error writing zip file: {e}")
3633

37-
# Unzipping the file
34+
# Unzip the file
3835
try:
3936
with zipfile.ZipFile(zip_filename, "r") as zip_ref:
40-
# Extract all files to the current directory
4137
zip_ref.extractall()
4238
print("Unzipped files successfully.")
43-
except zipfile.BadZipFile as e:
39+
except Exception as e:
4440
print(f"Error unzipping file: {e}")
4541

46-
annotation_bounds = pd.read_csv(
47-
"spyogenes/AADGQTVSGGSILYR3_manual_annotations.tsv", sep="\t"
48-
) # contain annotations across all runs
49-
chrom_df = pd.read_csv(
50-
"spyogenes/chroms_AADGQTVSGGSILYR3.tsv", sep="\t"
51-
) # contains chromatogram for precursor across all runs
52-
53-
##### Set Plotting Variables #####
54-
pd.options.plotting.backend = "ms_matplotlib"
55-
RUN_NAMES = [
56-
"Run #0 Spyogenes 0% human plasma",
57-
"Run #1 Spyogenes 0% human plasma",
58-
"Run #2 Spyogenes 0% human plasma",
59-
"Run #3 Spyogenes 10% human plasma",
60-
"Run #4 Spyogenes 10% human plasma",
61-
"Run #5 Spyogenes 10% human plasma",
62-
]
63-
64-
fig, axs = plt.subplots(len(np.unique(chrom_df["run"])), 1, figsize=(10, 15))
65-
66-
# plt.close ### required for running in jupyter notebook setting
67-
68-
# For each run fill in the axs object with the corresponding chromatogram
69-
plot_list = []
70-
for i, run in enumerate(RUN_NAMES):
71-
run_df = chrom_df[chrom_df["run_name"] == run]
72-
current_bounds = annotation_bounds[annotation_bounds["run"] == run]
42+
# Load the data
43+
annotation_bounds = pd.read_csv("spyogenes/AADGQTVSGGSILYR3_manual_annotations.tsv", sep="\t")
44+
chrom_df = pd.read_csv("spyogenes/chroms_AADGQTVSGGSILYR3.tsv", sep="\t")
7345

74-
run_df.plot(
75-
kind="chromatogram",
76-
x="rt",
77-
y="int",
78-
grid=False,
79-
by="ion_annotation",
80-
title=run_df.iloc[0]["run_name"],
81-
title_font_size=16,
82-
xaxis_label_font_size=14,
83-
yaxis_label_font_size=14,
84-
xaxis_tick_font_size=12,
85-
yaxis_tick_font_size=12,
86-
canvas=axs[i],
87-
relative_intensity=True,
88-
annotation_data=current_bounds,
89-
xlabel="Retention Time (sec)",
90-
ylabel="Relative\nIntensity",
91-
annotation_legend_config=dict(show=False),
92-
legend_config={"show": False},
93-
)
46+
##### Plotting Using Tile By #####
47+
# Instead of pre-creating subplots and looping over RUN_NAMES,
48+
# we call the plot method once and provide a tile_by parameter.
49+
fig = chrom_df.plot(
50+
kind="chromatogram",
51+
x="rt",
52+
y="int",
53+
tile_by="run_name", # Automatically groups data by run_name and creates subplots
54+
tile_columns=1, # Layout: 1 column (one subplot per row)
55+
grid=False,
56+
by="ion_annotation",
57+
title_font_size=16,
58+
xaxis_label_font_size=14,
59+
yaxis_label_font_size=14,
60+
xaxis_tick_font_size=12,
61+
yaxis_tick_font_size=12,
62+
relative_intensity=True,
63+
annotation_data=annotation_bounds,
64+
xlabel="Retention Time (sec)",
65+
ylabel="Relative\nIntensity",
66+
annotation_legend_config={"show": False},
67+
legend_config={"show": False},
68+
)
9469

9570
fig.tight_layout()
9671
fig

pyopenms_viz/_config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@ def default_legend_factory():
205205
legend_config: LegendConfig | dict = field(default_factory=default_legend_factory)
206206
opacity: float = 1.0
207207

208+
tile_by: str | None = None # Name of the column to tile the plot by.
209+
tile_columns: int = 1 # How many columns in the subplot grid.
210+
tile_figsize: Tuple[int, int] = (10, 15) # Overall figure size for tiled plots.
211+
208212
def __post_init__(self):
209213
# if legend_config is a dictionary, update it to LegendConfig object
210214
if isinstance(self.legend_config, dict):

pyopenms_viz/_core.py

Lines changed: 77 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
from pandas.util._decorators import Appender
1515
import re
1616

17+
import matplotlib.pyplot as plt
18+
from math import ceil
19+
import numpy as np
20+
1721
from numpy import ceil, log1p, log2, nan, mean, repeat, concatenate
1822
from ._config import (
1923
LegendConfig,
@@ -539,7 +543,6 @@ def _create_tooltips(self, entries: dict, index: bool = True):
539543

540544

541545
class ChromatogramPlot(BaseMSPlot, ABC):
542-
543546
_config: ChromatogramConfig = None
544547

545548
@property
@@ -560,9 +563,7 @@ def load_config(self, **kwargs):
560563

561564
def __init__(self, data, config: ChromatogramConfig = None, **kwargs) -> None:
562565
super().__init__(data, config, **kwargs)
563-
564566
self.label_suffix = self.x # set label suffix for bounding box
565-
566567
self._check_and_aggregate_duplicates()
567568

568569
# sort data by x so in order
@@ -579,45 +580,89 @@ def __init__(self, data, config: ChromatogramConfig = None, **kwargs) -> None:
579580

580581
def plot(self):
581582
"""
582-
Create the plot
583+
Create the plot. If the configuration includes a valid tile_by column,
584+
the data will be split into subplots based on unique values in that column.
583585
"""
584-
tooltip_entries = {"retention time": self.x, "intensity": self.y}
585-
if "Annotation" in self.data.columns:
586-
tooltip_entries["annotation"] = "Annotation"
587-
if "product_mz" in self.data.columns:
588-
tooltip_entries["product m/z"] = "product_mz"
589-
tooltips, custom_hover_data = self._create_tooltips(
590-
tooltip_entries, index=False
591-
)
592-
593-
linePlot = self.get_line_renderer(data=self.data, config=self._config)
594-
595-
self.canvas = linePlot.generate(tooltips, custom_hover_data)
596-
self._modify_y_range((0, self.data[self.y].max()), (0, 0.1))
597-
598-
if self._interactive:
599-
self.manual_boundary_renderer = self._add_bounding_vertical_drawer()
600-
601-
if self.annotation_data is not None:
602-
self._add_peak_boundaries(self.annotation_data)
586+
# Check for tiling functionality
587+
tile_by = self._config.tile_by if hasattr(self._config, "tile_by") else None
588+
589+
if tile_by and tile_by in self.data.columns:
590+
# Group the data by the tile_by column
591+
grouped = self.data.groupby(tile_by)
592+
num_groups = len(grouped)
593+
594+
# Get tiling options from config
595+
tile_columns = self._config.tile_columns if hasattr(self._config, "tile_columns") else 1
596+
tile_rows = int(ceil(num_groups / tile_columns))
597+
figsize = self._config.tile_figsize if hasattr(self._config, "tile_figsize") else (10, 15)
598+
599+
# Create a figure with a grid of subplots
600+
fig, axes = plt.subplots(tile_rows, tile_columns, figsize=figsize, squeeze=False)
601+
axes = axes.flatten() # Easier indexing for a 1D list
602+
603+
# Loop through each group and plot on its own axis
604+
for i, (group_val, group_df) in enumerate(grouped):
605+
ax = axes[i]
606+
607+
# Prepare tooltips for this group (if applicable)
608+
tooltip_entries = {"retention time": self.x, "intensity": self.y}
609+
if "Annotation" in group_df.columns:
610+
tooltip_entries["annotation"] = "Annotation"
611+
if "product_mz" in group_df.columns:
612+
tooltip_entries["product m/z"] = "product_mz"
613+
tooltips, custom_hover_data = self._create_tooltips(tooltip_entries, index=False)
614+
615+
# Get a line renderer instance and generate the plot for the current group,
616+
# passing the current axis (canvas) using a parameter like `canvas` or `ax`.
617+
linePlot = self.get_line_renderer(data=group_df, config=self._config)
618+
# Here, we assume that your renderer can accept the axis to plot on:
619+
linePlot.canvas = ax
620+
linePlot.generate(tooltips, custom_hover_data)
621+
622+
623+
# Set the title of this subplot based on the group value
624+
ax.set_title(f"{tile_by}: {group_val}", fontsize=14)
625+
# Optionally adjust the y-axis limits for the subplot
626+
ax.set_ylim(0, group_df[self.y].max())
627+
628+
# If you have annotations that should be split, filter them too
629+
if self.annotation_data is not None and tile_by in self.annotation_data.columns:
630+
group_annotations = self.annotation_data[self.annotation_data[tile_by] == group_val]
631+
self._add_peak_boundaries(group_annotations)
632+
633+
# Remove any extra axes if the grid size is larger than the number of groups
634+
for j in range(i + 1, len(axes)):
635+
fig.delaxes(axes[j])
636+
637+
fig.tight_layout()
638+
self.canvas = fig
639+
else:
640+
# Fallback: plot on a single canvas if no valid tiling is specified
641+
tooltip_entries = {"retention time": self.x, "intensity": self.y}
642+
if "Annotation" in self.data.columns:
643+
tooltip_entries["annotation"] = "Annotation"
644+
if "product_mz" in self.data.columns:
645+
tooltip_entries["product m/z"] = "product_mz"
646+
tooltips, custom_hover_data = self._create_tooltips(tooltip_entries, index=False)
647+
linePlot = self.get_line_renderer(data=self.data, config=self._config)
648+
self.canvas = linePlot.generate(tooltips, custom_hover_data)
649+
self._modify_y_range((0, self.data[self.y].max()), (0, 0.1))
650+
651+
if self._interactive:
652+
self.manual_boundary_renderer = self._add_bounding_vertical_drawer()
653+
if self.annotation_data is not None:
654+
self._add_peak_boundaries(self.annotation_data)
603655

604656
def _add_peak_boundaries(self, annotation_data):
605657
"""
606658
Prepare data for adding peak boundaries to the plot.
607-
This is not a complete method should be overridden by subclasses.
608-
609-
Args:
610-
annotation_data (DataFrame): The feature data containing the peak boundaries.
611-
612-
Returns:
613-
None
659+
(Override this method if needed.)
614660
"""
615-
# compute the apex intensity
616661
self.compute_apex_intensity(annotation_data)
617662

618663
def compute_apex_intensity(self, annotation_data):
619664
"""
620-
Compute the apex intensity of the peak group based on the peak boundaries
665+
Compute the apex intensity of the peak group based on the peak boundaries.
621666
"""
622667
for idx, feature in annotation_data.iterrows():
623668
annotation_data.loc[idx, "apexIntensity"] = self.data.loc[

0 commit comments

Comments
 (0)