Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/spatialdata_plot/pl/_datashader.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,11 @@ def _ax_show_and_transform(
return im


def _pad_degenerate_extent(ext: list[Any]) -> list[Any]:
"""Pad a zero-width extent to a unit window centered on its value; pass others through."""
return [ext[0] - 0.5, ext[1] + 0.5] if ext[1] == ext[0] else ext


def _compute_datashader_canvas_params(
x_ext: list[Any],
y_ext: list[Any],
Expand All @@ -725,6 +730,10 @@ def _compute_datashader_canvas_params(

Shared logic used by both the dask-based and pandas-based entry points.
"""
# A zero-width extent (single point, coincident points, axis-aligned line) has no scale to
# build a canvas from; pad it so the factor below doesn't divide by zero.
x_ext, y_ext = _pad_degenerate_extent(x_ext), _pad_degenerate_extent(y_ext)

# Compute canvas size in pixels, capped at the figure's display resolution.
# Using np.max ensures the canvas never exceeds display pixels on either axis,
# preventing pixel-based operations (spread, line_width) from being downscaled
Expand Down
32 changes: 32 additions & 0 deletions tests/pl/test_render_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
_build_datashader_color_key,
_ds_aggregate,
_ds_shade_categorical,
_pad_degenerate_extent,
)
from spatialdata_plot.pl.render import _warn_groups_ignored_continuous
from tests.conftest import DPI, PlotTester, PlotTesterMeta, _viridis_with_under_over, get_standard_RNG
Expand Down Expand Up @@ -1255,3 +1256,34 @@ def test_density_defaults_silent_and_force_datashader(sdata_blobs: SpatialData,
last = list(out.plotting_tree.values())[-1]
assert (last.density, last.density_how, last.method) == (True, "linear", "datashader")
assert not any("ignored when density=True" in str(w.message) for w in recwarn.list)


# ---------------------------------------------------------------------------
# Zero-extent datashader canvas (#724)
# ---------------------------------------------------------------------------


def test_pad_degenerate_extent():
# Regression test for #724: a zero-width extent expands to a unit window centered on the value,
# and a non-degenerate extent is passed through unchanged (so normal data is unaffected).
assert _pad_degenerate_extent([5.0, 5.0]) == [4.5, 5.5]
assert _pad_degenerate_extent([-3.0, -3.0]) == [-3.5, -2.5]
assert _pad_degenerate_extent([0.0, 10.0]) == [0.0, 10.0]


@pytest.mark.parametrize(
"coords",
[
[[5.0, 5.0]],
[[5.0, 5.0], [5.0, 5.0], [5.0, 5.0]],
[[5.0, 0.0], [5.0, 1.0], [5.0, 2.0]],
],
ids=["single_point", "coincident_points", "axis_aligned_line"],
)
def test_datashader_zero_extent_renders(coords):
# Regression test for #724: zero-extent point sets crashed the datashader backend with
# "cannot convert float NaN to integer". They must now render without raising.
df = pd.DataFrame(np.asarray(coords, dtype=float), columns=["x", "y"])
sdata = SpatialData(points={"points": PointsModel.parse(df)})
sdata.pl.render_points("points", method="datashader").pl.show()
plt.close("all")
Loading