From 2ac14660ef8397dc57a20d615f5c743f8e785276 Mon Sep 17 00:00:00 2001 From: alsmith Date: Wed, 11 Mar 2026 12:59:09 +0000 Subject: [PATCH 1/3] Improve plot aesthetics and add defaults method for GenomicFigure --- plotnado/figure.py | 28 +++++++++++++++++++--------- plotnado/figure.pyi | 1 + plotnado/tracks/base.py | 10 ++++++---- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/plotnado/figure.py b/plotnado/figure.py index b1ef359..8764a45 100644 --- a/plotnado/figure.py +++ b/plotnado/figure.py @@ -89,14 +89,14 @@ class GenomicFigure: """Compose and plot multiple genomic tracks. Example: - fig = ( - GenomicFigure() - .bigwig("signal.bw", title="H3K27ac") - .genes() - .axis() - .scalebar() - ) - fig.plot("chr1:1000000-2000000") + >>> fig = ( + ... GenomicFigure() + ... .bigwig("signal.bw", title="H3K27ac") + ... .genes() + ... .axis() + ... .scalebar() + ... ) + >>> fig.plot("chr1:1000000-2000000") """ def __init__( @@ -1488,6 +1488,10 @@ def autocolor(self, palette: str | list[str] | None = None) -> Self: self._apply_autocolor() return self + def defaults(self, genome: str = "hg38", palette: str | list[str] | None = None) -> Self: + """Apply the standard scaffold: autocolor, scalebar, and genes.""" + return self.autocolor(palette=palette).scalebar().genes(genome) + def _apply_autocolor(self) -> None: if self._autocolor_palette is None: return @@ -1715,7 +1719,13 @@ def plot( hspace = self.theme.subplot_hspace if self.theme is not None else 0.08 gs = GridSpec(len(main_tracks), 1, height_ratios=heights, hspace=hspace) - axes = [fig.add_subplot(gs[index]) for index in range(len(main_tracks))] + axes: list[matplotlib.axes.Axes] = [] + for index in range(len(main_tracks)): + if index == 0: + ax = fig.add_subplot(gs[index]) + else: + ax = fig.add_subplot(gs[index], sharex=axes[0]) + axes.append(ax) def create_overlay_ax(zorder: int, label: str): ax = fig.add_axes(axes[0].get_position(), label=label, zorder=zorder) diff --git a/plotnado/figure.pyi b/plotnado/figure.pyi index 334f085..b5b607a 100644 --- a/plotnado/figure.pyi +++ b/plotnado/figure.pyi @@ -789,6 +789,7 @@ class GenomicFigure: def autoscale(self, enable: bool = ...) -> Self: ... def autocolor(self, palette: str = ...) -> Self: ... + def defaults(self, genome: str = ..., palette: str | list[str] | None = ...) -> Self: ... def highlight(self, region: str | GenomicRegion) -> Self: ... def highlight_style(self, color: str | None = ..., alpha: float | None = ...) -> Self: ... diff --git a/plotnado/tracks/base.py b/plotnado/tracks/base.py index 50dc1f0..564f8ec 100644 --- a/plotnado/tracks/base.py +++ b/plotnado/tracks/base.py @@ -436,8 +436,9 @@ def _plot_title(self, ax: matplotlib.axes.Axes, gr: GenomicRegion) -> None: if self.title_location == "left" else gr.end - (0.01 * gr.length) ) - y_pos = self.y_delta * self.title_height + y_pos = self.y_min + (self.y_delta * self.title_height) h_align = "left" if self.title_location == "left" else "right" + title_bbox = self._text_bbox() or self._default_text_bbox(0.6) ax.text( x_pos, @@ -445,7 +446,7 @@ def _plot_title(self, ax: matplotlib.axes.Axes, gr: GenomicRegion) -> None: self.title, horizontalalignment=h_align, verticalalignment="top", - bbox=self._text_bbox(), + bbox=title_bbox, fontdict={ "size": self.title_size, "color": self.title_color, @@ -478,14 +479,15 @@ def _plot_scale_at( else gr.start + (0.01 * gr.length) ) h_align = "right" if location == "right" else "left" + scale_bbox = self._text_bbox() or self._default_text_bbox(0.6) ax.text( x_pos, - self.y_delta * self.scale_height, + self.y_min + (self.y_delta * self.scale_height), f"[ {y_min} - {y_max} ]", horizontalalignment=h_align, verticalalignment="top", - bbox=self._text_bbox(), + bbox=scale_bbox, fontdict={ "size": self.scale_size, "color": self.scale_color, From 20273719fb7c38c20f21f926c6089b9bdb530d8a Mon Sep 17 00:00:00 2001 From: alsmith Date: Wed, 11 Mar 2026 13:50:57 +0000 Subject: [PATCH 2/3] Add scaling and normalization options to QuantNado coverage tracks and update GenomicFigure parameters --- plotnado/_kwargs.py | 8 ++++ plotnado/figure.py | 8 ++++ plotnado/figure.pyi | 8 ++++ plotnado/tracks/quantnado.py | 74 ++++++++++++++++++++++++++++++++++-- test_quantnado.py | 18 +++++++++ 5 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 test_quantnado.py diff --git a/plotnado/_kwargs.py b/plotnado/_kwargs.py index 795103d..e58eaf0 100644 --- a/plotnado/_kwargs.py +++ b/plotnado/_kwargs.py @@ -658,8 +658,12 @@ class QuantnadoCoverageKwargs(TypedDict, total=False): height: float autoscale_group: str | None color_group: str | None + scaling_factor: float quantnado: Any | None dataset_path: str | None + normalise: str | None + normalize: str | None + library_sizes: pd.Series | dict | None coverage_data: Any | None color: str alpha: float @@ -697,8 +701,12 @@ class QuantnadoStrandedCoverageKwargs(TypedDict, total=False): height: float autoscale_group: str | None color_group: str | None + scaling_factor: float quantnado: Any | None dataset_path: str | None + normalise: str | None + normalize: str | None + library_sizes: pd.Series | dict | None coverage_fwd_data: Any | None coverage_rev_data: Any | None color: str diff --git a/plotnado/figure.py b/plotnado/figure.py index 8764a45..a09d0a7 100644 --- a/plotnado/figure.py +++ b/plotnado/figure.py @@ -1162,6 +1162,10 @@ def quantnado_coverage( data: Any | None = ..., dataset_path: str | None = ..., height: float = ..., + scaling_factor: float = ..., + library_sizes: pd.Series | dict | None = ..., + normalise: str | None = ..., + normalize: str | None = ..., quantnado: Any | None = ..., title: str | None = ..., alpha: float = ..., @@ -1223,6 +1227,10 @@ def quantnado_stranded_coverage( data: Any | None = ..., dataset_path: str | None = ..., height: float = ..., + scaling_factor: float = ..., + library_sizes: pd.Series | dict | None = ..., + normalise: str | None = ..., + normalize: str | None = ..., quantnado: Any | None = ..., title: str | None = ..., alpha: float = ..., diff --git a/plotnado/figure.pyi b/plotnado/figure.pyi index b5b607a..c7eefda 100644 --- a/plotnado/figure.pyi +++ b/plotnado/figure.pyi @@ -619,8 +619,12 @@ class GenomicFigure: height: float = 1.0, autoscale_group: str | None = None, color_group: str | None = None, + scaling_factor: float = 1.0, quantnado: Any | None = None, dataset_path: str | None = None, + normalise: str | None = None, + normalize: str | None = None, + library_sizes: pd.Series | dict | None = None, coverage_data: Any | None = None, color: str = '#2171b5', alpha: float = 0.75, @@ -659,8 +663,12 @@ class GenomicFigure: height: float = 1.0, autoscale_group: str | None = None, color_group: str | None = None, + scaling_factor: float = 1.0, quantnado: Any | None = None, dataset_path: str | None = None, + normalise: str | None = None, + normalize: str | None = None, + library_sizes: pd.Series | dict | None = None, coverage_fwd_data: Any | None = None, coverage_rev_data: Any | None = None, color: str = '#1f78b4', diff --git a/plotnado/tracks/quantnado.py b/plotnado/tracks/quantnado.py index 9ce723e..538768f 100644 --- a/plotnado/tracks/quantnado.py +++ b/plotnado/tracks/quantnado.py @@ -7,6 +7,7 @@ import matplotlib.axes import numpy as np +import pandas as pd from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator from .base import Track, TrackLabeller @@ -176,6 +177,22 @@ def _label_or_clean( class QuantNadoCoverageTrack(_QuantNadoSourceMixin, Track): sample: str = Field(description="Sample name to render from QuantNado data.") + scaling_factor: float = Field( + default=1.0, + description="User-defined multiplicative factor applied to the extracted coverage signal.", + ) + normalise: str | None = Field( + default=None, + description="Optional normalization mode passed to QuantNado extract_region, e.g. 'cpm' or 'rpkm'.", + ) + normalize: str | None = Field( + default=None, + description="American-English alias for `normalise`.", + ) + library_sizes: pd.Series | dict | None = Field( + default=None, + description="Optional library sizes forwarded to QuantNado normalization.", + ) coverage_data: Any | None = Field( default=None, description="Optional precomputed xarray-like coverage data with dims (sample, position).", @@ -198,13 +215,26 @@ def _validate_source(self) -> "QuantNadoCoverageTrack": def fetch_data(self, gr: GenomicRegion) -> dict[str, np.ndarray]: if self.coverage_data is not None: positions, values = _extract_series(self.coverage_data, self.sample, gr) + values = values * float(self.scaling_factor) return {"position": positions, "value": values} qn = self._resolve_quantnado() if qn is None: raise RuntimeError("No QuantNado source available for coverage track") - data = qn.extract_region(region=_region_string(gr), samples=[self.sample]) + coverage_store = getattr(qn, "coverage", None) + if coverage_store is None: + raise RuntimeError("QuantNado source has no coverage store for coverage track") + + data = coverage_store.extract_region( + region=_region_string(gr), + samples=[self.sample], + as_xarray=False, + normalise=self.normalise, + normalize=self.normalize, + library_sizes=self.library_sizes, + ) positions, values = _extract_series(data, self.sample, gr) + values = values * float(self.scaling_factor) return {"position": positions, "value": values} def plot(self, ax: matplotlib.axes.Axes, gr: GenomicRegion) -> None: @@ -259,6 +289,22 @@ def plot(self, ax: matplotlib.axes.Axes, gr: GenomicRegion) -> None: class QuantNadoStrandedCoverageTrack(_QuantNadoSourceMixin, Track): sample: str = Field(description="Sample name to render from QuantNado data.") + scaling_factor: float = Field( + default=1.0, + description="User-defined multiplicative factor applied to forward and reverse coverage signals.", + ) + normalise: str | None = Field( + default=None, + description="Optional normalization mode passed to QuantNado coverage store extraction.", + ) + normalize: str | None = Field( + default=None, + description="American-English alias for `normalise`.", + ) + library_sizes: pd.Series | dict | None = Field( + default=None, + description="Optional library sizes forwarded to QuantNado normalization.", + ) coverage_fwd_data: Any | None = Field( default=None, description="Optional forward-strand xarray-like coverage data with dims (sample, position).", @@ -289,6 +335,9 @@ def fetch_data(self, gr: GenomicRegion) -> dict[str, np.ndarray]: if self.coverage_fwd_data is not None and self.coverage_rev_data is not None: pos_fwd, fwd = _extract_series(self.coverage_fwd_data, self.sample, gr) pos_rev, rev = _extract_series(self.coverage_rev_data, self.sample, gr) + factor = float(self.scaling_factor) + fwd = fwd * factor + rev = rev * factor if pos_fwd.size and pos_rev.size and np.array_equal(pos_fwd, pos_rev): pos = pos_fwd elif pos_fwd.size: @@ -305,10 +354,29 @@ def fetch_data(self, gr: GenomicRegion) -> dict[str, np.ndarray]: raise RuntimeError("QuantNado source has no coverage store for stranded coverage track") region = _region_string(gr) - fwd_data = coverage_store.extract_region(region=region, samples=[self.sample], strand="+") - rev_data = coverage_store.extract_region(region=region, samples=[self.sample], strand="-") + fwd_data = coverage_store.extract_region( + region=region, + samples=[self.sample], + as_xarray=False, + strand="+", + normalise=self.normalise, + normalize=self.normalize, + library_sizes=self.library_sizes, + ) + rev_data = coverage_store.extract_region( + region=region, + samples=[self.sample], + as_xarray=False, + strand="-", + normalise=self.normalise, + normalize=self.normalize, + library_sizes=self.library_sizes, + ) pos_fwd, fwd = _extract_series(fwd_data, self.sample, gr) pos_rev, rev = _extract_series(rev_data, self.sample, gr) + factor = float(self.scaling_factor) + fwd = fwd * factor + rev = rev * factor if pos_fwd.size and pos_rev.size and np.array_equal(pos_fwd, pos_rev): pos = pos_fwd elif pos_fwd.size: diff --git a/test_quantnado.py b/test_quantnado.py new file mode 100644 index 0000000..0407b1b --- /dev/null +++ b/test_quantnado.py @@ -0,0 +1,18 @@ +from plotnado import GenomicFigure, QuantNadoCoverageTrack +import quantnado as qn + +ds_path = "/ceph/project/milne_group/cchahrou/processed_data/seqnado_output/multiomics/dataset/" +ds = qn.open_dataset(ds_path) + +fig = GenomicFigure() +fig.autocolor() +fig.scalebar() +fig.genes("hg38") +fig.quantnado_coverage("ATAC-SEM-1", quantnado=ds, title="ATAC", title_color="black", normalize="rpkm", scaling_factor=0.4) +fig.quantnado_stranded_coverage("RNA-SEM-1", quantnado=ds, title="RNA", title_color="black") +fig.quantnado_methylation("TAPS-SEM", quantnado=ds, title="TAPS", title_color="black") +fig.quantnado_variant('gDNA-SEM', quantnado=ds, title="gDNA Variants", title_color="black") +plot = fig.plot_gene("GNAQ") + +plot.savefig("test_quantnado.png", dpi=300) + From 6ab5aadbf5281faece03b79d0d3d5c93979844d6 Mon Sep 17 00:00:00 2001 From: alsmith151 Date: Wed, 25 Mar 2026 17:45:54 +0000 Subject: [PATCH 3/3] Refactor license and dynamic dependencies format in pyproject.toml --- pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 38500aa..e7d6830 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,12 +8,8 @@ authors = [{ name = "Alastair Smith", email = "alastair.smith@ndcls.ox.ac.uk" }] description = "A simple plotting library for making genomic tracks" readme = "README.md" requires-python = ">=3.12" -license = "GPL-3.0-or-later" +license = { text = "GPL-3.0-or-later" } dynamic = ["version", "dependencies"] -scripts = { "plotnado" = "plotnado.cli.cli:main" } - -[tool.setuptools.dynamic] -dependencies = { file = ["requirements.txt"] } keywords = ["genomics", "bioinformatics", "plotting", "genome-browser", "tracks"] classifiers = [ "Development Status :: 3 - Alpha", @@ -24,6 +20,10 @@ classifiers = [ "Topic :: Scientific/Engineering :: Bio-Informatics", "Topic :: Scientific/Engineering :: Visualization", ] +scripts = { "plotnado" = "plotnado.cli.cli:main" } + +[tool.setuptools.dynamic] +dependencies = { file = ["requirements.txt"] } [project.optional-dependencies] cooler = ["cooler", "capcruncher"]