Skip to content

Datashader path ignores na_color transparency for continuous data #565

@timtreis

Description

@timtreis

Bug description

When rendering shapes with the datashader backend and continuous color data, na_color=None (or any transparent na_color) does not work — NaN shapes are rendered as opaque lightgray instead of being invisible.

The matplotlib path handles this correctly.

Root cause

Two issues in _render_shapes (datashader continuous path):

1. Alpha channel stripped from na_color_hex

In src/spatialdata_plot/pl/render.py:533:

na_color_hex = _hex_no_alpha(render_params.cmap_params.na_color.get_hex())

_hex_no_alpha strips the alpha channel, so Color(None) (which stores color="#d3d3d3", alpha="00") becomes "#d3d3d3" — fully opaque grey. This is then passed to ds.tf.shade(nan_agg, cmap=na_color_hex) in _ds_shade_continuous, producing an opaque grey NaN overlay.

2. User's fill_alpha applied to NaN overlay

In src/spatialdata_plot/pl/_datashader.py:252:

shade_kwargs["min_alpha"] = _convert_alpha_to_datashader_range(alpha)

The user's fill_alpha (e.g., alpha=0.7) is applied as min_alpha to the NaN overlay layer. Even if na_color_hex were transparent, this forces the NaN overlay to have nonzero alpha.

Comparison with matplotlib path

The matplotlib path in _get_collection_shape (utils.py) uses na_color.get_hex_with_alpha()with alpha — so na_color=None correctly produces fully transparent fills for NaN shapes.

Suggested fix

  1. Pass na_color_hex with its alpha component to _ds_shade_continuous, or check na_color.alpha == "00" and skip the NaN overlay entirely.
  2. Do not apply fill_alpha as min_alpha on the NaN overlay when na_color is fully transparent.

Minimal reproducer

import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from spatialdata import SpatialData
from spatialdata.models import ShapesModel
import geopandas as gpd
from shapely import box
import spatialdata_plot  # noqa: F401

# Create 20k shapes (triggers datashader auto-switch at >10k)
shapes = gpd.GeoDataFrame(
    geometry=[box(i % 100, i // 100, i % 100 + 1, i // 100 + 1) for i in range(20000)]
)
shapes = ShapesModel.parse(shapes)

# Only 5k have actual values; the rest are NaN
values = np.full(20000, np.nan)
values[:5000] = np.random.default_rng(0).uniform(0, 100, 5000)
shapes["value"] = values

sdata = SpatialData(shapes={"grid": shapes})

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

# Datashader: NaN shapes appear as grey (BUG)
sdata.pl.render_shapes("grid", color="value", na_color=None, method="datashader").pl.show(ax=ax1)
ax1.set_title("datashader: na_color=None (grey = bug)")

# Matplotlib: NaN shapes are transparent (CORRECT)
sdata.pl.render_shapes("grid", color="value", na_color=None, method="matplotlib").pl.show(ax=ax2)
ax2.set_title("matplotlib: na_color=None (correct)")

fig.savefig("/tmp/na_color_bug.png", dpi=100)

Version info

Observed on current main branch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions