Nothing
#' Internal helper to create a 3D dimension reduction plot using plotly
#'
#' @description
#' Renders an interactive 3D scatter plot via **plotly** for dimension reduction
#' data with three ordination axes. Called automatically by `DimPlotAtomic()`
#' when `length(dims) == 3`.
#'
#' The function supports two visualisation modes:
#' - **group_by** — discrete group colouring with one trace per group, matching
#' the 2D legend behaviour via `label_use`.
#' - **features** — continuous colour scale with a `plotly` colorscale bar.
#'
#' Graphs / networks are rendered as 3D line segments (with NA separators for
#' line breaks). Lineage curves are fitted via `stats::loess()` per lineage
#' column and drawn as smoothed paths. For datasets exceeding 100 000 points,
#' per-point hover text is disabled to reduce the JSON payload.
#'
#' @section Architecture:
#' **DimPlotAtomic3D** executes the following steps:
#'
#' 1. **plotly availability check** — errors early if the `plotly` package is
#' not installed.
#' 2. **Axis labels** — defaults `xlab` / `ylab` / `zlab` from `dims`.
#' 3. **Marker size scaling** — multiplies `pt_size` by 2 to convert from
#' ggplot2 mm units to plotly pixel units.
#' 4. **Large-data detection** — disables per-point hover text when
#' `nrow(data) > 1e5`.
#' 5. **Base plotly object** — `plotly::plot_ly()` initialisation.
#' 6. **Graph / network edges** (if `graph` is provided) — resolves the graph
#' source (`@attribute`, Graph object, matrix, data.frame, column indices,
#' or column names), aligns row/column names with `data`, sets upper-triangle
#' and zero entries to `NA`, melts into an edge data frame, builds x/y/z
#' coordinate vectors (interleaved with NA for line breaks), and adds
#' `scatter3d` line traces.
#' 7. **Main scatter traces**:
#' - *group_by mode*: iterates over `labels_tb` entries, creating one trace
#' per group with the resolved colour. NA groups use `bg_color`. Legend
#' labels use `label_use` (with count annotations if `show_stat = TRUE`).
#' - *features mode*: builds a plotly-style colorscale from the continuous
#' palette. NA points are rendered first in `bg_color` without a colour
#' bar; non-NA points follow with a `showscale = TRUE` colour bar.
#' 8. **Highlight** — if `highlight` is specified, highlighted points are
#' plotted as open circles with increased marker size (to account for
#' stroke). Supports `TRUE` (all points), a filter expression string, or
#' a vector of row names / numeric indices.
#' 9. **Lineages** — each lineage column is validated, trimmed to
#' `[lineages_trim[1], lineages_trim[2]]` quantiles, ordered by value,
#' and a `loess(span = lineages_span, degree = 2)` smooth is fitted per
#' dimension. The smoothed curve is rendered as a 3D line trace.
#' 10. **Labels** — when `label = TRUE` and `group_by` is set, group median
#' coordinates are computed via `aggregate()`. In 3D, plotly does not
#' support text background/outline, so `label_bg` is used directly as
#' the text colour. Text size is scaled by 3× for plotly.
#' 11. **Layout** — subtitle is composited with an optional "Showing N sampled
#' points" note. The full title is rendered as HTML (`<br><sup>...</sup>`).
#' The scene sets `aspectratio = list(x = 1, y = 1, z = 1)` and axis
#' titles.
#'
#' @inheritParams DimPlotAtomic
#' @param colorby A character string naming the column used for colour mapping
#' (either the `group_by` factor or the `features` numeric column).
#' @param colors A named character vector mapping group levels to hex colours.
#' Only used in group_by mode.
#' @param feat_colors_value A numeric vector of the colour-scale anchor values
#' for feature mode, as returned by `prepare_continuous_color_scale()`.
#' @param label_use A character vector of formatted legend labels (with
#' optional count annotations) used in group_by mode.
#' @param labels_tb A table of group counts for trace iteration in group_by
#' mode.
#' @param keep_empty_group A logical value. If `TRUE`, empty factor levels are
#' preserved; traces with zero rows are still added to the legend.
#' @param n_sampled An integer indicating how many points were retained after
#' down-sampling (displayed in the subtitle). `NULL` if no sampling occurred.
#' @keywords internal
DimPlotAtomic3D <- function(
data,
dims,
group_by = NULL,
features = NULL,
colorby,
colors = NULL,
feat_colors_value = NULL,
label_use = NULL,
labels_tb = NULL,
keep_empty_group = FALSE,
bg_color = "grey80",
color_name = "",
pt_size = NULL,
pt_alpha = 1,
show_stat = TRUE,
title = NULL,
subtitle = NULL,
xlab = NULL,
ylab = NULL,
label = FALSE,
label_insitu = FALSE,
label_size = 4,
label_fg = "white",
label_bg = "black",
highlight = NULL,
highlight_color = "black",
highlight_size = 1,
highlight_stroke = 0.8,
highlight_alpha = 1,
graph = NULL,
edge_size = c(0.05, 0.5),
edge_alpha = 0.1,
edge_color = "grey40",
lineages = NULL,
lineages_trim = c(0.01, 0.99),
lineages_span = 0.75,
lineages_palette = "Dark2",
lineages_palcolor = NULL,
palette = "Spectral",
palcolor = NULL,
palreverse = FALSE,
n_sampled = NULL
) {
if (!requireNamespace("plotly", quietly = TRUE)) {
stop(
"Package 'plotly' is required for 3D plots. Install it with: install.packages('plotly')"
)
}
xlab <- xlab %||% dims[1]
ylab <- ylab %||% dims[2]
zlab <- dims[3]
# Scale pt_size for plotly (plotly marker sizes are in pixels, ggplot sizes are in mm)
stopifnot(
"[DimPlot] pt_size must be a numeric value for 3D plots." = is.numeric(
pt_size
)
)
marker_size <- max(pt_size * 2, 1)
# Disable per-point hover text for large datasets to reduce JSON payload
large_data <- nrow(data) > 1e5
hover_mode <- if (large_data) "none" else "text"
p <- plotly::plot_ly()
## Graph / network edges (rendered first so they appear behind points)
if (!is.null(graph)) {
if (
is.character(graph) && length(graph) == 1 && startsWith(graph, "@")
) {
graph_name <- substring(graph, 2)
net_mat <- attr(data, graph_name)
if (is.null(net_mat)) {
stop(paste0(
"[DimPlot] The graph '",
graph_name,
"' is not found in the data attributes."
))
}
} else if (inherits(graph, "Graph")) {
net_mat <- as.matrix(graph)
} else if (
is.matrix(graph) ||
is.data.frame(graph) ||
inherits(graph, "dgCMatrix")
) {
net_mat <- graph
} else if (is.numeric(graph)) {
graph_cols <- colnames(data)[graph]
net_mat <- data[graph_cols]
} else if (is.character(graph)) {
net_mat <- data[graph]
} else {
stop(
"[DimPlot] The 'graph' should be a matrix, data.frame, Graph object, indexes, or column names."
)
}
if (!is.matrix(net_mat)) {
net_mat <- as.matrix(net_mat)
}
if (!is.null(rownames(net_mat)) && !is.null(colnames(net_mat))) {
net_mat <- net_mat[rownames(data), rownames(data)]
} else if (is.null(rownames(net_mat))) {
stop("The graph matrix should have rownames and colnames.")
} else if (is.null(rownames(data))) {
stop("'graph' requires the input data to have rownames.")
}
net_mat[net_mat == 0] <- NA
net_mat[upper.tri(net_mat)] <- NA
net_df <- reshape2::melt(
net_mat,
na.rm = TRUE,
stringsAsFactors = FALSE
)
net_df$value <- as.numeric(net_df$value)
net_df$Var1 <- as.character(net_df$Var1)
net_df$Var2 <- as.character(net_df$Var2)
# Build edge coordinates with NA separators for line breaks
edge_x <- as.vector(rbind(
data[net_df$Var1, dims[1]],
data[net_df$Var2, dims[1]],
NA
))
edge_y <- as.vector(rbind(
data[net_df$Var1, dims[2]],
data[net_df$Var2, dims[2]],
NA
))
edge_z <- as.vector(rbind(
data[net_df$Var1, dims[3]],
data[net_df$Var2, dims[3]],
NA
))
# Scale edge width: use mean of edge_size range, scaled for plotly
edge_width <- mean(edge_size) * 10
p <- plotly::add_trace(
p = p,
x = edge_x,
y = edge_y,
z = edge_z,
type = "scatter3d",
mode = "lines",
line = list(color = edge_color, width = edge_width),
opacity = edge_alpha,
name = "edges",
showlegend = FALSE,
hoverinfo = "none"
)
}
## Main scatter traces
if (!is.null(group_by)) {
# Group-by mode: one trace per group for consistent legend with 2D (label_use)
for (i in seq_along(labels_tb)) {
grp <- names(labels_tb)[i]
is_na_grp <- grp == "NA"
if (is_na_grp) {
grp_data <- data[is.na(data[[group_by]]), , drop = FALSE]
grp_color <- bg_color
} else {
grp_data <- data[
!is.na(data[[group_by]]) &
as.character(data[[group_by]]) == grp,
,
drop = FALSE
]
grp_color <- colors[grp]
}
if (nrow(grp_data) == 0 && !isTRUE(keep_empty_group)) {
next
}
legend_label <- if (!is.null(label_use)) label_use[i] else grp
trace_args <- list(
p = p,
x = grp_data[[dims[1]]],
y = grp_data[[dims[2]]],
z = grp_data[[dims[3]]],
type = "scatter3d",
mode = "markers",
marker = list(
size = marker_size,
color = grp_color,
opacity = pt_alpha
),
name = legend_label,
legendgroup = grp,
showlegend = TRUE,
hoverinfo = hover_mode
)
if (!large_data) {
trace_args$text <- if (is_na_grp) {
paste0("Cell: ", rownames(grp_data))
} else {
paste0("Cell: ", rownames(grp_data), "\nGroup: ", grp)
}
}
p <- do_call(plotly::add_trace, trace_args)
}
} else {
# Feature mode: continuous coloring
feat_colors <- palette_this(
palette = palette,
palcolor = palcolor,
type = "continuous",
reverse = palreverse
)
n_colors <- length(feat_colors)
plotly_colorscale <- lapply(seq_len(n_colors), function(i) {
list((i - 1) / (n_colors - 1), feat_colors[i])
})
na_mask <- is.na(data[[features]])
# Plot NA/background points
if (any(na_mask)) {
data_na <- data[na_mask, , drop = FALSE]
na_trace_args <- list(
p = p,
x = data_na[[dims[1]]],
y = data_na[[dims[2]]],
z = data_na[[dims[3]]],
type = "scatter3d",
mode = "markers",
marker = list(
size = marker_size,
color = bg_color,
opacity = pt_alpha
),
name = "NA",
showlegend = FALSE,
hoverinfo = hover_mode
)
if (!large_data) {
na_trace_args$text <- paste0("Cell: ", rownames(data_na))
}
p <- do_call(plotly::add_trace, na_trace_args)
}
# Plot non-NA points with colorscale
data_valid <- data[!na_mask, , drop = FALSE]
if (nrow(data_valid) > 0) {
colorbar_title <- if (nchar(color_name) > 0) color_name else colorby
feat_trace_args <- list(
p = p,
x = data_valid[[dims[1]]],
y = data_valid[[dims[2]]],
z = data_valid[[dims[3]]],
type = "scatter3d",
mode = "markers",
marker = list(
size = marker_size,
color = data_valid[[features]],
colorscale = plotly_colorscale,
cmin = min(feat_colors_value, na.rm = TRUE),
cmax = max(feat_colors_value, na.rm = TRUE),
opacity = pt_alpha,
showscale = TRUE,
colorbar = list(
title = list(text = colorbar_title)
)
),
showlegend = FALSE,
hoverinfo = hover_mode
)
if (!large_data) {
feat_trace_args$text <- paste0(
"Cell: ",
rownames(data_valid),
"\nValue: ",
round(data_valid[[features]], 3)
)
}
p <- do_call(plotly::add_trace, feat_trace_args)
}
}
## Highlight
if (!is.null(highlight)) {
if (isTRUE(highlight)) {
hi_df <- data
} else if (length(highlight) == 1 && is.character(highlight)) {
hi_df <- eval(parse(
text = paste0("dplyr::filter(data, ", highlight, ")")
))
} else {
all_inst <- if (is.numeric(highlight)) {
seq_len(nrow(data))
} else {
rownames(data)
}
if (!any(highlight %in% all_inst)) {
stop("No highlight items found in the data (rownames).")
}
if (!all(highlight %in% all_inst)) {
warning(
"Not all highlight items found in the data (rownames).",
immediate. = TRUE
)
}
hi_df <- data[intersect(highlight, all_inst), , drop = FALSE]
}
if (nrow(hi_df) > 0) {
hi_trace_args <- list(
p = p,
x = hi_df[[dims[1]]],
y = hi_df[[dims[2]]],
z = hi_df[[dims[3]]],
type = "scatter3d",
mode = "markers",
marker = list(
size = highlight_size * 2 + highlight_stroke,
color = highlight_color,
opacity = highlight_alpha,
symbol = "circle-open"
),
name = "highlight",
showlegend = FALSE,
hoverinfo = hover_mode
)
if (!large_data) {
hi_trace_args$text <- paste0("Cell: ", rownames(hi_df))
}
p <- do_call(plotly::add_trace, hi_trace_args)
}
}
## Lineages
if (!is.null(lineages)) {
lineages <- unique(check_columns(
data,
lineages,
force_factor = FALSE,
allow_multi = TRUE
))
lineage_colors <- palette_this(
lineages,
palette = lineages_palette,
palcolor = lineages_palcolor,
reverse = palreverse
)
for (l in lineages) {
trim_pass <- (data[[l]] >
quantile(data[[l]], lineages_trim[1], na.rm = TRUE) &
data[[l]] < quantile(data[[l]], lineages_trim[2], na.rm = TRUE))
na_pass <- !is.na(data[[l]])
index <- which(trim_pass & na_pass)
index <- index[order(data[index, l])]
dat_sub <- data[index, , drop = FALSE]
weights_used <- rep(1, nrow(dat_sub))
fitted_vals <- lapply(dims, function(x) {
loess(
formula(paste(x, l, sep = "~")),
weights = weights_used,
data = dat_sub,
span = lineages_span,
degree = 2
)$fitted
})
names(fitted_vals) <- dims
dat_smooth <- as.data.frame(fitted_vals)
dat_smooth <- unique(na.omit(dat_smooth))
if (nrow(dat_smooth) > 0) {
p <- plotly::add_trace(
p = p,
x = dat_smooth[[dims[1]]],
y = dat_smooth[[dims[2]]],
z = dat_smooth[[dims[3]]],
text = paste0("Lineage: ", l),
type = "scatter3d",
mode = "lines",
line = list(width = 6, color = lineage_colors[l]),
name = l,
showlegend = TRUE,
hoverinfo = "text"
)
}
}
}
## Labels
if (isTRUE(label) && !is.null(group_by)) {
# Compute label positions at group median coordinates (same as 2D)
label_data <- data[!is.na(data[[group_by]]), , drop = FALSE]
label_df <- aggregate(
label_data[, dims, drop = FALSE],
by = list(label_data[[group_by]]),
FUN = median
)
colnames(label_df)[1] <- ".group"
if (!isTRUE(label_insitu)) {
label_df[, ".label"] <- seq_len(nrow(label_df))
} else {
label_df[, ".label"] <- label_df[, ".group"]
}
# In 3D, plotly doesn't support text background/outline like geom_text_repel,
# so we use label_bg as the text color for visibility
p <- plotly::add_trace(
p = p,
x = label_df[[dims[1]]],
y = label_df[[dims[2]]],
z = label_df[[dims[3]]],
text = as.character(label_df[[".label"]]),
type = "scatter3d",
mode = "text",
textfont = list(
size = label_size * 3,
color = label_bg
),
textposition = "middle center",
hoverinfo = "text",
showlegend = FALSE
)
}
## Layout
# Use subtitle from parent (already includes show_stat "N = ..." if applicable)
subtitle_text <- subtitle %||% ""
if (!is.null(n_sampled)) {
sample_note <- paste0("Showing ", n_sampled, " sampled points")
if (nchar(subtitle_text) > 0) {
subtitle_text <- paste0(subtitle_text, "; ", sample_note)
} else {
subtitle_text <- sample_note
}
}
title_text <- title %||% ""
if (nchar(title_text) > 0 && nchar(subtitle_text) > 0) {
full_title <- paste0(title_text, "<br><sup>", subtitle_text, "</sup>")
} else if (nchar(subtitle_text) > 0) {
full_title <- paste0("<sup>", subtitle_text, "</sup>")
} else {
full_title <- title_text
}
p <- plotly::layout(
p = p,
title = list(
text = full_title,
font = list(size = 16, color = "black"),
y = 0.95
),
showlegend = TRUE,
legend = list(
itemsizing = "constant",
y = 0.5,
x = 1,
xanchor = "left"
),
scene = list(
xaxis = list(title = xlab),
yaxis = list(title = ylab),
zaxis = list(title = zlab),
aspectratio = list(x = 1, y = 1, z = 1)
)
)
return(p)
}
#' Atomic Dimension Reduction Plot without splitting the data
#'
#' @description
#' Core implementation for dimension reduction visualisation. This is the
#' internal workhorse dispatched by both `DimPlot()` (group-based) and
#' `FeatureDimPlot()` (continuous feature expression). It renders a 2D
#' scatter plot of ordination axes with extensive annotation capabilities,
#' and automatically delegates to `DimPlotAtomic3D()` for interactive 3D
#' plots when three `dims` are provided.
#'
#' The function supports two primary colouring modes:
#' - **group_by** — discrete factor colouring with a legend of group levels,
#' optional density/statistical overlays, and group mark / label annotations.
#' - **features** — continuous numeric colouring with a gradient colour bar,
#' multi-feature faceting via `tidyr::pivot_longer()`, and optional cutoff
#' / quantile trimming via `prepare_continuous_color_scale()`.
#'
#' Additional annotation layers include: graph / network edges drawn as
#' segments between connected nodes, 2D density contours (filled or outline),
#' group marks (hull, ellipse, rect, circle via `ggforce`), lineage curves
#' (LOESS-smoothed paths with optional whiskers), velocity / RNA-velocity
#' arrows (raw, grid, or stream via `VelocityPlot()`), statistical summary
#' mini-plots (pie, ring, bar, line) embedded at group centroids, background
#' points from other facets (faded context), and group labels with repulsion
#' (via `ggrepel::geom_text_repel()`).
#'
#' Rendering scales automatically: scatter points for small datasets,
#' `scattermore::geom_scattermore()` raster for `n > 1e5`, or hex-binned
#' aggregation (`stat_summary_hex()` / `geom_hex()`). Legend assembly uses
#' `cowplot::get_plot_component()` with independent guide-boxes for base
#' groups, lineages, velocity, and stat-by annotations — combined via
#' `rbind` / `cbind` and re-inserted with `add_grob()`.
#'
#' @section Architecture:
#' **DimPlotAtomic** executes the following steps:
#'
#' 1. **ggplot dispatch** — selects `gglogger::ggplot` or `ggplot2::ggplot`.
#' 2. **Order validation** — `match.arg(order)`.
#' 3. **Dimension resolution** — converts numeric indices to column names,
#' validates exactly 2 or 3 dims via `check_columns()`.
#' 4. **Column resolution** — validates `group_by` (force_factor, concat_multi),
#' `features`, and `facet_by`. Requires at least one of `group_by` or
#' `features`. Blocks `facet_by` when multiple features are present (features
#' themselves become facets).
#' 5. **Auto defaults** — `pt_size` defaults to `min(3000 / nrow(data), 0.6)`
#' when `NULL`. If a character string is given, it is treated as a column name
#' for variable point sizing (2D only, ignored in raster mode);
#' `raster` auto-enables when `nrow(data) > 1e5`; `theme_blank` suppresses
#' axis labels.
#' 6. **Label force-enable** — if `label_repel` or `label_insitu` is `TRUE`,
#' `label` is forced to `TRUE` with a message.
#' 7. **NA / empty handling** — `process_keep_na_empty()` filters the data;
#' `keep_empty` values extracted for group_by and facet_by.
#' 8. **group_by preprocessing** — group values / colours resolved via
#' `palette_this()`. NA levels mapped to literal `"NA"` string.
#' `label_use` is constructed based on `label`, `label_insitu`, and
#' `show_stat` combinations. Facet labeller is set to `as_labeller()` with
#' per-facet N annotations when `show_stat = TRUE`.
#' 9. **Multi-feature pivot** — when `length(features) > 1`, data is pivoted
#' to long format via `tidyr::pivot_longer()` with `.feature` as the facet
#' variable and `.value` as the numeric column.
#' 10. **Continuous colour scale** — when `features` is set,
#' `prepare_continuous_color_scale()` applies quantile/cutoff trimming
#' and returns anchor values for the gradient.
#' 11. **Point ordering** — applies the selected `order` strategy (as-is,
#' reverse, high-top, low-top, random) by sorting or shuffling rows.
#' 12. **3D branch** — if `length(dims) == 3`, emits a warning for unsupported
#' features, optionally down-samples with stratified group sampling (when
#' `raster = TRUE`), and delegates to `DimPlotAtomic3D()`.
#' 13. **Group marks** (2D only, when `add_mark = TRUE`) — dispatches to
#' `ggforce::geom_mark_hull/ellipse/rect/circle`, with `new_scale_fill()`
#' / `new_scale_color()` to isolate mark scales.
#' 14. **Graph / network** (2D only) — resolves the graph source (same logic
#' as 3D), melts the matrix, handles faceted data by per-facet edge
#' splitting, and adds `geom_segment(aes(linewidth = value))` with
#' `scale_linewidth_continuous()`.
#' 15. **Density overlay** (2D only) — filled (`stat_density_2d(geom = "raster")`)
#' or outline (`geom_density_2d()`).
#' 16. **Base scales and theme** — x / y limits from data range,
#' `do_call(theme, theme_args)`, aspect ratio and legend position.
#' 17. **Background points for facet context** — for faceted plots (excluding
#' multi-feature auto-faceting), points from other facets are added as
#' faded background (raster / hex / point depending on settings).
#' 18. **Main point layer** — dispatches by rendering mode:
#' - *raster*: `scattermore::geom_scattermore()` with separate NA / non-NA
#' group layers.
#' - *hex*: `geom_hex()` (group_by with optional count alpha, or
#' `stat_summary_hex()` for features). Raises `has_fill = TRUE`.
#' - *standard*: `geom_point()`.
#' 19. **Highlight** (2D only) — resolves highlight specification (TRUE, filter
#' expression, row names, indices), errors on hex + highlight combo, renders
#' highlight points with stroke (outer ring + inner colour).
#' 20. **Colour scales** — `scale_color_manual()` for group_by (with
#' `keep_empty`-aware `breaks`/`limits`/`drop`), or
#' `scale_color_gradientn()` for features. `scale_fill_manual()` /
#' `scale_fill_gradientn()` added when `has_fill` is TRUE.
#' 21. **Base legend** — extracted via `cowplot::get_plot_component("guide-box-bottom")`.
#' 22. **Lineages** (2D only, no facet_by) — per-lineage LOESS fitting
#' (`span = lineages_span, degree = 2`) with optional whiskers connecting
#' smoothed to raw coordinates. Rendered as `geom_path()` with background
#' stroke + foreground colour, plus `scale_color_manual()`. Legend extracted
#' as a separate guide-box.
#' 23. **Velocity** (2D only, no facet_by) — delegates to `VelocityPlot()` with
#' `return_layer = TRUE`. Adds `new_scale_color()` when the velocity layer
#' has its own colour scale. Legend extracted separately.
#' 24. **Stat-by mini-plots** (2D only, no facet_by, no features) — dispatches
#' to `PieChart()` / `RingPlot()` / `BarPlot()` / `LinePlot()` via
#' `do_call()`. Each mini-plot is rendered as `annotation_custom()` at the
#' group's median coordinates, scaled by `stat_plot_size * range`.
#' Legend extracted from the stat plot.
#' 25. **Group labels** (2D only, no features) — `geom_text_repel()` with
#' optional repulsion (`force = label_repulsion`) or fixed segments
#' (`force = 0`, bold, `min.segment.length = 0`). Labels positioned at
#' group median coordinates (computed per facet if applicable).
#' 26. **Legend assembly** — when additional legends exist (lineages, velocity,
#' stat_by), they are combined with the base legend via `cbind` (vertical
#' direction) or `rbind` (horizontal). The combined legend is re-inserted
#' via `add_grob(gtable, legend, legend.position)`.
#' 27. **Dimension calculation** — `calculate_plot_dimensions(base_height = 5.5,
#' aspect.ratio, legend_n, legend_nchar)` sets `height` / `width` attributes.
#' 28. **Faceting** — `facet_plot()` is called only when no additional legends
#' were assembled (otherwise the combined-legend grob is returned directly).
#'
#' @inheritParams common_args
#' @inheritParams VelocityPlot
#' @param dims A character vector of the column names to plot on the x, y (and optionally z) axes or a numeric vector
#' of the column indices. When 3 dimensions are provided, a 3D interactive plot is created using plotly.
#' Supported in 3D: group_by, features, labels, highlight, lineages, graph/network, show_stat, order.
#' Not supported in 3D: add_mark, stat_by, add_density, velocity, hex, facet_by, raster.
#' @param features A character vector of the column names to plot as features (continuous colouring).
#' When multiple features are provided and `facet_by` is not set, the data is pivoted to long format and
#' faceted by feature name.
#' @param group_by A character string of the column name to group the data by for discrete colouring.
#' A character/factor column is expected. If multiple columns are provided, the columns will be concatenated with `group_by_sep`.
#' @param group_by_sep A character string to concatenate the columns in `group_by`, if multiple columns are provided.
#' @param pt_size A numeric value for point size, or a character string naming a numeric column
#' in \code{data} to map point sizes to. If \code{NULL} (default), auto-calculated as
#' \code{min(3000 / nrow(data), 0.6)}. Column-name mapping only applies to 2D plots and
#' is ignored with a message when \code{raster = TRUE}.
#' @param pt_alpha A numeric value in `[0, 1]` for the point transparency. Default is `1`.
#' @param bg_color A character string specifying the colour used for NA-valued points and
#' background context points drawn from other facets. Default is `"grey80"`.
#' @param label_insitu A logical value. If `TRUE`, the raw group names are placed at the group median
#' coordinates instead of numeric indices. Forces `label = TRUE`. Default is `FALSE`.
#' @param show_stat A logical value. If `TRUE` (default), the number of points per group is shown
#' in the legend labels and subtitle. Ignored when `theme = "theme_blank"`.
#' @param order A character string controlling the draw order of points:
#' * `"as-is"` (default) — the row order in the data is preserved.
#' * `"reverse"` — rows are reversed.
#' * `"high-top"` — points with high values (last factor levels for group_by) are drawn last (on top).
#' * `"low-top"` — points with low values (first factor levels) are drawn last.
#' * `"random"` — rows are randomly shuffled.
#'
#' For `high-top` and `low-top`, NA values are always plotted at the bottom.
#' When applied to `group_by`, only the draw order changes — legend colours and order
#' are unaffected. Within the same level, point order is preserved. For precise
#' control, set factor levels before plotting.
#' See <https://github.com/pwwang/scplotter/issues/29#issuecomment-3009694130> for examples.
#' @param label A logical value. If `TRUE`, group labels (numeric indices by default, or group
#' names when `label_insitu = TRUE`) are placed at the median coordinates of each group.
#' Forced to `TRUE` when `label_repel` or `label_insitu` is set.
#' @param label_size A numeric value for the label text size. Passed to `ggrepel::geom_text_repel()`.
#' Default is `4`.
#' @param label_fg A character string for the label text (foreground) colour. Default is `"white"`.
#' @param label_bg A character string for the label background / outline colour. Default is `"black"`.
#' @param label_bg_r A numeric value for the background fill ratio of the label bounding box.
#' Passed to `ggrepel::geom_text_repel(bg.r = ...)`. Default is `0.1`.
#' @param label_repel A logical value. If `TRUE`, labels are repelled from each other with force
#' `label_repulsion`. A visible point anchor is drawn. Forces `label = TRUE`.
#' @param label_repulsion A numeric value for the repulsion force when `label_repel = TRUE`.
#' Passed to `ggrepel::geom_text_repel(force = ...)`. Default is `20`.
#' @param label_pt_size A numeric value for the size of the anchor point drawn when
#' `label_repel = TRUE`. Default is `1`.
#' @param label_pt_color A character string for the colour of the label anchor point. Default is `"black"`.
#' @param label_segment_color A character string for the colour of the line segment connecting
#' the label to the anchor. Used in non-repel mode (`label_repel = FALSE`) where
#' `min.segment.length = 0`. Default is `"black"`.
#' @param highlight A specification for highlighted points:
#' - `NULL` (default): no highlighting.
#' - `TRUE`: highlight all points (adds a dark outline around every point).
#' - A character string: a dplyr filter expression (e.g., `"clusters == 'Ductal'"`).
#' - A character vector: row names to highlight.
#' - A numeric vector: row indices to highlight.
#' @param highlight_alpha A numeric value in `[0, 1]` for the transparency of highlighted points.
#' Default is `1`.
#' @param highlight_size A numeric value for the size of the inner (coloured) highlight point.
#' Default is `1`.
#' @param highlight_color A character string for the colour of the outer highlight ring.
#' Default is `"black"`.
#' @param highlight_stroke A numeric value for the thickness of the outer highlight ring (the
#' difference between the outer ring size and `highlight_size`). Default is `0.8`.
#' @param add_mark A logical value. If `TRUE`, group boundaries are drawn around points using
#' `ggforce` marks. Requires `group_by`. Only supported in 2D.
#' @param mark_type A character string specifying the mark shape. Options:
#' `"hull"` (convex hull, default), `"ellipse"`, `"rect"`, or `"circle"`.
#' @param mark_expand A unit value for the outward expansion of the mark boundary.
#' Passed to `ggforce::geom_mark_*(expand = ...)`. Default is `unit(3, "mm")`.
#' @param mark_alpha A numeric value in `[0, 1]` for the transparency of the mark fill.
#' Default is `0.1`.
#' @param mark_linetype A numeric value for the line type of the mark boundary. Default is `1` (solid).
#' @param stat_by A character string naming a column used to compute per-group statistical summary
#' mini-plots embedded at group centroid positions. Only supported with `group_by` (not `features`).
#' Only supported in 2D without `facet_by`.
#' @param stat_plot_type A character string specifying the mini-plot type. Options:
#' `"pie"` (default), `"ring"`, `"bar"`, or `"line"`.
#' @param stat_plot_size A numeric value for the size of the stat mini-plot, expressed as a
#' fraction of the axis range. Default is `0.1`.
#' @param stat_args A list of additional arguments passed to the stat plot function
#' (e.g., `list(palette = "Set1")`). Default is `list(palette = "Set1")`.
#' @param graph A specification for network / graph edges to overlay. Sources:
#' - A character string starting with `"@"` (e.g., `"@graph"`): extracts the attribute
#' named `"graph"` from `attributes(data)`.
#' - A `Graph` object (e.g., Seurat): coerced to dense matrix via `as.matrix()`.
#' - A `matrix`, `data.frame`, or `dgCMatrix`: used directly as the adjacency matrix.
#' - Numeric indices or character column names: extracts columns from `data`.
#' Edges are drawn for non-zero, lower-triangle entries. Requires `data` to have row names
#' matching the matrix dimnames.
#' @param edge_size A numeric vector of length 2 specifying the range `[min, max]` for
#' `scale_linewidth_continuous(range = ...)` applied to edge widths. Default is `c(0.05, 0.5)`.
#' @param edge_alpha A numeric value in `[0, 1]` for the transparency of graph edges.
#' Default is `0.1`.
#' @param edge_color A character string for the colour of graph edges. Default is `"grey40"`.
#' @param add_density A logical value. If `TRUE`, a 2D density layer is overlaid. Only supported in 2D.
#' @param density_color A character string for the colour of the density contour lines.
#' Used when `density_filled = FALSE`. Default is `"grey80"`.
#' @param density_filled A logical value. If `TRUE`, the density is rendered as a filled raster
#' (`stat_density_2d(geom = "raster")`) instead of contour lines. A separate fill scale is used.
#' @param density_filled_palette A character string naming the palette for the filled density layer.
#' Default is `"Greys"`.
#' @param density_filled_palcolor A character vector of specific colours for the filled density
#' palette. Default is `NULL` (auto-resolved from `density_filled_palette`).
#' @param lineages A character vector of column names representing pseudotime / trajectory lineages.
#' Each column is fitted with a LOESS smooth (`span = lineages_span, degree = 2`) across the
#' 2D embedding, after trimming the top and bottom `lineages_trim` quantiles.
#' Only supported in 2D without `facet_by`.
#' @param lineages_trim A numeric vector of length 2 specifying the lower and upper quantile
#' thresholds `[0, 1]` for trimming lineage values before LOESS fitting.
#' Default is `c(0.01, 0.99)`.
#' @param lineages_span A numeric value passed as `span` to `stats::loess()` controlling the
#' smoothness of the lineage curve. Smaller values follow the data more closely.
#' Default is `0.75`.
#' @param lineages_palette A character string naming the palette for lineage colours.
#' Default is `"Dark2"`.
#' @param lineages_palcolor A character vector of specific colours for lineage curves.
#' Default is `NULL` (auto-resolved from `lineages_palette`).
#' @param lineages_arrow A ggplot2 `arrow` specification applied to the end of lineage paths.
#' Default is `arrow(length = unit(0.1, "inches"))`.
#' @param lineages_linewidth A numeric value for the width of the lineage curve lines.
#' Default is `1`.
#' @param lineages_line_bg A character string for the colour of the background (wider) stroke
#' drawn behind each lineage curve for improved visibility. Default is `"white"`.
#' @param lineages_line_bg_stroke A numeric value for the additional width of the background
#' stroke relative to `lineages_linewidth`. The background line has total width
#' `lineages_linewidth + lineages_line_bg_stroke`. Default is `0.5`.
#' @param lineages_whiskers A logical value. If `TRUE`, short line segments connect the
#' smoothed lineage curve to the original data coordinates of the fitted points.
#' Default is `FALSE`.
#' @param lineages_whiskers_linewidth A numeric value for the width of the whisker lines.
#' Default is `0.5`.
#' @param lineages_whiskers_alpha A numeric value in `[0, 1]` for the transparency of the
#' whisker lines. Default is `0.5`.
#' @param velocity A specification for RNA-velocity arrows. Can be:
#' - `NULL` (default): no velocity overlay.
#' - A character / integer vector: column names or indices in `data` for the velocity embedding.
#' - A data frame or matrix: the velocity embedding itself (must align with `data` rows).
#' Only supported in 2D without `facet_by`.
#' @param velocity_plot_type A character string specifying the velocity rendering style.
#' Options: `"raw"` (arrows from embedding), `"grid"` (grid-based arrows), or `"stream"`
#' (streamlines). Default is `"raw"`.
#' @param velocity_n_neighbors A numeric value for the number of neighbours used in the
#' velocity grid computation. Default is `NULL` (auto).
#' @param velocity_density A numeric value for the velocity kernel density bandwidth.
#' Default is `1`.
#' @param velocity_smooth A numeric value for the velocity smoothing parameter.
#' Default is `0.5`.
#' @param velocity_scale A numeric value for scaling the velocity arrows. Default is `1`.
#' @param velocity_min_mass A numeric value for the minimum cell mass threshold in velocity
#' grid computation. Default is `1`.
#' @param velocity_cutoff_perc A numeric value for the velocity cutoff percentage.
#' Default is `5`.
#' @param velocity_group_palette A character string naming the palette for velocity group
#' colours (used in `"raw"` plot type). Default is `"Set2"`.
#' @param velocity_group_palcolor A character vector of specific colours for velocity groups.
#' Default is `NULL` (auto-resolved from `velocity_group_palette`).
#' @param raster A logical value. If `TRUE`, points are rendered via
#' `scattermore::geom_scattermore()` for efficient rasterised plotting.
#' Default is `NULL`, which auto-enables when `nrow(data) > 1e5`.
#' @param raster_dpi A numeric vector of length 2 `[x_dpi, y_dpi]` specifying the raster
#' resolution in pixels. Passed to `scattermore::geom_scattermore(pixels = ...)`.
#' Default is `c(512, 512)`. If a single value is provided it is recycled to both dimensions.
#' @param hex A logical value. If `TRUE`, points are rendered as hexagonal bins via
#' `geom_hex()` / `stat_summary_hex()`. Not supported with highlight.
#' Default is `FALSE`. Only supported in 2D.
#' @param hex_linewidth A numeric value for the width of the hexagon boundary lines.
#' Default is `0.5`.
#' @param hex_count A logical value. If `TRUE` and `group_by` is set, hex fill alpha
#' is mapped to `after_stat(count)` so denser bins are more opaque.
#' For `features` mode hex_count is ignored. Default is `!is.null(group_by)`.
#' @param hex_bins A numeric value for the number of hex bins along each axis.
#' Passed to `geom_hex(bins = ...)`. Default is `50`.
#' @param hex_binwidth A numeric value for the width of individual hex bins.
#' Passed to `geom_hex(binwidth = ...)`. Takes precedence over `hex_bins` when set.
#' @param bg_cutoff A numeric threshold. Feature values with absolute value below this
#' cutoff are set to `NA` (and therefore rendered in `bg_color`). Default is `NULL`.
#' @param color_name A character string used as the title for the continuous colour bar
#' in feature mode. Default is `""`.
#' @return A ggplot object or a plotly object (when 3 dimensions are provided)
#' @keywords internal
#' @importFrom scales rescale
#' @importFrom stats loess formula na.omit aggregate
#' @importFrom dplyr %>% group_by group_map pull filter
#' @importFrom tidyr pivot_longer
#' @importFrom ggplot2 scale_linewidth_continuous geom_density_2d stat_density_2d geom_hex
#' @importFrom ggplot2 ggplotGrob theme_void as_labeller stat_summary_hex
#' @importFrom cowplot get_plot_component
DimPlotAtomic <- function(
data,
dims = 1:2,
group_by = NULL,
group_by_sep = "_",
features = NULL,
lower_quantile = 0,
upper_quantile = 0.99,
lower_cutoff = NULL,
upper_cutoff = NULL,
pt_size = NULL,
pt_alpha = 1,
bg_color = "grey80",
bg_cutoff = NULL,
color_name = "",
label_insitu = FALSE,
show_stat = !identical(theme, "theme_blank"),
label = FALSE,
label_size = 4,
label_fg = "white",
label_bg = "black",
label_bg_r = 0.1,
label_repel = FALSE,
label_repulsion = 20,
label_pt_size = 1,
label_pt_color = "black",
label_segment_color = "black",
order = c("as-is", "reverse", "high-top", "low-top", "random"),
highlight = NULL,
highlight_alpha = 1,
highlight_size = 1,
highlight_color = "black",
highlight_stroke = 0.8,
add_mark = FALSE,
mark_type = c("hull", "ellipse", "rect", "circle"),
mark_expand = unit(3, "mm"),
mark_alpha = 0.1,
mark_linetype = 1,
stat_by = NULL,
stat_plot_type = c("pie", "ring", "bar", "line"),
stat_plot_size = 0.1,
stat_palette = "Set1",
stat_args = list(),
graph = NULL,
edge_size = c(0.05, 0.5),
edge_alpha = 0.1,
edge_color = "grey40",
add_density = FALSE,
density_color = "grey80",
density_filled = FALSE,
density_filled_palette = "Greys",
density_filled_palcolor = NULL,
lineages = NULL,
lineages_trim = c(0.01, 0.99),
lineages_span = 0.75,
lineages_palette = "Dark2",
lineages_palcolor = NULL,
lineages_arrow = ggplot2::arrow(length = unit(0.1, "inches")),
lineages_linewidth = 1,
lineages_line_bg = "white",
lineages_line_bg_stroke = 0.5,
lineages_whiskers = FALSE,
lineages_whiskers_linewidth = 0.5,
lineages_whiskers_alpha = 0.5,
velocity = NULL,
velocity_plot_type = c("raw", "grid", "stream"),
velocity_n_neighbors = NULL,
velocity_density = 1,
velocity_smooth = 0.5,
velocity_scale = 1,
velocity_min_mass = 1,
velocity_cutoff_perc = 5,
velocity_group_palette = "Set2",
velocity_group_palcolor = NULL,
arrow_angle = 20,
arrow_color = "black",
streamline_l = 5,
streamline_minl = 1,
streamline_res = 1,
streamline_n = 15,
arrow_alpha = 1,
streamline_width = c(0, 0.8),
streamline_alpha = 1,
streamline_color = NULL,
streamline_palette = "RdYlBu",
streamline_palcolor = NULL,
streamline_bg_color = "white",
streamline_bg_stroke = 0.5,
keep_na = FALSE,
keep_empty = FALSE,
facet_by = NULL,
facet_scales = "fixed",
facet_nrow = NULL,
facet_ncol = NULL,
facet_byrow = TRUE,
title = NULL,
subtitle = NULL,
xlab = NULL,
ylab = NULL,
theme = "theme_this",
theme_args = list(),
aspect.ratio = 1,
legend.position = "right",
legend.direction = "vertical",
raster = NULL,
raster_dpi = c(512, 512),
hex = FALSE,
hex_linewidth = 0.5,
hex_count = !is.null(group_by),
hex_bins = 50,
hex_binwidth = NULL,
palette = ifelse(is.null(features), "Paired", "Spectral"),
palcolor = NULL,
palreverse = FALSE,
seed = 8525,
...
) {
ggplot <- if (getOption("plotthis.gglogger.enabled", FALSE)) {
gglogger::ggplot
} else {
ggplot2::ggplot
}
order <- match.arg(order)
## Setting up the parameters
if (is.numeric(dims)) {
dims <- colnames(data)[dims]
}
if (!length(dims) %in% c(2, 3)) {
stop("Only 2 or 3 dimensions are allowed for dimension reduction plot.")
}
dims <- check_columns(data, dims, allow_multi = TRUE)
if (length(raster_dpi) == 1) {
raster_dpi <- rep(raster_dpi, 2)
}
group_by <- check_columns(
data,
group_by,
force_factor = TRUE,
allow_multi = TRUE,
concat_multi = TRUE,
concat_sep = group_by_sep
)
features <- check_columns(data, features, allow_multi = TRUE)
if (is.null(group_by) && is.null(features)) {
stop("Either 'group_by' or 'features' should be specified.")
}
facet_by <- check_columns(
data,
facet_by,
force_factor = TRUE,
allow_multi = TRUE
)
if (length(features) > 1 && !is.null(facet_by)) {
stop(
"Cannot specify 'facet_by' with multiple features. The plot will be faceted by features."
)
}
if (!is.numeric(pt_size) && !is.null(pt_size)) {
pt_size <- check_columns(data, pt_size)
}
pt_size <- pt_size %||% min(3000 / nrow(data), 0.6)
pt_size_num <- if (is.numeric(pt_size)) {
pt_size
} else {
min(3000 / nrow(data), 0.6)
}
raster <- raster %||% (nrow(data) > 1e5)
if (!is.numeric(pt_size) && isTRUE(raster)) {
warning(
"[DimPlot] 'raster' is enabled. Column-based point size ('pt_size') ",
"is ignored, using numeric fallback for raster mode.",
immediate. = TRUE
)
pt_size <- pt_size_num
}
xlab <- xlab %||% dims[1]
ylab <- ylab %||% dims[2]
if (identical(theme, "theme_blank")) {
theme_args[["xlab"]] <- xlab
theme_args[["ylab"]] <- ylab
}
if ((isTRUE(label_repel) || isTRUE(label_insitu)) && !isTRUE(label)) {
warning(
"[DimPlot] Forcing label to be TRUE when label_repel or label_insitu is TRUE.",
immediate. = TRUE
)
label <- TRUE
}
data <- process_keep_na_empty(data, keep_na, keep_empty)
keep_empty_group <- if (!is.null(group_by)) keep_empty[[group_by]] else NULL
keep_empty_facet <- if (!is.null(facet_by)) {
keep_empty[[facet_by[1]]]
} else {
NULL
}
if (length(facet_by) > 1) {
keep_empty_facet2 <- if (!is.null(facet_by[2])) {
keep_empty[[facet_by[2]]]
} else {
NULL
}
stopifnot(
"[(Feature)DimPlot] `keep_empty` for `facet_by` variables must be identical." = identical(
keep_empty_facet,
keep_empty_facet2
)
)
}
facet_labeller <- "label_value"
if (!is.null(group_by)) {
group_vals <- levels(data[[group_by]])
if (anyNA(data[[group_by]])) {
group_vals <- c(group_vals, NA)
}
colors <- palette_this(
group_vals,
palette = palette,
palcolor = palcolor,
NA_keep = TRUE,
reverse = palreverse
)
names(colors)[is.na(names(colors))] <- "NA"
group_vals[is.na(group_vals)] <- "NA"
labels_tb <- table(data[[group_by]], useNA = "ifany")
# labels_tb <- labels_tb[labels_tb != 0]
# convert NA names to "NA"
names(labels_tb)[is.na(names(labels_tb))] <- "NA"
labels_tb <- labels_tb[group_vals]
if (isTRUE(label_insitu)) {
if (isTRUE(show_stat)) {
label_use <- paste0(names(labels_tb), "(", labels_tb, ")")
} else {
label_use <- paste0(names(labels_tb))
}
} else {
if (isTRUE(label)) {
if (isTRUE(show_stat)) {
label_use <- paste0(
seq_along(labels_tb),
": ",
names(labels_tb),
"(",
labels_tb,
")"
)
} else {
label_use <- paste0(
seq_along(labels_tb),
": ",
names(labels_tb)
)
}
} else {
if (isTRUE(show_stat)) {
label_use <- paste0(names(labels_tb), "(", labels_tb, ")")
} else {
label_use <- paste0(names(labels_tb))
}
}
}
if (isTRUE(show_stat)) {
if (is.null(facet_by)) {
subtitle <- subtitle %||%
paste0("N = ", sum(!is.na(data[[group_by]])))
} else if (length(facet_by) == 1) {
facet_labeller <- as_labeller(function(values) {
sapply(values, function(v) {
data_sub <- data[data[[facet_by]] == v, , drop = FALSE]
paste0(
v,
" (N = ",
sum(!is.na(data_sub[[group_by]])),
")"
)
})
})
}
}
}
## Making long data for features when length(features) > 1
if (length(dims) == 3 && length(features) > 1) {
stop(
"Multiple features are not supported for 3D plots. ",
"Use `split_by = TRUE` in `FeatureDimPlot()` to plot features separately with `combine = FALSE`."
)
}
multifeats <- length(features) > 1
if (multifeats) {
data <- data %>%
pivot_longer(
cols = features,
names_to = ".feature",
values_to = ".value"
)
data$.feature <- factor(data$.feature, levels = features)
facet_by <- ".feature"
features <- ".value"
}
if (!is.null(features)) {
result <- prepare_continuous_color_scale(
data,
features,
lower_quantile = lower_quantile,
upper_quantile = upper_quantile,
lower_cutoff = lower_cutoff,
upper_cutoff = upper_cutoff,
bg_cutoff = bg_cutoff
)
data <- result$data
feat_colors_value <- result$feat_colors_value
}
colorby <- ifelse(is.null(features), group_by, features)
if (order == "reverse") {
data <- data[nrow(data):1, , drop = FALSE]
} else if (order == "high-top") {
data <- dplyr::arrange(data, !is.na(!!sym(colorby)), !!sym(colorby))
} else if (order == "low-top") {
data <- dplyr::arrange(
data,
!is.na(!!sym(colorby)),
dplyr::desc(!!sym(colorby))
)
} else if (order == "random") {
data <- data[sample(nrow(data)), , drop = FALSE]
}
## 3D plot using plotly
if (length(dims) == 3) {
unsupported_args <- c(
if (isTRUE(add_mark)) "add_mark",
if (!is.null(stat_by)) "stat_by",
if (isTRUE(add_density)) "add_density",
if (!is.null(velocity)) "velocity",
if (isTRUE(hex)) "hex",
if (!is.null(facet_by)) "facet_by"
)
if (length(unsupported_args) > 0) {
warning(
"The following features are not supported for 3D plots and will be ignored: ",
paste(unsupported_args, collapse = ", "),
immediate. = TRUE
)
}
# Downsample for large datasets (analogous to raster in 2D)
n_sampled <- NULL
if (isTRUE(raster) && nrow(data) > prod(raster_dpi)) {
set.seed(seed)
max_pts <- prod(raster_dpi)
if (!is.null(group_by)) {
# Stratified sampling: proportional to group size, at least 1 per non-empty group
grp_vals_all <- as.character(data[[group_by]])
grp_vals_all[is.na(grp_vals_all)] <- "NA"
grp_counts <- table(grp_vals_all)
grp_n <- pmax(
1L,
as.integer(round(grp_counts / sum(grp_counts) * max_pts))
)
names(grp_n) <- names(grp_counts)
sampled_idx <- unlist(lapply(names(grp_counts), function(g) {
idx <- which(grp_vals_all == g)
sample(idx, min(length(idx), grp_n[g]))
}))
data <- data[sort(sampled_idx), , drop = FALSE]
} else {
data <- data[sort(sample(nrow(data), max_pts)), , drop = FALSE]
}
n_sampled <- nrow(data)
}
return(DimPlotAtomic3D(
data = data,
dims = dims,
group_by = group_by,
features = features,
colorby = colorby,
colors = if (!is.null(group_by)) colors else NULL,
feat_colors_value = if (!is.null(features)) {
feat_colors_value
} else {
NULL
},
label_use = if (!is.null(group_by)) label_use else NULL,
labels_tb = if (!is.null(group_by)) labels_tb else NULL,
keep_empty_group = isTRUE(keep_empty_group),
bg_color = bg_color,
color_name = color_name,
pt_size = pt_size,
pt_alpha = pt_alpha,
show_stat = show_stat,
title = title,
subtitle = subtitle,
xlab = xlab,
ylab = ylab,
label = label,
label_insitu = label_insitu,
label_size = label_size,
label_fg = label_fg,
label_bg = label_bg,
highlight = highlight,
highlight_color = highlight_color,
highlight_size = highlight_size,
highlight_stroke = highlight_stroke,
highlight_alpha = highlight_alpha,
graph = graph,
edge_size = edge_size,
edge_alpha = edge_alpha,
edge_color = edge_color,
lineages = lineages,
lineages_trim = lineages_trim,
lineages_span = lineages_span,
lineages_palette = lineages_palette,
lineages_palcolor = lineages_palcolor,
palette = palette,
palcolor = palcolor,
palreverse = palreverse,
n_sampled = n_sampled
))
}
# Do we have fill scale?
has_fill <- FALSE
p <- ggplot(data)
## Adding the mark
if (isTRUE(add_mark)) {
if (is.null(group_by)) {
stop("Cannot add mark without 'group_by'.")
}
# if (!requireNamespace("ggforce", quietly = TRUE)) {
# stop("'ggforce' package is required for adding mark to the plot.")
# }
mark_type <- match.arg(mark_type)
mark_fun <- switch(
mark_type,
hull = ggforce::geom_mark_hull,
ellipse = ggforce::geom_mark_ellipse,
rect = ggforce::geom_mark_rect,
circle = ggforce::geom_mark_circle
)
p <- p +
mark_fun(
data = data[!is.na(data[[group_by]]), , drop = FALSE],
mapping = aes(
x = !!sym(dims[1]),
y = !!sym(dims[2]),
color = !!sym(group_by),
fill = !!sym(group_by)
),
expand = mark_expand,
alpha = mark_alpha,
linetype = mark_linetype,
show.legend = FALSE
) +
scale_fill_manual(
values = colors[names(labels_tb)],
guide = "none"
) +
scale_color_manual(
values = colors[names(labels_tb)],
guide = "none"
) +
new_scale_fill() +
new_scale_color()
}
## Adding the graph/network
if (!is.null(graph)) {
if (
is.character(graph) && length(graph) == 1 && startsWith(graph, "@")
) {
graph <- substring(graph, 2)
net_mat <- attr(data, graph)
if (is.null(net_mat)) {
stop(paste0(
"[DimPlot] The graph '",
graph,
"' is not found in the data attributes."
))
}
} else if (inherits(graph, "Graph")) {
# SeuratObject Graph
net_mat <- as.matrix(graph)
} else if (
is.matrix(graph) ||
is.data.frame(graph) ||
inherits(graph, "dgCMatrix")
) {
net_mat <- graph
} else if (is.numeric(graph)) {
graph <- colnames(data)[graph]
net_mat <- data[graph]
} else if (is.character(graph)) {
net_mat <- data[graph]
} else {
stop(
"[DimPlot] The 'graph' should be a matrix, data.frame, Graph object, indexes, or column names."
)
}
if (!is.matrix(net_mat)) {
net_mat <- as.matrix(net_mat)
}
if (!is.null(rownames(net_mat)) && !is.null(colnames(net_mat))) {
net_mat <- net_mat[rownames(data), rownames(data)]
} else if (is.null(rownames(net_mat))) {
stop("The graph matrix should have rownames and colnames.")
} else if (is.null(rownames(data))) {
stop("'graph' requires the input data to have rownames.")
}
net_mat[net_mat == 0] <- NA
net_mat[upper.tri(net_mat)] <- NA
handle_single_facet_value <- function(mat) {
net_df <- reshape2::melt(
mat,
na.rm = TRUE,
stringsAsFactors = FALSE
)
net_df$value <- as.numeric(net_df$value)
net_df$Var1 <- as.character(net_df$Var1)
net_df$Var2 <- as.character(net_df$Var2)
net_df$x <- data[net_df$Var1, dims[1]]
net_df$y <- data[net_df$Var1, dims[2]]
net_df$xend <- data[net_df$Var2, dims[1]]
net_df$yend <- data[net_df$Var2, dims[2]]
return(net_df)
}
if (!is.null(facet_by)) {
net_df <- do_call(
rbind,
lapply(split(data, data[, facet_by]), function(d) {
d <- handle_single_facet_value(net_mat[
rownames(d),
rownames(d)
])
d[, facet_by] <- d[1, facet_by]
d
})
)
} else {
net_df <- handle_single_facet_value(net_mat)
}
p <- p +
geom_segment(
data = net_df,
mapping = aes(
x = !!sym("x"),
y = !!sym("y"),
xend = !!sym("xend"),
yend = !!sym("yend"),
linewidth = !!sym("value")
),
color = edge_color,
alpha = edge_alpha,
show.legend = FALSE
) +
scale_linewidth_continuous(range = edge_size, guide = "none")
}
## Adding the density plot
if (isTRUE(add_density)) {
if (isTRUE(density_filled)) {
filled_color <- palette_this(
palette = density_filled_palette,
palcolor = density_filled_palcolor,
reverse = palreverse
)
p <- p +
stat_density_2d(
geom = "raster",
aes(
x = !!sym(dims[1]),
y = !!sym(dims[2]),
fill = after_stat(!!sym("density"))
),
contour = FALSE,
inherit.aes = FALSE,
show.legend = FALSE
) +
scale_fill_gradientn(name = "Density", colours = filled_color) +
new_scale_fill()
} else {
p <- p +
geom_density_2d(
aes(x = !!sym(dims[1]), y = !!sym(dims[2])),
color = density_color,
inherit.aes = FALSE,
show.legend = FALSE
)
}
}
## Setting up the theme
x_min <- min(data[[dims[1]]], na.rm = TRUE)
x_max <- max(data[[dims[1]]], na.rm = TRUE)
y_min <- min(data[[dims[2]]], na.rm = TRUE)
y_max <- max(data[[dims[2]]], na.rm = TRUE)
p <- p +
labs(title = title, subtitle = subtitle, x = xlab, y = ylab) +
scale_x_continuous(limits = c(x_min, x_max)) +
scale_y_continuous(limits = c(y_min, y_max)) +
do_call(theme, theme_args) +
ggplot2::theme(
aspect.ratio = aspect.ratio,
legend.position = legend.position,
legend.direction = legend.direction
)
## Adding the background points for each facet
if (!is.null(facet_by) && isFALSE(multifeats)) {
# Add the points from other subplots as background
all_fc_values <- distinct(data, !!!syms(facet_by))
colnames(all_fc_values) <- paste0(".facet_", colnames(all_fc_values))
fc_data <- expand_grid(data, all_fc_values)
# Remove the points inside the facet
fc_indicator <- TRUE
for (fc in facet_by) {
fc_indicator <- fc_indicator &
(fc_data[[fc]] == fc_data[[paste0(".facet_", fc)]])
}
fc_data <- fc_data[!fc_indicator, , drop = FALSE]
fc_data[, facet_by, drop = FALSE] <- fc_data[,
paste0(".facet_", facet_by),
drop = FALSE
]
if (isTRUE(raster)) {
p <- p +
scattermore::geom_scattermore(
data = fc_data,
mapping = aes(x = !!sym(dims[1]), y = !!sym(dims[2])),
pointsize = ceiling(pt_size_num),
alpha = pt_alpha / 2,
color = bg_color,
pixels = raster_dpi
)
} else if (isTRUE(hex)) {
p <- p +
geom_hex(
data = fc_data,
mapping = aes(x = !!sym(dims[1]), y = !!sym(dims[2])),
linewidth = hex_linewidth,
bins = hex_bins,
binwidth = hex_binwidth,
fill = bg_color,
alpha = pt_alpha / 2
)
} else {
if (is.numeric(pt_size)) {
p <- p +
geom_point(
data = fc_data,
mapping = aes(x = !!sym(dims[1]), y = !!sym(dims[2])),
size = pt_size,
alpha = pt_alpha / 2,
color = bg_color
)
} else {
p <- p +
geom_point(
data = fc_data,
mapping = aes(
x = !!sym(dims[1]),
y = !!sym(dims[2]),
size = !!sym(pt_size)
),
alpha = pt_alpha / 2,
color = bg_color
)
}
}
rm(fc_data)
}
## Raserting the plot/Adding the points
if (isTRUE(raster)) {
# if (!requireNamespace("scattermore", quietly = TRUE)) {
# stop("'scattermore' package is required to raster the plot.")
# }
if (!is.null(group_by)) {
p <- p +
scattermore::geom_scattermore(
data = data[is.na(data[[group_by]]), , drop = FALSE],
mapping = aes(x = !!sym(dims[1]), y = !!sym(dims[2])),
color = bg_color,
pointsize = ceiling(pt_size_num),
alpha = pt_alpha,
pixels = raster_dpi,
show.legend = TRUE
) +
scattermore::geom_scattermore(
data = data[!is.na(data[[group_by]]), , drop = FALSE],
mapping = aes(
x = !!sym(dims[1]),
y = !!sym(dims[2]),
color = !!sym(group_by)
),
pointsize = ceiling(pt_size_num),
alpha = pt_alpha,
pixels = raster_dpi,
show.legend = TRUE
)
} else {
# features
p <- p +
scattermore::geom_scattermore(
mapping = aes(x = !!sym(dims[1]), y = !!sym(dims[2])),
color = bg_color,
pointsize = ceiling(pt_size_num),
alpha = pt_alpha,
pixels = raster_dpi
) +
scattermore::geom_scattermore(
mapping = aes(
x = !!sym(dims[1]),
y = !!sym(dims[2]),
color = !!sym(colorby)
),
pointsize = ceiling(pt_size_num),
alpha = pt_alpha,
pixels = raster_dpi
)
}
} else if (isTRUE(hex)) {
# if (!requireNamespace("hexbin", quietly = TRUE)) {
# stop("'hexbin' package is required to add hexgons the plot.")
# }
has_fill <- TRUE
if (isTRUE(hex_count)) {
if (!is.null(features)) {
stop(
"Don't know how to count for the hex when 'group_by' is not provided."
)
}
p <- p +
geom_hex(
mapping = aes(
x = !!sym(dims[1]),
y = !!sym(dims[2]),
color = !!sym(group_by),
fill = !!sym(group_by),
alpha = after_stat(!!sym("count"))
),
linewidth = hex_linewidth,
bins = hex_bins,
binwidth = hex_binwidth
)
} else if (!is.null(group_by)) {
p <- p +
geom_hex(
mapping = aes(
x = !!sym(dims[1]),
y = !!sym(dims[2]),
color = !!sym(group_by),
fill = !!sym(group_by)
),
linewidth = hex_linewidth,
bins = hex_bins,
binwidth = hex_binwidth,
show.legend = TRUE
)
} else {
# features
data_na <- data[is.na(data[[features]]), , drop = FALSE]
if (nrow(data_na) > 0) {
p <- p +
geom_hex(
data = data_na,
mapping = aes(x = !!sym(dims[1]), y = !!sym(dims[2])),
fill = bg_color,
linewidth = hex_linewidth,
bins = hex_bins,
binwidth = hex_binwidth,
alpha = pt_alpha / 2
)
}
p <- p +
stat_summary_hex(
data = data[!is.na(data[[features]]), , drop = FALSE],
mapping = aes(
x = !!sym(dims[1]),
y = !!sym(dims[2]),
z = !!sym(colorby)
),
linewidth = hex_linewidth,
bins = hex_bins,
binwidth = hex_binwidth,
alpha = pt_alpha
)
}
} else {
if (is.numeric(pt_size)) {
p <- p +
geom_point(
mapping = aes(
x = !!sym(dims[1]),
y = !!sym(dims[2]),
color = !!sym(colorby)
),
size = pt_size,
alpha = pt_alpha,
show.legend = TRUE
)
} else {
p <- p +
geom_point(
mapping = aes(
x = !!sym(dims[1]),
y = !!sym(dims[2]),
color = !!sym(colorby),
size = !!sym(pt_size)
),
alpha = pt_alpha,
show.legend = TRUE
)
}
}
if (!is.numeric(pt_size)) {
p <- p +
scale_size_area(max_size = 6, n.breaks = 4) +
guides(
size = guide_legend(
override.aes = list(fill = "grey30", shape = 21),
order = 1
)
)
}
## Adding the highlight
if (!is.null(highlight)) {
if (isTRUE(hex)) {
stop("Highlight is not supported for hex plot.")
}
if (isTRUE(highlight)) {
hi_df <- data
} else if (length(highlight) == 1 && is.character(highlight)) {
hi_df <- eval(parse(text = paste0('filter(data, ', highlight, ')')))
} else {
all_inst <- if (is.numeric(highlight)) {
1:nrow(data)
} else {
rownames(data)
}
if (!any(highlight %in% all_inst)) {
stop("No highlight items found in the data (rownames).")
}
if (!all(highlight %in% all_inst)) {
warning(
"Not all highlight items found in the data (rownames).",
immediate. = TRUE
)
}
hi_df <- data[intersect(highlight, all_inst), , drop = FALSE]
rm(all_inst)
}
if (nrow(hi_df) > 0) {
if (isTRUE(raster)) {
p <- p +
scattermore::geom_scattermore(
data = hi_df,
aes(x = !!sym(dims[1]), y = !!sym(dims[2])),
color = highlight_color,
pointsize = floor(highlight_size) + highlight_stroke,
alpha = highlight_alpha,
pixels = raster_dpi
) +
scattermore::geom_scattermore(
data = hi_df,
aes(
x = !!sym(dims[1]),
y = !!sym(dims[2]),
color = !!sym(colorby)
),
pointsize = floor(highlight_size),
alpha = highlight_alpha,
pixels = raster_dpi
)
} else {
p <- p +
geom_point(
data = hi_df,
aes(x = !!sym(dims[1]), y = !!sym(dims[2])),
color = highlight_color,
size = highlight_size + highlight_stroke,
alpha = highlight_alpha
) +
geom_point(
data = hi_df,
aes(
x = !!sym(dims[1]),
y = !!sym(dims[2]),
color = !!sym(colorby)
),
size = highlight_size,
alpha = highlight_alpha
)
}
}
}
if (!is.null(group_by)) {
if (!isFALSE(keep_empty_group)) {
p <- p +
scale_color_manual(
values = colors[names(labels_tb)],
labels = label_use,
na.value = bg_color,
breaks = group_vals,
limits = group_vals,
drop = FALSE,
guide = guide_legend(
title.hjust = 0,
order = 1,
override.aes = list(size = 3, alpha = 1)
)
)
if (has_fill) {
p <- p +
scale_fill_manual(
values = colors[names(labels_tb)],
labels = label_use,
na.value = bg_color,
breaks = group_vals,
limits = group_vals,
drop = FALSE,
guide = guide_legend(
title.hjust = 0,
order = 1
)
)
}
} else {
p <- p +
scale_color_manual(
values = colors[names(labels_tb)],
labels = label_use,
na.value = bg_color,
guide = guide_legend(
title.hjust = 0,
order = 1,
override.aes = list(size = 3, alpha = 1)
)
)
if (has_fill) {
p <- p +
scale_fill_manual(
values = colors[names(labels_tb)],
labels = label_use,
na.value = bg_color,
guide = guide_legend(
title.hjust = 0,
order = 1
)
)
}
}
} else {
# features
p <- p +
scale_color_gradientn(
name = color_name,
colors = palette_this(
palette = palette,
palcolor = palcolor,
type = "continuous",
reverse = palreverse
),
values = rescale(feat_colors_value),
limits = range(feat_colors_value),
na.value = bg_color,
guide = guide_colorbar(
frame.colour = "black",
ticks.colour = "black",
title.hjust = 0
)
)
if (has_fill) {
p <- p +
scale_fill_gradientn(
name = color_name,
colors = palette_this(
palette = palette,
palcolor = palcolor,
type = "continuous",
reverse = palreverse
),
values = rescale(feat_colors_value),
limits = range(feat_colors_value),
na.value = bg_color,
guide = guide_colorbar(
frame.colour = "black",
ticks.colour = "black",
title.hjust = 0
)
)
}
}
# There may be warnings about fonts, but we don't care here
legend_base <- suppressWarnings(get_plot_component(
p +
theme_this(
legend.position = "bottom",
legend.direction = legend.direction
),
"guide-box-bottom"
))
legend_list <- list()
## Adding the lineages
if (!is.null(lineages)) {
if (!is.null(facet_by)) {
stop("'lineages' is not supported when 'facet_by' is not NULL.")
}
lineages <- unique(check_columns(
data,
lineages,
force_factor = FALSE,
allow_multi = TRUE
))
lineage_colors <- palette_this(
lineages,
palette = lineages_palette,
palcolor = lineages_palcolor,
reverse = palreverse
)
lineage_layers <- lapply(lineages, function(l) {
trim_pass <- (data[[l]] >
quantile(data[[l]], lineages_trim[1], na.rm = TRUE) &
data[[l]] < quantile(data[[l]], lineages_trim[2], na.rm = TRUE))
na_pass <- !is.na(data[[l]])
index <- which(trim_pass & na_pass)
index <- index[order(data[index, l])]
dat_sub <- data[index, , drop = FALSE]
# if (is.null(weights)) {
weights_used <- rep(1, nrow(dat_sub))
# } else {
# weights_used <- dat_sub[[weights]]
# }
fitted <- lapply(dims, function(x) {
loess(
formula(paste(x, l, sep = "~")),
weights = weights_used,
data = dat_sub,
span = lineages_span,
degree = 2
)$fitted
})
names(fitted) <- dims
fitted[["index"]] <- index
dat_smooth <- as.data.frame(fitted)
colnames(dat_smooth) <- c(
paste0("Axis_", 1:(ncol(dat_smooth) - 1)),
"index"
)
dat_smooth[, "Lineages"] <- factor(l, levels = lineages)
dat_smooth <- unique(na.omit(dat_smooth))
curve <- list()
if (isTRUE(lineages_whiskers)) {
dat_smooth[, "raw_Axis_1"] <- data[
dat_smooth[, "index"],
dims[1]
]
dat_smooth[, "raw_Axis_2"] <- data[
dat_smooth[, "index"],
dims[2]
]
curve <- c(
curve,
geom_segment(
data = dat_smooth,
mapping = aes(
x = !!sym("Axis_1"),
y = !!sym("Axis_2"),
xend = !!sym("raw_Axis_1"),
yend = !!sym("raw_Axis_2"),
color = !!sym("Lineages")
),
linewidth = lineages_whiskers_linewidth,
alpha = lineages_whiskers_alpha,
show.legend = TRUE,
inherit.aes = FALSE
)
)
}
curve <- c(
curve,
geom_path(
data = dat_smooth,
mapping = aes(x = !!sym("Axis_1"), y = !!sym("Axis_2")),
color = lineages_line_bg,
linewidth = lineages_linewidth + lineages_line_bg_stroke,
arrow = lineages_arrow,
show.legend = TRUE,
inherit.aes = FALSE
),
geom_path(
data = dat_smooth,
mapping = aes(
x = !!sym("Axis_1"),
y = !!sym("Axis_2"),
color = !!sym("Lineages")
),
linewidth = lineages_linewidth,
arrow = lineages_arrow,
show.legend = TRUE,
inherit.aes = FALSE
)
)
return(curve)
})
lineage_layers[[length(lineage_layers) + 1]] <- scale_color_manual(
values = lineage_colors
)
suppressMessages({
legend_list$lineages <- suppressWarnings(get_plot_component(
ggplot() +
lineage_layers +
theme_this(
legend.position = "bottom",
legend.direction = legend.direction
),
"guide-box-bottom"
))
})
p <- suppressWarnings({
p +
new_scale_color() +
lineage_layers +
ggplot2::theme(legend.position = "none")
})
if (is.null(legend_list$lineages)) {
legend_list["lineages"] <- list(NULL)
}
}
if (!is.null(velocity)) {
if (!is.null(facet_by)) {
stop("'velocity' is not supported when 'facet_by' is not NULL.")
}
velocity_plot_type <- match.arg(velocity_plot_type)
if (is.data.frame(velocity) || is.matrix(velocity)) {
v_embedding <- velocity
} else {
v_embedding <- data[, velocity, drop = FALSE]
}
velocity_layers <- VelocityPlot(
embedding = data[, dims, drop = FALSE],
v_embedding = v_embedding,
plot_type = velocity_plot_type,
group_by = if (velocity_plot_type == "raw") {
data[[group_by]]
} else {
NULL
},
group_name = group_by,
group_palette = velocity_group_palette,
group_palcolor = velocity_group_palcolor,
n_neighbors = velocity_n_neighbors,
density = velocity_density,
keep_na = keep_na[[group_by]],
keep_empty = keep_empty[[group_by]],
smooth = velocity_smooth,
scale = velocity_scale,
min_mass = velocity_min_mass,
cutoff_perc = velocity_cutoff_perc,
arrow_angle = arrow_angle,
arrow_color = arrow_color,
streamline_l = streamline_l,
streamline_minl = streamline_minl,
arrow_alpha = arrow_alpha,
streamline_res = streamline_res,
streamline_n = streamline_n,
streamline_width = streamline_width,
streamline_alpha = streamline_alpha,
streamline_color = streamline_color,
streamline_palette = streamline_palette,
streamline_palcolor = streamline_palcolor,
streamline_bg_color = streamline_bg_color,
streamline_bg_stroke = streamline_bg_stroke,
return_layer = TRUE
)
velocity_scales <- attr(velocity_layers, "scales")
if (!is.null(velocity_scales) && "color" %in% velocity_scales) {
p <- p + new_scale_color() + velocity_layers
legend_list$velocity <- suppressWarnings(get_plot_component(
ggplot() +
velocity_layers +
theme_this(
legend.position = "bottom",
legend.direction = legend.direction
),
"guide-box-bottom"
))
} else {
p <- p + velocity_layers
legend_list["velocity"] <- list(NULL)
}
}
## Adding the statistic plots
if (!is.null(stat_by)) {
if (!is.null(facet_by)) {
stop("'stat_by' is not supported when 'facet_by' is not NULL.")
}
if (!is.null(features)) {
stop("'stat_by' is not supported without 'group_by'.")
}
stat_by <- check_columns(data, stat_by, force_factor = TRUE)
stat_plot_type <- match.arg(stat_plot_type)
stat_args$data <- data
stat_args$split_by <- group_by
stat_args$legend.position <- "bottom"
stat_args$legend.direction <- legend.direction
stat_args$combine <- FALSE
stat_args$theme <- theme
stat_args$theme_args <- theme_args
stat_args$title <- character(0)
# keep all levels so they appear in the legend, even if some levels have no data
# (in case some stat plots don't show all levels due to filtering)
stat_args$keep_empty <- TRUE
stat_args$palette <- stat_palette
if (stat_plot_type == "pie") {
stat_args$x <- stat_by
stat_plots <- do_call(PieChart, stat_args)
} else if (stat_plot_type == "ring") {
stat_args$group_by <- stat_by
stat_plots <- do_call(RingPlot, stat_args)
} else if (stat_plot_type == "bar") {
stat_args$x <- stat_by
stat_plots <- do_call(BarPlot, stat_args)
} else if (stat_plot_type == "line") {
stat_args$x <- stat_by
stat_plots <- do_call(LinePlot, stat_args)
}
coord_df <- aggregate(
p$data[, dims],
by = list(p$data[[group_by]]),
FUN = median
)
colnames(coord_df)[1] <- group_by
x_range <- diff(layer_scales(p)$x$range$range)
y_range <- diff(layer_scales(p)$y$range$range)
stat_plot_list <- list()
for (i in seq_len(nrow(coord_df))) {
stat_plot_list[[i]] <- annotation_custom(
ggplotGrob(
stat_plots[[coord_df[i, group_by]]] +
theme_void() +
ggplot2::theme(legend.position = "none")
),
xmin = coord_df[i, dims[1]] - x_range * stat_plot_size / 2,
ymin = coord_df[i, dims[2]] - y_range * stat_plot_size / 2,
xmax = coord_df[i, dims[1]] + x_range * stat_plot_size / 2,
ymax = coord_df[i, dims[2]] + y_range * stat_plot_size / 2
)
}
p <- p + stat_plot_list
legend_list$stat_by <- suppressWarnings(get_plot_component(
stat_plots[[coord_df[i, group_by]]] +
ggplot2::theme(legend.position = "bottom"),
"guide-box-bottom"
))
}
## Adding the labels
if (isTRUE(label)) {
if (!is.null(features)) {
stop("Adding labels is not supported when 'features' is specified.")
}
if (!is.null(facet_by)) {
label_df <- aggregate(
data[, dims],
by = list(data[[group_by]], data[[facet_by]]),
FUN = median
)
colnames(label_df)[2:length(facet_by) + 1] <- facet_by
} else {
label_df <- aggregate(
data[, dims],
by = list(data[[group_by]]),
FUN = median
)
}
colnames(label_df)[1] <- ".label"
label_df <- label_df[!is.na(label_df[, ".label"]), , drop = FALSE]
if (!isTRUE(label_insitu)) {
label_df[, ".label"] <- seq_len(nrow(label_df))
}
if (isTRUE(label_repel)) {
p <- p +
geom_point(
data = label_df,
mapping = aes(x = !!sym(dims[1]), y = !!sym(dims[2])),
color = label_pt_color,
size = label_pt_size
) +
geom_text_repel(
data = label_df,
aes(
x = !!sym(dims[1]),
y = !!sym(dims[2]),
label = !!sym(".label")
),
point.size = label_pt_size,
max.overlaps = 100,
force = label_repulsion,
color = label_fg,
bg.color = label_bg,
bg.r = label_bg_r,
size = label_size,
inherit.aes = FALSE
)
} else {
p <- p +
geom_text_repel(
data = label_df,
aes(
x = !!sym(dims[1]),
y = !!sym(dims[2]),
label = !!sym(".label")
),
fontface = "bold",
min.segment.length = 0,
segment.color = label_segment_color,
point.size = NA,
max.overlaps = 100,
force = 0,
color = label_fg,
bg.color = label_bg,
bg.r = label_bg_r,
size = label_size,
inherit.aes = FALSE
)
}
}
## Posing all legends
if (length(legend_list) > 0) {
legend_list <- legend_list[!sapply(legend_list, is.null)]
if (legend.direction == "vertical") {
legend <- do_call(cbind, c(list(base = legend_base), legend_list))
} else {
legend <- do_call(rbind, c(list(base = legend_base), legend_list))
}
orig_data <- p$data
gtable <- ggplotGrob(p + ggplot2::theme(legend.position = "none"))
gtable <- add_grob(gtable, legend, legend.position)
p <- wrap_plots(gtable)
p$data <- orig_data
}
## Putting reasonable height and width
dims <- calculate_plot_dimensions(
base_height = 5.5,
aspect.ratio = aspect.ratio,
legend.position = legend.position,
legend.direction = legend.direction,
legend_n = if (!is.null(group_by)) length(group_vals) else 1,
legend_nchar = if (!is.null(group_by)) max(nchar(label_use)) else 5
)
attr(p, "height") <- dims$height
attr(p, "width") <- dims$width
if (length(legend_list) == 0) {
p <- facet_plot(
p,
facet_by,
facet_scales,
facet_nrow,
facet_ncol,
facet_byrow,
labeller = facet_labeller,
legend.position = legend.position,
legend.direction = legend.direction,
drop = !isTRUE(keep_empty_facet)
)
}
p
}
#' DimPlot / FeatureDimPlot
#'
#' @description
#' `DimPlot` visualizes dimension reduction data (PCA, t-SNE, UMAP, etc.) as a 2D or 3D
#' scatter plot. `DimPlot()` colours points by a discrete grouping variable
#' (e.g., clusters), while `FeatureDimPlot()` colours points by a continuous
#' numeric feature (e.g., gene expression, lineage scores).
#'
#' Both functions share the same internal engine (`DimPlotAtomic`) and support
#' an extensive set of annotation layers: group boundary marks, network/graph
#' edges, 2D density contours, lineage/trajectory curves, RNA-velocity arrows
#' (raw, grid, or stream), statistical summary mini-plots at group centroids,
#' point highlighting, background context points from other facets, and
#' flexible label positioning.
#'
#' When `dims` has 3 elements, both functions automatically return an
#' interactive **plotly** 3D scatter plot (via `DimPlotAtomic3D`). Certain
#' 2D-only features are silently ignored in 3D mode (see `@param dims` for
#' the full list).
#'
#' Rendering scales with dataset size: standard `geom_point()` for small data,
#' automatic rasterisation via `scattermore::geom_scattermore()` when
#' `nrow(data) > 1e5`, or hex-bin aggregation (`geom_hex()` /
#' `stat_summary_hex()`).
#'
#' @section split_by Workflow (DimPlot):
#'
#' When `split_by` is specified, `DimPlot()` executes the following pipeline:
#'
#' 1. **Argument validation** — `validate_common_args()` checks the seed and
#' blocks `split_by` + `velocity` combinations.
#' 2. **NA / empty normalisation** — `check_keep_na()` / `check_keep_empty()`
#' convert `keep_na` / `keep_empty` to per-column lists.
#' 3. **Theme resolution** — `process_theme()` resolves the theme string to a
#' theme function.
#' 4. **Split column resolution** — `check_columns()` validates `split_by`
#' (force_factor, concat_multi).
#' 5. **Pre-filtering** — `process_keep_na_empty()` removes NA / empty levels
#' from the split column, then `data` is split by `split_by` levels (order
#' preserved). When `graph` references an attribute (`@graph`), the graph
#' matrix is also subset per split.
#' 6. **Per-split parameter resolution** — `check_palette()`,
#' `check_palcolor()`, `check_legend()` resolve palette, palcolor,
#' legend.position, and legend.direction for each split.
#' 7. **Per-split dispatch** — each split is passed to `DimPlotAtomic()` with
#' its resolved parameters. Title defaults to the split level name unless
#' `title` is a function.
#' 8. **Combination** — `combine_plots()` assembles the list of plots via
#' `patchwork::wrap_plots()`, applying `nrow`, `ncol`, `byrow`, `axes`,
#' `axis_titles`, `guides`, and `design`.
#'
#' @section split_by Workflow (FeatureDimPlot):
#'
#' `FeatureDimPlot()` supports two forms of splitting:
#'
#' **A. `split_by = TRUE` (split by features)**
#'
#' 1. Each feature in `features` is dispatched individually to
#' `DimPlotAtomic()`, producing one plot per feature. The plot title
#' defaults to the feature name.
#' 2. Plots are combined via `combine_plots()` with `split_by = ".features"`.
#'
#' **B. `split_by` as a column name (split by data column)**
#'
#' 1. Data is split by the named column's levels (same pipeline as DimPlot
#' steps 1–8 above). Graph attribute splitting is supported.
#'
#' @rdname dimplot
#' @inheritParams common_args
#' @inheritParams DimPlotAtomic
#' @return A `ggplot` object (single plot), a `patchwork` / `wrap_plots` object
#' (when `split_by` is provided and `combine = TRUE`), or a list of `ggplot`
#' objects (when `split_by` is provided and `combine = FALSE`).
#' When `dims` has 3 elements, a **plotly** object is returned instead.
#' @export
#' @seealso \code{\link{VelocityPlot}}
#' @examples
#' \donttest{
#' data(dim_example)
#'
#' # basic dim plot
#' DimPlot(dim_example, group_by = "clusters")
#' DimPlot(dim_example, group_by = "clusters", theme = "theme_blank")
#' DimPlot(dim_example, group_by = "clusters", theme = ggplot2::theme_classic,
#' theme_args = list(base_size = 16), palette = "seurat")
#'
#' # raster and highlighting
#' DimPlot(dim_example, group_by = "clusters", raster = TRUE, raster_dpi = 50)
#' DimPlot(dim_example, group_by = "clusters", highlight = 1:20,
#' highlight_color = "black", highlight_stroke = 2)
#' DimPlot(dim_example, group_by = "clusters", highlight = TRUE, facet_by = "group",
#' theme = "theme_blank")
#'
#' # labels
#' DimPlot(dim_example, group_by = "clusters", label = TRUE,
#' label_size = 5, label_bg_r = 0.2)
#' DimPlot(dim_example, group_by = "clusters", label = TRUE, label_fg = "red",
#' label_bg = "yellow", label_size = 5)
#' DimPlot(dim_example, group_by = "clusters", label = TRUE, label_insitu = TRUE)
#'
#' # group marks
#' DimPlot(dim_example, group_by = "clusters", add_mark = TRUE)
#' DimPlot(dim_example, group_by = "clusters", add_mark = TRUE, mark_linetype = 2)
#' DimPlot(dim_example, group_by = "clusters", add_mark = TRUE, mark_type = "ellipse")
#'
#' # density overlays
#' DimPlot(dim_example, group_by = "clusters", add_density = TRUE)
#' DimPlot(dim_example, group_by = "clusters", add_density = TRUE, density_filled = TRUE)
#' DimPlot(dim_example, group_by = "clusters", add_density = TRUE, density_filled = TRUE,
#' density_filled_palette = "Blues", highlight = TRUE)
#'
#' # statistics at group centroids
#' DimPlot(dim_example, group_by = "clusters", stat_by = "group")
#' DimPlot(dim_example, group_by = "clusters", stat_by = "group",
#' stat_plot_type = "bar", stat_plot_size = 0.06)
#'
#' # hex bins
#' DimPlot(dim_example, group_by = "clusters", hex = TRUE)
#' DimPlot(dim_example, group_by = "clusters", hex = TRUE, hex_bins = 20)
#' DimPlot(dim_example, group_by = "clusters", hex = TRUE, hex_count = FALSE)
#'
#' # graph / network edges
#' DimPlot(dim_example, group_by = "clusters", graph = "@graph", edge_color = "grey80")
#'
#' # lineages / trajectories
#' DimPlot(dim_example, group_by = "clusters", lineages = c("stochasticbasis_1", "stochasticbasis_2"))
#' DimPlot(dim_example, group_by = "clusters", lineages = c("stochasticbasis_1", "stochasticbasis_2"),
#' lineages_whiskers = TRUE, lineages_whiskers_linewidth = 0.1)
#' DimPlot(dim_example, group_by = "clusters", lineages = c("stochasticbasis_1", "stochasticbasis_2"),
#' lineages_span = 0.4)
#'
#' # split_by
#' DimPlot(dim_example, group_by = "clusters", split_by = "group",
#' palette = list(A = "Paired", B = "Set1"))
#'
#' # velocity
#' DimPlot(dim_example, group_by = "clusters", velocity = c("stochasticbasis_1", "stochasticbasis_2"),
#' pt_alpha = 0)
#' DimPlot(dim_example, group_by = "clusters", velocity = 3:4,
#' velocity_plot_type = "grid", arrow_alpha = 0.6)
#' DimPlot(dim_example, group_by = "clusters", velocity = 3:4,
#' velocity_plot_type = "stream")
#'
#' # 3D plots (returns a plotly object)
#' DimPlot(dim_example, dims = 1:3, group_by = "clusters")
#' DimPlot(dim_example, dims = 1:3, group_by = "clusters", label = TRUE,
#' label_insitu = TRUE)
#' DimPlot(dim_example, dims = c("basis_1", "basis_2", "stochasticbasis_1"),
#' group_by = "clusters", graph = "@graph", edge_color = "grey80")
#'
#' # keep_na and keep_empty
#' dim_example$clusters[dim_example$clusters == "Ductal"] <- NA
#'
#' DimPlot(dim_example, group_by = "clusters", keep_na = FALSE, keep_empty = TRUE)
#' DimPlot(dim_example, group_by = "clusters", keep_na = TRUE, keep_empty = TRUE)
#' DimPlot(dim_example, group_by = "clusters", keep_na = TRUE, keep_empty = FALSE)
#' }
DimPlot <- function(
data,
dims = 1:2,
group_by,
group_by_sep = "_",
split_by = NULL,
split_by_sep = "_",
pt_size = NULL,
pt_alpha = 1,
bg_color = "grey80",
label_insitu = FALSE,
show_stat = !identical(theme, "theme_blank"),
label = FALSE,
label_size = 4,
label_fg = "white",
label_bg = "black",
label_bg_r = 0.1,
label_repel = FALSE,
label_repulsion = 20,
label_pt_size = 1,
label_pt_color = "black",
label_segment_color = "black",
order = c("as-is", "reverse", "high-top", "low-top", "random"),
highlight = NULL,
highlight_alpha = 1,
highlight_size = 1,
highlight_color = "black",
highlight_stroke = 0.8,
add_mark = FALSE,
mark_type = c("hull", "ellipse", "rect", "circle"),
mark_expand = unit(3, "mm"),
mark_alpha = 0.1,
mark_linetype = 1,
stat_by = NULL,
stat_plot_type = c("pie", "ring", "bar", "line"),
stat_plot_size = 0.1,
stat_args = list(palette = "Set1"),
graph = NULL,
edge_size = c(0.05, 0.5),
edge_alpha = 0.1,
edge_color = "grey40",
add_density = FALSE,
density_color = "grey80",
density_filled = FALSE,
density_filled_palette = "Greys",
density_filled_palcolor = NULL,
lineages = NULL,
lineages_trim = c(0.01, 0.99),
lineages_span = 0.75,
lineages_palette = "Dark2",
lineages_palcolor = NULL,
lineages_arrow = arrow(length = unit(0.1, "inches")),
lineages_linewidth = 1,
lineages_line_bg = "white",
lineages_line_bg_stroke = 0.5,
lineages_whiskers = FALSE,
lineages_whiskers_linewidth = 0.5,
lineages_whiskers_alpha = 0.5,
velocity = NULL,
velocity_plot_type = c("raw", "grid", "stream"),
velocity_n_neighbors = NULL,
velocity_density = 1,
velocity_smooth = 0.5,
velocity_scale = 1,
velocity_min_mass = 1,
velocity_cutoff_perc = 5,
velocity_group_palette = "Set2",
velocity_group_palcolor = NULL,
arrow_angle = 20,
arrow_color = "black",
arrow_alpha = 1,
streamline_l = 5,
streamline_minl = 1,
streamline_res = 1,
streamline_n = 15,
streamline_width = c(0, 0.8),
streamline_alpha = 1,
streamline_color = NULL,
streamline_palette = "RdYlBu",
streamline_palcolor = NULL,
streamline_bg_color = "white",
streamline_bg_stroke = 0.5,
keep_na = FALSE,
keep_empty = FALSE,
facet_by = NULL,
facet_scales = "fixed",
facet_nrow = NULL,
facet_ncol = NULL,
facet_byrow = TRUE,
title = NULL,
subtitle = NULL,
xlab = NULL,
ylab = NULL,
theme = "theme_this",
theme_args = list(),
aspect.ratio = 1,
legend.position = "right",
legend.direction = "vertical",
raster = NULL,
raster_dpi = c(512, 512),
hex = FALSE,
hex_linewidth = 0.5,
hex_count = TRUE,
hex_bins = 50,
hex_binwidth = NULL,
palette = "Paired",
palcolor = NULL,
palreverse = FALSE,
seed = 8525,
combine = TRUE,
nrow = NULL,
ncol = NULL,
byrow = TRUE,
axes = NULL,
axis_titles = axes,
guides = NULL,
design = NULL,
...
) {
validate_common_args(seed, facet_by = facet_by)
keep_na <- check_keep_na(keep_na, c(facet_by, group_by, split_by, stat_by))
keep_empty <- check_keep_empty(
keep_empty,
c(facet_by, group_by, split_by, stat_by)
)
theme <- process_theme(theme)
split_by_orig <- split_by
split_by <- check_columns(
data,
split_by,
force_factor = TRUE,
allow_multi = TRUE,
concat_multi = TRUE,
concat_sep = split_by_sep
)
stopifnot(
"[DimPlot] 'split_by' is not supported for velocity plot." = is.null(
velocity
) ||
is.null(split_by)
)
if (!is.null(split_by)) {
data <- process_keep_na_empty(data, keep_na, keep_empty, col = split_by)
keep_na[split_by_orig] <- NULL
keep_empty[split_by_orig] <- NULL
datas <- split(data, data[[split_by]])
# keep the order of levels
datas <- datas[levels(data[[split_by]])]
if (
is.character(graph) && length(graph) == 1 && startsWith(graph, "@")
) {
# split the graph as well
datas <- lapply(datas, function(d) {
gh <- attr(data, substring(graph, 2))[rownames(d), rownames(d)]
attr(d, substring(graph, 2)) <- gh
d
})
}
} else {
datas <- list(data)
split_by <- names(datas) <- "..."
}
palette <- check_palette(palette, names(datas))
palcolor <- check_palcolor(palcolor, names(datas))
legend.direction <- check_legend(
legend.direction,
names(datas),
"legend.direction"
)
legend.position <- check_legend(
legend.position,
names(datas),
"legend.position"
)
plots <- lapply(
names(datas),
function(nm) {
default_title <- if (length(datas) == 1 && identical(nm, "...")) {
NULL
} else {
nm
}
if (is.function(title)) {
title <- title(default_title)
} else {
title <- title %||% default_title
}
DimPlotAtomic(
datas[[nm]],
dims = dims,
group_by = group_by,
group_by_sep = group_by_sep,
pt_size = pt_size,
pt_alpha = pt_alpha,
bg_color = bg_color,
label_insitu = label_insitu,
show_stat = show_stat,
label = label,
label_size = label_size,
label_fg = label_fg,
label_bg = label_bg,
label_bg_r = label_bg_r,
label_repel = label_repel,
label_repulsion = label_repulsion,
label_pt_size = label_pt_size,
label_pt_color = label_pt_color,
label_segment_color = label_segment_color,
order = order,
highlight = highlight,
highlight_alpha = highlight_alpha,
highlight_size = highlight_size,
highlight_color = highlight_color,
highlight_stroke = highlight_stroke,
add_mark = add_mark,
mark_type = mark_type,
mark_expand = mark_expand,
mark_alpha = mark_alpha,
mark_linetype = mark_linetype,
stat_by = stat_by,
stat_plot_type = stat_plot_type,
stat_plot_size = stat_plot_size,
stat_args = stat_args,
graph = graph,
edge_size = edge_size,
edge_alpha = edge_alpha,
edge_color = edge_color,
add_density = add_density,
density_color = density_color,
density_filled = density_filled,
density_filled_palette = density_filled_palette,
density_filled_palcolor = density_filled_palcolor,
lineages = lineages,
lineages_trim = lineages_trim,
lineages_span = lineages_span,
lineages_palette = lineages_palette,
lineages_palcolor = lineages_palcolor,
lineages_arrow = lineages_arrow,
lineages_linewidth = lineages_linewidth,
lineages_line_bg = lineages_line_bg,
lineages_line_bg_stroke = lineages_line_bg_stroke,
lineages_whiskers = lineages_whiskers,
lineages_whiskers_linewidth = lineages_whiskers_linewidth,
lineages_whiskers_alpha = lineages_whiskers_alpha,
velocity = velocity,
velocity_plot_type = velocity_plot_type,
velocity_n_neighbors = velocity_n_neighbors,
velocity_density = velocity_density,
velocity_smooth = velocity_smooth,
velocity_scale = velocity_scale,
velocity_min_mass = velocity_min_mass,
velocity_cutoff_perc = velocity_cutoff_perc,
arrow_alpha = arrow_alpha,
velocity_group_palette = velocity_group_palette,
velocity_group_palcolor = velocity_group_palcolor,
arrow_angle = arrow_angle,
arrow_color = arrow_color,
streamline_l = streamline_l,
streamline_minl = streamline_minl,
streamline_res = streamline_res,
streamline_n = streamline_n,
streamline_width = streamline_width,
streamline_alpha = streamline_alpha,
streamline_color = streamline_color,
streamline_palette = streamline_palette,
streamline_palcolor = streamline_palcolor,
streamline_bg_color = streamline_bg_color,
streamline_bg_stroke = streamline_bg_stroke,
keep_na = keep_na,
keep_empty = keep_empty,
facet_by = facet_by,
facet_scales = facet_scales,
facet_nrow = facet_nrow,
facet_ncol = facet_ncol,
facet_byrow = facet_byrow,
title = title,
subtitle = subtitle,
xlab = xlab,
ylab = ylab,
theme = theme,
theme_args = theme_args,
aspect.ratio = aspect.ratio,
legend.position = legend.position[[nm]],
legend.direction = legend.direction[[nm]],
raster = raster,
raster_dpi = raster_dpi,
hex = hex,
hex_linewidth = hex_linewidth,
hex_count = hex_count,
hex_bins = hex_bins,
hex_binwidth = hex_binwidth,
palette = palette[[nm]],
palcolor = palcolor[[nm]],
palreverse = palreverse,
seed = seed,
...
)
}
)
names(plots) <- names(datas)
combine_plots(
plots,
combine = combine,
split_by = split_by,
nrow = nrow,
ncol = ncol,
byrow = byrow,
axes = axes,
axis_titles = axis_titles,
guides = guides,
design = design
)
}
#' @description
#' `FeatureDimPlot` to visualize feature expression on dimension reduction plots.
#' Colours points by a
#' continuous numeric variable (e.g., gene expression, module score, lineage
#' pseudotime) using a gradient colour scale, with optional quantile trimming
#' and background cutoff.
#'
#' When multiple `features` are provided and `facet_by` is not set, the data
#' is automatically pivoted to long format and faceted by feature name.
#' `split_by = TRUE` dispatches each feature to a separate plot for
#' independent layout control. `split_by` as a column name splits by that
#' column's levels, producing one plot per level with per-split palette
#' support.
#'
#' For detailed split_by workflows, see the main `DimPlot / FeatureDimPlot`
#' documentation (`@section split_by Workflow (FeatureDimPlot)`).
#'
#' @export
#' @rdname dimplot
#' @inheritParams common_args
#' @inheritParams DimPlotAtomic
#' @param split_by A character vector of column names to split the data by and
#' plot separately. If `TRUE`, the data is split by `features` — each feature
#' is plotted in its own panel. Use this instead of `facet_by` when you need
#' independent layout control (`nrow`, `ncol`) or per-feature palettes via
#' `split_by = TRUE` combined with `palette` / `palcolor`.
#' @return A `ggplot` object (single plot), a `patchwork` / `wrap_plots` object
#' (when `split_by` is provided and `combine = TRUE`), or a list of `ggplot`
#' objects (when `split_by` is provided and `combine = FALSE`).
#' When `dims` has 3 elements, a **plotly** object is returned instead.
#' @export
#' @examples
#' \donttest{
#' data(dim_example)
#'
#' # single feature
#' FeatureDimPlot(dim_example, features = "stochasticbasis_1", pt_size = 2)
#' FeatureDimPlot(dim_example, features = "stochasticbasis_1", pt_size = 2, bg_cutoff = 0)
#' FeatureDimPlot(dim_example, features = "stochasticbasis_1", raster = TRUE, raster_dpi = 30)
#'
#' # multiple features (auto-pivoted to long, faceted by feature)
#' FeatureDimPlot(dim_example, features = c("stochasticbasis_1", "stochasticbasis_2"),
#' pt_size = 2)
#'
#' # single feature with facet_by (facet_by works when only 1 feature)
#' FeatureDimPlot(dim_example, features = c("stochasticbasis_1"), pt_size = 2,
#' facet_by = "group")
#'
#' # multiple features with split_by for independent layout
#' FeatureDimPlot(dim_example, features = c("stochasticbasis_1", "stochasticbasis_2"),
#' split_by = "group", nrow = 2)
#'
#' # highlight and hex
#' FeatureDimPlot(dim_example, features = c("stochasticbasis_1", "stochasticbasis_2"),
#' highlight = TRUE)
#' FeatureDimPlot(dim_example, features = c("stochasticbasis_1", "stochasticbasis_2"),
#' hex = TRUE, hex_bins = 15)
#' FeatureDimPlot(dim_example, features = c("stochasticbasis_1", "stochasticbasis_2"),
#' hex = TRUE, hex_bins = 15, split_by = "group", palette = list(A = "Reds", B = "Blues"))
#'
#' # 3D plots (returns a plotly object)
#' FeatureDimPlot(dim_example, dims = 1:3, features = "stochasticbasis_2", pt_size = 2)
#' FeatureDimPlot(dim_example, dims = c("basis_1", "basis_2", "stochasticbasis_1"),
#' features = "stochasticbasis_2")
#' }
FeatureDimPlot <- function(
data,
dims = 1:2,
features,
split_by = NULL,
split_by_sep = "_",
lower_quantile = 0,
upper_quantile = 0.99,
lower_cutoff = NULL,
upper_cutoff = NULL,
pt_size = NULL,
pt_alpha = 1,
bg_color = "grey80",
bg_cutoff = NULL,
label_insitu = FALSE,
show_stat = !identical(theme, "theme_blank"),
color_name = "",
label = FALSE,
label_size = 4,
label_fg = "white",
label_bg = "black",
label_bg_r = 0.1,
label_repel = FALSE,
label_repulsion = 20,
label_pt_size = 1,
label_pt_color = "black",
label_segment_color = "black",
order = c("as-is", "reverse", "high-top", "low-top", "random"),
highlight = NULL,
highlight_alpha = 1,
highlight_size = 1,
highlight_color = "black",
highlight_stroke = 0.8,
add_mark = FALSE,
mark_type = c("hull", "ellipse", "rect", "circle"),
mark_expand = unit(3, "mm"),
mark_alpha = 0.1,
mark_linetype = 1,
keep_na = FALSE,
keep_empty = FALSE,
stat_by = NULL,
stat_plot_type = c("pie", "ring", "bar", "line"),
stat_plot_size = 0.1,
stat_args = list(palette = "Set1"),
graph = NULL,
edge_size = c(0.05, 0.5),
edge_alpha = 0.1,
edge_color = "grey40",
add_density = FALSE,
density_color = "grey80",
density_filled = FALSE,
density_filled_palette = "Greys",
density_filled_palcolor = NULL,
lineages = NULL,
lineages_trim = c(0.01, 0.99),
lineages_span = 0.75,
lineages_palette = "Dark2",
lineages_palcolor = NULL,
lineages_arrow = arrow(length = unit(0.1, "inches")),
lineages_linewidth = 1,
lineages_line_bg = "white",
lineages_line_bg_stroke = 0.5,
lineages_whiskers = FALSE,
lineages_whiskers_linewidth = 0.5,
lineages_whiskers_alpha = 0.5,
velocity = NULL,
velocity_plot_type = c("raw", "grid", "stream"),
velocity_n_neighbors = NULL,
velocity_density = 1,
velocity_smooth = 0.5,
velocity_scale = 1,
velocity_min_mass = 1,
velocity_cutoff_perc = 5,
velocity_group_palette = "Set2",
velocity_group_palcolor = NULL,
arrow_angle = 20,
arrow_color = "black",
arrow_alpha = 1,
streamline_l = 5,
streamline_minl = 1,
streamline_res = 1,
streamline_n = 15,
streamline_width = c(0, 0.8),
streamline_alpha = 1,
streamline_color = NULL,
streamline_palette = "RdYlBu",
streamline_palcolor = NULL,
streamline_bg_color = "white",
streamline_bg_stroke = 0.5,
facet_by = NULL,
facet_scales = "fixed",
facet_nrow = NULL,
facet_ncol = NULL,
facet_byrow = TRUE,
title = NULL,
subtitle = NULL,
xlab = NULL,
ylab = NULL,
theme = "theme_this",
theme_args = list(),
aspect.ratio = 1,
legend.position = "right",
legend.direction = "vertical",
raster = NULL,
raster_dpi = c(512, 512),
hex = FALSE,
hex_linewidth = 0.5,
hex_count = FALSE,
hex_bins = 50,
hex_binwidth = NULL,
palette = "Spectral",
palcolor = NULL,
palreverse = FALSE,
seed = 8525,
combine = TRUE,
nrow = NULL,
ncol = NULL,
byrow = TRUE,
axes = NULL,
axis_titles = axes,
guides = NULL,
design = NULL,
...
) {
validate_common_args(seed, facet_by = facet_by)
theme <- process_theme(theme)
if (isTRUE(split_by)) {
keep_na <- check_keep_na(keep_na, c(facet_by, stat_by))
keep_empty <- check_keep_empty(keep_empty, c(facet_by, stat_by))
plots <- lapply(
features,
function(feature) {
DimPlotAtomic(
data,
dims = dims,
lower_quantile = lower_quantile,
upper_quantile = upper_quantile,
lower_cutoff = lower_cutoff,
upper_cutoff = upper_cutoff,
pt_size = pt_size,
pt_alpha = pt_alpha,
bg_color = bg_color,
color_name = color_name,
label_insitu = label_insitu,
show_stat = show_stat,
features = feature,
bg_cutoff = bg_cutoff,
label = label,
label_size = label_size,
label_fg = label_fg,
label_bg = label_bg,
label_bg_r = label_bg_r,
label_repel = label_repel,
label_repulsion = label_repulsion,
label_pt_size = label_pt_size,
label_pt_color = label_pt_color,
label_segment_color = label_segment_color,
order = order,
keep_na = keep_na,
keep_empty = keep_empty,
highlight = highlight,
highlight_alpha = highlight_alpha,
highlight_size = highlight_size,
highlight_color = highlight_color,
highlight_stroke = highlight_stroke,
add_mark = add_mark,
mark_type = mark_type,
mark_expand = mark_expand,
mark_alpha = mark_alpha,
mark_linetype = mark_linetype,
stat_by = stat_by,
stat_plot_type = stat_plot_type,
stat_plot_size = stat_plot_size,
stat_args = stat_args,
graph = graph,
edge_size = edge_size,
edge_alpha = edge_alpha,
edge_color = edge_color,
add_density = add_density,
density_color = density_color,
density_filled = density_filled,
density_filled_palette = density_filled_palette,
density_filled_palcolor = density_filled_palcolor,
lineages = lineages,
lineages_trim = lineages_trim,
lineages_span = lineages_span,
lineages_palette = lineages_palette,
lineages_palcolor = lineages_palcolor,
lineages_arrow = lineages_arrow,
lineages_linewidth = lineages_linewidth,
lineages_line_bg = lineages_line_bg,
lineages_line_bg_stroke = lineages_line_bg_stroke,
lineages_whiskers = lineages_whiskers,
lineages_whiskers_linewidth = lineages_whiskers_linewidth,
lineages_whiskers_alpha = lineages_whiskers_alpha,
velocity = velocity,
velocity_plot_type = velocity_plot_type,
velocity_n_neighbors = velocity_n_neighbors,
velocity_density = velocity_density,
velocity_smooth = velocity_smooth,
velocity_scale = velocity_scale,
velocity_min_mass = velocity_min_mass,
velocity_cutoff_perc = velocity_cutoff_perc,
arrow_alpha = arrow_alpha,
velocity_group_palette = velocity_group_palette,
velocity_group_palcolor = velocity_group_palcolor,
arrow_angle = arrow_angle,
arrow_color = arrow_color,
streamline_l = streamline_l,
streamline_minl = streamline_minl,
streamline_res = streamline_res,
streamline_n = streamline_n,
streamline_width = streamline_width,
streamline_alpha = streamline_alpha,
streamline_color = streamline_color,
streamline_palette = streamline_palette,
streamline_palcolor = streamline_palcolor,
streamline_bg_color = streamline_bg_color,
streamline_bg_stroke = streamline_bg_stroke,
facet_by = facet_by,
facet_scales = facet_scales,
facet_nrow = facet_nrow,
facet_ncol = facet_ncol,
facet_byrow = facet_byrow,
title = title %||% feature,
subtitle = subtitle,
xlab = xlab,
ylab = ylab,
theme = theme,
theme_args = theme_args,
aspect.ratio = aspect.ratio,
legend.position = legend.position,
legend.direction = legend.direction,
raster = raster,
raster_dpi = raster_dpi,
hex = hex,
hex_linewidth = hex_linewidth,
hex_count = hex_count,
hex_bins = hex_bins,
hex_binwidth = hex_binwidth,
palette = palette,
palcolor = palcolor,
palreverse = palreverse,
seed = seed,
...
)
}
)
names(plots) <- features
split_by <- ".features"
} else {
keep_na <- check_keep_na(keep_na, c(facet_by, stat_by, split_by))
keep_empty <- check_keep_empty(
keep_empty,
c(facet_by, stat_by, split_by)
)
split_by_orig <- split_by
split_by <- check_columns(
data,
split_by,
force_factor = TRUE,
allow_multi = TRUE,
concat_multi = TRUE,
concat_sep = split_by_sep
)
if (!is.null(split_by)) {
data <- process_keep_na_empty(
data,
keep_na,
keep_empty,
col = split_by
)
keep_na[split_by_orig] <- NULL
keep_empty[split_by_orig] <- NULL
datas <- split(data, data[[split_by]])
# keep the order of levels
datas <- datas[levels(data[[split_by]])]
if (
is.character(graph) &&
length(graph) == 1 &&
startsWith(graph, "@")
) {
# split the graph as well
datas <- lapply(datas, function(d) {
gh <- attr(data, substring(graph, 2))[
rownames(d),
rownames(d)
]
attr(d, substring(graph, 2)) <- gh
d
})
}
} else {
datas <- list(data)
split_by <- names(datas) <- "..."
}
palette <- check_palette(palette, names(datas))
palcolor <- check_palcolor(palcolor, names(datas))
legend.direction <- check_legend(
legend.direction,
names(datas),
"legend.direction"
)
legend.position <- check_legend(
legend.position,
names(datas),
"legend.position"
)
plots <- lapply(
names(datas),
function(nm) {
default_title <- if (
length(datas) == 1 && identical(nm, "...")
) {
NULL
} else {
nm
}
if (is.function(title)) {
title <- title(default_title)
} else {
title <- title %||% default_title
}
DimPlotAtomic(
datas[[nm]],
dims = dims,
features = features,
lower_quantile = lower_quantile,
upper_quantile = upper_quantile,
lower_cutoff = lower_cutoff,
upper_cutoff = upper_cutoff,
pt_size = pt_size,
pt_alpha = pt_alpha,
bg_color = bg_color,
color_name = color_name,
label_insitu = label_insitu,
show_stat = show_stat,
bg_cutoff = bg_cutoff,
label = label,
label_size = label_size,
label_fg = label_fg,
label_bg = label_bg,
label_bg_r = label_bg_r,
label_repel = label_repel,
label_repulsion = label_repulsion,
label_pt_size = label_pt_size,
label_pt_color = label_pt_color,
label_segment_color = label_segment_color,
order = order,
keep_na = keep_na,
keep_empty = keep_empty,
highlight = highlight,
highlight_alpha = highlight_alpha,
highlight_size = highlight_size,
highlight_color = highlight_color,
highlight_stroke = highlight_stroke,
add_mark = add_mark,
mark_type = mark_type,
mark_expand = mark_expand,
mark_alpha = mark_alpha,
mark_linetype = mark_linetype,
stat_by = stat_by,
stat_plot_type = stat_plot_type,
stat_plot_size = stat_plot_size,
stat_args = stat_args,
graph = graph,
edge_size = edge_size,
edge_alpha = edge_alpha,
edge_color = edge_color,
add_density = add_density,
density_color = density_color,
density_filled = density_filled,
density_filled_palette = density_filled_palette,
density_filled_palcolor = density_filled_palcolor,
lineages = lineages,
lineages_trim = lineages_trim,
lineages_span = lineages_span,
lineages_palette = lineages_palette,
lineages_palcolor = lineages_palcolor,
lineages_arrow = lineages_arrow,
lineages_linewidth = lineages_linewidth,
lineages_line_bg = lineages_line_bg,
lineages_line_bg_stroke = lineages_line_bg_stroke,
lineages_whiskers = lineages_whiskers,
lineages_whiskers_linewidth = lineages_whiskers_linewidth,
lineages_whiskers_alpha = lineages_whiskers_alpha,
velocity = velocity,
velocity_plot_type = velocity_plot_type,
velocity_n_neighbors = velocity_n_neighbors,
velocity_density = velocity_density,
velocity_smooth = velocity_smooth,
velocity_scale = velocity_scale,
velocity_min_mass = velocity_min_mass,
velocity_cutoff_perc = velocity_cutoff_perc,
arrow_alpha = arrow_alpha,
velocity_group_palette = velocity_group_palette,
velocity_group_palcolor = velocity_group_palcolor,
arrow_angle = arrow_angle,
arrow_color = arrow_color,
streamline_l = streamline_l,
streamline_minl = streamline_minl,
streamline_res = streamline_res,
streamline_n = streamline_n,
streamline_width = streamline_width,
streamline_alpha = streamline_alpha,
streamline_color = streamline_color,
streamline_palette = streamline_palette,
streamline_palcolor = streamline_palcolor,
streamline_bg_color = streamline_bg_color,
streamline_bg_stroke = streamline_bg_stroke,
facet_by = facet_by,
facet_scales = facet_scales,
facet_nrow = facet_nrow,
facet_ncol = facet_ncol,
facet_byrow = facet_byrow,
title = title,
subtitle = subtitle,
xlab = xlab,
ylab = ylab,
theme = theme,
theme_args = theme_args,
aspect.ratio = aspect.ratio,
legend.position = legend.position[[nm]],
legend.direction = legend.direction[[nm]],
raster = raster,
raster_dpi = raster_dpi,
hex = hex,
hex_linewidth = hex_linewidth,
hex_count = hex_count,
hex_bins = hex_bins,
hex_binwidth = hex_binwidth,
palette = palette[[nm]],
palcolor = palcolor[[nm]],
palreverse = palreverse,
seed = seed,
...
)
}
)
names(plots) <- names(datas)
}
combine_plots(
plots,
combine = combine,
split_by = split_by,
nrow = nrow,
ncol = ncol,
byrow = byrow,
axes = axes,
axis_titles = axis_titles,
guides = guides,
design = design
)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.