| Title: | Hierarchy-Aware, Interactively Editable Color Palettes for Clusters |
|---|---|
| Description: | Assigns perceptually-informed color palettes to clustered data (e.g. scRNA-seq clusters / cell subtypes from 'Seurat' or plain UMAP coordinate tables), where clusters that belong to the same "family" (e.g. subtypes of T cells) can be colored either harmoniously (shared hue, varying shade) or with maximal contrast (with family membership encoded via a secondary channel). Families can be auto-detected from embedding-space hierarchical clustering or supplied explicitly, and every generated color remains individually overridable, with manual overrides preserved across regeneration. Colors are constructed in the HCL space as in Zeileis et al. (2020) <doi:10.18637/jss.v096.i01>, separated using the CIEDE2000 color difference of Sharma, Wu and Dalal (2005) <doi:10.1002/col.20070>, and checked for color-vision deficiency using the simulation of Machado, Oliveira and Fernandes (2009) <doi:10.1109/TVCG.2009.113>. Core algorithms have no UI dependencies; an optional interactive 'shiny' UI is provided separately. |
| Authors: | Chenbin Huang [aut, cre] (ORCID: <https://orcid.org/0000-0002-2179-2217>) |
| Maintainer: | Chenbin Huang <[email protected]> |
| License: | Apache License (== 2.0) |
| Version: | 1.0.1 |
| Built: | 2026-10-20 22:37:02 UTC |
| Source: | https://github.com/cran/palettome |
as_cluster_data() normalizes either a plain data frame of embedding
coordinates + cluster labels, or a Seurat object, into the common
palettome_data structure that every other core function (
detect_families(), generate_palette(), plot_palette_static(), ...)
consumes. This is the only place that needs to know about Seurat, and
Seurat is never required to be installed to use the rest of the package.
as_cluster_data(x, ...) ## S3 method for class 'data.frame' as_cluster_data( x, coord_cols, cluster_col, family_col = NULL, cell_id_col = NULL, ... ) ## S3 method for class 'Seurat' as_cluster_data( x, reduction = "umap", dims = 1:2, cluster_col = "seurat_clusters", family_col = NULL, ... )as_cluster_data(x, ...) ## S3 method for class 'data.frame' as_cluster_data( x, coord_cols, cluster_col, family_col = NULL, cell_id_col = NULL, ... ) ## S3 method for class 'Seurat' as_cluster_data( x, reduction = "umap", dims = 1:2, cluster_col = "seurat_clusters", family_col = NULL, ... )
x |
A |
... |
Passed to methods. |
coord_cols |
Character vector of 2 or 3 column names holding
embedding coordinates (e.g. |
cluster_col |
Name of the column holding cluster/subtype labels. |
family_col |
Optional name of a column holding an explicit
parent/family label. When |
cell_id_col |
Optional name of a column with unique per-row/cell
identifiers. When |
reduction |
Name of the dimensional reduction to pull coordinates
from (passed to |
dims |
Integer vector of length 2 or 3 selecting which dimensions of
|
A palettome_data object: a list with elements
A data frame with columns cell_id, the coordinate
columns (as named in coord_cols), cluster, and optionally
family.
Character vector of coordinate column names in
cells, in order (length 2 or 3).
Logical; whether an explicit family column was supplied.
cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") str(pdata$cells) # With an explicit family column cells$lineage <- substr(cells$cluster, 1, 1) pdata_fam <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster", family_col = "lineage") pdata_fam$has_familycells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") str(pdata$cells) # With an explicit family column cells$lineage <- substr(cells$cluster, 1, 1) pdata_fam <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster", family_col = "lineage") pdata_fam$has_family
The most common thing to actually do with a palettome_session is hand
its colors to something else's plotting call (e.g. Seurat::DimPlot(cols = cluster_colors(session)), ggplot2::scale_color_manual(values = cluster_colors(session))). This pulls just the cluster -> color
mapping out of the fuller session object in whichever shape is
convenient, without needing to know the session's internal structure.
cluster_colors(session, format = c("vector", "data.frame"))cluster_colors(session, format = c("vector", "data.frame"))
session |
A |
format |
|
A named character vector, or a data frame, depending on format.
assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) cluster_colors(session) cluster_colors(session, format = "data.frame")assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) cluster_colors(session) cluster_colors(session, format = "data.frame")
Thin wrapper over farver::compare_colour() in CIE Lab space, used both
by the contrast-mode selector and by the package tests to check that
generated colors respect a minimum perceptual separation.
color_distance(hex, method = "CIE2000")color_distance(hex, method = "CIE2000")
hex |
Character vector of hex colors. |
method |
Distance method passed to |
A symmetric numeric matrix of pairwise distances, named by hex.
d <- color_distance(c("#1B9E77", "#D95F02", "#7570B3")) round(d, 1)d <- color_distance(c("#1B9E77", "#D95F02", "#7570B3")) round(d, 1)
Precomputes cluster centroids (mean coordinate per cluster), cell counts,
and (if present) the family each cluster belongs to. Every downstream
interactive operation (recoloring, regrouping) should be able to work off
this small summary rather than touching pdata$cells again.
compute_centroids(pdata)compute_centroids(pdata)
pdata |
A |
A data frame with one row per cluster: cluster, one column per
coordinate named <coord>_centroid, n_cells, and family if
pdata$has_family is TRUE.
cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") compute_centroids(pdata)cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") compute_centroids(pdata)
Computes cluster centroids (or uses a supplied connectivity/distance
matrix) and runs hierarchical clustering on them. Kept separate from
detect_families() so a UI can render a dendrogram preview once and then
re-cut it at different k/h cheaply via cut_families(), without
recomputing distances every time the user drags a cut-height slider.
compute_family_dendrogram( pdata, dist_method = "euclidean", hclust_method = "average", connectivity = NULL )compute_family_dendrogram( pdata, dist_method = "euclidean", hclust_method = "average", connectivity = NULL )
pdata |
A |
dist_method |
Distance metric for centroid-based clustering, passed
to |
hclust_method |
Agglomeration method, passed to |
connectivity |
Optional square cluster-by-cluster connectivity or
similarity matrix (e.g. from a shared-nearest-neighbor graph), with row
and column names matching cluster ids. When supplied, it is converted to
a distance matrix ( |
A list with hclust (an stats::hclust object), centroids
(from compute_centroids()), and clusters (the cluster id order used).
cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") dendro <- compute_family_dendrogram(pdata) dendro$hclustcells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") dendro <- compute_family_dendrogram(pdata) dendro$hclust
Cut a precomputed dendrogram into families
cut_families(dendro, k = NULL, h = NULL)cut_families(dendro, k = NULL, h = NULL)
dendro |
A list as returned by |
k |
Desired number of families (mutually exclusive with |
h |
Height at which to cut the dendrogram (mutually exclusive with
|
A data frame with columns cluster and family_id (character,
"F1", "F2", ...).
cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") dendro <- compute_family_dendrogram(pdata) cut_families(dendro, k = 3)cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") dendro <- compute_family_dendrogram(pdata) cut_families(dendro, k = 3)
If pdata carries an explicit family column (i.e. pdata$has_family is
TRUE), that mapping is used as-is and no clustering is performed.
Otherwise, families are auto-detected by hierarchical clustering of
cluster centroids (or a supplied connectivity graph). Either way, the
result is meant to be a starting point: it is a plain
cluster -> family_id data frame that a UI can freely rewrite by hand
(drag-to-regroup) afterward.
detect_families( pdata, k = NULL, h = NULL, dist_method = "euclidean", hclust_method = "average", connectivity = NULL )detect_families( pdata, k = NULL, h = NULL, dist_method = "euclidean", hclust_method = "average", connectivity = NULL )
pdata |
A |
k, h
|
See |
dist_method |
Distance metric for centroid-based clustering, passed
to |
hclust_method |
Agglomeration method, passed to |
connectivity |
Optional square cluster-by-cluster connectivity or
similarity matrix (e.g. from a shared-nearest-neighbor graph), with row
and column names matching cluster ids. When supplied, it is converted to
a distance matrix ( |
A palettome_families object: a list with
data frame with columns cluster, family_id.
"explicit", "hclust", or "graph".
The compute_family_dendrogram() result, or NULL if
method == "explicit"; kept so a UI can preview/re-cut the
dendrogram without recomputation.
Neighbor information ready to pass to
generate_palette() – the supplied connectivity matrix if any,
otherwise the cluster centroids – so neighbor-aware coloring works
without recomputing anything.
cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") fam <- detect_families(pdata, k = 3) fam$method fam$assignmentcells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") fam <- detect_families(pdata, k = 3) fam$method fam$assignment
Precomputing a small, cluster-stratified subsample is the core trick that
keeps hierarchy editing and recoloring responsive on multi-million-cell
datasets: interactive redraws use this subsample, and only a final export
needs to touch every cell. See inst/benchmark/ for a benchmark
justifying the approach and default threshold.
downsample_stratified(pdata, max_n = 50000, min_per_cluster = 20, seed = 1)downsample_stratified(pdata, max_n = 50000, min_per_cluster = 20, seed = 1)
pdata |
A |
max_n |
Target total number of points after subsampling. If
|
min_per_cluster |
Minimum points kept per cluster even if that over-represents small clusters relative to a uniform per-cluster share, so rare clusters stay visible. |
seed |
Random seed for reproducible subsampling. The caller's random number state is restored on exit. |
A palettome_data object with a (possibly) reduced cells table.
cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") small <- downsample_stratified(pdata, max_n = 60, min_per_cluster = 5) table(small$cells$cluster)cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") small <- downsample_stratified(pdata, max_n = 60, min_per_cluster = 5) table(small$cells$cluster)
Export cluster and/or family color tables as CSV
export_palette_csv(session, clusters_file, families_file = NULL)export_palette_csv(session, clusters_file, families_file = NULL)
session |
A |
clusters_file |
Path to write the cluster -> color table to (columns
|
families_file |
Optional path to also write the family -> color
table to (columns |
Invisibly, TRUE.
assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) clusters_file <- file.path(tempdir(), "clusters.csv") families_file <- file.path(tempdir(), "families.csv") export_palette_csv(session, clusters_file, families_file) read.csv(clusters_file) unlink(c(clusters_file, families_file))assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) clusters_file <- file.path(tempdir(), "clusters.csv") families_file <- file.path(tempdir(), "families.csv") export_palette_csv(session, clusters_file, families_file) read.csv(clusters_file) unlink(c(clusters_file, families_file))
Writes the stable, language-agnostic JSON contract described in the
package README (schema_version, clusters, families, params) so a
future non-R (e.g. Python/Scanpy) frontend could read/write the same
file.
export_palette_json(session, file)export_palette_json(session, file)
session |
A |
file |
Path to write to. |
Invisibly, the JSON string that was written.
assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) path <- file.path(tempdir(), "palette.json") export_palette_json(session, path) unlink(path)assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) path <- file.path(tempdir(), "palette.json") export_palette_json(session, path) unlink(path)
Same as cluster_colors(), but for each family's representative/accent
color (the harmonious-mode base hue, or the contrast-mode secondary
channel) rather than per-cluster fill colors.
family_colors(session, format = c("vector", "data.frame"))family_colors(session, format = c("vector", "data.frame"))
session |
A |
format |
|
A named character vector, or a data frame, depending on format.
assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) family_colors(session) family_colors(session, format = "data.frame")assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) family_colors(session) family_colors(session, format = "data.frame")
The core color-generation entry point. Given a cluster -> family assignment, it places one base hue per family and then colors each cluster, in one of two modes:
generate_palette( assignment, session = NULL, mode = c("harmonious", "contrast"), harmonious_style = c("sweep", "per_family"), sweep_anchors = NULL, neighbor_hues = NULL, neighbors = NULL, seed = 1, lightness_range = "auto", chroma_range = "auto", hue_range = c(0, 360), cvd = c("none", "deutan", "protan", "tritan"), n_cells = NULL )generate_palette( assignment, session = NULL, mode = c("harmonious", "contrast"), harmonious_style = c("sweep", "per_family"), sweep_anchors = NULL, neighbor_hues = NULL, neighbors = NULL, seed = 1, lightness_range = "auto", chroma_range = "auto", hue_range = c(0, 360), cvd = c("none", "deutan", "protan", "tritan"), n_cells = NULL )
assignment |
A data frame with columns |
session |
An existing |
mode |
|
harmonious_style |
|
sweep_anchors |
Optional character vector of 2+ hex colors (only
used when |
neighbor_hues |
|
neighbors |
Optional neighbor information used to order families and
clusters in space: a data frame of centroids ( |
seed |
Random seed, so the same inputs always give the same colors. The caller's random number state is restored on exit. |
lightness_range, chroma_range
|
Length-2 numeric ranges in HCL space,
or |
hue_range |
Length-2 numeric range in degrees to draw hues from. |
cvd |
|
n_cells |
Optional named integer vector or |
mode = "harmonious" – a coordinated palette in HCL space.
harmonious_style = "sweep" (default) lays families along a single
analogous hue arc with a shared monotone lightness ramp and muted
chroma (a smooth blue -> purple -> rose -> tan style sweep);
harmonious_style = "per_family" gives each family its own hue and
colors its members as graded shades of it.
mode = "contrast" – every cluster gets a maximally-distinguishable
color via greedy max-min selection in Lab space; family membership is
carried by $families$color as a secondary channel.
Neighbor-awareness: when neighbors is supplied, families that are
adjacent in the embedding/graph get contrasting hues by default
(neighbor_hues = "contrast", the default in contrast mode) or
analogous hues (neighbor_hues = "coherent", the default in
harmonious mode); and within a family, spatially-adjacent clusters are
given far-apart lightness for local contrast.
Passing a previous session makes this incremental: any cluster or
family with manual_color == TRUE keeps its color, the generator treats
those as fixed and avoids clashing with them, and with
lightness_range/chroma_range left at "auto" the non-manual colors
are regenerated inside the L/C envelope implied by the manual picks.
A palettome_session object (see the package README for the JSON
schema).
cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") fam <- detect_families(pdata, k = 3) # Harmonious: one coordinated hue sweep, neighbor-aware session <- generate_palette(fam$assignment, mode = "harmonious", neighbors = fam$neighbors, seed = 1) cluster_colors(session) # Different seeds give different sweeps cluster_colors(generate_palette(fam$assignment, seed = 7)) # Contrast: every cluster maximally distinguishable session_c <- generate_palette(fam$assignment, mode = "contrast", seed = 1) cluster_colors(session_c) # Sweep along a gradient you choose cluster_colors(generate_palette(fam$assignment, sweep_anchors = c("#0B3D91", "#FF5733")))cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") fam <- detect_families(pdata, k = 3) # Harmonious: one coordinated hue sweep, neighbor-aware session <- generate_palette(fam$assignment, mode = "harmonious", neighbors = fam$neighbors, seed = 1) cluster_colors(session) # Different seeds give different sweeps cluster_colors(generate_palette(fam$assignment, seed = 7)) # Contrast: every cluster maximally distinguishable session_c <- generate_palette(fam$assignment, mode = "contrast", seed = 1) cluster_colors(session_c) # Sweep along a gradient you choose cluster_colors(generate_palette(fam$assignment, sweep_anchors = c("#0B3D91", "#FF5733")))
Import a palette session from JSON
import_palette_json(file)import_palette_json(file)
file |
Path to a JSON file written by |
A palettome_session.
assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) path <- file.path(tempdir(), "palette.json") export_palette_json(session, path) session_back <- import_palette_json(path) identical(cluster_colors(session_back), cluster_colors(session)) unlink(path)assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) path <- file.path(tempdir(), "palette.json") export_palette_json(session, path) session_back <- import_palette_json(path) identical(cluster_colors(session_back), cluster_colors(session)) unlink(path)
This is the only function in the package that requires UI packages
(shiny, plotly, colourpicker, shinyjs). All of them are Suggests-only, so
installing/loading the palettome package itself never requires them, and
every other function (detect_families(), generate_palette(),
plot_palette_static(), export_palette_json(), ...) works without
them. If any are missing, this fails with a friendly message rather than
an obscure error, and points out that the rest of the package still
works.
launch_palettome_ui( pdata, families = NULL, session = NULL, k = NULL, h = NULL, connectivity = NULL, preview_max_n = 50000, ... )launch_palettome_ui( pdata, families = NULL, session = NULL, k = NULL, h = NULL, connectivity = NULL, preview_max_n = 50000, ... )
pdata |
A |
families |
Optional: a |
session |
Optional: a |
k, h
|
Passed to |
connectivity |
Optional cluster-by-cluster connectivity/distance
matrix passed to |
preview_max_n |
Passed to |
... |
Passed on to |
Invisibly, the edited palettome_session once the app closes
(via the "Done" button, or by interrupting R with Esc / Ctrl+C), so
session <- launch_palettome_ui(...) keeps every change made in the
UI. The app also shows the current colors as pasteable named-vector R
code.
cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") if (interactive()) { session <- launch_palettome_ui(pdata, k = 3) cluster_colors(session) }cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") if (interactive()) { session <- launch_palettome_ui(pdata, k = 3) cluster_colors(session) }
Feature companion to manual recoloring: keeps the hue you chose but pulls
its lightness and chroma to something in-gamut and coherent – either
matched to the lightness/chroma distribution of an existing session
(so a hand-picked color sits with the rest), or, with no session, toward
the natural lightness for that hue and a pleasing (non-neon) chroma.
optimize_color(color, session = NULL, target = c("auto", "muted", "vivid"))optimize_color(color, session = NULL, target = c("auto", "muted", "vivid"))
color |
A hex color string. |
session |
Optional |
target |
|
A hex color string with the same hue, adjusted L/C.
assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) optimize_color("#7B00FF") optimize_color("#7B00FF", session = session, target = "muted")assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) optimize_color("#7B00FF") optimize_color("#7B00FF", session = session, target = "muted")
A dependency-light preview: uses dendextend
to color branches by family when it is installed, and falls back to base
stats::plot.hclust() with stats::rect.hclust() otherwise. Either way
this only needs base R + stats to run.
plot_dendrogram(dendro, k = NULL, h = NULL)plot_dendrogram(dendro, k = NULL, h = NULL)
dendro |
A list from |
k, h
|
Passed to |
Invisibly, the (possibly colored) dendrogram object that was plotted.
cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") fam <- detect_families(pdata, k = 3) plot_dendrogram(fam$dendro, k = 3)cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") fam <- detect_families(pdata, k = 3) plot_dendrogram(fam$dendro, k = 3)
Works with zero UI packages installed: uses ggplot2 for a nicer plot if
it happens to be available (it is Suggests-only, never required), and
otherwise falls back to base graphics::plot(). Either way, no Shiny/UI
dependency is ever touched.
plot_palette_static( pdata, session, cvd = "none", point_size = 0.6, alpha = 0.7 )plot_palette_static( pdata, session, cvd = "none", point_size = 0.6, alpha = 0.7 )
pdata |
A |
session |
A |
cvd |
|
point_size, alpha
|
Passed through to the plotting backend. |
For large datasets, consider subsampling pdata$cells (e.g. stratified
per cluster) before calling this for interactive-style previews, and only
plotting the full data for a final export; see inst/benchmark/ for the
rendering-strategy discussion behind this recommendation.
A ggplot object if ggplot2 is installed, otherwise NULL
(invisibly) after drawing directly to the current base graphics device.
cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") fam <- detect_families(pdata, k = 3) session <- generate_palette(fam$assignment, neighbors = fam$neighbors, seed = 1) plot_palette_static(pdata, session)cells <- data.frame( x = rnorm(120, rep(c(0, 1, 6, 7, 3, 4), each = 20)), y = rnorm(120, rep(c(0, 1, 0, 1, 6, 7), each = 20)), cluster = rep(c("T1", "T2", "B1", "B2", "M1", "M2"), each = 20) ) pdata <- as_cluster_data(cells, coord_cols = c("x", "y"), cluster_col = "cluster") fam <- detect_families(pdata, k = 3) session <- generate_palette(fam$assignment, neighbors = fam$neighbors, seed = 1) plot_palette_static(pdata, session)
A base-graphics-only (no ggplot2/Shiny needed) legend figure: one row per cluster, colored swatch + label, visually grouped and separated by family, with the family's own color shown as a header bar. Suitable for exporting as PNG/SVG alongside the main plot.
plot_swatches(session, cvd = "none", order_by = c("lightness", "id"))plot_swatches(session, cvd = "none", order_by = c("lightness", "id"))
session |
A |
cvd |
|
order_by |
|
Invisibly, NULL; draws to the current graphics device.
assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) plot_swatches(session) plot_swatches(session, cvd = "deutan")assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) plot_swatches(session) plot_swatches(session, cvd = "deutan")
Reset manual color overrides
reset_overrides(session, cluster = TRUE, family = TRUE)reset_overrides(session, cluster = TRUE, family = TRUE)
session |
A |
cluster, family
|
Character vector of ids to reset, or |
The updated palettome_session (colors are left as-is; only the
manual_color flags are cleared – call generate_palette() again to
recompute the now-unlocked colors).
assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) session <- set_manual_color(session, cluster = "c1", color = "#2E5A87") session <- reset_overrides(session) any(session$clusters$manual_color)assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) session <- set_manual_color(session, cluster = "c1", color = "#2E5A87") session <- reset_overrides(session) any(session$clusters$manual_color)
Sets color and flags manual_color = TRUE for the given cluster and/or
family in a session. Manually-colored entries are left untouched by later
generate_palette() calls (until reset with reset_overrides()).
set_manual_color(session, cluster = NULL, family = NULL, color)set_manual_color(session, cluster = NULL, family = NULL, color)
session |
A |
cluster, family
|
Cluster ids / family ids to recolor (either or both; at least one required). May be vectors to recolor several at once. |
color |
Hex color string(s): either a single color applied to every
target, or one color per id (recycled separately for |
The updated palettome_session.
assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) session <- set_manual_color(session, cluster = "c1", color = "#2E5A87") session$clusters[, c("cluster", "color", "manual_color")] # Several clusters at once, one color each session <- set_manual_color(session, cluster = c("c3", "c4"), color = c("#2E5A87", "#6E6E6E")) # Manual picks survive regeneration session <- generate_palette(assignment, session = session, mode = "contrast") cluster_colors(session)[["c1"]]assignment <- data.frame(cluster = paste0("c", 1:4), family_id = rep(c("F1", "F2"), each = 2)) session <- generate_palette(assignment, mode = "harmonious", seed = 1) session <- set_manual_color(session, cluster = "c1", color = "#2E5A87") session$clusters[, c("cluster", "color", "manual_color")] # Several clusters at once, one color each session <- set_manual_color(session, cluster = c("c3", "c4"), color = c("#2E5A87", "#6E6E6E")) # Manual picks survive regeneration session <- generate_palette(assignment, session = session, mode = "contrast") cluster_colors(session)[["c1"]]
Preview-only helper: does not mutate a session, just maps colors through
colorspace::deutan()/colorspace::protan()/colorspace::tritan() so a
UI can toggle a colorblind-safe preview of an already-generated palette.
simulate_cvd(hex, type = c("deutan", "protan", "tritan"), severity = 1)simulate_cvd(hex, type = c("deutan", "protan", "tritan"), severity = 1)
hex |
Character vector of hex colors. |
type |
One of |
severity |
Severity in |
Character vector of simulated hex colors, same length as hex.
pal <- c("#1B9E77", "#D95F02", "#7570B3", "#E7298A") simulate_cvd(pal, type = "deutan") simulate_cvd(pal, type = "tritan", severity = 0.5)pal <- c("#1B9E77", "#D95F02", "#7570B3", "#E7298A") simulate_cvd(pal, type = "deutan") simulate_cvd(pal, type = "tritan", severity = 0.5)