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

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)

Source distribution for stanhue 1.1.1
File Size Uploaded Provenance
stanhue-1.1.1.tar.gz 22.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for stanhue 1.1.1
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

1.1.1 This release

2 release files Full provenance

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