diff --git a/src/spatialdata_plot/pl/_datashader.py b/src/spatialdata_plot/pl/_datashader.py index 3b50dd52..0c197871 100644 --- a/src/spatialdata_plot/pl/_datashader.py +++ b/src/spatialdata_plot/pl/_datashader.py @@ -268,16 +268,20 @@ def _render_ds_image( shaded: Any, factor: float, zorder: int, - alpha: float, extent: list[float] | None, nan_result: Any | None = None, ) -> Any: - """Render a shaded datashader image onto matplotlib axes, with optional NaN overlay.""" + """Render a shaded datashader image onto matplotlib axes, with optional NaN overlay. + + Alpha is NOT passed to ``ax.imshow`` because it is already encoded in + the RGBA channels produced by ``ds.tf.shade(min_alpha=...)``. Passing + it again would apply transparency twice (see #367). + """ if nan_result is not None: rgba_nan, trans_nan = _create_image_from_datashader_result(nan_result, factor, ax) - _ax_show_and_transform(rgba_nan, trans_nan, ax, zorder=zorder, alpha=alpha, extent=extent) + _ax_show_and_transform(rgba_nan, trans_nan, ax, zorder=zorder, extent=extent) rgba_image, trans_data = _create_image_from_datashader_result(shaded, factor, ax) - return _ax_show_and_transform(rgba_image, trans_data, ax, zorder=zorder, alpha=alpha, extent=extent) + return _ax_show_and_transform(rgba_image, trans_data, ax, zorder=zorder, extent=extent) def _render_ds_outlines( @@ -315,7 +319,7 @@ def _render_ds_outlines( how="linear", ) rgba, trans = _create_image_from_datashader_result(shaded, factor, ax) - _ax_show_and_transform(rgba, trans, ax, zorder=render_params.zorder, alpha=alpha, extent=extent) + _ax_show_and_transform(rgba, trans, ax, zorder=render_params.zorder, extent=extent) def _build_ds_colorbar( diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index 488db5c9..b1cb321d 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -486,7 +486,6 @@ def _render_shapes( shaded, factor, render_params.zorder, - render_params.fill_alpha, x_ext + y_ext, nan_result=nan_shaded, ) @@ -886,7 +885,6 @@ def _render_points( shaded, factor, render_params.zorder, - render_params.alpha, x_ext + y_ext, nan_result=nan_shaded, ) diff --git a/tests/_images/Points_datashader_continuous_color.png b/tests/_images/Points_datashader_continuous_color.png index 2e1bec49..068fa6ee 100644 Binary files a/tests/_images/Points_datashader_continuous_color.png and b/tests/_images/Points_datashader_continuous_color.png differ diff --git a/tests/_images/Points_mpl_and_datashader_point_sizes_agree_after_altered_dpi.png b/tests/_images/Points_mpl_and_datashader_point_sizes_agree_after_altered_dpi.png index 9445837c..f5e2f77a 100644 Binary files a/tests/_images/Points_mpl_and_datashader_point_sizes_agree_after_altered_dpi.png and b/tests/_images/Points_mpl_and_datashader_point_sizes_agree_after_altered_dpi.png differ diff --git a/tests/pl/test_render_points.py b/tests/pl/test_render_points.py index 69191ea8..b5b29728 100644 --- a/tests/pl/test_render_points.py +++ b/tests/pl/test_render_points.py @@ -701,3 +701,22 @@ def test_datashader_points_visible_with_nonuniform_scale(sdata_blobs: SpatialDat """ _set_transformations(sdata_blobs["blobs_points"], {"global": Scale([1, 5], axes=("x", "y"))}) sdata_blobs.pl.render_points("blobs_points", method="datashader", color="black").pl.show() + + +def test_datashader_alpha_not_applied_twice(sdata_blobs: SpatialData): + """Datashader alpha must not be applied twice (once in shade, once in imshow). + + Regression test for https://github.com/scverse/spatialdata-plot/issues/367. + Before the fix, alpha was passed both to ds.tf.shade(min_alpha=...) and to + ax.imshow(alpha=...), resulting in effective transparency of alpha**2. + """ + fig, ax = plt.subplots() + sdata_blobs.pl.render_points(method="datashader", alpha=0.5, color="red").pl.show(ax=ax) + + axes_images = [c for c in ax.get_children() if isinstance(c, matplotlib.image.AxesImage)] + for img in axes_images: + assert img.get_alpha() is None, ( + f"Datashader AxesImage has alpha={img.get_alpha()}, which would be applied " + "on top of the alpha already in the RGBA channels — causing double transparency." + ) + plt.close(fig) diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index dece73b3..a29931d9 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -1093,3 +1093,28 @@ def test_datashader_outline_width_uses_points_units(sdata_blobs: SpatialData): sdata_blobs.pl.render_shapes( element="blobs_polygons", method="datashader", outline_alpha=1.0, outline_width=(8.0, 3.0) ).pl.show() + + +def test_datashader_alpha_not_applied_twice(sdata_blobs: SpatialData): + """Datashader fill_alpha and outline_alpha must not be applied twice. + + Regression test for https://github.com/scverse/spatialdata-plot/issues/367. + Before the fix, alpha was passed both to ds.tf.shade(min_alpha=...) and to + ax.imshow(alpha=...), resulting in effective transparency of alpha**2. + """ + fig, ax = plt.subplots() + sdata_blobs.pl.render_shapes( + method="datashader", + fill_alpha=0.5, + color="red", + outline_alpha=0.5, + outline_color="blue", + ).pl.show(ax=ax) + + axes_images = [c for c in ax.get_children() if isinstance(c, matplotlib.image.AxesImage)] + for img in axes_images: + assert img.get_alpha() is None, ( + f"Datashader AxesImage has alpha={img.get_alpha()}, which would be applied " + "on top of the alpha already in the RGBA channels — causing double transparency." + ) + plt.close(fig)