From c440fba93295299dc24657c532f6fff3cf9c82fe Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Tue, 18 Nov 2025 20:51:56 +0100 Subject: [PATCH 01/16] now warning if color or table doesn't exist --- src/spatialdata_plot/pl/utils.py | 25 +++++++++++++------------ tests/pl/test_render_shapes.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index cccb587e..5a578dd8 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -873,9 +873,14 @@ def _set_color_source_vec( return color_source_vector, color_vector, True - logger.warning(f"Color key '{value_to_plot}' for element '{element_name}' not been found, using default colors.") - color = np.full(sdata[table_name].n_obs, na_color.get_hex_with_alpha()) - return color, color, False + if table_name is None: + raise KeyError( + f"Unable to locate color key '{value_to_plot}' for element '{element_name}'. " + "Please ensure the key exists in a table annotating this element." + ) + raise KeyError( + f"Unable to locate color key '{value_to_plot}' in table '{table_name}' for element '{element_name}'." + ) def _map_color_seg( @@ -2136,16 +2141,12 @@ def _validate_col_for_column_table( table_name = None elif table_name is not None: tables = get_element_annotators(sdata, element_name) - if table_name not in tables or ( - col_for_color not in sdata[table_name].obs.columns and col_for_color not in sdata[table_name].var_names - ): - warnings.warn( - f"Table '{table_name}' does not annotate element '{element_name}'.", - UserWarning, - stacklevel=2, + if table_name not in tables: + raise KeyError(f"Table '{table_name}' does not annotate element '{element_name}'.") + if col_for_color not in sdata[table_name].obs.columns and col_for_color not in sdata[table_name].var_names: + raise KeyError( + f"Column '{col_for_color}' not found in obs/var of table '{table_name}' for element '{element_name}'." ) - table_name = None - col_for_color = None else: tables = get_element_annotators(sdata, element_name) for table_name in tables.copy(): diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index 8fc39582..146ae16f 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -194,6 +194,36 @@ def test_plot_colorbar_can_be_normalised(self, sdata_blobs: SpatialData): norm = Normalize(vmin=0, vmax=5, clip=True) sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=norm).pl.show() + def test_render_shapes_raises_when_color_key_missing(self, sdata_blobs_shapes_annotated: SpatialData): + with pytest.raises(KeyError, match="Unable to locate color key 'ghost'"): + sdata_blobs_shapes_annotated.pl.render_shapes( + element="blobs_polygons", + color="ghost", + ).pl.show() + + def test_render_shapes_raises_for_invalid_table_name(self, sdata_blobs_shapes_annotated: SpatialData): + table = sdata_blobs_shapes_annotated["table"] + table.obs["region"] = pd.Categorical(["blobs_polygons"] * table.n_obs) + table.uns["spatialdata_attrs"]["region"] = "blobs_polygons" + table.obs["valid_col"] = np.arange(table.n_obs) + + with pytest.raises(KeyError, match="Table 'not_a_table' does not annotate element 'blobs_polygons'"): + sdata_blobs_shapes_annotated.pl.render_shapes( + element="blobs_polygons", color="valid_col", table_name="not_a_table" + ) + + def test_render_shapes_raises_for_missing_column_in_table(self, sdata_blobs_shapes_annotated: SpatialData): + table = sdata_blobs_shapes_annotated["table"] + table.obs["region"] = pd.Categorical(["blobs_polygons"] * table.n_obs) + table.uns["spatialdata_attrs"]["region"] = "blobs_polygons" + + with pytest.raises( + KeyError, match="Column 'not_a_column' not found in obs/var of table 'table' for element 'blobs_polygons'" + ): + sdata_blobs_shapes_annotated.pl.render_shapes( + element="blobs_polygons", color="not_a_column", table_name="table" + ) + def test_plot_can_plot_shapes_after_spatial_query(self, sdata_blobs: SpatialData): # subset to only shapes, should be unnecessary after rasterizeation of multiscale images is included blob = SpatialData.init_from_elements( From 67acca81bf6d3ac6526a6fc549d64d1e1ea5fa87 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Tue, 18 Nov 2025 21:02:47 +0100 Subject: [PATCH 02/16] fixing tests --- tests/pl/test_render_labels.py | 21 +++++++++++---------- tests/pl/test_render_points.py | 21 +++++++++++---------- tests/pl/test_render_shapes.py | 33 +++++++++++++++++---------------- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/tests/pl/test_render_labels.py b/tests/pl/test_render_labels.py index 8d96ec96..e51d33f9 100644 --- a/tests/pl/test_render_labels.py +++ b/tests/pl/test_render_labels.py @@ -299,7 +299,7 @@ def test_plot_can_handle_dropping_small_labels_after_rasterize_categorical(self, sdata_blobs.pl.render_labels("blobs_labels_large", color="category", table_name="table").pl.show() -def test_warns_when_table_does_not_annotate_element(sdata_blobs: SpatialData): +def test_raises_when_table_does_not_annotate_element(sdata_blobs: SpatialData): # Work on an independent copy since we mutate tables sdata_blobs_local = deepcopy(sdata_blobs) @@ -310,12 +310,13 @@ def test_warns_when_table_does_not_annotate_element(sdata_blobs: SpatialData): sdata_blobs_local["other_table"] = other_table # Rendering "blobs_labels" with a table that annotates "blobs_multiscale_labels" - # should raise a warning and fall back to using no table. - with pytest.warns(UserWarning, match="does not annotate element"): - ( - sdata_blobs_local.pl.render_labels( - "blobs_labels", - color="channel_0_sum", - table_name="other_table", - ).pl.show() - ) + # should now raise to alert the user about the mismatch. + with pytest.raises( + KeyError, + match="Table 'other_table' does not annotate element 'blobs_labels'", + ): + sdata_blobs_local.pl.render_labels( + "blobs_labels", + color="channel_0_sum", + table_name="other_table", + ).pl.show() diff --git a/tests/pl/test_render_points.py b/tests/pl/test_render_points.py index 5e3fc38a..49a8386a 100644 --- a/tests/pl/test_render_points.py +++ b/tests/pl/test_render_points.py @@ -493,7 +493,7 @@ def test_plot_can_annotate_points_with_table_layer(self, sdata_blobs: SpatialDat sdata_blobs.pl.render_points("blobs_points", color="feature0", size=10, table_layer="normalized").pl.show() -def test_warns_when_table_does_not_annotate_element(sdata_blobs: SpatialData): +def test_raises_when_table_does_not_annotate_element(sdata_blobs: SpatialData): # Work on an independent copy since we mutate tables sdata_blobs_local = deepcopy(sdata_blobs) @@ -504,15 +504,16 @@ def test_warns_when_table_does_not_annotate_element(sdata_blobs: SpatialData): sdata_blobs_local["other_table"] = other_table # Rendering "blobs_points" with a table that annotates "blobs_labels" - # should raise a warning and fall back to using no table. - with pytest.warns(UserWarning, match="does not annotate element"): - ( - sdata_blobs_local.pl.render_points( - "blobs_points", - color="channel_0_sum", - table_name="other_table", - ).pl.show() - ) + # should now raise to alert the user about the mismatch. + with pytest.raises( + KeyError, + match="Table 'other_table' does not annotate element 'blobs_points'", + ): + sdata_blobs_local.pl.render_points( + "blobs_points", + color="channel_0_sum", + table_name="other_table", + ).pl.show() def test_datashader_colors_points_from_table_obs(sdata_blobs: SpatialData): diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index 146ae16f..90fa5c6f 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -195,10 +195,11 @@ def test_plot_colorbar_can_be_normalised(self, sdata_blobs: SpatialData): sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=norm).pl.show() def test_render_shapes_raises_when_color_key_missing(self, sdata_blobs_shapes_annotated: SpatialData): - with pytest.raises(KeyError, match="Unable to locate color key 'ghost'"): + missing_col = "__non_existent_column__" + with pytest.raises(KeyError, match=f"Unable to locate color key '{missing_col}'"): sdata_blobs_shapes_annotated.pl.render_shapes( element="blobs_polygons", - color="ghost", + color=missing_col, ).pl.show() def test_render_shapes_raises_for_invalid_table_name(self, sdata_blobs_shapes_annotated: SpatialData): @@ -733,27 +734,27 @@ def test_plot_datashader_can_render_shapes_with_colored_double_outline(self, sda method="datashader", ).pl.show() + def test_raises_when_table_does_not_annotate_element(sdata_blobs: SpatialData): + # Work on an independent copy since we mutate tables + sdata_blobs_local = deepcopy(sdata_blobs) -def test_warns_when_table_does_not_annotate_element(sdata_blobs: SpatialData): - # Work on an independent copy since we mutate tables - sdata_blobs_local = deepcopy(sdata_blobs) + # Create a table that annotates a DIFFERENT element than the one we will render + other_table = sdata_blobs_local["table"].copy() + other_table.obs["region"] = pd.Categorical(["blobs_points"] * other_table.n_obs) # Different region + other_table.uns["spatialdata_attrs"]["region"] = "blobs_points" + sdata_blobs_local["other_table"] = other_table - # Create a table that annotates a DIFFERENT element than the one we will render - other_table = sdata_blobs_local["table"].copy() - other_table.obs["region"] = pd.Categorical(["blobs_points"] * other_table.n_obs) # Different region - other_table.uns["spatialdata_attrs"]["region"] = "blobs_points" - sdata_blobs_local["other_table"] = other_table - - # Rendering "blobs_circles" with a table that annotates "blobs_points" - # should raise a warning and fall back to using no table. - with pytest.warns(UserWarning, match="does not annotate element"): - ( + # Rendering "blobs_circles" with a table that annotates "blobs_points" + # should now raise to alert the user about the mismatch. + with pytest.raises( + KeyError, + match="Table 'other_table' does not annotate element 'blobs_circles'", + ): sdata_blobs_local.pl.render_shapes( "blobs_circles", color="channel_0_sum", table_name="other_table", ).pl.show() - ) def test_plot_can_handle_nan_values_in_color_data(self, sdata_blobs: SpatialData): """Test that NaN values in color data are handled gracefully.""" From 6140193eb829278e0ab9c6df398575a5b873b177 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Tue, 18 Nov 2025 22:47:15 +0100 Subject: [PATCH 03/16] update --- src/spatialdata_plot/pl/render.py | 25 +++++++++++++++++++------ src/spatialdata_plot/pl/utils.py | 26 ++++++++++++++------------ tests/pl/test_render_shapes.py | 2 +- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index 76e57d82..f7999837 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -144,12 +144,25 @@ def _render_shapes( # continuous case: leave NaNs as NaNs; utils maps them to na_color during draw if color_source_vector is None and not values_are_categorical: - color_vector = np.asarray(color_vector, dtype=float) - if np.isnan(color_vector).any(): - nan_count = int(np.isnan(color_vector).sum()) - logger.warning( - f"Found {nan_count} NaN values in color data. These observations will be colored with the 'na_color'." - ) + _series = color_vector if isinstance(color_vector, pd.Series) else pd.Series(color_vector) + + try: + color_vector = np.asarray(_series, dtype=float) + except (TypeError, ValueError): + nan_count = int(_series.isna().sum()) + if nan_count: + logger.warning( + f"Found {nan_count} NaN values in color data. " + "These observations will be colored with the 'na_color'." + ) + color_vector = _series.to_numpy() + else: + if np.isnan(color_vector).any(): + nan_count = int(np.isnan(color_vector).sum()) + logger.warning( + f"Found {nan_count} NaN values in color data. " + "These observations will be colored with the 'na_color'." + ) # Using dict.fromkeys here since set returns in arbitrary order # remove the color of NaN values, else it might be assigned to a category diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 5a578dd8..1763607e 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -2149,19 +2149,21 @@ def _validate_col_for_column_table( ) else: tables = get_element_annotators(sdata, element_name) - for table_name in tables.copy(): - if col_for_color not in sdata[table_name].obs.columns and col_for_color not in sdata[table_name].var_names: - tables.remove(table_name) + for annotates in tables.copy(): + if col_for_color not in sdata[annotates].obs.columns and col_for_color not in sdata[annotates].var_names: + tables.remove(annotates) if len(tables) == 0: - col_for_color = None - elif len(tables) >= 1: - table_name = next(iter(tables)) - if len(tables) > 1: - warnings.warn( - f"Multiple tables contain column '{col_for_color}', using table '{table_name}'.", - UserWarning, - stacklevel=2, - ) + raise KeyError( + f"Unable to locate color key '{col_for_color}' for element '{element_name}'. " + "Please ensure the key exists in a table annotating this element." + ) + table_name = next(iter(tables)) + if len(tables) > 1: + warnings.warn( + f"Multiple tables contain column '{col_for_color}', using table '{table_name}'.", + UserWarning, + stacklevel=2, + ) return col_for_color, table_name diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index 90fa5c6f..b5989826 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -734,7 +734,7 @@ def test_plot_datashader_can_render_shapes_with_colored_double_outline(self, sda method="datashader", ).pl.show() - def test_raises_when_table_does_not_annotate_element(sdata_blobs: SpatialData): + def test_raises_when_table_does_not_annotate_element(self, sdata_blobs: SpatialData): # Work on an independent copy since we mutate tables sdata_blobs_local = deepcopy(sdata_blobs) From aa9ed765c13f6270167cd4f0741186f4171030b9 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Tue, 18 Nov 2025 23:22:39 +0100 Subject: [PATCH 04/16] bump --- src/spatialdata_plot/pl/render.py | 3 ++ src/spatialdata_plot/pl/utils.py | 83 ++++++++++++++++++++----------- tests/pl/test_render_points.py | 18 +++++++ tests/pl/test_render_shapes.py | 18 +++++++ 4 files changed, 92 insertions(+), 30 deletions(-) diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index 76e57d82..c5d58f80 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -121,6 +121,7 @@ def _render_shapes( cmap_params=render_params.cmap_params, table_name=table_name, table_layer=table_layer, + coordinate_system=coordinate_system, ) values_are_categorical = color_source_vector is not None @@ -681,6 +682,7 @@ def _render_points( alpha=render_params.alpha, table_name=table_name, render_type="points", + coordinate_system=coordinate_system, ) if added_color_from_table and col_for_color is not None: @@ -1217,6 +1219,7 @@ def _render_labels( cmap_params=render_params.cmap_params, table_name=table_name, table_layer=table_layer, + coordinate_system=coordinate_system, ) # rasterize could have removed labels from label diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index cccb587e..bdb0f7d8 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -807,6 +807,7 @@ def _set_color_source_vec( table_name: str | None = None, table_layer: str | None = None, render_type: Literal["points"] | None = None, + coordinate_system: str | None = None, ) -> tuple[ArrayLike | pd.Series | None, ArrayLike, bool]: if value_to_plot is None and element is not None: color = np.full(len(element), na_color.get_hex_with_alpha()) @@ -873,9 +874,12 @@ def _set_color_source_vec( return color_source_vector, color_vector, True - logger.warning(f"Color key '{value_to_plot}' for element '{element_name}' not been found, using default colors.") - color = np.full(sdata[table_name].n_obs, na_color.get_hex_with_alpha()) - return color, color, False + table_repr = f"'{table_name}'" if table_name is not None else "unspecified table" + coord_repr = f"'{coordinate_system}'" if coordinate_system is not None else "unspecified coordinate system" + raise KeyError( + f"Color key '{value_to_plot}' for element '{element_name}' could not be resolved " + f"(table={table_repr}, coordinate_system={coord_repr})." + ) def _map_color_seg( @@ -2132,35 +2136,54 @@ def _validate_col_for_column_table( table_name: str | None, labels: bool = False, ) -> tuple[str | None, str | None]: + if col_for_color is None: + return None, None + if not labels and col_for_color in sdata[element_name].columns: - table_name = None - elif table_name is not None: - tables = get_element_annotators(sdata, element_name) - if table_name not in tables or ( - col_for_color not in sdata[table_name].obs.columns and col_for_color not in sdata[table_name].var_names - ): - warnings.warn( - f"Table '{table_name}' does not annotate element '{element_name}'.", - UserWarning, - stacklevel=2, + return col_for_color, None + + tables = list(get_element_annotators(sdata, element_name)) + + def _table_has_column(tname: str) -> bool: + table = sdata[tname] + return col_for_color in table.obs.columns or col_for_color in table.var_names + + def _region_message(tname: str) -> str: + if tname not in sdata.tables: + return "" + attrs = sdata.tables[tname].uns.get("spatialdata_attrs", {}) + regions: list[str] = [] + if (region_key := attrs.get("region_key")) and region_key in sdata.tables[tname].obs.columns: + regions = sorted({str(r) for r in sdata.tables[tname].obs[region_key].unique().tolist()}) + elif (region_values := attrs.get("region")) is not None: + if isinstance(region_values, str): + regions = [region_values] + elif isinstance(region_values, list): + regions = [str(r) for r in region_values] + if regions: + return f" Regions available in table '{tname}': {regions}." + return "" + + if table_name is not None: + if table_name not in tables: + raise KeyError( + f"Table '{table_name}' does not annotate element '{element_name}'.{_region_message(table_name)}" ) - table_name = None - col_for_color = None - else: - tables = get_element_annotators(sdata, element_name) - for table_name in tables.copy(): - if col_for_color not in sdata[table_name].obs.columns and col_for_color not in sdata[table_name].var_names: - tables.remove(table_name) - if len(tables) == 0: - col_for_color = None - elif len(tables) >= 1: - table_name = next(iter(tables)) - if len(tables) > 1: - warnings.warn( - f"Multiple tables contain column '{col_for_color}', using table '{table_name}'.", - UserWarning, - stacklevel=2, - ) + if not _table_has_column(table_name): + raise KeyError(f"Column '{col_for_color}' not found in table '{table_name}'.") + return col_for_color, table_name + + candidate_tables = [tname for tname in tables if _table_has_column(tname)] + if not candidate_tables: + raise KeyError(f"Column '{col_for_color}' not found in any table annotating element '{element_name}'.") + + table_name = candidate_tables[0] + if len(candidate_tables) > 1: + warnings.warn( + f"Multiple tables contain column '{col_for_color}', using table '{table_name}'.", + UserWarning, + stacklevel=2, + ) return col_for_color, table_name diff --git a/tests/pl/test_render_points.py b/tests/pl/test_render_points.py index 5e3fc38a..292d6dad 100644 --- a/tests/pl/test_render_points.py +++ b/tests/pl/test_render_points.py @@ -178,6 +178,24 @@ def test_plot_datashader_can_color_by_category(self, sdata_blobs: SpatialData): method="datashader", ).pl.show() + def test_render_points_missing_color_column_raises_key_error(self, sdata_blobs: SpatialData) -> None: + sdata_blobs["table"].obs["region"] = pd.Categorical(["blobs_points"] * sdata_blobs["table"].n_obs) + sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_points" + with pytest.raises(KeyError, match="does_not_exist"): + sdata_blobs.pl.render_points(element="blobs_points", color="does_not_exist") + + def test_render_points_missing_region_for_table_raises_key_error(self, sdata_blobs: SpatialData) -> None: + blob = deepcopy(sdata_blobs) + blob["table"].obs["region"] = pd.Categorical(["blobs_points"] * blob["table"].n_obs) + blob["table"].uns["spatialdata_attrs"]["region"] = "blobs_points" + blob["table"].obs["table_value"] = np.arange(blob["table"].n_obs) + other_table = blob["table"].copy() + other_table.obs["region"] = pd.Categorical(["other"] * other_table.n_obs) + other_table.uns["spatialdata_attrs"]["region"] = "other" + blob["other_table"] = other_table + with pytest.raises(KeyError, match="does not annotate element"): + blob.pl.render_points(element="blobs_points", color="table_value", table_name="other_table") + def test_plot_datashader_colors_from_table_obs(self, sdata_blobs: SpatialData): n_obs = len(sdata_blobs["blobs_points"]) obs = pd.DataFrame( diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index 8fc39582..82d5a484 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -194,6 +194,24 @@ def test_plot_colorbar_can_be_normalised(self, sdata_blobs: SpatialData): norm = Normalize(vmin=0, vmax=5, clip=True) sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=norm).pl.show() + def test_render_shapes_missing_color_column_raises_key_error( + self, sdata_blobs_shapes_annotated: SpatialData + ) -> None: + with pytest.raises(KeyError, match="does_not_exist"): + sdata_blobs_shapes_annotated.pl.render_shapes(element="blobs_polygons", color="does_not_exist") + + def test_render_shapes_missing_region_for_table_raises_key_error( + self, sdata_blobs_shapes_annotated: SpatialData + ) -> None: + blob = deepcopy(sdata_blobs_shapes_annotated) + blob["table"].obs["table_value"] = np.arange(blob["table"].n_obs) + other_table = blob["table"].copy() + other_table.obs["region"] = pd.Categorical(["other"] * other_table.n_obs) + other_table.uns["spatialdata_attrs"]["region"] = "other" + blob["other_table"] = other_table + with pytest.raises(KeyError, match="does not annotate element"): + blob.pl.render_shapes(element="blobs_polygons", color="table_value", table_name="other_table") + def test_plot_can_plot_shapes_after_spatial_query(self, sdata_blobs: SpatialData): # subset to only shapes, should be unnecessary after rasterizeation of multiscale images is included blob = SpatialData.init_from_elements( From 88e4470e476550cc1c008ebd7f2b0ebb3911cfa6 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Tue, 18 Nov 2025 23:39:02 +0100 Subject: [PATCH 05/16] changed how we capture text --- tests/pl/test_render_shapes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index b5989826..d75934d1 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -756,7 +756,7 @@ def test_raises_when_table_does_not_annotate_element(self, sdata_blobs: SpatialD table_name="other_table", ).pl.show() - def test_plot_can_handle_nan_values_in_color_data(self, sdata_blobs: SpatialData): + def test_plot_can_handle_nan_values_in_color_data(self, sdata_blobs: SpatialData, caplog): """Test that NaN values in color data are handled gracefully.""" sdata_blobs["table"].obs["region"] = pd.Categorical(["blobs_circles"] * sdata_blobs["table"].n_obs) sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_circles" @@ -765,8 +765,9 @@ def test_plot_can_handle_nan_values_in_color_data(self, sdata_blobs: SpatialData sdata_blobs.shapes["blobs_circles"]["color_with_nan"] = [1.0, 2.0, np.nan, 4.0, 5.0] # Test that rendering works with NaN values and issues warning - with pytest.warns(UserWarning, match="Found 1 NaN values in color data"): + with caplog.at_level("WARNING", logger="spatialdata_plot._logging"): sdata_blobs.pl.render_shapes(element="blobs_circles", color="color_with_nan", na_color="red").pl.show() + assert "Found 1 NaN values in color data" in caplog.text def test_plot_colorbar_normalization_with_nan_values(self, sdata_blobs: SpatialData): """Test that colorbar normalization works correctly with NaN values.""" From e2001560cee6c682fe858ce4aa0a3dea0366a2dc Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Tue, 18 Nov 2025 23:46:46 +0100 Subject: [PATCH 06/16] removed caplog --- tests/pl/test_render_shapes.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index d75934d1..caccffa2 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -764,10 +764,8 @@ def test_plot_can_handle_nan_values_in_color_data(self, sdata_blobs: SpatialData # Add color column with NaN values sdata_blobs.shapes["blobs_circles"]["color_with_nan"] = [1.0, 2.0, np.nan, 4.0, 5.0] - # Test that rendering works with NaN values and issues warning - with caplog.at_level("WARNING", logger="spatialdata_plot._logging"): + with pytest.warns(UserWarning, match="Found 1 NaN values in color data"): sdata_blobs.pl.render_shapes(element="blobs_circles", color="color_with_nan", na_color="red").pl.show() - assert "Found 1 NaN values in color data" in caplog.text def test_plot_colorbar_normalization_with_nan_values(self, sdata_blobs: SpatialData): """Test that colorbar normalization works correctly with NaN values.""" From 29e3ab599f012489463299d9d82ba8b678215f55 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Wed, 19 Nov 2025 00:13:57 +0100 Subject: [PATCH 07/16] dropped warnings package --- src/spatialdata_plot/_logging.py | 31 +++++++++++++++++++++++++++++++ src/spatialdata_plot/pl/basic.py | 30 ++++++------------------------ src/spatialdata_plot/pl/render.py | 19 ++++++------------- src/spatialdata_plot/pl/utils.py | 13 ++----------- tests/pl/test_render_shapes.py | 3 ++- 5 files changed, 47 insertions(+), 49 deletions(-) diff --git a/src/spatialdata_plot/_logging.py b/src/spatialdata_plot/_logging.py index be1cf5f7..f5b0feed 100644 --- a/src/spatialdata_plot/_logging.py +++ b/src/spatialdata_plot/_logging.py @@ -1,6 +1,11 @@ # from https://github.com/scverse/spatialdata/blob/main/src/spatialdata/_logging.py import logging +import re +from collections.abc import Iterator +from contextlib import contextmanager + +from _pytest.logging import LogCaptureFixture def _setup_logger() -> "logging.Logger": @@ -21,3 +26,29 @@ def _setup_logger() -> "logging.Logger": logger = _setup_logger() + + +@contextmanager +def logger_warns( + caplog: LogCaptureFixture, + logger: logging.Logger, + match: str | None = None, + level: int = logging.WARNING, +) -> Iterator[None]: + """ + Context manager similar to pytest.warns, but for logging.Logger. + + Usage: + with logger_warns(caplog, logger, match="Found 1 NaN"): + call_code_that_logs() + """ + with caplog.at_level(level, logger=logger.name): + yield + + records = [r for r in caplog.records if r.levelno >= level] + + if match is not None: + pattern = re.compile(match) + if not any(pattern.search(r.getMessage()) for r in records): + msgs = [r.getMessage() for r in records] + raise AssertionError(f"Did not find log matching {match!r} in records: {msgs!r}") diff --git a/src/spatialdata_plot/pl/basic.py b/src/spatialdata_plot/pl/basic.py index 029168be..d34d9fe4 100644 --- a/src/spatialdata_plot/pl/basic.py +++ b/src/spatialdata_plot/pl/basic.py @@ -1,7 +1,6 @@ from __future__ import annotations import sys -import warnings from collections import OrderedDict from copy import deepcopy from pathlib import Path @@ -23,6 +22,7 @@ from xarray import DataArray, DataTree from spatialdata_plot._accessor import register_spatial_data_accessor +from spatialdata_plot._logging import logger from spatialdata_plot.pl.render import ( _render_images, _render_labels, @@ -272,11 +272,7 @@ def render_shapes( """ # TODO add Normalize object in tutorial notebook and point to that notebook here if "vmin" in kwargs or "vmax" in kwargs: - warnings.warn( - "`vmin` and `vmax` are deprecated. Pass matplotlib `Normalize` object to norm instead.", - DeprecationWarning, - stacklevel=2, - ) + logger.warning("`vmin` and `vmax` are deprecated. Pass matplotlib `Normalize` object to norm instead.") params_dict = _validate_shape_render_params( self._sdata, element=element, @@ -423,11 +419,7 @@ def render_points( """ # TODO add Normalize object in tutorial notebook and point to that notebook here if "vmin" in kwargs or "vmax" in kwargs: - warnings.warn( - "`vmin` and `vmax` are deprecated. Pass matplotlib `Normalize` object to norm instead.", - DeprecationWarning, - stacklevel=2, - ) + logger.warning("`vmin` and `vmax` are deprecated. Pass matplotlib `Normalize` object to norm instead.") params_dict = _validate_points_render_params( self._sdata, element=element, @@ -544,11 +536,7 @@ def render_images( """ # TODO add Normalize object in tutorial notebook and point to that notebook here if "vmin" in kwargs or "vmax" in kwargs: - warnings.warn( - "`vmin` and `vmax` are deprecated. Pass matplotlib `Normalize` object to norm instead.", - DeprecationWarning, - stacklevel=2, - ) + logger.warning("`vmin` and `vmax` are deprecated. Pass matplotlib `Normalize` object to norm instead.") params_dict = _validate_image_render_params( self._sdata, element=element, @@ -679,11 +667,7 @@ def render_labels( """ # TODO add Normalize object in tutorial notebook and point to that notebook here if "vmin" in kwargs or "vmax" in kwargs: - warnings.warn( - "`vmin` and `vmax` are deprecated. Pass matplotlib `Normalize` object to norm instead.", - DeprecationWarning, - stacklevel=2, - ) + logger.warning("`vmin` and `vmax` are deprecated. Pass matplotlib `Normalize` object to norm instead.") params_dict = _validate_label_render_params( self._sdata, element=element, @@ -918,9 +902,7 @@ def show( # go through tree for i, cs in enumerate(coordinate_systems): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=UserWarning) - sdata = self._copy() + sdata = self._copy() _, has_images, has_labels, has_points, has_shapes = ( cs_contents.query(f"cs == '{cs}'").iloc[0, :].values.tolist() ) diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index 477bd81b..acc3c7a1 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -1,6 +1,5 @@ from __future__ import annotations -import warnings from collections import abc from copy import copy @@ -101,11 +100,9 @@ def _render_shapes( and (color_col := sdata_filt[table_name].obs[col_for_color]).dtype == "O" and not _is_coercable_to_float(color_col) ): - warnings.warn( + logger.warning( f"Converting copy of '{col_for_color}' column to categorical dtype for categorical plotting. " - f"Consider converting before plotting.", - UserWarning, - stacklevel=2, + f"Consider converting before plotting." ) sdata_filt[table_name].obs[col_for_color] = sdata_filt[table_name].obs[col_for_color].astype("category") @@ -555,11 +552,9 @@ def _render_points( coords = ["x", "y"] if table_name is not None and col_for_color not in points.columns: - warnings.warn( + logger.warning( f"Annotating points with {col_for_color} which is stored in the table `{table_name}`. " - f"To improve performance, it is advisable to store point annotations directly in the .parquet file.", - UserWarning, - stacklevel=2, + f"To improve performance, it is advisable to store point annotations directly in the .parquet file." ) if col_for_color is None or ( @@ -573,11 +568,9 @@ def _render_points( and (color_col := sdata_filt[table_name].obs[col_for_color]).dtype == "O" and not _is_coercable_to_float(color_col) ): - warnings.warn( + logger.warning( f"Converting copy of '{col_for_color}' column to categorical dtype for categorical " - f"plotting. Consider converting before plotting.", - UserWarning, - stacklevel=2, + f"plotting. Consider converting before plotting." ) sdata_filt[table_name].obs[col_for_color] = sdata_filt[table_name].obs[col_for_color].astype("category") else: diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 78a4a13b..dbe0489d 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -2,7 +2,6 @@ import math import os -import warnings from collections import OrderedDict from collections.abc import Iterable, Mapping, Sequence from copy import copy @@ -2163,11 +2162,7 @@ def _validate_col_for_column_table( ) table_name = next(iter(tables)) if len(tables) > 1: - warnings.warn( - f"Multiple tables contain column '{col_for_color}', using table '{table_name}'.", - UserWarning, - stacklevel=2, - ) + logger.warning(f"Multiple tables contain column '{col_for_color}', using table '{table_name}'.") return col_for_color, table_name @@ -2731,11 +2726,7 @@ def _multipolygon_to_square(multipolygon: shapely.MultiPolygon) -> tuple[shapely else: non_point_count += 1 if non_point_count > 0: - warnings.warn( - "visium_hex supports Points best. Non-Point geometries will use regular hex conversion.", - UserWarning, - stacklevel=2, - ) + logger.warning("visium_hex supports Points best. Non-Point geometries will use regular hex conversion.") if len(point_centers) >= 2: centers = np.array(point_centers, dtype=float) # pairwise min distance diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index caccffa2..099eddce 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -17,6 +17,7 @@ from spatialdata.transformations._utils import _set_transformations import spatialdata_plot # noqa: F401 +from spatialdata_plot._logging import logger, logger_warns from tests.conftest import DPI, PlotTester, PlotTesterMeta, _viridis_with_under_over, get_standard_RNG sc.pl.set_rcParams_defaults() @@ -764,7 +765,7 @@ def test_plot_can_handle_nan_values_in_color_data(self, sdata_blobs: SpatialData # Add color column with NaN values sdata_blobs.shapes["blobs_circles"]["color_with_nan"] = [1.0, 2.0, np.nan, 4.0, 5.0] - with pytest.warns(UserWarning, match="Found 1 NaN values in color data"): + with logger_warns(caplog, logger, match="Found 1 NaN values in color data"): sdata_blobs.pl.render_shapes(element="blobs_circles", color="color_with_nan", na_color="red").pl.show() def test_plot_colorbar_normalization_with_nan_values(self, sdata_blobs: SpatialData): From 33fecec746d8e2fe26e1ae9a89baceb46c122c19 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Wed, 19 Nov 2025 00:20:58 +0100 Subject: [PATCH 08/16] img from runner --- src/spatialdata_plot/pl/utils.py | 7 ++++--- ...s_colorbar_normalization_with_nan_values.png | Bin 0 -> 21365 bytes 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 tests/_images/Shapes_colorbar_normalization_with_nan_values.png diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index dbe0489d..91485b7f 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -423,9 +423,10 @@ def _as_rgba_array(x: Any) -> np.ndarray: used_norm = colors.Normalize(vmin=vmin, vmax=vmax, clip=False) fill_c[is_num] = cmap(used_norm(num[is_num])) - # non-numeric entries as explicit colors - if (~is_num).any(): - fill_c[~is_num] = ColorConverter().to_rgba_array(c_series[~is_num].tolist()) + # non-numeric, non-NaN entries as explicit colors + non_numeric_color_mask = (~is_num) & c_series.notna().to_numpy() + if non_numeric_color_mask.any(): + fill_c[non_numeric_color_mask] = ColorConverter().to_rgba_array(c_series[non_numeric_color_mask].tolist()) # Case C: single color or list of color-like specs (strings or tuples) else: diff --git a/tests/_images/Shapes_colorbar_normalization_with_nan_values.png b/tests/_images/Shapes_colorbar_normalization_with_nan_values.png new file mode 100644 index 0000000000000000000000000000000000000000..2e224d5b7fdb7b20acd35c1e686a57a0e1b21e47 GIT binary patch literal 21365 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

