Skip to content

Commit efcec6d

Browse files
committed
fix(datashader): handle zero-extent point sets instead of NaN canvas (#724)
A single point (or coincident points / an axis-aligned line) has zero spatial extent, so _compute_datashader_canvas_params computed factor=0 and then int(round(width / 0)) raised 'cannot convert float NaN to integer'. Pad any degenerate axis to a unit window centered on its value before deriving the canvas scale, so the data lands in a finite canvas and renders. Non-degenerate extents are passed through unchanged, so normal data is unaffected. Fixes #724.
1 parent 0919069 commit efcec6d

2 files changed

Lines changed: 41 additions & 0 deletions

File tree

src/spatialdata_plot/pl/_datashader.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,11 @@ def _ax_show_and_transform(
716716
return im
717717

718718

719+
def _pad_degenerate_extent(ext: list[Any]) -> list[Any]:
720+
"""Pad a zero-width extent to a unit window centered on its value; pass others through."""
721+
return [ext[0] - 0.5, ext[1] + 0.5] if ext[1] == ext[0] else ext
722+
723+
719724
def _compute_datashader_canvas_params(
720725
x_ext: list[Any],
721726
y_ext: list[Any],
@@ -725,6 +730,10 @@ def _compute_datashader_canvas_params(
725730
726731
Shared logic used by both the dask-based and pandas-based entry points.
727732
"""
733+
# A zero-width extent (single point, coincident points, axis-aligned line) has no scale to
734+
# build a canvas from; pad it so the factor below doesn't divide by zero.
735+
x_ext, y_ext = _pad_degenerate_extent(x_ext), _pad_degenerate_extent(y_ext)
736+
728737
# Compute canvas size in pixels, capped at the figure's display resolution.
729738
# Using np.max ensures the canvas never exceeds display pixels on either axis,
730739
# preventing pixel-based operations (spread, line_width) from being downscaled

tests/pl/test_render_points.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
_build_datashader_color_key,
3030
_ds_aggregate,
3131
_ds_shade_categorical,
32+
_pad_degenerate_extent,
3233
)
3334
from spatialdata_plot.pl.render import _warn_groups_ignored_continuous
3435
from tests.conftest import DPI, PlotTester, PlotTesterMeta, _viridis_with_under_over, get_standard_RNG
@@ -1255,3 +1256,34 @@ def test_density_defaults_silent_and_force_datashader(sdata_blobs: SpatialData,
12551256
last = list(out.plotting_tree.values())[-1]
12561257
assert (last.density, last.density_how, last.method) == (True, "linear", "datashader")
12571258
assert not any("ignored when density=True" in str(w.message) for w in recwarn.list)
1259+
1260+
1261+
# ---------------------------------------------------------------------------
1262+
# Zero-extent datashader canvas (#724)
1263+
# ---------------------------------------------------------------------------
1264+
1265+
1266+
def test_pad_degenerate_extent():
1267+
# Regression test for #724: a zero-width extent expands to a unit window centered on the value,
1268+
# and a non-degenerate extent is passed through unchanged (so normal data is unaffected).
1269+
assert _pad_degenerate_extent([5.0, 5.0]) == [4.5, 5.5]
1270+
assert _pad_degenerate_extent([-3.0, -3.0]) == [-3.5, -2.5]
1271+
assert _pad_degenerate_extent([0.0, 10.0]) == [0.0, 10.0]
1272+
1273+
1274+
@pytest.mark.parametrize(
1275+
"coords",
1276+
[
1277+
[[5.0, 5.0]],
1278+
[[5.0, 5.0], [5.0, 5.0], [5.0, 5.0]],
1279+
[[5.0, 0.0], [5.0, 1.0], [5.0, 2.0]],
1280+
],
1281+
ids=["single_point", "coincident_points", "axis_aligned_line"],
1282+
)
1283+
def test_datashader_zero_extent_renders(coords):
1284+
# Regression test for #724: zero-extent point sets crashed the datashader backend with
1285+
# "cannot convert float NaN to integer". They must now render without raising.
1286+
df = pd.DataFrame(np.asarray(coords, dtype=float), columns=["x", "y"])
1287+
sdata = SpatialData(points={"points": PointsModel.parse(df)})
1288+
sdata.pl.render_points("points", method="datashader").pl.show()
1289+
plt.close("all")

0 commit comments

Comments
 (0)