From 7ea1bf7a1952acc3f6c4b1e224bfc50b82f2d21b Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Mon, 20 Oct 2025 18:32:15 +0200 Subject: [PATCH 1/3] added test --- src/spatialdata_plot/pl/basic.py | 8 ++- src/spatialdata_plot/pl/render_params.py | 2 +- src/spatialdata_plot/pl/utils.py | 74 ++++++++++++++++++++++-- tests/conftest.py | 25 ++++++++ tests/pl/test_render_shapes.py | 38 ++++++++---- 5 files changed, 127 insertions(+), 20 deletions(-) diff --git a/src/spatialdata_plot/pl/basic.py b/src/spatialdata_plot/pl/basic.py index a948077f..883f51ce 100644 --- a/src/spatialdata_plot/pl/basic.py +++ b/src/spatialdata_plot/pl/basic.py @@ -170,7 +170,7 @@ def render_shapes( method: str | None = None, table_name: str | None = None, table_layer: str | None = None, - shape: Literal["circle", "hex", "square"] | None = None, + shape: Literal["circle", "hex", "visium_hex", "square"] | None = None, **kwargs: Any, ) -> sd.SpatialData: """ @@ -243,9 +243,11 @@ def render_shapes( table_layer: str | None Layer of the table to use for coloring if `color` is in :attr:`sdata.table.var_names`. If None, the data in :attr:`sdata.table.X` is used for coloring. - shape: Literal["circle", "hex", "square"] | None + shape: Literal["circle", "hex", "visium_hex", "square"] | None If None (default), the shapes are rendered as they are. Else, if either of "circle", "hex" or "square" is - specified, the shapes are converted to a circle/hexagon/square before rendering. + specified, the shapes are converted to a circle/hexagon/square before rendering. If "visium_hex" is + specified, the shapes are assumed to be Visium spots and the size of the hexagons is adjusted to be adjacent + to each other. **kwargs : Any Additional arguments for customization. This can include: diff --git a/src/spatialdata_plot/pl/render_params.py b/src/spatialdata_plot/pl/render_params.py index 15812c0c..a8382753 100644 --- a/src/spatialdata_plot/pl/render_params.py +++ b/src/spatialdata_plot/pl/render_params.py @@ -211,7 +211,7 @@ class ShapesRenderParams: zorder: int = 0 table_name: str | None = None table_layer: str | None = None - shape: Literal["circle", "hex", "square"] | None = None + shape: Literal["circle", "hex", "visium_hex", "square"] | None = None ds_reduction: Literal["sum", "mean", "any", "count", "std", "var", "max", "min"] | None = None diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index ff1f8daa..82b84973 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -1802,7 +1802,9 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st if (norm := param_dict.get("norm")) is not None: if element_type in {"images", "labels"} and not isinstance(norm, Normalize): raise TypeError("Parameter 'norm' must be of type Normalize.") - if element_type in ["shapes", "points"] and not isinstance(norm, bool | Normalize): + if element_type in {"shapes", "points"} and not isinstance( + norm, bool | Normalize + ): raise TypeError("Parameter 'norm' must be a boolean or a mpl.Normalize.") if (scale := param_dict.get("scale")) is not None: @@ -1821,11 +1823,14 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st raise ValueError("Parameter 'size' must be a positive number.") if element_type == "shapes" and (shape := param_dict.get("shape")) is not None: + valid_shapes = {"circle", "hex", "visium_hex", "square"} if not isinstance(shape, str): - raise TypeError("Parameter 'shape' must be a String from ['circle', 'hex', 'square'] if not None.") - if shape not in ["circle", "hex", "square"]: + raise TypeError( + f"Parameter 'shape' must be a String from {valid_shapes} if not None." + ) + if shape not in valid_shapes: raise ValueError( - f"'{shape}' is not supported for 'shape', please choose from[None, 'circle', 'hex', 'square']." + f"'{shape}' is not supported for 'shape', please choose from {valid_shapes}." ) table_name = param_dict.get("table_name") @@ -2040,7 +2045,7 @@ def _validate_shape_render_params( scale: float | int, table_name: str | None, table_layer: str | None, - shape: Literal["circle", "hex", "square"] | None, + shape: Literal["circle", "hex", "visium_hex", "square"] | None, method: str | None, ds_reduction: str | None, ) -> dict[str, dict[str, Any]]: @@ -2647,9 +2652,10 @@ def _convert_shapes( # define individual conversion methods def _circle_to_hexagon(center: shapely.Point, radius: float) -> tuple[shapely.Polygon, None]: + # Create hexagon with point at top (30° offset from standard orientation) vertices = [ (center.x + radius * math.cos(math.radians(angle)), center.y + radius * math.sin(math.radians(angle))) - for angle in range(0, 360, 60) + for angle in range(30, 390, 60) # Start at 30° and go every 60° ] return shapely.Polygon(vertices), None @@ -2718,6 +2724,62 @@ def _multipolygon_to_circle(multipolygon: shapely.MultiPolygon) -> tuple[shapely "Polygon": _polygon_to_hexagon, "Multipolygon": _multipolygon_to_hexagon, } + elif target_shape == "visium_hex": + # For visium_hex, we only support Points and warn for other geometry types + point_centers = [] + non_point_count = 0 + + for i in range(shapes.shape[0]): + if shapes["geometry"][i].type == "Point": + point_centers.append((shapes["geometry"][i].x, shapes["geometry"][i].y)) + else: + non_point_count += 1 + + if non_point_count > 0: + warnings.warn( + f"visium_hex conversion only supports Point geometries. Found {non_point_count} non-Point geometries " + f"that will be converted using regular hex conversion. Consider using shape='hex' for mixed geometry types.", + UserWarning, + stacklevel=2, + ) + + if len(point_centers) < 2: + # If we have fewer than 2 points, fall back to regular hex conversion + conversion_methods = { + "Point": _circle_to_hexagon, + "Polygon": _polygon_to_hexagon, + "Multipolygon": _multipolygon_to_hexagon, + } + else: + # Calculate typical spacing between point centers + centers_array = np.array(point_centers) + distances = [] + for i in range(len(point_centers)): + for j in range(i + 1, len(point_centers)): + dist = np.linalg.norm(centers_array[i] - centers_array[j]) + distances.append(dist) + + # Use min dist of closest neighbors as the side length for radius calc + side_length = np.min(distances) + hex_radius = (side_length * 2.0 / math.sqrt(3)) / 2.0 + + # Create conversion methods + def _circle_to_visium_hex(center: shapely.Point, radius: float) -> tuple[shapely.Polygon, None]: + return _circle_to_hexagon(center, hex_radius) + + def _polygon_to_visium_hex(polygon: shapely.Polygon) -> tuple[shapely.Polygon, None]: + # Fall back to regular hex conversion for non-points + return _polygon_to_hexagon(polygon) + + def _multipolygon_to_visium_hex(multipolygon: shapely.MultiPolygon) -> tuple[shapely.Polygon, None]: + # Fall back to regular hex conversion for non-points + return _multipolygon_to_hexagon(multipolygon) + + conversion_methods = { + "Point": _circle_to_visium_hex, + "Polygon": _polygon_to_visium_hex, + "Multipolygon": _multipolygon_to_visium_hex, + } else: conversion_methods = { "Point": _circle_to_square, diff --git a/tests/conftest.py b/tests/conftest.py index 896eaeb7..38cf3c62 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import itertools from abc import ABC, ABCMeta from collections.abc import Callable from functools import wraps @@ -525,3 +526,27 @@ def _get_sdata_with_multiple_images(share_coordinate_system: str = "all"): return sdata return _get_sdata_with_multiple_images + + +# Visium hex test fixtures +@pytest.fixture +def sdata_hexagonal_grid_spots(): + """Create a hexagonal grid of points for testing visium_hex functionality.""" + from shapely.geometry import Point + + spacing = 10.0 + n_rows, n_cols = 4, 4 + + points = [] + for i, j in itertools.product(range(n_rows), range(n_cols)): + # Offset every second row by half the spacing for proper hexagonal packing + x = j * spacing + (i % 2) * spacing / 2 + y = i * spacing * 0.866 # sqrt(3)/2 for proper hexagonal spacing + points.append(Point(x, y)) + + # Create GeoDataFrame with radius column + gdf = GeoDataFrame(geometry=points) + gdf["radius"] = 2.0 # Small radius for original circles + + # Create SpatialData object + return SpatialData(shapes={"spots": gdf}) diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index fb676386..7cca8df8 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -40,7 +40,7 @@ def test_plot_can_render_circles_with_outline(self, sdata_blobs: SpatialData): sdata_blobs.pl.render_shapes(element="blobs_circles", outline_alpha=1).pl.show() def test_plot_can_render_circles_with_colored_outline(self, sdata_blobs: SpatialData): - sdata_blobs.pl.render_shapes(element="blobs_circles", outline_color="red").pl.show() + sdata_blobs.pl.render_shapes(element="blobs_circles", outline_alpha=1, outline_color="red").pl.show() def test_plot_can_render_polygons(self, sdata_blobs: SpatialData): sdata_blobs.pl.render_shapes(element="blobs_polygons").pl.show() @@ -49,13 +49,17 @@ def test_plot_can_render_polygons_with_outline(self, sdata_blobs: SpatialData): sdata_blobs.pl.render_shapes(element="blobs_polygons", outline_alpha=1).pl.show() def test_plot_can_render_polygons_with_str_colored_outline(self, sdata_blobs: SpatialData): - sdata_blobs.pl.render_shapes(element="blobs_polygons", outline_color="red").pl.show() + sdata_blobs.pl.render_shapes(element="blobs_polygons", outline_alpha=1, outline_color="red").pl.show() def test_plot_can_render_polygons_with_rgb_colored_outline(self, sdata_blobs: SpatialData): - sdata_blobs.pl.render_shapes(element="blobs_polygons", outline_color=(0.0, 0.0, 1.0, 1.0)).pl.show() + sdata_blobs.pl.render_shapes( + element="blobs_polygons", outline_alpha=1, outline_color=(0.0, 0.0, 1.0, 1.0) + ).pl.show() def test_plot_can_render_polygons_with_rgba_colored_outline(self, sdata_blobs: SpatialData): - sdata_blobs.pl.render_shapes(element="blobs_polygons", outline_color=(0.0, 1.0, 0.0, 1.0)).pl.show() + sdata_blobs.pl.render_shapes( + element="blobs_polygons", outline_alpha=1, outline_color=(0.0, 1.0, 0.0, 1.0) + ).pl.show() def test_plot_can_render_empty_geometry(self, sdata_blobs: SpatialData): sdata_blobs.shapes["blobs_circles"].at[0, "geometry"] = gpd.points_from_xy([None], [None])[0] @@ -65,7 +69,7 @@ def test_plot_can_render_circles_with_default_outline_width(self, sdata_blobs: S sdata_blobs.pl.render_shapes(element="blobs_circles", outline_alpha=1).pl.show() def test_plot_can_render_circles_with_specified_outline_width(self, sdata_blobs: SpatialData): - sdata_blobs.pl.render_shapes(element="blobs_circles", outline_width=3.0).pl.show() + sdata_blobs.pl.render_shapes(element="blobs_circles", outline_alpha=1, outline_width=3.0).pl.show() def test_plot_can_render_multipolygons(self): def _make_multi(): @@ -402,19 +406,23 @@ def test_plot_datashader_can_render_with_diff_alpha_outline(self, sdata_blobs: S sdata_blobs.pl.render_shapes(method="datashader", element="blobs_polygons", outline_alpha=0.5).pl.show() def test_plot_datashader_can_render_with_diff_width_outline(self, sdata_blobs: SpatialData): - sdata_blobs.pl.render_shapes(method="datashader", element="blobs_polygons", outline_width=5.0).pl.show() + sdata_blobs.pl.render_shapes( + method="datashader", element="blobs_polygons", outline_alpha=1.0, outline_width=5.0 + ).pl.show() def test_plot_datashader_can_render_with_colored_outline(self, sdata_blobs: SpatialData): - sdata_blobs.pl.render_shapes(method="datashader", element="blobs_polygons", outline_color="red").pl.show() + sdata_blobs.pl.render_shapes( + method="datashader", element="blobs_polygons", outline_alpha=1, outline_color="red" + ).pl.show() def test_plot_datashader_can_render_with_rgb_colored_outline(self, sdata_blobs: SpatialData): sdata_blobs.pl.render_shapes( - method="datashader", element="blobs_polygons", outline_color=(0.0, 0.0, 1.0) + method="datashader", element="blobs_polygons", outline_alpha=1, outline_color=(0.0, 0.0, 1.0) ).pl.show() def test_plot_datashader_can_render_with_rgba_colored_outline(self, sdata_blobs: SpatialData): sdata_blobs.pl.render_shapes( - method="datashader", element="blobs_polygons", outline_color=(0.0, 1.0, 0.0, 1.0) + method="datashader", element="blobs_polygons", outline_alpha=1, outline_color=(0.0, 1.0, 0.0, 1.0) ).pl.show() def test_plot_can_set_clims_clip(self, sdata_blobs: SpatialData): @@ -593,6 +601,12 @@ def test_plot_can_render_multipolygons_to_square(self, sdata_blobs: SpatialData) def test_plot_can_render_multipolygons_to_circle(self, sdata_blobs: SpatialData): sdata_blobs.pl.render_shapes(element="blobs_multipolygons", shape="circle").pl.show() + def test_plot_visium_hex_hexagonal_grid_comparison(self, sdata_hexagonal_grid_spots: SpatialData): + _, axs = plt.subplots(nrows=1, ncols=2, layout="tight") + + sdata_hexagonal_grid_spots.pl.render_shapes(element="spots", shape="circle").pl.show(ax=axs[0]) + sdata_hexagonal_grid_spots.pl.render_shapes(element="spots", shape="visium_hex").pl.show(ax=axs[1]) + def test_plot_datashader_can_render_circles_to_hex(self, sdata_blobs: SpatialData): sdata_blobs.pl.render_shapes(element="blobs_circles", shape="hex", method="datashader").pl.show() @@ -616,6 +630,7 @@ def test_plot_datashader_can_render_multipolygons_to_square(self, sdata_blobs: S def test_plot_datashader_can_render_multipolygons_to_circle(self, sdata_blobs: SpatialData): sdata_blobs.pl.render_shapes(element="blobs_multipolygons", shape="circle", method="datashader").pl.show() + def test_plot_can_render_shapes_with_double_outline(self, sdata_blobs: SpatialData): sdata_blobs.pl.render_shapes("blobs_circles", outline_width=(10.0, 5.0)).pl.show() @@ -631,7 +646,10 @@ def test_plot_can_render_double_outline_with_diff_alpha(self, sdata_blobs: Spati def test_plot_outline_alpha_takes_precedence(self, sdata_blobs: SpatialData): sdata_blobs.pl.render_shapes( - element="blobs_circles", outline_color=("#ff660033", "#33aa0066"), outline_width=(20, 10), outline_alpha=1.0 + element="blobs_circles", + outline_color=("#ff660033", "#33aa0066"), + outline_width=(20, 10), + outline_alpha=(1.0, 1.0), ).pl.show() def test_plot_datashader_can_render_shapes_with_double_outline(self, sdata_blobs: SpatialData): From 62e24f462e08e11586b6ec7778ebff5a37d3303f Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Mon, 20 Oct 2025 18:39:47 +0200 Subject: [PATCH 2/3] fixed import --- tests/conftest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 38cf3c62..61c66573 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -528,11 +528,11 @@ def _get_sdata_with_multiple_images(share_coordinate_system: str = "all"): return _get_sdata_with_multiple_images -# Visium hex test fixtures @pytest.fixture def sdata_hexagonal_grid_spots(): """Create a hexagonal grid of points for testing visium_hex functionality.""" from shapely.geometry import Point + from spatialdata.models import ShapesModel spacing = 10.0 n_rows, n_cols = 4, 4 @@ -548,5 +548,7 @@ def sdata_hexagonal_grid_spots(): gdf = GeoDataFrame(geometry=points) gdf["radius"] = 2.0 # Small radius for original circles - # Create SpatialData object - return SpatialData(shapes={"spots": gdf}) + # Use ShapesModel.parse() to create a properly validated GeoDataFrame + shapes_gdf = ShapesModel.parse(gdf) + + return SpatialData(shapes={"spots": shapes_gdf}) \ No newline at end of file From 9a1b1f255348bb9a1f4cb360c9fb66ed696ff1bd Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Mon, 20 Oct 2025 18:44:49 +0200 Subject: [PATCH 3/3] added img from runner --- .../Shapes_visium_hex_hexagonal_grid.png | Bin 0 -> 12310 bytes tests/pl/test_render_shapes.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/_images/Shapes_visium_hex_hexagonal_grid.png diff --git a/tests/_images/Shapes_visium_hex_hexagonal_grid.png b/tests/_images/Shapes_visium_hex_hexagonal_grid.png new file mode 100644 index 0000000000000000000000000000000000000000..b6c593692df0b5767ddc7b16016aeebaebf914b7 GIT binary patch literal 12310 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 literal 0 HcmV?d00001 diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index 7cca8df8..1a6dc3bb 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -601,7 +601,7 @@ def test_plot_can_render_multipolygons_to_square(self, sdata_blobs: SpatialData) def test_plot_can_render_multipolygons_to_circle(self, sdata_blobs: SpatialData): sdata_blobs.pl.render_shapes(element="blobs_multipolygons", shape="circle").pl.show() - def test_plot_visium_hex_hexagonal_grid_comparison(self, sdata_hexagonal_grid_spots: SpatialData): + def test_plot_visium_hex_hexagonal_grid(self, sdata_hexagonal_grid_spots: SpatialData): _, axs = plt.subplots(nrows=1, ncols=2, layout="tight") sdata_hexagonal_grid_spots.pl.render_shapes(element="spots", shape="circle").pl.show(ax=axs[0])