;{8PO=_N;oe2;EPti&aGgOy+=oJ&m;M%lYv!)H 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 literal 0 HcmV?d00001 From 73f75ee977f1d54e87d96359cb96cf2ade5f01a3 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Wed, 19 Nov 2025 00:36:37 +0100 Subject: [PATCH 09/16] updated test --- src/spatialdata_plot/_logging.py | 19 +++++++++++++++++-- src/spatialdata_plot/pl/render.py | 31 +++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/spatialdata_plot/_logging.py b/src/spatialdata_plot/_logging.py index f5b0feed..b7438972 100644 --- a/src/spatialdata_plot/_logging.py +++ b/src/spatialdata_plot/_logging.py @@ -42,10 +42,25 @@ def logger_warns( with logger_warns(caplog, logger, match="Found 1 NaN"): call_code_that_logs() """ + # Store initial record count to only check new records + initial_record_count = len(caplog.records) + + # Add caplog's handler directly to the logger to capture logs even if propagate=False + handler = caplog.handler + logger.addHandler(handler) + original_level = logger.level + logger.setLevel(level) + + # Use caplog.at_level to ensure proper capture setup with caplog.at_level(level, logger=logger.name): - yield + try: + yield + finally: + logger.removeHandler(handler) + logger.setLevel(original_level) - records = [r for r in caplog.records if r.levelno >= level] + # Only check records that were added during this context + records = [r for r in caplog.records[initial_record_count:] if r.levelno >= level] if match is not None: pattern = re.compile(match) diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index acc3c7a1..e3f31205 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -487,10 +487,33 @@ def _render_shapes( if not values_are_categorical: vmin = render_params.cmap_params.norm.vmin vmax = render_params.cmap_params.norm.vmax - if vmin is None: - vmin = float(np.nanmin(color_vector)) - if vmax is None: - vmax = float(np.nanmax(color_vector)) + if vmin is None or vmax is None: + # Extract numeric values only (filter out strings and other non-numeric types) + if isinstance(color_vector, np.ndarray): + if np.issubdtype(color_vector.dtype, np.number): + # Already numeric, can use directly + numeric_values = color_vector + else: + # Mixed types - extract only numeric values using pandas + numeric_values = pd.to_numeric(color_vector, errors="coerce") + numeric_values = numeric_values[np.isfinite(numeric_values)] + if len(numeric_values) > 0: + if vmin is None: + vmin = float(np.nanmin(numeric_values)) + if vmax is None: + vmax = float(np.nanmax(numeric_values)) + else: + # No numeric values found, use defaults + if vmin is None: + vmin = 0.0 + if vmax is None: + vmax = 1.0 + else: + # Not a numpy array, use defaults + if vmin is None: + vmin = 0.0 + if vmax is None: + vmax = 1.0 _cax.set_clim(vmin=vmin, vmax=vmax) if ( From 39e58072110e37bd84533153d30fb4a5fab19271 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Wed, 19 Nov 2025 00:41:41 +0100 Subject: [PATCH 10/16] images from runner --- ..._can_handle_mixed_numeric_and_color_data.png | Bin 0 -> 23393 bytes ...apes_can_handle_nan_values_in_color_data.png | Bin 0 -> 23484 bytes ...pes_can_handle_non_numeric_radius_values.png | Bin 0 -> 16115 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/_images/Shapes_can_handle_mixed_numeric_and_color_data.png create mode 100644 tests/_images/Shapes_can_handle_nan_values_in_color_data.png create mode 100644 tests/_images/Shapes_can_handle_non_numeric_radius_values.png diff --git a/tests/_images/Shapes_can_handle_mixed_numeric_and_color_data.png b/tests/_images/Shapes_can_handle_mixed_numeric_and_color_data.png new file mode 100644 index 0000000000000000000000000000000000000000..8ab0b742d651b040ef5e3ac16d8ff86e4671b1c2 GIT binary patch literal 23393 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 literal 0 HcmV?d00001 diff --git a/tests/_images/Shapes_can_handle_nan_values_in_color_data.png b/tests/_images/Shapes_can_handle_nan_values_in_color_data.png new file mode 100644 index 0000000000000000000000000000000000000000..46d9dc44661817abc439ad88a09b0e75cb67af5a GIT binary patch literal 23484 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 N`?U6J7VWhU{Xa_2Q@H>D literal 0 HcmV?d00001 diff --git a/tests/_images/Shapes_can_handle_non_numeric_radius_values.png b/tests/_images/Shapes_can_handle_non_numeric_radius_values.png new file mode 100644 index 0000000000000000000000000000000000000000..8e06b52fdb55a76f0b579616a94b8d0b99466a6d GIT binary patch literal 16115 zcmbWe2RPR6`!{~yHrab*3nAQyjI3-TBUzb|y_G#e*|C6M`B8%WVtg#N zo}R8A5&{BF|MLib7x#w(8kOvNa20%46+;gMf>0CvgA95c!;U~mo2e?u>3L^tO#Aq2 zES=+SjXZc6)9W0~@xX~pfkV%(*nI=5&Zs8*h5GyVDY;Qb?@aaB)PmIP92%}rz5U8j zbY^7f^dw(T!6p*pGjDbDQDxc&+@Ctw2?ouL_|bzU7gs@?rx>c5dC|H3F6lCV_y2}_8com6l_@Rofz-! z$D^8e`gw6qUho;VBPv$L3Wd9A86-Tp9X{oRoFBQ(ccWsJI$*N)PO7S^(lRp{qhn&e zZ0_#w+wAY}uXo2X90zb|Wj#PlMp5zh#}pP7MNm;wSJ9aHKfj$ODAyX)6-7me_~5%| ze^)b=G)cCuBI4WFisucV9ggF_D^?X3v0^jci)+M8QW5(bQ+b-162}2vYhM*l_BY(N z=Hx=FHm2&7hhAOZi@zl=Kex6PZPgREa=U1b^{z-7ev_j1 zZ)BE@@G`3(&vz41Qc=CuFEhe9arjz_&s$O$)al{j@$=6gQ~TBrA0*v9Jc#zk-`)S9 z*C64wItP=AINT_KC-`}Oc4F|}mJIz+^F8r-hSfH`BO~R5c4U@2MMPm~%|ywq3(bU)EJv~Jn9UZfL861ApyH!+I-&z{Z+#b`C zvFLb8wEUw%ScINP`goZ=vCcau5+>o*`7E?3v26e z)>}`_C*IvRsCA^nA-;^KrKMG@l}Ty=^B?^wPnpF3WTWne_ZExq!JpXc4qwg3-`@SS z;_3S65&!OW*ex9)n7DI){=~h$^~`dz!eZ<1-%c0{QR5bd>`2y4lzAH>hE5n~v*m;z z0|TSNs)zWDN;!%1b_Fb^)BPzIyi1ol60fLlogK`wv#`kScyK9DQdx;)N>*y%<4bvmI z0Ki&%Avl=;6*@{p;`e(X&I52v2n|{wBhWE zii%I3s}u6tjOqghdw%EzOG!z~$~I;rF)K}8a<_!~*Ze#gB_+p2`kH6kbN03u*C0yB zx(nTTj=xWf85MQ7&++gfiwNcNBpKc%OnR|JeC6QeWJ=Ut35#COXCeJFUmqVjR{F8R zG%PmBOkx!kgs3B;)(Mj-UAeDlXD9fk-=U~A!Q9%VzpL1Uw21fiSNL9%%JVVTgwRgy zt+i!{2WK;1u$k4UHU8Fq5kqJ`%9i8h5NN%#QP&oVyVESgs(|7Sp&eKlXp`JTn3}fz z?hC#;*zS8QvQ-|0WMVp9{k`urN=7En7x4i9_ph#Kh)1$2DtL}gPCw`8b>9xc>&?+A zR&%oIj=6eQ6on#VWo3m>yS2C1^#uD;K0&kPJ2PDAQ&R73Me_x<8E$8Lc<67{M|ZV7 zMYgtHFGt|9Qp;9}CU`6jTfmO-UMWazqvfq>H^#3qLhl-ebw77xR(?K5Sa`U_Tw6$* zOptV9Vxpi$JJ#gnq&e&eoy_)XDk>xj)mCcQ*6e3#Syc4H#JnAAZa^Re`%Vyjo>$@?oKK!Ssj%!KDAMC;IzL|P z?eA}g!I6@Yu?E$3f6+6a6r4MB^O>D6I6uDRy>=~2mRbNO%Yfj8N(72UlA+!=#l_ID zt;RuC4tb$%Y2Ed~Mp6>fYwf(*#|JrH5*?RCikWyN;bEELeKKrp?8#<7G3kp_k5JWZ z93uL#@|I(QXC%xoT;4io5=TT~ywSpaZ{#zhtF|_Q>34vk{sM7!92wUwbiV=PvNU1F z4P=HCeul)|hFb2Fs9DbYjmc(5wYDGqAZpRwd?4Si?#({L_T39UKRY6czv;&_`#c0Scj_~f_h&vnWWsd*UYtua_UTq=)6FwHABE)` z35j8&O+FqFP_5R!mZb?<6Y$?}46`11fedch=s)uRnpXq`x|0f--f%A zDI;*^7fDWmnIVaLddR+|a6ietK9KS7kK|msOYoqE&&=+h`+-yqfj3+C#+xO#&QD~Q zhEGwfgee3 zJj!{3alZDa(#r6?RZsA%>%7+$)e2T8e%RGCQFL`YldZOCn=n&9eiVM)fzA44zmSHT zA1ClAye?I*Tr)EN3FQ&pI|9y7?K~xvi_4~a%563s*IW_7gtxcD1E5) z?LxF~sxGy)DV@P;bTsCsKX+s8;L}R!4!DAncwDjYw`{H}M2qL==XXlR3%*O}=;|VK z^rj+49cKuewx3$pJWjs%NVK&l@Q=y25gM9Z->ZClH@EW&1_rU(JC4S?Yn^iQd}=c; zo1c2mh0_FYwk~DL;-T1*)LAMkDmkQw6K*JdU6(>85Rk zGh;%QvpD{n`n1PB&Oc}mH-m=On%|&@CtuI z@_;mgiO7NVvbDdj1fizAqhqwf;?}aKgyVE0)j+C%GGM@uy?b_JZ(aj{pq2K&0f)G) zPonrtn`pJQwPAX?iB8uIXA15{-eb~g^h*_cn_BqeXt$}^cEhM+$;9L^!z5F3SK&)e z$kFXB?-lY3jF~zuPma4D%cQODuWm9N&S9S43Ql;NusUO)q=bWcXR#InLpQCJOY(zG$8p?uIjve+ zeoe6qdh$-39nqMY^9dzZjurC$J)l^8#!Opcdf$6mzbI~?y2fe1;8E@J_v_+caCA_l zjKL;Z>9X&Qx=n-bAJT-0yHK6GJ50Z-yf~i_VepBLqD{Y{?VYLL5SLx!IXEO4yf<&M2$D|LsGlE<^#8hoAZXSz`YyA9C5GV@DPSa@koq zHR0efEk$Kn9{UZWocoe9lg@+;Vi>{?>6Z*%EH2*8dF;}1j>oF?dJxNfm9oOGZkb!f zI^)JiKee;Hd*g8)dpv@cVQ!XHdRMPP28G*mYP`gM&__N>mhiUT^|iff7>b1r!~4Af zozGLn=-^OXMP)gSx=S=W{NrI#N{09fg_wznFdZ>YT?B#U=RW6$n1`(wNzzQ}`QxAO;$2lV z#OIJP^Tbl%dH(#={R_7Y9-h&1m>LxK7c4NLK>?7&}Ews;|l;%dqU%>> z`5SUCE{Q9Ih~Yd?qcts-m-mv21|=*VBs`Y&E1~d_(V8xXcGl7EN@|rJ;FLH<3Px-) z4RIJuIM2vt>sYhzRi`OV2@JM0cXB*!PI@Vx%sgdSCRlk;au#Rk7B2Uj{zTWEJX~Tl zy}huK$s)+17TwXzqGUw=`!m1ke}1m2Qh>A31#7X?tC#miIn-GgL>*;kTLQFO0|O?f zrt+X%Lt`9*>@*#Y{|=}6rA#$62>k;BUXR+6($hy}Nkm;tt_{^#GqkLW2I$ zqep3CE=;v9vvB~;Emyx3)1oZHS~PTY$lKf7NhvAA!osj(=&ut{aB8&OGYhbGElo?K z_222^LL)nQd5l*gc27*3eSbbsq~9tni}kV(OCX;pkB$qD=;UxmS3@T@L$A6J73=Gc z5D)=STfKh$y0pIDz|01~*x=AmtX+)}d~|? zYF=G&nH{FxY|u={Vi+&SSeu;skT^^F=Bl{(H}0P8zkfyg-83>E++%_Aou@`XM1&HN zkdhMqLp)WXS$2p1PO)E!DNM2^6LNirl`X=7tp=FrYp|i7dnR)4^cX0cw>(8qKKJaapE9kX;7H*KxF9}Je9Mz`1jfOFN%l`SC zJ{S#t2mFm*YdB^4(E%s>uL0a5&Rd?;10PYmBaun$?CgB(4*wtgnHH`mxZ3dK>eYk% z-d+_2C8bqnKOdiu`+x3Z?JZlp_h5gjQ|nTAoRZcz?q-mtSk?ge`bka>%cw;fk*bRY z6(wcPapYRHKFO!X`rPw4RB^G0Vgq9(s$|erlWZk(8A5T>gY~b8|C~SvlzG?rt9+rz$h9!$G{a z8^akM9yX|R&TJvHA_O#bt57pD=bg%wQjPsx$*b}auN%G48!&O5`s>MNYy9nWQH!so zhES8?(Jy%kyi9al9RIF2fR4ebsr2G;d+N)V2g%9Fc_`xx3%Qs%MC=j|Ki;Em zeEEY#?9va7cX1R?UYn*Z?sd+{bWx;nEo``L4oi*j@U~)If8&-6Gpgoe9_GBAkszI0tzpRxImtEG zw~m$|*>(Dh-$0Z%@Bdw*!;E9Zd`g5Vbu)yF-5Uc?40p69?)of=|0$}~dO8ocB0zFh zD@N@lJniuwro234uLz&j?=|a>p8c|95q&W`V?Rz{P(#4YogmXf4&xoa>4$NCE~^nE zrK-W2oGx2hig>xWz}ERIESyf-hEPT0_Xa5@E(Vq1`fdJ~FHhh8?xG3VY{@HLjs?Jg zg*e|P3G`9NyDG!QmE$to%3PR_C~qD16FWaHsi^4R=%)%)FSFBK)zDQ0z`{edH6#{X zUA?Rl@uf8ROk3=&MTa+oiUq5n1NEm|Y!?qfvDUNhAX#Dt9I00sDSCuwW(zC${znDN z+!0?)eN{cNUReFL4ICeROC{(;uf?(GIL;&eqBHqTpBP25EW&*G)6Qd6jzzmwy-_lQ zr^JlG?8@HkM=N8wd8n*XzL`&1smjeYJi+96Z7$Yj2FNev0utnG;R&RHCAkjwMxUq2 zh>6WjKSq}6dvbiP_oG=JkLDuZfk348<=qfXcV!(7BR;A&k?EGlVnl4mkMSa;<0Oj& z!O9hmttk(YPlpYF_4sel8Z;YuQg%)r@6BJ};O_Fj$#-|2CT9%sAI@IN`!iC&mMs~Xu+$L9Xgiyw^;e}|0Y9HFJFs8O@_fc` zWUI1OMlhf;%^>2aukYu47t^(x+l1N}SEJRs%H_Eu)9Sw096j*WI{r-|ZTTu;r{0HR z`7?I?quZVwoX^O6$C5e78Z)h1X8cIJ--}cs85m~P;+(CY?=-rHU(k|?m#5+-&Fz&QWN})@v!i%&qanR`auuJyYq*ys6y_BM`;xEv z55IXxC&T1@;^gy-^~KMV&5#0hMP&;i?vUhru7a+oH?jre(nMWcq^G-gi}d{U^ z*6Tdnmi291=GMTtk`PsNKYb*1_v}CfnRhi^to+pB!PM05_|EE==OP@Ia!gWldE3#^ zipp%4;aHvG;Kwt_;jDtAYK>6_7lccTVdcb^65RD4s%y4MJCd%5!()e0Z9HZC{Hms= zV5g~IM#4XowX8i$Y=LYN4-SsNYQ z;zReBNnDu9@jKWK#LPG?$5G{BleyS)KebyrbM<#o#DD&L?r|bExqp=EKlJMJ<=OA_ zhrce()SH%PiEOgAe|qh&`d#J%>zyeTW9*R?p+T*NC#5_ZF1oF6gnTVK+P{%WT<;co zL6Vt0A`n|vdgxTSI_{I`<}r_7Tu)0YN(jYlc*YY|SNC6L>$hL2T$pA`Y&%a;aHbv5 zgg?WLZ&fBaPiuTlKT$AG$OzSP&x!X9^S( zHAaE|4ny+^ru_{JkrxL+{->ceHOsd7H1z#T>=ZX$$NC9q1%of5E*2R&s7P*y7!eUN z4VEYBm*e;csHc9bQrxfLn`sG%S{vp3cCB=I+4k`8(0r!Jr%1Pun3!2QrdYR-?aCGG z5~hJK_BHe;2b*HvWhWGMZh%Ngsi~i$5$*Zuk&2pHdl)_in%kQVC|Sd=UHb&oZc ze%mnA!(Bm{BQ^x{C}7ckTtRt9tcIReocwU2HTdEzzV52yB!ZGEGP0-V+3Z{2CB_{6 zvcgn#E!^SEq-%U#+4}U8v@UKivy}{hAa^Vj~ML@fDJzRu9L|ge?IsTbB)qq- z=C;wp!P{8p?($Ya7m1A-2!rK02OQRnM=4{Alu_2sr*b$`wN%ic2EOJyD=e zb3;3qPA_=)mPu z%dfDxo$okn+CT#-!)#hqv`Oo1D7~9#SYGc)_;ic4wM?mBuVBO#|H^I6t6uB_Pyrf% zB9sPD^kl8PHOp99T>K@{9FTPks(Uz7vIkKCZ|H>tKcejpi&4y)r#Xu|&)? z@e;&vt_%!PGKMVZ-eCYWL7{N$=SJOZ_x5}b88dS{&Vq>kX=TczOpx9Ge9mgWs{aaIlvEGQ9dCo5PzB^Ya+r=vhI zVrOH+kd80QLpgmHK})V3&q$QLy>G-ZNM%4==%r>@x(tSbN6y)zuL!a+O2wUsx`?F-|Ik=Fnpm2V)TmxQ4&Q{b^ zJ~_AE0LYYfkm41t0@c9|)JKF)+U|S7P7ZDK-_#brv307zH@O@&4!{!_kgBnSaYtU! z)UmUt^ejn$5<^X=?%gsT-z)|1TO%R#xA<2`SFRStT}7X?kdYLp7xH8^4SnE_ZK4j_ zYi0Gvyhu>wlJk8$UQO!Q6R3TL@>!c)S$VFD2C!Iz9#2+2)f4myV{c;N%J;_J1c!z$ zb3eER0$#z&xH>>#-Se069w@`LuDE*wmktjgZW>63hfl^B zzK%XsFHbo-uFbvaAZr;fEC)S3*PRKQ&H)Y%nr(b@xR1hbA`fME$~PrAvvAXJZ&qN6+$#&Bz;kYF>x0e2OIf`Ea?<*X=NxPTs-gv{di} z&4b}oG=qf9&9iI%8&y$>)GxjB<#@^ZHTsVxCc)MN3d$DJ(kFy?1muk2t}u11Ch<&d zpI+zY2?l_SogK&{L69U11KOzzzeH8@=5e^V1ct*`L~U&@I|?6te2~W@oLa}97v#5i zs7M(uiP&>UkSEE(8J5dXFnO&}idC+9=tOH0pyf%v_+NS~~(p`ifeJ}h*gvntI$J`swip`gGt zGc&Ulp$~id)Dk3Zop4P&WH5;gmd!9FW4Jen*IUz%MJB5VK}dsS_ugfeX*S@15iQCC zYi0p*^zuY`_WuvA>vvz0PPi5x();na-I#(htLAJ=({w2P&_MTt_m1C$aDuIEY=pXL zfsJ6eL_}l_8rHE8m>8^qW6>xXhi?W(MjnD%>C=r7sb< zX;034KHzdF^m{xO1#t{c0Ti6f#?J<(S1QlmqFPv4vHL9(Y?oL=)Cn>*;*Nnq#Q&1v z)jQ-uaag&ya6u)+L0kr2zs_a0JGkc!VMSF{doU8S!eN5nw#Gbgl?eXfwsMZq zbpVN0$DXV2jENJuNHaB`ou2w<+;9V0dsqQzp`V+Z@qhn%!dW7|x#}k7<}3g*uMr^< zUcDkfSij)q<_4&rnlFtlkJpy^@uoNF)($yiq^BN_`I)bIH{oiyK5^p29k9jRDKokb z^F)p|eSj8*SY{ZzZ(>5&FDW6>{{1_>xVZSSI~XLuZ6nCNw7N=wICc*sr3-tC(G!ok zF%=!xd5D2=`0Gx5%~;~Y>gUHqXw4s&glTT@rSReD?#O7#J%RP_HR`K%iokwaGOayL zOHF;&-Y%b?pRaO0qo%mWdo!tcKKxP$2nJ13GnVFmE?-7Gd`L-6^-}5V6@DLu3fviy z15^&{RKx<5P(79ny$tv6kX91$`rA*C27XddfuW-%64q$l*-^r`JGgs@sc~wY= z{Q2|e$2~yOfSW*(^mZ`kOOWT^nnk`eVJs}f?0ANGau6Mt?+zmaQI9KzZA1eINOYFFAc(9Knk^u2nfonzxxYEVBxi(;*+uvwq5AW>k5cdB+Qf(we z9LY@M<$nj?#zjZdN&2T%%7xNa*QExT=hR%cy5A^RYe!)f9=<^E&0w!2IV6myoA33d zu3dm(tkTk1^t~JkLz)-BUmyTA1ydBQkjwj67%SV7u9xp_xqjmDbW89W=`oj&ex!--Qb!I^e(@%Ki%W&w$aE#v1&FjkF zIufe>!q3A1;h)sU&=46C;?E!ywrNFJ@0V)!(xjQ-f%SHnj$UVWbrXzq;Q3_pfX(8|M7W=LQ^cz_^bO1_}g z)bwY~O1^}Qg;K!-y)$5 zRz3CI&%fFc$p*RX#Z@sHgN8(bd%lY?e>V@de$J|be+H5#x*$70L;Crx%x04!i^wt! zULYG=MK`O@~Hd`)v`yiQv2v5&HoVt;+p}+tn#KE;&+;03H(Q=Vx zMwa&EhY1VkQ!UaPp4_fSRbVGU(ZfMa_ZzVYnFvK(h&ebS;ucqEG4;zABY)?`2Q!E% zC@ZL>z%Ukzi}2EJGT45>GpK$VB$tj^Mr}|}>irXYM#+lbjY0xQRe6V(tm#aR)yL?*oVQC^TlD{TRy-CWNamy;5joxI-lh5OAxH_?5 z8s01nGfL-Oi~X|r`SW2|%-u}6G*Wzcb#HDJV&V|}GJEyg<-;Q+1y@wulLQ@GiN}@_ z04yeJZ+lX{vHOV8Vtd!NXjuhsG>@0mk{%-2;vj$kJHLy7#uP;ja8m-Alz{z zVcx;<%GC7a2ty*)7!`orV30U+nsXIp3IYX1EBgAZwt2?M)h4gMM$1 zjw0j2xHBH$Kj@4g!-=1&b0LXU3Iz%1SG>%*EU02g1O$75*3%u~`0E~v8d%ua7T}r$ zSB3Z4-o$edujqtrSfHw?t*zC`#CfjchRVMDTyL=BS=*Q57cyeBD4(j@kotxOB!b}5 zrGc|o|0Vf@*0ss}A6nOzMP85UgDQ*A1&smzx=91+@k^{2s#&s7yMRU1?N=v0+G7d> ze%9LBdhP?-5cne7!07ldp^4u!{yO=a!opi6mJB!dL9&0F6YJP6J)Dc4^V@+@#dN_B)f0Llk*4Re1Ps~{_UESpo~sQOx#&NzQ&D! ztd+!-sHXa@g;DONZ*pKR=rFO?SR-eIsY(ASk1-5&tN*SJlDR@soFR^B(!vKbE(pQc zAMFeXvCccbA_PJ7_pM!1)#%mK-WYPc=Oh6i|L13Aed>fvsC@?>ty0F~vQ|Mz{Vi9` zslGXLQ<^i3^d$two!)-9l8{L=j|4_83tU=iIr=tJa#3Uq48i9UO}Z4fHU%9-)*ZB1 zfz>a&r||&n?7g9!caoLmLYDC&o}jP7&4a!QC3RF>FgG5=$#~^UVPuhp#!x~3^E(Mc zpFS~5e4a0-5c(m0uJcJ?Si8EDGa|L2z*b=LT8oO4rIV5ZKLKSJi~={HvM-eU`g1b4 z7Rj+?67CgyHg@!Zdu!2fU{lk79H>0s{pFU5{rUNq2+aqI@L;#{kgr4_YFl3-0{@6M zEu19%7n|$aTQfFc^yh0v5piu*+Gpf1(Sus~g4V4DW|(ZplsqXF&_>~-!;{@^PfP#% zja!d6<^LHhFP{b+6}g&%2W4h&bjxy#RslxiV;V+mZ5FtusX#Lp7FI-Ddld+ZU*5rl z=gPuNHkFnJkLE#rZ8+nn1!3ql`rv#ad9KTgz zkG;9w0iMxFE$O4qoF8DGM?L5z{k#5+0&Q3x&BFydVho7Ny=2nQ<;__HJ&T}hpUPJa zE-3-kFA)`acC?f|I6C^g%%~;`X%74Ucu6UqX-G;+3K$!by_3a}>=AI+ej6{jw|4?Y z%}(Ix9bA7vl&pksm@G*QvFAlbhDo?E>xk~p~EQ`p9k2T zClJ{DXPiF9p_p%pohHx0txZ3g9Y0OZ&lQ1^ER97Ht#< zgSfyaS3ewK6BgF}PQmiI*o@ixB4AWIt#D=R=G5A++A&ea*ah; zm;#DHp~=lZe~87z#Nbj$#J^06Xs!+MDV5#E{=0|~R&lOT4aQRvSPQJ5uOvWCinfT4 z6>2F@3%w8?8XCez)YsQ1`woLA07}OZzNO>k85dP?%QqRBAug>{jNwOjii1;liU>ZNY%H1IJ-(3}7zf#M|$=#I{-vKY!)s*ttCaD}l;o$G@v)zqeObP7VnI4DP*q_v#xPbD>QE5e$L3V{D897JxFOF7+ABP6A^Q0V)K!XD%wpQt9;$!= zf5OI+b9Uy3>qY`#Lc6;Q8qZpe7Sj!C?5WUQ43PHBcNd2YntiXszTo9{XDePA3wb;X ztHLf#l7{xi9Z$-zWYsED1FfCyZKPG{mveO1x;UA=$T~TO7K*TE&(z82QP)9ztFjK( zzXZnRW!uh|C`@llBW8IG{2?1TE|S{iU!=hgvQ;8 z)J_gm9^_OsIjDknRSd4`-}IN<73}ui48Xm;tF#KqQ%0JZp=$_Mh;%;1_ZbXYnSjm- z9O%h7I@%U*qrQfLF5HM%snA6Pa|*%oEVIgO?5Bi33KO}bq(@lTC{E^|sA zIZ5yJmA7Kt^o3IBNROu_W)r+3o+9_XNUMR>Ol!?yF`p8h>76M~TDDcIu0+oGCz-jG zn&5vrb?TId$A6LMWIRK(f9hlY<3>(QSO0^BpGY9y3yb2T=WmwbRicyNg3q=h3n$Lu znt=I+(TwS@-y9=;6Yqvn)2FMj>-}E}d~+ziA0PRvPn3q5wW_)f7l&{zU4OvEt*d8u zo4Qbo;UTsHupc7H<+?M~T$}ceimd|?LU8Z?9DU4-L)=+}Y~g(DHz+je=-S3hWc8^p z4!&N?DT+v~to&)b@!kI>F$u}Ji!4Ixvb6N##HAQgcJ0clH@!8oV2CTyFQdPF`Le<@ z%gR5~AC&iaQE#p6=t=@ze$pzfO;in=C9La1rkhM zim?AJTwLN|_gI1M3969@xALB`ID?pTXGwWW)T`?b?SL8s0|O&sVicU6-^q!;HJj(= z;gP>{hZGv*0#CPW3uh0M4sxYi&S!65sz=1bTFLAxHucA*$5HQv9aTuBfY)mnwpxW z5Lz6eYH;jJyiy9*H$EYu&vSb+XMZU$+c-7S+R)F@9nU<1sDQvX5WvIP`gI3t%r@wyqqEX$?XLY# za=0ikc40*3;sHpBj^GY2Anye*@bcL+T))Gu*MYO3tfS`-n&{pJn&m0Q8i3=IC~gXv z&9C{Y6f0V7VJ|nX@s%uT7~H#aCm8Ctyr7HoJ2yq|jpoROCL|?=0ye-z#9bDD0=1`A zTQCx*he^;J@o3{ac9}l!5Pn)cG_<9Br=f>_QRrOBJx&QuPiH{nR8%CaPDG%a!JvNz zZI>^|0-w5awsZij!M`Wa@f86?Dq7h?xPrt7Ih*bas_lXrSUg1=n>X*d%-aV=L!`9+ zQ`4=G0pNs4c<}-Up>q2+cCgR@7zF2l@e@*TU?RSJ0|8FJprD|)X6&OYp3rmG6G_1( z2qm%S=1e9$3ONM@bV_If$C%>#2i*R>gM)l`AH(}~_3+@m$A}qpaV@#ml0rq*Dt~|? znn3)2rH~6a?|685VIu;>3)FAl?rIOegmV9_0y3WV8NKTNfr&morN0#zDBT@H7Yg!y zp7V5LK~|6vBWBY%Q~7*Vzc!<>D+X)#e=}n)yYltJ;&)yC#0r)`%HrbU3AvLqN{Lhk`e5{72*3I-13Q zW}2hn)^a7|mhjq(Oq(STZ_TXmiHZN^cZ8+^@1up(ZRi$VbBVumrmtyZ!wHi0Gf)xc z&hx@Mpj^TXo^9+O7#K6tmNM)COIpubeSSu2YN{NaY=ak}!4_4-zYEWkB(0edzN5NQ zcQ8;uV1T#sihzLN(}%|{CN2JSl8@Gdd#u!}b9u= z4j6iXBO(r82@!(+hpyhfzPYf&ip*LAiyOgIOAii*{<}T#GUW=?>9W8Xe}bI~eLW#J zMW^rmXU7QQG&Q@rS{SOoLV*t24k-IRL02sRwQp8p*#9R}C~S3bg_6=*LE~=Taak_J zp7rSu6Xvk(u@V0J-yW(!^BV$|5o5ZTi&9`<%Xtl;=ZeZoIjHB62#BE|;=Yz+OihNa zNEBE$ttEIVI4Ul=rTl0O$}Tf&%|icxXCV^yr~1X=&cXnwuHZTugkH)?$<3w?fKCG~ z;1fazGip8qbJtREjM_Lj{PV}!ha*g)p38U8zk?kGE90ddE%Y;BwxR9NurVzw78Vvz z-9=!&18lV(Ezn{P{cKh+VU_?`tiZ}_3Ba&4Cnp%CwCCUm^Igy#l3!RTtgNj3wfdpf zK2*#Vptqoz@cp09nCY8D`T66J=Gp!`_){Mq3#8w0W5a9{b^gJXwX`f-vO>(wlp@a~05o z=q^gj%70Q~J`|V#ZsHCj)q&O&psoL-(Q@bFMxe)doDP==^q(VC6*U#!$(uj Date: Wed, 19 Nov 2025 01:35:53 +0100 Subject: [PATCH 11/16] modified tests --- src/spatialdata_plot/pl/render.py | 25 +---- src/spatialdata_plot/pl/utils.py | 89 +++++++++++++++--- ...an_handle_mixed_numeric_and_color_data.png | Bin 23393 -> 0 bytes ...s_can_handle_non_numeric_radius_values.png | Bin 16115 -> 0 bytes tests/pl/test_render_shapes.py | 13 +-- 5 files changed, 82 insertions(+), 45 deletions(-) delete mode 100644 tests/_images/Shapes_can_handle_mixed_numeric_and_color_data.png delete mode 100644 tests/_images/Shapes_can_handle_non_numeric_radius_values.png diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index e3f31205..b943c701 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -48,7 +48,6 @@ _get_extent_and_range_for_datashader_canvas, _get_linear_colormap, _hex_no_alpha, - _is_coercable_to_float, _map_color_seg, _maybe_set_colors, _mpl_ax_contains_elements, @@ -93,18 +92,7 @@ def _render_shapes( ) sdata_filt[table_name] = table = joined_table - if ( - col_for_color is not None - and table_name is not None - and col_for_color in sdata_filt[table_name].obs.columns - and (color_col := sdata_filt[table_name].obs[col_for_color]).dtype == "O" - and not _is_coercable_to_float(color_col) - ): - logger.warning( - f"Converting copy of '{col_for_color}' column to categorical dtype for categorical plotting. " - f"Consider converting before plotting." - ) - sdata_filt[table_name].obs[col_for_color] = sdata_filt[table_name].obs[col_for_color].astype("category") + shapes = sdata_filt[element] # get color vector (categorical or continuous) color_source_vector, color_vector, _ = _set_color_source_vec( @@ -585,17 +573,6 @@ def _render_points( and (col_for_color in sdata_filt[table_name].obs.columns or col_for_color in sdata_filt[table_name].var_names) ): points = points[coords].compute() - if ( - col_for_color - and col_for_color in sdata_filt[table_name].obs.columns - and (color_col := sdata_filt[table_name].obs[col_for_color]).dtype == "O" - and not _is_coercable_to_float(color_col) - ): - logger.warning( - f"Converting copy of '{col_for_color}' column to categorical dtype for categorical " - f"plotting. Consider converting before plotting." - ) - sdata_filt[table_name].obs[col_for_color] = sdata_filt[table_name].obs[col_for_color].astype("category") else: coords += [col_for_color] points = points[coords].compute() diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 91485b7f..240b87e9 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -45,7 +45,7 @@ from matplotlib_scalebar.scalebar import ScaleBar from numpy.ma.core import MaskedArray from numpy.random import default_rng -from pandas.api.types import CategoricalDtype +from pandas.api.types import CategoricalDtype, is_bool_dtype, is_numeric_dtype, is_string_dtype from pandas.core.arrays.categorical import Categorical from scanpy import settings from scanpy.plotting._tools.scatterplots import _add_categorical_legend @@ -794,6 +794,64 @@ def _get_colors_for_categorical_obs( return palette[:len_cat] # type: ignore[return-value] +def _format_element_name(element_name: list[str] | str | None) -> str: + if isinstance(element_name, str): + return element_name + if isinstance(element_name, list) and len(element_name) > 0: + return ", ".join(element_name) + return "" + + +def _infer_color_data_kind( + series: pd.Series, + value_to_plot: str, + element_name: list[str] | str | None, + table_name: str | None, + warn_on_object_to_categorical: bool = False, +) -> tuple[Literal["numeric", "categorical"], pd.Series | pd.Categorical]: + element_label = _format_element_name(element_name) + + if isinstance(series.dtype, pd.CategoricalDtype): + return "categorical", pd.Categorical(series) + + if is_bool_dtype(series.dtype): + return "numeric", series.astype(float) + + if is_numeric_dtype(series.dtype): + return "numeric", pd.to_numeric(series, errors="coerce") + + if is_string_dtype(series.dtype) or series.dtype == object: + non_na = series[~pd.isna(series)] + if len(non_na) == 0: + return "numeric", pd.to_numeric(series, errors="coerce") + + numeric_like = pd.to_numeric(non_na, errors="coerce") + has_numeric = numeric_like.notna().any() + has_non_numeric = numeric_like.isna().any() + + if has_numeric and has_non_numeric: + invalid_examples = non_na[numeric_like.isna()].astype(str).unique()[:3] + location = f" in table '{table_name}'" if table_name is not None else "" + raise TypeError( + f"Column '{value_to_plot}' for element '{element_label}'{location} contains both numeric and " + f"non-numeric values (e.g. {', '.join(invalid_examples)}). " + "Please ensure that the column stores consistent data." + ) + + if has_numeric: + return "numeric", pd.to_numeric(series, errors="coerce") + + if warn_on_object_to_categorical: + logger.warning( + f"Converting copy of '{value_to_plot}' column to categorical dtype for categorical plotting. " + "Consider converting before plotting." + ) + + return "categorical", pd.Categorical(series) + + return "numeric", pd.to_numeric(series, errors="coerce") + + def _set_color_source_vec( sdata: sd.SpatialData, element: SpatialElement | None, @@ -826,7 +884,7 @@ def _set_color_source_vec( f"Color key '{value_to_plot}' for element '{element_name}' been found in multiple locations: {origins}." ) - if len(origins) == 1: + if len(origins) == 1 and value_to_plot is not None: color_source_vector = get_values( value_key=value_to_plot, sdata=sdata, @@ -835,9 +893,20 @@ def _set_color_source_vec( table_layer=table_layer, )[value_to_plot] - # numerical case, return early - # TODO temporary split until refactor is complete - if color_source_vector is not None and not isinstance(color_source_vector.dtype, pd.CategoricalDtype): + color_series = ( + color_source_vector if isinstance(color_source_vector, pd.Series) else pd.Series(color_source_vector) + ) + + kind, processed = _infer_color_data_kind( + series=color_series, + value_to_plot=value_to_plot, + element_name=element_name, + table_name=table_name, + warn_on_object_to_categorical=table_name is not None, + ) + + if kind == "numeric": + numeric_vector = processed if ( not isinstance(element, GeoDataFrame) and isinstance(palette, list) @@ -849,9 +918,10 @@ def _set_color_source_vec( "Ignoring categorical palette which is given for a continuous variable. " "Consider using `cmap` to pass a ColorMap." ) - return None, color_source_vector, False + return None, numeric_vector, False - color_source_vector = pd.Categorical(color_source_vector) # convert, e.g., `pd.Series` + assert isinstance(processed, pd.Categorical) + color_source_vector = processed # convert, e.g., `pd.Series` color_mapping = _get_categorical_color_mapping( adata=sdata.get(table_name, None), @@ -2286,11 +2356,6 @@ def _get_wanted_render_elements( raise ValueError(f"Unknown element type {element_type}") -def _is_coercable_to_float(series: pd.Series) -> bool: - numeric_series = pd.to_numeric(series, errors="coerce") - return not numeric_series.isnull().any() - - def _ax_show_and_transform( array: MaskedArray[tuple[int, ...], Any] | npt.NDArray[Any], trans_data: CompositeGenericTransform, diff --git a/tests/_images/Shapes_can_handle_mixed_numeric_and_color_data.png b/tests/_images/Shapes_can_handle_mixed_numeric_and_color_data.png deleted file mode 100644 index 8ab0b742d651b040ef5e3ac16d8ff86e4671b1c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23393 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 diff --git a/tests/_images/Shapes_can_handle_non_numeric_radius_values.png b/tests/_images/Shapes_can_handle_non_numeric_radius_values.png deleted file mode 100644 index 8e06b52fdb55a76f0b579616a94b8d0b99466a6d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16115 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 Date: Wed, 19 Nov 2025 01:41:37 +0100 Subject: [PATCH 12/16] img from runner --- ..._annotate_points_with_table_and_groups.png | Bin 17173 -> 19164 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/_images/Points_can_annotate_points_with_table_and_groups.png b/tests/_images/Points_can_annotate_points_with_table_and_groups.png index 4941bfd28b3a6d625909de01856e2d29479283a4..b03781a4776d4462064b7beff5d56c14ed880ab2 100644 GIT binary patch literal 19164 zcmbTebyQSe_%}L)bPOORU4k?s-9rjUiL|63-O?#FG$^4+cSuR6bVv!(NT+mn+&$ml zyWaP%yYBtxvc!Rz!_1jIdq4YA&k0vkk;lb)f(3ySGV2) zZiHQBbzEONSh%{IIGaOWnYcRII=I?enbNwMJG)pp*z5Qw6}OBpFmkJP<+Pam9%2lT@!d;V|4 z4gB#+`$2u9v~;LK^V+|y`Kn{AN40!0BS`Y9%ObM9N{&ldWt9lB2;IBXWf)`n=oA>8 zU&@@PF4Dv&4)qYvq=_wz^mYb)y&K&1R^~sr8+^ZUx6F;x832W0cuE=6qR<9Iv%j$f zf?ur&6=}PSqo_nk_GW9mm-qh8&+}M~V_eSLkVY$VCne3QVBA3qWdjat0;8eRV(v$3&FPfydbvSJk#6%Bd9 zgDr`v=a8Udl;`{Nk?xDWqz*st_~l|K!)>O@OfC1ah3ybjCk{!A4*9v+@fl?5)fxDQ;&Wh2w*r(fUTU~pw64`gX$=-ZbU zR)l~5{#ASZI%w#d0E4I~B`K#tKzVt&UTg+bi#vELkn?RL_k5GPw%*0O`yvnI%ZT_* zlnGfVIj;rI>gsBahwp5)Rg@&HF%~v9@y`C=hR!cvzHmD^IYoT1x3`ZfDth+Xz~H-% z`~F`cG)R?6XQbBMJp4Qx73ON_4J_XFxR=PRN*BCL>l z?~Afu*Bu=&j1XAf2U%HJ*1i3OmPqh4$PhvzBBzIY-`le(BSo=9W~OJbQ=~IV_%QGD z=P*Y{?)&>2hrBZpw_T5IH}H82`dn6MIJ2;`cf~WvLrF;T$IlO!J6WDRQ)hp;SoH7f z@0UvEd<%h0OiU~}3PvD(fCxITVc0J;&y07{%j|%u>}9gh>?!E9f+`nF9o^L>^If+J z&B*tR*h6%Ih|BENH-6lwPoHl6DZqEzoetcatIJW%5Qk!*ei8mqtf8N!25w2gd;)6>(*gq#^6neRu^mz*0sZuaV&4ws~EFIQs`_ot?&j{9x}SA-H`jAEmp z<8_TlJVb#H&!jJoZo1i%m*4&`##d!DC8YtmIxuzGM|0$tmzDx}%zBJ>rz;(uojWSc zdK2uI9|cGSq0#a4lYC}Y?GZU>MsaX(V0`+N)r%4d32D08im1$>;p3MtWIkZ7Ualt_ zE$!`Hy|J8^Wa1{)?+?W5mz63*IDZyx?8A0LU;fiTY9eCj~*{9V4Cfau=>ITm~ zDjPZ|B%%oFVCY2bt`n1zN;48xQiW>epdM3#$?3E+$x+++Tlfv1hOL+jQT|iWdi}b# zhHvXz>o&1&F(lUBBh}R}P|?te?H<;;$0cacIY~`B9@1Vn^9zsTX5eEQvsEW=?CuV_ zVd%vYVpx5Ou;yvx6bcR_TsI9dqN)#_aBypj!_(~O{C#@l-uClPN2NduM5+uHT$5$k z)S80|OL=GKoTPYMSY@e-#so!Kw{wVjnE6&kg*FO(BE$R&rixDh1Mf9te3#J2TM%2< zoh@U=O|e=4wI}-Fa*%3k>TebStQz{ZgB}vl?ah05Qb`GEJHFFC>eO%FqBvX&k7Ivz zC3s@RWh?%H$l|G-V-#k_QB|1HPEW}qA?y^J35pE4T=$3>JN`JIoRK04X)`d6 zdKqsRvCd61VP*7TG?Yl_lJ$rcvb5^A$DRMJN%7mOXAL^F4_IxGKR1_4Y1GtDVm?97 zN152iSkOX+`kZ)v;$T1Yx1m(x;CDUusl9p5xxqFPo9@?yeB%DL2SwfP+sMVcB?H45 zqH_iKJt!Cuh(lN-8wbUTU}Z86UR7Gd&L^OB?3l1HxLYfq5%4f zjTT3)X43mp{AyQr`yvY23ltU*po=v^JWlw+5LJps!E+%p-C z1B)vm_AtQH`+8J)cV}-KCbOL-<;r(`n=x>?sw}QY=2Ce*6tg9@UW(~SCrV8!BC$2> zMz)4oh@SoQn}+kzK)8Wsq&bTxZTpsyN{-&`+D={343myOJ4M|<2_G2~UFhn&NikF| zVT?u3o1>Zi0o$+~QkKBNmt(~1%%9hB9BV3$SnE#iReIqxswSfg%BMOhQDEl13RZx( z7ZNBPOan70i6IGwhD{UU}nnC5&`lI6Pvo zpL+z)b;iMQ^{}WxI<`%atZqx0rae93YRj7=Ci~kynrWj>pkX9wgZ)@@oFz%WwabOp z7lqPMDAUL#Se80VNk`9AGLymL%t5|CD9&p#8cWXmz0zSlrB2<8o{WRAA5?@zVP3Q{ z-6_&jN2{Y0OB7C(r!|tNNB-A?!NG;t8NJ^mW~dOek_HCBY~HK zlYFaFYN*(V=qmBx`ip%thIQC}@bGMM{W?L1DW<$L zPz;NU;xN=R|NHaFrqZ$F>HU2OL^tT;kaFjz;b2P$s9`dZ(L4J{G~O<^-#kg*irGCj zH_tz_FL_b2u%#wY3u8o!A_VWJ?3+8A>xq3{wY{={`{ndEvxjw9u-9y+17w1YAw?cD zxG+(ZwRhnZxq6L)t}%_O-k{NX)}YaACewx?5d~xPF&IEQ>n;sfU1tRz%(!GbCCXG1 zm6yR#F{zAq+5C%)i)txS`*KGpj7A(5JoGRH7f_+b^W|7y>t3jRZ;Y`_{&*ZV?X~e# z>`Ep5r45mSmKMp=S1G}nnY3gS6rJ<)Eu(HX7YFicYEV>UBvDb(3hRktxw-D%-pg5=0W!>p{HsiccyLKI%Ul=AcxGBqjrjvd>y4+}ciU^HOterOT zX^T0nCHkT#ngSZxeRy{EM(<#A6CDDuy^M?u>htH%A(^9Y^zj%oGc%S`WoZ^4pUMn$ zht_v=<};levPv?$)A}!NI|VS1d zvOK1L@!~P$cd5RBj-HKTfAHxJ8i0OP|&rGiI63UUgMriA9yBUwhY=g8DYU zwZx6A(wD{JZ{VMYwS-8%r6Z)ut4XdXE-9&WSQLjyy1DTqe)hd_gGg=;r!8-6gkGI& zA_(7VZ*1?@XpYnMsnvJ=Iy^S5?`n)_L_|b`X(FT85IVBSOC{`T3~EEV2njQDL9!U-m@=nf^JT|e-@E4< zi&3zrSHIrMJ^frn$J#lOaFWHg*wcf;7opT(?E(;JDGt(50{s*@hvaRbn0WW}SH z!KmC%$3rq14%V3?cU7YWTTJ8dylJp!46i3vQek<1d{uD#xE}?A*rPZ7>vW^w(7UV} zGuqj*YQ-q~0lX|ZjT<$sgjf##C~OlU)-kn2h2kIkNQnu|xw>ljx#8kB%x~jGA+%T4 zJXpVIevlDZ4c71Pa~EkNO(!@6Pl*Uskr9mMR_m{07$pT~j^G=gW!a1@1T;gp17E0{ z5i#(&{S-4vZfGMb$~f%;JFiLCy0PL2zhX@){GL#0o$P$?W#3v&XLAXjVOWkicJpX| z$a>!?`k7=3OPQ+vcsc5_^*pj+t%a(s>a>~puio4C`%BkMn!!gs;8nIl20voS0;qnV zfuuCL#eB^=DdQKDbm{JeI!QASnic|ioqcA8hYC9ldCy?9C0wrqHfJ$8vAET;;ev$UV?k%HR9 zQ=?50wY_rQaZ^}GvN}Ygnzi3k0DyYiMB^i`tI^Zi>j;2o*jnZ~iF)K>9TM#O1z^gV z43Px?%EwrFO#OvJ=FIT{>FOx(&s&{7TOnN-`TAGmMZFD46PHH0dWJfFuqBfXuei!! zM9*w4I~j!&cd5HR#uk!fjbPH7s8HfJ4-FpZCU&+Ib`OmAr5tZ2Ke*O-ZMYFnn5GVL z24>_u`fID#`rDh(ZItkv5o25MQ6y=kEwQ8@@qMstLE?Z-Pp? zvRAWHm)xpR_}=$c*#!@t>i>RBZTpu?@C9m4D#SVGpo-&djO&jgBg87OHBr#WShuEh zbb38{iBuSsHJ;sHukwD0gR_lGPI4=u zG{^74u=%qcveOY#Q|VhK(wcvM++J-*O6{=uYTa&H4p50>jfGpDM+HBWkRV$5J%W59 zdK`5I_}I=LOND4e9-43$CpSE}21{lZgPO?{Y1uz1t_wP?QrmJUFKrfjvE}j4x$)B+ zp_hFeSvBoGJl|S*xrl}vFY{CzoG(jxl5uyS%@=__RgQzHoM;ZO=B1NoGQKQ<6}dm; zKK{;o8S#lqY;MN%PG|nh>pQ?Ue452xRo-sI7C2hit5!cJsvw()j2~DR{HBe@85X3n z4@g{2)srThZNF%llR&7J8nQ`$wivnMSvua^XL5co`3!88b)&Ygb{jq9t-c~T?&@KN zJ`$=V`i%|130%(;)stxgu*bP1L2i!J4Oc5{go_8CvgHLTrN2sLodp zTuMCd995Ao*ikchkKERfkfIpX%XF-EN#U50=<0@p&66`SyXAv(RkG`yy4&S@Odmc$=4D^jYZgDr$CX&t~<{sF8(g! zsQV$URV3~8$HH>FoSPqvx35N;;w)rI+NG5WE1t z`*X6aRn>W;CjIbMBO2L@yfDCRlJ^{Oo&c`n`7-SJ!ODE}d*Tm6t>JrcsXhIwC$D{Z zZ?26kjA&{*ElEs^C{eU%f=<<`uCAImbKWQs;b6GF6MEj6nTL|CA(UzD z(WiWT#5_Da7&tgGFc|Vvl~njw-uI?JP*=Q(h>h(9;v%=z7$f+irKN=cxZBg?Vj-1Q?87lEON>Z1zL5-J25en|}(#HojdZUqUY|E6VHZ z$!cqV{m}vtf$U^sNE$$TAP4jO-vPfMB;eWWd}Ht^&_@w6w+uZ2B_$=d{a?YZ-d>c# zG-VbR77R{K&dbdV|8zF$_wV2TZOltsbZ+E&)vlU^f;vLGTnP}Xd7`2fC}Ebzvj;fJNX>qXp&mav97P?%#NV`K`*IoJ-G(4XOz(B z-Tz72ej9&PkT3OHK=?$$|8z?oz%36?&*lHa4y;%I7dt?OynFW!BAK%X#s$D%K4^p2 z*TV$Wtdg2@)$o#7Iajyt3kUZO{j7c4Fc`dNO9r{Wudi);qJ+n!164;yC-eJv9Eh>G zdEWT&-@lY{a&jyj9FHIgiHYrfeHg&(P}~cK(Lq>d|NX!<6Jh%VZwbNlqy7`V4#%Ji zh3z=3wg5#OsQOqyxn+ObsL60ZPD4{p#)}jHG}zoctIae%3&_1s=gUeT-W@FaAEYs} zFBu6-#YjWp!(k?Wwox^7b;E(RBF#>+3N&xanIB96z5n6;#PjE0aV94xBiz94n>TM1 zTjCn2mESgnuB@y;Nl6QI+}zwI8w2&F-}wKo&DM)z3rRe5u*2jH)Oujq;jn8i3bc%w zH0&S@mmz!p4g-k1@n62cCrfE0zWfiyFDKh6{Np&7vwzStdjy8p%d7YVdOEouSx3-7 zPwuNtB{f^>JZWnAa3vLWdGy#&_Y2%?DpEH*2r8YQh$-{*^YW0uVi#B#6#$Q4Ub0BY z+JNP>wPk5LSBrag09*KBjtzzmCM}Hw0e(_YULI4e{d}=yCpQkbjR3|&(8#9EM!;zw zfK19W&PWYR6ZKwoSkhBzl0w+)A{Gbrxt9W+kk=3OnMY(8Jg#~(2M>px_(#AIFdNO5 z1yo4z!*;?7pM-=2_#BPzZ?7=$@p~_iR(ZTmEmOsPp8$r@36S~Y@b<1QRKUc$nKOKl zMMOkg_y3Yp2-wSbc?q{Z+G1$q(cHcPc{(#gJL-oGc&{U=Y&~VS<@}v z0^(O|M79gfSA`$NhSu$RPm^tygG|9z8ldIqBS=Zz3Qh%#Jr36}XQU zJ=@tSykZVCn-wwLyy`XYmvf@KL zcG9Z3tQ0p&tawIE=y(B`%jsWu+b+J&y-21?)NjnJXVI>eMV7C%P zjzaBn6N9#bq33tPoT!r=(MaQ3qSM^9yaNOTcG6`L1QVgNEL*Cw=okG|=1 zQxInOnfnAk?|F5NcB#so!7e-MrGp_&&0fY90$&kZ)&1gGexObs_djd;tdRDH$c+S< zBxekl!B0WTTE&i>T7cp;qiSsC$B)6pr?2I8Mi1SLLy-VXF@5%A-f7pXlHL;{Y50=Q zPdMo$fdfOG2JxiP^LV?Pg{=cT*V&oba^`QH65$~VDJtxVtEPTmCjrPdq-272LdGcOd zpHe0dc&{0~zu$^+{Q}Zv_>8(vH8T;|&Umk{+hhwxT-ucH?<5qqhpg_e(s9xh17ygV zpfFcuHeMvHpEE}dbBmnia?vt)T>4e-1+=Pug=yrOe@#s{n=}XbcGS$FT(!G$=K=z; zpX7)WW%x89QoDfZ(o5=H&XOe3^v7K3Edp?gC8oU1R-lH&X~z>&3hNomWz>G}h8a@-_v&CV56*aE5`Lwuy1AimOnaNN_oT_Jr0~<7NNuQ_ zYsc@Hl+p#|@gSh~39kW25n3h^*q9aM?k5%`Msp?^@HsC#+JLk7qyKZqOKXC9N|sq? z!8VU#D!CK_09ikW;_AZpP&13w}`#4qY9d8=2Qor2X^I4F9>E*jNWW{5|mp z4KYYV`FZ&;CbR(Ho>>9zb*D|!Inne6VvTP8kUAc{UN~Nm7PQ@&)QU^pb}R7uD*T=` z{;@>Ne(q#F+$0+vGhF3H>?|LA@@-%LKBufV=rfhso)~=tuiJi=PP}dwNsyMf39IB4 zP}k$nd%mkz4lH6a5jXZZlV5)yKJgbp;~F$ui(0BLIgI=2 z!ewMZp|@qCo7iA@)f`Cb(jwIsT(l%zc1;05wU75bK)P?x35fNRh*iSALF{h^H`x*} zQQK*}(M)cfAl`<&%M)Ai;kBd{-OsJSKqUdr+H}1WgCHdy_y^44L@!35G6Mg;{d{kZ zc<1_jFVgBy{sb4w3A9$91E4q`GVp)V4B_(}ObwVh*yp=Q*{2Y=r`BOGEWCCpjLBBRzjeCI1&_p6xa zI{8l@S7~S^0V$X_oRj&}rz#k!8EVcJXWJ9?o+nR%pI7O1W({crRqO~@j4ZQ^%*>sW zlO!)+z9bX(5d_kgN~&OB|7T{lS=CfQf`KHC&ZYJsr`t;><#bVUpub%%1>ujoH77`( zDmdj?fB4gMVRI@R2nk3o{VIh|IG2L0#^jZ$W8Xiu7<(3FM_O?=stIx1cq%B)_QWUM z%|O9~3@yiBD7yKRb>A8e<8$(W*-tlSenQdytMqNlj+(Vq)zzpOBl3|X96%+7KoHVY zS2$68V&Wq%E-p$TXH=Js!FJ%@;y?s_uU}YOTSKX-@gO>_K0*sVC$E$<#Hk>^Ywh$6 ze)>@sigAbH49!p)1z$tj!= zzX><#OI+M%XJ^MHBm@^1v&zWHb%2-$9UYy5q9V-M`8jwfh37gyYmw6{zadzm@a|G?hD}rhU~IAzgpfXDZD$DGy2@O z=IC&?7AAP*BmVCB`XCW8amd=*J4PlZI$_}_l6!fj!NI5iIi) zppeKDY2j))8GU`*Z_~t7Z)*xt9GC^@48FfI>3T!G$-UydWNjCrVy_xA0n7m)9(G1k z2sj=rh$%1<%E-&-6c%D2oNdf^bG3E=WdJQP%=89m`^&Ks_ie>*N*wR5KiE!JAeCsB z_w3G8O}JFV{$^r_8IJ8NW^{f1JG(O%IGi*s*3ipa>+u6@if@z>Z{4w7(63vR;a;5v z#QAM{l1{NtW~4&Au?GcSHj8rx);wxYX3BzCHy2&0vg# zS$1=?#oYi}aP#xa0iQ#!*5_{yxu^%X-{mqoyHP7;mBrA(^?-v;+cT`Up2jrldHnAGJ97Vgt`ha);DS&BH**GG5sx5yW?JD^`Bn53-OpX z_K5quVxI%&FHK2}!Vm$)3(T*dzXKY|fS=Q+*U*`rFt@7VZ3**lPVS$_dW}8WCi~!gQpiG$$kS)D3Un&7WR!rp_E*B&=xY{O)-*EyuK9; z#Udf>ls0ntUkN4{2Hh)Ia)m-wyyE(o1_oU6N5t^+%HHqydw_WJeQk(wh+JyL5Nw?0i9_@g7Mm(jQ@psXS+UT$SM0(p2 zB_utH0~SxmXPB?U2;nhAO6z@GC7}ij9>i*|u9x?W52=Yi@vc)WPdPRE+U#`SKF85_ zNA8?VYwNz<%osp|_>oVMf0WNVwb|VIz4w3(dyKRGjMj;?NgSQbLX#T-z#W znQJ@gt+RQJV!xN#u<0u@62fKZDTQ4z804bVSU-zekF$V*T3PG=3<*d`c$|=&%)rmD zH5xjwP7m@x==jvZI^XAhIvP3sP}B1(DWOm-=_({q(v48?KgD&F+9-1s{vL?7})I;m6VA0qN1XVEiCfKRam}L{r+xKP*QKeNrLbFXzpofb_FcB z`BQIDv;N2eOvQ;fa`YcMb*xhV>LVbT=T}p1+4gHN$`DmIqB~z6nrLckua4y@=`?%r zAe>iFK8x>sL86ZU@Nu28!a_+sy_DI-3-e$srY*tWT2}Kdx9plETT@&TAii|A*%9|b zlxEaTa%#C$ogp4r`fb@EJZW5F`4gDA!1#m&fQ=VO9(Y@`HEj3SJ6zp)#RyaG{C`Zj zh$F%*H4ym<1v_Y-p~74I{Ia*wAo&H4jC{=wC?^BAT9cBq!5~-X)(2+d0c11IG4UPj z)^uNQ6(yxV*DfXsTRvp`X;*uMj~}Drg#~^8&JOgBz>!S`Uf$P5uR3{R8-G&R^lqLk z|NB=I*vFRxc4GhLDdMbjevq3A%i+yA<@5wGgUI3AP$3Y5BMS{IC}37rR)$hhMcd8Q z2AWXgkJ-)qcm&IWf-MAM?ai5#lr-J=p(Go(lN;Q!d0p(|if5D`t*R9O6J+{%78{T` z$lZpx*{JD)MWeM0JGSTQxV+AHG5!7hK_qhN_Uc5Z()1BB3JQF9#D#7mWF z!OFkft6Plw{22w}=3%`GgMHeaY5Uzi+NNY#?3s_%DJy(ti5Yz7COy z-VaW1J+}>$J$>veM#sgts`3b-+rB^fR=V`><6O_mOt1FT*7X!bIni5gcO_o{{4Qgq zE92q}VuK zhGk)K8>pr)6pJF<^#~QC}v%%|cXn{=4gb%|3!5JNT8}3?N z&1a^}acFDKLK}OHQBfanwl)s}0LmP?r=Bo<=3#JlZn#Zt`uOmld@A%qmMW=|sD6#i zc?){P`ZQFVB*$}i;y`5u<_C8A+ac!!aj4laWbwW2*==a4NZ`rUIwYVyFh)n@dEw0Y zTjulL*9!fdNNOU-lC3=LN`n5}IwSVqhI!E9;!bhWML$6nmnX z)=#&st>Ojd`||nHt|Z>rpPCBhIIS;RrGn1U5cXpKl!GfI?DdTU@NvK_9k;!qo{+tI zP8B;KW%P4{qw2_lnbdKc!$qhqE~fMwICrLhitz~g-A}=Ex$|>w=w3XLk5HymG%9Cm ze_7M*nc>|zxg#e-pu0O00iQ|%A_GozY_uDJ8z<3$XwwO!6@iG7lsjL-F>Ja- ziS+0|q(lAfSM2a3hx3c#KRK`1fc8WiFXp#6id&2i?UVw!B#_>K|4D`!JntKM;&q6RZli2ubLM28Uq*zOOrecXvKXn-96X>9kiQ zj{K$s55v7i7i~qojQISh$IZseAybcCc07VX0mSMOb_sDF#2=Bnvue&ZV7xl=Uu`n( zy_q+GjUbc10G3QP8t|=NXbi76)d^?mv4_a8`u`gU3@d*u{dWl|FZ!9M&UuR8u%H4$ z-XR%>+YAK!ekU=jbiJkWA|21ClYjjrz=BNC*)BJEjo$X?Smdc!Bq1E_B}f-sT^Fvn zOvwB<0ZK%5tj8*<0=HwE-8=ClWHG!s4@OLgEH+sMWiFXT2s#k)Qu!*Sbgok# z17%E0Sd%HGLi*-r(`(J5Xj|I8Zs5)PLx$Z3wS$*e!c&psH?|?QJnJVco^hKnOqrmC z{bHZ6sJ1VW+IsrpS$rnaN<<b_hYe9+>R=GGS5>u9;w1PM@JS>b9H4er|Dp--c-1ELPZFC2!Yt}dXapLD*y~i z0VP2Y!&BT9Gg4Ng=w3ow3S-o09L1y^$oo8JvDk6E0l&w|!dB@2>2uM^Y=Wiv$Vs1z z)t;22B*}Qp;r$FQ99Vxqpu^rCF~fzpvu>I2!QlH@s2v|eKc6)cVoh#Ofdme=hu~jYbc634aNknAio5{_MjckzJuc)X%Xj44? z_dY&%XH#GJY?3sZ(nQ=>0LubjVp{b~Z* zI)D?oHBmwe@<}}2=eCH5ARb=LpE$r~NI{6G^LQOFJ9TySdapB9@w?O4U0q$4Yl1*h zCEg~g&7E_@R1Y(XVe=QVxm=HaK~s7VsGKJJpC5^jukC5K*y>9r?8<`rvDYOcmQsk2 zFdP-A6#&sa`}*}OJUaT#tlM~@x`L{zyrE&b7yeSbXVC5A2mWQJO1F1$atqp2&-XQr z|Azj#G(HjfTaqo(2Y0g)q;j~!>`*uv+otXVW)y-Z8nk#3)5k0PtJ&VRURz&ppPJIP zY6%KL`CeKY|Me>-q_3|J0!ei9^Q-y!o0pe&c_UwR;=3>?34sJ@Z`IV)QSd2VJ2vQL zG*Kjes3Sy~qLhF8ep}6gfOGf&7s;r^KOtqUb7-^TLW2%s+{T7?>88m&K)@H9eZ!}7 zIb!}lWtZdj)K+i3hCw9T{gz-{!May`1Mk$kha|VC`uw#KXz>BU;%e21co2|1fWeh$ z$i>En0@2acEwW{MjHRxpcRlU~GqIbPXmm?4{IP=H6FvXOoNnjLVp2Mdya zZzpE`d9jFr;&J|#wd*H&$kzI1o2e#OImn4mAHRcBCyu@Z)7rm4e(QF`+?Nyf`W^dj z_+{UnrLpP zoP0_E`uA>c4t}z*v2}v#2AcPfeNIQ^0Z$9jXaWs@RKWJ^TJ(R22K`Q^Q)O=vEk>cZ z6g?ViGH>}ae!XVI&#XN@ig~a{fvnV~_m16ZJD$#pvCn=BSAA&g(ntv2LNDD7e9<#M zPleN2_#Gb;v;Amwg!2tIa9pFP#q_AM2slg~imr!d`-^L@k0h)fDekxf?b8UyTb99z zAUoEz5gn!=U77aw_we!&FK&C8))s}@&2DJG``q!`kjmrdbZgyP%J{)?!bJRO-GNcF z6t`v~O~(B;u?`ID&JEKNe8?al52-uP`XOxea|Y?aUKAUxW}RcQ$8FqoD|m|-=6$S5~V3<65+o8dLj!pDkC_xPZwA;C?m<<29!tO*Kh6i+hP6KdS$2?)DT05a3aEhalCE#@(vXs;`#=duRPptB*jb)wu33|PsCXmsiy{2y@FEgQ08)j}*;5c1H4$MiT{f|i>kn!M?Gz(Y$Cf0bLMVt< zuVU+5cKOp|&c%lEANz_@OxW>-sAhO;G7zg#{$ z3P}naYt$NuJ^DU8v-(N&KI0qIm>Z&XqW&r|BwX{^L4QK z(7_N)2&ewls2^U_uUi2nGaC+Y<6bX$cwsciCB3U!1m=SApHH#D4-5+RD!;%@LIFn26d>8fn_0)}u)&j|L zrV5Jh#LixFNr=dPy%LrdRy(GhjV6SrU!5P~au2SqM}!fkj<;FUI>m3 z|D5=&7HD?V{P~VG_JIcrSYzSoL_D^xm`Wo$UyBzaY)S)Ro*)X{$6Bnxeqot&jf3Xd z2*fyrbn+wt=VqK*tRQBrbo1i zM?F(X2W=(INVRiSU3U3Hzb=SR!?l)=>OrZd1hHMmYN)Zw1E@y1M1|<45I>>xpj@_Y zh_zB(xk7Wq7rfGC`dpz3%%WA0*X}A>j#!sv@5yFE9Ll-Cs;%o@BUzK$lz*G_De{^A zu-Qnr$oFm#F7(vjqPwgUMa zgpDdm8vslYtx~5!xmGF6K&w+)B_&J(^xKw%5c(!ou<3@q*%W zj?Lz3tA-;@xqrq-#rVgigO}4HOgQIy8hG7tZzA(R=O=5itcG&9JqjsBti52x_m3#0Lq1l`}m>0;PGYlcVyxBGC`rhFOr$RH7k24Od-hXw`)f$o{g#jaBU`TX=1=m7Q`3Or(DXsS1TN2*=h89)$*LQcK-kDi-f<5uLn;B}$Tsmx6a zE05e=Y6P&=Ur;l6Dzhf$>?DUdK8&)birhDaFz{xxghL^EUYI+cyK{Aj#$$x%2r`u1 zyu6+1V%`u4Xx$0Jqw0VYFtja;mc4lKfBcUpv2(vU;GTbT)*w1pa^Bs}-O}M2b03}e zzFqNv@DP=R1Oyb!QIln)wa40^&{Mgs{g?Zx`6KM_{bhfJp{80m5QqFZDhTgY)YbI{ zV-l(9=@ol$FfgFBwzewnd5Thl-uy_=(*kJ&PA@G<yD%3P!($M4dr1gR+-9dMs z%D}^*1uoVSN|PK{UnWa90RFYh0{9oSo^MSbIdS9xw;1)&qb$(fJ`Lh`Juy@zlNYhE zvGP7X&87|6*~nH_R@T0%8au1d*N~d2sXu@IyfFk!G~4caV>-q(P7aT;E7Awl6H zJw%Ji^QaTzLrpqW_KwBS?8XP`27s=WGDyps*x5ycO9ddB*`Zh=EL>dLR@z!xq5O8% z2ipW#A&|_mvL8PP|I05%8_|bHMoJqPq?&@(Epjjohg@ja@^bU?D!n+nG=vr&_SiW$ zwA&W@2_>TOkTVJJwxFUR^$Mlz?oRhJ*U>iCnJkKLu3R--4%}&$k zOzN+)-D4`#sPw*7nCa&~?ZyARrH_ zK@x{P8UW|H`S~A5HgiBg7C4*;JF?mHL=B7_a5-G}W{LeD?&?g9o;`bpNHWej{EP+{ z4p=??_viBTk7}i9Hx>{A&Kg$0TMjHO-K(M?tSKoj{`mFl4EQnn_a^ARH^7Nk`04P; z!zzm8`gnlrYd`>`=kMRtE-~i9_^Yf!>YWR3TCu9DP z9~$QV|1bSbFFvs3eiafDs-!|L3PXfeK~|7Xb!B%qO25v&k_yc;7;z`?Qh)(J8v-(2 z&(0jmpYGf3nI%FAcmx@p+Q*Xabzk-3+2v{ zkJYLc`mp`j%+_`=ikvs}rJ|x~61#2(896zR&E$9W*(pFE$*E&|LBcly@gTU$#1NQp z8$icO5od2Xn$R}l0oNtLfgqkb;tUK{R7l7pMC0ZLh&v5}P($uN( z^6+=$g7|zsLQ4Gaw`;Y<_FhpXU1u?NR#7Mv`3!Mh38bYlTe(xA;cgXzI55b#9!^g4 z!48ueQ%!%|GhAOGI%Kuo#WzC6^s!ORtTi?zc)=R2Biy}BQ0nes7f7^F)-DwnSDQp*fVv_hjfTxGL>uMSF4C`sNp>2_o8Op8 zgZh{9wU>bq6du0K!#eK9xY0`!O+VjgG(6Kl%hZ>L>W{<2a?^BSJ8fLZO6y{G`r*)T zgc6vAS#P6Gw>-GmP2la2@g3nC*tq=GB3EGK37PHObphsJfxu~c@|)HD138K;t4hJ^ zD}+qs6*M~tt_s^%`}NRxVT6>?YBHJbK^!DeV5^lr1fj1NIRbgQ4S6Ua|5Fevtp}rt zu`;Pt(nlnOnrX04M1p1)FSbwD>klIjq$1JSldT<9J0CovFd7p@8+-E921R%G9%KEAtS_mW5aUt7Y% zOW><$CU|6_li=5%n{qjiE`t3gNT<^2^YH@zLd?~o#MW7QF_*aUsO4~76vR4a6*i1zKEWV~b zMmj&S-s8i8=KXwH=21GFB9f8!b>3^O7t*OzD$acDY^t|W2X+(afdUT=y)3J@=0sr? z$7LC3P){S%mU90kHO(fefVd@l?xL?r?bNe*u0u BhME8X literal 17173 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 myH*=qNtg> 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 Date: Wed, 19 Nov 2025 01:54:22 +0100 Subject: [PATCH 13/16] hid logging capture so it doesn' t import at runtime --- src/spatialdata_plot/_logging.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/spatialdata_plot/_logging.py b/src/spatialdata_plot/_logging.py index b7438972..364cba27 100644 --- a/src/spatialdata_plot/_logging.py +++ b/src/spatialdata_plot/_logging.py @@ -4,8 +4,10 @@ import re from collections.abc import Iterator from contextlib import contextmanager +from typing import TYPE_CHECKING -from _pytest.logging import LogCaptureFixture +if TYPE_CHECKING: # pragma: no cover + from _pytest.logging import LogCaptureFixture def _setup_logger() -> "logging.Logger": @@ -30,7 +32,7 @@ def _setup_logger() -> "logging.Logger": @contextmanager def logger_warns( - caplog: LogCaptureFixture, + caplog: "LogCaptureFixture", logger: logging.Logger, match: str | None = None, level: int = logging.WARNING, From 47192d8d24bc479eacbb1094ee336a659490c12d Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Wed, 19 Nov 2025 02:09:22 +0100 Subject: [PATCH 14/16] added shortcut failure if no table annotates the element --- src/spatialdata_plot/pl/utils.py | 7 +++++++ tests/pl/test_render_shapes.py | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 240b87e9..81e3a938 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -2223,6 +2223,13 @@ def _validate_col_for_column_table( ) else: tables = get_element_annotators(sdata, element_name) + if len(tables) == 0: + raise KeyError( + f"Element '{element_name}' has no annotating tables. " + f"Cannot use column '{col_for_color}' for coloring. " + "Please ensure the element is annotated by at least one table." + ) + # Now check which tables contain the column for annotates in tables.copy(): if col_for_color not in sdata[annotates].obs.columns and col_for_color not in sdata[annotates].var_names: tables.remove(annotates) diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index 2e9b7810..f89d0dfe 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -757,6 +757,28 @@ def test_raises_when_table_does_not_annotate_element(self, sdata_blobs: SpatialD table_name="other_table", ).pl.show() + def test_raises_when_element_has_no_annotating_tables(self, sdata_blobs: SpatialData): + """Test that rendering an element with no annotating tables raises a clear error.""" + # Work on an independent copy since we mutate tables + sdata_blobs_local = deepcopy(sdata_blobs) + + # Change the region to something else so it no longer annotates "blobs_circles" + table = sdata_blobs_local["table"].copy() + table.obs["region"] = pd.Categorical(["blobs_points"] * table.n_obs) + table.uns["spatialdata_attrs"]["region"] = "blobs_points" + sdata_blobs_local["table"] = table + + # Now "blobs_circles" should have no annotating tables + # Trying to render it with a color column should raise an error + with pytest.raises( + KeyError, + match="Element 'blobs_circles' has no annotating tables", + ): + sdata_blobs_local.pl.render_shapes( + "blobs_circles", + color="channel_0_sum", + ).pl.show() + def test_plot_can_handle_nan_values_in_color_data(self, sdata_blobs: SpatialData, caplog): """Test that NaN values in color data are handled gracefully.""" sdata_blobs["table"].obs["region"] = pd.Categorical(["blobs_circles"] * sdata_blobs["table"].n_obs) From a165e641aa0aea61a3299e186f69d7858ee8db95 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Wed, 19 Nov 2025 21:02:34 +0100 Subject: [PATCH 15/16] fixed tests --- src/spatialdata_plot/pl/render.py | 9 +++++++-- src/spatialdata_plot/pl/utils.py | 23 ++++++++++++++++------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index 9d409ff5..85c012bf 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from collections import abc from copy import copy @@ -137,18 +138,22 @@ def _render_shapes( except (TypeError, ValueError): nan_count = int(_series.isna().sum()) if nan_count: - logger.warning( + msg = ( f"Found {nan_count} NaN values in color data. " "These observations will be colored with the 'na_color'." ) + warnings.warn(msg, UserWarning, stacklevel=2) + logger.warning(msg) color_vector = _series.to_numpy() else: if np.isnan(color_vector).any(): nan_count = int(np.isnan(color_vector).sum()) - logger.warning( + msg = ( f"Found {nan_count} NaN values in color data. " "These observations will be colored with the 'na_color'." ) + warnings.warn(msg, UserWarning, stacklevel=2) + logger.warning(msg) # Using dict.fromkeys here since set returns in arbitrary order # remove the color of NaN values, else it might be assigned to a category diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 49aa9fbf..7be17872 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -829,14 +829,23 @@ def _infer_color_data_kind( has_numeric = numeric_like.notna().any() has_non_numeric = numeric_like.isna().any() + # For table-backed columns, mixed numeric/non-numeric values are considered an error. + # For element-backed data (no table_name), fall back to categorical handling to remain robust. if has_numeric and has_non_numeric: - invalid_examples = non_na[numeric_like.isna()].astype(str).unique()[:3] - location = f" in table '{table_name}'" if table_name is not None else "" - raise TypeError( - f"Column '{value_to_plot}' for element '{element_label}'{location} contains both numeric and " - f"non-numeric values (e.g. {', '.join(invalid_examples)}). " - "Please ensure that the column stores consistent data." - ) + if table_name is not None: + invalid_examples = non_na[numeric_like.isna()].astype(str).unique()[:3] + location = f" in table '{table_name}'" if table_name is not None else "" + raise TypeError( + f"Column '{value_to_plot}' for element '{element_label}'{location} contains both numeric and " + f"non-numeric values (e.g. {', '.join(invalid_examples)}). " + "Please ensure that the column stores consistent data." + ) + if warn_on_object_to_categorical: + logger.warning( + f"Converting copy of '{value_to_plot}' column to categorical dtype for categorical plotting. " + "Consider converting before plotting." + ) + return "categorical", pd.Categorical(series) if has_numeric: return "numeric", pd.to_numeric(series, errors="coerce") From adcb9a57e6da5c75e38bc2168accd507f472afa9 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Wed, 19 Nov 2025 21:07:10 +0100 Subject: [PATCH 16/16] fixed tests --- src/spatialdata_plot/pl/render.py | 9 ++------- src/spatialdata_plot/pl/utils.py | 23 +++++++---------------- tests/pl/test_render_shapes.py | 14 +++++++------- 3 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index 85c012bf..9d409ff5 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -1,6 +1,5 @@ from __future__ import annotations -import warnings from collections import abc from copy import copy @@ -138,22 +137,18 @@ def _render_shapes( except (TypeError, ValueError): nan_count = int(_series.isna().sum()) if nan_count: - msg = ( + logger.warning( f"Found {nan_count} NaN values in color data. " "These observations will be colored with the 'na_color'." ) - warnings.warn(msg, UserWarning, stacklevel=2) - logger.warning(msg) color_vector = _series.to_numpy() else: if np.isnan(color_vector).any(): nan_count = int(np.isnan(color_vector).sum()) - msg = ( + logger.warning( f"Found {nan_count} NaN values in color data. " "These observations will be colored with the 'na_color'." ) - warnings.warn(msg, UserWarning, stacklevel=2) - logger.warning(msg) # Using dict.fromkeys here since set returns in arbitrary order # remove the color of NaN values, else it might be assigned to a category diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 7be17872..49aa9fbf 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -829,23 +829,14 @@ def _infer_color_data_kind( has_numeric = numeric_like.notna().any() has_non_numeric = numeric_like.isna().any() - # For table-backed columns, mixed numeric/non-numeric values are considered an error. - # For element-backed data (no table_name), fall back to categorical handling to remain robust. if has_numeric and has_non_numeric: - if table_name is not None: - invalid_examples = non_na[numeric_like.isna()].astype(str).unique()[:3] - location = f" in table '{table_name}'" if table_name is not None else "" - raise TypeError( - f"Column '{value_to_plot}' for element '{element_label}'{location} contains both numeric and " - f"non-numeric values (e.g. {', '.join(invalid_examples)}). " - "Please ensure that the column stores consistent data." - ) - if warn_on_object_to_categorical: - logger.warning( - f"Converting copy of '{value_to_plot}' column to categorical dtype for categorical plotting. " - "Consider converting before plotting." - ) - return "categorical", pd.Categorical(series) + invalid_examples = non_na[numeric_like.isna()].astype(str).unique()[:3] + location = f" in table '{table_name}'" if table_name is not None else "" + raise TypeError( + f"Column '{value_to_plot}' for element '{element_label}'{location} contains both numeric and " + f"non-numeric values (e.g. {', '.join(invalid_examples)}). " + "Please ensure that the column stores consistent data." + ) if has_numeric: return "numeric", pd.to_numeric(series, errors="coerce") diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index dbfb24ae..48d8dd2d 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -795,17 +795,16 @@ def test_raises_when_element_has_no_annotating_tables(self, sdata_blobs: Spatial def test_plot_can_handle_nan_values_in_color_data(sdata_blobs: SpatialData, caplog): - """Test that NaN values in color data are handled gracefully (warning + log).""" + """Test that NaN values in color data are handled gracefully and logged.""" sdata_blobs["table"].obs["region"] = pd.Categorical(["blobs_circles"] * sdata_blobs["table"].n_obs) sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_circles" # Add color column with NaN values sdata_blobs.shapes["blobs_circles"]["color_with_nan"] = [1.0, 2.0, np.nan, 4.0, 5.0] - # Expect both a UserWarning and a logger warning + # Expect a logger warning about NaN values with logger_warns(caplog, logger, match="Found 1 NaN values in color data"): - with pytest.warns(UserWarning, match="Found 1 NaN values in color data"): - sdata_blobs.pl.render_shapes(element="blobs_circles", color="color_with_nan", na_color="red").pl.show() + sdata_blobs.pl.render_shapes(element="blobs_circles", color="color_with_nan", na_color="red").pl.show() def test_plot_colorbar_normalization_with_nan_values(sdata_blobs: SpatialData): @@ -827,11 +826,12 @@ def test_plot_can_handle_non_numeric_radius_values(sdata_blobs: SpatialData): def test_plot_can_handle_mixed_numeric_and_color_data(sdata_blobs: SpatialData): - """Test handling of mixed numeric and color-like data without raising.""" + """Test that mixed numeric and color-like data raises a clear error.""" sdata_blobs["table"].obs["region"] = pd.Categorical(["blobs_circles"] * sdata_blobs["table"].n_obs) sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_circles" sdata_blobs.shapes["blobs_circles"]["mixed_data"] = [1.0, 2.0, np.nan, "red", 5.0] - # Should not raise; underlying code treats data robustly - sdata_blobs.pl.render_shapes(element="blobs_circles", color="mixed_data", na_color="gray").pl.show() + # Mixed numeric / non-numeric values should raise a TypeError + with pytest.raises(TypeError, match="contains both numeric and non-numeric values"): + sdata_blobs.pl.render_shapes(element="blobs_circles", color="mixed_data", na_color="gray").pl.show()