The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
Hierarchy-aware, interactively editable color palettes for clustered data — built for scRNA-seq clusters/cell subtypes (Seurat, UMAP) but not hard-dependent on Seurat.
LLM was used in this project to speed up the development.
Clusters that belong to the same family / compartment (e.g. subtypes of T cells, subtypes of myeloid cells) can be colored either:
harmonious_style = "sweep"
(default) lays the families along one analogous hue arc with a shared
monotone lightness ramp and muted chroma (e.g. a smooth blue → purple →
rose → tan progression is just one example — a different
seed gives a genuinely different arc: green, teal, red,
gold, …, not a small jitter around one fixed look — and
sweep_anchors lets you specify the exact gradient to sweep
through instead of an automatic one);
harmonious_style = "per_family" gives each family its own
hue and colors its members as graded shades of it.
Color choice is color-theory aware even for random seeds: chroma is
kept inside the displayable gamut and away from the neon and muddy
yellow-green zones, hues are spread with a low-discrepancy sequence, and
each hue gets its natural lightness. When neighbor information is
supplied, families that are adjacent in the embedding
get contrasting hues by default (or analogous, by choice —
neighbor_hues), and within a family spatially-adjacent
clusters are given far-apart shades for local contrast.
simulate_cvd() and the cvd argument of the
plotting functions preview a palette as it appears with color-vision
deficiency:
Every generated color can be overridden by hand, and manual overrides survive re-running the generator (new mode, new seed, edited families) until explicitly reset; while manual overrides are in place the remaining colors auto-fit to the lightness/chroma envelope of your picks.
Core is fully independent of the UI.
as_cluster_data(), detect_families(),
generate_palette(), plot_palette_static(),
plot_swatches(),
export_palette_json()/export_palette_csv(),
import_palette_json(),
downsample_stratified(), color_distance(), and
simulate_cvd() depend only on colorspace,
farver, stats, graphics,
methods, utils, and jsonlite —
all listed in Imports, so a plain
install.packages("palettome") (or
devtools::load_all()) gives you a fully working package
with zero UI packages installed.
The interactive UI is exactly one function,
launch_palettome_ui(). It requires shiny,
plotly, colourpicker, and
shinyjs, all listed under Suggests only. If
they aren’t installed, launch_palettome_ui() fails with a
message telling you which packages to install and reminding you that
every other function still works without them — it never blocks or
breaks the rest of the package.
| Function | Core or UI |
|---|---|
as_cluster_data(),
compute_centroids() |
Core |
detect_families(),
compute_family_dendrogram(), cut_families(),
plot_dendrogram() |
Core |
generate_palette(), set_manual_color(),
reset_overrides(), optimize_color() |
Core |
cluster_colors(), family_colors() |
Core |
color_distance(), simulate_cvd() |
Core |
downsample_stratified() |
Core |
plot_palette_static(),
plot_swatches() |
Core (ggplot2 used opportunistically if installed; base-R fallback otherwise) |
export_palette_json(),
export_palette_csv(),
import_palette_json() |
Core |
launch_palettome_ui() |
UI (Suggests-only: shiny, plotly, colourpicker, shinyjs) |
devtools::load_all() # or library(palettome) once installed
pdata <- as_cluster_data(
my_df,
coord_cols = c("UMAP_1", "UMAP_2"),
cluster_col = "cluster"
)
fam <- detect_families(pdata, k = 4) # auto-detect via hclust
# fam$neighbors carries centroids (or a connectivity matrix) so families and
# clusters can be placed in space — pass it through for neighbor-aware color
session <- generate_palette(
fam$assignment,
mode = "harmonious", harmonious_style = "sweep",
neighbors = fam$neighbors, seed = 1
)
plot_palette_static(pdata, session) # ggplot2 if installed, base R otherwise
plot_swatches(session) # family-grouped swatch legend
export_palette_json(session, "palette.json")Key generate_palette() arguments:
| argument | meaning |
|---|---|
mode |
"harmonious" or "contrast" |
harmonious_style |
"sweep" (one analogous multi-hue sweep) or
"per_family" (one hue per family, shaded) |
sweep_anchors |
(sweep only) 2+ hex colors to interpolate the sweep through, instead of the automatic gradient |
neighbor_hues |
"contrast" / "coherent", or
NULL to follow the mode (contrast→contrast,
harmonious→coherent) |
neighbors |
centroids data frame or connectivity matrix; NULL
disables neighbor-aware placement |
lightness_range, chroma_range |
length-2 numeric, or "auto" (default): derived from
manual picks if any, else from mode/style presets |
seed, hue_range, cvd |
random seed; hue window in degrees;
"none"/"deutan"/"protan"/"tritan" |
To resume editing later, or hand off manual overrides:
session <- set_manual_color(session, cluster = "3", color = "#2E5A87")
# several at once: one color each (or a single color for all); ids not in
# the session are skipped with a warning
session <- set_manual_color(session, cluster = c("4", "5"), color = c("#6E6E6E", "#C9A227"))
session2 <- generate_palette(fam$assignment, session = session, mode = "contrast")
# clusters/families with manual_color == TRUE keep their color unchanged;
# with lightness_range/chroma_range = "auto" the rest fit to the manual picks
optimize_color("#7B00FF", session = session2) # snap a hand-picked hex into the palette envelopeDifferent seeds give genuinely different sweep gradients (not a small jitter around one look), or specify the exact gradient yourself:
generate_palette(fam$assignment, mode = "harmonious", seed = 7) # try a few seeds
generate_palette(fam$assignment, mode = "harmonious",
sweep_anchors = c("#0B3D91", "#FF5733", "#FFD166")) # your own gradientsession$clusters/session$families are
already plain data frames (see the JSON schema below), but for the
common case of just needing the colors themselves – to hand to
Seurat::DimPlot(cols = ...),
ggplot2::scale_color_manual(values = ...), etc. – use:
cluster_colors(session) # named vector: c(c1 = "#4E5716", c2 = "#899925", ...)
cluster_colors(session, format = "data.frame") # data frame: cluster, color
family_colors(session) # same, one row per familyas_cluster_data() has a Seurat method, so
you don’t need to hand-extract embeddings/metadata yourself:
pdata <- as_cluster_data(
seurat_obj,
reduction = "umap", # any reduction name (Seurat::Embeddings())
dims = 1:2, # or 1:3 for a 3D embedding
cluster_col = "seurat_clusters", # any column of seurat_obj[[]]
family_col = NULL # optional: a metadata column with an explicit parent/family label
)
# from here on it's identical to the data-frame path:
fam <- detect_families(pdata, k = 4)
session <- generate_palette(fam$assignment, mode = "harmonious", seed = 1)
# hand the result back to Seurat's own plotting:
Seurat::DimPlot(seurat_obj, reduction = "umap", group.by = "seurat_clusters",
cols = cluster_colors(session))This method requires the Seurat package (Suggests-only,
checked with a friendly error if missing) but nothing else in the
package does – everything downstream of as_cluster_data()
only ever sees the plain palettome_data structure either
input path produces.
install.packages(c("shiny", "plotly", "colourpicker", "shinyjs"))
session <- launch_palettome_ui(pdata) # auto-detects families
session <- launch_palettome_ui(pdata, session = session) # resume a saved session
cluster_colors(session) # your edited colorslaunch_palettome_ui() returns the edited session when
the app closes – either via the Done – return session to
R button or by interrupting R (Esc / Ctrl+C) – so assigning its
result keeps every change made in the UI.
The app shows a WebGL (plotly scattergl)
scatter plot of the embedding (fixed 1:1 x/y aspect ratio, so distances
aren’t visually distorted), a family dendrogram, and a drag-and-drop
compartment panel: drag a cluster chip into a different family bin to
regroup it, hit + Add compartment to make a new (empty,
dashed) bin to drag clusters into, and empty compartments are removed
automatically. Mode, harmonious style, neighbor-hue rule, seed, and a
colorblind simulation preview are all live. Hit 🎲
Randomize next to the seed for a fresh sweep gradient at a
click, or type 2+ comma- separated hex colors into Custom
gradient anchors to sweep through your own colors instead of an
automatic gradient (a live swatch strip previews what you typed). When
the auto light/chroma toggle is switched off, the Lightness- and
Chroma-range sliders each get a swatch strip underneath rendered in the
current palette’s own hue, dimmed outside the selected sub-range – so
the numeric range reads as actual colors, not just two numbers. An
R code panel shows the current cluster and family
colors as named-vector R code
(cluster_colors <- c("3" = "#2E5A87", ...)) with a copy
button, ready to paste into a script. Sessions export as JSON/CSV, and
the final plot exports as a full-resolution PNG.
Click anywhere on a chip or a family’s whole color bar (not just its label) to open the recolor dialog:
optimize_color()).export_palette_json() writes, and
import_palette_json() reads, this structure. It is
deliberately plain and language-agnostic — a future Python/Scanpy
implementation can read and write the exact same file without any
R-specific types.
{
"schema_version": "1.0",
"clusters": [
{ "cluster": "0", "family_id": "F1", "color": "#3E7CB1", "manual_color": false, "n_cells": 1523 },
{ "cluster": "1", "family_id": "F1", "color": "#7FB1DA", "manual_color": true, "n_cells": 842 }
],
"families": [
{ "family_id": "F1", "color": "#2C5F8A", "manual_color": false }
],
"params": {
"mode": "harmonious",
"harmonious_style": "sweep",
"sweep_anchors": null,
"neighbor_hues": "coherent",
"seed": 1,
"lightness_range": [34, 82],
"chroma_range": [24, 46],
"hue_range": [0, 360],
"cvd": "none",
"auto_lightness": true,
"auto_chroma": true
}
}clusters[].color is the per-cluster fill color actually
used for plotting.families[].color is the family’s representative/accent
color: in harmonious mode it’s a point on the family’s part of the sweep
(or the family’s base hue for per_family); in contrast mode
it’s the secondary channel that shows family membership even though
cluster colors themselves are maximally spread apart.manual_color: true marks an entry a person set by hand;
generate_palette() never overwrites these unless
reset_overrides() is called first.n_cells is informational (legend sizing, etc.) and
optional/nullable.params.lightness_range / chroma_range are
always the resolved numeric ranges;
auto_lightness / auto_chroma record whether
they were derived automatically (so a reload can keep
auto-fitting).CSV export (export_palette_csv()) writes the
clusters table (and optionally the families
table) as plain CSV with the same column names.
as_cluster_data() accommodates either input into one
palettome_data object so every other function only has to
know one shape:
as_cluster_data(df, coord_cols = c("UMAP_1","UMAP_2"[,"UMAP_3"]), cluster_col = "cluster", family_col = NULL, cell_id_col = NULL)as_cluster_data(seurat_obj, reduction = "umap", dims = 1:2, cluster_col = "seurat_clusters", family_col = NULL)
(requires the Seurat package, Suggests-only)If family_col/an explicit family is supplied,
detect_families() uses it directly; otherwise it
hierarchically clusters cluster centroids (or a supplied cluster-cluster
connectivity/distance matrix) and lets you pick a cut (k or
h), with plot_dendrogram() for a preview.
Either way, the resulting cluster -> family_id
assignment is just a plain data frame you (or the UI’s drag-and-drop
panel) can freely rewrite afterward — detection is a starting point, not
a final answer.
See inst/benchmark/ for
the full write-up. In short: the browser never receives one point per
cell for interactive editing. downsample_stratified()
precomputes a per-cluster-stratified subsample (default cap 50,000
points) server-side for live dragging/recoloring, and
plotly’s WebGL scattergl trace renders that
subsample smoothly; a full, un-subsampled render is only produced on
final PNG export via plot_palette_static(). Cluster
centroids, hierarchy, and per-cluster cell counts are precomputed once
(compute_centroids()) so hierarchy edits and recoloring
never touch the raw per-cell table at all.
devtools::test()Covers: perceptual-distance thresholds and gamut/muddy-zone
constraints for contrast-mode selection; the harmonious sweep being a
monotone-lightness analogous progression; neighbor_hues
switching adjacent families between analogous and contrasting;
spatially-adjacent subclusters getting far-apart shades;
lightness_range = "auto" fitting to manual picks;
optimize_color() preserving hue; and manual overrides
surviving generate_palette() re-runs (including across a
family reassignment). Plus unit tests for the color-theory primitives
(R/color_theory.R) and adjacency helpers
(R/adjacency.R).
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.