stanhue
Hierarchical auto-coloring for scatter plots with many categorical labels.
One function. Two inputs. Publication-quality palettes.
Python • R • Algorithm • Custom Palette
Gallery
All plots below are generated fully automatically — no manual color picking.
| PBMC CITE-seq (57 cell types) | Brain (21 cell types, 2.5M cells) |
| Heart (17 cell types) | Meniscus (15 cell types) |
Brain — 382 clusters, 83 auto-groups (click to expand)
Why?
When a scatter plot has dozens of categories (e.g., 30+ cell types on a UMAP), picking colors by hand is tedious and the result is usually ugly. Random palettes scatter similar hues across unrelated groups; sequential palettes make neighbors indistinguishable.
stanhue solves this by inferring group structure from the 2D layout itself, then assigning colors so that:
- Distant groups get different hue families (blue vs red vs green)
- Related categories get adjacent shades within the same family
- Dominant categories (most points) anchor each group's representative color
- Overlapping categories (points mixed together in 2D) get perceptually distant colors, even if they belong to the same group
It returns a simple {label: hex_color} mapping. You bring your own plotting
code.
Quick Start
Python
from stanhue import assign_celltype_colors
colors = assign_celltype_colors(coords, labels)
# {"CD4 Naive": "#a6cee3", "CD8 TEM": "#1f78b4", ...}
R
source("scatter_colormap.R")
colors <- assign_celltype_colors(coords, labels)
# c("CD4 Naive" = "#a6cee3", "CD8 TEM" = "#1f78b4", ...)
Installation
pip (Python)
pip install git+https://github.com/chansigit/stanhue.git
Then from stanhue import assign_celltype_colors.
Script (no package manager needed)
Just copy the script you need:
| Language | File | Dependencies |
|---|---|---|
| Python | stanhue/scatter_colormap.py (standalone, no package needed) |
numpy, scipy |
| R | scatter_colormap.R |
base R only (stats) |
# Python
pip install numpy scipy
# R — no extra packages needed for core functionality
Claude Code plugin
/plugins add chansigit/stanhue
Once installed, mention "stanhue" to Claude and the skill activates automatically.
Python API
from stanhue import assign_celltype_colors, get_groups
# Basic usage
color_map = assign_celltype_colors(
coords, # (n, 2) array — any 2D embedding
labels, # (n,) array — categorical labels
n_major_groups=None, # auto-detect, or set manually
palette=None, # default: PAIRED_PALETTE (12 colors)
overlap_aware=True, # overlapping categories get distant colors
overlap_threshold=0.1,
)
# Inspect grouping structure
groups = get_groups(coords, labels)
# {1: ["CD4 Naive", "CD4 TCM", ...], 2: ["CD8 TEM", ...], ...}
Seurat / SCE convenience (R)
# Seurat
colors <- color_seurat(seurat_obj, reduction = "umap", group_by = "cell_type")
# SingleCellExperiment
colors <- color_sce(sce_obj, dimred = "UMAP", col_name = "cell_type")
Algorithm
flowchart TD
A["🔢 Input: 2D coords + labels"] --> B["1️⃣ Compute centroid per category"]
B --> C["2️⃣ Ward hierarchical clustering on centroids"]
C --> D{"Auto-determine k?"}
D -- "Yes" --> E["Scan dendrogram gaps from<br/>large k → small k, pick first<br/>significant jump (≥ 2× median)"]
D -- "No (user-specified)" --> F["Use provided n_major_groups"]
E --> G["3️⃣ Cut dendrogram into k groups"]
F --> G
G --> H["4️⃣ Order within each group:<br/>• dominant (most cells) → position 0<br/>• rest by dendrogram leaf order"]
H --> I["5️⃣ Sort groups by total cell count (descending)<br/>assign palette offsets with step = 2"]
I --> J{"n_groups > palette_size / 2?"}
J -- "No" --> K["Offsets: 0, 2, 4, 6, 8, 10"]
J -- "Yes" --> L["Interleave: evens first,<br/>then odds, then cycle"]
K --> M["6️⃣ Walk palette from each<br/>group's offset (mod palette_len)"]
L --> M
M --> O["7️⃣ Overlap-aware refinement:<br/>grid-based label mixing → overlap matrix<br/>dominant keeps anchor; overlapping members<br/>pick max CIELAB ΔE slot within the group"]
O --> N["✅ Output: { label: '#hex' }"]
style A fill:#f0f0f0,stroke:#333
style N fill:#d4edda,stroke:#28a745
The algorithm is deterministic — same input always produces the same colors.
Custom Palette
The default is ColorBrewer Paired (12 colors, 6 light/dark pairs). Pass any ordered list of hex colors to override:
warm = ["#fee5d9", "#fcbba1", "#fc9272", "#fb6a4a", "#de2d26", "#a50f15"]
colors = assign_celltype_colors(coords, labels, palette=warm)
The offset logic adapts automatically.
Parameters
| Parameter | Default | Description |
|---|---|---|
coords |
required | 2D coordinates, shape (n, 2) |
labels |
required | Categorical labels, length n |
n_major_groups |
auto | Number of top-level groups. None = auto-detect via relative gap |
palette |
PAIRED_PALETTE |
Ordered hex color list of any length |
overlap_aware |
True |
Categories that overlap in 2D get perceptually distant colors (CIELAB ΔE). Non-overlapping categories keep the hierarchical scheme. False = legacy behavior |
overlap_threshold |
0.1 |
Spatial mixing score (0–1) below which two categories are treated as non-overlapping |
Tips
- Two clusters share a color? Increase
n_major_groups. - Related categories got unrelated colors? Decrease
n_major_groups. - Two overlapping categories still look alike? Lower
overlap_threshold(e.g. 0.05). Colors within a lineage look scrambled? Raise it, or setoverlap_aware=False. - Few categories (≤ palette size)? Colors are handed out in farthest-point order in CIELAB, so 2 categories get two strongly contrasting hues rather than light/dark of the same hue.
- 30+ categories? Consider a larger palette (e.g., 20 colors).
- Works with any 2D embedding: UMAP, tSNE, PCA, PHATE, etc.
- Not limited to single-cell data — any scatter plot with categorical labels.
Input Validation
Both implementations validate inputs and raise clear errors:
coordsmust be(n, 2)numeric without NaN/Inflabelsmust matchcoordsrow count- At least 1 unique label required
License
MIT
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.0.tar.gz.
File metadata
- Download URL: stanhue-1.1.0.tar.gz
- Upload date:
- Size: 21.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a2f85660d29b76825f4dc68065d7ac501ddf010027dbc3544ffbe46baaf1df7
|
|
| MD5 |
8535f9fae2fcde2bb8f1b6d85b1d5a7a
|
|
| BLAKE2b-256 |
3d8d8acc4db273b39214d0c702962e5ed77004af1be7210e811511ffe0d0f238
|
Provenance
The following attestation bundles were made for stanhue-1.1.0.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.0.tar.gz -
Subject digest:
6a2f85660d29b76825f4dc68065d7ac501ddf010027dbc3544ffbe46baaf1df7 - Sigstore transparency entry: 2721420531
- Sigstore integration time:
-
Permalink:
chansigit/stanhue@4f2e63fa29c8c1f9923a4fbdafd99685b1befabc -
Branch / Tag:
refs/heads/main - Owner: https://github.com/chansigit
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4f2e63fa29c8c1f9923a4fbdafd99685b1befabc -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file stanhue-1.1.0-py3-none-any.whl.
File metadata
- Download URL: stanhue-1.1.0-py3-none-any.whl
- Upload date:
- Size: 15.9 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 |
40039e516e61195bdae5681236cb7994aa4da5fb68890faf8c42650d99c57f99
|
|
| MD5 |
97f7d9101afa4c252465576fb71782f8
|
|
| BLAKE2b-256 |
a69d9f659e98699c36af417ed0aa46609c5a17a9cf93b98ccb6475fd44009e21
|
Provenance
The following attestation bundles were made for stanhue-1.1.0-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.0-py3-none-any.whl -
Subject digest:
40039e516e61195bdae5681236cb7994aa4da5fb68890faf8c42650d99c57f99 - Sigstore transparency entry: 2721421536
- Sigstore integration time:
-
Permalink:
chansigit/stanhue@4f2e63fa29c8c1f9923a4fbdafd99685b1befabc -
Branch / Tag:
refs/heads/main - Owner: https://github.com/chansigit
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4f2e63fa29c8c1f9923a4fbdafd99685b1befabc -
Trigger Event:
workflow_dispatch
-
Statement type: