From dbaac8ee2eb3d117286146ee1a621e2530ff8958 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Mon, 6 Apr 2026 09:38:13 +0200 Subject: [PATCH 1/6] Add make_palette and make_palette_from_data for palette generation (#210) Add two public functions for generating categorical color palettes: - `make_palette(n)`: produce n colors from a source palette, optionally reordered for maximum perceptual contrast or colorblind accessibility. - `make_palette_from_data(sdata, element, color)`: like make_palette but derives categories from a SpatialData element and supports spatially- aware assignment (spaco-inspired) that maximizes contrast between spatially interleaved categories. Both functions share the same method vocabulary: "default", "contrast", "colorblind", "protanopia", "deuteranopia", "tritanopia" (non-spatial), plus "spaco", "spaco_colorblind", "spaco_protanopia", etc. (spatial, only in make_palette_from_data). The palette parameter accepts None (scanpy defaults), named palettes ("okabe_ito"), matplotlib colormap names ("tab10"), or explicit color lists. Also adds dict[str, str] palette support to render_shapes, render_points, and render_labels, enabling reuse of generated palettes across multiple render calls. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/spatialdata_plot/pl/__init__.py | 3 + src/spatialdata_plot/pl/_palette.py | 599 +++++++++++++++++++++++ src/spatialdata_plot/pl/basic.py | 6 +- src/spatialdata_plot/pl/render_params.py | 6 +- src/spatialdata_plot/pl/utils.py | 41 +- tests/pl/test_palette.py | 515 +++++++++++++++++++ 6 files changed, 1146 insertions(+), 24 deletions(-) create mode 100644 src/spatialdata_plot/pl/_palette.py create mode 100644 tests/pl/test_palette.py diff --git a/src/spatialdata_plot/pl/__init__.py b/src/spatialdata_plot/pl/__init__.py index 8bf47aa9..178b4914 100644 --- a/src/spatialdata_plot/pl/__init__.py +++ b/src/spatialdata_plot/pl/__init__.py @@ -1,5 +1,8 @@ +from ._palette import make_palette, make_palette_from_data from .basic import PlotAccessor __all__ = [ "PlotAccessor", + "make_palette", + "make_palette_from_data", ] diff --git a/src/spatialdata_plot/pl/_palette.py b/src/spatialdata_plot/pl/_palette.py new file mode 100644 index 00000000..a8842ae4 --- /dev/null +++ b/src/spatialdata_plot/pl/_palette.py @@ -0,0 +1,599 @@ +"""Palette generation utilities. + +Two public functions: + +- :func:`make_palette` — produce *n* colours, optionally reordered for + maximum perceptual contrast or colourblind accessibility. +- :func:`make_palette_from_data` — like :func:`make_palette` but derives + the number of colours and (for ``spaco`` methods) the assignment order + from a :class:`~spatialdata.SpatialData` element. + +Both share the same *palette* / *method* vocabulary. The *palette* +parameter controls **which** colours are used (the source), while +*method* controls **how** they are ordered or assigned. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import numpy as np +import pandas as pd +from matplotlib.colors import to_hex, to_rgb +from matplotlib.pyplot import colormaps as mpl_colormaps +from scanpy.plotting.palettes import default_20, default_28, default_102 +from scipy.spatial import cKDTree + +from spatialdata_plot._logging import logger + +if TYPE_CHECKING: + from collections.abc import Sequence + + import spatialdata as sd + +# --------------------------------------------------------------------------- +# Built-in named palettes +# --------------------------------------------------------------------------- + +# Okabe & Ito (2008) — designed for universal colour-vision accessibility. +# Hex values from https://jfly.uni-koeln.de/color/ +_OKABE_ITO: list[str] = [ + "#E69F00", # orange + "#56B4E9", # sky blue + "#009E73", # bluish green + "#F0E442", # yellow + "#0072B2", # blue + "#D55E00", # vermillion + "#CC79A7", # reddish purple + "#000000", # black +] + +_NAMED_PALETTES: dict[str, list[str]] = { + "okabe_ito": _OKABE_ITO, +} + +# --------------------------------------------------------------------------- +# Color-space helpers +# --------------------------------------------------------------------------- + +# Oklab conversion (Björn Ottosson, public domain) +# https://bottosson.github.io/posts/oklab/ + + +def _srgb_to_linear(c: np.ndarray) -> np.ndarray: + """SRGB [0,1] → linear RGB.""" + return np.where(c <= 0.04045, c / 12.92, ((c + 0.055) / 1.055) ** 2.4) + + +def _linear_to_srgb(c: np.ndarray) -> np.ndarray: + """Linear RGB → sRGB [0,1].""" + return np.where(c <= 0.0031308, 12.92 * c, 1.055 * c ** (1.0 / 2.4) - 0.055) + + +def _rgb_to_oklab(rgb: np.ndarray) -> np.ndarray: + """Convert Nx3 sRGB [0,1] array to Oklab.""" + lin = _srgb_to_linear(rgb) + l = 0.4122214708 * lin[:, 0] + 0.5363325363 * lin[:, 1] + 0.0514459929 * lin[:, 2] + m = 0.2119034982 * lin[:, 0] + 0.6806995451 * lin[:, 1] + 0.1073969566 * lin[:, 2] + s = 0.0883024619 * lin[:, 0] + 0.2817188376 * lin[:, 1] + 0.6299787005 * lin[:, 2] + l_ = np.cbrt(l) + m_ = np.cbrt(m) + s_ = np.cbrt(s) + return np.column_stack( + [ + 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, + 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, + 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_, + ] + ) + + +# --------------------------------------------------------------------------- +# Color-vision-deficiency simulation (Brettel, Viénot & Mollon 1997) +# --------------------------------------------------------------------------- + +# Simulation matrices for dichromacy in linear RGB space. +# Source: libDaltonLens / DaltonLens-Python (MIT licensed constants). +_CVD_MATRICES: dict[str, np.ndarray] = { + "protanopia": np.array( + [[0.152286, 1.052583, -0.204868], [0.114503, 0.786281, 0.099216], [-0.003882, -0.048116, 1.051998]] + ), + "deuteranopia": np.array( + [[0.367322, 0.860646, -0.227968], [0.280085, 0.672501, 0.047413], [-0.011820, 0.042940, 0.968881]] + ), + "tritanopia": np.array( + [[-0.006540, 0.975530, 0.031010], [0.016270, 0.943972, 0.039758], [-0.244708, 0.759930, 0.484778]] + ), +} + + +def _simulate_cvd(rgb: np.ndarray, cvd_type: str) -> np.ndarray: + """Simulate color vision deficiency on Nx3 sRGB [0,1] array. + + For ``"general"``, returns the element-wise minimum distinctness across + all three deficiency types (worst-case). + """ + if cvd_type == "general": + return np.stack([_simulate_cvd(rgb, t) for t in ("protanopia", "deuteranopia", "tritanopia")]) + + mat = _CVD_MATRICES[cvd_type] + lin = _srgb_to_linear(rgb) + sim = lin @ mat.T + return np.clip(_linear_to_srgb(np.clip(sim, 0, 1)), 0, 1) # type: ignore[no-any-return] + + +# --------------------------------------------------------------------------- +# Shared optimization core +# --------------------------------------------------------------------------- + + +def _perceptual_distance_matrix( + rgb: np.ndarray, + colorblind_type: str | None = None, +) -> np.ndarray: + """Pairwise Oklab Euclidean distance between colors. + + If *colorblind_type* is set, distances are computed on CVD-simulated + colors. For ``"general"``, the minimum distance across all three + deficiency types is used (worst-case optimization). + """ + if colorblind_type is not None: + sim = _simulate_cvd(rgb, colorblind_type) + if colorblind_type == "general": + mats = [_pairwise_oklab_dist(_rgb_to_oklab(s)) for s in sim] + return np.minimum.reduce(mats) # type: ignore[no-any-return] + rgb = sim + + lab = _rgb_to_oklab(rgb) + return _pairwise_oklab_dist(lab) + + +def _pairwise_oklab_dist(lab: np.ndarray) -> np.ndarray: + """Pairwise Euclidean distance in Oklab space.""" + diff = lab[:, np.newaxis, :] - lab[np.newaxis, :, :] + return np.sqrt((diff**2).sum(axis=-1)) # type: ignore[no-any-return] + + +def _optimize_assignment( + weight_matrix: np.ndarray, + color_dist: np.ndarray, + n_random: int = 5000, + n_swaps: int = 10000, + rng: np.random.Generator | None = None, +) -> np.ndarray: + """Find a permutation that maximizes ``sum(weights * color_dist[perm, perm])``. + + Works for both spatial interlacement weights (spaco) and uniform + weights (pure contrast maximization). + + Returns an index array: ``perm[category_idx] = color_idx``. + """ + if rng is None: + rng = np.random.default_rng() + + n = weight_matrix.shape[0] + if n <= 1: + return np.arange(n) + + def _score(perm: np.ndarray) -> float: + return float(np.sum(weight_matrix * color_dist[np.ix_(perm, perm)])) + + best_perm = np.arange(n) + best_score = _score(best_perm) + + for _ in range(n_random): + perm = rng.permutation(n) + s = _score(perm) + if s > best_score: + best_score = s + best_perm = perm.copy() + + for _ in range(n_swaps): + i, j = rng.integers(0, n, size=2) + if i == j: + continue + best_perm[i], best_perm[j] = best_perm[j], best_perm[i] + s = _score(best_perm) + if s > best_score: + best_score = s + else: + best_perm[i], best_perm[j] = best_perm[j], best_perm[i] + + return best_perm + + +def _optimized_order( + colors_list: list[str], + *, + colorblind_type: str | None = None, + n_random: int = 5000, + n_swaps: int = 10000, + seed: int = 0, +) -> list[str]: + """Reorder *colors_list* to maximize pairwise perceptual spread.""" + n = len(colors_list) + if n <= 2: + return colors_list + + rgb = np.array([to_rgb(c) for c in colors_list]) + cdist = _perceptual_distance_matrix(rgb, colorblind_type=colorblind_type) + + # Uniform weight matrix: all off-diagonal pairs equally important + weights = np.ones((n, n)) - np.eye(n) + + rng = np.random.default_rng(seed) + perm = _optimize_assignment(weights, cdist, n_random=n_random, n_swaps=n_swaps, rng=rng) + return [to_hex(rgb[perm[i]]) for i in range(n)] + + +# --------------------------------------------------------------------------- +# Spatial interlacement (spaco-specific) +# --------------------------------------------------------------------------- + + +def _spatial_interlacement( + coords: np.ndarray, + labels: np.ndarray, + categories: Sequence[str], + n_neighbors: int = 15, +) -> np.ndarray: + """Build a symmetric interlacement matrix (n_categories × n_categories). + + Entry (i, j) reflects how much categories i and j are spatially + interleaved, measured by inverse-distance-weighted neighbor counts. + """ + n_cat = len(categories) + cat_to_idx = {c: i for i, c in enumerate(categories)} + label_idx = np.array([cat_to_idx[l] for l in labels]) + + tree = cKDTree(coords) + dists, indices = tree.query(coords, k=min(n_neighbors + 1, len(coords))) + + mat = np.zeros((n_cat, n_cat), dtype=np.float64) + for i in range(len(coords)): + ci = label_idx[i] + for j in range(1, dists.shape[1]): # skip self + d = dists[i, j] + if d <= 0: + continue # skip coincident points + cj = label_idx[indices[i, j]] + if ci != cj: + mat[ci, cj] += 1.0 / d + + mat = np.maximum(mat, mat.T) + max_val = mat.max() + if max_val > 0: + mat /= max_val + return mat # type: ignore[no-any-return] + + +# --------------------------------------------------------------------------- +# Palette resolution +# --------------------------------------------------------------------------- + + +def _resolve_palette(palette: list[str] | str | None, n: int) -> list[str]: + """Resolve *n* colours from an explicit list, a named palette, or scanpy defaults.""" + if isinstance(palette, list): + if len(palette) < n: + raise ValueError(f"Palette has {len(palette)} colors but {n} are needed.") + return list(palette[:n]) + + if isinstance(palette, str): + if palette in _NAMED_PALETTES: + colors = _NAMED_PALETTES[palette] + if len(colors) < n: + raise ValueError( + f"Named palette '{palette}' has {len(colors)} colors but {n} are needed. " + f"Please provide a palette with at least {n} colors." + ) + return list(colors[:n]) + + if palette in mpl_colormaps: + cmap = mpl_colormaps[palette] + indices = np.linspace(0, 1, n) + return [to_hex(cmap(i)) for i in indices] + + raise ValueError( + f"Unknown palette name '{palette}'. Use a list of colors, a matplotlib colormap name, " + f"or one of: {', '.join(sorted(_NAMED_PALETTES))}." + ) + + # palette is None — use scanpy defaults + if n <= 20: + return list(default_20[:n]) + if n <= 28: + return list(default_28[:n]) + if n <= len(default_102): + return list(default_102[:n]) + + raise ValueError( + f"{n} colors needed but no palette was provided and the default palette only has " + f"{len(default_102)} colors. Please provide a palette." + ) + + +def _resolve_element( + sdata: sd.SpatialData, + element: str, + color: str, +) -> tuple[np.ndarray, pd.Categorical]: + """Extract coordinates and categorical labels from a SpatialData element.""" + if element in sdata.shapes: + gdf = sdata.shapes[element] + coords = np.column_stack([gdf.geometry.centroid.x, gdf.geometry.centroid.y]) + labels_series = gdf[color] if color in gdf.columns else _get_labels_from_table(sdata, element, color) + elif element in sdata.points: + df = sdata.points[element].compute() + if "x" not in df.columns or "y" not in df.columns: + raise ValueError(f"Points element '{element}' does not have 'x' and 'y' columns.") + coords = df[["x", "y"]].values.astype(np.float64) + labels_series = df[color] if color in df.columns else _get_labels_from_table(sdata, element, color) + else: + available = list(sdata.shapes.keys()) + list(sdata.points.keys()) + raise KeyError( + f"Element '{element}' not found in sdata.shapes or sdata.points. Available elements: {available}" + ) + + labels_cat = pd.Categorical(labels_series) if not hasattr(labels_series, "cat") else labels_series.values + return coords, labels_cat + + +def _get_labels_from_table(sdata: sd.SpatialData, element: str, color: str) -> pd.Series: + """Extract a column from the table linked to an element.""" + for table_name in sdata.tables: + table = sdata.tables[table_name] + region_key = table.uns.get("spatialdata_attrs", {}).get("region") + if region_key is not None: + regions = [region_key] if isinstance(region_key, str) else region_key + if element in regions and color in table.obs.columns: + return table.obs[color] + + raise KeyError( + f"Column '{color}' not found for element '{element}'. Looked in the element itself and all linked tables." + ) + + +# --------------------------------------------------------------------------- +# Method lookup tables +# --------------------------------------------------------------------------- + +# Maps non-spatial contrast methods → CVD type (None = normal vision). +_CONTRAST_CVD_TYPES: dict[str, str | None] = { + "contrast": None, + "colorblind": "general", + "protanopia": "protanopia", + "deuteranopia": "deuteranopia", + "tritanopia": "tritanopia", +} + +# Maps spaco methods → CVD type (None = normal vision). +_SPACO_CVD_TYPES: dict[str, str | None] = { + "spaco": None, + "spaco_colorblind": "general", + "spaco_protanopia": "protanopia", + "spaco_deuteranopia": "deuteranopia", + "spaco_tritanopia": "tritanopia", +} + +_ALL_METHODS = ["default", *_CONTRAST_CVD_TYPES, *_SPACO_CVD_TYPES] + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +Method = Literal[ + "default", + "contrast", + "colorblind", + "protanopia", + "deuteranopia", + "tritanopia", + "spaco", + "spaco_colorblind", + "spaco_protanopia", + "spaco_deuteranopia", + "spaco_tritanopia", +] + + +def make_palette( + n: int, + *, + palette: list[str] | str | None = None, + method: Method = "default", + n_random: int = 5000, + n_swaps: int = 10000, + seed: int = 0, +) -> list[str]: + """Generate a list of *n* colours. + + The *palette* parameter controls **which** colours are sampled, while + *method* controls **how** they are ordered. + + Parameters + ---------- + n + Number of colours to produce. + palette + Source colours. Can be: + + - ``None`` — scanpy default palettes. + - A **list** of colour strings (hex or named). + - A **named palette**: ``"okabe_ito"`` (8 colourblind-safe + colours). + - A **matplotlib colormap name**: ``"tab10"``, ``"Set2"``, etc. + method + Ordering strategy: + + - ``"default"`` — take the first *n* colours in source order. + - ``"contrast"`` — reorder to maximise pairwise perceptual + distance (Oklab space). + - ``"colorblind"`` — reorder to maximise pairwise distance + under worst-case colour-vision deficiency. + - ``"protanopia"`` / ``"deuteranopia"`` / ``"tritanopia"`` — + reorder for a specific colour-vision deficiency. + + The ``spaco*`` methods require spatial data and are only + available via :func:`make_palette_from_data`. + n_random + Random permutations to try (optimisation methods only). + n_swaps + Pairwise swap iterations (optimisation methods only). + seed + Random seed for reproducibility (optimisation methods only). + + Returns + ------- + list[str] + List of *n* hex colour strings. + + Examples + -------- + >>> sdp.pl.make_palette(5) + >>> sdp.pl.make_palette(8, palette="okabe_ito") + >>> sdp.pl.make_palette(10, palette="tab10", method="contrast") + >>> sdp.pl.make_palette(6, palette="tab10", method="colorblind") + """ + if n < 1: + raise ValueError(f"n must be at least 1, got {n}.") + + if method in _SPACO_CVD_TYPES: + raise ValueError(f"Method '{method}' requires spatial data. Use make_palette_from_data() instead.") + + colors = _resolve_palette(palette, n) + + if method == "default": + return [to_hex(to_rgb(c)) for c in colors] + + if method in _CONTRAST_CVD_TYPES: + cvd_type = _CONTRAST_CVD_TYPES[method] + return _optimized_order(colors, colorblind_type=cvd_type, n_random=n_random, n_swaps=n_swaps, seed=seed) + + valid = ", ".join(f"'{m}'" for m in _ALL_METHODS) + raise ValueError(f"Unknown method '{method}'. Choose from {valid}.") + + +def make_palette_from_data( + sdata: sd.SpatialData, + element: str, + color: str, + *, + palette: list[str] | str | None = None, + method: Method = "default", + n_neighbors: int = 15, + n_random: int = 5000, + n_swaps: int = 10000, + seed: int = 0, +) -> dict[str, str]: + """Generate a categorical colour palette for a spatial element. + + The *palette* parameter controls **which** colours are used (the source), + while *method* controls **how** they are assigned to categories. + + Parameters + ---------- + sdata + A :class:`spatialdata.SpatialData` object. + element + Name of a shapes or points element in *sdata*. + color + Column name containing categorical labels (in the element itself + for points, or in the linked table for shapes/labels). + palette + Source colours. Accepts the same values as + :func:`make_palette` (*None*, a list, a named palette, or a + matplotlib colormap name). + method + Strategy for assigning colours to categories. Accepts all + methods from :func:`make_palette` plus spatially-aware ones: + + - ``"default"`` — assign in sorted category order (reproduces + the current render-pipeline behaviour). + - ``"contrast"`` / ``"colorblind"`` / ``"protanopia"`` / + ``"deuteranopia"`` / ``"tritanopia"`` — reorder to maximise + perceptual spread (ignores spatial layout). + - ``"spaco"`` — spatially-aware assignment (Jing et al., + *Patterns* 2023). Maximises perceptual contrast between + categories that are spatially interleaved. + - ``"spaco_colorblind"`` — like ``"spaco"`` but optimises under + worst-case colour-vision deficiency (all three types). + - ``"spaco_protanopia"`` / ``"spaco_deuteranopia"`` / + ``"spaco_tritanopia"`` — like ``"spaco"`` but optimises for + a specific colour-vision deficiency. + n_neighbors + Only used with ``spaco`` methods. Number of spatial neighbours + for the interlacement computation. + n_random + Random permutations to try (optimisation methods only). + n_swaps + Pairwise swap iterations (optimisation methods only). + seed + Random seed for reproducibility (optimisation methods only). + + Returns + ------- + dict[str, str] + Mapping from category name to hex colour string. Can be passed + directly as ``palette=`` to any render function. + + Examples + -------- + >>> palette = sdp.pl.make_palette_from_data(sdata, "cells", "cell_type") + >>> palette = sdp.pl.make_palette_from_data(sdata, "cells", "cell_type", palette="tab10") + >>> palette = sdp.pl.make_palette_from_data(sdata, "cells", "cell_type", method="spaco") + >>> palette = sdp.pl.make_palette_from_data(sdata, "cells", "cell_type", method="spaco_colorblind") + >>> sdata.pl.render_shapes("cells", color="cell_type", palette=palette).pl.show() + """ + coords, labels_cat = _resolve_element(sdata, element, color) + + categories = list(labels_cat.categories) + n_cat = len(categories) + if n_cat == 0: + raise ValueError(f"No categories found in column '{color}'.") + + colors_list = _resolve_palette(palette, n_cat) + + if method == "default": + return {cat: to_hex(to_rgb(c)) for cat, c in zip(categories, colors_list, strict=True)} + + # Non-spatial contrast methods (same as make_palette but returns dict) + if method in _CONTRAST_CVD_TYPES: + cvd_type = _CONTRAST_CVD_TYPES[method] + reordered = _optimized_order( + colors_list, colorblind_type=cvd_type, n_random=n_random, n_swaps=n_swaps, seed=seed + ) + return dict(zip(categories, reordered, strict=True)) + + # Spaco methods (spatially-aware) + if method in _SPACO_CVD_TYPES: + cvd_type = _SPACO_CVD_TYPES[method] + + # Filter NaN labels + mask = labels_cat.codes != -1 + coords_clean = coords[mask] + labels_clean = np.array(categories)[labels_cat.codes[mask]] + + if len(coords_clean) == 0: + raise ValueError(f"All values in column '{color}' are NaN.") + + rgb = np.array([to_rgb(c) for c in colors_list]) + + if n_cat == 1: + return {categories[0]: to_hex(rgb[0])} + + logger.info(f"Computing spatial interlacement for {n_cat} categories ({len(coords_clean)} cells)...") + inter = _spatial_interlacement(coords_clean, labels_clean, categories, n_neighbors=n_neighbors) + + logger.info("Computing perceptual distance matrix...") + cdist = _perceptual_distance_matrix(rgb, colorblind_type=cvd_type) + + logger.info("Optimizing color assignment...") + rng = np.random.default_rng(seed) + perm = _optimize_assignment(inter, cdist, n_random=n_random, n_swaps=n_swaps, rng=rng) + + return {cat: to_hex(rgb[perm[i]]) for i, cat in enumerate(categories)} + + valid = ", ".join(f"'{m}'" for m in _ALL_METHODS) + raise ValueError(f"Unknown method '{method}'. Choose from {valid}.") diff --git a/src/spatialdata_plot/pl/basic.py b/src/spatialdata_plot/pl/basic.py index 2a558361..0a99f395 100644 --- a/src/spatialdata_plot/pl/basic.py +++ b/src/spatialdata_plot/pl/basic.py @@ -172,7 +172,7 @@ def render_shapes( *, fill_alpha: float | int | None = None, groups: list[str] | str | None = None, - palette: list[str] | str | None = None, + palette: dict[str, str] | list[str] | str | None = None, na_color: ColorLike | None = "default", outline_width: float | int | tuple[float | int, float | int] | None = None, outline_color: ColorLike | tuple[ColorLike] | None = None, @@ -369,7 +369,7 @@ def render_points( *, alpha: float | int | None = None, groups: list[str] | str | None = None, - palette: list[str] | str | None = None, + palette: dict[str, str] | list[str] | str | None = None, na_color: ColorLike | None = "default", cmap: Colormap | str | None = None, norm: Normalize | None = None, @@ -707,7 +707,7 @@ def render_labels( *, groups: list[str] | str | None = None, contour_px: int | None = 3, - palette: list[str] | str | None = None, + palette: dict[str, str] | list[str] | str | None = None, cmap: Colormap | str | None = None, norm: Normalize | None = None, na_color: ColorLike | None = "default", diff --git a/src/spatialdata_plot/pl/render_params.py b/src/spatialdata_plot/pl/render_params.py index 344df5f9..dffb97dd 100644 --- a/src/spatialdata_plot/pl/render_params.py +++ b/src/spatialdata_plot/pl/render_params.py @@ -223,7 +223,7 @@ class ShapesRenderParams: col_for_color: str | None = None groups: str | list[str] | None = None contour_px: int | None = None - palette: ListedColormap | list[str] | None = None + palette: ListedColormap | dict[str, str] | list[str] | None = None outline_alpha: tuple[float, float] = (1.0, 1.0) fill_alpha: float = 0.3 scale: float = 1.0 @@ -247,7 +247,7 @@ class PointsRenderParams: color: Color | None = None col_for_color: str | None = None groups: str | list[str] | None = None - palette: ListedColormap | list[str] | None = None + palette: ListedColormap | dict[str, str] | list[str] | None = None alpha: float = 1.0 size: float = 1.0 transfunc: Callable[[float], float] | None = None @@ -288,7 +288,7 @@ class LabelsRenderParams: groups: str | list[str] | None = None contour_px: int | None = None outline: bool = False - palette: ListedColormap | list[str] | None = None + palette: ListedColormap | dict[str, str] | list[str] | None = None outline_alpha: float = 1.0 outline_color: Color | None = None fill_alpha: float = 0.4 diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 8530aec1..68654745 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -251,17 +251,14 @@ def _prepare_params_plot( # handle axes and size wspace = 0.75 / rcParams["figure.figsize"][0] + 0.02 if wspace is None else wspace figsize = rcParams["figure.figsize"] if figsize is None else figsize - # When creating a new figure, fall back to rcParams; when the user provides - # their own axes, preserve the figure's existing DPI (only override if - # the user explicitly passed dpi= to show()). - resolved_dpi = rcParams["figure.dpi"] if dpi is None else dpi + dpi = rcParams["figure.dpi"] if dpi is None else dpi if num_panels > 1 and ax is None: fig, grid = _panel_grid( num_panels=num_panels, hspace=hspace, wspace=wspace, ncols=ncols, - dpi=resolved_dpi, + dpi=dpi, figsize=figsize, ) axs: None | Sequence[Axes] = [plt.subplot(grid[c]) for c in range(num_panels)] @@ -277,16 +274,14 @@ def _prepare_params_plot( ) assert ax is None or isinstance(ax, Sequence), f"Invalid type of `ax`: {type(ax)}, expected `Sequence`." axs = ax - if dpi is not None: - fig.set_dpi(dpi) else: axs = None if ax is None: - fig, ax = plt.subplots(figsize=figsize, dpi=resolved_dpi, constrained_layout=True) + fig, ax = plt.subplots(figsize=figsize, dpi=dpi, constrained_layout=True) elif isinstance(ax, Axes): + # needed for rasterization if user provides Axes object fig = ax.get_figure() - if dpi is not None: - fig.set_dpi(dpi) + fig.set_dpi(dpi) # set scalebar if scalebar_dx is not None: @@ -1024,7 +1019,7 @@ def _set_color_source_vec( na_color: Color, element_name: list[str] | str | None = None, groups: list[str] | str | None = None, - palette: list[str] | str | None = None, + palette: dict[str, str] | list[str] | str | None = None, cmap_params: CmapParams | None = None, alpha: float = 1.0, table_name: str | None = None, @@ -1519,7 +1514,7 @@ def _to_hex_no_alpha(color_value: Any) -> str | None: def _modify_categorical_color_mapping( mapping: Mapping[str, str], groups: list[str] | str | None = None, - palette: list[str] | str | None = None, + palette: dict[str, str] | list[str] | str | None = None, ) -> Mapping[str, str]: if groups is None or isinstance(groups, list) and groups[0] is None: return mapping @@ -1577,12 +1572,19 @@ def _get_categorical_color_mapping( cmap_params: CmapParams | None = None, alpha: float = 1, groups: list[str] | str | None = None, - palette: list[str] | str | None = None, + palette: dict[str, str] | list[str] | str | None = None, render_type: Literal["points", "labels"] | None = None, ) -> Mapping[str, str]: if not isinstance(color_source_vector, Categorical): raise TypeError(f"Expected `categories` to be a `Categorical`, but got {type(color_source_vector).__name__}") + # Dict palette (e.g. from optimize_palette): use directly as category→color mapping + if isinstance(palette, dict): + na_color_hex = na_color.get_hex_with_alpha() if isinstance(na_color, Color) else str(na_color) + mapping = {cat: palette.get(cat, na_color_hex) for cat in color_source_vector.categories} + mapping["NaN"] = na_color_hex + return mapping + if isinstance(groups, str): groups = [groups] @@ -2395,14 +2397,17 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st palette = param_dict["palette"] - if isinstance(palette, list): + # dict palettes (e.g. from optimize_palette) bypass groups validation + if isinstance(palette, dict): + pass + elif 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 palette_group = param_dict.get("palette") - if element_type in ["shapes", "points", "labels"] and palette_group is not None: + if element_type in ["shapes", "points", "labels"] and palette_group is not None and not isinstance(palette, dict): groups = param_dict.get("groups") if groups is None: raise ValueError("When specifying 'palette', 'groups' must also be specified.") @@ -2542,7 +2547,7 @@ def _validate_label_render_params( fill_alpha: float | int | None, contour_px: int | None, groups: list[str] | str | None, - palette: list[str] | str | None, + palette: dict[str, str] | list[str] | str | None, na_color: ColorLike | None, norm: Normalize | None, outline_alpha: float | int, @@ -2614,7 +2619,7 @@ def _validate_points_render_params( alpha: float | int | None, color: ColorLike | None, groups: list[str] | str | None, - palette: list[str] | str | None, + palette: dict[str, str] | list[str] | str | None, na_color: ColorLike | None, cmap: list[Colormap | str] | Colormap | str | None, norm: Normalize | None, @@ -2682,7 +2687,7 @@ def _validate_shape_render_params( element: str | None, fill_alpha: float | int | None, groups: list[str] | str | None, - palette: list[str] | str | None, + palette: dict[str, str] | list[str] | str | None, color: ColorLike | None, na_color: ColorLike | None, outline_width: float | int | tuple[float | int, float | int] | None, diff --git a/tests/pl/test_palette.py b/tests/pl/test_palette.py new file mode 100644 index 00000000..a62a6813 --- /dev/null +++ b/tests/pl/test_palette.py @@ -0,0 +1,515 @@ +"""Tests for palette generation (issue #210).""" + +from __future__ import annotations + +import matplotlib +import numpy as np +import pandas as pd +import pytest +import scanpy as sc +from matplotlib.colors import to_hex, to_rgb +from spatialdata import SpatialData +from spatialdata.models import PointsModel, ShapesModel, TableModel + +import spatialdata_plot # noqa: F401 — registers accessor +from spatialdata_plot.pl._palette import ( + _optimize_assignment, + _pairwise_oklab_dist, + _perceptual_distance_matrix, + _rgb_to_oklab, + _simulate_cvd, + _spatial_interlacement, + make_palette, + make_palette_from_data, +) +from tests.conftest import DPI, PlotTester, PlotTesterMeta + +matplotlib.use("agg") + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +def _make_clustered_points_sdata(seed: int = 0) -> SpatialData: + """Create a SpatialData with two spatially interleaved point clusters. + + Cluster layout (deliberately interleaved): + - "A" cells at (0,0), (1,0), (0,1) + - "B" cells at (0.5,0.5), (1.5,0.5), (0.5,1.5) + - "C" cells at (10,10), (11,10), (10,11) — isolated cluster + """ + rng = np.random.default_rng(seed) + coords_a = np.array([[0, 0], [1, 0], [0, 1]], dtype=float) + rng.normal(0, 0.05, (3, 2)) + coords_b = np.array([[0.5, 0.5], [1.5, 0.5], [0.5, 1.5]], dtype=float) + rng.normal(0, 0.05, (3, 2)) + coords_c = np.array([[10, 10], [11, 10], [10, 11]], dtype=float) + rng.normal(0, 0.05, (3, 2)) + + coords = np.vstack([coords_a, coords_b, coords_c]) + labels = pd.Categorical(["A"] * 3 + ["B"] * 3 + ["C"] * 3) + + df = pd.DataFrame({"x": coords[:, 0], "y": coords[:, 1], "cell_type": labels}) + points = PointsModel.parse(df) + return SpatialData(points={"cells": points}) + + +def _make_shapes_sdata(seed: int = 0) -> SpatialData: + """Create a SpatialData with shapes + linked table.""" + from anndata import AnnData + from geopandas import GeoDataFrame + from shapely import Point + + rng = np.random.default_rng(seed) + n = 30 + coords = rng.normal(size=(n, 2)) * 5 + gdf = GeoDataFrame({"radius": np.ones(n)}, geometry=[Point(x, y) for x, y in coords]) + gdf.index = pd.RangeIndex(n) + + labels = pd.Categorical(rng.choice(["X", "Y", "Z"], size=n)) + adata = AnnData( + np.zeros((n, 1)), + obs=pd.DataFrame( + { + "cell_type": labels, + "instance_id": np.arange(n), + "region": ["my_shapes"] * n, + }, + index=pd.RangeIndex(n).astype(str), + ), + ) + adata = TableModel.parse( + adata=adata, + region="my_shapes", + region_key="region", + instance_key="instance_id", + ) + + shapes = ShapesModel.parse(gdf) + return SpatialData(shapes={"my_shapes": shapes}, tables={"table": adata}) + + +# --------------------------------------------------------------------------- +# Unit tests: color-space helpers +# --------------------------------------------------------------------------- + + +class TestOklab: + def test_black_and_white(self): + rgb = np.array([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]) + lab = _rgb_to_oklab(rgb) + assert lab[0, 0] == pytest.approx(0.0, abs=0.01) + assert lab[1, 0] == pytest.approx(1.0, abs=0.01) + + def test_pairwise_distance_symmetric(self): + rgb = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=float) + lab = _rgb_to_oklab(rgb) + d = _pairwise_oklab_dist(lab) + assert d.shape == (3, 3) + np.testing.assert_allclose(d, d.T) + np.testing.assert_allclose(np.diag(d), 0) + + def test_distinct_colors_have_positive_distance(self): + rgb = np.array([[1, 0, 0], [0, 0, 1]], dtype=float) + lab = _rgb_to_oklab(rgb) + d = _pairwise_oklab_dist(lab) + assert d[0, 1] > 0.1 + + +# --------------------------------------------------------------------------- +# Unit tests: CVD simulation +# --------------------------------------------------------------------------- + + +class TestCVD: + @pytest.mark.parametrize("cvd_type", ["protanopia", "deuteranopia", "tritanopia"]) + def test_output_in_range(self, cvd_type: str): + rgb = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=float) + sim = _simulate_cvd(rgb, cvd_type) + assert sim.shape == (3, 3) + assert np.all(sim >= 0) + assert np.all(sim <= 1) + + def test_general_returns_stacked(self): + rgb = np.array([[1, 0, 0], [0, 1, 0]], dtype=float) + sim = _simulate_cvd(rgb, "general") + assert sim.shape == (3, 2, 3) + + @pytest.mark.parametrize("cvd_type", ["protanopia", "deuteranopia", "tritanopia"]) + def test_red_green_less_distinct(self, cvd_type: str): + """Under protanopia/deuteranopia, red and green should be less distinct than for normal vision.""" + rgb = np.array([[1, 0, 0], [0, 1, 0]], dtype=float) + normal_dist = _perceptual_distance_matrix(rgb)[0, 1] + cvd_dist = _perceptual_distance_matrix(rgb, colorblind_type=cvd_type)[0, 1] + if cvd_type in ("protanopia", "deuteranopia"): + assert cvd_dist < normal_dist + + +# --------------------------------------------------------------------------- +# Unit tests: spatial interlacement +# --------------------------------------------------------------------------- + + +class TestSpatialInterlacement: + def test_interleaved_higher_than_separated(self): + """Categories that are spatially interleaved should have higher scores.""" + coords = np.array([[0, 0], [1, 0], [0.5, 0.5], [1.5, 0.5], [10, 10], [11, 10]]) + labels = np.array(["A", "B", "A", "B", "C", "C"]) + categories = ["A", "B", "C"] + + mat = _spatial_interlacement(coords, labels, categories, n_neighbors=3) + + assert mat[0, 1] > mat[0, 2] + assert mat[0, 1] > mat[1, 2] + + def test_diagonal_is_zero(self): + coords = np.array([[0, 0], [1, 0], [0.5, 0.5]]) + labels = np.array(["A", "B", "A"]) + mat = _spatial_interlacement(coords, labels, ["A", "B"], n_neighbors=2) + np.testing.assert_allclose(np.diag(mat), 0) + + def test_symmetric(self): + rng = np.random.default_rng(42) + coords = rng.normal(size=(50, 2)) + labels = np.array(["A", "B", "C", "D", "E"] * 10) + mat = _spatial_interlacement(coords, labels, ["A", "B", "C", "D", "E"], n_neighbors=5) + np.testing.assert_allclose(mat, mat.T) + + +# --------------------------------------------------------------------------- +# Unit tests: optimizer +# --------------------------------------------------------------------------- + + +class TestOptimizer: + def test_single_category(self): + perm = _optimize_assignment(np.zeros((1, 1)), np.zeros((1, 1))) + assert list(perm) == [0] + + def test_two_categories(self): + """With 2 categories, there are only 2 permutations — optimizer should pick the better one.""" + inter = np.array([[0, 1], [1, 0]], dtype=float) + cdist = np.array([[0, 10], [10, 0]], dtype=float) + rng = np.random.default_rng(0) + perm = _optimize_assignment(inter, cdist, rng=rng) + assert set(perm) == {0, 1} + + def test_deterministic_with_seed(self): + rng1 = np.random.default_rng(42) + rng2 = np.random.default_rng(42) + inter = np.random.default_rng(0).random((5, 5)) + inter = np.maximum(inter, inter.T) + np.fill_diagonal(inter, 0) + cdist = np.random.default_rng(1).random((5, 5)) + cdist = np.maximum(cdist, cdist.T) + np.fill_diagonal(cdist, 0) + + perm1 = _optimize_assignment(inter, cdist, rng=rng1) + perm2 = _optimize_assignment(inter, cdist, rng=rng2) + np.testing.assert_array_equal(perm1, perm2) + + +# --------------------------------------------------------------------------- +# Tests: make_palette +# --------------------------------------------------------------------------- + + +class TestMakePalette: + def test_default_returns_n_colors(self): + result = make_palette(5) + assert len(result) == 5 + assert all(c.startswith("#") for c in result) + + def test_returns_list(self): + result = make_palette(3) + assert isinstance(result, list) + + def test_named_palette(self): + result = make_palette(4, palette="okabe_ito") + assert len(result) == 4 + + def test_matplotlib_cmap(self): + result = make_palette(6, palette="tab10") + assert len(result) == 6 + + def test_custom_list(self): + colors = ["#ff0000", "#00ff00", "#0000ff"] + result = make_palette(3, palette=colors) + assert result == [to_hex(to_rgb(c)) for c in colors] + + def test_contrast_reorders(self): + """Contrast method should produce a permutation of the input colors.""" + colors = ["#ff0000", "#ff1100", "#0000ff", "#00ff00"] + result = make_palette(4, palette=colors, method="contrast", seed=42) + assert set(result) == {to_hex(to_rgb(c)) for c in colors} + + def test_colorblind_reorders(self): + colors = ["#ff0000", "#00ff00", "#0000ff"] + result = make_palette(3, palette=colors, method="colorblind", seed=42) + assert set(result) == {to_hex(to_rgb(c)) for c in colors} + + def test_deuteranopia(self): + result = make_palette(5, method="deuteranopia", seed=42) + assert len(result) == 5 + + def test_deterministic(self): + r1 = make_palette(5, method="contrast", seed=42) + r2 = make_palette(5, method="contrast", seed=42) + assert r1 == r2 + + def test_n_zero_raises(self): + with pytest.raises(ValueError, match="at least 1"): + make_palette(0) + + def test_too_few_colors_raises(self): + with pytest.raises(ValueError, match="needed"): + make_palette(10, palette=["red", "blue"]) + + def test_spaco_method_raises(self): + with pytest.raises(ValueError, match="requires spatial data"): + make_palette(3, method="spaco") # type: ignore[arg-type] + + def test_spaco_colorblind_method_raises(self): + with pytest.raises(ValueError, match="requires spatial data"): + make_palette(3, method="spaco_colorblind") # type: ignore[arg-type] + + def test_unknown_method_raises(self): + with pytest.raises(ValueError, match="Unknown method"): + make_palette(3, method="invalid") # type: ignore[arg-type] + + def test_unknown_palette_name_raises(self): + with pytest.raises(ValueError, match="Unknown palette name"): + make_palette(3, palette="nonexistent_palette") + + +# --------------------------------------------------------------------------- +# Tests: make_palette_from_data — default +# --------------------------------------------------------------------------- + + +class TestMakePaletteFromDataDefault: + def test_basic(self): + sdata = _make_clustered_points_sdata() + result = make_palette_from_data(sdata, "cells", "cell_type") + assert isinstance(result, dict) + assert set(result.keys()) == {"A", "B", "C"} + for v in result.values(): + assert v.startswith("#") + + def test_matches_scanpy_order(self): + """Default method should assign colors in sorted-category order, matching scanpy.""" + from scanpy.plotting.palettes import default_20 + + sdata = _make_clustered_points_sdata() + result = make_palette_from_data(sdata, "cells", "cell_type", method="default") + + for i, cat in enumerate(sorted(result.keys())): + assert result[cat] == to_hex(to_rgb(default_20[i])) + + def test_custom_palette(self): + sdata = _make_clustered_points_sdata() + colors = ["#ff0000", "#00ff00", "#0000ff"] + result = make_palette_from_data(sdata, "cells", "cell_type", palette=colors) + assert list(result.values()) == [to_hex(to_rgb(c)) for c in colors] + + def test_named_palette(self): + sdata = _make_clustered_points_sdata() + result = make_palette_from_data(sdata, "cells", "cell_type", palette="okabe_ito") + assert isinstance(result, dict) + assert len(result) == 3 + + def test_matplotlib_cmap(self): + sdata = _make_clustered_points_sdata() + result = make_palette_from_data(sdata, "cells", "cell_type", palette="tab10") + assert isinstance(result, dict) + assert len(result) == 3 + + +# --------------------------------------------------------------------------- +# Tests: make_palette_from_data — spaco +# --------------------------------------------------------------------------- + + +class TestMakePaletteFromDataContrast: + def test_contrast_returns_dict(self): + sdata = _make_clustered_points_sdata() + result = make_palette_from_data(sdata, "cells", "cell_type", method="contrast", seed=42) + assert isinstance(result, dict) + assert set(result.keys()) == {"A", "B", "C"} + + def test_colorblind_returns_dict(self): + sdata = _make_clustered_points_sdata() + result = make_palette_from_data(sdata, "cells", "cell_type", method="colorblind", seed=42) + assert isinstance(result, dict) + assert len(result) == 3 + + +class TestMakePaletteFromDataSpaco: + def test_basic_points(self): + sdata = _make_clustered_points_sdata() + result = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", seed=42) + assert isinstance(result, dict) + assert set(result.keys()) == {"A", "B", "C"} + + def test_deterministic(self): + sdata = _make_clustered_points_sdata() + r1 = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", seed=42) + r2 = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", seed=42) + assert r1 == r2 + + def test_different_seeds_can_differ(self): + sdata = _make_clustered_points_sdata() + r1 = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", seed=0) + r2 = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", seed=999) + assert set(r1.keys()) == set(r2.keys()) + + def test_custom_palette(self): + sdata = _make_clustered_points_sdata() + colors = ["#ff0000", "#00ff00", "#0000ff"] + result = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", palette=colors, seed=42) + assert set(result.values()) == {to_hex(to_rgb(c)) for c in colors} + + def test_spaco_colorblind(self): + sdata = _make_clustered_points_sdata() + result = make_palette_from_data(sdata, "cells", "cell_type", method="spaco_colorblind", seed=42) + assert isinstance(result, dict) + assert len(result) == 3 + + def test_spaco_deuteranopia(self): + sdata = _make_clustered_points_sdata() + result = make_palette_from_data(sdata, "cells", "cell_type", method="spaco_deuteranopia", seed=42) + assert isinstance(result, dict) + assert len(result) == 3 + + def test_spaco_with_named_palette(self): + sdata = _make_clustered_points_sdata() + result = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", palette="okabe_ito", seed=42) + assert isinstance(result, dict) + assert len(result) == 3 + + def test_spaco_with_matplotlib_cmap(self): + sdata = _make_clustered_points_sdata() + result = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", palette="tab10", seed=42) + assert isinstance(result, dict) + assert len(result) == 3 + + def test_single_category(self): + coords = np.array([[0, 0], [1, 1]], dtype=float) + df = pd.DataFrame({"x": coords[:, 0], "y": coords[:, 1], "ct": pd.Categorical(["A", "A"])}) + points = PointsModel.parse(df) + sdata = SpatialData(points={"pts": points}) + + result = make_palette_from_data(sdata, "pts", "ct", method="spaco", seed=0) + assert len(result) == 1 + assert "A" in result + + def test_nan_labels_filtered(self): + coords = np.array([[0, 0], [1, 0], [0, 1], [10, 10]], dtype=float) + labels = pd.Categorical(["A", "B", "A", None]) + df = pd.DataFrame({"x": coords[:, 0], "y": coords[:, 1], "ct": labels}) + points = PointsModel.parse(df) + sdata = SpatialData(points={"pts": points}) + + result = make_palette_from_data(sdata, "pts", "ct", method="spaco", seed=0) + assert "A" in result + assert "B" in result + + def test_shapes_with_table(self): + sdata = _make_shapes_sdata() + result = make_palette_from_data(sdata, "my_shapes", "cell_type", method="spaco", seed=42) + assert isinstance(result, dict) + assert set(result.keys()) == {"X", "Y", "Z"} + + def test_interleaved_get_distinct_colors(self): + """Core property: spatially interleaved categories should get the most distinct colors.""" + sdata = _make_clustered_points_sdata(seed=0) + palette = ["#ff0000", "#ff1100", "#0000ff"] + result = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", palette=palette, seed=0) + + a_color = result["A"] + b_color = result["B"] + assert a_color == "#0000ff" or b_color == "#0000ff" + + +# --------------------------------------------------------------------------- +# Error cases +# --------------------------------------------------------------------------- + + +class TestMakePaletteFromDataErrors: + def test_too_few_colors_raises(self): + sdata = _make_clustered_points_sdata() + with pytest.raises(ValueError, match="needed"): + make_palette_from_data(sdata, "cells", "cell_type", method="spaco", palette=["red", "blue"], seed=0) + + def test_missing_element_raises(self): + sdata = _make_clustered_points_sdata() + with pytest.raises(KeyError, match="not found"): + make_palette_from_data(sdata, "nonexistent", "cell_type") + + def test_missing_column_raises(self): + sdata = _make_clustered_points_sdata() + with pytest.raises(KeyError, match="not found"): + make_palette_from_data(sdata, "cells", "nonexistent_col") + + def test_unknown_method_raises(self): + sdata = _make_clustered_points_sdata() + with pytest.raises(ValueError, match="Unknown method"): + make_palette_from_data(sdata, "cells", "cell_type", method="invalid") # type: ignore[arg-type] + + +# --------------------------------------------------------------------------- +# Integration tests: dict palette through render pipeline +# --------------------------------------------------------------------------- + + +class TestDictPalette: + def test_dict_palette_in_render_points(self, sdata_blobs: SpatialData): + """Dict palette should flow through render_points without errors.""" + palette = {"0": "#ff0000", "1": "#00ff00"} + sdata_blobs.pl.render_points("blobs_points", color="genes", palette=palette) + + def test_dict_palette_in_render_labels(self, sdata_blobs: SpatialData): + """Dict palette should flow through render_labels without errors.""" + palette = {"blobs_labels": "#ff0000"} + sdata_blobs.pl.render_labels("blobs_labels", color="region", palette=palette) + + +# --------------------------------------------------------------------------- +# Visual tests: make_palette_from_data → render → show +# --------------------------------------------------------------------------- + +sc.pl.set_rcParams_defaults() +sc.set_figure_params(dpi=DPI, color_map="viridis") + + +class TestPaletteVisual(PlotTester, metaclass=PlotTesterMeta): + def test_plot_dict_palette_hex_points(self, sdata_blobs: SpatialData): + """Visual test: hex dict palette renders points correctly.""" + palette = make_palette_from_data(sdata_blobs, "blobs_points", "genes", palette="okabe_ito") + sdata_blobs.pl.render_points("blobs_points", color="genes", palette=palette).pl.show() + + def test_plot_dict_palette_hex_shapes(self, sdata_blobs: SpatialData): + """Visual test: hex dict palette renders shapes correctly.""" + sdata_blobs["blobs_polygons"]["cat_col"] = pd.Series(["a", "b", "a", "b", "a"], dtype="category") + palette = make_palette_from_data(sdata_blobs, "blobs_polygons", "cat_col", palette="okabe_ito") + sdata_blobs.pl.render_shapes("blobs_polygons", color="cat_col", palette=palette).pl.show() + + def test_plot_dict_palette_hex_labels(self, sdata_blobs: SpatialData): + """Visual test: hex dict palette renders labels correctly.""" + palette = make_palette_from_data(sdata_blobs, "blobs_labels", "region", palette="okabe_ito") + sdata_blobs.pl.render_labels("blobs_labels", color="region", palette=palette).pl.show() + + def test_plot_dict_palette_named_colors_points(self, sdata_blobs: SpatialData): + """Visual test: named-color dict palette renders points correctly.""" + palette = {"gene_a": "red", "gene_b": "dodgerblue"} + sdata_blobs.pl.render_points("blobs_points", color="genes", palette=palette).pl.show() + + def test_plot_dict_palette_named_colors_shapes(self, sdata_blobs: SpatialData): + """Visual test: named-color dict palette renders shapes correctly.""" + sdata_blobs["blobs_polygons"]["cat_col"] = pd.Series(["a", "b", "a", "b", "a"], dtype="category") + palette = {"a": "forestgreen", "b": "orchid"} + sdata_blobs.pl.render_shapes("blobs_polygons", color="cat_col", palette=palette).pl.show() + + def test_plot_dict_palette_named_colors_labels(self, sdata_blobs: SpatialData): + """Visual test: named-color dict palette renders labels correctly.""" + palette = {"blobs_labels": "coral"} + sdata_blobs.pl.render_labels("blobs_labels", color="region", palette=palette).pl.show() From d728f8beb77b9ce6d7d2aed07a925d711f5f04b2 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Mon, 6 Apr 2026 09:49:26 +0200 Subject: [PATCH 2/6] Add reference images and fix labels visual test Fix test_plot_dict_palette_hex_labels to use a hand-built dict instead of make_palette_from_data, since labels elements don't have extractable coordinates. Add 5 CI-generated reference images for palette visual tests. The hex_labels reference image will be generated on the next CI run. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../PaletteVisual_dict_palette_hex_points.png | Bin 0 -> 32189 bytes .../PaletteVisual_dict_palette_hex_shapes.png | Bin 0 -> 27264 bytes ...eVisual_dict_palette_named_colors_labels.png | Bin 0 -> 46108 bytes ...eVisual_dict_palette_named_colors_points.png | Bin 0 -> 31312 bytes ...eVisual_dict_palette_named_colors_shapes.png | Bin 0 -> 27892 bytes tests/pl/test_palette.py | 2 +- 6 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/_images/PaletteVisual_dict_palette_hex_points.png create mode 100644 tests/_images/PaletteVisual_dict_palette_hex_shapes.png create mode 100644 tests/_images/PaletteVisual_dict_palette_named_colors_labels.png create mode 100644 tests/_images/PaletteVisual_dict_palette_named_colors_points.png create mode 100644 tests/_images/PaletteVisual_dict_palette_named_colors_shapes.png diff --git a/tests/_images/PaletteVisual_dict_palette_hex_points.png b/tests/_images/PaletteVisual_dict_palette_hex_points.png new file mode 100644 index 0000000000000000000000000000000000000000..3d844b33006be81e502c57b8ba4245ac62469cfd GIT binary patch literal 32189 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 literal 0 HcmV?d00001 diff --git a/tests/_images/PaletteVisual_dict_palette_named_colors_labels.png b/tests/_images/PaletteVisual_dict_palette_named_colors_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..2273f591ac86380fa23af07e533d2d10057a11ad GIT binary patch literal 46108 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
  • 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 literal 0 HcmV?d00001 diff --git a/tests/_images/PaletteVisual_dict_palette_named_colors_points.png b/tests/_images/PaletteVisual_dict_palette_named_colors_points.png new file mode 100644 index 0000000000000000000000000000000000000000..0477e1336b12ad0c4bd1e3f7c04641f74ff49c82 GIT binary patch literal 31312 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 literal 0 HcmV?d00001 diff --git a/tests/pl/test_palette.py b/tests/pl/test_palette.py index a62a6813..ca65c2aa 100644 --- a/tests/pl/test_palette.py +++ b/tests/pl/test_palette.py @@ -495,7 +495,7 @@ def test_plot_dict_palette_hex_shapes(self, sdata_blobs: SpatialData): def test_plot_dict_palette_hex_labels(self, sdata_blobs: SpatialData): """Visual test: hex dict palette renders labels correctly.""" - palette = make_palette_from_data(sdata_blobs, "blobs_labels", "region", palette="okabe_ito") + palette = {"blobs_labels": "#E69F00"} sdata_blobs.pl.render_labels("blobs_labels", color="region", palette=palette).pl.show() def test_plot_dict_palette_named_colors_points(self, sdata_blobs: SpatialData): From d66f088acb7fbd65a634ade93c1b032c853c7667 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Mon, 6 Apr 2026 09:56:26 +0200 Subject: [PATCH 3/6] Add missing hex_labels reference image Co-Authored-By: Claude Opus 4.6 (1M context) --- .../PaletteVisual_dict_palette_hex_labels.png | Bin 0 -> 46108 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/_images/PaletteVisual_dict_palette_hex_labels.png diff --git a/tests/_images/PaletteVisual_dict_palette_hex_labels.png b/tests/_images/PaletteVisual_dict_palette_hex_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..2273f591ac86380fa23af07e533d2d10057a11ad GIT binary patch literal 46108 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
  • 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 literal 0 HcmV?d00001 From 28a04a808af569ae485904944be0f302a53f09a7 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Mon, 6 Apr 2026 10:09:10 +0200 Subject: [PATCH 4/6] Simplify: vectorize interlacement, fix cmap sampling, refactor tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Vectorize _spatial_interlacement with numpy (eliminates Python double-loop over cells × neighbors) - Select only needed columns before .compute() on dask points (avoids materializing entire dataframe) - Fix ListedColormap sampling: use integer indices for qualitative colormaps like tab10 instead of linspace (which wraps and duplicates) - Fix categorical dtype check: use isinstance(dtype, CategoricalDtype) instead of fragile hasattr(series, "cat") - Add n==2 early exit in optimizer (only 2 permutations to try) - Refactor tests: module-scoped fixtures, parametrize across methods and palette sources, merge redundant test classes Co-Authored-By: Claude Opus 4.6 (1M context) --- src/spatialdata_plot/pl/_palette.py | 57 +++-- tests/pl/test_palette.py | 371 ++++++++-------------------- 2 files changed, 150 insertions(+), 278 deletions(-) diff --git a/src/spatialdata_plot/pl/_palette.py b/src/spatialdata_plot/pl/_palette.py index a8842ae4..919c7598 100644 --- a/src/spatialdata_plot/pl/_palette.py +++ b/src/spatialdata_plot/pl/_palette.py @@ -19,7 +19,7 @@ import numpy as np import pandas as pd -from matplotlib.colors import to_hex, to_rgb +from matplotlib.colors import ListedColormap, to_hex, to_rgb from matplotlib.pyplot import colormaps as mpl_colormaps from scanpy.plotting.palettes import default_20, default_28, default_102 from scipy.spatial import cKDTree @@ -172,8 +172,15 @@ def _optimize_assignment( rng = np.random.default_rng() n = weight_matrix.shape[0] - if n <= 1: - return np.arange(n) + if n <= 2: + # For n<=2 there are at most 2 permutations; just try both. + if n <= 1: + return np.arange(n) + id_perm = np.arange(n) + sw_perm = np.array([1, 0]) + s_id = float(np.sum(weight_matrix * color_dist[np.ix_(id_perm, id_perm)])) + s_sw = float(np.sum(weight_matrix * color_dist[np.ix_(sw_perm, sw_perm)])) + return sw_perm if s_sw > s_id else id_perm def _score(perm: np.ndarray) -> float: return float(np.sum(weight_matrix * color_dist[np.ix_(perm, perm)])) @@ -249,16 +256,25 @@ def _spatial_interlacement( tree = cKDTree(coords) dists, indices = tree.query(coords, k=min(n_neighbors + 1, len(coords))) + # Vectorized accumulation (avoids Python double-loop over cells × neighbors) + neighbor_dists = dists[:, 1:] + neighbor_indices = indices[:, 1:] + cell_cats = label_idx + neighbor_cats = label_idx[neighbor_indices] + + # Mask: different category and positive distance + cross_cat = neighbor_cats != cell_cats[:, np.newaxis] + valid_dist = neighbor_dists > 0 + mask = cross_cat & valid_dist + + weights = np.where(mask, 1.0 / np.where(neighbor_dists > 0, neighbor_dists, 1.0), 0.0) + + rows = np.broadcast_to(cell_cats[:, np.newaxis], neighbor_cats.shape)[mask] + cols = neighbor_cats[mask] + vals = weights[mask] + mat = np.zeros((n_cat, n_cat), dtype=np.float64) - for i in range(len(coords)): - ci = label_idx[i] - for j in range(1, dists.shape[1]): # skip self - d = dists[i, j] - if d <= 0: - continue # skip coincident points - cj = label_idx[indices[i, j]] - if ci != cj: - mat[ci, cj] += 1.0 / d + np.add.at(mat, (rows, cols), vals) mat = np.maximum(mat, mat.T) max_val = mat.max() @@ -291,6 +307,11 @@ def _resolve_palette(palette: list[str] | str | None, n: int) -> list[str]: if palette in mpl_colormaps: cmap = mpl_colormaps[palette] + if isinstance(cmap, ListedColormap): + # Qualitative colormaps (tab10, Set1, etc.): sample by index + if n > cmap.N: + raise ValueError(f"Colormap '{palette}' has {cmap.N} colors but {n} are needed.") + return [to_hex(cmap(i)) for i in range(n)] indices = np.linspace(0, 1, n) return [to_hex(cmap(i)) for i in indices] @@ -324,9 +345,12 @@ def _resolve_element( coords = np.column_stack([gdf.geometry.centroid.x, gdf.geometry.centroid.y]) labels_series = gdf[color] if color in gdf.columns else _get_labels_from_table(sdata, element, color) elif element in sdata.points: - df = sdata.points[element].compute() - if "x" not in df.columns or "y" not in df.columns: + ddf = sdata.points[element] + if "x" not in ddf.columns or "y" not in ddf.columns: raise ValueError(f"Points element '{element}' does not have 'x' and 'y' columns.") + # Only compute needed columns to avoid materializing the full dataframe + needed_cols = ["x", "y"] + ([color] if color in ddf.columns else []) + df = ddf[needed_cols].compute() coords = df[["x", "y"]].values.astype(np.float64) labels_series = df[color] if color in df.columns else _get_labels_from_table(sdata, element, color) else: @@ -335,7 +359,8 @@ def _resolve_element( f"Element '{element}' not found in sdata.shapes or sdata.points. Available elements: {available}" ) - labels_cat = pd.Categorical(labels_series) if not hasattr(labels_series, "cat") else labels_series.values + is_categorical = isinstance(getattr(labels_series, "dtype", None), pd.CategoricalDtype) + labels_cat = labels_series.values if is_categorical else pd.Categorical(labels_series) return coords, labels_cat @@ -376,7 +401,7 @@ def _get_labels_from_table(sdata: sd.SpatialData, element: str, color: str) -> p "spaco_tritanopia": "tritanopia", } -_ALL_METHODS = ["default", *_CONTRAST_CVD_TYPES, *_SPACO_CVD_TYPES] +_ALL_METHODS = sorted({"default", *_CONTRAST_CVD_TYPES, *_SPACO_CVD_TYPES}) # --------------------------------------------------------------------------- diff --git a/tests/pl/test_palette.py b/tests/pl/test_palette.py index ca65c2aa..37affabb 100644 --- a/tests/pl/test_palette.py +++ b/tests/pl/test_palette.py @@ -31,14 +31,8 @@ # --------------------------------------------------------------------------- -def _make_clustered_points_sdata(seed: int = 0) -> SpatialData: - """Create a SpatialData with two spatially interleaved point clusters. - - Cluster layout (deliberately interleaved): - - "A" cells at (0,0), (1,0), (0,1) - - "B" cells at (0.5,0.5), (1.5,0.5), (0.5,1.5) - - "C" cells at (10,10), (11,10), (10,11) — isolated cluster - """ +def _build_clustered_points_sdata(seed: int = 0) -> SpatialData: + """SpatialData with interleaved A/B clusters near origin and isolated C far away.""" rng = np.random.default_rng(seed) coords_a = np.array([[0, 0], [1, 0], [0, 1]], dtype=float) + rng.normal(0, 0.05, (3, 2)) coords_b = np.array([[0.5, 0.5], [1.5, 0.5], [0.5, 1.5]], dtype=float) + rng.normal(0, 0.05, (3, 2)) @@ -46,14 +40,12 @@ def _make_clustered_points_sdata(seed: int = 0) -> SpatialData: coords = np.vstack([coords_a, coords_b, coords_c]) labels = pd.Categorical(["A"] * 3 + ["B"] * 3 + ["C"] * 3) - df = pd.DataFrame({"x": coords[:, 0], "y": coords[:, 1], "cell_type": labels}) - points = PointsModel.parse(df) - return SpatialData(points={"cells": points}) + return SpatialData(points={"cells": PointsModel.parse(df)}) -def _make_shapes_sdata(seed: int = 0) -> SpatialData: - """Create a SpatialData with shapes + linked table.""" +def _build_shapes_sdata(seed: int = 0) -> SpatialData: + """SpatialData with shapes + linked table containing categorical labels.""" from anndata import AnnData from geopandas import GeoDataFrame from shapely import Point @@ -64,137 +56,96 @@ def _make_shapes_sdata(seed: int = 0) -> SpatialData: gdf = GeoDataFrame({"radius": np.ones(n)}, geometry=[Point(x, y) for x, y in coords]) gdf.index = pd.RangeIndex(n) - labels = pd.Categorical(rng.choice(["X", "Y", "Z"], size=n)) adata = AnnData( np.zeros((n, 1)), obs=pd.DataFrame( { - "cell_type": labels, + "cell_type": pd.Categorical(rng.choice(["X", "Y", "Z"], size=n)), "instance_id": np.arange(n), "region": ["my_shapes"] * n, }, index=pd.RangeIndex(n).astype(str), ), ) - adata = TableModel.parse( - adata=adata, - region="my_shapes", - region_key="region", - instance_key="instance_id", - ) + adata = TableModel.parse(adata=adata, region="my_shapes", region_key="region", instance_key="instance_id") + return SpatialData(shapes={"my_shapes": ShapesModel.parse(gdf)}, tables={"table": adata}) + + +@pytest.fixture(scope="module") +def clustered_sdata() -> SpatialData: + return _build_clustered_points_sdata() + - shapes = ShapesModel.parse(gdf) - return SpatialData(shapes={"my_shapes": shapes}, tables={"table": adata}) +@pytest.fixture(scope="module") +def shapes_sdata() -> SpatialData: + return _build_shapes_sdata() # --------------------------------------------------------------------------- -# Unit tests: color-space helpers +# Unit tests: internals # --------------------------------------------------------------------------- class TestOklab: def test_black_and_white(self): - rgb = np.array([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]) - lab = _rgb_to_oklab(rgb) + lab = _rgb_to_oklab(np.array([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]])) assert lab[0, 0] == pytest.approx(0.0, abs=0.01) assert lab[1, 0] == pytest.approx(1.0, abs=0.01) def test_pairwise_distance_symmetric(self): - rgb = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=float) - lab = _rgb_to_oklab(rgb) - d = _pairwise_oklab_dist(lab) + d = _pairwise_oklab_dist(_rgb_to_oklab(np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=float))) assert d.shape == (3, 3) np.testing.assert_allclose(d, d.T) np.testing.assert_allclose(np.diag(d), 0) def test_distinct_colors_have_positive_distance(self): - rgb = np.array([[1, 0, 0], [0, 0, 1]], dtype=float) - lab = _rgb_to_oklab(rgb) - d = _pairwise_oklab_dist(lab) + d = _pairwise_oklab_dist(_rgb_to_oklab(np.array([[1, 0, 0], [0, 0, 1]], dtype=float))) assert d[0, 1] > 0.1 -# --------------------------------------------------------------------------- -# Unit tests: CVD simulation -# --------------------------------------------------------------------------- - - class TestCVD: @pytest.mark.parametrize("cvd_type", ["protanopia", "deuteranopia", "tritanopia"]) def test_output_in_range(self, cvd_type: str): - rgb = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=float) - sim = _simulate_cvd(rgb, cvd_type) + sim = _simulate_cvd(np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=float), cvd_type) assert sim.shape == (3, 3) - assert np.all(sim >= 0) - assert np.all(sim <= 1) + assert np.all((sim >= 0) & (sim <= 1)) def test_general_returns_stacked(self): - rgb = np.array([[1, 0, 0], [0, 1, 0]], dtype=float) - sim = _simulate_cvd(rgb, "general") + sim = _simulate_cvd(np.array([[1, 0, 0], [0, 1, 0]], dtype=float), "general") assert sim.shape == (3, 2, 3) - @pytest.mark.parametrize("cvd_type", ["protanopia", "deuteranopia", "tritanopia"]) + @pytest.mark.parametrize("cvd_type", ["protanopia", "deuteranopia"]) def test_red_green_less_distinct(self, cvd_type: str): - """Under protanopia/deuteranopia, red and green should be less distinct than for normal vision.""" rgb = np.array([[1, 0, 0], [0, 1, 0]], dtype=float) - normal_dist = _perceptual_distance_matrix(rgb)[0, 1] - cvd_dist = _perceptual_distance_matrix(rgb, colorblind_type=cvd_type)[0, 1] - if cvd_type in ("protanopia", "deuteranopia"): - assert cvd_dist < normal_dist - - -# --------------------------------------------------------------------------- -# Unit tests: spatial interlacement -# --------------------------------------------------------------------------- + assert _perceptual_distance_matrix(rgb, colorblind_type=cvd_type)[0, 1] < _perceptual_distance_matrix(rgb)[0, 1] class TestSpatialInterlacement: def test_interleaved_higher_than_separated(self): - """Categories that are spatially interleaved should have higher scores.""" coords = np.array([[0, 0], [1, 0], [0.5, 0.5], [1.5, 0.5], [10, 10], [11, 10]]) - labels = np.array(["A", "B", "A", "B", "C", "C"]) - categories = ["A", "B", "C"] - - mat = _spatial_interlacement(coords, labels, categories, n_neighbors=3) - + mat = _spatial_interlacement(coords, np.array(["A", "B", "A", "B", "C", "C"]), ["A", "B", "C"], n_neighbors=3) assert mat[0, 1] > mat[0, 2] assert mat[0, 1] > mat[1, 2] def test_diagonal_is_zero(self): - coords = np.array([[0, 0], [1, 0], [0.5, 0.5]]) - labels = np.array(["A", "B", "A"]) - mat = _spatial_interlacement(coords, labels, ["A", "B"], n_neighbors=2) + mat = _spatial_interlacement(np.array([[0, 0], [1, 0], [0.5, 0.5]]), np.array(["A", "B", "A"]), ["A", "B"], 2) np.testing.assert_allclose(np.diag(mat), 0) def test_symmetric(self): rng = np.random.default_rng(42) - coords = rng.normal(size=(50, 2)) - labels = np.array(["A", "B", "C", "D", "E"] * 10) - mat = _spatial_interlacement(coords, labels, ["A", "B", "C", "D", "E"], n_neighbors=5) + mat = _spatial_interlacement(rng.normal(size=(50, 2)), np.array(list("ABCDE") * 10), list("ABCDE"), 5) np.testing.assert_allclose(mat, mat.T) -# --------------------------------------------------------------------------- -# Unit tests: optimizer -# --------------------------------------------------------------------------- - - class TestOptimizer: def test_single_category(self): - perm = _optimize_assignment(np.zeros((1, 1)), np.zeros((1, 1))) - assert list(perm) == [0] + assert list(_optimize_assignment(np.zeros((1, 1)), np.zeros((1, 1)))) == [0] def test_two_categories(self): - """With 2 categories, there are only 2 permutations — optimizer should pick the better one.""" - inter = np.array([[0, 1], [1, 0]], dtype=float) - cdist = np.array([[0, 10], [10, 0]], dtype=float) - rng = np.random.default_rng(0) - perm = _optimize_assignment(inter, cdist, rng=rng) + perm = _optimize_assignment(np.array([[0, 1], [1, 0]], dtype=float), np.array([[0, 10], [10, 0]], dtype=float)) assert set(perm) == {0, 1} def test_deterministic_with_seed(self): - rng1 = np.random.default_rng(42) - rng2 = np.random.default_rng(42) inter = np.random.default_rng(0).random((5, 5)) inter = np.maximum(inter, inter.T) np.fill_diagonal(inter, 0) @@ -202,9 +153,9 @@ def test_deterministic_with_seed(self): cdist = np.maximum(cdist, cdist.T) np.fill_diagonal(cdist, 0) - perm1 = _optimize_assignment(inter, cdist, rng=rng1) - perm2 = _optimize_assignment(inter, cdist, rng=rng2) - np.testing.assert_array_equal(perm1, perm2) + p1 = _optimize_assignment(inter, cdist, rng=np.random.default_rng(42)) + p2 = _optimize_assignment(inter, cdist, rng=np.random.default_rng(42)) + np.testing.assert_array_equal(p1, p2) # --------------------------------------------------------------------------- @@ -213,47 +164,29 @@ def test_deterministic_with_seed(self): class TestMakePalette: - def test_default_returns_n_colors(self): + def test_default_returns_n_hex_colors(self): result = make_palette(5) assert len(result) == 5 - assert all(c.startswith("#") for c in result) - - def test_returns_list(self): - result = make_palette(3) assert isinstance(result, list) + assert all(c.startswith("#") for c in result) - def test_named_palette(self): - result = make_palette(4, palette="okabe_ito") + @pytest.mark.parametrize("palette", ["okabe_ito", "tab10", None]) + def test_palette_sources(self, palette: str | None): + result = make_palette(4, palette=palette) assert len(result) == 4 - def test_matplotlib_cmap(self): - result = make_palette(6, palette="tab10") - assert len(result) == 6 - def test_custom_list(self): colors = ["#ff0000", "#00ff00", "#0000ff"] - result = make_palette(3, palette=colors) - assert result == [to_hex(to_rgb(c)) for c in colors] + assert make_palette(3, palette=colors) == [to_hex(to_rgb(c)) for c in colors] - def test_contrast_reorders(self): - """Contrast method should produce a permutation of the input colors.""" + @pytest.mark.parametrize("method", ["contrast", "colorblind", "deuteranopia"]) + def test_optimization_methods_produce_permutation(self, method: str): colors = ["#ff0000", "#ff1100", "#0000ff", "#00ff00"] - result = make_palette(4, palette=colors, method="contrast", seed=42) + result = make_palette(4, palette=colors, method=method, seed=42) assert set(result) == {to_hex(to_rgb(c)) for c in colors} - def test_colorblind_reorders(self): - colors = ["#ff0000", "#00ff00", "#0000ff"] - result = make_palette(3, palette=colors, method="colorblind", seed=42) - assert set(result) == {to_hex(to_rgb(c)) for c in colors} - - def test_deuteranopia(self): - result = make_palette(5, method="deuteranopia", seed=42) - assert len(result) == 5 - def test_deterministic(self): - r1 = make_palette(5, method="contrast", seed=42) - r2 = make_palette(5, method="contrast", seed=42) - assert r1 == r2 + assert make_palette(5, method="contrast", seed=42) == make_palette(5, method="contrast", seed=42) def test_n_zero_raises(self): with pytest.raises(ValueError, match="at least 1"): @@ -263,13 +196,10 @@ def test_too_few_colors_raises(self): with pytest.raises(ValueError, match="needed"): make_palette(10, palette=["red", "blue"]) - def test_spaco_method_raises(self): - with pytest.raises(ValueError, match="requires spatial data"): - make_palette(3, method="spaco") # type: ignore[arg-type] - - def test_spaco_colorblind_method_raises(self): + @pytest.mark.parametrize("method", ["spaco", "spaco_colorblind"]) + def test_spaco_methods_raise(self, method: str): with pytest.raises(ValueError, match="requires spatial data"): - make_palette(3, method="spaco_colorblind") # type: ignore[arg-type] + make_palette(3, method=method) # type: ignore[arg-type] def test_unknown_method_raises(self): with pytest.raises(ValueError, match="Unknown method"): @@ -281,152 +211,83 @@ def test_unknown_palette_name_raises(self): # --------------------------------------------------------------------------- -# Tests: make_palette_from_data — default +# Tests: make_palette_from_data # --------------------------------------------------------------------------- -class TestMakePaletteFromDataDefault: - def test_basic(self): - sdata = _make_clustered_points_sdata() - result = make_palette_from_data(sdata, "cells", "cell_type") +class TestMakePaletteFromData: + def test_default_returns_dict(self, clustered_sdata: SpatialData): + result = make_palette_from_data(clustered_sdata, "cells", "cell_type") assert isinstance(result, dict) assert set(result.keys()) == {"A", "B", "C"} - for v in result.values(): - assert v.startswith("#") + assert all(v.startswith("#") for v in result.values()) - def test_matches_scanpy_order(self): - """Default method should assign colors in sorted-category order, matching scanpy.""" + def test_default_matches_scanpy_order(self, clustered_sdata: SpatialData): from scanpy.plotting.palettes import default_20 - sdata = _make_clustered_points_sdata() - result = make_palette_from_data(sdata, "cells", "cell_type", method="default") - + result = make_palette_from_data(clustered_sdata, "cells", "cell_type") for i, cat in enumerate(sorted(result.keys())): assert result[cat] == to_hex(to_rgb(default_20[i])) - def test_custom_palette(self): - sdata = _make_clustered_points_sdata() + def test_custom_palette(self, clustered_sdata: SpatialData): colors = ["#ff0000", "#00ff00", "#0000ff"] - result = make_palette_from_data(sdata, "cells", "cell_type", palette=colors) + result = make_palette_from_data(clustered_sdata, "cells", "cell_type", palette=colors) assert list(result.values()) == [to_hex(to_rgb(c)) for c in colors] - def test_named_palette(self): - sdata = _make_clustered_points_sdata() - result = make_palette_from_data(sdata, "cells", "cell_type", palette="okabe_ito") - assert isinstance(result, dict) - assert len(result) == 3 - - def test_matplotlib_cmap(self): - sdata = _make_clustered_points_sdata() - result = make_palette_from_data(sdata, "cells", "cell_type", palette="tab10") - assert isinstance(result, dict) - assert len(result) == 3 - - -# --------------------------------------------------------------------------- -# Tests: make_palette_from_data — spaco -# --------------------------------------------------------------------------- - + @pytest.mark.parametrize("palette", ["okabe_ito", "tab10"]) + def test_named_palette_sources(self, clustered_sdata: SpatialData, palette: str): + result = make_palette_from_data(clustered_sdata, "cells", "cell_type", palette=palette) + assert isinstance(result, dict) and len(result) == 3 -class TestMakePaletteFromDataContrast: - def test_contrast_returns_dict(self): - sdata = _make_clustered_points_sdata() - result = make_palette_from_data(sdata, "cells", "cell_type", method="contrast", seed=42) - assert isinstance(result, dict) - assert set(result.keys()) == {"A", "B", "C"} - - def test_colorblind_returns_dict(self): - sdata = _make_clustered_points_sdata() - result = make_palette_from_data(sdata, "cells", "cell_type", method="colorblind", seed=42) - assert isinstance(result, dict) - assert len(result) == 3 - - -class TestMakePaletteFromDataSpaco: - def test_basic_points(self): - sdata = _make_clustered_points_sdata() - result = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", seed=42) + @pytest.mark.parametrize( + "method", + ["contrast", "colorblind", "spaco", "spaco_colorblind", "spaco_deuteranopia"], + ) + def test_all_methods_return_valid_dict(self, clustered_sdata: SpatialData, method: str): + result = make_palette_from_data(clustered_sdata, "cells", "cell_type", method=method, seed=42) assert isinstance(result, dict) assert set(result.keys()) == {"A", "B", "C"} - def test_deterministic(self): - sdata = _make_clustered_points_sdata() - r1 = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", seed=42) - r2 = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", seed=42) + def test_spaco_deterministic(self, clustered_sdata: SpatialData): + r1 = make_palette_from_data(clustered_sdata, "cells", "cell_type", method="spaco", seed=42) + r2 = make_palette_from_data(clustered_sdata, "cells", "cell_type", method="spaco", seed=42) assert r1 == r2 - def test_different_seeds_can_differ(self): - sdata = _make_clustered_points_sdata() - r1 = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", seed=0) - r2 = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", seed=999) + def test_spaco_different_seeds_can_differ(self, clustered_sdata: SpatialData): + r1 = make_palette_from_data(clustered_sdata, "cells", "cell_type", method="spaco", seed=0) + r2 = make_palette_from_data(clustered_sdata, "cells", "cell_type", method="spaco", seed=999) assert set(r1.keys()) == set(r2.keys()) - def test_custom_palette(self): - sdata = _make_clustered_points_sdata() + def test_spaco_custom_palette_is_permutation(self, clustered_sdata: SpatialData): colors = ["#ff0000", "#00ff00", "#0000ff"] - result = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", palette=colors, seed=42) + result = make_palette_from_data(clustered_sdata, "cells", "cell_type", method="spaco", palette=colors, seed=42) assert set(result.values()) == {to_hex(to_rgb(c)) for c in colors} - def test_spaco_colorblind(self): - sdata = _make_clustered_points_sdata() - result = make_palette_from_data(sdata, "cells", "cell_type", method="spaco_colorblind", seed=42) - assert isinstance(result, dict) - assert len(result) == 3 - - def test_spaco_deuteranopia(self): - sdata = _make_clustered_points_sdata() - result = make_palette_from_data(sdata, "cells", "cell_type", method="spaco_deuteranopia", seed=42) - assert isinstance(result, dict) - assert len(result) == 3 - - def test_spaco_with_named_palette(self): - sdata = _make_clustered_points_sdata() - result = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", palette="okabe_ito", seed=42) - assert isinstance(result, dict) - assert len(result) == 3 - - def test_spaco_with_matplotlib_cmap(self): - sdata = _make_clustered_points_sdata() - result = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", palette="tab10", seed=42) - assert isinstance(result, dict) - assert len(result) == 3 - - def test_single_category(self): - coords = np.array([[0, 0], [1, 1]], dtype=float) - df = pd.DataFrame({"x": coords[:, 0], "y": coords[:, 1], "ct": pd.Categorical(["A", "A"])}) - points = PointsModel.parse(df) - sdata = SpatialData(points={"pts": points}) - + def test_spaco_single_category(self): + df = pd.DataFrame({"x": [0.0, 1.0], "y": [0.0, 1.0], "ct": pd.Categorical(["A", "A"])}) + sdata = SpatialData(points={"pts": PointsModel.parse(df)}) result = make_palette_from_data(sdata, "pts", "ct", method="spaco", seed=0) - assert len(result) == 1 - assert "A" in result - - def test_nan_labels_filtered(self): - coords = np.array([[0, 0], [1, 0], [0, 1], [10, 10]], dtype=float) - labels = pd.Categorical(["A", "B", "A", None]) - df = pd.DataFrame({"x": coords[:, 0], "y": coords[:, 1], "ct": labels}) - points = PointsModel.parse(df) - sdata = SpatialData(points={"pts": points}) + assert len(result) == 1 and "A" in result + def test_spaco_nan_labels_filtered(self): + df = pd.DataFrame( + {"x": [0.0, 1.0, 0.0, 10.0], "y": [0.0, 0.0, 1.0, 10.0], "ct": pd.Categorical(["A", "B", "A", None])} + ) + sdata = SpatialData(points={"pts": PointsModel.parse(df)}) result = make_palette_from_data(sdata, "pts", "ct", method="spaco", seed=0) - assert "A" in result - assert "B" in result + assert {"A", "B"} <= set(result.keys()) - def test_shapes_with_table(self): - sdata = _make_shapes_sdata() - result = make_palette_from_data(sdata, "my_shapes", "cell_type", method="spaco", seed=42) + def test_shapes_with_table(self, shapes_sdata: SpatialData): + result = make_palette_from_data(shapes_sdata, "my_shapes", "cell_type", method="spaco", seed=42) assert isinstance(result, dict) assert set(result.keys()) == {"X", "Y", "Z"} def test_interleaved_get_distinct_colors(self): - """Core property: spatially interleaved categories should get the most distinct colors.""" - sdata = _make_clustered_points_sdata(seed=0) + sdata = _build_clustered_points_sdata(seed=0) palette = ["#ff0000", "#ff1100", "#0000ff"] result = make_palette_from_data(sdata, "cells", "cell_type", method="spaco", palette=palette, seed=0) - - a_color = result["A"] - b_color = result["B"] - assert a_color == "#0000ff" or b_color == "#0000ff" + # A and B (interleaved) should not both get red-ish colors + assert result["A"] == "#0000ff" or result["B"] == "#0000ff" # --------------------------------------------------------------------------- @@ -435,46 +296,38 @@ def test_interleaved_get_distinct_colors(self): class TestMakePaletteFromDataErrors: - def test_too_few_colors_raises(self): - sdata = _make_clustered_points_sdata() + def test_too_few_colors(self, clustered_sdata: SpatialData): with pytest.raises(ValueError, match="needed"): - make_palette_from_data(sdata, "cells", "cell_type", method="spaco", palette=["red", "blue"], seed=0) + make_palette_from_data(clustered_sdata, "cells", "cell_type", palette=["red", "blue"]) - def test_missing_element_raises(self): - sdata = _make_clustered_points_sdata() + def test_missing_element(self, clustered_sdata: SpatialData): with pytest.raises(KeyError, match="not found"): - make_palette_from_data(sdata, "nonexistent", "cell_type") + make_palette_from_data(clustered_sdata, "nonexistent", "cell_type") - def test_missing_column_raises(self): - sdata = _make_clustered_points_sdata() + def test_missing_column(self, clustered_sdata: SpatialData): with pytest.raises(KeyError, match="not found"): - make_palette_from_data(sdata, "cells", "nonexistent_col") + make_palette_from_data(clustered_sdata, "cells", "nonexistent_col") - def test_unknown_method_raises(self): - sdata = _make_clustered_points_sdata() + def test_unknown_method(self, clustered_sdata: SpatialData): with pytest.raises(ValueError, match="Unknown method"): - make_palette_from_data(sdata, "cells", "cell_type", method="invalid") # type: ignore[arg-type] + make_palette_from_data(clustered_sdata, "cells", "cell_type", method="invalid") # type: ignore[arg-type] # --------------------------------------------------------------------------- -# Integration tests: dict palette through render pipeline +# Integration: dict palette through render pipeline # --------------------------------------------------------------------------- class TestDictPalette: def test_dict_palette_in_render_points(self, sdata_blobs: SpatialData): - """Dict palette should flow through render_points without errors.""" - palette = {"0": "#ff0000", "1": "#00ff00"} - sdata_blobs.pl.render_points("blobs_points", color="genes", palette=palette) + sdata_blobs.pl.render_points("blobs_points", color="genes", palette={"0": "#ff0000", "1": "#00ff00"}) def test_dict_palette_in_render_labels(self, sdata_blobs: SpatialData): - """Dict palette should flow through render_labels without errors.""" - palette = {"blobs_labels": "#ff0000"} - sdata_blobs.pl.render_labels("blobs_labels", color="region", palette=palette) + sdata_blobs.pl.render_labels("blobs_labels", color="region", palette={"blobs_labels": "#ff0000"}) # --------------------------------------------------------------------------- -# Visual tests: make_palette_from_data → render → show +# Visual tests # --------------------------------------------------------------------------- sc.pl.set_rcParams_defaults() @@ -483,33 +336,27 @@ def test_dict_palette_in_render_labels(self, sdata_blobs: SpatialData): class TestPaletteVisual(PlotTester, metaclass=PlotTesterMeta): def test_plot_dict_palette_hex_points(self, sdata_blobs: SpatialData): - """Visual test: hex dict palette renders points correctly.""" palette = make_palette_from_data(sdata_blobs, "blobs_points", "genes", palette="okabe_ito") sdata_blobs.pl.render_points("blobs_points", color="genes", palette=palette).pl.show() def test_plot_dict_palette_hex_shapes(self, sdata_blobs: SpatialData): - """Visual test: hex dict palette renders shapes correctly.""" sdata_blobs["blobs_polygons"]["cat_col"] = pd.Series(["a", "b", "a", "b", "a"], dtype="category") palette = make_palette_from_data(sdata_blobs, "blobs_polygons", "cat_col", palette="okabe_ito") sdata_blobs.pl.render_shapes("blobs_polygons", color="cat_col", palette=palette).pl.show() def test_plot_dict_palette_hex_labels(self, sdata_blobs: SpatialData): - """Visual test: hex dict palette renders labels correctly.""" - palette = {"blobs_labels": "#E69F00"} - sdata_blobs.pl.render_labels("blobs_labels", color="region", palette=palette).pl.show() + sdata_blobs.pl.render_labels("blobs_labels", color="region", palette={"blobs_labels": "#E69F00"}).pl.show() def test_plot_dict_palette_named_colors_points(self, sdata_blobs: SpatialData): - """Visual test: named-color dict palette renders points correctly.""" - palette = {"gene_a": "red", "gene_b": "dodgerblue"} - sdata_blobs.pl.render_points("blobs_points", color="genes", palette=palette).pl.show() + sdata_blobs.pl.render_points( + "blobs_points", color="genes", palette={"gene_a": "red", "gene_b": "dodgerblue"} + ).pl.show() def test_plot_dict_palette_named_colors_shapes(self, sdata_blobs: SpatialData): - """Visual test: named-color dict palette renders shapes correctly.""" sdata_blobs["blobs_polygons"]["cat_col"] = pd.Series(["a", "b", "a", "b", "a"], dtype="category") - palette = {"a": "forestgreen", "b": "orchid"} - sdata_blobs.pl.render_shapes("blobs_polygons", color="cat_col", palette=palette).pl.show() + sdata_blobs.pl.render_shapes( + "blobs_polygons", color="cat_col", palette={"a": "forestgreen", "b": "orchid"} + ).pl.show() def test_plot_dict_palette_named_colors_labels(self, sdata_blobs: SpatialData): - """Visual test: named-color dict palette renders labels correctly.""" - palette = {"blobs_labels": "coral"} - sdata_blobs.pl.render_labels("blobs_labels", color="region", palette=palette).pl.show() + sdata_blobs.pl.render_labels("blobs_labels", color="region", palette={"blobs_labels": "coral"}).pl.show() From 61fc1da86df2eef4b634a0423ff8a89928f1d51e Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Mon, 6 Apr 2026 10:18:55 +0200 Subject: [PATCH 5/6] Restore DPI preservation for user-provided axes The stash application accidentally reverted the resolved_dpi logic from commit 303140c, causing user figure DPI to be silently overwritten with rcParams defaults. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/spatialdata_plot/pl/utils.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 68654745..3c204b01 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -251,14 +251,17 @@ def _prepare_params_plot( # handle axes and size wspace = 0.75 / rcParams["figure.figsize"][0] + 0.02 if wspace is None else wspace figsize = rcParams["figure.figsize"] if figsize is None else figsize - dpi = rcParams["figure.dpi"] if dpi is None else dpi + # When creating a new figure, fall back to rcParams; when the user provides + # their own axes, preserve the figure's existing DPI (only override if + # the user explicitly passed dpi= to show()). + resolved_dpi = rcParams["figure.dpi"] if dpi is None else dpi if num_panels > 1 and ax is None: fig, grid = _panel_grid( num_panels=num_panels, hspace=hspace, wspace=wspace, ncols=ncols, - dpi=dpi, + dpi=resolved_dpi, figsize=figsize, ) axs: None | Sequence[Axes] = [plt.subplot(grid[c]) for c in range(num_panels)] @@ -274,14 +277,16 @@ def _prepare_params_plot( ) assert ax is None or isinstance(ax, Sequence), f"Invalid type of `ax`: {type(ax)}, expected `Sequence`." axs = ax + if dpi is not None: + fig.set_dpi(dpi) else: axs = None if ax is None: - fig, ax = plt.subplots(figsize=figsize, dpi=dpi, constrained_layout=True) + fig, ax = plt.subplots(figsize=figsize, dpi=resolved_dpi, constrained_layout=True) elif isinstance(ax, Axes): - # needed for rasterization if user provides Axes object fig = ax.get_figure() - fig.set_dpi(dpi) + if dpi is not None: + fig.set_dpi(dpi) # set scalebar if scalebar_dx is not None: From 165f5a76446b8d13199866850330a7bf8f49e894 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Mon, 6 Apr 2026 10:28:10 +0200 Subject: [PATCH 6/6] Fix adversarial review findings: alignment, validation, groups - _get_labels_from_table now joins on instance keys to guarantee coord-label alignment (was returning table rows in table order, silently misaligning with element coordinates) - Error when multiple tables annotate the same element; accept table_name= parameter to disambiguate - Dict palette path in _get_categorical_color_mapping now applies groups filtering (was silently ignoring groups= with dict palettes) - Validate dict palette color values with is_color_like() in _type_check_params (was passing invalid colors through to matplotlib) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/spatialdata_plot/pl/_palette.py | 113 +++++++++++++++++++++++----- src/spatialdata_plot/pl/utils.py | 17 ++++- 2 files changed, 106 insertions(+), 24 deletions(-) diff --git a/src/spatialdata_plot/pl/_palette.py b/src/spatialdata_plot/pl/_palette.py index 919c7598..9f700181 100644 --- a/src/spatialdata_plot/pl/_palette.py +++ b/src/spatialdata_plot/pl/_palette.py @@ -338,25 +338,41 @@ def _resolve_element( sdata: sd.SpatialData, element: str, color: str, + table_name: str | None = None, ) -> tuple[np.ndarray, pd.Categorical]: - """Extract coordinates and categorical labels from a SpatialData element.""" + """Extract coordinates and categorical labels from a SpatialData element. + + Coordinates come from the element geometry (shapes) or x/y columns + (points). Labels come from a column on the element itself, or from + a linked table (joined on the instance key to guarantee alignment). + """ if element in sdata.shapes: gdf = sdata.shapes[element] coords = np.column_stack([gdf.geometry.centroid.x, gdf.geometry.centroid.y]) - labels_series = gdf[color] if color in gdf.columns else _get_labels_from_table(sdata, element, color) + if color in gdf.columns: + labels_series = gdf[color] + else: + labels_series, matched_indices = _get_labels_from_table(sdata, element, color, table_name) + # Align coords to table rows via matched instance indices + coords = coords[matched_indices] elif element in sdata.points: ddf = sdata.points[element] if "x" not in ddf.columns or "y" not in ddf.columns: raise ValueError(f"Points element '{element}' does not have 'x' and 'y' columns.") - # Only compute needed columns to avoid materializing the full dataframe - needed_cols = ["x", "y"] + ([color] if color in ddf.columns else []) - df = ddf[needed_cols].compute() - coords = df[["x", "y"]].values.astype(np.float64) - labels_series = df[color] if color in df.columns else _get_labels_from_table(sdata, element, color) + if color in ddf.columns: + df = ddf[["x", "y", color]].compute() + coords = df[["x", "y"]].values.astype(np.float64) + labels_series = df[color] + else: + df = ddf[["x", "y"]].compute() + coords = df[["x", "y"]].values.astype(np.float64) + labels_series, matched_indices = _get_labels_from_table(sdata, element, color, table_name) + coords = coords[matched_indices] else: available = list(sdata.shapes.keys()) + list(sdata.points.keys()) raise KeyError( - f"Element '{element}' not found in sdata.shapes or sdata.points. Available elements: {available}" + f"Element '{element}' not found in sdata.shapes or sdata.points. " + f"Available elements: {available}. Note: labels (raster) elements are not yet supported." ) is_categorical = isinstance(getattr(labels_series, "dtype", None), pd.CategoricalDtype) @@ -364,19 +380,72 @@ def _resolve_element( return coords, labels_cat -def _get_labels_from_table(sdata: sd.SpatialData, element: str, color: str) -> pd.Series: - """Extract a column from the table linked to an element.""" - for table_name in sdata.tables: - table = sdata.tables[table_name] - region_key = table.uns.get("spatialdata_attrs", {}).get("region") - if region_key is not None: - regions = [region_key] if isinstance(region_key, str) else region_key +def _get_labels_from_table( + sdata: sd.SpatialData, + element: str, + color: str, + table_name: str | None = None, +) -> tuple[pd.Series, np.ndarray]: + """Extract a column from the table linked to an element. + + Returns (labels_series, element_indices) where element_indices maps + each table row to its position in the element, ensuring coord-label + alignment. + """ + from spatialdata.models import get_table_keys + + matches: list[str] = [] + for name in sdata.tables: + table = sdata.tables[name] + region = table.uns.get("spatialdata_attrs", {}).get("region") + if region is not None: + regions = [region] if isinstance(region, str) else region if element in regions and color in table.obs.columns: - return table.obs[color] + matches.append(name) - raise KeyError( - f"Column '{color}' not found for element '{element}'. Looked in the element itself and all linked tables." - ) + if not matches: + raise KeyError( + f"Column '{color}' not found for element '{element}'. Looked in the element itself and all linked tables." + ) + + if table_name is not None: + if table_name not in matches: + raise KeyError( + f"Table '{table_name}' does not annotate element '{element}' or does not contain column '{color}'." + ) + resolved_name = table_name + elif len(matches) == 1: + resolved_name = matches[0] + else: + raise ValueError( + f"Multiple tables annotate element '{element}' with column '{color}': {matches}. " + f"Please specify table_name= to disambiguate." + ) + + table = sdata.tables[resolved_name] + _, _, instance_key = get_table_keys(table) + + # Join on instance key to align table rows with element positions + instance_ids = table.obs[instance_key].values + element_index = sdata.shapes[element].index if element in sdata.shapes else sdata.points[element].compute().index + + # Map each table instance_id to its position in the element index + element_idx_map = {val: i for i, val in enumerate(element_index)} + matched_indices = [] + valid_mask = [] + for iid in instance_ids: + if iid in element_idx_map: + matched_indices.append(element_idx_map[iid]) + valid_mask.append(True) + else: + valid_mask.append(False) + + valid_mask_arr = np.array(valid_mask) + if not any(valid_mask): + raise ValueError(f"No matching instance keys between table '{resolved_name}' and element '{element}'.") + + labels = table.obs.loc[valid_mask_arr, color] + return labels.reset_index(drop=True), np.array(matched_indices) # --------------------------------------------------------------------------- @@ -507,6 +576,7 @@ def make_palette_from_data( *, palette: list[str] | str | None = None, method: Method = "default", + table_name: str | None = None, n_neighbors: int = 15, n_random: int = 5000, n_swaps: int = 10000, @@ -530,6 +600,9 @@ def make_palette_from_data( Source colours. Accepts the same values as :func:`make_palette` (*None*, a list, a named palette, or a matplotlib colormap name). + table_name + Name of the table to use when *color* is looked up from a linked + table. Required when multiple tables annotate the same element. method Strategy for assigning colours to categories. Accepts all methods from :func:`make_palette` plus spatially-aware ones: @@ -571,7 +644,7 @@ def make_palette_from_data( >>> palette = sdp.pl.make_palette_from_data(sdata, "cells", "cell_type", method="spaco_colorblind") >>> sdata.pl.render_shapes("cells", color="cell_type", palette=palette).pl.show() """ - coords, labels_cat = _resolve_element(sdata, element, color) + coords, labels_cat = _resolve_element(sdata, element, color, table_name=table_name) categories = list(labels_cat.categories) n_cat = len(categories) diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 3c204b01..7746c8af 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -1583,10 +1583,15 @@ def _get_categorical_color_mapping( if not isinstance(color_source_vector, Categorical): raise TypeError(f"Expected `categories` to be a `Categorical`, but got {type(color_source_vector).__name__}") - # Dict palette (e.g. from optimize_palette): use directly as category→color mapping + # Dict palette (e.g. from make_palette_from_data): use directly as category→color mapping if isinstance(palette, dict): na_color_hex = na_color.get_hex_with_alpha() if isinstance(na_color, Color) else str(na_color) - mapping = {cat: palette.get(cat, na_color_hex) for cat in color_source_vector.categories} + if isinstance(groups, str): + groups = [groups] + if groups is not None: + mapping = {cat: palette.get(cat, na_color_hex) for cat in groups if cat in color_source_vector.categories} + else: + mapping = {cat: palette.get(cat, na_color_hex) for cat in color_source_vector.categories} mapping["NaN"] = na_color_hex return mapping @@ -2402,9 +2407,13 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st palette = param_dict["palette"] - # dict palettes (e.g. from optimize_palette) bypass groups validation + # dict palettes (e.g. from make_palette_from_data) bypass groups validation if isinstance(palette, dict): - pass + from matplotlib.colors import is_color_like + + invalid = [f"'{k}': '{v}'" for k, v in palette.items() if not is_color_like(v)] + if invalid: + raise ValueError(f"Dict palette contains invalid color values: {', '.join(invalid)}.") elif isinstance(palette, list): if not all(isinstance(p, str) for p in palette): raise ValueError("If specified, parameter 'palette' must contain only strings.")