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
8 changes: 8 additions & 0 deletions plotnado/_kwargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,10 @@ class QuantnadoCoverageKwargs(TypedDict, total=False):
color_group: str | None
quantnado: Any | None
dataset_path: str | None
scaling_factor: float
normalise: str | None
normalize: str | None
library_sizes: Series | dict | None
coverage_data: Any | None
color: str
alpha: float
Expand Down Expand Up @@ -713,6 +717,10 @@ class QuantnadoStrandedCoverageKwargs(TypedDict, total=False):
color_group: str | None
quantnado: Any | None
dataset_path: str | None
scaling_factor: float
normalise: str | None
normalize: str | None
library_sizes: Series | dict | None
coverage_fwd_data: Any | None
coverage_rev_data: Any | None
color: str
Expand Down
12 changes: 11 additions & 1 deletion plotnado/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,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
Expand Down Expand Up @@ -555,7 +559,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)
Expand Down
9 changes: 9 additions & 0 deletions plotnado/figure.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -699,8 +699,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,
Expand Down Expand Up @@ -739,8 +743,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',
Expand Down Expand Up @@ -869,6 +877,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: ...

Expand Down
8 changes: 8 additions & 0 deletions plotnado/figure_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -1079,7 +1079,11 @@ def quantnado_coverage(
data: Any | None = ...,
dataset_path: str | None = ...,
height: float = ...,
library_sizes: Series | dict | None = ...,
normalise: str | None = ...,
normalize: str | None = ...,
quantnado: Any | None = ...,
scaling_factor: float = ...,
title: str | None = ...,
alpha: float = ...,
baseline_alpha: float = ...,
Expand Down Expand Up @@ -1140,7 +1144,11 @@ def quantnado_stranded_coverage(
data: Any | None = ...,
dataset_path: str | None = ...,
height: float = ...,
library_sizes: Series | dict | None = ...,
normalise: str | None = ...,
normalize: str | None = ...,
quantnado: Any | None = ...,
scaling_factor: float = ...,
title: str | None = ...,
alpha: float = ...,
baseline_alpha: float = ...,
Expand Down
10 changes: 6 additions & 4 deletions plotnado/tracks/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,16 +436,17 @@ 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,
y_pos,
self.title,
horizontalalignment=h_align,
verticalalignment="top",
bbox=self._text_bbox(),
bbox=title_bbox,
fontdict={
"size": self.title_size,
"color": self.title_color,
Expand Down Expand Up @@ -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,
Expand Down
73 changes: 70 additions & 3 deletions plotnado/tracks/quantnado.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -204,6 +205,22 @@ class QuantNadoCoverageTrack(_QuantNadoSourceMixin, Track):
>>> QuantNadoCoverageTrack(sample="tumor", dataset_path="study.qn")
"""
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).",
Expand All @@ -226,13 +243,25 @@ 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:
Expand Down Expand Up @@ -293,6 +322,22 @@ class QuantNadoStrandedCoverageTrack(_QuantNadoSourceMixin, Track):
>>> QuantNadoStrandedCoverageTrack(sample="tumor", dataset_path="study.qn")
"""
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).",
Expand Down Expand Up @@ -323,6 +368,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:
Expand All @@ -339,10 +387,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:
Expand Down
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"]
Expand Down
53 changes: 45 additions & 8 deletions tests/test_quantnado_tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ def __init__(
self.coverage_calls: list[dict] = []
self._coverage = coverage
coverage_responses = {"+": coverage_fwd, "-": coverage_rev}
self.coverage = _RecordingStore({k: v for k, v in coverage_responses.items() if v is not None})
stranded_coverage = {k: v for k, v in coverage_responses.items() if v is not None}
self.coverage = _RecordingStore(stranded_coverage or coverage)
self.methylation = _RecordingStore(
{"methylation_pct": methylation} if methylation is not None else {}
)
Expand Down Expand Up @@ -164,11 +165,26 @@ def test_coverage_calls_quantnado_extract_region(self):
gr = GenomicRegion(chromosome="chr1", start=100, end=105)
da = _make_dataarray([1, 2, 3, 4, 5])
qn = _RecordingQuantNado(coverage=da)
track = QuantNadoCoverageTrack(sample="s1", quantnado=qn)
track = QuantNadoCoverageTrack(
sample="s1",
quantnado=qn,
scaling_factor=2.0,
normalize="rpkm",
)
fetched = track.fetch_data(gr)

assert fetched["position"].tolist() == [100, 101, 102, 103, 104]
assert qn.coverage_calls == [{"region": "chr1:100-105", "samples": ["s1"]}]
assert fetched["value"].tolist() == [2.0, 4.0, 6.0, 8.0, 10.0]
assert qn.coverage.calls == [
{
"region": "chr1:100-105",
"samples": ["s1"],
"as_xarray": False,
"normalise": None,
"normalize": "rpkm",
"library_sizes": None,
}
]

def test_stranded_calls_coverage_store_with_strands(self):
gr = GenomicRegion(chromosome="chr1", start=100, end=103)
Expand All @@ -177,14 +193,35 @@ def test_stranded_calls_coverage_store_with_strands(self):
coverage_fwd=_make_dataarray([1, 2, 3]),
coverage_rev=_make_dataarray([3, 2, 1]),
)
track = QuantNadoStrandedCoverageTrack(sample="s1", quantnado=qn)
track = QuantNadoStrandedCoverageTrack(
sample="s1",
quantnado=qn,
scaling_factor=0.5,
normalise="cpm",
)
fetched = track.fetch_data(gr)

assert fetched["forward"].tolist() == [1.0, 2.0, 3.0]
assert fetched["reverse"].tolist() == [3.0, 2.0, 1.0]
assert fetched["forward"].tolist() == [0.5, 1.0, 1.5]
assert fetched["reverse"].tolist() == [1.5, 1.0, 0.5]
assert qn.coverage.calls == [
{"region": "chr1:100-103", "samples": ["s1"], "strand": "+"},
{"region": "chr1:100-103", "samples": ["s1"], "strand": "-"},
{
"region": "chr1:100-103",
"samples": ["s1"],
"as_xarray": False,
"strand": "+",
"normalise": "cpm",
"normalize": None,
"library_sizes": None,
},
{
"region": "chr1:100-103",
"samples": ["s1"],
"as_xarray": False,
"strand": "-",
"normalise": "cpm",
"normalize": None,
"library_sizes": None,
},
]

def test_methylation_calls_store_with_variable(self):
Expand Down
Loading