From 1e0d3108ece20002061058aedff9342f3288903f Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Mon, 12 Aug 2024 14:29:20 +0200 Subject: [PATCH 01/24] Fix *Plot.style() methods --- src/scanpy/plotting/_baseplot_class.py | 7 +++- src/scanpy/plotting/_dotplot.py | 57 ++++++++++++-------------- src/scanpy/plotting/_matrixplot.py | 16 ++++---- src/scanpy/plotting/_stacked_violin.py | 56 +++++++++++-------------- src/scanpy/plotting/_utils.py | 12 +++--- tests/test_score_genes.py | 5 ++- 6 files changed, 75 insertions(+), 78 deletions(-) diff --git a/src/scanpy/plotting/_baseplot_class.py b/src/scanpy/plotting/_baseplot_class.py index b3b6803c8c..1cb6b04971 100644 --- a/src/scanpy/plotting/_baseplot_class.py +++ b/src/scanpy/plotting/_baseplot_class.py @@ -13,6 +13,7 @@ from .. import logging as logg from .._compat import old_positionals +from .._utils import _empty from ._anndata import _get_dendrogram_key, _plot_dendrogram, _prepare_dataframe from ._utils import check_colornorm, make_grid_spec @@ -25,6 +26,7 @@ from matplotlib.axes import Axes from matplotlib.colors import Normalize + from .._utils import Empty from ._utils import ColorLike, _AxesSubplot _VarNames = Union[str, Sequence[str]] @@ -403,7 +405,7 @@ def add_totals( return self @old_positionals("cmap") - def style(self, *, cmap: str | None = DEFAULT_COLORMAP) -> Self: + def style(self, *, cmap: str | None | Empty = _empty) -> Self: """\ Set visual style parameters @@ -417,7 +419,8 @@ def style(self, *, cmap: str | None = DEFAULT_COLORMAP) -> Self: Returns `self` for method chaining. """ - self.cmap = cmap + if cmap is not _empty: + self.cmap = cmap return self @old_positionals("show", "title", "width") diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index 2048cd0e8e..f5bcf4c95d 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -8,7 +8,7 @@ from .. import logging as logg from .._compat import old_positionals from .._settings import settings -from .._utils import _doc_params +from .._utils import _doc_params, _empty from ._baseplot_class import BasePlot, doc_common_groupby_plot_args from ._docs import doc_common_plot_args, doc_show_save_ax, doc_vboundnorm from ._utils import ( @@ -27,11 +27,9 @@ from matplotlib.axes import Axes from matplotlib.colors import Normalize + from .._utils import Empty from ._baseplot_class import _VarNames - from ._utils import ( - ColorLike, - _AxesSubplot, - ) + from ._utils import ColorLike, _AxesSubplot @_doc_params(common_plot_args=doc_common_plot_args) @@ -303,18 +301,18 @@ def __init__( def style( self, *, - cmap: str = DEFAULT_COLORMAP, - color_on: Literal["dot", "square"] | None = DEFAULT_COLOR_ON, - dot_max: float | None = DEFAULT_DOT_MAX, - dot_min: float | None = DEFAULT_DOT_MIN, - smallest_dot: float | None = DEFAULT_SMALLEST_DOT, - largest_dot: float | None = DEFAULT_LARGEST_DOT, - dot_edge_color: ColorLike | None = DEFAULT_DOT_EDGECOLOR, - dot_edge_lw: float | None = DEFAULT_DOT_EDGELW, - size_exponent: float | None = DEFAULT_SIZE_EXPONENT, - grid: float | None = False, - x_padding: float | None = DEFAULT_PLOT_X_PADDING, - y_padding: float | None = DEFAULT_PLOT_Y_PADDING, + cmap: str | None | Empty = _empty, + color_on: Literal["dot", "square"] | None | Empty = _empty, + dot_max: float | None | Empty = _empty, + dot_min: float | None | Empty = _empty, + smallest_dot: float | None | Empty = _empty, + largest_dot: float | None | Empty = _empty, + dot_edge_color: ColorLike | None | Empty = _empty, + dot_edge_lw: float | None | Empty = _empty, + size_exponent: float | None | Empty = _empty, + grid: bool | None | Empty = _empty, + x_padding: float | None | Empty = _empty, + y_padding: float | None | Empty = _empty, ) -> Self: r"""\ Modifies plot visual parameters @@ -389,30 +387,29 @@ def style( ... .show() """ - # change only the values that had changed - if cmap != self.cmap: + if cmap is not _empty: self.cmap = cmap - if dot_max != self.dot_max: + if dot_max is not _empty: self.dot_max = dot_max - if dot_min != self.dot_min: + if dot_min is not _empty: self.dot_min = dot_min - if smallest_dot != self.smallest_dot: + if smallest_dot is not _empty: self.smallest_dot = smallest_dot - if largest_dot != self.largest_dot: + if largest_dot is not _empty: self.largest_dot = largest_dot - if color_on != self.color_on: + if color_on is not _empty: self.color_on = color_on - if size_exponent != self.size_exponent: + if size_exponent is not _empty: self.size_exponent = size_exponent - if dot_edge_color != self.dot_edge_color: + if dot_edge_color is not _empty: self.dot_edge_color = dot_edge_color - if dot_edge_lw != self.dot_edge_lw: + if dot_edge_lw is not _empty: self.dot_edge_lw = dot_edge_lw - if grid != self.grid: + if grid is not _empty: self.grid = grid - if x_padding != self.plot_x_padding: + if x_padding is not _empty: self.plot_x_padding = x_padding - if y_padding != self.plot_y_padding: + if y_padding is not _empty: self.plot_y_padding = y_padding return self diff --git a/src/scanpy/plotting/_matrixplot.py b/src/scanpy/plotting/_matrixplot.py index f5fc18a72f..6ea05ff6bd 100644 --- a/src/scanpy/plotting/_matrixplot.py +++ b/src/scanpy/plotting/_matrixplot.py @@ -9,7 +9,7 @@ from .. import logging as logg from .._compat import old_positionals from .._settings import settings -from .._utils import _doc_params +from .._utils import _doc_params, _empty from ._baseplot_class import BasePlot, doc_common_groupby_plot_args from ._docs import ( doc_common_plot_args, @@ -27,6 +27,7 @@ from matplotlib.axes import Axes from matplotlib.colors import Normalize + from .._utils import Empty from ._baseplot_class import _VarNames from ._utils import ColorLike, _AxesSubplot @@ -198,9 +199,9 @@ def __init__( def style( self, - cmap: str = DEFAULT_COLORMAP, - edge_color: ColorLike | None = DEFAULT_EDGE_COLOR, - edge_lw: float | None = DEFAULT_EDGE_LW, + cmap: str | None | Empty = _empty, + edge_color: ColorLike | None | Empty = _empty, + edge_lw: float | None | Empty = _empty, ) -> Self: """\ Modifies plot visual parameters. @@ -243,12 +244,11 @@ def style( """ - # change only the values that had changed - if cmap != self.cmap: + if cmap is not _empty: self.cmap = cmap - if edge_color != self.edge_color: + if edge_color is not _empty: self.edge_color = edge_color - if edge_lw != self.edge_lw: + if edge_lw is not _empty: self.edge_lw = edge_lw return self diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index 862bf76098..7c7c462b3c 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -12,7 +12,7 @@ from .. import logging as logg from .._compat import old_positionals from .._settings import settings -from .._utils import _doc_params +from .._utils import _doc_params, _empty from ._baseplot_class import BasePlot, doc_common_groupby_plot_args from ._docs import doc_common_plot_args, doc_show_save_ax, doc_vboundnorm from ._utils import ( @@ -30,6 +30,7 @@ from matplotlib.axes import Axes from matplotlib.colors import Normalize + from .._utils import Empty from ._baseplot_class import _VarNames from ._utils import _AxesSubplot @@ -264,19 +265,19 @@ def __init__( def style( self, *, - cmap: str | None = DEFAULT_COLORMAP, - stripplot: bool | None = DEFAULT_STRIPPLOT, - jitter: float | bool | None = DEFAULT_JITTER, - jitter_size: int | None = DEFAULT_JITTER_SIZE, - linewidth: float | None = DEFAULT_LINE_WIDTH, - row_palette: str | None = DEFAULT_ROW_PALETTE, - density_norm: Literal["area", "count", "width"] = DEFAULT_DENSITY_NORM, - yticklabels: bool | None = DEFAULT_PLOT_YTICKLABELS, - ylim: tuple[float, float] | None = DEFAULT_YLIM, - x_padding: float | None = DEFAULT_PLOT_X_PADDING, - y_padding: float | None = DEFAULT_PLOT_Y_PADDING, + cmap: str | None | Empty = _empty, + stripplot: bool | None | Empty = _empty, + jitter: float | bool | None | Empty = _empty, + jitter_size: int | None | Empty = _empty, + linewidth: float | None | Empty = _empty, + row_palette: str | None | Empty = _empty, + density_norm: Literal["area", "count", "width"] | Empty = _empty, + yticklabels: bool | None | Empty = _empty, + ylim: tuple[float, float] | None | Empty = _empty, + x_padding: float | None | Empty = _empty, + y_padding: float | None | Empty = _empty, # deprecated - scale: Literal["area", "count", "width"] | None = None, + scale: Literal["area", "count", "width"] | Empty = _empty, ) -> Self: r"""\ Modifies plot visual parameters @@ -334,19 +335,18 @@ def style( ... .style(row_palette='Blues', linewidth=0).show() """ - # modify only values that had changed - if cmap != self.cmap: + if cmap is not _empty: self.cmap = cmap - if row_palette != self.row_palette: + if row_palette is not _empty: self.row_palette = row_palette self.kwds["color"] = self.row_palette - if stripplot != self.stripplot: + if stripplot is not _empty: self.stripplot = stripplot - if jitter != self.jitter: + if jitter is not _empty: self.jitter = jitter - if jitter_size != self.jitter_size: + if jitter_size is not _empty: self.jitter_size = jitter_size - if yticklabels != self.plot_yticklabels: + if yticklabels is not _empty: self.plot_yticklabels = yticklabels if self.plot_yticklabels: # space needs to be added to avoid overlapping @@ -354,21 +354,15 @@ def style( self.wspace = 0.3 else: self.wspace = StackedViolin.DEFAULT_WSPACE - if ylim != self.ylim: + if ylim is not _empty: self.ylim = ylim - if x_padding != self.plot_x_padding: + if x_padding is not _empty: self.plot_x_padding = x_padding - if y_padding != self.plot_y_padding: + if y_padding is not _empty: self.plot_y_padding = y_padding - if linewidth != self.kwds["linewidth"] and linewidth != self.DEFAULT_LINE_WIDTH: + if linewidth is not _empty: self.kwds["linewidth"] = linewidth - density_norm = _deprecated_scale( - density_norm, scale, default=self.DEFAULT_DENSITY_NORM - ) - if ( - density_norm != self.kwds["density_norm"] - and density_norm != self.DEFAULT_DENSITY_NORM - ): + if (density_norm := _deprecated_scale(density_norm, scale)) is not _empty: self.kwds["density_norm"] = density_norm return self diff --git a/src/scanpy/plotting/_utils.py b/src/scanpy/plotting/_utils.py index c26cc121b1..a093117d0b 100644 --- a/src/scanpy/plotting/_utils.py +++ b/src/scanpy/plotting/_utils.py @@ -19,7 +19,7 @@ from .. import logging as logg from .._compat import old_positionals from .._settings import settings -from .._utils import NeighborsView +from .._utils import NeighborsView, _empty from . import palettes if TYPE_CHECKING: @@ -32,6 +32,8 @@ from numpy.typing import ArrayLike from PIL.Image import Image + from .._utils import Empty + # TODO: more DensityNorm = Literal["area", "count", "width"] @@ -1294,11 +1296,11 @@ def check_colornorm(vmin=None, vmax=None, vcenter=None, norm=None): def _deprecated_scale( - density_norm: DensityNorm, scale: DensityNorm | None, *, default: DensityNorm -) -> DensityNorm: - if scale is None: + density_norm: DensityNorm | Empty, scale: DensityNorm | Empty +) -> DensityNorm | Empty: + if scale is _empty: return density_norm - if density_norm != default: + if density_norm is not _empty: msg = "can’t specify both `scale` and `density_norm`" raise ValueError(msg) msg = "`scale` is deprecated, use `density_norm` instead" diff --git a/tests/test_score_genes.py b/tests/test_score_genes.py index c243b8e022..4ac1b62224 100644 --- a/tests/test_score_genes.py +++ b/tests/test_score_genes.py @@ -19,7 +19,8 @@ from numpy.typing import NDArray -HERE = Path(__file__).parent / "_data" +HERE = Path(__file__).parent +DATA_PATH = HERE / "_data" def _create_random_gene_names(n_genes, name_length) -> NDArray[np.str_]: @@ -72,7 +73,7 @@ def test_score_with_reference(): sc.pp.scale(adata) sc.tl.score_genes(adata, gene_list=adata.var_names[:100], score_name="Test") - with (HERE / "score_genes_reference_paul2015.pkl").open("rb") as file: + with (DATA_PATH / "score_genes_reference_paul2015.pkl").open("rb") as file: reference = pickle.load(file) # np.testing.assert_allclose(reference, adata.obs["Test"].to_numpy()) np.testing.assert_array_equal(reference, adata.obs["Test"].to_numpy()) From 49446c2e20d6e33dcd163ade950ffc59d5845a96 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Mon, 12 Aug 2024 14:33:30 +0200 Subject: [PATCH 02/24] relnote --- docs/release-notes/1.10.3.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/1.10.3.md b/docs/release-notes/1.10.3.md index b7bd113d3c..94d8e66153 100644 --- a/docs/release-notes/1.10.3.md +++ b/docs/release-notes/1.10.3.md @@ -13,5 +13,6 @@ * Fix `legend_loc` argument in {func}`scanpy.pl.embedding` not accepting matplotlib parameters {pr}`3163` {smaller}`P Angerer` * Fix dispersion cutoff in {func}`~scanpy.pp.highly_variable_genes` in presence of `NaN`s {pr}`3176` {smaller}`P Angerer` * Fix axis labeling for swapped axes in {func}`~scanpy.pl.rank_genes_groups_stacked_violin` {pr}`3196` {smaller}`Ilan Gold` +* Fix :meth:`scanpy.pl.DotPlot.style`, :meth:`scanpy.pl.MatrixPlot.style`, and :meth:`scanpy.pl.StackedViolin.style` resetting all non-specified parameters {pr}`3206` {smaller}`P Angerer` #### Performance From 73a852f5f3e39a633547467ba4b11e16f04fe79a Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Mon, 12 Aug 2024 15:03:21 +0200 Subject: [PATCH 03/24] Fix rank_genes_groups_violin --- src/scanpy/plotting/_tools/__init__.py | 6 +- src/scanpy/plotting/_utils.py | 7 +- tests/test_plotting.py | 402 ++++++++++++------------- 3 files changed, 210 insertions(+), 205 deletions(-) diff --git a/src/scanpy/plotting/_tools/__init__.py b/src/scanpy/plotting/_tools/__init__.py index dceb779fd8..d0cd81662d 100644 --- a/src/scanpy/plotting/_tools/__init__.py +++ b/src/scanpy/plotting/_tools/__init__.py @@ -15,7 +15,7 @@ from ... import logging as logg from ..._compat import old_positionals from ..._settings import settings -from ..._utils import _doc_params, sanitize_anndata, subsample +from ..._utils import _doc_params, _empty, sanitize_anndata, subsample from ...get import rank_genes_groups_df from .._anndata import ranking from .._docs import ( @@ -47,6 +47,8 @@ from matplotlib.colors import Colormap, Normalize from matplotlib.figure import Figure + from ..._utils import Empty + # ------------------------------------------------------------------------------ # PCA # ------------------------------------------------------------------------------ @@ -1221,7 +1223,7 @@ def rank_genes_groups_violin( show: bool | None = None, save: bool | None = None, # deprecated - scale: Literal["area", "count", "width"] | None = None, + scale: Literal["area", "count", "width"] | Empty = _empty, ): """\ Plot ranking of genes for all tested comparisons. diff --git a/src/scanpy/plotting/_utils.py b/src/scanpy/plotting/_utils.py index a093117d0b..1281c8ca09 100644 --- a/src/scanpy/plotting/_utils.py +++ b/src/scanpy/plotting/_utils.py @@ -1296,11 +1296,14 @@ def check_colornorm(vmin=None, vmax=None, vcenter=None, norm=None): def _deprecated_scale( - density_norm: DensityNorm | Empty, scale: DensityNorm | Empty + density_norm: DensityNorm | Empty, + scale: DensityNorm | Empty, + *, + default: DensityNorm | Empty = _empty, ) -> DensityNorm | Empty: if scale is _empty: return density_norm - if density_norm is not _empty: + if density_norm != default: msg = "can’t specify both `scale` and `density_norm`" raise ValueError(msg) msg = "`scale` is deprecated, use `density_norm` instead" diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 3b86a9e0d5..3c790a842e 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -585,221 +585,220 @@ def test_correlation(image_comparer): save_and_compare_images("correlation") -@pytest.mark.parametrize( - ("name", "fn"), - [ - ( - "ranked_genes_sharey", - partial( - sc.pl.rank_genes_groups, n_genes=12, n_panels_per_row=3, show=False - ), - ), - ( - "ranked_genes", - partial( - sc.pl.rank_genes_groups, - n_genes=12, - n_panels_per_row=3, - sharey=False, - show=False, - ), - ), - ( - "ranked_genes_heatmap", - partial( - sc.pl.rank_genes_groups_heatmap, n_genes=4, cmap="YlGnBu", show=False - ), - ), - ( - "ranked_genes_heatmap_swap_axes", - partial( - sc.pl.rank_genes_groups_heatmap, - n_genes=20, - swap_axes=True, - use_raw=False, - show_gene_labels=False, - show=False, - vmin=-3, - vmax=3, - cmap="bwr", - ), +_RANK_GENES_GROUPS_PARAMS = [ + ( + "sharey", + partial(sc.pl.rank_genes_groups, n_genes=12, n_panels_per_row=3, show=False), + ), + ( + "basic", + partial( + sc.pl.rank_genes_groups, + n_genes=12, + n_panels_per_row=3, + sharey=False, + show=False, ), - ( - "ranked_genes_heatmap_swap_axes_vcenter", - partial( - sc.pl.rank_genes_groups_heatmap, - n_genes=20, - swap_axes=True, - use_raw=False, - show_gene_labels=False, - show=False, - vmin=-3, - vcenter=1, - vmax=3, - cmap="RdBu_r", - ), + ), + ( + "heatmap", + partial(sc.pl.rank_genes_groups_heatmap, n_genes=4, cmap="YlGnBu", show=False), + ), + ( + "heatmap_swap_axes", + partial( + sc.pl.rank_genes_groups_heatmap, + n_genes=20, + swap_axes=True, + use_raw=False, + show_gene_labels=False, + show=False, + vmin=-3, + vmax=3, + cmap="bwr", ), - ( - "ranked_genes_stacked_violin", - partial( - sc.pl.rank_genes_groups_stacked_violin, - n_genes=3, - show=False, - groups=["3", "0", "5"], - ), + ), + ( + "heatmap_swap_axes_vcenter", + partial( + sc.pl.rank_genes_groups_heatmap, + n_genes=20, + swap_axes=True, + use_raw=False, + show_gene_labels=False, + show=False, + vmin=-3, + vcenter=1, + vmax=3, + cmap="RdBu_r", ), - ( - "ranked_genes_dotplot", - partial(sc.pl.rank_genes_groups_dotplot, n_genes=4, show=False), + ), + ( + "stacked_violin", + partial( + sc.pl.rank_genes_groups_stacked_violin, + n_genes=3, + show=False, + groups=["3", "0", "5"], ), - ( - "ranked_genes_dotplot_gene_names", - partial( - sc.pl.rank_genes_groups_dotplot, - var_names={ - "T-cell": ["CD3D", "CD3E", "IL32"], - "B-cell": ["CD79A", "CD79B", "MS4A1"], - "myeloid": ["CST3", "LYZ"], - }, - values_to_plot="logfoldchanges", - cmap="bwr", - vmin=-3, - vmax=3, - show=False, - ), + ), + ( + "dotplot", + partial(sc.pl.rank_genes_groups_dotplot, n_genes=4, show=False), + ), + ( + "dotplot_gene_names", + partial( + sc.pl.rank_genes_groups_dotplot, + var_names={ + "T-cell": ["CD3D", "CD3E", "IL32"], + "B-cell": ["CD79A", "CD79B", "MS4A1"], + "myeloid": ["CST3", "LYZ"], + }, + values_to_plot="logfoldchanges", + cmap="bwr", + vmin=-3, + vmax=3, + show=False, ), - ( - "ranked_genes_dotplot_logfoldchange", - partial( - sc.pl.rank_genes_groups_dotplot, - n_genes=4, - values_to_plot="logfoldchanges", - vmin=-5, - vmax=5, - min_logfoldchange=3, - cmap="RdBu_r", - swap_axes=True, - title="log fold changes swap_axes", - show=False, - ), + ), + ( + "dotplot_logfoldchange", + partial( + sc.pl.rank_genes_groups_dotplot, + n_genes=4, + values_to_plot="logfoldchanges", + vmin=-5, + vmax=5, + min_logfoldchange=3, + cmap="RdBu_r", + swap_axes=True, + title="log fold changes swap_axes", + show=False, ), - ( - "ranked_genes_dotplot_logfoldchange_vcenter", - partial( - sc.pl.rank_genes_groups_dotplot, - n_genes=4, - values_to_plot="logfoldchanges", - vmin=-5, - vcenter=1, - vmax=5, - min_logfoldchange=3, - cmap="RdBu_r", - swap_axes=True, - title="log fold changes swap_axes", - show=False, - ), + ), + ( + "dotplot_logfoldchange_vcenter", + partial( + sc.pl.rank_genes_groups_dotplot, + n_genes=4, + values_to_plot="logfoldchanges", + vmin=-5, + vcenter=1, + vmax=5, + min_logfoldchange=3, + cmap="RdBu_r", + swap_axes=True, + title="log fold changes swap_axes", + show=False, ), - ( - "ranked_genes_matrixplot", - partial( - sc.pl.rank_genes_groups_matrixplot, - n_genes=5, - show=False, - title="matrixplot", - gene_symbols="symbol", - use_raw=False, - ), + ), + ( + "matrixplot", + partial( + sc.pl.rank_genes_groups_matrixplot, + n_genes=5, + show=False, + title="matrixplot", + gene_symbols="symbol", + use_raw=False, ), - ( - "ranked_genes_matrixplot_gene_names_symbol", - partial( - sc.pl.rank_genes_groups_matrixplot, - var_names={ - "T-cell": ["CD3D__", "CD3E__", "IL32__"], - "B-cell": ["CD79A__", "CD79B__", "MS4A1__"], - "myeloid": ["CST3__", "LYZ__"], - }, - values_to_plot="logfoldchanges", - cmap="bwr", - vmin=-3, - vmax=3, - gene_symbols="symbol", - use_raw=False, - show=False, - ), + ), + ( + "matrixplot_gene_names_symbol", + partial( + sc.pl.rank_genes_groups_matrixplot, + var_names={ + "T-cell": ["CD3D__", "CD3E__", "IL32__"], + "B-cell": ["CD79A__", "CD79B__", "MS4A1__"], + "myeloid": ["CST3__", "LYZ__"], + }, + values_to_plot="logfoldchanges", + cmap="bwr", + vmin=-3, + vmax=3, + gene_symbols="symbol", + use_raw=False, + show=False, ), - ( - "ranked_genes_matrixplot_n_genes_negative", - partial( - sc.pl.rank_genes_groups_matrixplot, - n_genes=-5, - show=False, - title="matrixplot n_genes=-5", - ), + ), + ( + "matrixplot_n_genes_negative", + partial( + sc.pl.rank_genes_groups_matrixplot, + n_genes=-5, + show=False, + title="matrixplot n_genes=-5", ), - ( - "ranked_genes_matrixplot_swap_axes", - partial( - sc.pl.rank_genes_groups_matrixplot, - n_genes=5, - show=False, - swap_axes=True, - values_to_plot="logfoldchanges", - vmin=-6, - vmax=6, - cmap="bwr", - title="log fold changes swap_axes", - ), + ), + ( + "matrixplot_swap_axes", + partial( + sc.pl.rank_genes_groups_matrixplot, + n_genes=5, + show=False, + swap_axes=True, + values_to_plot="logfoldchanges", + vmin=-6, + vmax=6, + cmap="bwr", + title="log fold changes swap_axes", ), - ( - "ranked_genes_matrixplot_swap_axes_vcenter", - partial( - sc.pl.rank_genes_groups_matrixplot, - n_genes=5, - show=False, - swap_axes=True, - values_to_plot="logfoldchanges", - vmin=-6, - vcenter=1, - vmax=6, - cmap="bwr", - title="log fold changes swap_axes", - ), + ), + ( + "matrixplot_swap_axes_vcenter", + partial( + sc.pl.rank_genes_groups_matrixplot, + n_genes=5, + show=False, + swap_axes=True, + values_to_plot="logfoldchanges", + vmin=-6, + vcenter=1, + vmax=6, + cmap="bwr", + title="log fold changes swap_axes", ), - ( - "ranked_genes_tracksplot", - partial( - sc.pl.rank_genes_groups_tracksplot, - n_genes=3, - show=False, - groups=["3", "2", "1"], - ), + ), + ( + "tracksplot", + partial( + sc.pl.rank_genes_groups_tracksplot, + n_genes=3, + show=False, + groups=["3", "2", "1"], ), - ( - "ranked_genes_violin", - partial( - sc.pl.rank_genes_groups_violin, - groups="0", - n_genes=5, - use_raw=True, - jitter=False, - strip=False, - show=False, - ), + ), + ( + "violin", + partial( + sc.pl.rank_genes_groups_violin, + groups="0", + n_genes=5, + use_raw=True, + jitter=False, + strip=False, + show=False, ), - ( - "ranked_genes_violin_not_raw", - partial( - sc.pl.rank_genes_groups_violin, - groups="0", - n_genes=5, - use_raw=False, - jitter=False, - strip=False, - show=False, - ), + ), + ( + "violin_not_raw", + partial( + sc.pl.rank_genes_groups_violin, + groups="0", + n_genes=5, + use_raw=False, + jitter=False, + strip=False, + show=False, ), - ], + ), +] + + +@pytest.mark.parametrize( + ("name", "fn"), + [pytest.param(name, fn, id=name) for name, fn in _RANK_GENES_GROUPS_PARAMS], ) def test_rank_genes_groups(image_comparer, name, fn): save_and_compare_images = partial(image_comparer, ROOT, tol=15) @@ -812,7 +811,8 @@ def test_rank_genes_groups(image_comparer, name, fn): with plt.rc_context({"axes.grid": True, "figure.figsize": (4, 4)}): fn(pbmc) - save_and_compare_images(name) + key = "ranked_genes" if name == "basic" else f"ranked_genes_{name}" + save_and_compare_images(key) plt.close() From edddf35c352e2bc234ac0bc22113adefbe67caca Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Tue, 13 Aug 2024 14:43:04 +0200 Subject: [PATCH 04/24] WIP less hacky plotting classes --- src/scanpy/plotting/_baseplot_class.py | 226 +++++++++++++------------ src/scanpy/plotting/_dotplot.py | 24 +-- src/scanpy/plotting/_utils.py | 31 +++- tests/test_plotting_utils.py | 31 +++- 4 files changed, 189 insertions(+), 123 deletions(-) diff --git a/src/scanpy/plotting/_baseplot_class.py b/src/scanpy/plotting/_baseplot_class.py index 1cb6b04971..a596f7d5b3 100644 --- a/src/scanpy/plotting/_baseplot_class.py +++ b/src/scanpy/plotting/_baseplot_class.py @@ -3,7 +3,8 @@ from __future__ import annotations import collections.abc as cabc -from typing import TYPE_CHECKING, NamedTuple +from dataclasses import KW_ONLY, dataclass # noqa: TCH003 +from typing import TYPE_CHECKING, ClassVar, NamedTuple from warnings import warn import numpy as np @@ -15,7 +16,7 @@ from .._compat import old_positionals from .._utils import _empty from ._anndata import _get_dendrogram_key, _plot_dendrogram, _prepare_dataframe -from ._utils import check_colornorm, make_grid_spec +from ._utils import DefaultProxy, check_colornorm, make_grid_spec if TYPE_CHECKING: from collections.abc import Iterable, Mapping, Sequence @@ -25,9 +26,10 @@ from anndata import AnnData from matplotlib.axes import Axes from matplotlib.colors import Normalize + from matplotlib.figure import Figure from .._utils import Empty - from ._utils import ColorLike, _AxesSubplot + from ._utils import ColorLike _VarNames = Union[str, Sequence[str]] @@ -59,6 +61,7 @@ class VBoundNorm(NamedTuple): """ +@dataclass class BasePlot: """\ Generic class for the visualization of AnnData categories and @@ -74,19 +77,80 @@ class BasePlot: BasePlot(adata, ...).legend(title='legend').style(cmap='binary').show() """ - DEFAULT_SAVE_PREFIX = "baseplot_" - MIN_FIGURE_HEIGHT = 2.5 - DEFAULT_CATEGORY_HEIGHT = 0.35 - DEFAULT_CATEGORY_WIDTH = 0.37 + adata: AnnData + var_names: _VarNames | Mapping[str, _VarNames] + groupby: str | Sequence[str] + _: KW_ONLY + use_raw: bool | None = None + log: bool = True + num_categories: int = 5 + categories_order: Sequence[str] | None = None + title: str | None = None + figsize: tuple[float, float] | None = None + gene_symbols: _VarNames | None = None + var_group_positions: Sequence[tuple[int, int]] | None = None + var_group_labels: Sequence[str] | None = None + var_group_rotation: float | None = None + layer: str | None = None + ax: Axes | None = None + vmin: float | None = None + vmax: float | None = None + vcenter: float | None = None + norm: Normalize | None = None + + DEFAULT_SAVE_PREFIX: ClassVar[str] = "baseplot_" + # maximum number of categories allowed to be plotted + MAX_NUM_CATEGORIES: ClassVar[int] = 500 + + # minimum height required for legends to plot properly + min_figure_height: float = 2.5 + MIN_FIGURE_HEIGHT: ClassVar[DefaultProxy[float]] = DefaultProxy("min_figure_height") + + category_height = 0.35 + DEFAULT_CATEGORY_HEIGHT: ClassVar[DefaultProxy[float]] = DefaultProxy( + "category_height" + ) + category_width = 0.37 + DEFAULT_CATEGORY_WIDTH: ClassVar[DefaultProxy[float]] = DefaultProxy( + "category_width" + ) # gridspec parameter. Sets the space between mainplot, dendrogram and legend - DEFAULT_WSPACE = 0 - - DEFAULT_COLORMAP = "winter" - DEFAULT_LEGENDS_WIDTH = 1.5 - DEFAULT_COLOR_LEGEND_TITLE = "Expression\nlevel in group" - - MAX_NUM_CATEGORIES = 500 # maximum number of categories allowed to be plotted + wspace: float = 0 + DEFAULT_WSPACE: ClassVar[DefaultProxy[float]] = DefaultProxy("wspace") + + cmap: str | None = "winter" + DEFAULT_COLORMAP: ClassVar[DefaultProxy[str]] = DefaultProxy("cmap") + legends_width: float = 1.5 + DEFAULT_LEGENDS_WIDTH: ClassVar[DefaultProxy[float]] = DefaultProxy("legends_width") + color_legend_title: str = "Expression\nlevel in group" + DEFAULT_COLOR_LEGEND_TITLE: ClassVar[DefaultProxy[str]] = DefaultProxy( + "color_legend_title" + ) + are_axes_swapped: bool = False + var_names_idx_order: Sequence[int] | None = None + group_extra_size: float = 0.0 + plot_group_extra: dict[str, object] | None = None + # after .render() is called the fig value is assigned and ax_dict + # contains a dictionary of the axes used in the plot + fig: Figure | None = None + ax_dict: dict[str, Axes] | None = None + + @property + def fig_title(self) -> str | None: + return self.title + + @property + def width(self) -> float | None: + return self.figsize[0] if self.figsize is not None else None + + @property + def height(self) -> float | None: + return self.figsize[1] if self.figsize is not None else None + + @property + def vboundnorm(self) -> VBoundNorm: + return VBoundNorm(self.vmin, self.vmax, self.vcenter, self.norm) @old_positionals( "use_raw", @@ -106,53 +170,25 @@ class BasePlot: "vcenter", "norm", ) - def __init__( - self, - adata: AnnData, - var_names: _VarNames | Mapping[str, _VarNames], - groupby: str | Sequence[str], - *, - use_raw: bool | None = None, - log: bool = False, - num_categories: int = 7, - categories_order: Sequence[str] | None = None, - title: str | None = None, - figsize: tuple[float, float] | None = None, - gene_symbols: str | None = None, - var_group_positions: Sequence[tuple[int, int]] | None = None, - var_group_labels: Sequence[str] | None = None, - var_group_rotation: float | None = None, - layer: str | None = None, - ax: _AxesSubplot | None = None, - vmin: float | None = None, - vmax: float | None = None, - vcenter: float | None = None, - norm: Normalize | None = None, - **kwds, - ): - self.var_names = var_names - self.var_group_labels = var_group_labels - self.var_group_positions = var_group_positions - self.var_group_rotation = var_group_rotation - self.width, self.height = figsize if figsize is not None else (None, None) - + def __post_init__(self, **kwds): self.has_var_groups = ( True - if var_group_positions is not None and len(var_group_positions) > 0 + if self.var_group_positions is not None + and len(self.var_group_positions) > 0 else False ) self._update_var_groups() self.categories, self.obs_tidy = _prepare_dataframe( - adata, + self.adata, self.var_names, - groupby, - use_raw=use_raw, - log=log, - num_categories=num_categories, - layer=layer, - gene_symbols=gene_symbols, + self.groupby, + use_raw=self.use_raw, + log=self.log, + num_categories=self.num_categories, + layer=self.layer, + gene_symbols=self.gene_symbols, ) if len(self.categories) > self.MAX_NUM_CATEGORIES: warn( @@ -160,55 +196,25 @@ def __init__( "Plot would be very large." ) - if categories_order is not None: - if set(self.obs_tidy.index.categories) != set(categories_order): + if self.categories_order is not None: + if set(self.obs_tidy.index.categories) != set(self.categories_order): logg.error( "Please check that the categories given by " "the `order` parameter match the categories that " "want to be reordered.\n\n" "Mismatch: " - f"{set(self.obs_tidy.index.categories).difference(categories_order)}\n\n" - f"Given order categories: {categories_order}\n\n" - f"{groupby} categories: {list(self.obs_tidy.index.categories)}\n" + f"{set(self.obs_tidy.index.categories).difference(self.categories_order)}\n\n" + f"Given order categories: {self.categories_order}\n\n" + f"{self.groupby} categories: {list(self.obs_tidy.index.categories)}\n" ) return - self.adata = adata - self.groupby = [groupby] if isinstance(groupby, str) else groupby - self.log = log + if isinstance(self.groupby, str): + self.groupby = [self.groupby] self.kwds = kwds - self.vboundnorm = VBoundNorm(vmin=vmin, vmax=vmax, vcenter=vcenter, norm=norm) - - # set default values for legend - self.color_legend_title = self.DEFAULT_COLOR_LEGEND_TITLE - self.legends_width = self.DEFAULT_LEGENDS_WIDTH - - # set style defaults - self.cmap = self.DEFAULT_COLORMAP - - # style default parameters - self.are_axes_swapped = False - self.categories_order = categories_order - self.var_names_idx_order = None - - self.wspace = self.DEFAULT_WSPACE - - # minimum height required for legends to plot properly - self.min_figure_height = self.MIN_FIGURE_HEIGHT - - self.fig_title = title - - self.group_extra_size = 0 - self.plot_group_extra = None - # after .render() is called the fig value is assigned and ax_dict - # contains a dictionary of the axes used in the plot - self.fig = None - self.ax_dict = None - self.ax = ax - @legacy_api("swap_axes") - def swap_axes(self, *, swap_axes: bool | None = True) -> Self: + def swap_axes(self, *, swap_axes: bool = True) -> Self: """ Plots a transposed image. @@ -221,17 +227,16 @@ def swap_axes(self, *, swap_axes: bool | None = True) -> Self: swap_axes Boolean to turn on (True) or off (False) 'swap_axes'. Default True - Returns ------- Returns `self` for method chaining. """ - self.DEFAULT_CATEGORY_HEIGHT, self.DEFAULT_CATEGORY_WIDTH = ( - self.DEFAULT_CATEGORY_WIDTH, - self.DEFAULT_CATEGORY_HEIGHT, + # TODO: this doesn’t make much sense + self.category_height, self.category_width = ( + self.category_width, + self.category_height, ) - self.are_axes_swapped = swap_axes return self @@ -241,7 +246,7 @@ def add_dendrogram( *, show: bool | None = True, dendrogram_key: str | None = None, - size: float | None = 0.8, + size: float = 0.8, ) -> Self: r"""\ Show dendrogram based on the hierarchical clustering between the `groupby` @@ -328,7 +333,7 @@ def add_totals( *, show: bool | None = True, sort: Literal["ascending", "descending"] | None = None, - size: float | None = 0.8, + size: float = 0.8, color: ColorLike | Sequence[ColorLike] | None = None, ) -> Self: r"""\ @@ -428,8 +433,8 @@ def legend( self, *, show: bool | None = True, - title: str | None = DEFAULT_COLOR_LEGEND_TITLE, - width: float | None = DEFAULT_LEGENDS_WIDTH, + title: str | Empty = _empty, + width: float | Empty = _empty, ) -> Self: r"""\ Configure legend parameters @@ -467,8 +472,10 @@ def legend( # turn of legends by setting width to 0 self.legends_width = 0 else: - self.color_legend_title = title - self.legends_width = width + if title is not _empty: + self.color_legend_title = title + if width is not _empty: + self.legends_width = width return self @@ -669,13 +676,10 @@ def make_figure(self): >>> sc.pl.DotPlot(adata, markers, groupby='bulk_labels', ax=ax1).make_figure() """ - category_height = self.DEFAULT_CATEGORY_HEIGHT - category_width = self.DEFAULT_CATEGORY_WIDTH - if self.height is None: - mainplot_height = len(self.categories) * category_height + mainplot_height = len(self.categories) * self.category_height mainplot_width = ( - len(self.var_names) * category_width + self.group_extra_size + len(self.var_names) * self.category_width + self.group_extra_size ) if self.are_axes_swapped: mainplot_height, mainplot_width = mainplot_width, mainplot_height @@ -684,8 +688,10 @@ def make_figure(self): # if the number of categories is small use # a larger height, otherwise the legends do not fit - self.height = max([self.min_figure_height, height]) - self.width = mainplot_width + self.legends_width + self.figsize = ( + mainplot_width + self.legends_width, + max([self.min_figure_height, height]), + ) else: self.min_figure_height = self.height mainplot_height = self.height @@ -709,9 +715,9 @@ def make_figure(self): if self.has_var_groups: # add some space in case 'brackets' want to be plotted on top of the image if self.are_axes_swapped: - var_groups_height = category_height + var_groups_height = self.category_height else: - var_groups_height = category_height / 2 + var_groups_height = self.category_height / 2 else: var_groups_height = 0 diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index f5bcf4c95d..205352e4fd 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from dataclasses import dataclass +from typing import TYPE_CHECKING, ClassVar import numpy as np from matplotlib import pyplot as plt @@ -33,6 +34,7 @@ @_doc_params(common_plot_args=doc_common_plot_args) +@dataclass class DotPlot(BasePlot): """\ Allows the visualization of two values that are encoded as @@ -93,10 +95,10 @@ class DotPlot(BasePlot): """ - DEFAULT_SAVE_PREFIX = "dotplot_" + DEFAULT_SAVE_PREFIX: ClassVar[str] = "dotplot_" # default style parameters - DEFAULT_COLORMAP = "winter" - DEFAULT_COLOR_ON = "dot" + cmap: str = "winter" + DEFAULT_COLOR_ON: ClassVar[Literal["dot", "square"]] = "dot" DEFAULT_DOT_MAX = None DEFAULT_DOT_MIN = None DEFAULT_SMALLEST_DOT = 0.0 @@ -107,8 +109,8 @@ class DotPlot(BasePlot): # default legend parameters DEFAULT_SIZE_LEGEND_TITLE = "Fraction of cells\nin group (%)" - DEFAULT_COLOR_LEGEND_TITLE = "Mean expression\nin group" - DEFAULT_LEGENDS_WIDTH = 1.5 # inches + color_legend_title = "Mean expression\nin group" + legends_width = 1.5 # inches DEFAULT_PLOT_X_PADDING = 0.8 # a unit is the distance between two x-axis ticks DEFAULT_PLOT_Y_PADDING = 1.0 # a unit is the distance between two y-axis ticks @@ -429,8 +431,8 @@ def legend( show_size_legend: bool | None = True, show_colorbar: bool | None = True, size_title: str | None = DEFAULT_SIZE_LEGEND_TITLE, - colorbar_title: str | None = DEFAULT_COLOR_LEGEND_TITLE, - width: float | None = DEFAULT_LEGENDS_WIDTH, + colorbar_title: str | Empty = _empty, + width: float | Empty = _empty, ) -> Self: """\ Configures dot size and the colorbar legends @@ -473,9 +475,11 @@ def legend( # turn of legends by setting width to 0 self.legends_width = 0 else: - self.color_legend_title = colorbar_title + if colorbar_title is not _empty: + self.color_legend_title = colorbar_title self.size_title = size_title - self.legends_width = width + if width is not _empty: + self.legends_width = width self.show_size_legend = show_size_legend self.show_colorbar = show_colorbar diff --git a/src/scanpy/plotting/_utils.py b/src/scanpy/plotting/_utils.py index 1281c8ca09..1898096d9f 100644 --- a/src/scanpy/plotting/_utils.py +++ b/src/scanpy/plotting/_utils.py @@ -3,7 +3,8 @@ import collections.abc as cabc import warnings from collections.abc import Sequence -from typing import TYPE_CHECKING, Callable, Literal, Union +from dataclasses import MISSING, Field, dataclass +from typing import TYPE_CHECKING, Callable, Generic, Literal, TypeVar, Union import matplotlib as mpl import numpy as np @@ -36,6 +37,10 @@ # TODO: more DensityNorm = Literal["area", "count", "width"] + O = TypeVar("O") + + +T = TypeVar("T") # These are needed by _wraps_plot_scatter _IGraphLayout = Literal["fa", "fr", "rt", "rt_circular", "drl", "eq_tree"] @@ -68,6 +73,30 @@ class _AxesSubplot(Axes, axes.SubplotBase): """Intersection between Axes and SubplotBase: Has methods of both""" +@dataclass +class DefaultProxy(Generic[T]): + attr: str + + def __get__(self, obj: O | None, objtype: type[O] | None = None) -> T: + if objtype is None: + if obj is None: + msg = f"Weird access to {self}" + raise AttributeError(msg) + objtype = type(obj) + + v = getattr(objtype, self.attr) + if isinstance(v, Field): + if v.default is not MISSING: + v = v.default + elif v.default_factory is not MISSING: + v = v.default_factory() + else: + raise AttributeError( + f"Field {self.attr} of class {objtype} has no default value" + ) + return v + + # ------------------------------------------------------------------------------- # Simple plotting functions # ------------------------------------------------------------------------------- diff --git a/tests/test_plotting_utils.py b/tests/test_plotting_utils.py index 6b53cd5b50..2f6d399603 100644 --- a/tests/test_plotting_utils.py +++ b/tests/test_plotting_utils.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import cast +from dataclasses import dataclass, field +from typing import ClassVar, cast import numpy as np import pytest @@ -8,7 +9,7 @@ from matplotlib import colormaps from matplotlib.colors import ListedColormap -from scanpy.plotting._utils import _validate_palette +from scanpy.plotting._utils import DefaultProxy, _validate_palette viridis = cast(ListedColormap, colormaps["viridis"]) @@ -27,3 +28,29 @@ def test_validate_palette_no_mod(palette, typ): adata = AnnData(uns=dict(test_colors=palette)) _validate_palette(adata, "test") assert palette is adata.uns["test_colors"], "Palette should not be modified" + + +@pytest.mark.parametrize( + "param", + [ + pytest.param(1, id="direct"), + pytest.param(field(default=1), id="default"), + pytest.param( + field(default_factory=lambda: 1), + marks=[ + pytest.mark.xfail( + "Tries to call factory while class not fully constructed" + ) + ], + id="default_factory", + ), + ], +) +def test_default_proxy(param): + @dataclass + class Test: + field_: int = param + DEFAULT: ClassVar[DefaultProxy[int]] = DefaultProxy("field_") + + assert Test(2).field_ == 2 + assert Test(2).DEFAULT == 1 From a5b13a50086d27f6552145d8143f156330945ee5 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Tue, 13 Aug 2024 15:35:42 +0200 Subject: [PATCH 05/24] dotplot cleaned up --- src/scanpy/plotting/_baseplot_class.py | 88 +++++----- src/scanpy/plotting/_dotplot.py | 215 ++++++++++--------------- 2 files changed, 118 insertions(+), 185 deletions(-) diff --git a/src/scanpy/plotting/_baseplot_class.py b/src/scanpy/plotting/_baseplot_class.py index a596f7d5b3..a6b562f640 100644 --- a/src/scanpy/plotting/_baseplot_class.py +++ b/src/scanpy/plotting/_baseplot_class.py @@ -3,11 +3,16 @@ from __future__ import annotations import collections.abc as cabc -from dataclasses import KW_ONLY, dataclass # noqa: TCH003 +from dataclasses import ( + KW_ONLY, # noqa: TCH003 # https://github.com/astral-sh/ruff/issues/12859 + dataclass, + field, +) from typing import TYPE_CHECKING, ClassVar, NamedTuple from warnings import warn import numpy as np +import pandas as pd from legacy_api_wrap import legacy_api from matplotlib import gridspec from matplotlib import pyplot as plt @@ -22,7 +27,6 @@ from collections.abc import Iterable, Mapping, Sequence from typing import Literal, Self, Union - import pandas as pd from anndata import AnnData from matplotlib.axes import Axes from matplotlib.colors import Normalize @@ -77,6 +81,10 @@ class BasePlot: BasePlot(adata, ...).legend(title='legend').style(cmap='binary').show() """ + DEFAULT_SAVE_PREFIX: ClassVar[str] = "baseplot_" + # maximum number of categories allowed to be plotted + MAX_NUM_CATEGORIES: ClassVar[int] = 500 + adata: AnnData var_names: _VarNames | Mapping[str, _VarNames] groupby: str | Sequence[str] @@ -87,7 +95,7 @@ class BasePlot: categories_order: Sequence[str] | None = None title: str | None = None figsize: tuple[float, float] | None = None - gene_symbols: _VarNames | None = None + gene_symbols: str | None = None var_group_positions: Sequence[tuple[int, int]] | None = None var_group_labels: Sequence[str] | None = None var_group_rotation: float | None = None @@ -98,35 +106,15 @@ class BasePlot: vcenter: float | None = None norm: Normalize | None = None - DEFAULT_SAVE_PREFIX: ClassVar[str] = "baseplot_" - # maximum number of categories allowed to be plotted - MAX_NUM_CATEGORIES: ClassVar[int] = 500 - # minimum height required for legends to plot properly min_figure_height: float = 2.5 - MIN_FIGURE_HEIGHT: ClassVar[DefaultProxy[float]] = DefaultProxy("min_figure_height") - category_height = 0.35 - DEFAULT_CATEGORY_HEIGHT: ClassVar[DefaultProxy[float]] = DefaultProxy( - "category_height" - ) category_width = 0.37 - DEFAULT_CATEGORY_WIDTH: ClassVar[DefaultProxy[float]] = DefaultProxy( - "category_width" - ) - # gridspec parameter. Sets the space between mainplot, dendrogram and legend wspace: float = 0 - DEFAULT_WSPACE: ClassVar[DefaultProxy[float]] = DefaultProxy("wspace") - cmap: str | None = "winter" - DEFAULT_COLORMAP: ClassVar[DefaultProxy[str]] = DefaultProxy("cmap") legends_width: float = 1.5 - DEFAULT_LEGENDS_WIDTH: ClassVar[DefaultProxy[float]] = DefaultProxy("legends_width") color_legend_title: str = "Expression\nlevel in group" - DEFAULT_COLOR_LEGEND_TITLE: ClassVar[DefaultProxy[str]] = DefaultProxy( - "color_legend_title" - ) are_axes_swapped: bool = False var_names_idx_order: Sequence[int] | None = None group_extra_size: float = 0.0 @@ -136,6 +124,13 @@ class BasePlot: fig: Figure | None = None ax_dict: dict[str, Axes] | None = None + kwds: Mapping[str, object] = field(default_factory=dict) + + # properties aliasing fields + @property + def has_var_groups(self) -> bool: + return len(self.var_group_positions or ()) > 0 + @property def fig_title(self) -> str | None: return self.title @@ -152,32 +147,23 @@ def height(self) -> float | None: def vboundnorm(self) -> VBoundNorm: return VBoundNorm(self.vmin, self.vmax, self.vcenter, self.norm) - @old_positionals( - "use_raw", - "log", - "num_categories", - "categories_order", - "title", - "figsize", - "gene_symbols", - "var_group_positions", - "var_group_labels", - "var_group_rotation", - "layer", - "ax", - "vmin", - "vmax", - "vcenter", - "norm", + # deprecated class vars + MIN_FIGURE_HEIGHT: ClassVar[DefaultProxy[float]] = DefaultProxy("min_figure_height") + DEFAULT_CATEGORY_HEIGHT: ClassVar[DefaultProxy[float]] = DefaultProxy( + "category_height" + ) + DEFAULT_CATEGORY_WIDTH: ClassVar[DefaultProxy[float]] = DefaultProxy( + "category_width" + ) + DEFAULT_WSPACE: ClassVar[DefaultProxy[float]] = DefaultProxy("wspace") + DEFAULT_COLORMAP: ClassVar[DefaultProxy[str]] = DefaultProxy("cmap") + DEFAULT_LEGENDS_WIDTH: ClassVar[DefaultProxy[float]] = DefaultProxy("legends_width") + DEFAULT_COLOR_LEGEND_TITLE: ClassVar[DefaultProxy[str]] = DefaultProxy( + "color_legend_title" ) - def __post_init__(self, **kwds): - self.has_var_groups = ( - True - if self.var_group_positions is not None - and len(self.var_group_positions) > 0 - else False - ) + def __post_init__(self): + cls = type(self) self._update_var_groups() self.categories, self.obs_tidy = _prepare_dataframe( @@ -190,13 +176,14 @@ def __post_init__(self, **kwds): layer=self.layer, gene_symbols=self.gene_symbols, ) - if len(self.categories) > self.MAX_NUM_CATEGORIES: + if len(self.categories) > cls.MAX_NUM_CATEGORIES: warn( - f"Over {self.MAX_NUM_CATEGORIES} categories found. " + f"Over {cls.MAX_NUM_CATEGORIES} categories found. " "Plot would be very large." ) if self.categories_order is not None: + assert isinstance(self.obs_tidy.index, pd.CategoricalIndex) if set(self.obs_tidy.index.categories) != set(self.categories_order): logg.error( "Please check that the categories given by " @@ -211,7 +198,6 @@ def __post_init__(self, **kwds): if isinstance(self.groupby, str): self.groupby = [self.groupby] - self.kwds = kwds @legacy_api("swap_axes") def swap_axes(self, *, swap_axes: bool = True) -> Self: @@ -482,6 +468,7 @@ def legend( def get_axes(self) -> dict[str, Axes]: if self.ax_dict is None: self.make_figure() + assert self.ax_dict is not None return self.ax_dict def _plot_totals( @@ -1127,7 +1114,6 @@ def _update_var_groups(self) -> None: self.var_names = _var_names self.var_group_labels = var_group_labels self.var_group_positions = var_group_positions - self.has_var_groups = True elif isinstance(self.var_names, str): self.var_names = [self.var_names] diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index 205352e4fd..d07baf55c5 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -13,6 +13,7 @@ from ._baseplot_class import BasePlot, doc_common_groupby_plot_args from ._docs import doc_common_plot_args, doc_show_save_ax, doc_vboundnorm from ._utils import ( + DefaultProxy, check_colornorm, fix_kwds, make_grid_spec, @@ -96,142 +97,107 @@ class DotPlot(BasePlot): """ DEFAULT_SAVE_PREFIX: ClassVar[str] = "dotplot_" + + categories_order: Sequence[str] | None = None + expression_cutoff: float = 0.0 + mean_only_expressed: bool = False + standard_scale: Literal["var", "group"] | None = None + dot_color_df: pd.DataFrame | None = None + dot_size_df: pd.DataFrame | None = None + # default style parameters - cmap: str = "winter" - DEFAULT_COLOR_ON: ClassVar[Literal["dot", "square"]] = "dot" - DEFAULT_DOT_MAX = None - DEFAULT_DOT_MIN = None - DEFAULT_SMALLEST_DOT = 0.0 - DEFAULT_LARGEST_DOT = 200.0 - DEFAULT_DOT_EDGECOLOR = "black" - DEFAULT_DOT_EDGELW = 0.2 - DEFAULT_SIZE_EXPONENT = 1.5 + color_on: Literal["dot", "square"] = "dot" + dot_max: float | None = None + dot_min: float | None = None + smallest_dot: float = 0.0 + largest_dot: float = 200 + dot_edge_color: ColorLike = "black" + dot_edge_lw: float = 0.2 + size_exponent: float = 1.5 + grid: bool = False + # a unit is the distance between two x-axis ticks + plot_x_padding: float = 0.8 + # a unit is the distance between two y-axis ticks + plot_y_padding: float = 1.0 # default legend parameters - DEFAULT_SIZE_LEGEND_TITLE = "Fraction of cells\nin group (%)" - color_legend_title = "Mean expression\nin group" - legends_width = 1.5 # inches - DEFAULT_PLOT_X_PADDING = 0.8 # a unit is the distance between two x-axis ticks - DEFAULT_PLOT_Y_PADDING = 1.0 # a unit is the distance between two y-axis ticks - - @old_positionals( - "use_raw", - "log", - "num_categories", - "categories_order", - "title", - "figsize", - "gene_symbols", - "var_group_positions", - "var_group_labels", - "var_group_rotation", - "layer", - "expression_cutoff", - "mean_only_expressed", - "standard_scale", - "dot_color_df", - "dot_size_df", - "ax", - "vmin", - "vmax", - "vcenter", - "norm", + size_title: str = "Fraction of cells\nin group (%)" + color_legend_title: str = "Mean expression\nin group" + legends_width: float = 1.5 # inches + show_size_legend: bool = True + show_colorbar: bool = True + + # deprecated default class variables + DEFAULT_COLOR_ON: ClassVar[DefaultProxy[Literal["dot", "square"]]] = DefaultProxy( + "color_on" ) - def __init__( - self, - adata: AnnData, - var_names: _VarNames | Mapping[str, _VarNames], - groupby: str | Sequence[str], - *, - use_raw: bool | None = None, - log: bool = False, - num_categories: int = 7, - categories_order: Sequence[str] | None = None, - title: str | None = None, - figsize: tuple[float, float] | None = None, - gene_symbols: str | None = None, - var_group_positions: Sequence[tuple[int, int]] | None = None, - var_group_labels: Sequence[str] | None = None, - var_group_rotation: float | None = None, - layer: str | None = None, - expression_cutoff: float = 0.0, - mean_only_expressed: bool = False, - standard_scale: Literal["var", "group"] | None = None, - dot_color_df: pd.DataFrame | None = None, - dot_size_df: pd.DataFrame | None = None, - ax: _AxesSubplot | None = None, - vmin: float | None = None, - vmax: float | None = None, - vcenter: float | None = None, - norm: Normalize | None = None, - **kwds, - ) -> None: - BasePlot.__init__( - self, - adata, - var_names, - groupby, - use_raw=use_raw, - log=log, - num_categories=num_categories, - categories_order=categories_order, - title=title, - figsize=figsize, - gene_symbols=gene_symbols, - var_group_positions=var_group_positions, - var_group_labels=var_group_labels, - var_group_rotation=var_group_rotation, - layer=layer, - ax=ax, - vmin=vmin, - vmax=vmax, - vcenter=vcenter, - norm=norm, - **kwds, - ) + DEFAULT_DOT_MAX: ClassVar[DefaultProxy[float | None]] = DefaultProxy("dot_max") + DEFAULT_DOT_MIN: ClassVar[DefaultProxy[float | None]] = DefaultProxy("dot_min") + DEFAULT_SMALLEST_DOT: ClassVar[DefaultProxy[float]] = DefaultProxy("smallest_dot") + DEFAULT_LARGEST_DOT: ClassVar[DefaultProxy[float]] = DefaultProxy("largest_dot") + DEFAULT_DOT_EDGECOLOR: ClassVar[DefaultProxy[ColorLike]] = DefaultProxy( + "dot_edge_color" + ) + DEFAULT_DOT_EDGELW: ClassVar[DefaultProxy[float]] = DefaultProxy("dot_edge_lw") + DEFAULT_SIZE_EXPONENT: ClassVar[DefaultProxy[float]] = DefaultProxy("size_exponent") + DEFAULT_PLOT_X_PADDING: ClassVar[DefaultProxy[float]] = DefaultProxy( + "plot_x_padding" + ) + DEFAULT_PLOT_Y_PADDING: ClassVar[DefaultProxy[float]] = DefaultProxy( + "plot_y_padding" + ) + DEFAULT_SIZE_LEGEND_TITLE: ClassVar[DefaultProxy[str]] = DefaultProxy("size_title") + def __post_init__(self) -> None: + super().__post_init__() # for if category defined by groupby (if any) compute for each var_name # 1. the fraction of cells in the category having a value >expression_cutoff # 2. the mean value over the category # 1. compute fraction of cells having value > expression_cutoff # transform obs_tidy into boolean matrix using the expression_cutoff - obs_bool = self.obs_tidy > expression_cutoff + obs_bool = self.obs_tidy > self.expression_cutoff # compute the sum per group which in the boolean matrix this is the number # of values >expression_cutoff, and divide the result by the total number of # values in the group (given by `count()`) - if dot_size_df is None: - dot_size_df = ( + if self.dot_size_df is None: + self.dot_size_df = ( obs_bool.groupby(level=0, observed=True).sum() / obs_bool.groupby(level=0, observed=True).count() ) - if dot_color_df is None: + if self.dot_color_df is None: # 2. compute mean expression value value - if mean_only_expressed: - dot_color_df = ( + if self.mean_only_expressed: + self.dot_color_df = ( self.obs_tidy.mask(~obs_bool) .groupby(level=0, observed=True) .mean() .fillna(0) ) else: - dot_color_df = self.obs_tidy.groupby(level=0, observed=True).mean() - - if standard_scale == "group": - dot_color_df = dot_color_df.sub(dot_color_df.min(1), axis=0) - dot_color_df = dot_color_df.div(dot_color_df.max(1), axis=0).fillna(0) - elif standard_scale == "var": - dot_color_df -= dot_color_df.min(0) - dot_color_df = (dot_color_df / dot_color_df.max(0)).fillna(0) - elif standard_scale is None: + self.dot_color_df = self.obs_tidy.groupby(level=0, observed=True).mean() + + if self.standard_scale == "group": + self.dot_color_df = self.dot_color_df.sub( + self.dot_color_df.min(1), axis=0 + ) + self.dot_color_df = self.dot_color_df.div( + self.dot_color_df.max(1), axis=0 + ).fillna(0) + elif self.standard_scale == "var": + self.dot_color_df -= self.dot_color_df.min(0) + self.dot_color_df = ( + self.dot_color_df / self.dot_color_df.max(0) + ).fillna(0) + elif self.standard_scale is None: pass else: logg.warning("Unknown type for standard_scale, ignored") else: # check that both matrices have the same shape - if dot_color_df.shape != dot_size_df.shape: + if self.dot_color_df.shape != self.dot_size_df.shape: logg.error( "the given dot_color_df data frame has a different shape than " "the data frame used for the dot size. Both data frames need " @@ -247,45 +213,27 @@ def __init__( # ['a', 'a', 'a', 'a', 'b'] unique_var_names, unique_idx = np.unique( - dot_color_df.columns, return_index=True + self.dot_color_df.columns, return_index=True ) # remove duplicate columns if len(unique_var_names) != len(self.var_names): - dot_color_df = dot_color_df.iloc[:, unique_idx] + self.dot_color_df = self.dot_color_df.iloc[:, unique_idx] # get the same order for rows and columns in the dot_color_df # using the order from the doc_size_df - dot_color_df = dot_color_df.loc[dot_size_df.index][dot_size_df.columns] + self.dot_color_df = self.dot_color_df.loc[self.dot_size_df.index][ + self.dot_size_df.columns + ] self.dot_color_df, self.dot_size_df = ( df.loc[ - categories_order if categories_order is not None else self.categories + self.categories_order + if self.categories_order is not None + else self.categories ] - for df in (dot_color_df, dot_size_df) + for df in (self.dot_color_df, self.dot_size_df) ) - # Set default style parameters - self.cmap = self.DEFAULT_COLORMAP - self.dot_max = self.DEFAULT_DOT_MAX - self.dot_min = self.DEFAULT_DOT_MIN - self.smallest_dot = self.DEFAULT_SMALLEST_DOT - self.largest_dot = self.DEFAULT_LARGEST_DOT - self.color_on = self.DEFAULT_COLOR_ON - self.size_exponent = self.DEFAULT_SIZE_EXPONENT - self.grid = False - self.plot_x_padding = self.DEFAULT_PLOT_X_PADDING - self.plot_y_padding = self.DEFAULT_PLOT_Y_PADDING - - self.dot_edge_color = self.DEFAULT_DOT_EDGECOLOR - self.dot_edge_lw = self.DEFAULT_DOT_EDGELW - - # set legend defaults - self.color_legend_title = self.DEFAULT_COLOR_LEGEND_TITLE - self.size_title = self.DEFAULT_SIZE_LEGEND_TITLE - self.legends_width = self.DEFAULT_LEGENDS_WIDTH - self.show_size_legend = True - self.show_colorbar = True - @old_positionals( "cmap", "color_on", @@ -593,7 +541,6 @@ def _mainplot(self, ax): if self.are_axes_swapped: _size_df = _size_df.T _color_df = _color_df.T - self.cmap = self.kwds.pop("cmap", self.cmap) normalize, dot_min, dot_max = self._dotplot( _size_df, @@ -627,7 +574,7 @@ def _dotplot( dot_color: pd.DataFrame, dot_ax: Axes, *, - cmap: str = "Reds", + cmap: str | None = "Reds", color_on: str | None = "dot", y_label: str | None = None, dot_max: float | None = None, From a840104ff5c9c7854b748a082cc9006753287067 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Tue, 13 Aug 2024 16:27:11 +0200 Subject: [PATCH 06/24] WIP stacked violin --- src/scanpy/plotting/_dotplot.py | 10 +- src/scanpy/plotting/_matrixplot.py | 11 +- src/scanpy/plotting/_stacked_violin.py | 166 +++++++++---------------- 3 files changed, 69 insertions(+), 118 deletions(-) diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index d07baf55c5..5a1a205e48 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -831,13 +831,13 @@ def dotplot( expression_cutoff: float = 0.0, mean_only_expressed: bool = False, cmap: str = "Reds", - dot_max: float | None = DotPlot.DEFAULT_DOT_MAX, - dot_min: float | None = DotPlot.DEFAULT_DOT_MIN, + dot_max: float | None = DotPlot.dot_max, + dot_min: float | None = DotPlot.dot_min, standard_scale: Literal["var", "group"] | None = None, - smallest_dot: float | None = DotPlot.DEFAULT_SMALLEST_DOT, + smallest_dot: float | None = DotPlot.smallest_dot, title: str | None = None, - colorbar_title: str | None = DotPlot.DEFAULT_COLOR_LEGEND_TITLE, - size_title: str | None = DotPlot.DEFAULT_SIZE_LEGEND_TITLE, + colorbar_title: str | None = DotPlot.color_legend_title, + size_title: str | None = DotPlot.size_title, figsize: tuple[float, float] | None = None, dendrogram: bool | str = False, gene_symbols: str | None = None, diff --git a/src/scanpy/plotting/_matrixplot.py b/src/scanpy/plotting/_matrixplot.py index 6ea05ff6bd..af459ac757 100644 --- a/src/scanpy/plotting/_matrixplot.py +++ b/src/scanpy/plotting/_matrixplot.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from dataclasses import dataclass +from typing import TYPE_CHECKING, ClassVar import numpy as np from matplotlib import pyplot as plt -from matplotlib import rcParams from .. import logging as logg from .._compat import old_positionals @@ -33,6 +33,7 @@ @_doc_params(common_plot_args=doc_common_plot_args) +@dataclass class MatrixPlot(BasePlot): """\ Allows the visualization of values using a color map. @@ -90,11 +91,11 @@ class MatrixPlot(BasePlot): sc.pl.MatrixPlot(adata, markers, groupby='bulk_labels').show() """ - DEFAULT_SAVE_PREFIX = "matrixplot_" - DEFAULT_COLOR_LEGEND_TITLE = "Mean expression\nin group" + DEFAULT_SAVE_PREFIX: ClassVar[str] = "matrixplot_" + colorbar_title: str = "Mean expression\nin group" # default style parameters - DEFAULT_COLORMAP = rcParams["image.cmap"] + cmap = None # aka: rcParams["image.cmap"] DEFAULT_EDGE_COLOR = "gray" DEFAULT_EDGE_LW = 0.1 diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index 7c7c462b3c..2d56e5fba8 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -1,7 +1,7 @@ from __future__ import annotations -import warnings -from typing import TYPE_CHECKING +from dataclasses import InitVar, dataclass +from typing import TYPE_CHECKING, ClassVar import numpy as np import pandas as pd @@ -16,6 +16,7 @@ from ._baseplot_class import BasePlot, doc_common_groupby_plot_args from ._docs import doc_common_plot_args, doc_show_save_ax, doc_vboundnorm from ._utils import ( + DefaultProxy, _deprecated_scale, check_colornorm, make_grid_spec, @@ -36,6 +37,7 @@ @_doc_params(common_plot_args=doc_common_plot_args) +@dataclass class StackedViolin(BasePlot): """\ Stacked violin plots. @@ -101,29 +103,59 @@ class StackedViolin(BasePlot): >>> adata = sc.datasets.pbmc68k_reduced() >>> markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ'] >>> sc.pl.StackedViolin(adata, markers, groupby='bulk_labels', dendrogram=True) # doctest: +ELLIPSIS - + StackedViolin(adata=AnnData...) Using var_names as dict: >>> markers = {{'T-cell': 'CD3D', 'B-cell': 'CD79A', 'myeloid': 'CST3'}} >>> sc.pl.StackedViolin(adata, markers, groupby='bulk_labels', dendrogram=True) # doctest: +ELLIPSIS - + StackedViolin(adata=AnnData...) """ - DEFAULT_SAVE_PREFIX = "stacked_violin_" - DEFAULT_COLOR_LEGEND_TITLE = "Median expression\nin group" + DEFAULT_SAVE_PREFIX: ClassVar[str] = "stacked_violin_" + + standard_scale: InitVar[Literal["var", "obs"] | None] = None + dendrogram: InitVar[str | None] = None + + # overrides + color_legend_title: str = "Median expression\nin group" + cmap: str | None = "Blues" + + # style parameters + row_palette: str | None = None + stripplot: bool = False + jitter: bool = False + jitter_size: int = 1 + plot_yticklabels: bool = False + ylim: tuple[float, float] | None = None + # a unit is the distance between two x-axis ticks + plot_x_padding: float = 0.5 + # a unit is the distance between two y-axis ticks + plot_y_padding: float = 0.5 + + # deprecated default class variables + DEFAULT_ROW_PALETTE: ClassVar[DefaultProxy[str | None]] = DefaultProxy( + "row_palette" + ) + DEFAULT_STRIPPLOT: ClassVar[DefaultProxy[bool]] = DefaultProxy("stripplot") + DEFAULT_JITTER: ClassVar[DefaultProxy[bool]] = DefaultProxy("jitter") + DEFAULT_JITTER_SIZE: ClassVar[DefaultProxy[int]] = DefaultProxy("jitter_size") + DEFAULT_PLOT_YTICKLABELS: ClassVar[DefaultProxy[bool]] = DefaultProxy( + "plot_yticklabels" + ) + DEFAULT_YLIM: ClassVar[DefaultProxy[tuple[float, float] | None]] = DefaultProxy( + "ylim" + ) + DEFAULT_PLOT_X_PADDING: ClassVar[DefaultProxy[float]] = DefaultProxy( + "plot_x_padding" + ) + DEFAULT_PLOT_Y_PADDING: ClassVar[DefaultProxy[float]] = DefaultProxy( + "plot_y_padding" + ) - DEFAULT_COLORMAP = "Blues" - DEFAULT_STRIPPLOT = False - DEFAULT_JITTER = False - DEFAULT_JITTER_SIZE = 1 + # kwds defaults: TODO: make work with proxys DEFAULT_LINE_WIDTH = 0.2 - DEFAULT_ROW_PALETTE = None DEFAULT_DENSITY_NORM: Literal["area", "count", "width"] = "width" - DEFAULT_PLOT_YTICKLABELS = False - DEFAULT_YLIM = None - DEFAULT_PLOT_X_PADDING = 0.5 # a unit is the distance between two x-axis ticks - DEFAULT_PLOT_Y_PADDING = 0.5 # a unit is the distance between two y-axis ticks # set by default the violin plot cut=0 to limit the extend # of the violin plot as this produces better plots that wont extend @@ -142,86 +174,12 @@ class StackedViolin(BasePlot): # None will draw unadorned violins. DEFAULT_INNER = None - def __getattribute__(self, name: str) -> object: - """Called unconditionally when accessing an instance attribute""" - # If the user has set the deprecated version on the class, - # and our code accesses the new version from the instance, - # return the user-specified version instead and warn. - # This is done because class properties are hard to do. - if name == "DEFAULT_DENSITY_NORM" and hasattr(self, "DEFAULT_SCALE"): - msg = "Don’t set DEFAULT_SCALE, use DEFAULT_DENSITY_NORM instead" - warnings.warn(msg, FutureWarning) - return object.__getattribute__(self, "DEFAULT_SCALE") - return object.__getattribute__(self, name) - - @old_positionals( - "use_raw", - "log", - "num_categories", - "categories_order", - "title", - "figsize", - "gene_symbols", - "var_group_positions", - "var_group_labels", - "var_group_rotation", - "layer", - "standard_scale", - "ax", - "vmin", - "vmax", - "vcenter", - "norm", - ) - def __init__( + def __post_init__( self, - adata: AnnData, - var_names: _VarNames | Mapping[str, _VarNames], - groupby: str | Sequence[str], - *, - use_raw: bool | None = None, - log: bool = False, - num_categories: int = 7, - categories_order: Sequence[str] | None = None, - title: str | None = None, - figsize: tuple[float, float] | None = None, - gene_symbols: str | None = None, - var_group_positions: Sequence[tuple[int, int]] | None = None, - var_group_labels: Sequence[str] | None = None, - var_group_rotation: float | None = None, - layer: str | None = None, - standard_scale: Literal["var", "group"] | None = None, - ax: _AxesSubplot | None = None, - vmin: float | None = None, - vmax: float | None = None, - vcenter: float | None = None, - norm: Normalize | None = None, - **kwds, + standard_scale: Literal["var", "obs"] | None = None, + dendrogram: str | None = None, ): - BasePlot.__init__( - self, - adata, - var_names, - groupby, - use_raw=use_raw, - log=log, - num_categories=num_categories, - categories_order=categories_order, - title=title, - figsize=figsize, - gene_symbols=gene_symbols, - var_group_positions=var_group_positions, - var_group_labels=var_group_labels, - var_group_rotation=var_group_rotation, - layer=layer, - ax=ax, - vmin=vmin, - vmax=vmax, - vcenter=vcenter, - norm=norm, - **kwds, - ) - + super().__post_init__() if standard_scale == "obs": self.obs_tidy = self.obs_tidy.sub(self.obs_tidy.min(1), axis=0) self.obs_tidy = self.obs_tidy.div(self.obs_tidy.max(1), axis=0).fillna(0) @@ -233,22 +191,15 @@ def __init__( else: logg.warning("Unknown type for standard_scale, ignored") - # Set default style parameters - self.cmap = self.DEFAULT_COLORMAP - self.row_palette = self.DEFAULT_ROW_PALETTE - self.stripplot = self.DEFAULT_STRIPPLOT - self.jitter = self.DEFAULT_JITTER - self.jitter_size = self.DEFAULT_JITTER_SIZE - self.plot_yticklabels = self.DEFAULT_PLOT_YTICKLABELS - self.ylim = self.DEFAULT_YLIM - self.plot_x_padding = self.DEFAULT_PLOT_X_PADDING - self.plot_y_padding = self.DEFAULT_PLOT_Y_PADDING - + self.kwds = dict(self.kwds) self.kwds.setdefault("cut", self.DEFAULT_CUT) self.kwds.setdefault("inner", self.DEFAULT_INNER) self.kwds.setdefault("linewidth", self.DEFAULT_LINE_WIDTH) self.kwds.setdefault("density_norm", self.DEFAULT_DENSITY_NORM) + if dendrogram: + self.add_dendrogram(dendrogram_key=dendrogram) + @old_positionals( "cmap", "stripplot", @@ -668,9 +619,9 @@ def stacked_violin( use_raw: bool | None = None, num_categories: int = 7, title: str | None = None, - colorbar_title: str | None = StackedViolin.DEFAULT_COLOR_LEGEND_TITLE, + colorbar_title: str | None = StackedViolin.color_legend_title, figsize: tuple[float, float] | None = None, - dendrogram: bool | str = False, + dendrogram: str | None = None, gene_symbols: str | None = None, var_group_positions: Sequence[tuple[int, int]] | None = None, var_group_labels: Sequence[str] | None = None, @@ -816,11 +767,10 @@ def stacked_violin( vmax=vmax, vcenter=vcenter, norm=norm, + dendrogram=dendrogram, **kwds, ) - if dendrogram: - vp.add_dendrogram(dendrogram_key=dendrogram) if swap_axes: vp.swap_axes() vp = vp.style( From 85bbc866a8991dd44761866863d2ee449723f8c5 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Wed, 14 Aug 2024 19:15:34 +0200 Subject: [PATCH 07/24] matrixplot converted --- src/scanpy/plotting/_matrixplot.py | 142 ++++++++--------------------- tests/test_plotting.py | 7 -- 2 files changed, 39 insertions(+), 110 deletions(-) diff --git a/src/scanpy/plotting/_matrixplot.py b/src/scanpy/plotting/_matrixplot.py index af459ac757..04db634853 100644 --- a/src/scanpy/plotting/_matrixplot.py +++ b/src/scanpy/plotting/_matrixplot.py @@ -1,9 +1,10 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import TYPE_CHECKING, ClassVar +from dataclasses import InitVar, dataclass +from typing import TYPE_CHECKING, ClassVar, cast import numpy as np +import pandas as pd from matplotlib import pyplot as plt from .. import logging as logg @@ -16,13 +17,12 @@ doc_show_save_ax, doc_vboundnorm, ) -from ._utils import check_colornorm, fix_kwds, savefig_or_show +from ._utils import DefaultProxy, check_colornorm, fix_kwds, savefig_or_show if TYPE_CHECKING: from collections.abc import Mapping, Sequence from typing import Literal, Self - import pandas as pd from anndata import AnnData from matplotlib.axes import Axes from matplotlib.colors import Normalize @@ -96,113 +96,49 @@ class MatrixPlot(BasePlot): colorbar_title: str = "Mean expression\nin group" # default style parameters cmap = None # aka: rcParams["image.cmap"] - DEFAULT_EDGE_COLOR = "gray" - DEFAULT_EDGE_LW = 0.1 - - @old_positionals( - "use_raw", - "log", - "num_categories", - "categories_order", - "title", - "figsize", - "gene_symbols", - "var_group_positions", - "var_group_labels", - "var_group_rotation", - "layer", - "standard_scale", - "ax", - "values_df", - "vmin", - "vmax", - "vcenter", - "norm", - ) - def __init__( + values_df: pd.DataFrame | None = None + standard_scale: InitVar[Literal["var", "group"] | None] = None + edge_color: ColorLike = "gray" + edge_lw: float = 0.1 + + # deprecated default class variables + DEFAULT_EDGE_COLOR: ClassVar[DefaultProxy[ColorLike]] = DefaultProxy("edge_color") + DEFAULT_EDGE_LW: ClassVar[DefaultProxy[float]] = DefaultProxy("edge_lw") + + def __post_init__( self, - adata: AnnData, - var_names: _VarNames | Mapping[str, _VarNames], - groupby: str | Sequence[str], - *, - use_raw: bool | None = None, - log: bool = False, - num_categories: int = 7, - categories_order: Sequence[str] | None = None, - title: str | None = None, - figsize: tuple[float, float] | None = None, - gene_symbols: str | None = None, - var_group_positions: Sequence[tuple[int, int]] | None = None, - var_group_labels: Sequence[str] | None = None, - var_group_rotation: float | None = None, - layer: str | None = None, - standard_scale: Literal["var", "group"] = None, - ax: _AxesSubplot | None = None, - values_df: pd.DataFrame | None = None, - vmin: float | None = None, - vmax: float | None = None, - vcenter: float | None = None, - norm: Normalize | None = None, - **kwds, + standard_scale: Literal["var", "group"] | None, ): - BasePlot.__init__( - self, - adata, - var_names, - groupby, - use_raw=use_raw, - log=log, - num_categories=num_categories, - categories_order=categories_order, - title=title, - figsize=figsize, - gene_symbols=gene_symbols, - var_group_positions=var_group_positions, - var_group_labels=var_group_labels, - var_group_rotation=var_group_rotation, - layer=layer, - ax=ax, - vmin=vmin, - vmax=vmax, - vcenter=vcenter, - norm=norm, - **kwds, + super().__post_init__() + if self.values_df is not None: + return + + # compute mean value + self.values_df = cast( + pd.DataFrame, + self.obs_tidy.groupby(level=0, observed=True) + .mean() + .loc[ + self.categories_order + if self.categories_order is not None + else self.categories + ], ) - if values_df is None: - # compute mean value - values_df = ( - self.obs_tidy.groupby(level=0, observed=True) - .mean() - .loc[ - self.categories_order - if self.categories_order is not None - else self.categories - ] - ) - - if standard_scale == "group": - values_df = values_df.sub(values_df.min(1), axis=0) - values_df = values_df.div(values_df.max(1), axis=0).fillna(0) - elif standard_scale == "var": - values_df -= values_df.min(0) - values_df = (values_df / values_df.max(0)).fillna(0) - elif standard_scale is None: - pass - else: - logg.warning("Unknown type for standard_scale, ignored") - - self.values_df = values_df - - self.cmap = self.DEFAULT_COLORMAP - self.edge_color = self.DEFAULT_EDGE_COLOR - self.edge_lw = self.DEFAULT_EDGE_LW + if standard_scale == "group": + self.values_df = self.values_df.sub(self.values_df.min(1), axis=0) + self.values_df = self.values_df.div(self.values_df.max(1), axis=0).fillna(0) + elif standard_scale == "var": + self.values_df -= self.values_df.min(0) + self.values_df = (self.values_df / self.values_df.max(0)).fillna(0) + elif standard_scale is not None: + logg.warning("Unknown type for standard_scale, ignored") def style( self, cmap: str | None | Empty = _empty, - edge_color: ColorLike | None | Empty = _empty, - edge_lw: float | None | Empty = _empty, + edge_color: ColorLike | Empty = _empty, + edge_lw: float | Empty = _empty, ) -> Self: """\ Modifies plot visual parameters. diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 3c790a842e..67a3dd759b 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -1719,10 +1719,3 @@ def test_string_mask(tmp_path, check_same_image): plt.close() check_same_image(p1, p2, tol=1) - - -def test_violin_scale_warning(monkeypatch): - adata = pbmc3k_processed() - monkeypatch.setattr(sc.pl.StackedViolin, "DEFAULT_SCALE", "count", raising=False) - with pytest.warns(FutureWarning, match="Don’t set DEFAULT_SCALE"): - sc.pl.StackedViolin(adata, adata.var_names[:3], groupby="louvain") From f981f7b357c4059ba00f25622c2a2f328c3e61b8 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Wed, 14 Aug 2024 19:20:23 +0200 Subject: [PATCH 08/24] Fix kwds --- src/scanpy/plotting/_dotplot.py | 4 ++-- src/scanpy/plotting/_matrixplot.py | 2 +- src/scanpy/plotting/_stacked_violin.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index 5a1a205e48..449f44d019 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -839,7 +839,7 @@ def dotplot( colorbar_title: str | None = DotPlot.color_legend_title, size_title: str | None = DotPlot.size_title, figsize: tuple[float, float] | None = None, - dendrogram: bool | str = False, + dendrogram: str | None = None, gene_symbols: str | None = None, var_group_positions: Sequence[tuple[int, int]] | None = None, var_group_labels: Sequence[str] | None = None, @@ -987,7 +987,7 @@ def dotplot( vmax=vmax, vcenter=vcenter, norm=norm, - **kwds, + kwds=kwds, ) if dendrogram: diff --git a/src/scanpy/plotting/_matrixplot.py b/src/scanpy/plotting/_matrixplot.py index 04db634853..ba8c1edff3 100644 --- a/src/scanpy/plotting/_matrixplot.py +++ b/src/scanpy/plotting/_matrixplot.py @@ -387,7 +387,7 @@ def matrixplot( vmax=vmax, vcenter=vcenter, norm=norm, - **kwds, + kwds=kwds, ) if dendrogram: diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index 2d56e5fba8..c1742d0659 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -768,7 +768,7 @@ def stacked_violin( vcenter=vcenter, norm=norm, dendrogram=dendrogram, - **kwds, + kwds=kwds, ) if swap_axes: From ba000a65cf18083d93d489b8b62c74aa69fac0bd Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Wed, 14 Aug 2024 19:26:35 +0200 Subject: [PATCH 09/24] centralize dendrogram --- src/scanpy/plotting/_baseplot_class.py | 9 ++++++++- src/scanpy/plotting/_dotplot.py | 7 +++---- src/scanpy/plotting/_matrixplot.py | 9 ++++----- src/scanpy/plotting/_stacked_violin.py | 12 +++--------- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/scanpy/plotting/_baseplot_class.py b/src/scanpy/plotting/_baseplot_class.py index a6b562f640..4c4e35e0f3 100644 --- a/src/scanpy/plotting/_baseplot_class.py +++ b/src/scanpy/plotting/_baseplot_class.py @@ -5,6 +5,7 @@ import collections.abc as cabc from dataclasses import ( KW_ONLY, # noqa: TCH003 # https://github.com/astral-sh/ruff/issues/12859 + InitVar, dataclass, field, ) @@ -106,6 +107,9 @@ class BasePlot: vcenter: float | None = None norm: Normalize | None = None + # convenience + dendrogram: InitVar[str | None] = None + # minimum height required for legends to plot properly min_figure_height: float = 2.5 category_height = 0.35 @@ -162,7 +166,7 @@ def vboundnorm(self) -> VBoundNorm: "color_legend_title" ) - def __post_init__(self): + def __post_init__(self, dendrogram: str | None): cls = type(self) self._update_var_groups() @@ -199,6 +203,9 @@ def __post_init__(self): if isinstance(self.groupby, str): self.groupby = [self.groupby] + if dendrogram: + self.add_dendrogram(dendrogram_key=dendrogram) + @legacy_api("swap_axes") def swap_axes(self, *, swap_axes: bool = True) -> Self: """ diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index 449f44d019..163d92e26e 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -148,8 +148,8 @@ class DotPlot(BasePlot): ) DEFAULT_SIZE_LEGEND_TITLE: ClassVar[DefaultProxy[str]] = DefaultProxy("size_title") - def __post_init__(self) -> None: - super().__post_init__() + def __post_init__(self, dendrogram: str | None) -> None: + super().__post_init__(dendrogram=dendrogram) # for if category defined by groupby (if any) compute for each var_name # 1. the fraction of cells in the category having a value >expression_cutoff # 2. the mean value over the category @@ -987,11 +987,10 @@ def dotplot( vmax=vmax, vcenter=vcenter, norm=norm, + dendrogram=dendrogram, kwds=kwds, ) - if dendrogram: - dp.add_dendrogram(dendrogram_key=dendrogram) if swap_axes: dp.swap_axes() diff --git a/src/scanpy/plotting/_matrixplot.py b/src/scanpy/plotting/_matrixplot.py index ba8c1edff3..75538e2b92 100644 --- a/src/scanpy/plotting/_matrixplot.py +++ b/src/scanpy/plotting/_matrixplot.py @@ -107,9 +107,10 @@ class MatrixPlot(BasePlot): def __post_init__( self, + dendrogram: str | None, standard_scale: Literal["var", "group"] | None, ): - super().__post_init__() + super().__post_init__(dendrogram=dendrogram) if self.values_df is not None: return @@ -281,7 +282,7 @@ def matrixplot( log: bool = False, num_categories: int = 7, figsize: tuple[float, float] | None = None, - dendrogram: bool | str = False, + dendrogram: str | None = None, title: str | None = None, cmap: str | None = MatrixPlot.DEFAULT_COLORMAP, colorbar_title: str | None = MatrixPlot.DEFAULT_COLOR_LEGEND_TITLE, @@ -387,11 +388,9 @@ def matrixplot( vmax=vmax, vcenter=vcenter, norm=norm, + dendrogram=dendrogram, kwds=kwds, ) - - if dendrogram: - mp.add_dendrogram(dendrogram_key=dendrogram) if swap_axes: mp.swap_axes() diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index c1742d0659..a0326f1659 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -115,7 +115,6 @@ class StackedViolin(BasePlot): DEFAULT_SAVE_PREFIX: ClassVar[str] = "stacked_violin_" standard_scale: InitVar[Literal["var", "obs"] | None] = None - dendrogram: InitVar[str | None] = None # overrides color_legend_title: str = "Median expression\nin group" @@ -176,19 +175,17 @@ class StackedViolin(BasePlot): def __post_init__( self, - standard_scale: Literal["var", "obs"] | None = None, dendrogram: str | None = None, + standard_scale: Literal["var", "obs"] | None = None, ): - super().__post_init__() + super().__post_init__(dendrogram=dendrogram) if standard_scale == "obs": self.obs_tidy = self.obs_tidy.sub(self.obs_tidy.min(1), axis=0) self.obs_tidy = self.obs_tidy.div(self.obs_tidy.max(1), axis=0).fillna(0) elif standard_scale == "var": self.obs_tidy -= self.obs_tidy.min(0) self.obs_tidy = (self.obs_tidy / self.obs_tidy.max(0)).fillna(0) - elif standard_scale is None: - pass - else: + elif standard_scale is not None: logg.warning("Unknown type for standard_scale, ignored") self.kwds = dict(self.kwds) @@ -197,9 +194,6 @@ def __post_init__( self.kwds.setdefault("linewidth", self.DEFAULT_LINE_WIDTH) self.kwds.setdefault("density_norm", self.DEFAULT_DENSITY_NORM) - if dendrogram: - self.add_dendrogram(dendrogram_key=dendrogram) - @old_positionals( "cmap", "stripplot", From 04b6ca9ca157f890e7f225ac594c8b6ceb53f04a Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Wed, 14 Aug 2024 19:32:25 +0200 Subject: [PATCH 10/24] centralize swap_axes --- src/scanpy/plotting/_baseplot_class.py | 5 ++++- src/scanpy/plotting/_dotplot.py | 6 ++---- src/scanpy/plotting/_matrixplot.py | 3 +-- src/scanpy/plotting/_stacked_violin.py | 12 +++++++----- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/scanpy/plotting/_baseplot_class.py b/src/scanpy/plotting/_baseplot_class.py index 4c4e35e0f3..4c8a27fdd7 100644 --- a/src/scanpy/plotting/_baseplot_class.py +++ b/src/scanpy/plotting/_baseplot_class.py @@ -109,6 +109,7 @@ class BasePlot: # convenience dendrogram: InitVar[str | None] = None + with_swapped_axes: InitVar[bool] = False # minimum height required for legends to plot properly min_figure_height: float = 2.5 @@ -166,7 +167,7 @@ def vboundnorm(self) -> VBoundNorm: "color_legend_title" ) - def __post_init__(self, dendrogram: str | None): + def __post_init__(self, dendrogram: str | None, with_swapped_axes: bool): cls = type(self) self._update_var_groups() @@ -205,6 +206,8 @@ def __post_init__(self, dendrogram: str | None): if dendrogram: self.add_dendrogram(dendrogram_key=dendrogram) + if with_swapped_axes: + self.swap_axes() @legacy_api("swap_axes") def swap_axes(self, *, swap_axes: bool = True) -> Self: diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index 163d92e26e..05413dd23d 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -845,7 +845,7 @@ def dotplot( var_group_labels: Sequence[str] | None = None, var_group_rotation: float | None = None, layer: str | None = None, - swap_axes: bool | None = False, + swap_axes: bool = False, dot_color_df: pd.DataFrame | None = None, show: bool | None = None, save: str | bool | None = None, @@ -988,12 +988,10 @@ def dotplot( vcenter=vcenter, norm=norm, dendrogram=dendrogram, + with_swapped_axes=swap_axes, kwds=kwds, ) - if swap_axes: - dp.swap_axes() - dp = dp.style( cmap=cmap, dot_max=dot_max, diff --git a/src/scanpy/plotting/_matrixplot.py b/src/scanpy/plotting/_matrixplot.py index 75538e2b92..b71a12f1dd 100644 --- a/src/scanpy/plotting/_matrixplot.py +++ b/src/scanpy/plotting/_matrixplot.py @@ -389,10 +389,9 @@ def matrixplot( vcenter=vcenter, norm=norm, dendrogram=dendrogram, + with_swapped_axes=swap_axes, kwds=kwds, ) - if swap_axes: - mp.swap_axes() mp = mp.style(cmap=cmap).legend(title=colorbar_title) if return_fig: diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index a0326f1659..eb6ec57680 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -175,10 +175,13 @@ class StackedViolin(BasePlot): def __post_init__( self, - dendrogram: str | None = None, - standard_scale: Literal["var", "obs"] | None = None, + dendrogram: str | None, + with_swapped_axes: bool, + standard_scale: Literal["var", "obs"] | None, ): - super().__post_init__(dendrogram=dendrogram) + super().__post_init__( + dendrogram=dendrogram, with_swapped_axes=with_swapped_axes + ) if standard_scale == "obs": self.obs_tidy = self.obs_tidy.sub(self.obs_tidy.min(1), axis=0) self.obs_tidy = self.obs_tidy.div(self.obs_tidy.max(1), axis=0).fillna(0) @@ -762,11 +765,10 @@ def stacked_violin( vcenter=vcenter, norm=norm, dendrogram=dendrogram, + with_swapped_axes=swap_axes, kwds=kwds, ) - if swap_axes: - vp.swap_axes() vp = vp.style( cmap=cmap, stripplot=stripplot, From 151530973db29d6209371a4b920edd9dac26f6f5 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 15 Aug 2024 17:51:43 -0400 Subject: [PATCH 11/24] make it work --- src/scanpy/plotting/_baseplot_class.py | 9 ++++-- src/scanpy/plotting/_utils.py | 29 +++++++++++++++++++ tests/test_plotting_utils.py | 40 ++++++++++++++++++++++---- 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/scanpy/plotting/_baseplot_class.py b/src/scanpy/plotting/_baseplot_class.py index 4c8a27fdd7..0980eb73ff 100644 --- a/src/scanpy/plotting/_baseplot_class.py +++ b/src/scanpy/plotting/_baseplot_class.py @@ -22,7 +22,12 @@ from .._compat import old_positionals from .._utils import _empty from ._anndata import _get_dendrogram_key, _plot_dendrogram, _prepare_dataframe -from ._utils import DefaultProxy, check_colornorm, make_grid_spec +from ._utils import ( + ClassDescriptorEnabled, + DefaultProxy, + check_colornorm, + make_grid_spec, +) if TYPE_CHECKING: from collections.abc import Iterable, Mapping, Sequence @@ -67,7 +72,7 @@ class VBoundNorm(NamedTuple): @dataclass -class BasePlot: +class BasePlot(metaclass=ClassDescriptorEnabled): """\ Generic class for the visualization of AnnData categories and selected `var` (features or genes). diff --git a/src/scanpy/plotting/_utils.py b/src/scanpy/plotting/_utils.py index 1898096d9f..93a0901ac5 100644 --- a/src/scanpy/plotting/_utils.py +++ b/src/scanpy/plotting/_utils.py @@ -73,9 +73,28 @@ class _AxesSubplot(Axes, axes.SubplotBase): """Intersection between Axes and SubplotBase: Has methods of both""" +class ClassDescriptorEnabled(type): + """Metaclass to allow descriptors’ `__set__` to be called when updating a class attribute. + + `DefaultProxy` below relies on that. + """ + + def __setattr__(cls, name: str, value: object) -> None: + desc = cls.__dict__.get(name) + if desc is not None and hasattr(type(desc), "__set__"): + return desc.__set__(None, value) + return super().__setattr__(name, value) + + @dataclass class DefaultProxy(Generic[T]): attr: str + cls: type = object # O, set automatically by __set_name__ + name: str = "" # ditto + + def __set_name__(self, owner: type, name: str) -> None: + self.cls = owner + self.name = name def __get__(self, obj: O | None, objtype: type[O] | None = None) -> T: if objtype is None: @@ -96,6 +115,16 @@ def __get__(self, obj: O | None, objtype: type[O] | None = None) -> T: ) return v + def __set__(self, obj: object | None, value: T) -> None: + if obj is None: # This is enabled by `ClassDescriptorEnabled` above + msg = ( + f"Subclass {self.cls.__name__} or " + f"use `functools.partial` to override {self.attr}." + ) + warnings.warn(msg, FutureWarning) + obj = self.cls + setattr(obj, self.attr, value) + # ------------------------------------------------------------------------------- # Simple plotting functions diff --git a/tests/test_plotting_utils.py b/tests/test_plotting_utils.py index 2f6d399603..6e79a26c8d 100644 --- a/tests/test_plotting_utils.py +++ b/tests/test_plotting_utils.py @@ -9,7 +9,11 @@ from matplotlib import colormaps from matplotlib.colors import ListedColormap -from scanpy.plotting._utils import DefaultProxy, _validate_palette +from scanpy.plotting._utils import ( + ClassDescriptorEnabled, + DefaultProxy, + _validate_palette, +) viridis = cast(ListedColormap, colormaps["viridis"]) @@ -39,18 +43,42 @@ def test_validate_palette_no_mod(palette, typ): field(default_factory=lambda: 1), marks=[ pytest.mark.xfail( - "Tries to call factory while class not fully constructed" + reason="Tries to call factory while class not fully constructed" ) ], id="default_factory", ), ], ) -def test_default_proxy(param): +@pytest.mark.parametrize("set_", ["instance", "field_", "DEFAULT"]) +def test_default_proxy(param, set_: str): @dataclass - class Test: + class Test(metaclass=ClassDescriptorEnabled): field_: int = param DEFAULT: ClassVar[DefaultProxy[int]] = DefaultProxy("field_") - assert Test(2).field_ == 2 - assert Test(2).DEFAULT == 1 + instance = Test(2) + assert instance.field_ == 2 + # instantiating doesn’t update the class + assert instance.DEFAULT == Test().field_ == 1 + + instance.field_ = 3 + # updating the instance doesn’t update the class + assert Test.field_ == Test.DEFAULT == 1 + + if set_ == "instance": + v = 1 + elif set_ == "field_": + Test.field_ = v = 4 + elif set_ == "DEFAULT": + with pytest.warns(FutureWarning): + Test.DEFAULT = v = 5 + else: + pytest.fail(f"Unknown {set_=}") + + # updating anything doesn’t update existing instances + assert instance.field_ == 3 + # setting the fields updates the class, but … + assert Test.field_ == Test.DEFAULT == v + # … sadly doesn’t update the __init__ method + assert Test().field_ == 1 From 5f91dbc54b957490dcd5d751b2501fab11ad01c0 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 19 Sep 2024 11:21:28 +0200 Subject: [PATCH 12/24] Fix post_init --- src/scanpy/plotting/_dotplot.py | 6 ++++-- src/scanpy/plotting/_matrixplot.py | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index 05413dd23d..56a0d5943c 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -148,8 +148,10 @@ class DotPlot(BasePlot): ) DEFAULT_SIZE_LEGEND_TITLE: ClassVar[DefaultProxy[str]] = DefaultProxy("size_title") - def __post_init__(self, dendrogram: str | None) -> None: - super().__post_init__(dendrogram=dendrogram) + def __post_init__(self, dendrogram: str | None, with_swapped_axes: bool) -> None: + super().__post_init__( + dendrogram=dendrogram, with_swapped_axes=with_swapped_axes + ) # for if category defined by groupby (if any) compute for each var_name # 1. the fraction of cells in the category having a value >expression_cutoff # 2. the mean value over the category diff --git a/src/scanpy/plotting/_matrixplot.py b/src/scanpy/plotting/_matrixplot.py index b71a12f1dd..2c9d35622b 100644 --- a/src/scanpy/plotting/_matrixplot.py +++ b/src/scanpy/plotting/_matrixplot.py @@ -108,9 +108,12 @@ class MatrixPlot(BasePlot): def __post_init__( self, dendrogram: str | None, + with_swapped_axes: bool, standard_scale: Literal["var", "group"] | None, ): - super().__post_init__(dendrogram=dendrogram) + super().__post_init__( + dendrogram=dendrogram, with_swapped_axes=with_swapped_axes + ) if self.values_df is not None: return From ba697dd436288342ed51969b2b3f483599140b65 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 19 Sep 2024 12:46:07 +0200 Subject: [PATCH 13/24] KW_ONLY --- src/scanpy/plotting/_baseplot_class.py | 7 +------ src/scanpy/plotting/_dotplot.py | 3 ++- src/scanpy/plotting/_matrixplot.py | 3 ++- src/scanpy/plotting/_stacked_violin.py | 3 ++- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/scanpy/plotting/_baseplot_class.py b/src/scanpy/plotting/_baseplot_class.py index 0980eb73ff..8b983fb800 100644 --- a/src/scanpy/plotting/_baseplot_class.py +++ b/src/scanpy/plotting/_baseplot_class.py @@ -3,12 +3,7 @@ from __future__ import annotations import collections.abc as cabc -from dataclasses import ( - KW_ONLY, # noqa: TCH003 # https://github.com/astral-sh/ruff/issues/12859 - InitVar, - dataclass, - field, -) +from dataclasses import KW_ONLY, InitVar, dataclass, field from typing import TYPE_CHECKING, ClassVar, NamedTuple from warnings import warn diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index 56a0d5943c..1b8f387f1a 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import KW_ONLY, dataclass from typing import TYPE_CHECKING, ClassVar import numpy as np @@ -98,6 +98,7 @@ class DotPlot(BasePlot): DEFAULT_SAVE_PREFIX: ClassVar[str] = "dotplot_" + _: KW_ONLY categories_order: Sequence[str] | None = None expression_cutoff: float = 0.0 mean_only_expressed: bool = False diff --git a/src/scanpy/plotting/_matrixplot.py b/src/scanpy/plotting/_matrixplot.py index 2c9d35622b..cb9170eca4 100644 --- a/src/scanpy/plotting/_matrixplot.py +++ b/src/scanpy/plotting/_matrixplot.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import InitVar, dataclass +from dataclasses import KW_ONLY, InitVar, dataclass from typing import TYPE_CHECKING, ClassVar, cast import numpy as np @@ -93,6 +93,7 @@ class MatrixPlot(BasePlot): DEFAULT_SAVE_PREFIX: ClassVar[str] = "matrixplot_" + _: KW_ONLY colorbar_title: str = "Mean expression\nin group" # default style parameters cmap = None # aka: rcParams["image.cmap"] diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index eb6ec57680..152116d4de 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import InitVar, dataclass +from dataclasses import KW_ONLY, InitVar, dataclass from typing import TYPE_CHECKING, ClassVar import numpy as np @@ -114,6 +114,7 @@ class StackedViolin(BasePlot): DEFAULT_SAVE_PREFIX: ClassVar[str] = "stacked_violin_" + _: KW_ONLY standard_scale: InitVar[Literal["var", "obs"] | None] = None # overrides From 7f44bfe73513a01108b776ae25373becf541efe2 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 19 Sep 2024 13:33:09 +0200 Subject: [PATCH 14/24] add test --- src/scanpy/plotting/_dotplot.py | 54 +++++++++++++++++---------------- tests/test_plotting.py | 11 +++++++ 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index f5bcf4c95d..ce1f08525e 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -95,7 +95,7 @@ class DotPlot(BasePlot): DEFAULT_SAVE_PREFIX = "dotplot_" # default style parameters - DEFAULT_COLORMAP = "winter" + DEFAULT_COLORMAP = "Reds" DEFAULT_COLOR_ON = "dot" DEFAULT_DOT_MAX = None DEFAULT_DOT_MIN = None @@ -261,6 +261,7 @@ def __init__( ] for df in (dot_color_df, dot_size_df) ) + self.standard_scale = standard_scale # Set default style parameters self.cmap = self.DEFAULT_COLORMAP @@ -596,9 +597,10 @@ def _mainplot(self, ax): _color_df, ax, cmap=self.cmap, + color_on=self.color_on, dot_max=self.dot_max, dot_min=self.dot_min, - color_on=self.color_on, + standard_scale=self.standard_scale, edge_color=self.dot_edge_color, edge_lw=self.dot_edge_lw, smallest_dot=self.smallest_dot, @@ -623,24 +625,23 @@ def _dotplot( dot_color: pd.DataFrame, dot_ax: Axes, *, - cmap: str = "Reds", - color_on: str | None = "dot", - y_label: str | None = None, - dot_max: float | None = None, - dot_min: float | None = None, - standard_scale: Literal["var", "group"] | None = None, - smallest_dot: float | None = 0.0, - largest_dot: float | None = 200, - size_exponent: float | None = 2, - edge_color: ColorLike | None = None, - edge_lw: float | None = None, + cmap: str, + color_on: str | None, + dot_max: float | None, + dot_min: float | None, + standard_scale: Literal["var", "group"] | None, + smallest_dot: float | None, + largest_dot: float | None, + size_exponent: float | None, + edge_color: ColorLike | None, + edge_lw: float | None, grid: bool | None = False, - x_padding: float | None = 0.8, - y_padding: float | None = 1.0, - vmin: float | None = None, - vmax: float | None = None, - vcenter: float | None = None, - norm: Normalize | None = None, + x_padding: float | None, + y_padding: float | None, + vmin: float | None, + vmax: float | None, + vcenter: float | None, + norm: Normalize | None, **kwds, ): """\ @@ -653,10 +654,13 @@ def _dotplot( Parameters ---------- - dot_size: Data frame containing the dot_size. - dot_color: Data frame containing the dot_color, should have the same, - shape, columns and indices as dot_size. - dot_ax: matplotlib axis + dot_size + Data frame containing the dot_size. + dot_color + Data frame containing the dot_color, should have the same, + shape, columns and indices as dot_size. + dot_ax + matplotlib axis cmap String denoting matplotlib color map. color_on @@ -664,7 +668,6 @@ def _dotplot( the color of the dot. Optionally, the colormap can be applied to an square behind the dot, in which case the dot is transparent and only the edge is shown. - y_label: String. Label for y axis dot_max If none, the maximum dot size is set to the maximum fraction value found (e.g. 0.6). If given, the value should be a number between 0 and 1. @@ -821,7 +824,6 @@ def _dotplot( ) dot_ax.tick_params(axis="both", labelsize="small") dot_ax.grid(visible=False) - dot_ax.set_ylabel(y_label) # to be consistent with the heatmap plot, is better to # invert the order of the y-axis, such that the first group is on @@ -879,7 +881,7 @@ def dotplot( num_categories: int = 7, expression_cutoff: float = 0.0, mean_only_expressed: bool = False, - cmap: str = "Reds", + cmap: str = DotPlot.DEFAULT_COLORMAP, dot_max: float | None = DotPlot.DEFAULT_DOT_MAX, dot_min: float | None = DotPlot.DEFAULT_DOT_MIN, standard_scale: Literal["var", "group"] | None = None, diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 40cf29e90e..b5d194f35d 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -367,6 +367,17 @@ def test_dotplot_obj(image_comparer): save_and_compare_images("dotplot_std_scale_var") +def test_dotplot_style_no_reset(): + pbmc = pbmc68k_reduced() + plot = sc.pl.dotplot(pbmc, "CD79A", "bulk_labels", return_fig=True) + assert isinstance(plot, sc.pl.DotPlot) + assert plot.cmap == sc.pl.DotPlot.DEFAULT_COLORMAP + plot.style(cmap="winter") + assert plot.cmap == "winter" + plot.style(color_on="square") + assert plot.cmap == "winter", "style() should not reset unspecified parameters" + + def test_dotplot_add_totals(image_comparer): save_and_compare_images = partial(image_comparer, ROOT, tol=5) From d27dcad76f0363e8e02000b0e1d14126a15de9c5 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 19 Sep 2024 16:16:57 +0200 Subject: [PATCH 15/24] Document all Nones --- src/scanpy/plotting/_baseplot_class.py | 7 +- src/scanpy/plotting/_dotplot.py | 99 ++++++++-------------- src/scanpy/plotting/_matrixplot.py | 14 +-- src/scanpy/plotting/_stacked_violin.py | 37 ++++---- src/scanpy/plotting/_tools/scatterplots.py | 9 +- 5 files changed, 72 insertions(+), 94 deletions(-) diff --git a/src/scanpy/plotting/_baseplot_class.py b/src/scanpy/plotting/_baseplot_class.py index 1cb6b04971..b5953bd947 100644 --- a/src/scanpy/plotting/_baseplot_class.py +++ b/src/scanpy/plotting/_baseplot_class.py @@ -24,7 +24,7 @@ import pandas as pd from anndata import AnnData from matplotlib.axes import Axes - from matplotlib.colors import Normalize + from matplotlib.colors import Colormap, Normalize from .._utils import Empty from ._utils import ColorLike, _AxesSubplot @@ -405,14 +405,15 @@ def add_totals( return self @old_positionals("cmap") - def style(self, *, cmap: str | None | Empty = _empty) -> Self: + def style(self, *, cmap: Colormap | str | None | Empty = _empty) -> Self: """\ Set visual style parameters Parameters ---------- cmap - colormap + Matplotlib color map, specified by name or directly. + If ``None``, use :obj:`matplotlib.rcParams`\\ ``["image.cmap"]`` Returns ------- diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index ce1f08525e..99ae4c4d98 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -25,7 +25,7 @@ import pandas as pd from anndata import AnnData from matplotlib.axes import Axes - from matplotlib.colors import Normalize + from matplotlib.colors import Colormap, Normalize from .._utils import Empty from ._baseplot_class import _VarNames @@ -302,18 +302,18 @@ def __init__( def style( self, *, - cmap: str | None | Empty = _empty, - color_on: Literal["dot", "square"] | None | Empty = _empty, + cmap: Colormap | str | None | Empty = _empty, + color_on: Literal["dot", "square"] | Empty = _empty, dot_max: float | None | Empty = _empty, dot_min: float | None | Empty = _empty, - smallest_dot: float | None | Empty = _empty, - largest_dot: float | None | Empty = _empty, + smallest_dot: float | Empty = _empty, + largest_dot: float | Empty = _empty, dot_edge_color: ColorLike | None | Empty = _empty, dot_edge_lw: float | None | Empty = _empty, - size_exponent: float | None | Empty = _empty, - grid: bool | None | Empty = _empty, - x_padding: float | None | Empty = _empty, - y_padding: float | None | Empty = _empty, + size_exponent: float | Empty = _empty, + grid: bool | Empty = _empty, + x_padding: float | Empty = _empty, + y_padding: float | Empty = _empty, ) -> Self: r"""\ Modifies plot visual parameters @@ -323,31 +323,30 @@ def style( cmap String denoting matplotlib color map. color_on - Options are 'dot' or 'square'. Be default the colomap is applied to - the color of the dot. Optionally, the colormap can be applied to an - square behind the dot, in which case the dot is transparent and only - the edge is shown. + By default the color map is applied to the color of the ``"dot"``. + Optionally, the colormap can be applied to a ``"square"`` behind the dot, + in which case the dot is transparent and only the edge is shown. dot_max - If none, the maximum dot size is set to the maximum fraction value found - (e.g. 0.6). If given, the value should be a number between 0 and 1. + If ``None``, the maximum dot size is set to the maximum fraction value found (e.g. 0.6). + If given, the value should be a number between 0 and 1. All fractions larger than dot_max are clipped to this value. dot_min - If none, the minimum dot size is set to 0. If given, - the value should be a number between 0 and 1. + If ``None``, the minimum dot size is set to 0. + If given, the value should be a number between 0 and 1. All fractions smaller than dot_min are clipped to this value. smallest_dot - If none, the smallest dot has size 0. All expression fractions with `dot_min` are plotted with this size. largest_dot - If none, the largest dot has size 200. All expression fractions with `dot_max` are plotted with this size. dot_edge_color - Dot edge color. When `color_on='dot'` the default is no edge. When - `color_on='square'`, edge color is white for darker colors and black - for lighter background square colors. + Dot edge color. + When `color_on='dot'`, ``None`` means no edge. + When `color_on='square'`, ``None`` means that + the edge color is white for darker colors and black for lighter background square colors. dot_edge_lw - Dot edge line width. When `color_on='dot'` the default is no edge. When - `color_on='square'`, line width = 1.5. + Dot edge line width. + When `color_on='dot'`, ``None`` means no edge. + When `color_on='square'`, ``None`` means a line width of 1.5. size_exponent Dot size is computed as: fraction ** size exponent and afterwards scaled to match the @@ -387,9 +386,8 @@ def style( ... .style(dot_edge_color='black', dot_edge_lw=1, grid=True) \ ... .show() """ + super().style(cmap=cmap) - if cmap is not _empty: - self.cmap = cmap if dot_max is not _empty: self.dot_max = dot_max if dot_min is not _empty: @@ -572,7 +570,7 @@ def _plot_legend(self, legend_ax, return_ax_dict, normalize): self._plot_colorbar(color_legend_ax, normalize) return_ax_dict["color_legend_ax"] = color_legend_ax - def _mainplot(self, ax): + def _mainplot(self, ax: Axes): # work on a copy of the dataframes. This is to avoid changes # on the original data frames after repetitive calls to the # DotPlot object, for example once with swap_axes and other without @@ -625,19 +623,19 @@ def _dotplot( dot_color: pd.DataFrame, dot_ax: Axes, *, - cmap: str, - color_on: str | None, + cmap: Colormap | str | None, + color_on: Literal["dot", "square"], dot_max: float | None, dot_min: float | None, standard_scale: Literal["var", "group"] | None, - smallest_dot: float | None, - largest_dot: float | None, - size_exponent: float | None, + smallest_dot: float, + largest_dot: float, + size_exponent: float, edge_color: ColorLike | None, edge_lw: float | None, - grid: bool | None = False, - x_padding: float | None, - y_padding: float | None, + grid: bool, + x_padding: float, + y_padding: float, vmin: float | None, vmax: float | None, vcenter: float | None, @@ -662,41 +660,17 @@ def _dotplot( dot_ax matplotlib axis cmap - String denoting matplotlib color map. color_on - Options are 'dot' or 'square'. Be default the colomap is applied to - the color of the dot. Optionally, the colormap can be applied to an - square behind the dot, in which case the dot is transparent and only - the edge is shown. dot_max - If none, the maximum dot size is set to the maximum fraction value found - (e.g. 0.6). If given, the value should be a number between 0 and 1. - All fractions larger than dot_max are clipped to this value. dot_min - If none, the minimum dot size is set to 0. If given, - the value should be a number between 0 and 1. - All fractions smaller than dot_min are clipped to this value. standard_scale - Whether or not to standardize that dimension between 0 and 1, - meaning for each variable or group, - subtract the minimum and divide each by its maximum. smallest_dot - If none, the smallest dot has size 0. - All expression levels with `dot_min` are plotted with this size. edge_color - Dot edge color. When `color_on='dot'` the default is no edge. When - `color_on='square'`, edge color is white edge_lw - Dot edge line width. When `color_on='dot'` the default is no edge. When - `color_on='square'`, line width = 1.5 grid - Adds a grid to the plot - x_paddding - Space between the plot left/right borders and the dots center. A unit - is the distance between the x ticks. Only applied when color_on = dot - y_paddding - Space between the plot top/bottom borders and the dots center. A unit is - the distance between the y ticks. Only applied when color_on = dot + x_padding + y_padding + See `style` kwds Are passed to :func:`matplotlib.pyplot.scatter`. @@ -805,7 +779,6 @@ def _dotplot( linewidth=edge_lw, edgecolor=edge_color, ) - dot_ax.scatter(x, y, **kwds) y_ticks = np.arange(dot_color.shape[0]) + 0.5 diff --git a/src/scanpy/plotting/_matrixplot.py b/src/scanpy/plotting/_matrixplot.py index 6ea05ff6bd..0c6311254d 100644 --- a/src/scanpy/plotting/_matrixplot.py +++ b/src/scanpy/plotting/_matrixplot.py @@ -25,7 +25,7 @@ import pandas as pd from anndata import AnnData from matplotlib.axes import Axes - from matplotlib.colors import Normalize + from matplotlib.colors import Colormap, Normalize from .._utils import Empty from ._baseplot_class import _VarNames @@ -199,7 +199,7 @@ def __init__( def style( self, - cmap: str | None | Empty = _empty, + cmap: Colormap | str | None | Empty = _empty, edge_color: ColorLike | None | Empty = _empty, edge_lw: float | None | Empty = _empty, ) -> Self: @@ -209,11 +209,14 @@ def style( Parameters ---------- cmap - String denoting matplotlib color map. + Matplotlib color map, specified by name or directly. + If ``None``, use :obj:`matplotlib.rcParams`\\ ``["image.cmap"]`` edge_color - Edge color between the squares of matrix plot. Default is gray + Edge color between the squares of matrix plot. + If ``None``, use :obj:`matplotlib.rcParams`\\ ``["patch.edgecolor"]`` edge_lw Edge line width. + If ``None``, use :obj:`matplotlib.rcParams`\\ ``["lines.linewidth"]`` Returns ------- @@ -243,9 +246,8 @@ def style( ) """ + super().style(cmap=cmap) - if cmap is not _empty: - self.cmap = cmap if edge_color is not _empty: self.edge_color = edge_color if edge_lw is not _empty: diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index 7c7c462b3c..0bde6788ad 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -28,7 +28,7 @@ from anndata import AnnData from matplotlib.axes import Axes - from matplotlib.colors import Normalize + from matplotlib.colors import Colormap, Normalize from .._utils import Empty from ._baseplot_class import _VarNames @@ -265,17 +265,17 @@ def __init__( def style( self, *, - cmap: str | None | Empty = _empty, - stripplot: bool | None | Empty = _empty, - jitter: float | bool | None | Empty = _empty, - jitter_size: int | None | Empty = _empty, + cmap: Colormap | str | None | Empty = _empty, + stripplot: bool | Empty = _empty, + jitter: float | bool | Empty = _empty, + jitter_size: int | float | Empty = _empty, linewidth: float | None | Empty = _empty, row_palette: str | None | Empty = _empty, density_norm: Literal["area", "count", "width"] | Empty = _empty, - yticklabels: bool | None | Empty = _empty, + yticklabels: bool | Empty = _empty, ylim: tuple[float, float] | None | Empty = _empty, - x_padding: float | None | Empty = _empty, - y_padding: float | None | Empty = _empty, + x_padding: float | Empty = _empty, + y_padding: float | Empty = _empty, # deprecated scale: Literal["area", "count", "width"] | Empty = _empty, ) -> Self: @@ -285,7 +285,8 @@ def style( Parameters ---------- cmap - String denoting matplotlib color map. + Matplotlib color map, specified by name or directly. + If ``None``, use :obj:`matplotlib.rcParams`\ ``["image.cmap"]`` stripplot Add a stripplot on top of the violin plot. See :func:`~seaborn.stripplot`. @@ -295,9 +296,11 @@ def style( jitter_size Size of the jitter points. linewidth - linewidth for the violin plots. + line width for the violin plots. + If None, use :obj:`matplotlib.rcParams`\ ``["lines.linewidth"]`` row_palette The row palette determines the colors to use for the stacked violins. + If ``None``, use :obj:`matplotlib.rcParams`\ ``["axes.prop_cycle"]`` The value should be a valid seaborn or matplotlib palette name (see :func:`~seaborn.color_palette`). Alternatively, a single color name or hex value can be passed, @@ -310,8 +313,9 @@ def style( yticklabels Set to true to view the y tick labels. ylim - minimum and maximum values for the y-axis. If set. All rows will have - the same y-axis range. Example: ylim=(0, 5) + minimum and maximum values for the y-axis. + If not ``None``, all rows will have the same y-axis range. + Example: ``ylim=(0, 5)`` x_padding Space between the plot left/right borders and the violins. A unit is the distance between the x ticks. @@ -334,9 +338,8 @@ def style( >>> sc.pl.StackedViolin(adata, markers, groupby='bulk_labels') \ ... .style(row_palette='Blues', linewidth=0).show() """ + super().style(cmap=cmap) - if cmap is not _empty: - self.cmap = cmap if row_palette is not _empty: self.row_palette = row_palette self.kwds["color"] = self.row_palette @@ -462,8 +465,8 @@ def _make_rows_of_violinplots( _matrix, colormap_array, _color_df, - x_spacer_size, - y_spacer_size, + x_spacer_size: float | int, + y_spacer_size: float | int, x_axis_order, ): import seaborn as sns # Slow import, only import if called @@ -831,7 +834,7 @@ def stacked_violin( row_palette=row_palette, density_norm=kwds.get("density_norm", scale), yticklabels=yticklabels, - linewidth=kwds.get("linewidth", StackedViolin.DEFAULT_LINE_WIDTH), + linewidth=kwds.get("linewidth", _empty), ).legend(title=colorbar_title) if return_fig: return vp diff --git a/src/scanpy/plotting/_tools/scatterplots.py b/src/scanpy/plotting/_tools/scatterplots.py index 4f2b208ef1..476d868510 100644 --- a/src/scanpy/plotting/_tools/scatterplots.py +++ b/src/scanpy/plotting/_tools/scatterplots.py @@ -183,11 +183,10 @@ def embedding( # Prevents warnings during legend creation na_color = colors.to_hex(na_color, keep_alpha=True) - if "edgecolor" not in kwargs: - # by default turn off edge color. Otherwise, for - # very small sizes the edge will not reduce its size - # (https://github.com/scverse/scanpy/issues/293) - kwargs["edgecolor"] = "none" + # by default turn off edge color. Otherwise, for + # very small sizes the edge will not reduce its size + # (https://github.com/scverse/scanpy/issues/293) + kwargs.setdefault("edgecolor", "none") # Vectorized arguments From 578e57297e8cfa48e5492e58b45567217a6ae43b Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 19 Sep 2024 17:14:16 +0200 Subject: [PATCH 16/24] fix main function parameters --- src/scanpy/plotting/_baseplot_class.py | 2 +- src/scanpy/plotting/_dotplot.py | 24 ++++++++++++------------ src/scanpy/plotting/_matrixplot.py | 8 +++++--- src/scanpy/plotting/_stacked_violin.py | 22 +++++++++++++--------- 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/scanpy/plotting/_baseplot_class.py b/src/scanpy/plotting/_baseplot_class.py index b5953bd947..93a0fccb00 100644 --- a/src/scanpy/plotting/_baseplot_class.py +++ b/src/scanpy/plotting/_baseplot_class.py @@ -878,7 +878,7 @@ def savefig(self, filename: str, bbox_inches: str | None = "tight", **kwargs): self.make_figure() plt.savefig(filename, bbox_inches=bbox_inches, **kwargs) - def _reorder_categories_after_dendrogram(self, dendrogram) -> None: + def _reorder_categories_after_dendrogram(self, dendrogram: str | None) -> None: """\ Function used by plotting functions that need to reorder the the groupby observations based on the dendrogram results. diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index 99ae4c4d98..b5bc5106ec 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -854,11 +854,7 @@ def dotplot( num_categories: int = 7, expression_cutoff: float = 0.0, mean_only_expressed: bool = False, - cmap: str = DotPlot.DEFAULT_COLORMAP, - dot_max: float | None = DotPlot.DEFAULT_DOT_MAX, - dot_min: float | None = DotPlot.DEFAULT_DOT_MIN, standard_scale: Literal["var", "group"] | None = None, - smallest_dot: float | None = DotPlot.DEFAULT_SMALLEST_DOT, title: str | None = None, colorbar_title: str | None = DotPlot.DEFAULT_COLOR_LEGEND_TITLE, size_title: str | None = DotPlot.DEFAULT_SIZE_LEGEND_TITLE, @@ -879,6 +875,11 @@ def dotplot( vmax: float | None = None, vcenter: float | None = None, norm: Normalize | None = None, + # Style parameters + cmap: Colormap | str | None = DotPlot.DEFAULT_COLORMAP, + dot_max: float | None = DotPlot.DEFAULT_DOT_MAX, + dot_min: float | None = DotPlot.DEFAULT_DOT_MIN, + smallest_dot: float = DotPlot.DEFAULT_SMALLEST_DOT, **kwds, ) -> DotPlot | dict | None: """\ @@ -916,15 +917,14 @@ def dotplot( If True, gene expression is averaged only over the cells expressing the given genes. dot_max - If none, the maximum dot size is set to the maximum fraction value found + If ``None``, the maximum dot size is set to the maximum fraction value found (e.g. 0.6). If given, the value should be a number between 0 and 1. All fractions larger than dot_max are clipped to this value. dot_min - If none, the minimum dot size is set to 0. If given, + If ``None``, the minimum dot size is set to 0. If given, the value should be a number between 0 and 1. All fractions smaller than dot_min are clipped to this value. smallest_dot - If none, the smallest dot has size 0. All expression levels with `dot_min` are plotted with this size. {show_save_ax} {vminmax} @@ -984,9 +984,7 @@ def dotplot( # backwards compatibility: previous version of dotplot used `color_map` # instead of `cmap` - cmap = kwds.get("color_map", cmap) - if "color_map" in kwds: - del kwds["color_map"] + cmap = kwds.pop("color_map", cmap) dp = DotPlot( adata, @@ -1015,7 +1013,9 @@ def dotplot( ) if dendrogram: - dp.add_dendrogram(dendrogram_key=dendrogram) + dp.add_dendrogram( + dendrogram_key=dendrogram if isinstance(dendrogram, str) else None + ) if swap_axes: dp.swap_axes() @@ -1024,7 +1024,7 @@ def dotplot( dot_max=dot_max, dot_min=dot_min, smallest_dot=smallest_dot, - dot_edge_lw=kwds.pop("linewidth", DotPlot.DEFAULT_DOT_EDGELW), + dot_edge_lw=kwds.pop("linewidth", _empty), ).legend(colorbar_title=colorbar_title, size_title=size_title) if return_fig: diff --git a/src/scanpy/plotting/_matrixplot.py b/src/scanpy/plotting/_matrixplot.py index 0c6311254d..3240849061 100644 --- a/src/scanpy/plotting/_matrixplot.py +++ b/src/scanpy/plotting/_matrixplot.py @@ -135,7 +135,7 @@ def __init__( var_group_labels: Sequence[str] | None = None, var_group_rotation: float | None = None, layer: str | None = None, - standard_scale: Literal["var", "group"] = None, + standard_scale: Literal["var", "group"] | None = None, ax: _AxesSubplot | None = None, values_df: pd.DataFrame | None = None, vmin: float | None = None, @@ -348,7 +348,7 @@ def matrixplot( figsize: tuple[float, float] | None = None, dendrogram: bool | str = False, title: str | None = None, - cmap: str | None = MatrixPlot.DEFAULT_COLORMAP, + cmap: Colormap | str | None = MatrixPlot.DEFAULT_COLORMAP, colorbar_title: str | None = MatrixPlot.DEFAULT_COLOR_LEGEND_TITLE, gene_symbols: str | None = None, var_group_positions: Sequence[tuple[int, int]] | None = None, @@ -456,7 +456,9 @@ def matrixplot( ) if dendrogram: - mp.add_dendrogram(dendrogram_key=dendrogram) + mp.add_dendrogram( + dendrogram_key=dendrogram if isinstance(dendrogram, str) else None + ) if swap_axes: mp.swap_axes() diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index 0bde6788ad..2f39508698 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -190,7 +190,7 @@ def __init__( var_group_labels: Sequence[str] | None = None, var_group_rotation: float | None = None, layer: str | None = None, - standard_scale: Literal["var", "group"] | None = None, + standard_scale: Literal["var", "obs"] | None = None, ax: _AxesSubplot | None = None, vmin: float | None = None, vmax: float | None = None, @@ -680,23 +680,24 @@ def stacked_violin( standard_scale: Literal["var", "obs"] | None = None, var_group_rotation: float | None = None, layer: str | None = None, - stripplot: bool = StackedViolin.DEFAULT_STRIPPLOT, - jitter: float | bool = StackedViolin.DEFAULT_JITTER, - size: int = StackedViolin.DEFAULT_JITTER_SIZE, - scale: Literal["area", "count", "width"] = StackedViolin.DEFAULT_DENSITY_NORM, - yticklabels: bool | None = StackedViolin.DEFAULT_PLOT_YTICKLABELS, order: Sequence[str] | None = None, swap_axes: bool = False, show: bool | None = None, save: bool | str | None = None, return_fig: bool | None = False, - row_palette: str | None = StackedViolin.DEFAULT_ROW_PALETTE, - cmap: str | None = StackedViolin.DEFAULT_COLORMAP, ax: _AxesSubplot | None = None, vmin: float | None = None, vmax: float | None = None, vcenter: float | None = None, norm: Normalize | None = None, + # Style options + cmap: Colormap | str | None = StackedViolin.DEFAULT_COLORMAP, + stripplot: bool = StackedViolin.DEFAULT_STRIPPLOT, + jitter: float | bool = StackedViolin.DEFAULT_JITTER, + size: int | float = StackedViolin.DEFAULT_JITTER_SIZE, + row_palette: str | None = StackedViolin.DEFAULT_ROW_PALETTE, + scale: Literal["area", "count", "width"] = StackedViolin.DEFAULT_DENSITY_NORM, + yticklabels: bool = StackedViolin.DEFAULT_PLOT_YTICKLABELS, **kwds, ) -> StackedViolin | dict | None: """\ @@ -814,6 +815,7 @@ def stacked_violin( var_group_labels=var_group_labels, var_group_rotation=var_group_rotation, layer=layer, + categories_order=order, ax=ax, vmin=vmin, vmax=vmax, @@ -823,7 +825,9 @@ def stacked_violin( ) if dendrogram: - vp.add_dendrogram(dendrogram_key=dendrogram) + vp.add_dendrogram( + dendrogram_key=dendrogram if isinstance(dendrogram, str) else None + ) if swap_axes: vp.swap_axes() vp = vp.style( From f7405aa5a3a841232e14e65fc7f60e61399f2811 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 19 Sep 2024 17:36:40 +0200 Subject: [PATCH 17/24] correct types for dendrogram[_key] --- src/scanpy/plotting/_anndata.py | 23 ++++++++++++++--------- src/scanpy/plotting/_baseplot_class.py | 6 ++++-- src/scanpy/plotting/_dotplot.py | 4 +--- src/scanpy/plotting/_matrixplot.py | 4 +--- src/scanpy/plotting/_stacked_violin.py | 4 +--- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/scanpy/plotting/_anndata.py b/src/scanpy/plotting/_anndata.py index b164f30c7c..cee6ccd945 100755 --- a/src/scanpy/plotting/_anndata.py +++ b/src/scanpy/plotting/_anndata.py @@ -2,8 +2,8 @@ from __future__ import annotations -import collections.abc as cabc from collections import OrderedDict +from collections.abc import Collection, Mapping, Sequence from itertools import product from typing import TYPE_CHECKING, get_args @@ -37,7 +37,7 @@ ) if TYPE_CHECKING: - from collections.abc import Collection, Iterable, Mapping, Sequence + from collections.abc import Iterable from typing import Literal, Union from anndata import AnnData @@ -212,7 +212,7 @@ def _scatter_obs( isinstance(layers, str) and layers in adata.layers.keys() ): layers = (layers, layers, layers) - elif isinstance(layers, cabc.Collection) and len(layers) == 3: + elif isinstance(layers, Collection) and len(layers) == 3: layers = tuple(layers) for layer in layers: if layer not in adata.layers.keys() and layer not in ["X", None]: @@ -291,7 +291,7 @@ def _scatter_obs( palette_was_none = False if palette is None: palette_was_none = True - if isinstance(palette, cabc.Sequence) and not isinstance(palette, str): + if isinstance(palette, Sequence) and not isinstance(palette, str): if not is_color_like(palette[0]): palettes = palette else: @@ -2235,8 +2235,8 @@ def _plot_gene_groups_brackets( def _reorder_categories_after_dendrogram( adata: AnnData, - groupby, - dendrogram, + groupby: str | Sequence[str], + dendrogram: bool | str | None, *, var_names=None, var_group_labels=None, @@ -2346,14 +2346,19 @@ def _format_first_three_categories(categories): return ", ".join(categories) -def _get_dendrogram_key(adata, dendrogram_key, groupby): +def _get_dendrogram_key( + adata: AnnData, dendrogram_key: bool | str | None, groupby: str | Sequence[str] +) -> str: # the `dendrogram_key` can be a bool an NoneType or the name of the # dendrogram key. By default the name of the dendrogram key is 'dendrogram' if not isinstance(dendrogram_key, str): if isinstance(groupby, str): dendrogram_key = f"dendrogram_{groupby}" - elif isinstance(groupby, list): + elif isinstance(groupby, Sequence): dendrogram_key = f'dendrogram_{"_".join(groupby)}' + else: + msg = f"groupby has wrong type: {type(groupby).__name__}." + raise AssertionError(msg) if dendrogram_key not in adata.uns: from ..tools._dendrogram import dendrogram @@ -2653,7 +2658,7 @@ def _check_var_names_type(var_names, var_group_labels, var_group_positions): var_names, var_group_labels, var_group_positions """ - if isinstance(var_names, cabc.Mapping): + if isinstance(var_names, Mapping): if var_group_labels is not None or var_group_positions is not None: logg.warning( "`var_names` is a dictionary. This will reset the current " diff --git a/src/scanpy/plotting/_baseplot_class.py b/src/scanpy/plotting/_baseplot_class.py index 93a0fccb00..a85d5e0919 100644 --- a/src/scanpy/plotting/_baseplot_class.py +++ b/src/scanpy/plotting/_baseplot_class.py @@ -240,7 +240,7 @@ def add_dendrogram( self, *, show: bool | None = True, - dendrogram_key: str | None = None, + dendrogram_key: bool | str | None = None, size: float | None = 0.8, ) -> Self: r"""\ @@ -878,7 +878,9 @@ def savefig(self, filename: str, bbox_inches: str | None = "tight", **kwargs): self.make_figure() plt.savefig(filename, bbox_inches=bbox_inches, **kwargs) - def _reorder_categories_after_dendrogram(self, dendrogram: str | None) -> None: + def _reorder_categories_after_dendrogram( + self, dendrogram: bool | str | None + ) -> None: """\ Function used by plotting functions that need to reorder the the groupby observations based on the dendrogram results. diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index b5bc5106ec..caf52756df 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -1013,9 +1013,7 @@ def dotplot( ) if dendrogram: - dp.add_dendrogram( - dendrogram_key=dendrogram if isinstance(dendrogram, str) else None - ) + dp.add_dendrogram(dendrogram_key=dendrogram) if swap_axes: dp.swap_axes() diff --git a/src/scanpy/plotting/_matrixplot.py b/src/scanpy/plotting/_matrixplot.py index 3240849061..804af79d44 100644 --- a/src/scanpy/plotting/_matrixplot.py +++ b/src/scanpy/plotting/_matrixplot.py @@ -456,9 +456,7 @@ def matrixplot( ) if dendrogram: - mp.add_dendrogram( - dendrogram_key=dendrogram if isinstance(dendrogram, str) else None - ) + mp.add_dendrogram(dendrogram_key=dendrogram) if swap_axes: mp.swap_axes() diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index 2f39508698..9a42045099 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -825,9 +825,7 @@ def stacked_violin( ) if dendrogram: - vp.add_dendrogram( - dendrogram_key=dendrogram if isinstance(dendrogram, str) else None - ) + vp.add_dendrogram(dendrogram_key=dendrogram) if swap_axes: vp.swap_axes() vp = vp.style( From e77b4970b9e649abcac38910c33148419ed5fbe0 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 20 Sep 2024 09:55:30 +0200 Subject: [PATCH 18/24] Fix deprecation --- src/scanpy/plotting/_stacked_violin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index 9a42045099..ed151dfc12 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -696,8 +696,10 @@ def stacked_violin( jitter: float | bool = StackedViolin.DEFAULT_JITTER, size: int | float = StackedViolin.DEFAULT_JITTER_SIZE, row_palette: str | None = StackedViolin.DEFAULT_ROW_PALETTE, - scale: Literal["area", "count", "width"] = StackedViolin.DEFAULT_DENSITY_NORM, + density_norm: Literal["area", "count", "width"] | Empty = _empty, yticklabels: bool = StackedViolin.DEFAULT_PLOT_YTICKLABELS, + # deprecated + scale: Literal["area", "count", "width"] | Empty = _empty, **kwds, ) -> StackedViolin | dict | None: """\ @@ -729,7 +731,7 @@ def stacked_violin( Order in which to show the categories. Note: if `dendrogram=True` the categories order will be given by the dendrogram and `order` will be ignored. - scale + density_norm The method used to scale the width of each violin. If 'width' (the default), each violin will have the same width. If 'area', each violin will have the same area. @@ -834,7 +836,7 @@ def stacked_violin( jitter=jitter, jitter_size=size, row_palette=row_palette, - density_norm=kwds.get("density_norm", scale), + density_norm=_deprecated_scale(density_norm, scale), yticklabels=yticklabels, linewidth=kwds.get("linewidth", _empty), ).legend(title=colorbar_title) From e62b0b1fbbd05eb46f87f250d3262609787b0d11 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 20 Sep 2024 10:01:03 +0200 Subject: [PATCH 19/24] unify categories_order --- src/scanpy/plotting/_dotplot.py | 2 ++ src/scanpy/plotting/_matrixplot.py | 2 ++ src/scanpy/plotting/_stacked_violin.py | 8 ++------ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index caf52756df..74e1aab359 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -852,6 +852,7 @@ def dotplot( use_raw: bool | None = None, log: bool = False, num_categories: int = 7, + categories_order: Sequence[str] | None = None, expression_cutoff: float = 0.0, mean_only_expressed: bool = False, standard_scale: Literal["var", "group"] | None = None, @@ -993,6 +994,7 @@ def dotplot( use_raw=use_raw, log=log, num_categories=num_categories, + categories_order=categories_order, expression_cutoff=expression_cutoff, mean_only_expressed=mean_only_expressed, standard_scale=standard_scale, diff --git a/src/scanpy/plotting/_matrixplot.py b/src/scanpy/plotting/_matrixplot.py index 804af79d44..e3062ddbc8 100644 --- a/src/scanpy/plotting/_matrixplot.py +++ b/src/scanpy/plotting/_matrixplot.py @@ -345,6 +345,7 @@ def matrixplot( use_raw: bool | None = None, log: bool = False, num_categories: int = 7, + categories_order: Sequence[str] | None = None, figsize: tuple[float, float] | None = None, dendrogram: bool | str = False, title: str | None = None, @@ -438,6 +439,7 @@ def matrixplot( use_raw=use_raw, log=log, num_categories=num_categories, + categories_order=categories_order, standard_scale=standard_scale, title=title, figsize=figsize, diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index ed151dfc12..089efd230d 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -680,7 +680,7 @@ def stacked_violin( standard_scale: Literal["var", "obs"] | None = None, var_group_rotation: float | None = None, layer: str | None = None, - order: Sequence[str] | None = None, + categories_order: Sequence[str] | None = None, swap_axes: bool = False, show: bool | None = None, save: bool | str | None = None, @@ -727,10 +727,6 @@ def stacked_violin( See :func:`~seaborn.stripplot`. size Size of the jitter points. - order - Order in which to show the categories. Note: if `dendrogram=True` - the categories order will be given by the dendrogram and `order` - will be ignored. density_norm The method used to scale the width of each violin. If 'width' (the default), each violin will have the same width. @@ -817,7 +813,7 @@ def stacked_violin( var_group_labels=var_group_labels, var_group_rotation=var_group_rotation, layer=layer, - categories_order=order, + categories_order=categories_order, ax=ax, vmin=vmin, vmax=vmax, From 55d8b61f11e7a1dced943ff306eb1d4ba3758be7 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 20 Sep 2024 12:06:48 +0200 Subject: [PATCH 20/24] =?UTF-8?q?Fix=20stacked=5Fviolin=E2=80=99s=20`stand?= =?UTF-8?q?ard=5Fscale`=20parameter=20(#3243)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes/3243.bugfix.md | 1 + src/scanpy/plotting/_stacked_violin.py | 6 +++++- tests/_images/dotplot/expected.png | Bin 13522 -> 13581 bytes tests/_images/dotplot_dict/expected.png | Bin 14613 -> 14753 bytes tests/_images/matrixplot/expected.png | Bin 5623 -> 5577 bytes tests/_images/matrixplot2/expected.png | Bin 7482 -> 7465 bytes .../matrixplot_std_scale_group/expected.png | Bin 6888 -> 6878 bytes tests/_images/stacked_violin/expected.png | Bin 8000 -> 8043 bytes .../stacked_violin_no_cat_obs/expected.png | Bin 13374 -> 13343 bytes .../expected.png | Bin 8440 -> 9009 bytes .../expected.png | Bin 9478 -> 9526 bytes tests/test_plotting.py | 15 +++++++++------ 12 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 docs/release-notes/3243.bugfix.md diff --git a/docs/release-notes/3243.bugfix.md b/docs/release-notes/3243.bugfix.md new file mode 100644 index 0000000000..5aa6063b1e --- /dev/null +++ b/docs/release-notes/3243.bugfix.md @@ -0,0 +1 @@ +Accept `'group'` instead of `'obs'` for `standard_scale` parameter in {func}`~scanpy.pl.stacked_violin` {smaller}`P Angerer` diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index 089efd230d..aa8cfe4329 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -223,6 +223,10 @@ def __init__( ) if standard_scale == "obs": + standard_scale = "group" + msg = "`standard_scale='obs'` is deprecated, use `standard_scale='group'` instead" + warnings.warn(msg, FutureWarning) + if standard_scale == "group": self.obs_tidy = self.obs_tidy.sub(self.obs_tidy.min(1), axis=0) self.obs_tidy = self.obs_tidy.div(self.obs_tidy.max(1), axis=0).fillna(0) elif standard_scale == "var": @@ -677,7 +681,7 @@ def stacked_violin( gene_symbols: str | None = None, var_group_positions: Sequence[tuple[int, int]] | None = None, var_group_labels: Sequence[str] | None = None, - standard_scale: Literal["var", "obs"] | None = None, + standard_scale: Literal["var", "group"] | None = None, var_group_rotation: float | None = None, layer: str | None = None, categories_order: Sequence[str] | None = None, diff --git a/tests/_images/dotplot/expected.png b/tests/_images/dotplot/expected.png index ea54ae3447b9bc422597bef934aa4f1daf849bda..9c4b822369debbebeefbb566ea834406e5a24026 100644 GIT binary patch literal 13581 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 literal 13522 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 E0R%UY2mk;8 literal 14613 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 PQG}BclNT)$*7y5gpGZJ6 diff --git a/tests/_images/matrixplot/expected.png b/tests/_images/matrixplot/expected.png index 216626af46924a32906da451345f493007051cd0..cb2421ba028572646bac2d775ab1a4d5dead7451 100644 GIT binary patch literal 5577 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 literal 5623 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 diff --git a/tests/_images/matrixplot2/expected.png b/tests/_images/matrixplot2/expected.png index b4cdc8dc02a18ff39d5879512576bec60214dd9c..58990e2526363224f2691b3eeb3279cd2db50422 100644 GIT binary patch literal 7465 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 KWZ@IT!2bXZuc_n! diff --git a/tests/_images/matrixplot_std_scale_group/expected.png b/tests/_images/matrixplot_std_scale_group/expected.png index 6d1970b3bb972f9aec1d1e09e63ee2060f043000..05fe80da7daadf6a428588a38e2c2871a4d9ac4e 100644 GIT binary patch literal 6878 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 BLG=It diff --git a/tests/_images/stacked_violin/expected.png b/tests/_images/stacked_violin/expected.png index 8130c0e8d829530332121cd8143bae436f338785..1cadbe8ad7a22ec5155661c09406433f1dd29358 100644 GIT binary patch literal 8043 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 CiS?5J literal 8000 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 F{{u}{c2xiX literal 13374 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 TH4c2%2TD>*PPANDKk)wmRp?DP diff --git a/tests/_images/stacked_violin_std_scale_group/expected.png b/tests/_images/stacked_violin_std_scale_group/expected.png index 963cab263ab1316ff8b0aa67ebea11da277fe98e..af4bf8948b7332bfb45e071465f9519213a6a10e 100644 GIT binary patch literal 9009 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 By37Co literal 8440 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 LFv()^_x}G6B7pK# literal 9478 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 diff --git a/tests/test_plotting.py b/tests/test_plotting.py index b5d194f35d..92eb61c252 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -167,9 +167,9 @@ def test_clustermap(image_comparer, obs_keys, name): save_and_compare_images(name) -@pytest.mark.parametrize( - ("id", "fn"), - [ +params_dotplot_matrixplot_stacked_violin = [ + pytest.param(id, fn, id=id) + for id, fn in [ ( "dotplot", partial( @@ -317,10 +317,13 @@ def test_clustermap(image_comparer, obs_keys, name): figsize=(8, 2.5), ), ), - ], -) + ] +] + + +@pytest.mark.parametrize(("id", "fn"), params_dotplot_matrixplot_stacked_violin) def test_dotplot_matrixplot_stacked_violin(image_comparer, id, fn): - save_and_compare_images = partial(image_comparer, ROOT, tol=15) + save_and_compare_images = partial(image_comparer, ROOT, tol=5) adata = krumsiek11() adata.obs["numeric_column"] = adata.X[:, 0] From 3b40cb2fe30c1651ebe9a8b28884c748305acbbf Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 20 Sep 2024 12:10:57 +0200 Subject: [PATCH 21/24] standard_scale type --- src/scanpy/plotting/_stacked_violin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index aa8cfe4329..1373b8f6cb 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -190,7 +190,7 @@ def __init__( var_group_labels: Sequence[str] | None = None, var_group_rotation: float | None = None, layer: str | None = None, - standard_scale: Literal["var", "obs"] | None = None, + standard_scale: Literal["var", "group"] | None = None, ax: _AxesSubplot | None = None, vmin: float | None = None, vmax: float | None = None, From 2d0990908541fa391d9669c0087665d696fa8f8e Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 20 Sep 2024 15:16:48 +0200 Subject: [PATCH 22/24] oops --- src/scanpy/plotting/_baseplot_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scanpy/plotting/_baseplot_class.py b/src/scanpy/plotting/_baseplot_class.py index 4e0e10d503..6e5c8cd2c5 100644 --- a/src/scanpy/plotting/_baseplot_class.py +++ b/src/scanpy/plotting/_baseplot_class.py @@ -240,7 +240,7 @@ def add_dendrogram( self, *, show: bool | None = True, - dendrogram_key: bool | str | None = None, + dendrogram_key: str | None = None, size: float | None = 0.8, ) -> Self: r"""\ From fe1ab429d273a6dcf6a3f3381b9cffc28b935eab Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 20 Sep 2024 16:18:25 +0200 Subject: [PATCH 23/24] fix type --- src/scanpy/plotting/_stacked_violin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index 66e382f5a7..691dd863d0 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -701,7 +701,7 @@ def stacked_violin( jitter: float | bool = StackedViolin.DEFAULT_JITTER, size: int | float = StackedViolin.DEFAULT_JITTER_SIZE, row_palette: str | None = StackedViolin.DEFAULT_ROW_PALETTE, - density_norm: Literal["area", "count", "width"] | Empty = _empty, + density_norm: DensityNorm | Empty = _empty, yticklabels: bool = StackedViolin.DEFAULT_PLOT_YTICKLABELS, # deprecated order: Sequence[str] | None | Empty = _empty, From 297ed3070eaa2d67e0a1ceeed9b9b3cf74345061 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 20 Sep 2024 17:09:45 +0200 Subject: [PATCH 24/24] fix cmap type --- src/scanpy/external/pl.py | 4 +++- src/scanpy/plotting/_dotplot.py | 2 +- src/scanpy/plotting/_stacked_violin.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/scanpy/external/pl.py b/src/scanpy/external/pl.py index 662bc88eb3..f387082476 100644 --- a/src/scanpy/external/pl.py +++ b/src/scanpy/external/pl.py @@ -25,6 +25,8 @@ from collections.abc import Collection from typing import Any + from matplotlib.colors import Colormap + __all__ = [ "phate", @@ -166,7 +168,7 @@ def sam( projection: str | np.ndarray = "X_umap", *, c: str | np.ndarray | None = None, - cmap: str = "Spectral_r", + cmap: Colormap | str | None = "Spectral_r", linewidth: float = 0.0, edgecolor: str = "k", axes: Axes | None = None, diff --git a/src/scanpy/plotting/_dotplot.py b/src/scanpy/plotting/_dotplot.py index 3919a84a52..051e16a812 100644 --- a/src/scanpy/plotting/_dotplot.py +++ b/src/scanpy/plotting/_dotplot.py @@ -107,7 +107,7 @@ class DotPlot(BasePlot): dot_size_df: pd.DataFrame | None = None # default style parameters - cmap: str | None = "Reds" # override BasePlot default + cmap: Colormap | str | None = "Reds" # override BasePlot default color_on: Literal["dot", "square"] = "dot" dot_max: float | None = None dot_min: float | None = None diff --git a/src/scanpy/plotting/_stacked_violin.py b/src/scanpy/plotting/_stacked_violin.py index f3f3c66a60..18496fa214 100644 --- a/src/scanpy/plotting/_stacked_violin.py +++ b/src/scanpy/plotting/_stacked_violin.py @@ -120,7 +120,7 @@ class StackedViolin(BasePlot): # overrides color_legend_title: str = "Median expression\nin group" - cmap: str | None = "Blues" + cmap: Colormap | str | None = "Blues" # style parameters row_palette: str | None = None