Skip to content

groups= filtering behavior changes silently when na_color='lightgray' is passed explicitly #600

@timtreis

Description

@timtreis

groups= filtering behavior changes silently when na_color="lightgray" is passed explicitly

Passing the documented default value of na_color changes the output.


Environment

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

Problem

When groups=["A"] is specified, non-matching elements are hidden by default. But if na_color="lightgray" (which is the documented default) is passed explicitly, non-matching elements are shown in lightgray instead of hidden.

The filter condition checks an internal default_color_set flag rather than comparing the actual color value:

# render.py ~395
should_filter = _na.default_color_set or _na.is_fully_transparent()

default_color_set is True only when na_color was never set. Once any value is passed — including the same "lightgray" string that is the default — the flag is False, and the filter is skipped.

Consequence: a user who reads the docs, sees na_color defaults to "lightgray", and explicitly writes na_color="lightgray" to be explicit gets a completely different plot from the user who omits the parameter. This is particularly confusing in notebook environments where output diffs are hard to spot.


Minimal reproducible example

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import geopandas as gpd
import anndata as ad
import dask
dask.config.set({"dataframe.query-planning": False})
import spatialdata as sd
from spatialdata.models import ShapesModel, TableModel
from shapely.geometry import box
import spatialdata_plot

shapes = ShapesModel.parse(gpd.GeoDataFrame(
    {"geometry": [box(i,0,i+1,1) for i in range(3)], "radius": [0.5]*3}, geometry="geometry"
))
obs = pd.DataFrame({
    "region": pd.Categorical(["s"]*3),
    "instance_id": [0, 1, 2],
    "cat": pd.Categorical(["A", "B", "C"]),
})
table = TableModel.parse(
    ad.AnnData(X=np.zeros((3,1)), obs=obs),
    region="s", region_key="region", instance_key="instance_id",
)
sdata = sd.SpatialData(shapes={"s": shapes}, tables={"t": table})

fig, (ax0, ax1) = plt.subplots(1, 2)

# groups=["A"] with default na_color
sdata.pl.render_shapes("s", color="cat", groups=["A"]).pl.show(ax=ax0)

# groups=["A"] with na_color="lightgray" (the documented default)
sdata.pl.render_shapes("s", color="cat", groups=["A"], na_color="lightgray").pl.show(ax=ax1)

print(f"default na_color:           {len(ax0.collections)} shape collections (B, C hidden)")
print(f"explicit na_color=lightgray: {len(ax1.collections)} shape collections (B, C visible)")

Expected behaviour

render_shapes("s", color="cat", groups=["A"]) and render_shapes("s", color="cat", groups=["A"], na_color="lightgray") should produce identical output, since "lightgray" is the stated default.

Actual behaviour

default na_color:            2 collections  (B, C hidden)
explicit na_color=lightgray: 3 collections  (B, C shown in lightgray)

Fix sketch

Two possible approaches:

Option 1 — compare the color value, not the flag:

from matplotlib.colors import to_hex
_DEFAULT_NA_HEX = to_hex("lightgray", keep_alpha=False)
should_filter = (
    _na.default_color_set
    or _na.is_fully_transparent()
    or _na.get_hex() == _DEFAULT_NA_HEX
)

Option 2 — document clearly that na_color is an opt-in to show non-matching elements. Any explicit na_color value (including the default) shows non-matching elements. This is a valid design choice but must be documented explicitly in the docstring, since the current docs just describe the color, not the behavioral toggle.


Related

  • finding_groups_nacolor_lightgray_inconsistency.md

Triage tier: Tier 1

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