Skip to content

Commit f7061ef

Browse files
timtreisclaude
andcommitted
Fix datashader path ignoring na_color transparency for continuous data (#565)
When na_color is fully transparent (alpha="00"), skip the NaN overlay entirely in the datashader continuous path for both shapes and points. Previously, _hex_no_alpha stripped the alpha channel before passing to ds.tf.shade, turning transparent "#d3d3d300" into opaque "#d3d3d3". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6b2c484 commit f7061ef

3 files changed

Lines changed: 70 additions & 0 deletions

File tree

src/spatialdata_plot/pl/render.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,9 @@ def _render_shapes(
530530

531531
agg, color_span = _apply_ds_norm(agg, norm)
532532
na_color_hex = _hex_no_alpha(render_params.cmap_params.na_color.get_hex())
533+
# Skip NaN overlay when na_color is fully transparent (#565)
534+
if render_params.cmap_params.na_color.alpha == "00":
535+
nan_agg = None
533536
color_key = _build_color_key(
534537
transformed_element,
535538
col_for_color,
@@ -925,6 +928,9 @@ def _render_points(
925928

926929
agg, color_span = _apply_ds_norm(agg, norm)
927930
na_color_hex = _hex_no_alpha(render_params.cmap_params.na_color.get_hex())
931+
# Skip NaN overlay when na_color is fully transparent (#565)
932+
if render_params.cmap_params.na_color.alpha == "00":
933+
nan_agg = None
928934
color_key = _build_color_key(
929935
transformed_element,
930936
col_for_color,

tests/pl/test_render_points.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,3 +913,25 @@ def test_shade_categorical_cmap_used_when_no_color_key():
913913
shaded_blue = _ds_shade_categorical(agg, None, np.array(["#0000ff"] * 100), alpha=1.0)
914914
# Different color_vector[0] values should produce different shaded output
915915
assert not np.array_equal(np.asarray(shaded_red), np.asarray(shaded_blue))
916+
917+
918+
def test_datashader_na_color_none_no_nan_overlay_points(sdata_blobs: SpatialData):
919+
"""Datashader must not render NaN overlay when na_color is fully transparent.
920+
921+
Regression test for https://github.com/scverse/spatialdata-plot/issues/565.
922+
"""
923+
pts = sdata_blobs.points["blobs_points"].compute()
924+
n = len(pts)
925+
values = np.full(n, np.nan)
926+
values[: n // 2] = np.random.default_rng(0).uniform(0, 100, n // 2)
927+
pts["val"] = values
928+
sdata_blobs.points["blobs_points"] = PointsModel.parse(pts)
929+
930+
fig, ax = plt.subplots()
931+
sdata_blobs.pl.render_points("blobs_points", color="val", na_color=None, method="datashader").pl.show(ax=ax)
932+
933+
assert len(ax.get_images()) == 1, (
934+
f"Expected 1 image (no NaN overlay), got {len(ax.get_images())}; "
935+
"datashader is still rendering an opaque NaN overlay despite na_color=None"
936+
)
937+
plt.close(fig)

tests/pl/test_render_shapes.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,3 +1210,45 @@ def test_datashader_colorbar_range_matches_data(sdata_blobs: SpatialData):
12101210
)
12111211
assert cbar_vmin >= data_min * 0.99 - 0.01, f"Colorbar min ({cbar_vmin:.2f}) is below data min ({data_min:.2f})"
12121212
plt.close(fig)
1213+
1214+
1215+
def test_datashader_na_color_none_no_nan_overlay(sdata_blobs: SpatialData):
1216+
"""Datashader must not render NaN overlay when na_color is fully transparent.
1217+
1218+
Regression test for https://github.com/scverse/spatialdata-plot/issues/565.
1219+
Before the fix, _hex_no_alpha stripped the transparency from na_color=None,
1220+
causing NaN shapes to render as opaque grey.
1221+
"""
1222+
n = len(sdata_blobs.shapes["blobs_circles"])
1223+
values = np.full(n, np.nan)
1224+
values[: n // 2] = np.random.default_rng(0).uniform(0, 100, n // 2)
1225+
sdata_blobs.shapes["blobs_circles"]["val"] = values
1226+
1227+
fig, ax = plt.subplots()
1228+
sdata_blobs.pl.render_shapes("blobs_circles", color="val", na_color=None, method="datashader").pl.show(ax=ax)
1229+
1230+
# With na_color=None the NaN overlay should be skipped: only 1 image (the main data).
1231+
# Before the fix there were 2 images (NaN overlay + main data).
1232+
assert len(ax.get_images()) == 1, (
1233+
f"Expected 1 image (no NaN overlay), got {len(ax.get_images())}; "
1234+
"datashader is still rendering an opaque NaN overlay despite na_color=None"
1235+
)
1236+
plt.close(fig)
1237+
1238+
1239+
def test_datashader_na_color_opaque_renders_nan_overlay(sdata_blobs: SpatialData):
1240+
"""Datashader must still render the NaN overlay when na_color is opaque."""
1241+
n = len(sdata_blobs.shapes["blobs_circles"])
1242+
values = np.full(n, np.nan)
1243+
values[: n // 2] = np.random.default_rng(0).uniform(0, 100, n // 2)
1244+
sdata_blobs.shapes["blobs_circles"]["val"] = values
1245+
1246+
fig, ax = plt.subplots()
1247+
sdata_blobs.pl.render_shapes("blobs_circles", color="val", na_color="red", method="datashader").pl.show(ax=ax)
1248+
1249+
# With opaque na_color, the NaN overlay should be present: 2 images.
1250+
assert len(ax.get_images()) == 2, (
1251+
f"Expected 2 images (NaN overlay + main data), got {len(ax.get_images())}; "
1252+
"NaN overlay is missing for opaque na_color"
1253+
)
1254+
plt.close(fig)

0 commit comments

Comments
 (0)