diff --git a/CHANGELOG.md b/CHANGELOG.md index a87fd0ab..5b138c91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning][]. ### Added - Multipolygons are now handled correctly (#93) +- Can now scale shapes (#152) - Can now plot columns from GeoDataFrame (#149) ### Fixed diff --git a/src/spatialdata_plot/pl/basic.py b/src/spatialdata_plot/pl/basic.py index 795e5ec7..f09425b5 100644 --- a/src/spatialdata_plot/pl/basic.py +++ b/src/spatialdata_plot/pl/basic.py @@ -145,7 +145,7 @@ def render_shapes( elements: str | list[str] | None = None, color: str | None = None, groups: str | Sequence[str] | None = None, - size: float = 1.0, + scale: float = 1.0, outline: bool = False, outline_width: float = 1.5, outline_color: str | list[float] = "#000000ff", @@ -171,7 +171,7 @@ def render_shapes( groups For discrete annotation in ``color``, select which values to plot (other values are set to NAs). - size + scale Value to scale circles, if present. outline If `True`, a thin border around points/shapes is plotted. @@ -212,11 +212,12 @@ def render_shapes( na_color=na_color, # type: ignore[arg-type] **kwargs, ) - outline_params = _set_outline(size, outline, outline_width, outline_color) + outline_params = _set_outline(outline, outline_width, outline_color) sdata.plotting_tree[f"{n_steps+1}_render_shapes"] = ShapesRenderParams( elements=elements, color=color, groups=groups, + scale=scale, outline_params=outline_params, layer=layer, cmap_params=cmap_params, diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index f5ceac11..6e264532 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -102,7 +102,7 @@ def _render_shapes( shapes = gpd.GeoDataFrame(shapes, geometry="geometry") _cax = _get_collection_shape( shapes=shapes, - s=render_params.size, + s=render_params.scale, c=color_vector, render_params=render_params, rasterized=sc_settings._vector_friendly, diff --git a/src/spatialdata_plot/pl/render_params.py b/src/spatialdata_plot/pl/render_params.py index ac78eeb6..9294dc2d 100644 --- a/src/spatialdata_plot/pl/render_params.py +++ b/src/spatialdata_plot/pl/render_params.py @@ -77,7 +77,7 @@ class ShapesRenderParams: palette: ListedColormap | str | None = None outline_alpha: float = 1.0 fill_alpha: float = 0.3 - size: float = 1.0 + scale: float = 1.0 transfunc: Callable[[float], float] | None = None diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 32324597..d60e53a1 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -185,7 +185,7 @@ def _get_collection_shape( Args: - shapes (list[GeoDataFrame]): List of geometrical shapes. - c: Color parameter. - - s (float): Size of the shape. + - s (float): Scale of the shape. - norm: Normalization for the color map. - fill_alpha (float, optional): Opacity for the fill color. - outline_alpha (float, optional): Opacity for the outline. @@ -241,21 +241,30 @@ def assign_fill_and_outline_to_row( geom = row["geometry"] if geom.geom_type == "Polygon": row = row.to_dict() - row["geometry"] = mplp.Polygon(geom.exterior.coords, closed=True) + coords = np.array(geom.exterior.coords) + centroid = np.mean(coords, axis=0) + scaled_coords = [(centroid + (np.array(coord) - centroid) * s).tolist() for coord in geom.exterior.coords] + row["geometry"] = mplp.Polygon(scaled_coords, closed=True) assign_fill_and_outline_to_row(shapes, fill_c, outline_c, row, idx) rows.append(row) elif geom.geom_type == "MultiPolygon": - mp = _make_patch_from_multipolygon(geom) - for _, m in enumerate(mp): + # mp = _make_patch_from_multipolygon(geom) + for polygon in geom.geoms: mp_copy = row.to_dict() - mp_copy["geometry"] = m + coords = np.array(polygon.exterior.coords) + centroid = np.mean(coords, axis=0) + scaled_coords = [(centroid + (coord - centroid) * s).tolist() for coord in coords] + mp_copy["geometry"] = mplp.Polygon(scaled_coords, closed=True) assign_fill_and_outline_to_row(shapes, fill_c, outline_c, mp_copy, idx) rows.append(mp_copy) elif geom.geom_type == "Point": row = row.to_dict() - row["geometry"] = mplp.Circle((geom.x, geom.y), radius=row["radius"]) + scaled_radius = row["radius"] * s + row["geometry"] = mplp.Circle( + (geom.x, geom.y), radius=scaled_radius + ) # Circle is always scaled from its center assign_fill_and_outline_to_row(shapes, fill_c, outline_c, row, idx) rows.append(row) @@ -576,7 +585,6 @@ def _prepare_cmap_norm( def _set_outline( - size: float, outline: bool = False, outline_width: float = 1.5, outline_color: str | list[float] = "#0000000ff", # black, white diff --git a/tests/_images/Shapes_can_scale_shapes.png b/tests/_images/Shapes_can_scale_shapes.png new file mode 100644 index 00000000..8a1cf605 Binary files /dev/null and b/tests/_images/Shapes_can_scale_shapes.png differ diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index e183cea7..25379fb2 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -97,3 +97,6 @@ def test_plot_can_color_from_geodataframe(self, sdata_blobs: SpatialData): elements="blobs_polygons", color="value", ).pl.show() + + def test_plot_can_scale_shapes(self, sdata_blobs: SpatialData): + sdata_blobs.pl.render_shapes(elements="blobs_circles", scale=0.5).pl.show()