From 5f9bb164ae5b0b9656e56adce1f1c75117028176 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Thu, 5 Mar 2026 19:57:13 +0100 Subject: [PATCH 1/2] Preserve Categorical dtype for color_vector with non-unique colors pd.Categorical.map() silently demotes to object dtype when mapped values aren't unique (e.g. two categories share the same color). This caused _map_color_seg to take a slow path where label2rgb processed one color per instance instead of just the unique categories, and caused datashader to ignore assigned colors for shapes/points. Wrap the .map() result back in pd.Categorical to ensure downstream consumers always receive a Categorical for categorical data. Also adds explicit na_action="ignore" to silence the FutureWarning from pandas >=2.1. Closes #469 Closes #540 Co-Authored-By: Claude Opus 4.6 --- src/spatialdata_plot/pl/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 9d3b2954..26823235 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -1120,7 +1120,10 @@ def _set_color_source_vec( raise ValueError("Unable to create color palette.") # do not rename categories, as colors need not be unique - color_vector = color_source_vector.map(color_mapping) + # pd.Categorical.map() demotes to object dtype when mapped values aren't unique + # (e.g. two categories share a color). Wrapping back in pd.Categorical ensures + # downstream consumers always receive a Categorical for categorical data. + color_vector = pd.Categorical(color_source_vector.map(color_mapping, na_action="ignore")) return color_source_vector, color_vector, True From 75d3dfa17b1237a8eb1e43308333c0a441ecd83f Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Thu, 5 Mar 2026 20:04:42 +0100 Subject: [PATCH 2/2] Unify categorical type checks to isinstance(dtype, CategoricalDtype) Replace three inconsistent idioms for checking categorical dtype: - pd.api.types.is_categorical_dtype() (deprecated in pandas 2.1) - type(x) is pd.core.arrays.categorical.Categorical (checks array type) - isinstance(x.dtype, pd.CategoricalDtype) (canonical) All four call sites now use the same isinstance pattern, matching the rest of the codebase. Co-Authored-By: Claude Opus 4.6 --- src/spatialdata_plot/pl/render.py | 6 ++---- src/spatialdata_plot/pl/utils.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index 014c3cc5..02934fbc 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -358,8 +358,7 @@ def _render_shapes( color_key = ( [_hex_no_alpha(x) for x in color_vector.categories.values] - if (type(color_vector) is pd.core.arrays.categorical.Categorical) - and (len(color_vector.categories.values) > 1) + if isinstance(color_vector.dtype, pd.CategoricalDtype) and (len(color_vector.categories.values) > 1) else None ) @@ -854,8 +853,7 @@ def _render_points( color_key: list[str] | None = ( list(color_vector.categories.values) - if (type(color_vector) is pd.core.arrays.categorical.Categorical) - and (len(color_vector.categories.values) > 1) + if isinstance(color_vector.dtype, pd.CategoricalDtype) and (len(color_vector.categories.values) > 1) else None ) diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 26823235..ae55a9c8 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -1149,7 +1149,7 @@ def _map_color_seg( ) -> ArrayLike: cell_id = np.array(cell_id) - if pd.api.types.is_categorical_dtype(color_vector.dtype): + if isinstance(color_vector.dtype, pd.CategoricalDtype): # Case A: users wants to plot a categorical column if np.any(color_source_vector.isna()): cell_id[color_source_vector.isna()] = 0