Skip to contents

R-CMD-check test-coverage

Reproducible, provenance-aware color systems for single-cell data visualization.

Why single-cell analyses need persistent color mappings

Palette calls assign colors by position. After a subset or factor reorder, the same cell type can silently receive a different color. sc_color_map() stores a named label-to-color contract once, so every downstream figure reuses the same assignments.

Installation

# install.packages("pak")
pak::pak("xie186/scChromatic")

Example dataset

sc_example is a deterministic synthetic PBMC-like dataset with 720 cells. It contains a UMAP-like embedding plus cell identity, parent lineage, sample, condition, marker expression, signed score, pseudotime, and QC columns.

data(sc_example)
dim(sc_example)
head(sc_example[c("cell_type", "sample", "condition", "MS4A1")])

Quick start

library(scChromatic)
library(ggplot2)

data(sc_example)
cell_types <- levels(sc_example$cell_type)
cell_map <- sc_color_map(sc_example$cell_type, palette = "chromatic")
as_named_colors(cell_map)
sc_color_map_plot(cell_map)

ggplot(sc_example, aes(UMAP1, UMAP2, color = cell_type)) +
  geom_point(size = 0.7) +
  scale_color_sc_map(cell_map)

Stable colors across subsets

full_colors <- as_named_colors(cell_map)[cell_types]
stimulated <- subset(
  sc_example,
  condition == "Stimulated" & cell_type %in% c("CD4 T", "NK", "Monocyte")
)
subset_colors <- as_named_colors(cell_map)[levels(droplevels(stimulated$cell_type))]
stopifnot(identical(subset_colors, full_colors[names(subset_colors)]))

ggplot(stimulated, aes(UMAP1, UMAP2, color = cell_type)) +
  geom_point(size = 0.7) +
  scale_color_sc_map(cell_map)

Relationship-aware maps

A symmetric affinity matrix can guide related annotations toward closer hues, while a soft penalty discourages worst-case separation shortfalls across normal and simulated CVD views. sc_relationship_from_knn() derives such a matrix from within-sample coordinate neighborhoods, then combines samples with equal weight so a large sample does not dominate. Samples missing either label are excluded for that pair, and pairs never observed together are errors by default. Hard locks never move; the stability budget limits changes to other canonical assignments.

affinity <- sc_relationship_from_knn(
  as.matrix(sc_example[c("UMAP1", "UMAP2")]),
  labels = sc_example$cell_type,
  sample = sc_example$sample,
  k = 15
)
relationship_map <- sc_relationship_map(
  affinity,
  canonical = cell_map,
  locked = "B cell",
  stability_budget = 1,
  seed = 2026
)
as_named_colors(relationship_map)

encoding <- sc_redundant_encoding(relationship_map, channel = "shape")
setNames(encoding$shape, encoding$label)

The UMAP-like coordinates above demonstrate the interface only. For quantitative context, prefer appropriately scaled PCA, model-derived latent, or spatial coordinates. Redundant encodings leave colors unchanged; channel = "pattern" returns pattern names and texture groups for compatible plotting layers. Allocate redundancy once for the full label set and reuse that table across subsets; rerunning on a changed label set can change the graph allocation.

Continuous expression and signed scores

ggplot(sc_example, aes(UMAP1, UMAP2, color = MS4A1)) +
  geom_point() +
  scale_color_sc_c("viridis")

ggplot(sc_example, aes(UMAP1, UMAP2, color = signed_score)) +
  geom_point() +
  scale_color_sc_c("chromatic_balance", midpoint = 0)

Lineages, pseudotime, and QC

lineage_map <- sc_hierarchy_map(sc_example$lineage, sc_example$cell_type)

ggplot(sc_example, aes(UMAP1, UMAP2, color = cell_type)) +
  geom_point(size = 0.7) +
  scale_color_sc_map(lineage_map)

ggplot(sc_example, aes(UMAP1, UMAP2, color = pseudotime)) +
  geom_point(size = 0.7) +
  scale_color_sc_c("cividis")

Portable, versioned color maps

path <- tempfile(fileext = ".json")
write_sc_color_map(cell_map, path)
restored_map <- read_sc_color_map(path)
stopifnot(identical(as_named_colors(restored_map), as_named_colors(cell_map)))

JSON and CSV preserve the complete mapping contract, including provenance, hierarchy, focus, aliases, locks, history, context, and seed metadata. The installed Draft 2020-12 JSON Schema at schema/sc-color-map.schema.json makes map files independently validatable outside R; unknown future schema versions are rejected instead of guessed.

Accessibility auditing

sc_palette_audit("chromatic", cvd = c("none", "deutan", "protan", "tritan"))
sc_palette_plot("okabe_ito", view = "both", cvd = c("none", "deutan"))
sc_palette_recommend(12, use = "cell_identity", geometry = "point")

Audit metrics diagnose color separation and background contrast; they are not a guarantee that every viewer or plotting geometry can distinguish every color.

Palette provenance

sc_palette_info() exposes capacity, intended use, source URL, exact source version and commit or archive hash where known, citation, license, derivation status, and frozen audit metrics. The full registry is installed at extdata/palette-provenance.csv; NOTICE records applicable third-party license notices.

Interoperability

as_named_colors() returns the ordinary named vectors expected by Seurat, SingleCellExperiment metadata workflows, ArchR plotting arguments, and ComplexHeatmap annotations. These frameworks remain optional. ArchR compatibility vectors are not distributed. The licensed scico_* LUTs are bundled from pinned sources and require no runtime framework dependency.