Skip to content

Add make_palette and make_palette_from_data for palette generation#581

Merged
timtreis merged 6 commits intomainfrom
feature/issue-210-palette-generation
Apr 6, 2026
Merged

Add make_palette and make_palette_from_data for palette generation#581
timtreis merged 6 commits intomainfrom
feature/issue-210-palette-generation

Conversation

@timtreis
Copy link
Copy Markdown
Member

@timtreis timtreis commented Apr 6, 2026

Summary

Closes #210.

  • Adds make_palette(n) — produce n colors from a source palette, optionally reordered for maximum perceptual contrast or colorblind accessibility.
  • Adds make_palette_from_data(sdata, element, color) — derives categories from a SpatialData element. Supports all make_palette methods plus spatially-aware assignment (spaco-inspired) that maximizes contrast between spatially interleaved categories.
  • Adds dict[str, str] palette support to render_shapes, render_points, and render_labels, enabling reuse of generated palettes across multiple render calls.

API overview

import spatialdata_plot as sdp

# Standalone: just produce N colors
sdp.pl.make_palette(5)
sdp.pl.make_palette(8, palette="okabe_ito")
sdp.pl.make_palette(10, palette="tab10", method="contrast")
sdp.pl.make_palette(6, method="colorblind")

# From data: spatial-aware assignment → dict[str, str]
palette = sdp.pl.make_palette_from_data(sdata, "cells", "cell_type", method="spaco")
palette = sdp.pl.make_palette_from_data(sdata, "cells", "cell_type", method="spaco_colorblind")
palette = sdp.pl.make_palette_from_data(sdata, "cells", "cell_type", palette="okabe_ito", method="spaco")

# Dict palette plugs directly into render calls
sdata.pl.render_shapes("cells", color="cell_type", palette=palette).pl.show()

Methods

Method make_palette make_palette_from_data What it does
"default" Source order (scanpy behaviour)
"contrast" Max perceptual spread (Oklab)
"colorblind" Max spread under worst-case CVD
"protanopia" Max spread under protanopia
"deuteranopia" Max spread under deuteranopia
"tritanopia" Max spread under tritanopia
"spaco" Spatial contrast (normal vision)
"spaco_colorblind" Spatial contrast + worst-case CVD
"spaco_*" Spatial contrast + specific CVD

Implementation details

  • Spaco core reimplemented (~100 lines) to avoid GPL dependency. Uses KDTree-based spatial interlacement + randomized permutation optimizer.
  • Oklab color space for perceptual distance (better than spaco's red-mean RGB).
  • Brettel/Viénot CVD simulation for colorblind-aware optimization.
  • Zero new dependencies — only numpy, scipy, matplotlib (all existing).
  • Okabe-Ito palette built-in as palette="okabe_ito".
  • Any matplotlib colormap name works as palette= (e.g., "tab10", "Set2").

timtreis and others added 5 commits April 6, 2026 09:38
)

Add two public functions for generating categorical color palettes:

- `make_palette(n)`: produce n colors from a source palette, optionally
  reordered for maximum perceptual contrast or colorblind accessibility.
- `make_palette_from_data(sdata, element, color)`: like make_palette but
  derives categories from a SpatialData element and supports spatially-
  aware assignment (spaco-inspired) that maximizes contrast between
  spatially interleaved categories.

Both functions share the same method vocabulary: "default", "contrast",
"colorblind", "protanopia", "deuteranopia", "tritanopia" (non-spatial),
plus "spaco", "spaco_colorblind", "spaco_protanopia", etc. (spatial,
only in make_palette_from_data).

The palette parameter accepts None (scanpy defaults), named palettes
("okabe_ito"), matplotlib colormap names ("tab10"), or explicit color
lists.

Also adds dict[str, str] palette support to render_shapes,
render_points, and render_labels, enabling reuse of generated palettes
across multiple render calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix test_plot_dict_palette_hex_labels to use a hand-built dict
instead of make_palette_from_data, since labels elements don't
have extractable coordinates.

Add 5 CI-generated reference images for palette visual tests.
The hex_labels reference image will be generated on the next CI run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Vectorize _spatial_interlacement with numpy (eliminates Python
  double-loop over cells × neighbors)
- Select only needed columns before .compute() on dask points
  (avoids materializing entire dataframe)
- Fix ListedColormap sampling: use integer indices for qualitative
  colormaps like tab10 instead of linspace (which wraps and duplicates)
- Fix categorical dtype check: use isinstance(dtype, CategoricalDtype)
  instead of fragile hasattr(series, "cat")
- Add n==2 early exit in optimizer (only 2 permutations to try)
- Refactor tests: module-scoped fixtures, parametrize across methods
  and palette sources, merge redundant test classes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The stash application accidentally reverted the resolved_dpi logic
from commit 303140c, causing user figure DPI to be silently
overwritten with rcParams defaults.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 6, 2026

Codecov Report

❌ Patch coverage is 83.95522% with 43 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.28%. Comparing base (e8c262e) to head (165f5a7).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/spatialdata_plot/pl/_palette.py 85.08% 21 Missing and 16 partials ⚠️
src/spatialdata_plot/pl/utils.py 62.50% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #581      +/-   ##
==========================================
+ Coverage   75.63%   76.28%   +0.64%     
==========================================
  Files          10       11       +1     
  Lines        2996     3259     +263     
  Branches      702      756      +54     
==========================================
+ Hits         2266     2486     +220     
- Misses        444      468      +24     
- Partials      286      305      +19     
Files with missing lines Coverage Δ
src/spatialdata_plot/pl/__init__.py 100.00% <100.00%> (ø)
src/spatialdata_plot/pl/basic.py 84.81% <ø> (ø)
src/spatialdata_plot/pl/render_params.py 86.50% <100.00%> (ø)
src/spatialdata_plot/pl/utils.py 66.11% <62.50%> (-0.08%) ⬇️
src/spatialdata_plot/pl/_palette.py 85.08% <85.08%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- _get_labels_from_table now joins on instance keys to guarantee
  coord-label alignment (was returning table rows in table order,
  silently misaligning with element coordinates)
- Error when multiple tables annotate the same element; accept
  table_name= parameter to disambiguate
- Dict palette path in _get_categorical_color_mapping now applies
  groups filtering (was silently ignoring groups= with dict palettes)
- Validate dict palette color values with is_color_like() in
  _type_check_params (was passing invalid colors through to matplotlib)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@timtreis timtreis merged commit 7817c38 into main Apr 6, 2026
8 checks passed
@timtreis timtreis deleted the feature/issue-210-palette-generation branch April 6, 2026 08:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use spaco for picking colors?

2 participants