stanhue
Automatic, publication-quality colors for scatter plots with many categorical labels.
Infers lineage structure from the 2D layout · keeps related categories in one hue family · pushes overlapping categories apart
Install · Quick start · Overlap-aware · How it works · API · FAQ
PBMC CITE-seq · 57 cell types |
Brain · 21 cell types · 2.5 M cells |
Heart · 17 cell types |
Meniscus · 15 cell types |
Every plot above is colored fully automatically — no manual palette picking.
Stress test: brain, 382 clusters, 83 auto-detected groups
Why stanhue?
With 30+ categories on a UMAP, random palettes scatter similar hues across unrelated groups and sequential palettes make neighbors indistinguishable. Picking colors by hand is tedious and rarely looks good.
stanhue reads the structure out of the 2D layout itself and assigns colors so that:
| 🎨 Distant groups → different hue families | blue vs red vs green |
| 🧬 Related categories → adjacent shades | light blue / dark blue for subtypes of one lineage |
| ⭐ Dominant categories → anchor colors | the biggest subtype carries the family's representative color |
| 🔀 Overlapping categories → far-apart colors | points mixed together in 2D never share a hue (new in 1.1) |
One function, two inputs, returns {label: "#hex"}. You keep your own plotting code.
Installation
Python
pip install stanhue
R — no packages needed beyond base R:
source("https://raw.githubusercontent.com/chansigit/stanhue/main/scatter_colormap.R")
Claude Code plugin
/plugins add chansigit/stanhue
Then mention "stanhue" in a conversation to activate the skill.
Prefer a single file?
stanhue/scatter_colormap.pyis a standalone script that only needsnumpyandscipy.
Quick start
Python
from stanhue import assign_celltype_colors
colors = assign_celltype_colors(adata.obsm["X_umap"], adata.obs["cell_type"])
# {'CD4 Naive': '#a6cee3', 'CD8 TEM': '#1f78b4', ...}
sc.pl.umap(adata, color="cell_type", palette=colors)
Straight from a .h5ad file, without loading the expression matrix:
from stanhue import color_h5ad
colors = color_h5ad("data.h5ad", label_key="cell_type", embedding_key="X_umap")
R
colors <- assign_celltype_colors(umap_coords, cell_types)
# Seurat / SingleCellExperiment shortcuts
colors <- color_seurat(seurat_obj, reduction = "umap", group_by = "cell_type")
colors <- color_sce(sce, dimred = "UMAP", col_name = "cell_type")
DimPlot(seurat_obj, group.by = "cell_type", cols = colors)
Works with any 2D embedding (UMAP, t-SNE, PCA, PHATE, spatial coordinates) and any categorical scatter plot, not just single-cell data.
Overlap-aware coloring
Hierarchical palettes have a blind spot: two categories that sit on top of each other end up with the two most similar colors available. Since 1.1, stanhue measures how much each pair of categories mixes in 2D and pushes overlapping pairs toward perceptually distant colors (CIELAB ΔE), while everything else keeps the hierarchical scheme.
- Few categories (≤ palette size): colors are handed out in farthest-point order in CIELAB space, so 2 categories get two strongly contrasting hues instead of light/dark of the same hue.
- Many categories: overlapping members swap slots within their hue family, or borrow from a neighboring one. Non-overlapping categories keep exactly the colors they had before, so results stay stable.
- Rare populations embedded inside a large one are detected too.
overlap_aware=Falsereproduces the 1.0 behavior bit for bit.
How it works
flowchart LR
A["2D coords<br/>+ labels"] --> B["Centroid<br/>per category"]
B --> C["Ward clustering<br/>→ auto-cut into k groups"]
C --> D["Order within group:<br/>dominant first,<br/>then dendrogram leaf order"]
D --> E["Groups get palette offsets<br/>(step 2 → distinct anchors)"]
E --> F["Overlap-aware refinement:<br/>grid label-mixing → ΔE-max slots"]
F --> G["{ label: '#hex' }"]
style A fill:#f0f4f8,stroke:#999
style G fill:#d4edda,stroke:#28a745
- Group. Category centroids are clustered with Ward linkage. The number of groups is found automatically from the first significant jump in merge distance, or set with
n_major_groups. - Order. Inside each group the largest category comes first; the rest follow the dendrogram so neighbors stay adjacent.
- Anchor. Groups are sorted by size and start at palette positions 0, 2, 4, … so every group's dominant color is distinct. Members walk the palette from there.
- Separate. A grid-based estimate of neighbor-label mixing gives an overlap score per pair. Overlapping categories pick, among their available slots, the color that maximizes overlap-weighted CIELAB ΔE.
The whole pipeline is deterministic: same input, same colors.
API
assign_celltype_colors(coords, labels, n_major_groups=None, palette=None,
return_groups=False, overlap_aware=True, overlap_threshold=0.1)
| Parameter | Default | Description |
|---|---|---|
coords |
required | (n, 2) array, any 2D embedding |
labels |
required | length-n categorical labels |
n_major_groups |
auto | number of top-level groups; None detects it from the dendrogram |
palette |
Paired (12) | ordered list of hex colors, any length |
return_groups |
False |
also return {group_id: [labels...]} for grouped legends |
overlap_aware |
True |
separate overlapping categories; False = pure hierarchical |
overlap_threshold |
0.1 |
mixing score (0–1) below which two categories count as non-overlapping |
Also available: get_groups(coords, labels) to inspect the grouping, color_h5ad(path, ...) for backed h5ad files, and plot_umap / plot_palette helpers (matplotlib). The R script mirrors the same functions plus color_seurat and color_sce.
Custom palette. The default is ColorBrewer Paired (6 light/dark pairs). Any ordered list works; the offset logic adapts to its length:
warm = ["#fee5d9", "#fcbba1", "#fc9272", "#fb6a4a", "#de2d26", "#a50f15"]
colors = assign_celltype_colors(coords, labels, palette=warm)
FAQ
Two clearly separate clusters share a color
Increasen_major_groups. With more categories than palette entries some reuse is unavoidable; a longer palette (e.g. 20 colors) helps.
Related subtypes got unrelated colors
Decreasen_major_groups so they fall into the same group.
Two overlapping categories still look alike
Loweroverlap_threshold (e.g. 0.05). If instead colors within a lineage look scrambled, raise it or set overlap_aware=False.
Input requirements
coords must be numeric (n, 2) without NaN/Inf, labels must have length n. Both implementations validate inputs and raise clear errors.
License
MIT © Sijie Chen
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file stanhue-1.1.1.tar.gz.
File metadata
- Download URL: stanhue-1.1.1.tar.gz
- Upload date:
- Size: 22.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
877ccc2cf40b93a479a0a9a8581a183fc84173737dcd8c0b1727b01d14aee36c
|
|
| MD5 |
20736aa67809205cdd551d66e51910ff
|
|
| BLAKE2b-256 |
5b5742b8e21186501d376f2b87be6b480ac2a1125de80e005a3478a0e905436f
|
Provenance
The following attestation bundles were made for stanhue-1.1.1.tar.gz:
Publisher:
publish.yml on chansigit/stanhue
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stanhue-1.1.1.tar.gz -
Subject digest:
877ccc2cf40b93a479a0a9a8581a183fc84173737dcd8c0b1727b01d14aee36c - Sigstore transparency entry: 2721606367
- Sigstore integration time:
-
Permalink:
chansigit/stanhue@8db47492908256cb0bc723254bb755ec04f50c20 -
Branch / Tag:
refs/tags/v1.1.1 - Owner: https://github.com/chansigit
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8db47492908256cb0bc723254bb755ec04f50c20 -
Trigger Event:
release
-
Statement type:
File details
Details for the file stanhue-1.1.1-py3-none-any.whl.
File metadata
- Download URL: stanhue-1.1.1-py3-none-any.whl
- Upload date:
- Size: 16.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cb6367a4a2569eb9525e3ed34f73801392012a5cd5c51dde087904e96a3542bd
|
|
| MD5 |
336e6edf817e3840a12b4af6a28e8aef
|
|
| BLAKE2b-256 |
191222eda1e35065d09ae254e52b13830509442f209535b53ade97c5bcc2b94f
|
Provenance
The following attestation bundles were made for stanhue-1.1.1-py3-none-any.whl:
Publisher:
publish.yml on chansigit/stanhue
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stanhue-1.1.1-py3-none-any.whl -
Subject digest:
cb6367a4a2569eb9525e3ed34f73801392012a5cd5c51dde087904e96a3542bd - Sigstore transparency entry: 2721606375
- Sigstore integration time:
-
Permalink:
chansigit/stanhue@8db47492908256cb0bc723254bb755ec04f50c20 -
Branch / Tag:
refs/tags/v1.1.1 - Owner: https://github.com/chansigit
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8db47492908256cb0bc723254bb755ec04f50c20 -
Trigger Event:
release
-
Statement type: