diff --git a/src/spatialdata_plot/pl/basic.py b/src/spatialdata_plot/pl/basic.py index d34d9fe4..d6216341 100644 --- a/src/spatialdata_plot/pl/basic.py +++ b/src/spatialdata_plot/pl/basic.py @@ -1,10 +1,11 @@ from __future__ import annotations +import contextlib import sys from collections import OrderedDict from copy import deepcopy from pathlib import Path -from typing import Any, Literal +from typing import Any, Literal, cast import matplotlib.pyplot as plt import numpy as np @@ -15,8 +16,10 @@ from dask.dataframe import DataFrame as DaskDataFrame from geopandas import GeoDataFrame from matplotlib.axes import Axes +from matplotlib.backend_bases import RendererBase from matplotlib.colors import Colormap, Normalize from matplotlib.figure import Figure +from mpl_toolkits.axes_grid1.inset_locator import inset_axes from spatialdata import get_extent from spatialdata._utils import _deprecation_alias from xarray import DataArray, DataTree @@ -28,9 +31,14 @@ _render_labels, _render_points, _render_shapes, + _split_colorbar_params, ) from spatialdata_plot.pl.render_params import ( + CBAR_DEFAULT_FRACTION, + CBAR_DEFAULT_LOCATION, + CBAR_DEFAULT_PAD, CmapParams, + ColorbarSpec, ImageRenderParams, LabelsRenderParams, LegendParams, @@ -172,6 +180,8 @@ def render_shapes( table_name: str | None = None, table_layer: str | None = None, shape: Literal["circle", "hex", "visium_hex", "square"] | None = None, + colorbar: bool | str | None = "auto", + colorbar_params: dict[str, object] | None = None, **kwargs: Any, ) -> sd.SpatialData: """ @@ -237,6 +247,11 @@ def render_shapes( method : str | None, optional Whether to use 'matplotlib' and 'datashader'. When None, the method is chosen based on the size of the data. + colorbar : + Whether to request a colorbar for continuous colors. Use "auto" (default) for automatic selection. + colorbar_params : + Parameters forwarded to Matplotlib's colorbar alongside layout hints such as ``loc``, ``width``, ``pad``, + and ``label``. table_name: str | None Name of the table containing the color(s) columns. If one name is given than the table is used for each spatial element to be plotted if the table annotates it. If you want to use different tables for particular @@ -292,6 +307,8 @@ def render_shapes( shape=shape, method=method, ds_reduction=kwargs.get("datashader_reduction"), + colorbar=colorbar, + colorbar_params=colorbar_params, ) sdata = self._copy() @@ -326,6 +343,8 @@ def render_shapes( zorder=n_steps, method=param_values["method"], ds_reduction=param_values["ds_reduction"], + colorbar=param_values["colorbar"], + colorbar_params=param_values["colorbar_params"], ) n_steps += 1 @@ -347,6 +366,8 @@ def render_points( method: str | None = None, table_name: str | None = None, table_layer: str | None = None, + colorbar: bool | str | None = "auto", + colorbar_params: dict[str, object] | None = None, **kwargs: Any, ) -> sd.SpatialData: """ @@ -396,6 +417,11 @@ def render_points( method : str | None, optional Whether to use 'matplotlib' and 'datashader'. When None, the method is chosen based on the size of the data. + colorbar : + Whether to request a colorbar for continuous colors. Use "auto" (default) for automatic selection. + colorbar_params : + Parameters forwarded to Matplotlib's colorbar alongside layout hints such as ``loc``, ``width``, ``pad``, + and ``label``. table_name: str | None Name of the table containing the color(s) columns. If one name is given than the table is used for each spatial element to be plotted if the table annotates it. If you want to use different tables for particular @@ -434,6 +460,8 @@ def render_points( table_name=table_name, table_layer=table_layer, ds_reduction=kwargs.get("datashader_reduction"), + colorbar=colorbar, + colorbar_params=colorbar_params, ) if method is not None: @@ -467,6 +495,8 @@ def render_points( zorder=n_steps, method=method, ds_reduction=param_values["ds_reduction"], + colorbar=param_values["colorbar"], + colorbar_params=param_values["colorbar_params"], ) n_steps += 1 @@ -484,6 +514,8 @@ def render_images( palette: list[str] | str | None = None, alpha: float | int = 1.0, scale: str | None = None, + colorbar: bool | str | None = "auto", + colorbar_params: dict[str, object] | None = None, **kwargs: Any, ) -> sd.SpatialData: """ @@ -526,6 +558,11 @@ def render_images( 3) "full": Renders the full image without rasterization. In the case of multiscale images, the highest resolution scale is selected. Note that this may result in long computing times for large images. + colorbar : + Whether to request a colorbar for continuous colors. Use "auto" (default) for automatic selection. + colorbar_params : + Parameters forwarded to Matplotlib's colorbar alongside layout hints such as ``loc``, ``width``, ``pad``, + and ``label``. kwargs Additional arguments to be passed to cmap, norm, and other rendering functions. @@ -547,6 +584,8 @@ def render_images( cmap=cmap, norm=norm, scale=scale, + colorbar=colorbar, + colorbar_params=colorbar_params, ) sdata = self._copy() @@ -580,6 +619,8 @@ def render_images( alpha=param_values["alpha"], scale=param_values["scale"], zorder=n_steps, + colorbar=param_values["colorbar"], + colorbar_params=param_values["colorbar_params"], ) n_steps += 1 @@ -600,6 +641,8 @@ def render_labels( outline_alpha: float | int = 0.0, fill_alpha: float | int = 0.4, scale: str | None = None, + colorbar: bool | str | None = "auto", + colorbar_params: dict[str, object] | None = None, table_name: str | None = None, table_layer: str | None = None, **kwargs: Any, @@ -653,6 +696,11 @@ def render_labels( (exception: a dpi is specified in `show()`. Then the image is rasterized to fit the canvas and dpi). 3) "full": render the full image without rasterization. In the case of a multiscale image, the scale with the highest resolution is selected. This can lead to long computing times for large images! + colorbar : + Whether to request a colorbar for continuous colors. Use "auto" (default) for automatic selection. + colorbar_params : + Parameters forwarded to Matplotlib's colorbar alongside layout hints such as ``loc``, ``width``, ``pad``, + and ``label``. table_name: str | None Name of the table containing the color columns. table_layer: str | None @@ -681,6 +729,8 @@ def render_labels( outline_alpha=outline_alpha, palette=palette, scale=scale, + colorbar=colorbar, + colorbar_params=colorbar_params, table_name=table_name, table_layer=table_layer, ) @@ -709,6 +759,8 @@ def render_labels( table_name=param_values["table_name"], table_layer=param_values["table_layer"], zorder=n_steps, + colorbar=param_values["colorbar"], + colorbar_params=param_values["colorbar_params"], ) n_steps += 1 return sdata @@ -723,6 +775,7 @@ def show( legend_fontoutline: int | None = None, na_in_legend: bool = True, colorbar: bool = True, + colorbar_params: dict[str, object] | None = None, wspace: float | None = None, hspace: float = 0.25, ncols: int = 4, @@ -761,7 +814,10 @@ def show( return_ax : Whether to return the axes object created. False by default. colorbar : - Whether to plot the colorbar. True by default. + Global switch to enable/disable all colorbars. Per-layer settings are ignored when this is False. + colorbar_params : + Global overrides passed to colorbars for all axes. Accepts the same keys as per-layer ``colorbar_params`` + (e.g., ``loc``, ``width``, ``pad``, ``label``). title : The title of the plot. If not provided the plot will have the name of the coordinate system as title. @@ -786,6 +842,7 @@ def show( legend_fontoutline, na_in_legend, colorbar, + colorbar_params, wspace, hspace, ncols, @@ -852,6 +909,7 @@ def show( # Check if user specified only certain elements to be plotted cs_contents = _get_cs_contents(sdata) + pending_colorbars: list[tuple[Axes, list[ColorbarSpec]]] = [] elements_to_be_rendered = _get_elements_to_be_rendered(render_cmds, cs_contents, cs) @@ -888,15 +946,94 @@ def show( ncols=ncols, frameon=frameon, ) + legend_colorbar = colorbar legend_params = LegendParams( legend_fontsize=legend_fontsize, legend_fontweight=legend_fontweight, legend_loc=legend_loc, legend_fontoutline=legend_fontoutline, na_in_legend=na_in_legend, - colorbar=colorbar, + colorbar=legend_colorbar, ) + def _draw_colorbar( + spec: ColorbarSpec, + fig: Figure, + renderer: RendererBase, + base_offsets_axes: dict[str, float], + trackers_axes: dict[str, float], + ) -> None: + base_layout = { + "location": CBAR_DEFAULT_LOCATION, + "fraction": CBAR_DEFAULT_FRACTION, + "pad": CBAR_DEFAULT_PAD, + } + layer_layout, layer_kwargs, layer_label_override = _split_colorbar_params(spec.params) + global_layout, global_kwargs, global_label_override = _split_colorbar_params(colorbar_params) + layout = {**base_layout, **layer_layout, **global_layout} + cbar_kwargs = {**layer_kwargs, **global_kwargs} + + location = cast(str, layout.get("location", base_layout["location"])) + if location not in {"left", "right", "top", "bottom"}: + location = CBAR_DEFAULT_LOCATION + default_orientation = "vertical" if location in {"right", "left"} else "horizontal" + cbar_kwargs.setdefault("orientation", default_orientation) + + fraction = float(cast(float | int, layout.get("fraction", base_layout["fraction"]))) + pad = float(cast(float | int, layout.get("pad", base_layout["pad"]))) + + if location in {"left", "right"}: + pad_axes = pad + trackers_axes[location] + x0 = -pad_axes - fraction if location == "left" else 1 + pad_axes + bbox = (float(x0), 0.0, float(fraction), 1.0) + else: + pad_axes = pad + trackers_axes[location] + y0 = -pad_axes - fraction if location == "bottom" else 1 + pad_axes + bbox = (0.0, float(y0), 1.0, float(fraction)) + cax = inset_axes( + spec.ax, + width="100%", + height="100%", + loc="center", + bbox_to_anchor=bbox, + bbox_transform=spec.ax.transAxes, + borderpad=0.0, + ) + + cb = fig.colorbar(spec.mappable, cax=cax, **cbar_kwargs) + if location == "left": + cb.ax.yaxis.set_ticks_position("left") + cb.ax.yaxis.set_label_position("left") + cb.ax.tick_params(labelleft=True, labelright=False) + elif location == "top": + cb.ax.xaxis.set_ticks_position("top") + cb.ax.xaxis.set_label_position("top") + cb.ax.tick_params(labeltop=True, labelbottom=False) + elif location == "right": + cb.ax.yaxis.set_ticks_position("right") + cb.ax.yaxis.set_label_position("right") + cb.ax.tick_params(labelright=True, labelleft=False) + elif location == "bottom": + cb.ax.xaxis.set_ticks_position("bottom") + cb.ax.xaxis.set_label_position("bottom") + cb.ax.tick_params(labelbottom=True, labeltop=False) + + final_label = global_label_override or layer_label_override or spec.label + if final_label: + cb.set_label(final_label) + if spec.alpha is not None: + with contextlib.suppress(Exception): + cb.solids.set_alpha(spec.alpha) + bbox_axes = cb.ax.get_tightbbox(renderer).transformed(spec.ax.transAxes.inverted()) + if location == "left": + trackers_axes["left"] = pad_axes + bbox_axes.width + elif location == "right": + trackers_axes["right"] = pad_axes + bbox_axes.width + elif location == "bottom": + trackers_axes["bottom"] = pad_axes + bbox_axes.height + elif location == "top": + trackers_axes["top"] = pad_axes + bbox_axes.height + cs_contents = _get_cs_contents(sdata) # go through tree @@ -908,6 +1045,7 @@ def show( ) ax = fig_params.ax if fig_params.axs is None else fig_params.axs[i] assert isinstance(ax, Axes) + axis_colorbar_requests: list[ColorbarSpec] | None = [] if legend_params.colorbar else None wants_images = False wants_labels = False @@ -937,6 +1075,7 @@ def show( fig_params=fig_params, scalebar_params=scalebar_params, legend_params=legend_params, + colorbar_requests=axis_colorbar_requests, rasterize=rasterize, ) @@ -954,6 +1093,7 @@ def show( fig_params=fig_params, scalebar_params=scalebar_params, legend_params=legend_params, + colorbar_requests=axis_colorbar_requests, ) elif cmd == "render_points" and has_points: @@ -970,6 +1110,7 @@ def show( fig_params=fig_params, scalebar_params=scalebar_params, legend_params=legend_params, + colorbar_requests=axis_colorbar_requests, ) elif cmd == "render_labels" and has_labels: @@ -978,7 +1119,8 @@ def show( ) if wanted_labels_on_this_cs: - if (table := params_copy.table_name) is not None: + table = params_copy.table_name + if table is not None: assert isinstance(params_copy.color, str) colors = sc.get.obs_df(sdata[table], [params_copy.color]) if isinstance(colors[params_copy.color].dtype, pd.CategoricalDtype): @@ -1002,6 +1144,7 @@ def show( fig_params=fig_params, scalebar_params=scalebar_params, legend_params=legend_params, + colorbar_requests=axis_colorbar_requests, rasterize=rasterize, ) @@ -1038,6 +1181,33 @@ def show( ax.set_xlim(x_min, x_max) ax.set_ylim(y_max, y_min) # (0, 0) is top-left + if legend_params.colorbar and axis_colorbar_requests: + pending_colorbars.append((ax, axis_colorbar_requests)) + + if pending_colorbars and fig_params.fig is not None: + fig = fig_params.fig + fig.canvas.draw() + renderer = fig.canvas.get_renderer() + for axis, requests in pending_colorbars: + unique_specs: list[ColorbarSpec] = [] + seen_mappables: set[int] = set() + for spec in requests: + mappable_id = id(spec.mappable) + if mappable_id in seen_mappables: + continue + seen_mappables.add(mappable_id) + unique_specs.append(spec) + tight_bbox = axis.get_tightbbox(renderer).transformed(axis.transAxes.inverted()) + base_offsets_axes = { + "left": max(0.0, -tight_bbox.x0), + "right": max(0.0, tight_bbox.x1 - 1), + "bottom": max(0.0, -tight_bbox.y0), + "top": max(0.0, tight_bbox.y1 - 1), + } + trackers_axes = {k: base_offsets_axes[k] for k in base_offsets_axes} + for spec in unique_specs: + _draw_colorbar(spec, fig, renderer, base_offsets_axes, trackers_axes) + if fig_params.fig is not None and save is not None: save_fig(fig_params.fig, path=save) diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index 53c1033c..2d607fe9 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -26,6 +26,7 @@ from spatialdata_plot._logging import logger from spatialdata_plot.pl.render_params import ( Color, + ColorbarSpec, FigParams, ImageRenderParams, LabelsRenderParams, @@ -61,6 +62,55 @@ _Normalize = Normalize | abc.Sequence[Normalize] +def _split_colorbar_params(params: dict[str, object] | None) -> tuple[dict[str, object], dict[str, object], str | None]: + """Split colorbar params into layout hints, Matplotlib kwargs, and label override.""" + layout: dict[str, object] = {} + cbar_kwargs: dict[str, object] = {} + label_override: str | None = None + for key, value in (params or {}).items(): + key_lower = key.lower() + if key_lower in {"loc", "location"}: + layout["location"] = value + elif key_lower == "width" or key_lower == "fraction": + layout["fraction"] = value + elif key_lower == "pad": + layout["pad"] = value + elif key_lower == "label": + label_override = None if value is None else str(value) + else: + cbar_kwargs[key] = value + return layout, cbar_kwargs, label_override + + +def _resolve_colorbar_label( + colorbar_params: dict[str, object] | None, fallback: str | None, *, is_default_channel_name: bool = False +) -> str | None: + """Pick a colorbar label from params or fall back to provided value.""" + _, _, label = _split_colorbar_params(colorbar_params) + if label is not None: + return label + if is_default_channel_name: + return None + return fallback + + +def _should_request_colorbar( + colorbar: bool | str | None, + *, + has_mappable: bool, + is_continuous: bool, + auto_condition: bool = True, +) -> bool: + """Resolve colorbar setting to a final boolean request.""" + if not has_mappable or not is_continuous: + return False + if colorbar is True: + return True + if colorbar in {False, None}: + return False + return bool(auto_condition) + + def _render_shapes( sdata: sd.SpatialData, render_params: ShapesRenderParams, @@ -69,6 +119,7 @@ def _render_shapes( fig_params: FigParams, scalebar_params: ScalebarParams, legend_params: LegendParams, + colorbar_requests: list[ColorbarSpec] | None = None, ) -> None: element = render_params.element col_for_color = render_params.col_for_color @@ -80,7 +131,8 @@ def _render_shapes( filter_tables=bool(render_params.table_name), ) - if (table_name := render_params.table_name) is None: + table_name = render_params.table_name + if table_name is None: table = None shapes = sdata_filt[element] else: @@ -159,16 +211,13 @@ def _render_shapes( else: palette = ListedColormap(dict.fromkeys(color_vector[~pd.Categorical(color_source_vector).isnull()])) - if ( + has_valid_color = ( len(set(color_vector)) != 1 or list(set(color_vector))[0] != render_params.cmap_params.na_color.get_hex_with_alpha() - ): + ) + if has_valid_color and color_source_vector is not None and col_for_color is not None: # necessary in case different shapes elements are annotated with one table - if color_source_vector is not None and col_for_color is not None: - color_source_vector = color_source_vector.remove_unused_categories() - - # False if user specified color-like with 'color' parameter - colorbar = False if col_for_color is None else legend_params.colorbar + color_source_vector = color_source_vector.remove_unused_categories() # Apply the transformation to the PatchCollection's paths trans, trans_data = _prepare_transformation(sdata_filt.shapes[element], coordinate_system) @@ -515,8 +564,11 @@ def _render_shapes( if color_source_vector is not None and render_params.col_for_color is not None: color_source_vector = color_source_vector.remove_unused_categories() - # False if user specified color-like with 'color' parameter - colorbar = False if render_params.col_for_color is None else legend_params.colorbar + wants_colorbar = _should_request_colorbar( + render_params.colorbar, + has_mappable=cax is not None, + is_continuous=render_params.col_for_color is not None and color_source_vector is None, + ) _ = _decorate_axs( ax=ax, @@ -534,7 +586,13 @@ def _render_shapes( legend_loc=legend_params.legend_loc, legend_fontoutline=legend_params.legend_fontoutline, na_in_legend=legend_params.na_in_legend, - colorbar=colorbar, + colorbar=wants_colorbar and legend_params.colorbar, + colorbar_params=render_params.colorbar_params, + colorbar_requests=colorbar_requests, + colorbar_label=_resolve_colorbar_label( + render_params.colorbar_params, + col_for_color if isinstance(col_for_color, str) else None, + ), scalebar_dx=scalebar_params.scalebar_dx, scalebar_units=scalebar_params.scalebar_units, ) @@ -548,6 +606,7 @@ def _render_points( fig_params: FigParams, scalebar_params: ScalebarParams, legend_params: LegendParams, + colorbar_requests: list[ColorbarSpec] | None = None, ) -> None: element = render_params.element col_for_color = render_params.col_for_color @@ -894,6 +953,12 @@ def _render_points( else: palette = ListedColormap(dict.fromkeys(color_vector[~pd.Categorical(color_source_vector).isnull()])) + wants_colorbar = _should_request_colorbar( + render_params.colorbar, + has_mappable=cax is not None, + is_continuous=col_for_color is not None and color_source_vector is None, + ) + _ = _decorate_axs( ax=ax, cax=cax, @@ -910,7 +975,13 @@ def _render_points( legend_loc=legend_params.legend_loc, legend_fontoutline=legend_params.legend_fontoutline, na_in_legend=legend_params.na_in_legend, - colorbar=legend_params.colorbar, + colorbar=wants_colorbar and legend_params.colorbar, + colorbar_params=render_params.colorbar_params, + colorbar_requests=colorbar_requests, + colorbar_label=_resolve_colorbar_label( + render_params.colorbar_params, + col_for_color if isinstance(col_for_color, str) else None, + ), scalebar_dx=scalebar_params.scalebar_dx, scalebar_units=scalebar_params.scalebar_units, ) @@ -925,6 +996,7 @@ def _render_images( scalebar_params: ScalebarParams, legend_params: LegendParams, rasterize: bool, + colorbar_requests: list[ColorbarSpec] | None = None, ) -> None: sdata_filt = sdata.filter_by_coordinate_system( coordinate_system=coordinate_system, @@ -1003,9 +1075,26 @@ def _render_images( norm=render_params.cmap_params.norm, ) - if legend_params.colorbar: + wants_colorbar = _should_request_colorbar( + render_params.colorbar, + has_mappable=n_channels == 1, + is_continuous=True, + auto_condition=n_channels == 1, + ) + if wants_colorbar and legend_params.colorbar and colorbar_requests is not None: sm = plt.cm.ScalarMappable(cmap=cmap, norm=render_params.cmap_params.norm) - fig_params.fig.colorbar(sm, ax=ax) + colorbar_requests.append( + ColorbarSpec( + ax=ax, + mappable=sm, + params=render_params.colorbar_params, + label=_resolve_colorbar_label( + render_params.colorbar_params, + str(channels[0]), + is_default_channel_name=isinstance(channels[0], (int, np.integer)), + ), + ) + ) # 2) Image has any number of channels but 1 else: @@ -1165,6 +1254,7 @@ def _render_labels( scalebar_params: ScalebarParams, legend_params: LegendParams, rasterize: bool, + colorbar_requests: list[ColorbarSpec] | None = None, ) -> None: element = render_params.element table_name = render_params.table_name @@ -1310,6 +1400,12 @@ def _draw_labels(seg_erosionpx: int | None, seg_boundaries: bool, alpha: float) else: raise ValueError("Parameters 'fill_alpha' and 'outline_alpha' cannot both be 0.") + colorbar_requested = _should_request_colorbar( + render_params.colorbar, + has_mappable=cax is not None, + is_continuous=color is not None and color_source_vector is None and not categorical, + ) + _ = _decorate_axs( ax=ax, cax=cax, @@ -1326,7 +1422,13 @@ def _draw_labels(seg_erosionpx: int | None, seg_boundaries: bool, alpha: float) legend_loc=legend_params.legend_loc, legend_fontoutline=legend_params.legend_fontoutline, na_in_legend=(legend_params.na_in_legend if groups is None else len(groups) == len(set(color_vector))), - colorbar=legend_params.colorbar, + colorbar=colorbar_requested and legend_params.colorbar, + colorbar_params=render_params.colorbar_params, + colorbar_requests=colorbar_requests, + colorbar_label=_resolve_colorbar_label( + render_params.colorbar_params, + color if isinstance(color, str) else None, + ), scalebar_dx=scalebar_params.scalebar_dx, scalebar_units=scalebar_params.scalebar_units, # scalebar_kwargs=scalebar_params.scalebar_kwargs, diff --git a/src/spatialdata_plot/pl/render_params.py b/src/spatialdata_plot/pl/render_params.py index a8382753..4936468f 100644 --- a/src/spatialdata_plot/pl/render_params.py +++ b/src/spatialdata_plot/pl/render_params.py @@ -6,6 +6,7 @@ import numpy as np from matplotlib.axes import Axes +from matplotlib.cm import ScalarMappable from matplotlib.colors import Colormap, ListedColormap, Normalize, rgb2hex, to_hex from matplotlib.figure import Figure @@ -183,6 +184,22 @@ class LegendParams: colorbar: bool = True +@dataclass +class ColorbarSpec: + """Data required to create a colorbar.""" + + ax: Axes + mappable: ScalarMappable + params: dict[str, object] | None = None + label: str | None = None + alpha: float | None = None + + +CBAR_DEFAULT_LOCATION = "right" +CBAR_DEFAULT_FRACTION = 0.075 +CBAR_DEFAULT_PAD = 0.015 + + @dataclass class ScalebarParams: """Scalebar params.""" @@ -213,6 +230,8 @@ class ShapesRenderParams: table_layer: str | None = None shape: Literal["circle", "hex", "visium_hex", "square"] | None = None ds_reduction: Literal["sum", "mean", "any", "count", "std", "var", "max", "min"] | None = None + colorbar: bool | str | None = "auto" + colorbar_params: dict[str, object] | None = None @dataclass @@ -233,6 +252,8 @@ class PointsRenderParams: table_name: str | None = None table_layer: str | None = None ds_reduction: Literal["sum", "mean", "any", "count", "std", "var", "max", "min"] | None = None + colorbar: bool | str | None = "auto" + colorbar_params: dict[str, object] | None = None @dataclass @@ -247,6 +268,8 @@ class ImageRenderParams: percentiles_for_norm: tuple[float | None, float | None] = (None, None) scale: str | None = None zorder: int = 0 + colorbar: bool | str | None = "auto" + colorbar_params: dict[str, object] | None = None @dataclass @@ -267,3 +290,5 @@ class LabelsRenderParams: table_name: str | None = None table_layer: str | None = None zorder: int = 0 + colorbar: bool | str | None = "auto" + colorbar_params: dict[str, object] | None = None diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 97ffac1d..11d5d10d 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -75,6 +75,7 @@ from spatialdata_plot.pl.render_params import ( CmapParams, Color, + ColorbarSpec, FigParams, ImageRenderParams, LabelsRenderParams, @@ -1525,6 +1526,9 @@ def _decorate_axs( legend_fontoutline: int | None = None, na_in_legend: bool = True, colorbar: bool = True, + colorbar_params: dict[str, object] | None = None, + colorbar_requests: list[ColorbarSpec] | None = None, + colorbar_label: str | None = None, scalebar_dx: Sequence[float] | None = None, scalebar_units: Sequence[str] | None = None, scalebar_kwargs: Mapping[str, Any] = MappingProxyType({}), @@ -1563,10 +1567,16 @@ def _decorate_axs( na_in_legend=na_in_legend, multi_panel=fig_params.axs is not None, ) - elif colorbar: - # TODO: na_in_legend should have some effect here - cb = plt.colorbar(cax, ax=ax, pad=0.01, fraction=0.08, aspect=30) - cb.solids.set_alpha(alpha) + elif colorbar and colorbar_requests is not None and cax is not None: + colorbar_requests.append( + ColorbarSpec( + ax=ax, + mappable=cax, + params=colorbar_params, + label=colorbar_label, + alpha=alpha, + ) + ) if isinstance(scalebar_dx, list) and isinstance(scalebar_units, list): scalebar = ScaleBar(scalebar_dx, units=scalebar_units, **scalebar_kwargs) @@ -1976,6 +1986,7 @@ def _validate_show_parameters( legend_fontoutline: int | None, na_in_legend: bool, colorbar: bool, + colorbar_params: dict[str, object] | None, wspace: float | None, hspace: float, ncols: int, @@ -2036,6 +2047,9 @@ def _validate_show_parameters( if not isinstance(colorbar, bool): raise TypeError("Parameter 'colorbar' must be a boolean.") + if colorbar_params is not None and not isinstance(colorbar_params, dict): + raise TypeError("Parameter 'colorbar_params' must be a dictionary or None.") + if wspace is not None and not isinstance(wspace, float): raise TypeError("Parameter 'wspace' must be a float.") @@ -2077,7 +2091,16 @@ def _validate_show_parameters( def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[str, Any]: - if (element := param_dict.get("element")) is not None and not isinstance(element, str): + colorbar = param_dict.get("colorbar", "auto") + if colorbar not in {True, False, None, "auto"}: + raise TypeError("Parameter 'colorbar' must be one of True, False or 'auto'.") + + colorbar_params = param_dict.get("colorbar_params") + if colorbar_params is not None and not isinstance(colorbar_params, dict): + raise TypeError("Parameter 'colorbar_params' must be a dictionary or None.") + + element = param_dict.get("element") + if element is not None and not isinstance(element, str): raise ValueError( "Parameter 'element' must be a string. If you want to display more elements, pass `element` " "as `None` or chain pl.render(...).pl.render(...).pl.show()" @@ -2091,7 +2114,8 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st elif element_type == "shapes": param_dict["element"] = [element] if element is not None else list(param_dict["sdata"].shapes.keys()) - if (channel := param_dict.get("channel")) is not None and not isinstance(channel, list | str | int): + channel = param_dict.get("channel") + if channel is not None and not isinstance(channel, list | str | int): raise TypeError("Parameter 'channel' must be a string, an integer, or a list of strings or integers.") if isinstance(channel, list): if not all(isinstance(c, str | int) for c in channel): @@ -2102,10 +2126,12 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st elif "channel" in param_dict: param_dict["channel"] = [channel] if channel is not None else None - if (contour_px := param_dict.get("contour_px")) and not isinstance(contour_px, int): + contour_px = param_dict.get("contour_px") + if contour_px and not isinstance(contour_px, int): raise TypeError("Parameter 'contour_px' must be an integer.") - if (color := param_dict.get("color")) and element_type in { + color = param_dict.get("color") + if color and element_type in { "shapes", "points", "labels", @@ -2135,7 +2161,8 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st elif "color" in param_dict and element_type != "labels": param_dict["col_for_color"] = None - if outline_width := param_dict.get("outline_width"): + outline_width = param_dict.get("outline_width") + if outline_width: # outline_width only exists for shapes at the moment if isinstance(outline_width, tuple): for ow in outline_width: @@ -2149,7 +2176,8 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st if isinstance(outline_width, float | int) and outline_width < 0: raise ValueError("Parameter 'outline_width' cannot be negative.") - if outline_alpha := param_dict.get("outline_alpha"): + outline_alpha = param_dict.get("outline_alpha") + if outline_alpha: if isinstance(outline_alpha, tuple): if element_type != "shapes": raise ValueError("Parameter 'outline_alpha' must be a single numeric.") @@ -2176,7 +2204,8 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st elif not isinstance(outline_alpha, float | int) or not 0 <= outline_alpha <= 1: raise TypeError("Parameter 'outline_alpha' must be numeric and between 0 and 1.") - if outline_color := param_dict.get("outline_color"): + outline_color = param_dict.get("outline_color") + if outline_color: if not isinstance(outline_color, str | tuple | list): raise TypeError("Parameter 'color' must be a string or a tuple/list of floats or colors.") if isinstance(outline_color, tuple | list): @@ -2201,7 +2230,8 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st if contour_px is not None and contour_px <= 0: raise ValueError("Parameter 'contour_px' must be a positive number.") - if (alpha := param_dict.get("alpha")) is not None: + alpha = param_dict.get("alpha") + if alpha is not None: if not isinstance(alpha, float | int): raise TypeError("Parameter 'alpha' must be numeric.") if not 0 <= alpha <= 1: @@ -2210,7 +2240,8 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st # set default alpha for points if not given by user explicitly or implicitly (as part of color) param_dict["alpha"] = 1.0 - if (fill_alpha := param_dict.get("fill_alpha")) is not None: + fill_alpha = param_dict.get("fill_alpha") + if fill_alpha is not None: if not isinstance(fill_alpha, float | int): raise TypeError("Parameter 'fill_alpha' must be numeric.") if fill_alpha < 0: @@ -2219,11 +2250,14 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st # set default fill_alpha for shapes if not given by user explicitly or implicitly (as part of color) param_dict["fill_alpha"] = 1.0 - if (cmap := param_dict.get("cmap")) is not None and (palette := param_dict.get("palette")) is not None: + cmap = param_dict.get("cmap") + palette = param_dict.get("palette") + if cmap is not None and palette is not None: raise ValueError("Both `palette` and `cmap` are specified. Please specify only one of them.") param_dict["cmap"] = cmap - if (groups := param_dict.get("groups")) is not None: + groups = param_dict.get("groups") + if groups is not None: if not isinstance(groups, list | str): raise TypeError("Parameter 'groups' must be a string or a list of strings.") if isinstance(groups, str): @@ -2233,19 +2267,20 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st palette = param_dict["palette"] - if isinstance((palette := param_dict["palette"]), list): + if isinstance(palette, list): if not all(isinstance(p, str) for p in palette): raise ValueError("If specified, parameter 'palette' must contain only strings.") elif isinstance(palette, str | type(None)) and "palette" in param_dict: param_dict["palette"] = [palette] if palette is not None else None - if element_type in ["shapes", "points", "labels"] and (palette := param_dict.get("palette")) is not None: + palette_group = param_dict.get("palette") + if element_type in ["shapes", "points", "labels"] and palette_group is not None: groups = param_dict.get("groups") if groups is None: raise ValueError("When specifying 'palette', 'groups' must also be specified.") - if len(groups) != len(palette): + if len(groups) != len(palette_group): raise ValueError( - f"The length of 'palette' and 'groups' must be the same, length is {len(palette)} and" + f"The length of 'palette' and 'groups' must be the same, length is {len(palette_group)} and" f"{len(groups)} respectively." ) @@ -2261,13 +2296,15 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st # validation happens within Color constructor param_dict["na_color"] = Color(param_dict.get("na_color")) - if (norm := param_dict.get("norm")) is not None: + norm = param_dict.get("norm") + if norm is not None: if element_type in {"images", "labels"} and not isinstance(norm, Normalize): raise TypeError("Parameter 'norm' must be of type Normalize.") if element_type in {"shapes", "points"} and not isinstance(norm, bool | Normalize): raise TypeError("Parameter 'norm' must be a boolean or a mpl.Normalize.") - if (scale := param_dict.get("scale")) is not None: + scale = param_dict.get("scale") + if scale is not None: if element_type in {"images", "labels"} and not isinstance(scale, str): raise TypeError("Parameter 'scale' must be a string if specified.") if element_type == "shapes": @@ -2276,13 +2313,15 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st if scale < 0: raise ValueError("Parameter 'scale' must be a positive number.") - if size := param_dict.get("size"): + size = param_dict.get("size") + if size: if not isinstance(size, float | int): raise TypeError("Parameter 'size' must be numeric.") if size < 0: raise ValueError("Parameter 'size' must be a positive number.") - if element_type == "shapes" and (shape := param_dict.get("shape")) is not None: + shape = param_dict.get("shape") + if element_type == "shapes" and shape is not None: valid_shapes = {"circle", "hex", "visium_hex", "square"} if not isinstance(shape, str): raise TypeError(f"Parameter 'shape' must be a String from {valid_shapes} if not None.") @@ -2336,7 +2375,8 @@ def _ensure_table_and_layer_exist_in_sdata( assert _ensure_table_and_layer_exist_in_sdata(param_dict.get("sdata"), table_name, table_layer) - if (method := param_dict.get("method")) not in ["matplotlib", "datashader", None]: + method = param_dict.get("method") + if method not in ["matplotlib", "datashader", None]: raise ValueError("If specified, parameter 'method' must be either 'matplotlib' or 'datashader'.") valid_ds_reduction_methods = [ @@ -2351,7 +2391,8 @@ def _ensure_table_and_layer_exist_in_sdata( "max", "min", ] - if (ds_reduction := param_dict.get("ds_reduction")) and (ds_reduction not in valid_ds_reduction_methods): + ds_reduction = param_dict.get("ds_reduction") + if ds_reduction and (ds_reduction not in valid_ds_reduction_methods): raise ValueError(f"Parameter 'ds_reduction' must be one of the following: {valid_ds_reduction_methods}.") if method == "datashader" and ds_reduction is None: @@ -2375,6 +2416,8 @@ def _validate_label_render_params( scale: str | None, table_name: str | None, table_layer: str | None, + colorbar: bool | str | None, + colorbar_params: dict[str, object] | None, ) -> dict[str, dict[str, Any]]: param_dict: dict[str, Any] = { "sdata": sdata, @@ -2391,6 +2434,8 @@ def _validate_label_render_params( "scale": scale, "table_name": table_name, "table_layer": table_layer, + "colorbar": colorbar, + "colorbar_params": colorbar_params, } param_dict = _type_check_params(param_dict, "labels") @@ -2419,6 +2464,8 @@ def _validate_label_render_params( element_params[el]["palette"] = param_dict["palette"] if element_params[el]["table_name"] is not None else None element_params[el]["groups"] = param_dict["groups"] if element_params[el]["table_name"] is not None else None + element_params[el]["colorbar"] = param_dict["colorbar"] + element_params[el]["colorbar_params"] = param_dict["colorbar_params"] return element_params @@ -2437,6 +2484,8 @@ def _validate_points_render_params( table_name: str | None, table_layer: str | None, ds_reduction: str | None, + colorbar: bool | str | None, + colorbar_params: dict[str, object] | None, ) -> dict[str, dict[str, Any]]: param_dict: dict[str, Any] = { "sdata": sdata, @@ -2452,6 +2501,8 @@ def _validate_points_render_params( "table_name": table_name, "table_layer": table_layer, "ds_reduction": ds_reduction, + "colorbar": colorbar, + "colorbar_params": colorbar_params, } param_dict = _type_check_params(param_dict, "points") @@ -2471,7 +2522,8 @@ def _validate_points_render_params( element_params[el]["table_name"] = None element_params[el]["col_for_color"] = None - if (col_for_color := param_dict["col_for_color"]) is not None: + col_for_color = param_dict["col_for_color"] + if col_for_color is not None: col_for_color, table_name = _validate_col_for_column_table( sdata, el, col_for_color, param_dict["table_name"] ) @@ -2481,6 +2533,8 @@ def _validate_points_render_params( element_params[el]["palette"] = param_dict["palette"] if param_dict["col_for_color"] is not None else None element_params[el]["groups"] = param_dict["groups"] if param_dict["col_for_color"] is not None else None element_params[el]["ds_reduction"] = param_dict["ds_reduction"] + element_params[el]["colorbar"] = param_dict["colorbar"] + element_params[el]["colorbar_params"] = param_dict["colorbar_params"] return element_params @@ -2504,6 +2558,8 @@ def _validate_shape_render_params( shape: Literal["circle", "hex", "visium_hex", "square"] | None, method: str | None, ds_reduction: str | None, + colorbar: bool | str | None, + colorbar_params: dict[str, object] | None, ) -> dict[str, dict[str, Any]]: param_dict: dict[str, Any] = { "sdata": sdata, @@ -2524,6 +2580,8 @@ def _validate_shape_render_params( "shape": shape, "method": method, "ds_reduction": ds_reduction, + "colorbar": colorbar, + "colorbar_params": colorbar_params, } param_dict = _type_check_params(param_dict, "shapes") @@ -2548,7 +2606,8 @@ def _validate_shape_render_params( element_params[el]["table_name"] = None element_params[el]["col_for_color"] = None - if (col_for_color := param_dict["col_for_color"]) is not None: + col_for_color = param_dict["col_for_color"] + if col_for_color is not None: col_for_color, table_name = _validate_col_for_column_table( sdata, el, col_for_color, param_dict["table_name"] ) @@ -2559,6 +2618,8 @@ def _validate_shape_render_params( element_params[el]["groups"] = param_dict["groups"] if param_dict["col_for_color"] is not None else None element_params[el]["method"] = param_dict["method"] element_params[el]["ds_reduction"] = param_dict["ds_reduction"] + element_params[el]["colorbar"] = param_dict["colorbar"] + element_params[el]["colorbar_params"] = param_dict["colorbar_params"] return element_params @@ -2616,6 +2677,8 @@ def _validate_image_render_params( cmap: list[Colormap | str] | Colormap | str | None, norm: Normalize | None, scale: str | None, + colorbar: bool | str | None, + colorbar_params: dict[str, object] | None, ) -> dict[str, dict[str, Any]]: param_dict: dict[str, Any] = { "sdata": sdata, @@ -2627,6 +2690,8 @@ def _validate_image_render_params( "cmap": cmap, "norm": norm, "scale": scale, + "colorbar": colorbar, + "colorbar_params": colorbar_params, } param_dict = _type_check_params(param_dict, "images") @@ -2681,7 +2746,8 @@ def _validate_image_render_params( element_params[el]["palette"] = palette element_params[el]["na_color"] = param_dict["na_color"] - if (cmap := param_dict["cmap"]) is not None: + cmap = param_dict["cmap"] + if cmap is not None: if len(cmap) == 1: cmap_length = len(channel) if channel is not None else len(spatial_element_ch) cmap = cmap * cmap_length @@ -2689,13 +2755,16 @@ def _validate_image_render_params( cmap = None element_params[el]["cmap"] = cmap element_params[el]["norm"] = param_dict["norm"] - if (scale := param_dict["scale"]) and isinstance(sdata[el], DataTree): - if scale not in list(sdata[el].keys()) and scale != "full": + scale = param_dict["scale"] + if scale and isinstance(param_dict["sdata"][el], DataTree): + if scale not in list(param_dict["sdata"][el].keys()) and scale != "full": element_params[el]["scale"] = None else: element_params[el]["scale"] = scale else: element_params[el]["scale"] = scale + element_params[el]["colorbar"] = param_dict["colorbar"] + element_params[el]["colorbar_params"] = param_dict["colorbar_params"] return element_params diff --git a/tests/_images/ColorbarControls_can_globally_turn_off_colorbars.png b/tests/_images/ColorbarControls_can_globally_turn_off_colorbars.png new file mode 100644 index 00000000..6128bea9 Binary files /dev/null and b/tests/_images/ColorbarControls_can_globally_turn_off_colorbars.png differ diff --git a/tests/_images/ColorbarControls_colorbar_can_adjust_pad.png b/tests/_images/ColorbarControls_colorbar_can_adjust_pad.png new file mode 100644 index 00000000..ed34c034 Binary files /dev/null and b/tests/_images/ColorbarControls_colorbar_can_adjust_pad.png differ diff --git a/tests/_images/ColorbarControls_colorbar_can_adjust_title.png b/tests/_images/ColorbarControls_colorbar_can_adjust_title.png new file mode 100644 index 00000000..d05e5b33 Binary files /dev/null and b/tests/_images/ColorbarControls_colorbar_can_adjust_title.png differ diff --git a/tests/_images/ColorbarControls_colorbar_can_adjust_width.png b/tests/_images/ColorbarControls_colorbar_can_adjust_width.png new file mode 100644 index 00000000..1a591f7e Binary files /dev/null and b/tests/_images/ColorbarControls_colorbar_can_adjust_width.png differ diff --git a/tests/_images/ColorbarControls_colorbar_can_have_colorbars_on_all_sides.png b/tests/_images/ColorbarControls_colorbar_can_have_colorbars_on_all_sides.png new file mode 100644 index 00000000..499b9413 Binary files /dev/null and b/tests/_images/ColorbarControls_colorbar_can_have_colorbars_on_all_sides.png differ diff --git a/tests/_images/ColorbarControls_colorbar_can_have_colorbars_on_different_sides.png b/tests/_images/ColorbarControls_colorbar_can_have_colorbars_on_different_sides.png new file mode 100644 index 00000000..0df7acc8 Binary files /dev/null and b/tests/_images/ColorbarControls_colorbar_can_have_colorbars_on_different_sides.png differ diff --git a/tests/_images/ColorbarControls_colorbar_can_have_two_colorbars_on_same_side.png b/tests/_images/ColorbarControls_colorbar_can_have_two_colorbars_on_same_side.png new file mode 100644 index 00000000..0584121c Binary files /dev/null and b/tests/_images/ColorbarControls_colorbar_can_have_two_colorbars_on_same_side.png differ diff --git a/tests/_images/ColorbarControls_colorbar_img_bottom.png b/tests/_images/ColorbarControls_colorbar_img_bottom.png new file mode 100644 index 00000000..70023e16 Binary files /dev/null and b/tests/_images/ColorbarControls_colorbar_img_bottom.png differ diff --git a/tests/_images/ColorbarControls_colorbar_img_default_location.png b/tests/_images/ColorbarControls_colorbar_img_default_location.png new file mode 100644 index 00000000..aec7d785 Binary files /dev/null and b/tests/_images/ColorbarControls_colorbar_img_default_location.png differ diff --git a/tests/_images/ColorbarControls_colorbar_img_left.png b/tests/_images/ColorbarControls_colorbar_img_left.png new file mode 100644 index 00000000..b23cbdc0 Binary files /dev/null and b/tests/_images/ColorbarControls_colorbar_img_left.png differ diff --git a/tests/_images/ColorbarControls_colorbar_img_top.png b/tests/_images/ColorbarControls_colorbar_img_top.png new file mode 100644 index 00000000..0d04d952 Binary files /dev/null and b/tests/_images/ColorbarControls_colorbar_img_top.png differ diff --git a/tests/_images/ColorbarControls_image_auto_colorbar_for_single_channel.png b/tests/_images/ColorbarControls_image_auto_colorbar_for_single_channel.png new file mode 100644 index 00000000..e9906422 Binary files /dev/null and b/tests/_images/ColorbarControls_image_auto_colorbar_for_single_channel.png differ diff --git a/tests/_images/ColorbarControls_multiple_images_in_one_cs_result_in_multiple_colorbars.png b/tests/_images/ColorbarControls_multiple_images_in_one_cs_result_in_multiple_colorbars.png new file mode 100644 index 00000000..a874a2db Binary files /dev/null and b/tests/_images/ColorbarControls_multiple_images_in_one_cs_result_in_multiple_colorbars.png differ diff --git a/tests/_images/ColorbarControls_single_channel_default_channel_name_omits_label.png b/tests/_images/ColorbarControls_single_channel_default_channel_name_omits_label.png new file mode 100644 index 00000000..2fe14027 Binary files /dev/null and b/tests/_images/ColorbarControls_single_channel_default_channel_name_omits_label.png differ diff --git a/tests/_images/Extent_correct_plot_after_transformations.png b/tests/_images/Extent_correct_plot_after_transformations.png index 5831ef3a..b183cb4c 100644 Binary files a/tests/_images/Extent_correct_plot_after_transformations.png and b/tests/_images/Extent_correct_plot_after_transformations.png differ diff --git a/tests/_images/Extent_extent_calculation_respects_element_selection_circles.png b/tests/_images/Extent_extent_calculation_respects_element_selection_circles.png index 62032bb4..be6c1669 100644 Binary files a/tests/_images/Extent_extent_calculation_respects_element_selection_circles.png and b/tests/_images/Extent_extent_calculation_respects_element_selection_circles.png differ diff --git a/tests/_images/Extent_extent_calculation_respects_element_selection_circles_and_polygons.png b/tests/_images/Extent_extent_calculation_respects_element_selection_circles_and_polygons.png index 2d22c88f..fc923bcc 100644 Binary files a/tests/_images/Extent_extent_calculation_respects_element_selection_circles_and_polygons.png and b/tests/_images/Extent_extent_calculation_respects_element_selection_circles_and_polygons.png differ diff --git a/tests/_images/Extent_extent_calculation_respects_element_selection_polygons.png b/tests/_images/Extent_extent_calculation_respects_element_selection_polygons.png index dee1ebae..e599f9d9 100644 Binary files a/tests/_images/Extent_extent_calculation_respects_element_selection_polygons.png and b/tests/_images/Extent_extent_calculation_respects_element_selection_polygons.png differ diff --git a/tests/_images/Extent_extent_of_img_full_canvas.png b/tests/_images/Extent_extent_of_img_full_canvas.png index c9bcf168..a62ff807 100644 Binary files a/tests/_images/Extent_extent_of_img_full_canvas.png and b/tests/_images/Extent_extent_of_img_full_canvas.png differ diff --git a/tests/_images/Extent_extent_of_img_is_correct_after_spatial_query.png b/tests/_images/Extent_extent_of_img_is_correct_after_spatial_query.png index 16bedd33..bb0246c6 100644 Binary files a/tests/_images/Extent_extent_of_img_is_correct_after_spatial_query.png and b/tests/_images/Extent_extent_of_img_is_correct_after_spatial_query.png differ diff --git a/tests/_images/Extent_extent_of_partial_canvas_on_full_canvas.png b/tests/_images/Extent_extent_of_partial_canvas_on_full_canvas.png index 47d378d5..53b93c47 100644 Binary files a/tests/_images/Extent_extent_of_partial_canvas_on_full_canvas.png and b/tests/_images/Extent_extent_of_partial_canvas_on_full_canvas.png differ diff --git a/tests/_images/Extent_extent_of_points_partial_canvas.png b/tests/_images/Extent_extent_of_points_partial_canvas.png index 60f04d50..e5cbe544 100644 Binary files a/tests/_images/Extent_extent_of_points_partial_canvas.png and b/tests/_images/Extent_extent_of_points_partial_canvas.png differ diff --git a/tests/_images/Images_can_do_rasterization.png b/tests/_images/Images_can_do_rasterization.png index a42d312e..158f5276 100644 Binary files a/tests/_images/Images_can_do_rasterization.png and b/tests/_images/Images_can_do_rasterization.png differ diff --git a/tests/_images/Images_can_pass_cmap.png b/tests/_images/Images_can_pass_cmap.png index 37bf7ba1..eb13f61e 100644 Binary files a/tests/_images/Images_can_pass_cmap.png and b/tests/_images/Images_can_pass_cmap.png differ diff --git a/tests/_images/Images_can_pass_cmap_list.png b/tests/_images/Images_can_pass_cmap_list.png index f0db8e8c..3f887d9b 100644 Binary files a/tests/_images/Images_can_pass_cmap_list.png and b/tests/_images/Images_can_pass_cmap_list.png differ diff --git a/tests/_images/Images_can_pass_cmap_to_each_channel.png b/tests/_images/Images_can_pass_cmap_to_each_channel.png index 78dc8caf..94cfcfcd 100644 Binary files a/tests/_images/Images_can_pass_cmap_to_each_channel.png and b/tests/_images/Images_can_pass_cmap_to_each_channel.png differ diff --git a/tests/_images/Images_can_pass_cmap_to_single_channel.png b/tests/_images/Images_can_pass_cmap_to_single_channel.png index 492dccbc..76cdadab 100644 Binary files a/tests/_images/Images_can_pass_cmap_to_single_channel.png and b/tests/_images/Images_can_pass_cmap_to_single_channel.png differ diff --git a/tests/_images/Images_can_pass_color_to_each_channel.png b/tests/_images/Images_can_pass_color_to_each_channel.png index 4dbfc58a..f24fe215 100644 Binary files a/tests/_images/Images_can_pass_color_to_each_channel.png and b/tests/_images/Images_can_pass_color_to_each_channel.png differ diff --git a/tests/_images/Images_can_pass_color_to_single_channel.png b/tests/_images/Images_can_pass_color_to_single_channel.png index 4196d5dc..60e23dfb 100644 Binary files a/tests/_images/Images_can_pass_color_to_single_channel.png and b/tests/_images/Images_can_pass_color_to_single_channel.png differ diff --git a/tests/_images/Images_can_pass_normalize_clip_False.png b/tests/_images/Images_can_pass_normalize_clip_False.png index 98b05197..02971873 100644 Binary files a/tests/_images/Images_can_pass_normalize_clip_False.png and b/tests/_images/Images_can_pass_normalize_clip_False.png differ diff --git a/tests/_images/Images_can_pass_normalize_clip_True.png b/tests/_images/Images_can_pass_normalize_clip_True.png index aa161fdf..2779196f 100644 Binary files a/tests/_images/Images_can_pass_normalize_clip_True.png and b/tests/_images/Images_can_pass_normalize_clip_True.png differ diff --git a/tests/_images/Images_can_pass_str_cmap.png b/tests/_images/Images_can_pass_str_cmap.png index 37bf7ba1..eb13f61e 100644 Binary files a/tests/_images/Images_can_pass_str_cmap.png and b/tests/_images/Images_can_pass_str_cmap.png differ diff --git a/tests/_images/Images_can_pass_str_cmap_list.png b/tests/_images/Images_can_pass_str_cmap_list.png index f0db8e8c..3f887d9b 100644 Binary files a/tests/_images/Images_can_pass_str_cmap_list.png and b/tests/_images/Images_can_pass_str_cmap_list.png differ diff --git a/tests/_images/Images_can_render_a_single_channel_from_image.png b/tests/_images/Images_can_render_a_single_channel_from_image.png index 349f9218..e9906422 100644 Binary files a/tests/_images/Images_can_render_a_single_channel_from_image.png and b/tests/_images/Images_can_render_a_single_channel_from_image.png differ diff --git a/tests/_images/Images_can_render_a_single_channel_from_multiscale_image.png b/tests/_images/Images_can_render_a_single_channel_from_multiscale_image.png index 349f9218..e9906422 100644 Binary files a/tests/_images/Images_can_render_a_single_channel_from_multiscale_image.png and b/tests/_images/Images_can_render_a_single_channel_from_multiscale_image.png differ diff --git a/tests/_images/Images_can_render_a_single_channel_str_from_image.png b/tests/_images/Images_can_render_a_single_channel_str_from_image.png index 349f9218..82efa621 100644 Binary files a/tests/_images/Images_can_render_a_single_channel_str_from_image.png and b/tests/_images/Images_can_render_a_single_channel_str_from_image.png differ diff --git a/tests/_images/Images_can_render_a_single_channel_str_from_multiscale_image.png b/tests/_images/Images_can_render_a_single_channel_str_from_multiscale_image.png index 349f9218..82efa621 100644 Binary files a/tests/_images/Images_can_render_a_single_channel_str_from_multiscale_image.png and b/tests/_images/Images_can_render_a_single_channel_str_from_multiscale_image.png differ diff --git a/tests/_images/Images_can_render_given_scale_of_multiscale_image.png b/tests/_images/Images_can_render_given_scale_of_multiscale_image.png index 59719093..4d70491a 100644 Binary files a/tests/_images/Images_can_render_given_scale_of_multiscale_image.png and b/tests/_images/Images_can_render_given_scale_of_multiscale_image.png differ diff --git a/tests/_images/Images_can_render_image.png b/tests/_images/Images_can_render_image.png index c9bcf168..a62ff807 100644 Binary files a/tests/_images/Images_can_render_image.png and b/tests/_images/Images_can_render_image.png differ diff --git a/tests/_images/Images_can_render_multiscale_image.png b/tests/_images/Images_can_render_multiscale_image.png index c9bcf168..a62ff807 100644 Binary files a/tests/_images/Images_can_render_multiscale_image.png and b/tests/_images/Images_can_render_multiscale_image.png differ diff --git a/tests/_images/Images_can_render_multiscale_image_with_custom_cmap.png b/tests/_images/Images_can_render_multiscale_image_with_custom_cmap.png index 4222e8c4..677908e7 100644 Binary files a/tests/_images/Images_can_render_multiscale_image_with_custom_cmap.png and b/tests/_images/Images_can_render_multiscale_image_with_custom_cmap.png differ diff --git a/tests/_images/Images_can_render_two_channels_from_image.png b/tests/_images/Images_can_render_two_channels_from_image.png index a17230f3..55085556 100644 Binary files a/tests/_images/Images_can_render_two_channels_from_image.png and b/tests/_images/Images_can_render_two_channels_from_image.png differ diff --git a/tests/_images/Images_can_render_two_channels_from_multiscale_image.png b/tests/_images/Images_can_render_two_channels_from_multiscale_image.png index a17230f3..55085556 100644 Binary files a/tests/_images/Images_can_render_two_channels_from_multiscale_image.png and b/tests/_images/Images_can_render_two_channels_from_multiscale_image.png differ diff --git a/tests/_images/Images_can_render_two_channels_str_from_image.png b/tests/_images/Images_can_render_two_channels_str_from_image.png index a17230f3..55085556 100644 Binary files a/tests/_images/Images_can_render_two_channels_str_from_image.png and b/tests/_images/Images_can_render_two_channels_str_from_image.png differ diff --git a/tests/_images/Images_can_render_two_channels_str_from_multiscale_image.png b/tests/_images/Images_can_render_two_channels_str_from_multiscale_image.png index a17230f3..55085556 100644 Binary files a/tests/_images/Images_can_render_two_channels_str_from_multiscale_image.png and b/tests/_images/Images_can_render_two_channels_str_from_multiscale_image.png differ diff --git a/tests/_images/Images_can_stack_render_images.png b/tests/_images/Images_can_stack_render_images.png index 604e323b..89a17ec0 100644 Binary files a/tests/_images/Images_can_stack_render_images.png and b/tests/_images/Images_can_stack_render_images.png differ diff --git a/tests/_images/Images_can_stick_to_zorder.png b/tests/_images/Images_can_stick_to_zorder.png index 521e2bb9..456c4aac 100644 Binary files a/tests/_images/Images_can_stick_to_zorder.png and b/tests/_images/Images_can_stick_to_zorder.png differ diff --git a/tests/_images/Images_can_stop_rasterization_with_scale_full.png b/tests/_images/Images_can_stop_rasterization_with_scale_full.png index e4b3df80..dc45bad2 100644 Binary files a/tests/_images/Images_can_stop_rasterization_with_scale_full.png and b/tests/_images/Images_can_stop_rasterization_with_scale_full.png differ diff --git a/tests/_images/Labels_can_annotate_labels_with_table_layer.png b/tests/_images/Labels_can_annotate_labels_with_table_layer.png index 7bf039eb..4a886732 100644 Binary files a/tests/_images/Labels_can_annotate_labels_with_table_layer.png and b/tests/_images/Labels_can_annotate_labels_with_table_layer.png differ diff --git a/tests/_images/Labels_can_color_labels_by_categorical_variable.png b/tests/_images/Labels_can_color_labels_by_categorical_variable.png index f634cbaf..a8108f8e 100644 Binary files a/tests/_images/Labels_can_color_labels_by_categorical_variable.png and b/tests/_images/Labels_can_color_labels_by_categorical_variable.png differ diff --git a/tests/_images/Labels_can_color_labels_by_categorical_variable_in_other_table.png b/tests/_images/Labels_can_color_labels_by_categorical_variable_in_other_table.png index 8f62e4b8..b3db4ac5 100644 Binary files a/tests/_images/Labels_can_color_labels_by_categorical_variable_in_other_table.png and b/tests/_images/Labels_can_color_labels_by_categorical_variable_in_other_table.png differ diff --git a/tests/_images/Labels_can_color_labels_by_continuous_variable.png b/tests/_images/Labels_can_color_labels_by_continuous_variable.png index 67d13ce7..80c3a8ec 100644 Binary files a/tests/_images/Labels_can_color_labels_by_continuous_variable.png and b/tests/_images/Labels_can_color_labels_by_continuous_variable.png differ diff --git a/tests/_images/Labels_can_color_with_norm_and_clipping.png b/tests/_images/Labels_can_color_with_norm_and_clipping.png index ebd0302f..474b9ad2 100644 Binary files a/tests/_images/Labels_can_color_with_norm_and_clipping.png and b/tests/_images/Labels_can_color_with_norm_and_clipping.png differ diff --git a/tests/_images/Labels_can_color_with_norm_no_clipping.png b/tests/_images/Labels_can_color_with_norm_no_clipping.png index f3c6a545..26488177 100644 Binary files a/tests/_images/Labels_can_color_with_norm_no_clipping.png and b/tests/_images/Labels_can_color_with_norm_no_clipping.png differ diff --git a/tests/_images/Labels_can_control_label_infill.png b/tests/_images/Labels_can_control_label_infill.png index 67d13ce7..80c3a8ec 100644 Binary files a/tests/_images/Labels_can_control_label_infill.png and b/tests/_images/Labels_can_control_label_infill.png differ diff --git a/tests/_images/Labels_can_control_label_outline.png b/tests/_images/Labels_can_control_label_outline.png index 2ca3aa00..70fd5685 100644 Binary files a/tests/_images/Labels_can_control_label_outline.png and b/tests/_images/Labels_can_control_label_outline.png differ diff --git a/tests/_images/Labels_can_do_rasterization.png b/tests/_images/Labels_can_do_rasterization.png index 364024f6..dd448731 100644 Binary files a/tests/_images/Labels_can_do_rasterization.png and b/tests/_images/Labels_can_do_rasterization.png differ diff --git a/tests/_images/Labels_can_handle_dropping_small_labels_after_rasterize_categorical.png b/tests/_images/Labels_can_handle_dropping_small_labels_after_rasterize_categorical.png index 36d7ae5a..4bb4398e 100644 Binary files a/tests/_images/Labels_can_handle_dropping_small_labels_after_rasterize_categorical.png and b/tests/_images/Labels_can_handle_dropping_small_labels_after_rasterize_categorical.png differ diff --git a/tests/_images/Labels_can_handle_dropping_small_labels_after_rasterize_continuous.png b/tests/_images/Labels_can_handle_dropping_small_labels_after_rasterize_continuous.png index ac0bacda..cea046a3 100644 Binary files a/tests/_images/Labels_can_handle_dropping_small_labels_after_rasterize_continuous.png and b/tests/_images/Labels_can_handle_dropping_small_labels_after_rasterize_continuous.png differ diff --git a/tests/_images/Labels_can_render_given_scale_of_multiscale_labels.png b/tests/_images/Labels_can_render_given_scale_of_multiscale_labels.png index cb50c54b..03ee33f7 100644 Binary files a/tests/_images/Labels_can_render_given_scale_of_multiscale_labels.png and b/tests/_images/Labels_can_render_given_scale_of_multiscale_labels.png differ diff --git a/tests/_images/Labels_can_render_labels.png b/tests/_images/Labels_can_render_labels.png index 3748225f..6e222e8b 100644 Binary files a/tests/_images/Labels_can_render_labels.png and b/tests/_images/Labels_can_render_labels.png differ diff --git a/tests/_images/Labels_can_render_multiscale_labels.png b/tests/_images/Labels_can_render_multiscale_labels.png index 3748225f..6e222e8b 100644 Binary files a/tests/_images/Labels_can_render_multiscale_labels.png and b/tests/_images/Labels_can_render_multiscale_labels.png differ diff --git a/tests/_images/Labels_can_stack_render_labels.png b/tests/_images/Labels_can_stack_render_labels.png index def46bd5..5856caa1 100644 Binary files a/tests/_images/Labels_can_stack_render_labels.png and b/tests/_images/Labels_can_stack_render_labels.png differ diff --git a/tests/_images/Labels_can_stop_rasterization_with_scale_full.png b/tests/_images/Labels_can_stop_rasterization_with_scale_full.png index a02237f6..80453bde 100644 Binary files a/tests/_images/Labels_can_stop_rasterization_with_scale_full.png and b/tests/_images/Labels_can_stop_rasterization_with_scale_full.png differ diff --git a/tests/_images/Labels_label_categorical_color.png b/tests/_images/Labels_label_categorical_color.png index 0851a002..62658aa4 100644 Binary files a/tests/_images/Labels_label_categorical_color.png and b/tests/_images/Labels_label_categorical_color.png differ diff --git a/tests/_images/Labels_label_colorbar_uses_alpha_of_less_transparent_infill.png b/tests/_images/Labels_label_colorbar_uses_alpha_of_less_transparent_infill.png index e3c99446..518d406e 100644 Binary files a/tests/_images/Labels_label_colorbar_uses_alpha_of_less_transparent_infill.png and b/tests/_images/Labels_label_colorbar_uses_alpha_of_less_transparent_infill.png differ diff --git a/tests/_images/Labels_label_colorbar_uses_alpha_of_less_transparent_outline.png b/tests/_images/Labels_label_colorbar_uses_alpha_of_less_transparent_outline.png index b1bd82c5..14ef6be6 100644 Binary files a/tests/_images/Labels_label_colorbar_uses_alpha_of_less_transparent_outline.png and b/tests/_images/Labels_label_colorbar_uses_alpha_of_less_transparent_outline.png differ diff --git a/tests/_images/Labels_respects_custom_colors_from_uns.png b/tests/_images/Labels_respects_custom_colors_from_uns.png index d540e08f..9b60803a 100644 Binary files a/tests/_images/Labels_respects_custom_colors_from_uns.png and b/tests/_images/Labels_respects_custom_colors_from_uns.png differ diff --git a/tests/_images/Labels_respects_custom_colors_from_uns_with_groups_and_palette.png b/tests/_images/Labels_respects_custom_colors_from_uns_with_groups_and_palette.png index 88a05954..499fb50f 100644 Binary files a/tests/_images/Labels_respects_custom_colors_from_uns_with_groups_and_palette.png and b/tests/_images/Labels_respects_custom_colors_from_uns_with_groups_and_palette.png differ diff --git a/tests/_images/Labels_subset_categorical_label_maintains_order.png b/tests/_images/Labels_subset_categorical_label_maintains_order.png index 8cbb46f8..9e1c2cc6 100644 Binary files a/tests/_images/Labels_subset_categorical_label_maintains_order.png and b/tests/_images/Labels_subset_categorical_label_maintains_order.png differ diff --git a/tests/_images/Labels_subset_categorical_label_maintains_order_when_palette_overwrite.png b/tests/_images/Labels_subset_categorical_label_maintains_order_when_palette_overwrite.png index 63abe870..6d0806c5 100644 Binary files a/tests/_images/Labels_subset_categorical_label_maintains_order_when_palette_overwrite.png and b/tests/_images/Labels_subset_categorical_label_maintains_order_when_palette_overwrite.png differ diff --git a/tests/_images/Labels_two_calls_with_coloring_result_in_two_colorbars.png b/tests/_images/Labels_two_calls_with_coloring_result_in_two_colorbars.png index ca0a2d32..73d7a1ab 100644 Binary files a/tests/_images/Labels_two_calls_with_coloring_result_in_two_colorbars.png and b/tests/_images/Labels_two_calls_with_coloring_result_in_two_colorbars.png differ diff --git a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_affine.png b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_affine.png index bad81ca7..7c61a3c7 100644 Binary files a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_affine.png and b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_affine.png differ diff --git a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_composition.png b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_composition.png index 8191d1ef..7bc4cf41 100644 Binary files a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_composition.png and b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_composition.png differ diff --git a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_inverse.png b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_inverse.png index f7f3466e..e3c030fa 100644 Binary files a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_inverse.png and b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_inverse.png differ diff --git a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_mapaxis.png b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_mapaxis.png index 0214a74c..dab7362a 100644 Binary files a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_mapaxis.png and b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_mapaxis.png differ diff --git a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_overlay.png b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_overlay.png index f7f3466e..e3c030fa 100644 Binary files a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_overlay.png and b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_overlay.png differ diff --git a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_rotation.png b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_rotation.png index c6a342a6..3912a0f2 100644 Binary files a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_rotation.png and b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_rotation.png differ diff --git a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_scale.png b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_scale.png index 05a4ed55..617859f9 100644 Binary files a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_scale.png and b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_scale.png differ diff --git a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_split.png b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_split.png index 997f1bf8..c714dd46 100644 Binary files a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_split.png and b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_split.png differ diff --git a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_translation.png b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_translation.png index 3d24c54c..3cca95c1 100644 Binary files a/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_translation.png and b/tests/_images/NotebooksTransformations_can_render_transformations_raccoon_translation.png differ diff --git a/tests/_images/Points_alpha_overwrites_opacity_from_color.png b/tests/_images/Points_alpha_overwrites_opacity_from_color.png index b7790708..e4d043d9 100644 Binary files a/tests/_images/Points_alpha_overwrites_opacity_from_color.png and b/tests/_images/Points_alpha_overwrites_opacity_from_color.png differ diff --git a/tests/_images/Points_can_annotate_points_with_table_X.png b/tests/_images/Points_can_annotate_points_with_table_X.png index bad4e3e6..3d3e9f5f 100644 Binary files a/tests/_images/Points_can_annotate_points_with_table_X.png and b/tests/_images/Points_can_annotate_points_with_table_X.png differ diff --git a/tests/_images/Points_can_annotate_points_with_table_and_groups.png b/tests/_images/Points_can_annotate_points_with_table_and_groups.png index b03781a4..00e7f427 100644 Binary files a/tests/_images/Points_can_annotate_points_with_table_and_groups.png and b/tests/_images/Points_can_annotate_points_with_table_and_groups.png differ diff --git a/tests/_images/Points_can_annotate_points_with_table_layer.png b/tests/_images/Points_can_annotate_points_with_table_layer.png index 051e6993..3d3e9f5f 100644 Binary files a/tests/_images/Points_can_annotate_points_with_table_layer.png and b/tests/_images/Points_can_annotate_points_with_table_layer.png differ diff --git a/tests/_images/Points_can_annotate_points_with_table_obs.png b/tests/_images/Points_can_annotate_points_with_table_obs.png index 8002e8f9..7a253d5e 100644 Binary files a/tests/_images/Points_can_annotate_points_with_table_obs.png and b/tests/_images/Points_can_annotate_points_with_table_obs.png differ diff --git a/tests/_images/Points_can_color_by_color_name.png b/tests/_images/Points_can_color_by_color_name.png index c4311868..92130994 100644 Binary files a/tests/_images/Points_can_color_by_color_name.png and b/tests/_images/Points_can_color_by_color_name.png differ diff --git a/tests/_images/Points_can_color_by_hex.png b/tests/_images/Points_can_color_by_hex.png index 93212977..138224b7 100644 Binary files a/tests/_images/Points_can_color_by_hex.png and b/tests/_images/Points_can_color_by_hex.png differ diff --git a/tests/_images/Points_can_color_by_hex_with_alpha.png b/tests/_images/Points_can_color_by_hex_with_alpha.png index 2ca1a695..ac973eb3 100644 Binary files a/tests/_images/Points_can_color_by_hex_with_alpha.png and b/tests/_images/Points_can_color_by_hex_with_alpha.png differ diff --git a/tests/_images/Points_can_color_by_rgb_array.png b/tests/_images/Points_can_color_by_rgb_array.png index b7790708..e4d043d9 100644 Binary files a/tests/_images/Points_can_color_by_rgb_array.png and b/tests/_images/Points_can_color_by_rgb_array.png differ diff --git a/tests/_images/Points_can_color_by_rgba_array.png b/tests/_images/Points_can_color_by_rgba_array.png index 74000340..ac1c7afe 100644 Binary files a/tests/_images/Points_can_color_by_rgba_array.png and b/tests/_images/Points_can_color_by_rgba_array.png differ diff --git a/tests/_images/Points_can_filter_with_groups_custom_palette.png b/tests/_images/Points_can_filter_with_groups_custom_palette.png index 469226f4..092099fc 100644 Binary files a/tests/_images/Points_can_filter_with_groups_custom_palette.png and b/tests/_images/Points_can_filter_with_groups_custom_palette.png differ diff --git a/tests/_images/Points_can_filter_with_groups_default_palette.png b/tests/_images/Points_can_filter_with_groups_default_palette.png index 28c9c1c2..8bdf767e 100644 Binary files a/tests/_images/Points_can_filter_with_groups_default_palette.png and b/tests/_images/Points_can_filter_with_groups_default_palette.png differ diff --git a/tests/_images/Points_can_render_points.png b/tests/_images/Points_can_render_points.png index 60f04d50..e5cbe544 100644 Binary files a/tests/_images/Points_can_render_points.png and b/tests/_images/Points_can_render_points.png differ diff --git a/tests/_images/Points_can_stack_render_points.png b/tests/_images/Points_can_stack_render_points.png index 2ed80ca9..8b008723 100644 Binary files a/tests/_images/Points_can_stack_render_points.png and b/tests/_images/Points_can_stack_render_points.png differ diff --git a/tests/_images/Points_can_use_norm_with_clip.png b/tests/_images/Points_can_use_norm_with_clip.png index 0abb42c6..123d0ead 100644 Binary files a/tests/_images/Points_can_use_norm_with_clip.png and b/tests/_images/Points_can_use_norm_with_clip.png differ diff --git a/tests/_images/Points_can_use_norm_without_clip.png b/tests/_images/Points_can_use_norm_without_clip.png index f20eac25..daa30d2a 100644 Binary files a/tests/_images/Points_can_use_norm_without_clip.png and b/tests/_images/Points_can_use_norm_without_clip.png differ diff --git a/tests/_images/Points_coloring_with_cmap.png b/tests/_images/Points_coloring_with_cmap.png index 4f7c6f39..3d59f288 100644 Binary files a/tests/_images/Points_coloring_with_cmap.png and b/tests/_images/Points_coloring_with_cmap.png differ diff --git a/tests/_images/Points_coloring_with_palette.png b/tests/_images/Points_coloring_with_palette.png index 4e626813..225bb452 100644 Binary files a/tests/_images/Points_coloring_with_palette.png and b/tests/_images/Points_coloring_with_palette.png differ diff --git a/tests/_images/Points_datashader_can_color_by_category.png b/tests/_images/Points_datashader_can_color_by_category.png index 4a706d1f..1c856028 100644 Binary files a/tests/_images/Points_datashader_can_color_by_category.png and b/tests/_images/Points_datashader_can_color_by_category.png differ diff --git a/tests/_images/Points_datashader_can_transform_points.png b/tests/_images/Points_datashader_can_transform_points.png index 201b2db1..6bd5506f 100644 Binary files a/tests/_images/Points_datashader_can_transform_points.png and b/tests/_images/Points_datashader_can_transform_points.png differ diff --git a/tests/_images/Points_datashader_can_use_any_as_reduction.png b/tests/_images/Points_datashader_can_use_any_as_reduction.png index 5d0df899..d0712364 100644 Binary files a/tests/_images/Points_datashader_can_use_any_as_reduction.png and b/tests/_images/Points_datashader_can_use_any_as_reduction.png differ diff --git a/tests/_images/Points_datashader_can_use_count_as_reduction.png b/tests/_images/Points_datashader_can_use_count_as_reduction.png index c3b52e90..7c64b7c6 100644 Binary files a/tests/_images/Points_datashader_can_use_count_as_reduction.png and b/tests/_images/Points_datashader_can_use_count_as_reduction.png differ diff --git a/tests/_images/Points_datashader_can_use_max_as_reduction.png b/tests/_images/Points_datashader_can_use_max_as_reduction.png index 410f7471..f04444c4 100644 Binary files a/tests/_images/Points_datashader_can_use_max_as_reduction.png and b/tests/_images/Points_datashader_can_use_max_as_reduction.png differ diff --git a/tests/_images/Points_datashader_can_use_mean_as_reduction.png b/tests/_images/Points_datashader_can_use_mean_as_reduction.png index 60064d5a..b2451e12 100644 Binary files a/tests/_images/Points_datashader_can_use_mean_as_reduction.png and b/tests/_images/Points_datashader_can_use_mean_as_reduction.png differ diff --git a/tests/_images/Points_datashader_can_use_min_as_reduction.png b/tests/_images/Points_datashader_can_use_min_as_reduction.png index 884665ef..fff0ff21 100644 Binary files a/tests/_images/Points_datashader_can_use_min_as_reduction.png and b/tests/_images/Points_datashader_can_use_min_as_reduction.png differ diff --git a/tests/_images/Points_datashader_can_use_norm_with_clip.png b/tests/_images/Points_datashader_can_use_norm_with_clip.png index 7bb613b0..55901644 100644 Binary files a/tests/_images/Points_datashader_can_use_norm_with_clip.png and b/tests/_images/Points_datashader_can_use_norm_with_clip.png differ diff --git a/tests/_images/Points_datashader_can_use_norm_without_clip.png b/tests/_images/Points_datashader_can_use_norm_without_clip.png index 450d2c54..bea23ec7 100644 Binary files a/tests/_images/Points_datashader_can_use_norm_without_clip.png and b/tests/_images/Points_datashader_can_use_norm_without_clip.png differ diff --git a/tests/_images/Points_datashader_can_use_std_as_reduction.png b/tests/_images/Points_datashader_can_use_std_as_reduction.png index 75e0881d..27479629 100644 Binary files a/tests/_images/Points_datashader_can_use_std_as_reduction.png and b/tests/_images/Points_datashader_can_use_std_as_reduction.png differ diff --git a/tests/_images/Points_datashader_can_use_std_as_reduction_not_all_zero.png b/tests/_images/Points_datashader_can_use_std_as_reduction_not_all_zero.png index f577fa25..406b7a8c 100644 Binary files a/tests/_images/Points_datashader_can_use_std_as_reduction_not_all_zero.png and b/tests/_images/Points_datashader_can_use_std_as_reduction_not_all_zero.png differ diff --git a/tests/_images/Points_datashader_can_use_sum_as_reduction.png b/tests/_images/Points_datashader_can_use_sum_as_reduction.png index c57544d6..8b0cac3d 100644 Binary files a/tests/_images/Points_datashader_can_use_sum_as_reduction.png and b/tests/_images/Points_datashader_can_use_sum_as_reduction.png differ diff --git a/tests/_images/Points_datashader_can_use_var_as_reduction.png b/tests/_images/Points_datashader_can_use_var_as_reduction.png index 75e0881d..27479629 100644 Binary files a/tests/_images/Points_datashader_can_use_var_as_reduction.png and b/tests/_images/Points_datashader_can_use_var_as_reduction.png differ diff --git a/tests/_images/Points_datashader_colors_from_table_obs.png b/tests/_images/Points_datashader_colors_from_table_obs.png index 35fc36fd..e1f156fc 100644 Binary files a/tests/_images/Points_datashader_colors_from_table_obs.png and b/tests/_images/Points_datashader_colors_from_table_obs.png differ diff --git a/tests/_images/Points_datashader_continuous_color.png b/tests/_images/Points_datashader_continuous_color.png index fc967349..2e1bec49 100644 Binary files a/tests/_images/Points_datashader_continuous_color.png and b/tests/_images/Points_datashader_continuous_color.png differ diff --git a/tests/_images/Points_datashader_matplotlib_stack.png b/tests/_images/Points_datashader_matplotlib_stack.png index 5768a171..7016d8e1 100644 Binary files a/tests/_images/Points_datashader_matplotlib_stack.png and b/tests/_images/Points_datashader_matplotlib_stack.png differ diff --git a/tests/_images/Points_datashader_norm_vmin_eq_vmax_with_clip.png b/tests/_images/Points_datashader_norm_vmin_eq_vmax_with_clip.png index 2d7ccf8a..c4376b51 100644 Binary files a/tests/_images/Points_datashader_norm_vmin_eq_vmax_with_clip.png and b/tests/_images/Points_datashader_norm_vmin_eq_vmax_with_clip.png differ diff --git a/tests/_images/Points_datashader_norm_vmin_eq_vmax_without_clip.png b/tests/_images/Points_datashader_norm_vmin_eq_vmax_without_clip.png index 5aaf98b2..2e19ff07 100644 Binary files a/tests/_images/Points_datashader_norm_vmin_eq_vmax_without_clip.png and b/tests/_images/Points_datashader_norm_vmin_eq_vmax_without_clip.png differ diff --git a/tests/_images/Points_mpl_and_datashader_point_sizes_agree_after_altered_dpi.png b/tests/_images/Points_mpl_and_datashader_point_sizes_agree_after_altered_dpi.png index 41f33b09..9445837c 100644 Binary files a/tests/_images/Points_mpl_and_datashader_point_sizes_agree_after_altered_dpi.png and b/tests/_images/Points_mpl_and_datashader_point_sizes_agree_after_altered_dpi.png differ diff --git a/tests/_images/Points_points_categorical_color.png b/tests/_images/Points_points_categorical_color.png index d4a0c5f0..3c041895 100644 Binary files a/tests/_images/Points_points_categorical_color.png and b/tests/_images/Points_points_categorical_color.png differ diff --git a/tests/_images/Points_points_categorical_color_column_datashader.png b/tests/_images/Points_points_categorical_color_column_datashader.png index 24e65703..373bc983 100644 Binary files a/tests/_images/Points_points_categorical_color_column_datashader.png and b/tests/_images/Points_points_categorical_color_column_datashader.png differ diff --git a/tests/_images/Points_points_categorical_color_column_matplotlib.png b/tests/_images/Points_points_categorical_color_column_matplotlib.png index 348246b4..b025e0e8 100644 Binary files a/tests/_images/Points_points_categorical_color_column_matplotlib.png and b/tests/_images/Points_points_categorical_color_column_matplotlib.png differ diff --git a/tests/_images/Points_points_coercable_categorical_color.png b/tests/_images/Points_points_coercable_categorical_color.png index 4a4763ac..3c041895 100644 Binary files a/tests/_images/Points_points_coercable_categorical_color.png and b/tests/_images/Points_points_coercable_categorical_color.png differ diff --git a/tests/_images/Points_points_continuous_color_column_datashader.png b/tests/_images/Points_points_continuous_color_column_datashader.png index 98dab8bd..6610c100 100644 Binary files a/tests/_images/Points_points_continuous_color_column_datashader.png and b/tests/_images/Points_points_continuous_color_column_datashader.png differ diff --git a/tests/_images/Points_points_continuous_color_column_matplotlib.png b/tests/_images/Points_points_continuous_color_column_matplotlib.png index ff1cfb5d..031566db 100644 Binary files a/tests/_images/Points_points_continuous_color_column_matplotlib.png and b/tests/_images/Points_points_continuous_color_column_matplotlib.png differ diff --git a/tests/_images/Points_points_transformed_ds_agrees_with_mpl.png b/tests/_images/Points_points_transformed_ds_agrees_with_mpl.png index c26c62ce..6dce017a 100644 Binary files a/tests/_images/Points_points_transformed_ds_agrees_with_mpl.png and b/tests/_images/Points_points_transformed_ds_agrees_with_mpl.png differ diff --git a/tests/_images/Points_respects_custom_colors_from_uns_for_points.png b/tests/_images/Points_respects_custom_colors_from_uns_for_points.png index ddc3b5fa..206b63fa 100644 Binary files a/tests/_images/Points_respects_custom_colors_from_uns_for_points.png and b/tests/_images/Points_respects_custom_colors_from_uns_for_points.png differ diff --git a/tests/_images/Shapes_alpha_overwrites_opacity_from_color.png b/tests/_images/Shapes_alpha_overwrites_opacity_from_color.png index 64b4feb2..1bf9680e 100644 Binary files a/tests/_images/Shapes_alpha_overwrites_opacity_from_color.png and b/tests/_images/Shapes_alpha_overwrites_opacity_from_color.png differ diff --git a/tests/_images/Shapes_can_annotate_shapes_with_table_layer.png b/tests/_images/Shapes_can_annotate_shapes_with_table_layer.png index 9cfd64f1..5a4fa893 100644 Binary files a/tests/_images/Shapes_can_annotate_shapes_with_table_layer.png and b/tests/_images/Shapes_can_annotate_shapes_with_table_layer.png differ diff --git a/tests/_images/Shapes_can_color_by_category_with_cmap.png b/tests/_images/Shapes_can_color_by_category_with_cmap.png index 08044643..05a14c35 100644 Binary files a/tests/_images/Shapes_can_color_by_category_with_cmap.png and b/tests/_images/Shapes_can_color_by_category_with_cmap.png differ diff --git a/tests/_images/Shapes_can_color_by_color_name.png b/tests/_images/Shapes_can_color_by_color_name.png index 0df42599..3f3c6565 100644 Binary files a/tests/_images/Shapes_can_color_by_color_name.png and b/tests/_images/Shapes_can_color_by_color_name.png differ diff --git a/tests/_images/Shapes_can_color_by_hex.png b/tests/_images/Shapes_can_color_by_hex.png index de1cae1f..d40a77c4 100644 Binary files a/tests/_images/Shapes_can_color_by_hex.png and b/tests/_images/Shapes_can_color_by_hex.png differ diff --git a/tests/_images/Shapes_can_color_by_hex_with_alpha.png b/tests/_images/Shapes_can_color_by_hex_with_alpha.png index 284766df..3ea46550 100644 Binary files a/tests/_images/Shapes_can_color_by_hex_with_alpha.png and b/tests/_images/Shapes_can_color_by_hex_with_alpha.png differ diff --git a/tests/_images/Shapes_can_color_by_rgb_array.png b/tests/_images/Shapes_can_color_by_rgb_array.png index 64b4feb2..1bf9680e 100644 Binary files a/tests/_images/Shapes_can_color_by_rgb_array.png and b/tests/_images/Shapes_can_color_by_rgb_array.png differ diff --git a/tests/_images/Shapes_can_color_by_rgba_array.png b/tests/_images/Shapes_can_color_by_rgba_array.png index 1429e83e..4fad8345 100644 Binary files a/tests/_images/Shapes_can_color_by_rgba_array.png and b/tests/_images/Shapes_can_color_by_rgba_array.png differ diff --git a/tests/_images/Shapes_can_color_from_geodataframe.png b/tests/_images/Shapes_can_color_from_geodataframe.png index 2004afea..9daaa703 100644 Binary files a/tests/_images/Shapes_can_color_from_geodataframe.png and b/tests/_images/Shapes_can_color_from_geodataframe.png differ diff --git a/tests/_images/Shapes_can_color_multipolygons_with_multiple_holes.png b/tests/_images/Shapes_can_color_multipolygons_with_multiple_holes.png index 39d3163a..afe1bfe5 100644 Binary files a/tests/_images/Shapes_can_color_multipolygons_with_multiple_holes.png and b/tests/_images/Shapes_can_color_multipolygons_with_multiple_holes.png differ diff --git a/tests/_images/Shapes_can_color_two_queried_shapes_elements_by_annotation.png b/tests/_images/Shapes_can_color_two_queried_shapes_elements_by_annotation.png index fd8ddc59..240934d2 100644 Binary files a/tests/_images/Shapes_can_color_two_queried_shapes_elements_by_annotation.png and b/tests/_images/Shapes_can_color_two_queried_shapes_elements_by_annotation.png differ diff --git a/tests/_images/Shapes_can_color_two_shapes_elements_by_annotation.png b/tests/_images/Shapes_can_color_two_shapes_elements_by_annotation.png index 1c7749b3..e99f972d 100644 Binary files a/tests/_images/Shapes_can_color_two_shapes_elements_by_annotation.png and b/tests/_images/Shapes_can_color_two_shapes_elements_by_annotation.png differ diff --git a/tests/_images/Shapes_can_color_with_norm_no_clipping.png b/tests/_images/Shapes_can_color_with_norm_no_clipping.png index 01294587..0cb5657c 100644 Binary files a/tests/_images/Shapes_can_color_with_norm_no_clipping.png and b/tests/_images/Shapes_can_color_with_norm_no_clipping.png differ diff --git a/tests/_images/Shapes_can_do_non_matching_table.png b/tests/_images/Shapes_can_do_non_matching_table.png index 6f6f6214..4f5272e4 100644 Binary files a/tests/_images/Shapes_can_do_non_matching_table.png and b/tests/_images/Shapes_can_do_non_matching_table.png differ diff --git a/tests/_images/Shapes_can_filter_with_groups.png b/tests/_images/Shapes_can_filter_with_groups.png index aafd683e..4f9b71be 100644 Binary files a/tests/_images/Shapes_can_filter_with_groups.png and b/tests/_images/Shapes_can_filter_with_groups.png differ diff --git a/tests/_images/Shapes_can_plot_queried_with_annotation_despite_random_shuffling.png b/tests/_images/Shapes_can_plot_queried_with_annotation_despite_random_shuffling.png index 41205c15..3031fde4 100644 Binary files a/tests/_images/Shapes_can_plot_queried_with_annotation_despite_random_shuffling.png and b/tests/_images/Shapes_can_plot_queried_with_annotation_despite_random_shuffling.png differ diff --git a/tests/_images/Shapes_can_plot_shapes_after_spatial_query.png b/tests/_images/Shapes_can_plot_shapes_after_spatial_query.png index 0a4a900c..4260a82b 100644 Binary files a/tests/_images/Shapes_can_plot_shapes_after_spatial_query.png and b/tests/_images/Shapes_can_plot_shapes_after_spatial_query.png differ diff --git a/tests/_images/Shapes_can_plot_with_annotation_despite_random_shuffling.png b/tests/_images/Shapes_can_plot_with_annotation_despite_random_shuffling.png index ac33e269..07f37d02 100644 Binary files a/tests/_images/Shapes_can_plot_with_annotation_despite_random_shuffling.png and b/tests/_images/Shapes_can_plot_with_annotation_despite_random_shuffling.png differ diff --git a/tests/_images/Shapes_can_render_circles.png b/tests/_images/Shapes_can_render_circles.png index 62032bb4..be6c1669 100644 Binary files a/tests/_images/Shapes_can_render_circles.png and b/tests/_images/Shapes_can_render_circles.png differ diff --git a/tests/_images/Shapes_can_render_circles_to_hex.png b/tests/_images/Shapes_can_render_circles_to_hex.png index 026fdd1e..65dca162 100644 Binary files a/tests/_images/Shapes_can_render_circles_to_hex.png and b/tests/_images/Shapes_can_render_circles_to_hex.png differ diff --git a/tests/_images/Shapes_can_render_circles_to_square.png b/tests/_images/Shapes_can_render_circles_to_square.png index 13003c8a..621039af 100644 Binary files a/tests/_images/Shapes_can_render_circles_to_square.png and b/tests/_images/Shapes_can_render_circles_to_square.png differ diff --git a/tests/_images/Shapes_can_render_circles_with_colored_outline.png b/tests/_images/Shapes_can_render_circles_with_colored_outline.png index 43510653..7298efbe 100644 Binary files a/tests/_images/Shapes_can_render_circles_with_colored_outline.png and b/tests/_images/Shapes_can_render_circles_with_colored_outline.png differ diff --git a/tests/_images/Shapes_can_render_circles_with_default_outline_width.png b/tests/_images/Shapes_can_render_circles_with_default_outline_width.png index eb6aafab..3fa74aab 100644 Binary files a/tests/_images/Shapes_can_render_circles_with_default_outline_width.png and b/tests/_images/Shapes_can_render_circles_with_default_outline_width.png differ diff --git a/tests/_images/Shapes_can_render_circles_with_outline.png b/tests/_images/Shapes_can_render_circles_with_outline.png index eb6aafab..3fa74aab 100644 Binary files a/tests/_images/Shapes_can_render_circles_with_outline.png and b/tests/_images/Shapes_can_render_circles_with_outline.png differ diff --git a/tests/_images/Shapes_can_render_circles_with_specified_outline_width.png b/tests/_images/Shapes_can_render_circles_with_specified_outline_width.png index 7b31504f..a8478498 100644 Binary files a/tests/_images/Shapes_can_render_circles_with_specified_outline_width.png and b/tests/_images/Shapes_can_render_circles_with_specified_outline_width.png differ diff --git a/tests/_images/Shapes_can_render_double_outline_with_diff_alpha.png b/tests/_images/Shapes_can_render_double_outline_with_diff_alpha.png index 944a049b..a2061363 100644 Binary files a/tests/_images/Shapes_can_render_double_outline_with_diff_alpha.png and b/tests/_images/Shapes_can_render_double_outline_with_diff_alpha.png differ diff --git a/tests/_images/Shapes_can_render_empty_geometry.png b/tests/_images/Shapes_can_render_empty_geometry.png index d3e74884..2099e7d8 100644 Binary files a/tests/_images/Shapes_can_render_empty_geometry.png and b/tests/_images/Shapes_can_render_empty_geometry.png differ diff --git a/tests/_images/Shapes_can_render_multipolygon_with_inverted_inner_ring_and_disjoint_part.png b/tests/_images/Shapes_can_render_multipolygon_with_inverted_inner_ring_and_disjoint_part.png index 1b8b3fde..4d6d3861 100644 Binary files a/tests/_images/Shapes_can_render_multipolygon_with_inverted_inner_ring_and_disjoint_part.png and b/tests/_images/Shapes_can_render_multipolygon_with_inverted_inner_ring_and_disjoint_part.png differ diff --git a/tests/_images/Shapes_can_render_multipolygons.png b/tests/_images/Shapes_can_render_multipolygons.png index 40f798e1..92cf6a37 100644 Binary files a/tests/_images/Shapes_can_render_multipolygons.png and b/tests/_images/Shapes_can_render_multipolygons.png differ diff --git a/tests/_images/Shapes_can_render_multipolygons_that_say_they_are_polygons.png b/tests/_images/Shapes_can_render_multipolygons_that_say_they_are_polygons.png old mode 100755 new mode 100644 index f5475dd4..cb36e787 Binary files a/tests/_images/Shapes_can_render_multipolygons_that_say_they_are_polygons.png and b/tests/_images/Shapes_can_render_multipolygons_that_say_they_are_polygons.png differ diff --git a/tests/_images/Shapes_can_render_multipolygons_to_circle.png b/tests/_images/Shapes_can_render_multipolygons_to_circle.png index fe5a17e7..d9586733 100644 Binary files a/tests/_images/Shapes_can_render_multipolygons_to_circle.png and b/tests/_images/Shapes_can_render_multipolygons_to_circle.png differ diff --git a/tests/_images/Shapes_can_render_multipolygons_to_hex.png b/tests/_images/Shapes_can_render_multipolygons_to_hex.png index e5ac72dc..2dc00528 100644 Binary files a/tests/_images/Shapes_can_render_multipolygons_to_hex.png and b/tests/_images/Shapes_can_render_multipolygons_to_hex.png differ diff --git a/tests/_images/Shapes_can_render_multipolygons_to_square.png b/tests/_images/Shapes_can_render_multipolygons_to_square.png index 0646e548..bca953b6 100644 Binary files a/tests/_images/Shapes_can_render_multipolygons_to_square.png and b/tests/_images/Shapes_can_render_multipolygons_to_square.png differ diff --git a/tests/_images/Shapes_can_render_multipolygons_with_multiple_holes.png b/tests/_images/Shapes_can_render_multipolygons_with_multiple_holes.png index 14f75db3..65168452 100644 Binary files a/tests/_images/Shapes_can_render_multipolygons_with_multiple_holes.png and b/tests/_images/Shapes_can_render_multipolygons_with_multiple_holes.png differ diff --git a/tests/_images/Shapes_can_render_polygon_with_inverted_inner_ring.png b/tests/_images/Shapes_can_render_polygon_with_inverted_inner_ring.png index d5bd7964..6b015cfa 100644 Binary files a/tests/_images/Shapes_can_render_polygon_with_inverted_inner_ring.png and b/tests/_images/Shapes_can_render_polygon_with_inverted_inner_ring.png differ diff --git a/tests/_images/Shapes_can_render_polygons.png b/tests/_images/Shapes_can_render_polygons.png index dee1ebae..e599f9d9 100644 Binary files a/tests/_images/Shapes_can_render_polygons.png and b/tests/_images/Shapes_can_render_polygons.png differ diff --git a/tests/_images/Shapes_can_render_polygons_to_circle.png b/tests/_images/Shapes_can_render_polygons_to_circle.png index fc3e3906..5cdf34c1 100644 Binary files a/tests/_images/Shapes_can_render_polygons_to_circle.png and b/tests/_images/Shapes_can_render_polygons_to_circle.png differ diff --git a/tests/_images/Shapes_can_render_polygons_to_hex.png b/tests/_images/Shapes_can_render_polygons_to_hex.png index 45d3be26..2fd8e383 100644 Binary files a/tests/_images/Shapes_can_render_polygons_to_hex.png and b/tests/_images/Shapes_can_render_polygons_to_hex.png differ diff --git a/tests/_images/Shapes_can_render_polygons_to_square.png b/tests/_images/Shapes_can_render_polygons_to_square.png index 02bf5e03..5e4a75bd 100644 Binary files a/tests/_images/Shapes_can_render_polygons_to_square.png and b/tests/_images/Shapes_can_render_polygons_to_square.png differ diff --git a/tests/_images/Shapes_can_render_polygons_with_outline.png b/tests/_images/Shapes_can_render_polygons_with_outline.png index ef938f9d..55ee5e49 100644 Binary files a/tests/_images/Shapes_can_render_polygons_with_outline.png and b/tests/_images/Shapes_can_render_polygons_with_outline.png differ diff --git a/tests/_images/Shapes_can_render_polygons_with_rgb_colored_outline.png b/tests/_images/Shapes_can_render_polygons_with_rgb_colored_outline.png index a3800d4c..2a286674 100644 Binary files a/tests/_images/Shapes_can_render_polygons_with_rgb_colored_outline.png and b/tests/_images/Shapes_can_render_polygons_with_rgb_colored_outline.png differ diff --git a/tests/_images/Shapes_can_render_polygons_with_rgba_colored_outline.png b/tests/_images/Shapes_can_render_polygons_with_rgba_colored_outline.png index 6d268c2b..17d48092 100644 Binary files a/tests/_images/Shapes_can_render_polygons_with_rgba_colored_outline.png and b/tests/_images/Shapes_can_render_polygons_with_rgba_colored_outline.png differ diff --git a/tests/_images/Shapes_can_render_polygons_with_str_colored_outline.png b/tests/_images/Shapes_can_render_polygons_with_str_colored_outline.png index 920fdc7b..c2241abc 100644 Binary files a/tests/_images/Shapes_can_render_polygons_with_str_colored_outline.png and b/tests/_images/Shapes_can_render_polygons_with_str_colored_outline.png differ diff --git a/tests/_images/Shapes_can_render_shapes_with_colored_double_outline.png b/tests/_images/Shapes_can_render_shapes_with_colored_double_outline.png index d1381b97..7ec7662f 100644 Binary files a/tests/_images/Shapes_can_render_shapes_with_colored_double_outline.png and b/tests/_images/Shapes_can_render_shapes_with_colored_double_outline.png differ diff --git a/tests/_images/Shapes_can_render_shapes_with_double_outline.png b/tests/_images/Shapes_can_render_shapes_with_double_outline.png index 2a01f8e5..07e959ca 100644 Binary files a/tests/_images/Shapes_can_render_shapes_with_double_outline.png and b/tests/_images/Shapes_can_render_shapes_with_double_outline.png differ diff --git a/tests/_images/Shapes_can_scale_shapes.png b/tests/_images/Shapes_can_scale_shapes.png index 42db8f60..967e9b87 100644 Binary files a/tests/_images/Shapes_can_scale_shapes.png and b/tests/_images/Shapes_can_scale_shapes.png differ diff --git a/tests/_images/Shapes_can_set_clims_clip.png b/tests/_images/Shapes_can_set_clims_clip.png index dad4903d..73e1c6bb 100644 Binary files a/tests/_images/Shapes_can_set_clims_clip.png and b/tests/_images/Shapes_can_set_clims_clip.png differ diff --git a/tests/_images/Shapes_can_stack_render_shapes.png b/tests/_images/Shapes_can_stack_render_shapes.png index f06fd1f7..6d50bd38 100644 Binary files a/tests/_images/Shapes_can_stack_render_shapes.png and b/tests/_images/Shapes_can_stack_render_shapes.png differ diff --git a/tests/_images/Shapes_colorbar_can_be_normalised.png b/tests/_images/Shapes_colorbar_can_be_normalised.png index f18723e8..0e9903b5 100644 Binary files a/tests/_images/Shapes_colorbar_can_be_normalised.png and b/tests/_images/Shapes_colorbar_can_be_normalised.png differ diff --git a/tests/_images/Shapes_colorbar_respects_input_limits.png b/tests/_images/Shapes_colorbar_respects_input_limits.png index 7f12d2d7..458ffff6 100644 Binary files a/tests/_images/Shapes_colorbar_respects_input_limits.png and b/tests/_images/Shapes_colorbar_respects_input_limits.png differ diff --git a/tests/_images/Shapes_coloring_with_palette.png b/tests/_images/Shapes_coloring_with_palette.png index c57d9cc9..aca48173 100644 Binary files a/tests/_images/Shapes_coloring_with_palette.png and b/tests/_images/Shapes_coloring_with_palette.png differ diff --git a/tests/_images/Shapes_datashader_can_color_by_category.png b/tests/_images/Shapes_datashader_can_color_by_category.png index cd4bd5f7..7886c694 100644 Binary files a/tests/_images/Shapes_datashader_can_color_by_category.png and b/tests/_images/Shapes_datashader_can_color_by_category.png differ diff --git a/tests/_images/Shapes_datashader_can_color_by_category_with_cmap.png b/tests/_images/Shapes_datashader_can_color_by_category_with_cmap.png index 46bc9acc..c72a92bc 100644 Binary files a/tests/_images/Shapes_datashader_can_color_by_category_with_cmap.png and b/tests/_images/Shapes_datashader_can_color_by_category_with_cmap.png differ diff --git a/tests/_images/Shapes_datashader_can_color_by_identical_value.png b/tests/_images/Shapes_datashader_can_color_by_identical_value.png index e9ee690a..97ef67a3 100644 Binary files a/tests/_images/Shapes_datashader_can_color_by_identical_value.png and b/tests/_images/Shapes_datashader_can_color_by_identical_value.png differ diff --git a/tests/_images/Shapes_datashader_can_color_by_value.png b/tests/_images/Shapes_datashader_can_color_by_value.png index 5645d159..c279dbbc 100644 Binary files a/tests/_images/Shapes_datashader_can_color_by_value.png and b/tests/_images/Shapes_datashader_can_color_by_value.png differ diff --git a/tests/_images/Shapes_datashader_can_color_with_norm_and_clipping.png b/tests/_images/Shapes_datashader_can_color_with_norm_and_clipping.png index 2e754d19..600fa842 100644 Binary files a/tests/_images/Shapes_datashader_can_color_with_norm_and_clipping.png and b/tests/_images/Shapes_datashader_can_color_with_norm_and_clipping.png differ diff --git a/tests/_images/Shapes_datashader_can_color_with_norm_no_clipping.png b/tests/_images/Shapes_datashader_can_color_with_norm_no_clipping.png index 64b053b6..0473d58c 100644 Binary files a/tests/_images/Shapes_datashader_can_color_with_norm_no_clipping.png and b/tests/_images/Shapes_datashader_can_color_with_norm_no_clipping.png differ diff --git a/tests/_images/Shapes_datashader_can_render_circles_to_hex.png b/tests/_images/Shapes_datashader_can_render_circles_to_hex.png index d6aebef8..1eff9228 100644 Binary files a/tests/_images/Shapes_datashader_can_render_circles_to_hex.png and b/tests/_images/Shapes_datashader_can_render_circles_to_hex.png differ diff --git a/tests/_images/Shapes_datashader_can_render_circles_to_square.png b/tests/_images/Shapes_datashader_can_render_circles_to_square.png index c5776d4a..f25772e3 100644 Binary files a/tests/_images/Shapes_datashader_can_render_circles_to_square.png and b/tests/_images/Shapes_datashader_can_render_circles_to_square.png differ diff --git a/tests/_images/Shapes_datashader_can_render_colored_shapes.png b/tests/_images/Shapes_datashader_can_render_colored_shapes.png index 3231e510..d3aa8375 100644 Binary files a/tests/_images/Shapes_datashader_can_render_colored_shapes.png and b/tests/_images/Shapes_datashader_can_render_colored_shapes.png differ diff --git a/tests/_images/Shapes_datashader_can_render_multipolygons_to_circle.png b/tests/_images/Shapes_datashader_can_render_multipolygons_to_circle.png index ee543dc1..82194a3d 100644 Binary files a/tests/_images/Shapes_datashader_can_render_multipolygons_to_circle.png and b/tests/_images/Shapes_datashader_can_render_multipolygons_to_circle.png differ diff --git a/tests/_images/Shapes_datashader_can_render_multipolygons_to_hex.png b/tests/_images/Shapes_datashader_can_render_multipolygons_to_hex.png index 7028fc4c..48bbce0f 100644 Binary files a/tests/_images/Shapes_datashader_can_render_multipolygons_to_hex.png and b/tests/_images/Shapes_datashader_can_render_multipolygons_to_hex.png differ diff --git a/tests/_images/Shapes_datashader_can_render_multipolygons_to_square.png b/tests/_images/Shapes_datashader_can_render_multipolygons_to_square.png index 90701900..5f3aec85 100644 Binary files a/tests/_images/Shapes_datashader_can_render_multipolygons_to_square.png and b/tests/_images/Shapes_datashader_can_render_multipolygons_to_square.png differ diff --git a/tests/_images/Shapes_datashader_can_render_polygons_to_circle.png b/tests/_images/Shapes_datashader_can_render_polygons_to_circle.png index 01f93369..adda1e0b 100644 Binary files a/tests/_images/Shapes_datashader_can_render_polygons_to_circle.png and b/tests/_images/Shapes_datashader_can_render_polygons_to_circle.png differ diff --git a/tests/_images/Shapes_datashader_can_render_polygons_to_hex.png b/tests/_images/Shapes_datashader_can_render_polygons_to_hex.png index 8f460b6c..f509af7b 100644 Binary files a/tests/_images/Shapes_datashader_can_render_polygons_to_hex.png and b/tests/_images/Shapes_datashader_can_render_polygons_to_hex.png differ diff --git a/tests/_images/Shapes_datashader_can_render_polygons_to_square.png b/tests/_images/Shapes_datashader_can_render_polygons_to_square.png index 2ae09482..04b445ab 100644 Binary files a/tests/_images/Shapes_datashader_can_render_polygons_to_square.png and b/tests/_images/Shapes_datashader_can_render_polygons_to_square.png differ diff --git a/tests/_images/Shapes_datashader_can_render_shapes.png b/tests/_images/Shapes_datashader_can_render_shapes.png index 399a2d8e..667760db 100644 Binary files a/tests/_images/Shapes_datashader_can_render_shapes.png and b/tests/_images/Shapes_datashader_can_render_shapes.png differ diff --git a/tests/_images/Shapes_datashader_can_render_shapes_with_colored_double_outline.png b/tests/_images/Shapes_datashader_can_render_shapes_with_colored_double_outline.png index 36ce6f7b..8614e760 100644 Binary files a/tests/_images/Shapes_datashader_can_render_shapes_with_colored_double_outline.png and b/tests/_images/Shapes_datashader_can_render_shapes_with_colored_double_outline.png differ diff --git a/tests/_images/Shapes_datashader_can_render_shapes_with_double_outline.png b/tests/_images/Shapes_datashader_can_render_shapes_with_double_outline.png index fc7d0270..cbc6c5e4 100644 Binary files a/tests/_images/Shapes_datashader_can_render_shapes_with_double_outline.png and b/tests/_images/Shapes_datashader_can_render_shapes_with_double_outline.png differ diff --git a/tests/_images/Shapes_datashader_can_render_with_colored_outline.png b/tests/_images/Shapes_datashader_can_render_with_colored_outline.png index ea8944fc..7ea92c52 100644 Binary files a/tests/_images/Shapes_datashader_can_render_with_colored_outline.png and b/tests/_images/Shapes_datashader_can_render_with_colored_outline.png differ diff --git a/tests/_images/Shapes_datashader_can_render_with_diff_alpha_outline.png b/tests/_images/Shapes_datashader_can_render_with_diff_alpha_outline.png index 67c6c431..7a1cada2 100644 Binary files a/tests/_images/Shapes_datashader_can_render_with_diff_alpha_outline.png and b/tests/_images/Shapes_datashader_can_render_with_diff_alpha_outline.png differ diff --git a/tests/_images/Shapes_datashader_can_render_with_diff_width_outline.png b/tests/_images/Shapes_datashader_can_render_with_diff_width_outline.png index 5f374369..b638716a 100644 Binary files a/tests/_images/Shapes_datashader_can_render_with_diff_width_outline.png and b/tests/_images/Shapes_datashader_can_render_with_diff_width_outline.png differ diff --git a/tests/_images/Shapes_datashader_can_render_with_different_alpha.png b/tests/_images/Shapes_datashader_can_render_with_different_alpha.png index 115f5554..550a479c 100644 Binary files a/tests/_images/Shapes_datashader_can_render_with_different_alpha.png and b/tests/_images/Shapes_datashader_can_render_with_different_alpha.png differ diff --git a/tests/_images/Shapes_datashader_can_render_with_outline.png b/tests/_images/Shapes_datashader_can_render_with_outline.png index c4d0b5c0..eaac5b07 100644 Binary files a/tests/_images/Shapes_datashader_can_render_with_outline.png and b/tests/_images/Shapes_datashader_can_render_with_outline.png differ diff --git a/tests/_images/Shapes_datashader_can_render_with_rgb_colored_outline.png b/tests/_images/Shapes_datashader_can_render_with_rgb_colored_outline.png index 9ce92a8b..3ea2f152 100644 Binary files a/tests/_images/Shapes_datashader_can_render_with_rgb_colored_outline.png and b/tests/_images/Shapes_datashader_can_render_with_rgb_colored_outline.png differ diff --git a/tests/_images/Shapes_datashader_can_render_with_rgba_colored_outline.png b/tests/_images/Shapes_datashader_can_render_with_rgba_colored_outline.png index c54b2b6f..cef9f0eb 100644 Binary files a/tests/_images/Shapes_datashader_can_render_with_rgba_colored_outline.png and b/tests/_images/Shapes_datashader_can_render_with_rgba_colored_outline.png differ diff --git a/tests/_images/Shapes_datashader_can_transform_circles.png b/tests/_images/Shapes_datashader_can_transform_circles.png index 9efe6bd5..9f53cb99 100644 Binary files a/tests/_images/Shapes_datashader_can_transform_circles.png and b/tests/_images/Shapes_datashader_can_transform_circles.png differ diff --git a/tests/_images/Shapes_datashader_can_transform_multipolygons.png b/tests/_images/Shapes_datashader_can_transform_multipolygons.png index 03fde2cf..e905f05d 100644 Binary files a/tests/_images/Shapes_datashader_can_transform_multipolygons.png and b/tests/_images/Shapes_datashader_can_transform_multipolygons.png differ diff --git a/tests/_images/Shapes_datashader_can_transform_polygons.png b/tests/_images/Shapes_datashader_can_transform_polygons.png index f58a9bd4..b351b932 100644 Binary files a/tests/_images/Shapes_datashader_can_transform_polygons.png and b/tests/_images/Shapes_datashader_can_transform_polygons.png differ diff --git a/tests/_images/Shapes_datashader_norm_vmin_eq_vmax_with_clip.png b/tests/_images/Shapes_datashader_norm_vmin_eq_vmax_with_clip.png index 54842f7b..a4e9b7ac 100644 Binary files a/tests/_images/Shapes_datashader_norm_vmin_eq_vmax_with_clip.png and b/tests/_images/Shapes_datashader_norm_vmin_eq_vmax_with_clip.png differ diff --git a/tests/_images/Shapes_datashader_norm_vmin_eq_vmax_without_clip.png b/tests/_images/Shapes_datashader_norm_vmin_eq_vmax_without_clip.png index 230e3aeb..295a6818 100644 Binary files a/tests/_images/Shapes_datashader_norm_vmin_eq_vmax_without_clip.png and b/tests/_images/Shapes_datashader_norm_vmin_eq_vmax_without_clip.png differ diff --git a/tests/_images/Shapes_datashader_shades_with_linear_cmap.png b/tests/_images/Shapes_datashader_shades_with_linear_cmap.png index ba0f08c0..f74465d7 100644 Binary files a/tests/_images/Shapes_datashader_shades_with_linear_cmap.png and b/tests/_images/Shapes_datashader_shades_with_linear_cmap.png differ diff --git a/tests/_images/Shapes_outline_alpha_takes_precedence.png b/tests/_images/Shapes_outline_alpha_takes_precedence.png index 02daf58f..90a27f73 100644 Binary files a/tests/_images/Shapes_outline_alpha_takes_precedence.png and b/tests/_images/Shapes_outline_alpha_takes_precedence.png differ diff --git a/tests/_images/Shapes_respects_custom_colors_from_uns.png b/tests/_images/Shapes_respects_custom_colors_from_uns.png index 742a1397..3923c496 100644 Binary files a/tests/_images/Shapes_respects_custom_colors_from_uns.png and b/tests/_images/Shapes_respects_custom_colors_from_uns.png differ diff --git a/tests/_images/Shapes_shapes_categorical_color.png b/tests/_images/Shapes_shapes_categorical_color.png index f9495181..72b83510 100644 Binary files a/tests/_images/Shapes_shapes_categorical_color.png and b/tests/_images/Shapes_shapes_categorical_color.png differ diff --git a/tests/_images/Shapes_shapes_coercable_categorical_color.png b/tests/_images/Shapes_shapes_coercable_categorical_color.png index 11625137..72b83510 100644 Binary files a/tests/_images/Shapes_shapes_coercable_categorical_color.png and b/tests/_images/Shapes_shapes_coercable_categorical_color.png differ diff --git a/tests/_images/Shapes_visium_hex_hexagonal_grid.png b/tests/_images/Shapes_visium_hex_hexagonal_grid.png index b6c59369..c9aa1f0e 100644 Binary files a/tests/_images/Shapes_visium_hex_hexagonal_grid.png and b/tests/_images/Shapes_visium_hex_hexagonal_grid.png differ diff --git a/tests/_images/Show_pad_extent_adds_padding.png b/tests/_images/Show_pad_extent_adds_padding.png index a4c52fc0..0f9840c2 100644 Binary files a/tests/_images/Show_pad_extent_adds_padding.png and b/tests/_images/Show_pad_extent_adds_padding.png differ diff --git a/tests/_images/Utils_can_set_zero_in_cmap_to_transparent.png b/tests/_images/Utils_can_set_zero_in_cmap_to_transparent.png index 7aa7dda8..f708ea55 100644 Binary files a/tests/_images/Utils_can_set_zero_in_cmap_to_transparent.png and b/tests/_images/Utils_can_set_zero_in_cmap_to_transparent.png differ diff --git a/tests/_images/Utils_colnames_that_are_valid_matplotlib_greyscale_colors_are_not_evaluated_as_colors.png b/tests/_images/Utils_colnames_that_are_valid_matplotlib_greyscale_colors_are_not_evaluated_as_colors.png index 7f12d2d7..c3533875 100644 Binary files a/tests/_images/Utils_colnames_that_are_valid_matplotlib_greyscale_colors_are_not_evaluated_as_colors.png and b/tests/_images/Utils_colnames_that_are_valid_matplotlib_greyscale_colors_are_not_evaluated_as_colors.png differ diff --git a/tests/_images/Utils_set_outline_accepts_str_or_float_or_list_thereof.png b/tests/_images/Utils_set_outline_accepts_str_or_float_or_list_thereof.png index 673a4ce1..17d48092 100644 Binary files a/tests/_images/Utils_set_outline_accepts_str_or_float_or_list_thereof.png and b/tests/_images/Utils_set_outline_accepts_str_or_float_or_list_thereof.png differ diff --git a/tests/conftest.py b/tests/conftest.py index b44d6d8e..2299f126 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ from anndata import AnnData from geopandas import GeoDataFrame from matplotlib.testing.compare import compare_images +from PIL import Image from shapely.geometry import MultiPolygon, Polygon from spatialdata import SpatialData from spatialdata.datasets import blobs, raccoon @@ -36,6 +37,9 @@ ACTUAL = HERE / "figures" TOL = 15 DPI = 80 +CANVAS_WIDTH = 400 +CANVAS_HEIGHT = 300 +_RESAMPLE = Image.Resampling.LANCZOS if hasattr(Image, "Resampling") else Image.LANCZOS def get_standard_RNG(): @@ -43,6 +47,23 @@ def get_standard_RNG(): return np.random.default_rng(seed=42) +def _resize_and_pad_image(path: Path, canvas_size: tuple[int, int] = (CANVAS_WIDTH, CANVAS_HEIGHT)) -> None: + """Scale image to fit canvas while keeping aspect ratio, then pad.""" + with Image.open(path) as img: + img = img.convert("RGBA") + target_w, target_h = canvas_size + if img.width == 0 or img.height == 0: + raise ValueError("Cannot resize image with zero dimension.") + scale = min(target_w / img.width, target_h / img.height) + new_w = max(1, int(round(img.width * scale))) + new_h = max(1, int(round(img.height * scale))) + resized = img.resize((new_w, new_h), resample=_RESAMPLE) + canvas = Image.new("RGBA", canvas_size, (255, 255, 255, 255)) + offset = ((target_w - new_w) // 2, (target_h - new_h) // 2) + canvas.paste(resized, offset, resized) + canvas.convert("RGB").save(path) + + @pytest.fixture() def full_sdata() -> SpatialData: return SpatialData( @@ -77,8 +98,7 @@ def test_sdata_single_image(): np.zeros((1, 10, 10)), dims=("c", "y", "x"), transformations={"data1": sd.transformations.Identity()} ) } - sdata = sd.SpatialData(images=images) - return sdata + return sd.SpatialData(images=images) @pytest.fixture @@ -86,8 +106,7 @@ def test_sdata_single_image_with_label(): """Creates a simple sdata object.""" images = {"data1": sd.models.Image2DModel.parse(np.zeros((1, 10, 10)), dims=("c", "y", "x"))} labels = {"label1": sd.models.Labels2DModel.parse(np.zeros((10, 10)), dims=("y", "x"))} - sdata = sd.SpatialData(images=images, labels=labels) - return sdata + return sd.SpatialData(images=images, labels=labels) @pytest.fixture @@ -104,8 +123,7 @@ def test_sdata_multiple_images(): np.zeros((1, 10, 10)), dims=("c", "y", "x"), transformations={"data1": sd.transformations.Identity()} ), } - sdata = sd.SpatialData(images=images) - return sdata + return sd.SpatialData(images=images) @pytest.fixture @@ -141,8 +159,7 @@ def test_sdata_multiple_images_dims(): "data2": sd.models.Image2DModel.parse(np.zeros((3, 10, 10)), dims=("c", "y", "x")), "data3": sd.models.Image2DModel.parse(np.zeros((3, 10, 10)), dims=("c", "y", "x")), } - sdata = sd.SpatialData(images=images) - return sdata + return sd.SpatialData(images=images) @pytest.fixture @@ -153,8 +170,7 @@ def test_sdata_multiple_images_diverging_dims(): "data2": sd.models.Image2DModel.parse(np.zeros((6, 10, 10)), dims=("c", "y", "x")), "data3": sd.models.Image2DModel.parse(np.zeros((3, 10, 10)), dims=("c", "y", "x")), } - sdata = sd.SpatialData(images=images) - return sdata + return sd.SpatialData(images=images) @pytest.fixture @@ -226,27 +242,28 @@ def empty_table() -> SpatialData: ) def sdata(request) -> SpatialData: if request.param == "full": - s = SpatialData( + return SpatialData( images=_get_images(), labels=_get_labels(), shapes=_get_shapes(), points=_get_points(), table=_get_table("sample1"), ) - elif request.param == "empty": - s = SpatialData() - else: - s = request.getfixturevalue(request.param) - return s + if request.param == "empty": + return SpatialData() + return request.getfixturevalue(request.param) def _get_images() -> dict[str, DataArray | DataTree]: - out = {} dims_2d = ("c", "y", "x") dims_3d = ("z", "y", "x", "c") - out["image2d"] = Image2DModel.parse( - get_standard_RNG().normal(size=(3, 64, 64)), dims=dims_2d, c_coords=["r", "g", "b"] - ) + out = { + "image2d": Image2DModel.parse( + get_standard_RNG().normal(size=(3, 64, 64)), + dims=dims_2d, + c_coords=["r", "g", "b"], + ) + } out["image2d_multiscale"] = Image2DModel.parse( get_standard_RNG().normal(size=(3, 64, 64)), scale_factors=[2, 2], dims=dims_2d, c_coords=["r", "g", "b"] ) @@ -274,11 +291,10 @@ def _get_images() -> dict[str, DataArray | DataTree]: def _get_labels() -> dict[str, DataArray | DataTree]: - out = {} dims_2d = ("y", "x") dims_3d = ("z", "y", "x") - out["labels2d"] = Labels2DModel.parse(get_standard_RNG().integers(0, 100, size=(64, 64)), dims=dims_2d) + out = {"labels2d": Labels2DModel.parse(get_standard_RNG().integers(0, 100, size=(64, 64)), dims=dims_2d)} out["labels2d_multiscale"] = Labels2DModel.parse( get_standard_RNG().integers(0, 100, size=(64, 64)), scale_factors=[2, 4], dims=dims_2d ) @@ -307,7 +323,6 @@ def _get_labels() -> dict[str, DataArray | DataTree]: def _get_polygons() -> dict[str, GeoDataFrame]: # TODO: add polygons from geojson and from ragged arrays since now only the GeoDataFrame initializer is tested. - out = {} poly = GeoDataFrame( { "geometry": [ @@ -340,19 +355,19 @@ def _get_polygons() -> dict[str, GeoDataFrame]: } ) - out["poly"] = ShapesModel.parse(poly, name="poly") - out["multipoly"] = ShapesModel.parse(multipoly, name="multipoly") - - return out + return { + "poly": ShapesModel.parse(poly, name="poly"), + "multipoly": ShapesModel.parse(multipoly, name="multipoly"), + } def _get_shapes() -> dict[str, AnnData]: - out = {} arr = get_standard_RNG().normal(size=(100, 2)) - out["shapes_0"] = ShapesModel.parse(arr, shape_type="Square", shape_size=3) - out["shapes_1"] = ShapesModel.parse(arr, shape_type="Circle", shape_size=np.repeat(1, len(arr))) - return out + return { + "shapes_0": ShapesModel.parse(arr, shape_type="Square", shape_size=3), + "shapes_1": ShapesModel.parse(arr, shape_type="Circle", shape_size=np.repeat(1, len(arr))), + } def _get_points() -> dict[str, pa.Table]: @@ -386,15 +401,22 @@ def _get_table( ) adata.obs[instance_key] = np.arange(adata.n_obs) if isinstance(region, str): - table = TableModel.parse(adata=adata, region=region, instance_key=instance_key) - elif isinstance(region, list): + return TableModel.parse(adata=adata, region=region, instance_key=instance_key) + if isinstance(region, list): adata.obs[region_key] = get_standard_RNG().choice(region, size=adata.n_obs) adata.obs[instance_key] = get_standard_RNG().integers(0, 10, size=(100,)) - table = TableModel.parse(adata=adata, region=region, region_key=region_key, instance_key=instance_key) - else: - table = TableModel.parse(adata=adata, region=region, region_key=region_key, instance_key=instance_key) - - return table + return TableModel.parse( + adata=adata, + region=region, + region_key=region_key, + instance_key=instance_key, + ) + return TableModel.parse( + adata=adata, + region=region, + region_key=region_key, + instance_key=instance_key, + ) class PlotTesterMeta(ABCMeta): @@ -411,15 +433,35 @@ def compare(cls, basename: str, tolerance: float | None = None): ACTUAL.mkdir(parents=True, exist_ok=True) out_path = ACTUAL / f"{basename}.png" - width, height = 400, 300 # fixed dimensions so runners don't change + width, height = CANVAS_WIDTH, CANVAS_HEIGHT # base dimensions; actual PNG may grow/shrink fig = plt.gcf() fig.set_size_inches(width / DPI, height / DPI) fig.set_dpi(DPI) - # Apply constrained layout and save the plot - fig.set_constrained_layout(True) - plt.savefig(out_path, dpi=DPI) - plt.close() + # Try to get a reasonable layout first (helps with axes/labels) + if not fig.get_constrained_layout(): + try: + fig.set_constrained_layout(True) + except (ValueError, RuntimeError): + try: + fig.tight_layout(pad=2.0, rect=[0.02, 0.02, 0.98, 0.98]) + except (ValueError, RuntimeError): + fig.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.1) + + plt.figure(fig.number) # ensure this figure is current + + # Force a draw so that tight bbox "sees" all artists (including colorbars) + fig.canvas.draw() + + # Let matplotlib adjust the output size so that all artists are included + fig.savefig( + out_path, + dpi=DPI, + bbox_inches="tight", + pad_inches=0.02, # small margin around everything + ) + _resize_and_pad_image(out_path, (width, height)) + plt.close(fig) if tolerance is None: # see https://github.com/scverse/squidpy/pull/302 @@ -433,7 +475,28 @@ def compare(cls, basename: str, tolerance: float | None = None): def _decorate(fn: Callable, clsname: str, name: str | None = None) -> Callable: @wraps(fn) def save_and_compare(self, *args, **kwargs): + # Get all figures before the test runs + figures_before = set(plt.get_fignums()) + fn(self, *args, **kwargs) + + # Get all figures after the test runs + figures_after = set(plt.get_fignums()) + + # Find the figure(s) created during the test + new_figures = figures_after - figures_before + + if new_figures: + # Use the most recently created figure (highest number) + fig_num = max(new_figures) + plt.figure(fig_num) + elif figures_after: + # If no new figures were created, use the current figure + # but ensure it's set as current + current_fig = plt.gcf() + plt.figure(current_fig.number) + # If no figures exist, plt.gcf() will create one, which is fine + self.compare(fig_name) if not callable(fn): diff --git a/tests/pl/test_colorbar.py b/tests/pl/test_colorbar.py new file mode 100644 index 00000000..6fef5035 --- /dev/null +++ b/tests/pl/test_colorbar.py @@ -0,0 +1,104 @@ +import matplotlib +import numpy as np +import scanpy as sc +from spatialdata import SpatialData +from spatialdata.models import Image2DModel, Labels2DModel + +import spatialdata_plot # noqa: F401 +from tests.conftest import DPI, PlotTester, PlotTesterMeta + +sc.pl.set_rcParams_defaults() +sc.set_figure_params(dpi=DPI, color_map="viridis") +matplotlib.use("agg") # same as GitHub action runner +_ = spatialdata_plot + +# WARNING: +# 1. all classes must both subclass PlotTester and use metaclass=PlotTesterMeta +# 2. tests which produce a plot must be prefixed with `test_plot_` +# 3. if the tolerance needs to be changed, don't prefix the function with `test_plot_`, but with something else +# the comp. function can be accessed as `self.compare(, tolerance=)` +# ".png" is appended to , no need to set it + + +class TestColorbarControls(PlotTester, metaclass=PlotTesterMeta): + def _make_multi_image_single_channel_sdata(self) -> SpatialData: + img1 = Image2DModel.parse(np.linspace(0, 1, 2500, dtype=float).reshape(1, 50, 50), dims=("c", "y", "x")) + img2 = Image2DModel.parse(np.linspace(2, 0, 2500, dtype=float).reshape(1, 50, 50), dims=("c", "y", "x")) + labels = Labels2DModel.parse(np.arange(2500, dtype=int).reshape(50, 50), dims=("y", "x")) + return SpatialData(images={"img1": img1, "img2": img2}, labels={"lab": labels}) + + def test_plot_image_auto_colorbar_for_single_channel(self, sdata_blobs: SpatialData): + # Create a single-channel image by selecting only channel 0 + original_img = sdata_blobs["blobs_image"] + single_channel_img = original_img.isel(c=0).expand_dims("c") + sdata_blobs["blobs_image_1c"] = single_channel_img + sdata_blobs.pl.render_images(element="blobs_image_1c").pl.show() + + def test_plot_colorbar_img_default_location(self, sdata_blobs: SpatialData): + sdata_blobs.pl.render_images("blobs_image", channel=0, cmap="Reds").pl.show() + + def test_plot_colorbar_img_bottom(self, sdata_blobs: SpatialData): + sdata_blobs.pl.render_images("blobs_image", channel=0, cmap="Reds", colorbar_params={"loc": "bottom"}).pl.show() + + def test_plot_colorbar_img_left(self, sdata_blobs: SpatialData): + sdata_blobs.pl.render_images("blobs_image", channel=0, cmap="Reds", colorbar_params={"loc": "left"}).pl.show() + + def test_plot_colorbar_img_top(self, sdata_blobs: SpatialData): + sdata_blobs.pl.render_images("blobs_image", channel=0, cmap="Reds", colorbar_params={"loc": "top"}).pl.show() + + def test_plot_colorbar_can_adjust_width(self, sdata_blobs: SpatialData): + sdata_blobs.pl.render_images("blobs_image", channel=0, cmap="Reds", colorbar_params={"width": 0.4}).pl.show() + + def test_plot_colorbar_can_adjust_title(self, sdata_blobs: SpatialData): + sdata_blobs.pl.render_images( + "blobs_image", channel=0, cmap="Reds", colorbar_params={"label": "Intensity"} + ).pl.show() + + def test_plot_colorbar_can_adjust_pad(self, sdata_blobs: SpatialData): + sdata_blobs.pl.render_images("blobs_image", channel=0, cmap="Reds", colorbar_params={"pad": 0.4}).pl.show() + + def test_plot_colorbar_can_have_colorbars_on_different_sides(self, sdata_blobs: SpatialData): + ( + sdata_blobs.pl.render_images("blobs_image", channel=0, cmap="Reds", colorbar_params={"loc": "top"}) + .pl.render_labels("blobs_labels", color="instance_id", colorbar_params={"loc": "bottom"}) + .pl.show() + ) + + def test_plot_colorbar_can_have_two_colorbars_on_same_side(self, sdata_blobs: SpatialData): + ( + sdata_blobs.pl.render_images("blobs_image", channel=0, cmap="Reds") + .pl.render_labels("blobs_labels", color="instance_id") + .pl.show() + ) + + def test_plot_colorbar_can_have_colorbars_on_all_sides(self, sdata_blobs: SpatialData): + # primarily shows that spacing is correct between colorbars and plot elements + shared_params = { + "element": "blobs_image", + "channel": 0, + "cmap": "Reds", + } + ( + sdata_blobs.pl.render_images(**shared_params, colorbar_params={"loc": "top", "label": "top_1"}) + .pl.render_images(**shared_params, colorbar_params={"loc": "right", "label": "right_1"}) + .pl.render_images(**shared_params, colorbar_params={"loc": "left", "label": "left_1"}) + .pl.render_images(**shared_params, colorbar_params={"loc": "bottom", "label": "bottom_1"}) + .pl.render_images(**shared_params, colorbar_params={"loc": "top", "label": "top_2"}) + .pl.render_images(**shared_params, colorbar_params={"loc": "right", "label": "right_2"}) + .pl.render_images(**shared_params, colorbar_params={"loc": "left", "label": "left_2"}) + .pl.render_images(**shared_params, colorbar_params={"loc": "bottom", "label": "bottom_2"}) + .pl.show() + ) + + def test_plot_multiple_images_in_one_cs_result_in_multiple_colorbars(self): + sdata = self._make_multi_image_single_channel_sdata() + sdata.pl.render_images(channel=0, cmap="Reds").pl.show() + + def test_plot_can_globally_turn_off_colorbars(self): + # adresses https://github.com/scverse/spatialdata-plot/issues/431 + sdata = self._make_multi_image_single_channel_sdata() + sdata.pl.render_images(channel=0, cmap="Reds").pl.show(colorbar=False) + + def test_plot_single_channel_default_channel_name_omits_label(self): + sdata = self._make_multi_image_single_channel_sdata() + sdata.pl.render_images(element="img1", channel=0, cmap="Reds").pl.show()