Skip to main content

stanhue logo

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

PyPI Python R License Release

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.py is a standalone script that only needs numpy and scipy.

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=False reproduces 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
  1. 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.
  2. Order. Inside each group the largest category comes first; the rest follow the dendrogram so neighbors stay adjacent.
  3. 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.
  4. 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 Increase n_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 Decrease n_major_groups so they fall into the same group.
Two overlapping categories still look alike Lower overlap_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

stanhue-1.1.1.tar.gz (22.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stanhue-1.1.1-py3-none-any.whl (16.7 kB view details)

Uploaded Python 3

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

Hashes for stanhue-1.1.1.tar.gz
Algorithm Hash digest
SHA256 877ccc2cf40b93a479a0a9a8581a183fc84173737dcd8c0b1727b01d14aee36c
MD5 20736aa67809205cdd551d66e51910ff
BLAKE2b-256 5b5742b8e21186501d376f2b87be6b480ac2a1125de80e005a3478a0e905436f

See more details on using hashes here.

Provenance

The following attestation bundles were made for stanhue-1.1.1.tar.gz:

Publisher: publish.yml on chansigit/stanhue

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for stanhue-1.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 cb6367a4a2569eb9525e3ed34f73801392012a5cd5c51dde087904e96a3542bd
MD5 336e6edf817e3840a12b4af6a28e8aef
BLAKE2b-256 191222eda1e35065d09ae254e52b13830509442f209535b53ade97c5bcc2b94f

See more details on using hashes here.

Provenance

The following attestation bundles were made for stanhue-1.1.1-py3-none-any.whl:

Publisher: publish.yml on chansigit/stanhue

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.1.1 This release

2 files

1.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page