From 1b6d996340bfffe11f7dca527c01396aaaf58232 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Thu, 20 Nov 2025 11:31:59 +0100 Subject: [PATCH 1/3] Fix MultiPolygon patch creation for inverted inner rings --- src/spatialdata_plot/pl/utils.py | 92 ++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 954afe40..992be3cb 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -52,6 +52,7 @@ from scanpy.plotting._utils import add_colors_for_categorical_sample_annotation from scanpy.plotting.palettes import default_20, default_28, default_102 from scipy.spatial import ConvexHull +from shapely.errors import GEOSException from skimage.color import label2rgb from skimage.morphology import erosion, square from skimage.segmentation import find_boundaries @@ -446,7 +447,36 @@ def _as_rgba_array(x: Any) -> np.ndarray: outline_c = [None] * fill_c.shape[0] # Build DataFrame of valid geometries - shapes_df = pd.DataFrame(shapes, copy=True) + # Prefer working directly on a GeoDataFrame copy when possible + if isinstance(shapes, GeoDataFrame): + shapes_df: GeoDataFrame | pd.DataFrame = shapes.copy() + else: + shapes_df = pd.DataFrame(shapes, copy=True) + + # Robustly normalise geometries to a canonical representation. + # This ensures consistent exterior/interior ring orientation so that + # matplotlib's fill rules handle holes correctly regardless of user input. + if "geometry" in shapes_df.columns: + + def _normalize_geom(geom: Any) -> Any: + if geom is None or getattr(geom, "is_empty", False): + return geom + # shapely.normalize is available in shapely>=2; fall back to geom.normalize() + normalize_func = getattr(shapely, "normalize", None) + if callable(normalize_func): + try: + return normalize_func(geom) + except (GEOSException, TypeError, ValueError): + return geom + if hasattr(geom, "normalize"): + try: + return geom.normalize() + except (GEOSException, TypeError, ValueError): + return geom + return geom + + shapes_df["geometry"] = shapes_df["geometry"].apply(_normalize_geom) + shapes_df = shapes_df[shapes_df["geometry"].apply(lambda geom: not geom.is_empty)] shapes_df = shapes_df.reset_index(drop=True) @@ -1672,52 +1702,36 @@ def _validate_polygons(shapes: GeoDataFrame) -> GeoDataFrame: return shapes -def _collect_polygon_rings( - geom: shapely.Polygon | shapely.MultiPolygon, -) -> list[tuple[np.ndarray, list[np.ndarray]]]: - """Collect exterior/interior coordinate rings from (Multi)Polygons.""" - polygons: list[tuple[np.ndarray, list[np.ndarray]]] = [] - - def _collect(part: shapely.Polygon | shapely.MultiPolygon) -> None: - if part.geom_type == "Polygon": - exterior = np.asarray(part.exterior.coords) - interiors = [np.asarray(interior.coords) for interior in part.interiors] - polygons.append((exterior, interiors)) - elif part.geom_type == "MultiPolygon": - for child in part.geoms: - _collect(child) - else: - raise ValueError(f"Unhandled geometry type: {repr(part.geom_type)}") - - _collect(geom) - return polygons - +def _make_patch_from_multipolygon(mp: shapely.MultiPolygon) -> list[mpatches.PathPatch]: + """ + Create PathPatches from a MultiPolygon, preserving holes robustly. -def _create_ring_codes(length: int) -> npt.NDArray[np.uint8]: - codes = np.full(length, mpath.Path.LINETO, dtype=mpath.Path.code_type) - codes[0] = mpath.Path.MOVETO - return codes + This follows the same strategy as GeoPandas' internal Polygon plotting: + each (multi)polygon part becomes a compound Path composed of the exterior + ring and all interior rings. Orientation is handled by prior geometry + normalization rather than manual ring reversal. + """ + patches: list[mpatches.PathPatch] = [] + for poly in mp.geoms: + if poly.is_empty: + continue -def _make_patch_from_multipolygon(mp: shapely.MultiPolygon) -> mpatches.PathPatch: - # https://matplotlib.org/stable/gallery/shapes_and_collections/donut.html + # Ensure 2D vertices in case geometries carry Z + exterior = np.asarray(poly.exterior.coords)[..., :2] + interiors = [np.asarray(ring.coords)[..., :2] for ring in poly.interiors] - patches = [] - for exterior, interiors in _collect_polygon_rings(mp): if len(interiors) == 0: + # Simple polygon without holes patches.append(mpatches.Polygon(exterior, closed=True)) continue - ring_vertices = [exterior] - ring_codes = [_create_ring_codes(len(exterior))] - for hole in interiors: - reversed_hole = hole[::-1] - ring_vertices.append(reversed_hole) - ring_codes.append(_create_ring_codes(len(reversed_hole))) - - vertices = np.concatenate(ring_vertices) - all_codes = np.concatenate(ring_codes) - patches.append(mpatches.PathPatch(mpath.Path(vertices, all_codes))) + # Build a compound path: exterior + all interior rings + compound_path = mpath.Path.make_compound_path( + mpath.Path(exterior, closed=True), + *[mpath.Path(ring, closed=True) for ring in interiors], + ) + patches.append(mpatches.PathPatch(compound_path)) return patches From 326417403867342099fe7810a0f6f8d473a009f7 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Thu, 20 Nov 2025 11:43:30 +0100 Subject: [PATCH 2/3] more multipolygon tests --- tests/pl/test_render_shapes.py | 75 ++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index 1acf3d3c..fce236d6 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -130,6 +130,81 @@ def test_plot_can_render_multipolygons_that_say_they_are_polygons(self): fig.tight_layout() + def test_plot_can_render_polygon_with_inverted_inner_ring(self): + ext = [ + (7.866043666934409, 32.80184055229537), + (19.016191271980425, 203.48380872801957), + (75.90086964475744, 236.02570144190528), + (229.48380872801957, 235.98380872801957), + (235.98380872801957, 5.516191271980426), + (197.42585593903195, 6.144892860751103), + (116.5, 96.4575926540027), + (55.65582863082729, 12.531294107459374), + (7.866043666934409, 32.80184055229537), + ] + + interior = [ + (160.12353079731844, 173.21221665537414), + (181.80184055229537, 159.13395633306558), + (198.86604366693442, 179.80184055229537), + (178.19815944770465, 198.86604366693442), + (160.12353079731844, 173.21221665537414), + ] + + polygon = Polygon(ext, [interior]) + geo_df = gpd.GeoDataFrame(geometry=[polygon]) + sdata = SpatialData(shapes={"inverted_ring": ShapesModel.parse(geo_df)}) + + fig, ax = plt.subplots() + sdata.pl.render_shapes(element="inverted_ring").pl.show(ax=ax) + ax.set_xlim(0, 250) + ax.set_ylim(0, 250) + + fig.tight_layout() + + def test_plot_can_render_multipolygon_with_inverted_inner_ring_and_disjoint_part(self): + ext = [ + (7.866043666934409, 32.80184055229537), + (19.016191271980425, 203.48380872801957), + (75.90086964475744, 236.02570144190528), + (229.48380872801957, 235.98380872801957), + (235.98380872801957, 5.516191271980426), + (197.42585593903195, 6.144892860751103), + (116.5, 96.4575926540027), + (55.65582863082729, 12.531294107459374), + (7.866043666934409, 32.80184055229537), + ] + + interior = [ + (160.12353079731844, 173.21221665537414), + (181.80184055229537, 159.13395633306558), + (198.86604366693442, 179.80184055229537), + (178.19815944770465, 198.86604366693442), + (160.12353079731844, 173.21221665537414), + ] + + # Part with a hole and non-standard orientation, plus a disjoint simple part + poly_with_hole = Polygon(ext, [interior]) + disjoint_poly = Polygon( + [ + (300.0, 300.0), + (320.0, 300.0), + (320.0, 320.0), + (300.0, 320.0), + (300.0, 300.0), + ] + ) + multipoly = MultiPolygon([poly_with_hole, disjoint_poly]) + geo_df = gpd.GeoDataFrame(geometry=[multipoly]) + sdata = SpatialData(shapes={"inverted_ring_multipoly": ShapesModel.parse(geo_df)}) + + fig, ax = plt.subplots() + sdata.pl.render_shapes(element="inverted_ring_multipoly").pl.show(ax=ax) + ax.set_xlim(0, 350) + ax.set_ylim(0, 350) + + 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)] From 4b5cb8ce99aa2f54ee7b88158983036b8d9b3c87 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Thu, 20 Nov 2025 11:48:56 +0100 Subject: [PATCH 3/3] images from runner --- src/spatialdata_plot/pl/utils.py | 2 -- ...th_inverted_inner_ring_and_disjoint_part.png | Bin 0 -> 13223 bytes ..._render_polygon_with_inverted_inner_ring.png | Bin 0 -> 12991 bytes 3 files changed, 2 deletions(-) create mode 100644 tests/_images/Shapes_can_render_multipolygon_with_inverted_inner_ring_and_disjoint_part.png create mode 100644 tests/_images/Shapes_can_render_polygon_with_inverted_inner_ring.png diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 992be3cb..97ffac1d 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -446,8 +446,6 @@ def _as_rgba_array(x: Any) -> np.ndarray: else: outline_c = [None] * fill_c.shape[0] - # Build DataFrame of valid geometries - # Prefer working directly on a GeoDataFrame copy when possible if isinstance(shapes, GeoDataFrame): shapes_df: GeoDataFrame | pd.DataFrame = shapes.copy() else: diff --git a/tests/_images/Shapes_can_render_multipolygon_with_inverted_inner_ring_and_disjoint_part.png b/tests/_images/Shapes_can_render_multipolygon_with_inverted_inner_ring_and_disjoint_part.png new file mode 100644 index 0000000000000000000000000000000000000000..1b8b3fdecd481c2b695587320bba3a75efe77d6b GIT binary patch literal 13223 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 literal 0 HcmV?d00001 diff --git a/tests/_images/Shapes_can_render_polygon_with_inverted_inner_ring.png b/tests/_images/Shapes_can_render_polygon_with_inverted_inner_ring.png new file mode 100644 index 0000000000000000000000000000000000000000..d5bd79646732086c9ad98ad9ee53580c9cc4e61e GIT binary patch literal 12991 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 literal 0 HcmV?d00001