diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index 76e57d82..498d8efd 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -57,6 +57,7 @@ _prepare_transformation, _rasterize_if_necessary, _set_color_source_vec, + _validate_polygons, ) _Normalize = Normalize | abc.Sequence[Normalize] @@ -184,6 +185,8 @@ def _render_shapes( ) shapes = _convert_shapes(shapes, render_params.shape, max_extent) + shapes = _validate_polygons(shapes) + # Determine which method to use for rendering method = render_params.method diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index cccb587e..1ba3e3ba 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -1246,6 +1246,38 @@ def _get_linear_colormap(colors: list[str], background: str) -> list[LinearSegme return [LinearSegmentedColormap.from_list(c, [background, c], N=256) for c in colors] +def _validate_polygons(shapes: GeoDataFrame) -> GeoDataFrame: + """ + Convert Polygons with holes to MultiPolygons to keep interior rings during rendering. + + Parameters + ---------- + shapes + GeoDataFrame containing a `geometry` column. + + Returns + ------- + GeoDataFrame + ``shapes`` with holed Polygons converted to MultiPolygons. + """ + if "geometry" not in shapes: + return shapes + + converted_count = 0 + for idx, geom in shapes["geometry"].items(): + if isinstance(geom, shapely.Polygon) and len(geom.interiors) > 0: + shapes.at[idx, "geometry"] = shapely.MultiPolygon([geom]) + converted_count += 1 + + if converted_count > 0: + logger.info( + "Converted %d Polygon(s) with holes to MultiPolygon(s) for correct rendering.", + converted_count, + ) + + return shapes + + def _collect_polygon_rings( geom: shapely.Polygon | shapely.MultiPolygon, ) -> list[tuple[np.ndarray, list[np.ndarray]]]: diff --git a/tests/_images/Shapes_can_render_multipolygons_that_say_they_are_polygons.png b/tests/_images/Shapes_can_render_multipolygons_that_say_they_are_polygons.png new file mode 100755 index 00000000..f5475dd4 Binary files /dev/null and b/tests/_images/Shapes_can_render_multipolygons_that_say_they_are_polygons.png differ diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index 8fc39582..6728f64f 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -115,6 +115,20 @@ def test_plot_can_render_multipolygons_with_multiple_holes(self): fig.tight_layout() + def test_plot_can_render_multipolygons_that_say_they_are_polygons(self): + exterior = [(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)] + interior = [(0.1, 0.1), (0.1, 0.9), (0.9, 0.9), (0.9, 0.1), (0.1, 0.1)] + polygon = Polygon(exterior, [interior]) + geo_df = gpd.GeoDataFrame(geometry=[polygon]) + sdata = SpatialData(shapes={"test": ShapesModel.parse(geo_df)}) + + fig, ax = plt.subplots() + sdata.pl.render_shapes(element="test").pl.show(ax=ax) + ax.set_xlim(-1, 2) + ax.set_ylim(-1, 2) + + fig.tight_layout() + def test_plot_can_color_multipolygons_with_multiple_holes(self): square = [(0.0, 0.0), (5.0, 0.0), (5.0, 5.0), (0.0, 5.0), (0.0, 0.0)] first_hole = [(1.0, 1.0), (2.0, 1.0), (2.0, 2.0), (1.0, 2.0), (1.0, 1.0)]