Skip to content

fill_alpha is ignored in the datashader render path — shapes always render at full opacity #617

@timtreis

Description

@timtreis

fill_alpha is ignored in the datashader render path — shapes always render at full opacity

Description

When render_shapes uses the datashader path (automatically triggered for >10,000 shapes), fill_alpha has no effect. Shapes always render at full opacity (alpha=255) regardless of the fill_alpha value — including fill_alpha=0.0. The matplotlib path correctly applies fill_alpha. Because the method switch from matplotlib to datashader is automatic and silent, large datasets render differently from small ones with no warning to the user.

Root cause: _ds_shade_categorical passes min_alpha=_convert_alpha_to_datashader_range(alpha) to ds.tf.shade(). min_alpha is a floor on alpha for non-empty pixels, not a scaling factor. Setting min_alpha=0 still allows datashader to render shapes at alpha=255.

Environment

spatialdata-plot: 0.3.4.dev (main, 5cfedc7)
spatialdata: 0.5.0
Python: 3.13

Minimal Reproducible Example

import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import geopandas as gpd
import dask; dask.config.set({"dataframe.query-planning": False})
import spatialdata as sd
from spatialdata.models import ShapesModel
import spatialdata_plot
from shapely.geometry import box
from PIL import Image

# Create enough shapes to trigger datashader (> 10,000)
n = 12000
shapes = ShapesModel.parse(gpd.GeoDataFrame(
    {
        "geometry": [box(i % 100, i // 100, i % 100 + 0.8, i // 100 + 0.8) for i in range(n)],
        "radius": [0.4] * n,
    },
    geometry="geometry",
))
sdata = sd.SpatialData(shapes={"s": shapes})

fig, ax = plt.subplots()
sdata.pl.render_shapes("s", fill_alpha=0.0).pl.show(ax=ax)
fig.savefig("/tmp/ds_alpha_test.png", dpi=20, facecolor="white")

arr = np.array(Image.open("/tmp/ds_alpha_test.png"))
print(f"Max alpha in image: {arr[:, :, 3].max()}")  # 255 instead of 0

Expected vs. Actual

Expected: fill_alpha=0.0 renders shapes invisible (alpha=0). fill_alpha=0.3 renders at 30% opacity, matching the matplotlib path.

Actual: Shapes render at full opacity (alpha=255) even with fill_alpha=0.0. No warning is emitted that datashader ignores fill_alpha.

Fix Sketch

After _datashader_map_aggregate_to_color returns an RGBA array, post-multiply the alpha channel by fill_alpha:

rgba_array[:, :, 3] = (rgba_array[:, :, 3] * fill_alpha).astype(np.uint8)

This makes the datashader path consistent with the matplotlib path for all fill_alpha values. The fix is applied after datashader compositing, so it does not interfere with datashader's internal alpha handling.

Labels: bug, shapes, priority: high


Triage tier: Tier 2

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions