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
Metadata
Release files for stanhue 1.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| stanhue-1.1.1.tar.gz | 22.8 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| stanhue-1.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 39.5 kB
Release files / stanhue-1.1.1.tar.gz
| Download URL | stanhue-1.1.1.tar.gz |
|---|---|
| Size | 22.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
877ccc2cf40b93a479a0a9a8581a183fc84173737dcd8c0b1727b01d14aee36c
|
|
BLAKE2b-256 checksum How to use checksums |
5b5742b8e21186501d376f2b87be6b480ac2a1125de80e005a3478a0e905436f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 5, 2026.
Transparency logRelease files / stanhue-1.1.1-py3-none-any.whl
| Download URL | stanhue-1.1.1-py3-none-any.whl |
|---|---|
| Size | 16.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cb6367a4a2569eb9525e3ed34f73801392012a5cd5c51dde087904e96a3542bd
|
|
BLAKE2b-256 checksum How to use checksums |
191222eda1e35065d09ae254e52b13830509442f209535b53ade97c5bcc2b94f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 5, 2026.
Transparency logSecurity for stanhue 1.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for stanhue-1.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 5, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.