-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* allow rels as units * draft new primitive guide * draft segment keys * document segment keys * rename `label_args()` to `extra_args()` * add segment key tests * document segments primitive * allow for sensible `oppo` parameter in keys * little bit of tuning * add tests for `primitive_segments()` * fix links in docs * allow for `key = <hclust>` in `primitive_segments()` * Add dendrogram guide * fix bug in r-axis when `coord_radial(theta = "y")` * add dendrogram scales * add news bullet * update pkgdown yaml * R CMD Check compliance * add piece about dendrograms
- Loading branch information
Showing
46 changed files
with
1,852 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
# Constructor ------------------------------------------------------------- | ||
|
||
#' Dendrogram guide | ||
#' | ||
#' This axis is a speciality axis for discrete data that has been | ||
#' hierarchically clustered. Please be aware that the guide cannot affect the | ||
#' scale limits, which should be set appropriately. This guide will give | ||
#' misleading results when this step is skipped! | ||
#' | ||
#' @inheritParams primitive_segments | ||
#' @inheritParams primitive_labels | ||
#' @inheritParams common_parameters | ||
#' @param ticks,axis_line Guides to use as ticks or axis lines. Defaults to | ||
#' drawing no ticks or axis lines. Can be specified as one of the following: | ||
#' * A `<Guide>` class object. | ||
#' * A `<function>` that returns a `<Guide>` class object. | ||
#' * A `<character[1]>` naming such a function, without the `guide_` or | ||
#' `primitive_` prefix. | ||
#' @return A `<Guide>` object. | ||
#' @export | ||
#' @family standalone guides | ||
#' | ||
#' @examples | ||
#' # Hierarchically cluster data | ||
#' clust <- hclust(dist(scale(mtcars)), "ave") | ||
#' | ||
#' # Using the guide along with appropriate limits | ||
#' p <- ggplot(mtcars, aes(disp, rownames(mtcars))) + | ||
#' geom_col() + | ||
#' scale_y_discrete(limits = clust$labels[clust$order]) | ||
#' | ||
#' # Standard usage | ||
#' p + guides(y = guide_axis_dendro(clust)) | ||
#' | ||
#' # Adding ticks and axis line | ||
#' p + guides(y = guide_axis_dendro(clust, ticks = "ticks", axis_line = "line")) + | ||
#' theme(axis.line = element_line()) | ||
#' | ||
#' # Controlling space allocated to dendrogram | ||
#' p + guides(y = guide_axis_dendro(clust, space = unit(4, "cm"))) + | ||
#' theme(axis.ticks.y.left = element_line("red")) | ||
#' | ||
#' # If want just the dendrograme, use `primitive_segments()` | ||
#' p + guides(y = primitive_segments(clust), y.sec = "axis") | ||
guide_axis_dendro <- function( | ||
key = "dendro", title = waiver(), theme = NULL, | ||
space = rel(10), vanish = TRUE, | ||
n.dodge = 1, angle = waiver(), check.overlap = FALSE, | ||
ticks = "none", axis_line = "none", | ||
order = 0, position = waiver() | ||
) { | ||
|
||
theme <- replace_null( | ||
theme %||% theme(), | ||
legendry.guide.spacing = unit(0, "cm") | ||
) | ||
|
||
labels <- primitive_labels( | ||
angle = angle, | ||
n.dodge = n.dodge, | ||
check.overlap = check.overlap | ||
) | ||
|
||
dendro <- primitive_segments( | ||
key = key, | ||
space = space, | ||
vanish = vanish | ||
) | ||
|
||
compose_stack( | ||
axis_line, ticks, labels, dendro, | ||
drop = c(3L, 4L), | ||
title = title, theme = theme, order = order, | ||
available_aes = c("any", "x", "y", "r", "theta"), | ||
position = position | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
#' Segment keys | ||
#' | ||
#' @description | ||
#' These functions are helper functions for working with segment data as keys | ||
#' in guides. They all share the goal of creating a guide key, but have | ||
#' different methods: | ||
#' | ||
#' * `key_segment_manual()` directly uses user-provided vectors to set segments. | ||
#' * `key_segment_map()` makes mappings from a `<data.frame>` to set segments. | ||
#' * `key_dendro()` is a specialty case for coercing dendrogram data to segments. | ||
#' Be aware that setting the key alone cannot affect the scale limits, and | ||
#' will give misleading results when used incorrectly! | ||
#' | ||
#' @param value,value_end A vector that is interpreted to be along the scale | ||
#' that the guide codifies. | ||
#' @param oppo,oppo_end A vector that is interpreted to be orthogonal to the | ||
#' `value` and `value_end` variables. | ||
#' @param data A `<data.frame>` or similar object coerced by | ||
#' [`fortify()`][ggplot2::fortify] to a `<data.frame>`, in which the `mapping` | ||
#' argument is evaluated. | ||
#' @param dendro A data structure that can be coerced to a dendrogram through | ||
#' the [`as.dendrogram()`][stats::as.dendrogram()] function. When `NULL` | ||
#' (default) an attempt is made to search for such data in the scale. | ||
#' @param type A string, either `"rectangle"` or `"triangle"`, indicating the | ||
#' shape of edges between nodes of the dendrogram. | ||
#' @param ... [`<data-masking>`][rlang::topic-data-mask] A set of mappings | ||
#' similar to those provided to [`aes()`][ggplot2::aes], which will be | ||
#' evaluated in the `data` argument. | ||
#' For `key_segments_map()`, these *must* contain `value` and `oppo` mappings. | ||
#' @param .call A [call][rlang::topic-error-call] to display in messages. | ||
#' | ||
#' @export | ||
#' @name key_segments | ||
#' @family keys | ||
#' @return | ||
#' For `key_segments_manual()` and `key_segments_map()`, a `<data.frame>` with | ||
#' the `<key_range>` class. | ||
#' | ||
#' @examples | ||
#' # Giving vectors directly | ||
#' key_segment_manual( | ||
#' value = 0:1, value_end = 2:3, | ||
#' oppo = 1:0, oppo_end = 3:2 | ||
#' ) | ||
#' | ||
#' # Taking columns of a data frame | ||
#' data <- data.frame(x = 0:1, y = 1:0, xend = 2:3, yend = 3:2) | ||
#' key_segment_map(data, value = x, oppo = y, value_end = xend, oppo_end = yend) | ||
#' | ||
#' # Using dendrogram data | ||
#' clust <- hclust(dist(USArrests), "ave") | ||
#' key_dendro(clust)(scale_x_discrete()) | ||
key_segment_manual <- function(value, oppo, value_end = value, | ||
oppo_end = oppo, ...) { | ||
df <- data_frame0( | ||
value = value, oppo = oppo, | ||
value_end = value_end, oppo_end = oppo_end, | ||
!!!extra_args(..., .valid_args = .line_params) | ||
) | ||
check_columns(df, c("value", "oppo")) | ||
class(df) <- c("key_segment", "key_guide", class(df)) | ||
df | ||
} | ||
|
||
#' @rdname key_segments | ||
#' @export | ||
key_segment_map <- function(data, ..., .call = caller_env()) { | ||
|
||
mapping <- enquos(...) | ||
mapping <- Filter(Negate(quo_is_missing), mapping) | ||
mapping <- new_aes(mapping, env = .call) | ||
|
||
df <- eval_aes( | ||
data, mapping, | ||
required = c("value", "oppo"), | ||
optional = c("value_end", "oppo_end", .line_params), | ||
call = .call, arg_mapping = "mapping", arg_data = "data" | ||
) | ||
|
||
df$colour <- df$color | ||
df$color <- NULL | ||
df <- rename(df, .line_params, paste0(".", .line_params)) | ||
class(df) <- c("key_segment", "key_guide", class(df)) | ||
df | ||
|
||
} | ||
|
||
#' @rdname key_segments | ||
#' @export | ||
key_dendro <- function(dendro = NULL, type = "rectangle") { | ||
force(dendro) | ||
function(scale, aesthetic = NULL, ...) { | ||
extract_dendro(scale$scale$clust %||% dendro, type = type) | ||
} | ||
} | ||
|
||
# Dendrogram utilities ---------------------------------------------------- | ||
|
||
# Simplified version of `stats:::plotNode`. | ||
# It only looks for the segments and ignores labels and most other attributes. | ||
extract_dendro <- function(tree, type = "rectangle") { | ||
|
||
# Check arguments | ||
whole_tree <- tree <- try_fetch( | ||
stats::as.dendrogram(tree), | ||
error = function(cnd) { | ||
cli::cli_abort("Could not find or coerce {.arg dendro} argument.", parent = cnd) | ||
} | ||
) | ||
type <- arg_match0(type, c("rectangle", "triangle")) | ||
|
||
# Initialise stuff | ||
depth <- 0 | ||
llimit <- list() | ||
x1 <- i <- 1 | ||
x2 <- number_of_members(tree) | ||
KK <- kk <- integer() | ||
|
||
n_obs <- stats::nobs(tree) | ||
n_segments <- switch(type, triangle = 2 * n_obs - 2, 4 * n_obs - 4) | ||
|
||
mtx <- matrix(NA_real_, n_segments, ncol = 4) | ||
colnames(mtx) <- c("value", "oppo", "value_end", "oppo_end") | ||
|
||
repeat { | ||
depth <- depth + 1 | ||
inner <- !stats::is.leaf(tree) && x1 != x2 | ||
|
||
node <- node_limit(x1, x2, tree) | ||
llimit[[depth]] <- node$limit | ||
|
||
ymax <- attr(tree, 'height') | ||
xmax <- node$x | ||
|
||
if (inner) { | ||
for (k in seq_along(tree)) { | ||
child <- tree[[k]] | ||
|
||
ymin <- attr(child, "height") %||% 0 | ||
xmin <- node$limit[k] + (attr(child, "midpoint") %||% 0) | ||
|
||
# Update segments | ||
if (type == "triangle") { | ||
mtx[i, ] <- c(xmax, ymax, xmin, ymin) | ||
i <- i + 1 | ||
} else { | ||
mtx[i + 0:1, ] <- c(xmax, xmin, ymax, ymax, xmin, xmin, ymax, ymin) | ||
i <- i + 2 | ||
} | ||
} | ||
if (length(tree) > 0) { | ||
KK[depth] <- length(tree) | ||
kk[depth] <- 1L | ||
x1 <- node$limit[1L] | ||
x2 <- node$limit[2L] | ||
tree <- tree[[1]] | ||
} | ||
} else { | ||
repeat { | ||
depth <- depth - 1L | ||
if (!depth || kk[depth] < KK[depth]) { | ||
break | ||
} | ||
} | ||
if (!depth) { | ||
break | ||
} | ||
length(kk) <- depth | ||
kk[depth] <- k <- kk[depth] + 1L | ||
x1 <- llimit[[depth]][k] | ||
x2 <- llimit[[depth]][k + 1L] | ||
tree <- whole_tree[[kk]] | ||
} | ||
} | ||
as.data.frame(mtx) | ||
} | ||
|
||
# Copy of `stats:::.memberDend()` | ||
number_of_members <- function(tree) { | ||
attr(tree, "x.member") %||% attr(tree, "members") %||% 1L | ||
} | ||
|
||
# Simplified version of `stats:::plotNodeLimit`, | ||
# It has `center = FALSE` build-in. | ||
node_limit <- function(x1, x2, subtree) { | ||
inner <- !stats::is.leaf(subtree) && x1 != x2 | ||
if (inner) { | ||
K <- length(subtree) | ||
limit <- integer(K) | ||
xx1 <- x1 | ||
for (k in 1L:K) { | ||
xx1 <- xx1 + number_of_members(subtree[[k]]) | ||
limit[k] <- xx1 | ||
} | ||
} else { | ||
limit <- x2 | ||
} | ||
limit <- c(x1, limit) | ||
mid <- attr(subtree, "midpoint") | ||
center <- inner && !is.numeric(mid) | ||
x <- if (center) mean(c(x1, x2)) else x1 + (mid %||% 0) | ||
list(x = x, limit = limit) | ||
} | ||
|
Oops, something went wrong.