Skip to content
Merged
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
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,55 @@
## Unreleased

### cnplot package
- Clone names are set in the regular weight: the clone labels beside the integer-CN profile
and the `<label> (<pct>%)` row-block labels on the heatmap. The CN key's `CNA` title is
regular as well.
- `BAF_LIM = (-0.01, 1.01)`: default limits for a BAF-like axis on [0, 1], exported from
`cnplot`. The pad keeps points at exactly 0 or 1 off the spine while the ticks still run
0.0 to 1.0. Used by the gallery/README BAF row (`ylim`) and the RDR-vs-BAF scatter
(`xlim`), which previously used `(-0.05, 1.05)` and `(0, 1)`.
- Fonts are configured in one module, `cnplot.cnplot_style`: the family list
(`FONT_FAMILY`), the embedding types (`FONT_TYPE`), the `FS_*` sizes, and `apply_style()`,
which writes them into rcParams on import. No other module names a family, a size or an
embedding type. `FONT_FAMILY` and `apply_style` are exported from `cnplot`.
- One type scale for every figure. The heatmap page, the integer-CN profile, the CN key,
the proportion legends and the 2D scatter no longer carry their own literal font sizes
(6 to 15 pt); they take `FS_TICK`, `FS_LEGEND`, `FS_AXLABEL`, `FS_AXTITLE` and `FS_TITLE`,
and the same sizes are set as rc defaults on import so text drawn without an explicit
size (a colorbar's ticks) follows. `FS_ANNOT` is new, for labels written on the data.
- Layout measured instead of assumed, so it holds at any font size:
- the CN key labels each state `(a,b)` and wraps its groups onto further rows until the
widest label fits under one swatch; it never shrinks the type or the notation;
- `plot_strip_legend` stacks legends by their rendered height. Its `entry_h` argument is
removed and `gap` defaults to half a legend line;
- the heatmap colorbar grows until neighbouring tick labels clear each other, and the
strip legends start to the right of its rendered tick labels;
- the `plot_heatmap_cnp` title is anchored above the mesh rather than at a fixed
figure height.
- Genome-axis labels are placed so they clear each other at the type scale:
- chromosome names keep their `rotation` (60 degrees by default); names closer together
than a line at that angle are nudged apart along the axis, and on an axes too narrow
to hold them in one row, alternate crowded names go on a second row (`chrname_tiers`
tells a layout which case applies);
- in Mb mode an Mb label within a line of the previous one is left blank (its tick mark
stays), the chromosome names under the Mb labels are offset in points rather than as a
fraction of the axes, and on a stacked page only the last row labels its Mb ticks;
- the gap between a row and the profile under it is sized in inches from the labels it
holds (`genome_label_room`) by `plot_scatter_1d_multisample` and `plot_heatmap_cnp`,
and `plot_heatmap_cnp` no longer opens the same gap between the profile and its key.
- The README gallery is drawn 20 in wide, the width the type scale assumes.
- `FigureSaver.close()` can be called twice on every supported matplotlib.
- Importing cnplot stops matplotlib's PDF backend storing rasterized layers as `/Indexed`
palette images. With `pdf.compression` on, the backend converts any RGB raster of 256 or
fewer colours to a palette and packs the indices at 1, 2 or 4 bits per pixel when the
palette is small, which vector editors misread, so a copy-number raster opened in the
wrong colours. Rasters are now written at 8 bits per component in DeviceRGB, still
Flate-compressed. The hook is on `PdfFile._writeImg`, so it applies to every PDF the
process writes, through `FigureSaver`, `PdfPages` or `Figure.savefig`. Rendered pixels
are unchanged; a PDF holding such rasters is larger.
This works around a known matplotlib bug, wrong colours when such a PDF is opened in
Adobe Illustrator ([matplotlib#27893](https://github.com/matplotlib/matplotlib/issues/27893)),
for which matplotlib has no setting; remove the hook once that is fixed upstream.
- `FigureSaver` no longer holds a whole-figure raster buffer per rasterized artist.
matplotlib renders each rasterized artist into a buffer the size of the entire figure
and passes the PDF backend a view of it, and the backend keeps every image until the
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CN-Plot

[![Version](https://img.shields.io/badge/version-0.1.1-blue.svg)](VERSION)
[![Version](https://img.shields.io/badge/version-0.1.1b2-blue.svg)](VERSION)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
[![Code style: Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)

Expand Down Expand Up @@ -47,7 +47,7 @@ from cnplot import plot_cnv_profile

seg_df = pd.read_table("sample.seg.ucn.tsv")

fig, (ax, ax_leg) = plt.subplots(2, 1, figsize=(12, 3), height_ratios=[3, 1])
fig, (ax, ax_leg) = plt.subplots(2, 1, figsize=(20, 3.5), height_ratios=[3, 1])
plot_cnv_profile(ax, seg_df, genome_axis, sample_id="S1", ax_leg=ax_leg,
title="Integer copy-number profile")
fig.savefig("profile.png", bbox_inches="tight") # returns None; you own the figure
Expand Down Expand Up @@ -140,6 +140,7 @@ fig = plot_heatmap_cnp(
title="Single-cell RDR heatmap",
row_labels=cell_labels, cmap=cmap, norm=norm,
cbar_label="RDR", cbar_ticks=ticks,
figsize=(20, 9),
mb_ticks=True, # Mb ticks every 50Mb per chromosome
strips=[
{"name": "Copy-typing", "matrix": posteriors, "order": clones, "cmap": clone_cmap},
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.1
0.1.1b2
Binary file modified examples/fcn_ab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/heatmap_baf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/heatmap_rdr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions examples/plot_gallery.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

from cnplot import ( # noqa: E402
BAF_LIM,
FS_TITLE,
GenomeAxis,
get_baf_cmap,
get_log2rdr_cmap,
Expand All @@ -50,7 +51,7 @@ def save(fig, name):


def suptitle(fig, text, y=1.0):
fig.suptitle(text, fontsize=14, fontweight="bold", y=y)
fig.suptitle(text, fontsize=FS_TITLE, fontweight="bold", y=y)


genome_axis = GenomeAxis(*reference())
Expand All @@ -64,7 +65,7 @@ def suptitle(fig, text, y=1.0):
)

# 1. integer copy-number profile
fig, (ax, lg) = plt.subplots(2, 1, figsize=(12, 3), height_ratios=[3, 1])
fig, (ax, lg) = plt.subplots(2, 1, figsize=(20, 3.5), height_ratios=[3, 1])
plot_cnv_profile(
ax, sim.seg_ucn, genome_axis, sample_id="S1", ax_leg=lg, show_pi_viol=False
)
Expand Down Expand Up @@ -166,6 +167,7 @@ def suptitle(fig, text, y=1.0):
cbar_label=label,
cbar_ticks=ticks,
strips=strips,
figsize=(20, 9),
mb_ticks=True,
profile_kwargs={"show_pi_viol": False},
)
Expand Down
Binary file modified examples/profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/scatter_1d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/scatter_2d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 17 additions & 5 deletions src/cnplot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,21 @@
from .cnplot_heatmap import plot_column_strips, plot_heatmap, plot_strip_legend
from .cnplot_intcnp import plot_cnv_profile
from .cnplot_io_utils import read_bed, read_chr_sizes
from .cnplot_utils import (
AXIS_LABELS,
BAF_LIM,
CN_PREFIX,
EXP_PREFIX,
from .cnplot_style import (
FONT_FAMILY,
FS_ANNOT,
FS_AXLABEL,
FS_AXTITLE,
FS_LEGEND,
FS_TICK,
FS_TITLE,
apply_style,
)
from .cnplot_utils import (
AXIS_LABELS,
BAF_LIM,
CN_PREFIX,
EXP_PREFIX,
MARKER_SIZE_LARGE,
MARKER_SIZE_SMALL,
MAX_NDOTS,
Expand All @@ -68,10 +73,12 @@
U_PREFIX,
FigureSaver,
adaptive_dot_size,
chrname_tiers,
decorate_genome_axis,
display_label,
draw_chr_boundaries,
draw_segment_boundaries,
genome_label_room,
get_clone_ylabels,
get_transparency,
resolve_colors,
Expand Down Expand Up @@ -104,6 +111,7 @@ def _read_version() -> str:
__version__ = _read_version()

__all__ = [
"apply_style",
"AXIS_LABELS",
"BAF_COLORS",
"BAF_LIM",
Expand All @@ -112,6 +120,8 @@ def _read_version() -> str:
"DATASET_CMAP",
"DEFAULT_EXCLUDED_CHROMS",
"EXP_PREFIX",
"FONT_FAMILY",
"FS_ANNOT",
"FS_AXLABEL",
"FS_AXTITLE",
"FS_LEGEND",
Expand All @@ -138,7 +148,9 @@ def _read_version() -> str:
"__version__",
"adaptive_dot_size",
"annotate_landmarks",
"chrname_tiers",
"decorate_genome_axis",
"genome_label_room",
"display_label",
"draw_chr_boundaries",
"draw_segment_boundaries",
Expand Down
6 changes: 4 additions & 2 deletions src/cnplot/cnplot_1d.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
from matplotlib.collections import LineCollection

from .cnplot_genome_axis import GenomeAxis
from .cnplot_utils import (
EXP_PREFIX,
from .cnplot_style import (
FS_AXLABEL,
FS_AXTITLE,
FS_LEGEND,
)
from .cnplot_utils import (
EXP_PREFIX,
decorate_genome_axis,
display_label,
draw_chr_boundaries,
Expand Down
13 changes: 9 additions & 4 deletions src/cnplot/cnplot_2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
get_clone_states,
select_sample,
)
from .cnplot_style import (
FS_ANNOT,
FS_AXTITLE,
FS_LEGEND,
)
from .cnplot_utils import (
EXP_PREFIX,
NORMAL_CLONE,
Expand Down Expand Up @@ -100,7 +105,7 @@ def annotate_landmarks(
landmarks: list,
markersize: float = 30,
linewidth: float = 1.0,
fontsize: int = 9,
fontsize: int = FS_ANNOT,
color="black",
) -> None:
"""Draw hollow markers with repelled ``(a, b)`` labels.
Expand Down Expand Up @@ -411,7 +416,7 @@ def plot_scatter_2d(
grid.ax_joint.legend(
handles=handles,
loc="best",
fontsize="small",
fontsize=FS_LEGEND,
fancybox=True,
framealpha=0.7,
handlelength=0,
Expand All @@ -421,9 +426,9 @@ def plot_scatter_2d(
grid.set_axis_labels(
xlabel=display_label(xcol if xlabel is None else xlabel),
ylabel=display_label(ycol if ylabel is None else ylabel),
fontsize=12,
fontsize=FS_AXTITLE,
)
if title is not None:
grid.figure.suptitle(title, fontsize=12, fontweight="bold")
grid.figure.suptitle(title, fontsize=FS_AXTITLE, fontweight="bold")
grid.figure.tight_layout()
return grid
44 changes: 37 additions & 7 deletions src/cnplot/cnplot_figures.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,27 @@
plot_cnv_profile,
select_sample,
)
from .cnplot_utils import (
from .cnplot_style import (
FS_AXLABEL,
FS_LEGEND,
FS_TITLE,
)
from .cnplot_utils import (
NORMAL_CLONE,
SAMPLE_COL,
chrname_tiers,
display_label,
genome_label_room,
resolve_marker_size,
)

logger = logging.getLogger(__name__)

# Share of a figure's width its main axes takes under matplotlib's default margins.
_AXES_FRAC = 0.775
# Space left between a profile and the key under it, in inches.
_KEY_GAP_IN = 0.25

__all__ = [
"make_row_spec",
"plot_heatmap_cnp",
Expand Down Expand Up @@ -89,7 +100,7 @@ def _draw_prop_legend(
handles=handles,
loc="center left",
bbox_to_anchor=(1.01, 0.5),
fontsize="small",
fontsize=FS_LEGEND,
fancybox=True,
framealpha=0.7,
handlelength=0,
Expand Down Expand Up @@ -263,14 +274,20 @@ def plot_scatter_1d_multisample(
has_profile = seg_df is not None
rows_h = row_height * n_rows * n_groups
profile_h = 2.0 if has_profile else 0.0
fig_h = rows_h + profile_h + 0.5
# NB: the rows-to-profile gap holds the profile's names and the last row's Mb labels
tiers = chrname_tiers(genome_axis, _AXES_FRAC * row_width)
room = genome_label_room(mb_ticks=mb_ticks, tiers=tiers) if has_profile else 0.0
fig_h = rows_h + profile_h + 0.5 + room
fig = plt.figure(figsize=(row_width, fig_h))

# Nested so the three gaps stay independent: rows within a group, group to
# group, and rows to profile. One flat grid would tie the last two together.
# NB: a top-side xlabel sits in the rows-to-profile gap; 0.2 clears the row above.
gap = max(profile_hspace, 0.2) if xlabel is not None else profile_hspace
if has_profile:
# NB: hspace is a fraction of the mean cell height; solve it for `room` inches
avail = (0.97 - fig.subplotpars.bottom) * fig_h
gap = max(gap, 2.0 * room / max(avail - room, room))
outer = GridSpec(
2,
1,
Expand Down Expand Up @@ -337,6 +354,9 @@ def plot_scatter_1d_multisample(
mb_tick_step=mb_tick_step,
rasterized=rasterized,
)
if mb_ticks and not (last_row and last_group):
# NB: stacked rows share the x axis; only the last one labels it
ax.tick_params(axis="x", labelbottom=False)
if last_row and show_props and seg_df is not None:
_draw_prop_legend(
ax, seg_df, group, group_col, normal, display_min_clone_prop
Expand Down Expand Up @@ -425,9 +445,13 @@ def plot_heatmap_cnp(
raise ValueError("profile must be 'cnv' or 'ascn'")
if profile_hspace is None:
base = 0.45 if heatmap_kwargs.get("mb_ticks") else 0.35
# NB: hspace is a fraction of axes height; the labels are not, so short pages
# need a larger fraction to hold the same band.
profile_hspace = max(base, 4.5 / figsize[1])
# NB: hspace is a fraction of the mean axes height; solve it for inches
room = genome_label_room(
mb_ticks=bool(heatmap_kwargs.get("mb_ticks")),
tiers=chrname_tiers(genome_axis, _AXES_FRAC * figsize[0]),
)
avail = (top - plt.rcParams["figure.subplot.bottom"]) * figsize[1]
profile_hspace = max(base, 3.0 * room / max(avail - 2.0 * room, room))
heatmap_kwargs.setdefault("show_colorbar", True)
heatmap_kwargs.setdefault("show_block_labels", False)
heatmap_kwargs["plot_chrname"] = False
Expand All @@ -439,6 +463,10 @@ def plot_heatmap_cnp(
3, 1, figsize=figsize, gridspec_kw={"height_ratios": list(height_ratios)}
)
fig.subplots_adjust(top=top, hspace=profile_hspace)
# NB: hspace also opens the profile-to-key gap, which holds no labels; close it
prof, key = axes[1].get_position(), axes[2].get_position()
lift = max(prof.y0 - key.y1 - _KEY_GAP_IN / figsize[1], 0.0)
axes[2].set_position([key.x0, key.y0 + lift, key.width, key.height])

plot_heatmap(axes[0], matrix, coords_df, genome_axis, **heatmap_kwargs)
draw_profile(
Expand All @@ -450,5 +478,7 @@ def plot_heatmap_cnp(
**profile_kwargs,
)
if title is not None:
fig.suptitle(title, fontsize=14, fontweight="bold", y=0.98)
# NB: anchored to the mesh, so a taller title grows the page upward
y = axes[0].get_position().y1 + 0.5 * FS_TITLE / 72.0 / figsize[1]
fig.suptitle(title, fontsize=FS_TITLE, fontweight="bold", y=y, va="bottom")
return fig
Loading
Loading