Skip to content

Commit eb0491d

Browse files
authored
perf(color): map categorical shapes colour by code, not per-row hex parse (#737)
1 parent 55f4970 commit eb0491d

2 files changed

Lines changed: 32 additions & 1 deletion

File tree

src/spatialdata_plot/pl/_color.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,16 @@ def to_rgba(self, cmap_params: CmapParams) -> np.ndarray:
777777
via norm+cmap with NaN/non-finite rows painted ``na_color``; an object vector mixes the two.
778778
"""
779779
if self.source_vector is not None: # categorical or none: color_vector holds per-row hex
780-
return np.asarray(colors.to_rgba_array(list(self.color_vector)))
780+
cv = self.color_vector
781+
if isinstance(getattr(cv, "dtype", None), pd.CategoricalDtype):
782+
# categories are hex (resolution fills NaN with an na_color category -> codes >= 0),
783+
# so parse the few categories once and gather by code instead of parsing every row
784+
lut = np.asarray(colors.to_rgba_array(cv.categories.to_numpy()))
785+
return lut[cv.codes]
786+
# object vector (align_to_length pad / uniform na): factorize the distinct colours and
787+
# gather back (factorize keeps any NaN as a real code, so the gather stays in-bounds)
788+
codes, uniq = pd.factorize(np.asarray(cv, dtype=object), sort=False, use_na_sentinel=False)
789+
return np.asarray(colors.to_rgba_array(list(uniq)))[codes]
781790
arr = np.asarray(self.color_vector)
782791
if arr.ndim == 2 and arr.shape[1] in (3, 4) and np.issubdtype(arr.dtype, np.number):
783792
return np.asarray(colors.to_rgba_array(arr))

tests/pl/test_utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,28 @@ def test_precomputed_rgba_passthrough(self):
11271127
arr = np.array([[1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0]])
11281128
np.testing.assert_allclose(ColorSpec("continuous", None, arr).to_rgba(self._params()), arr)
11291129

1130+
def test_codes_gather_matches_per_row_parse(self):
1131+
# the categorical codes-gather and the object factorize-gather must equal to_rgba_array(list(...))
1132+
from matplotlib import colors
1133+
1134+
from spatialdata_plot.pl._color import ColorSpec
1135+
1136+
hexes = ["#e41a1cff", "#377eb8ff", "#4daf4aff", "#984ea3ff"]
1137+
na = "#cccccc00"
1138+
rng = np.random.default_rng(0)
1139+
clean = pd.Categorical.from_codes(rng.integers(0, len(hexes), 2000), categories=hexes)
1140+
with_na = pd.Categorical.from_codes(rng.integers(0, len(hexes) + 1, 2000), categories=[*hexes, na])
1141+
variants = [
1142+
clean, # categorical fast-path
1143+
with_na, # NaN replaced with an na_color category -> codes still >= 0
1144+
clean[:800].remove_unused_categories(), # filtered -> remapped codes
1145+
np.full(500, na, dtype=object), # uniform na (object vector)
1146+
np.concatenate([np.asarray(list(clean[:300]), dtype=object), np.full(100, na, dtype=object)]), # padded
1147+
]
1148+
for cv in variants: # cv doubles as the (only-checked-for-not-None) source_vector
1149+
spec = ColorSpec("categorical" if hasattr(cv, "codes") else "none", cv, cv)
1150+
np.testing.assert_array_equal(spec.to_rgba(self._params()), np.asarray(colors.to_rgba_array(list(cv))))
1151+
11301152

11311153
class TestPercentileNormalize:
11321154
"""PercentileNormalize + _resolve_continuous_norm (issue #370: dim multichannel renders)."""

0 commit comments

Comments
 (0)