|
| 1 | +#' Control aesthetic evaluation |
| 2 | +#' |
| 3 | +#' Most aesthetics are mapped from variables found in the data. Sometimes, |
| 4 | +#' however, you want to delay the mapping until later in the rendering process. |
| 5 | +#' ggplot2 has three stages of the data that you can map aesthetics from. The |
| 6 | +#' default is to map at the beginning, using the layer data provided by the |
| 7 | +#' user. The second stage is after the data has been transformed by the layer |
| 8 | +#' stat. The third and last stage is after the data has been transformed and |
| 9 | +#' mapped by the plot scales. The most common example of mapping from stat |
| 10 | +#' transformed data is the height of bars in [geom_histogram()]: |
| 11 | +#' the height does not come from a variable in the underlying data, but |
| 12 | +#' is instead mapped to the `count` computed by [stat_bin()]. An example of |
| 13 | +#' mapping from scaled data could be to use a desaturated version of the stroke |
| 14 | +#' colour for fill. If you want to map directly from the layer data you should |
| 15 | +#' not do anything special. In order to map from stat transformed data you |
| 16 | +#' should use the `after_stat()` function to flag that evaluation of the |
| 17 | +#' aesthetic mapping should be postponed until after stat transformation. |
| 18 | +#' Similarly, you should use `after_scale()` to flag evaluation of mapping for |
| 19 | +#' after data has been scaled. If you want to map the same aesthetic multiple |
| 20 | +#' times, e.g. map `x` to a data column for the stat, but remap it for the geom, |
| 21 | +#' you can use the `stage()` function to collect multiple mappings. |
| 22 | +#' |
| 23 | +#' `after_stat()` replaces the old approaches of using either `stat()` or |
| 24 | +#' surrounding the variable names with `..`. |
| 25 | +#' |
| 26 | +#' @note Evaluation after stat transformation will only have access to the |
| 27 | +#' variables calculated by the stat. Evaluation after scaling will only have |
| 28 | +#' access to the final aesthetics of the layer (including non-mapped, default |
| 29 | +#' aesthetics). The original layer data can only be accessed at the first stage. |
| 30 | +#' |
| 31 | +#' @param x An aesthetic expression using variables calculated by the stat |
| 32 | +#' (`after_stat()`) or layer aesthetics (`after_scale()`). |
| 33 | +#' @param start An aesthetic expression using variables from the layer data. |
| 34 | +#' @param after_stat An aesthetic expression using variables calculated by the |
| 35 | +#' stat. |
| 36 | +#' @param after_scale An aesthetic expression using layer aesthetics. |
| 37 | +#' |
| 38 | +#' @rdname aes_eval |
| 39 | +#' @name aes_eval |
| 40 | +#' |
| 41 | +#' @examples |
| 42 | +#' # Default histogram display |
| 43 | +#' ggplot(mpg, aes(displ)) + |
| 44 | +#' geom_histogram(aes(y = after_stat(count))) |
| 45 | +#' |
| 46 | +#' # Scale tallest bin to 1 |
| 47 | +#' ggplot(mpg, aes(displ)) + |
| 48 | +#' geom_histogram(aes(y = after_stat(count / max(count)))) |
| 49 | +#' |
| 50 | +#' # Use a transparent version of colour for fill |
| 51 | +#' ggplot(mpg, aes(class, hwy)) + |
| 52 | +#' geom_boxplot(aes(colour = class, fill = after_scale(alpha(colour, 0.4)))) |
| 53 | +#' |
| 54 | +#' # Use stage to modify the scaled fill |
| 55 | +#' ggplot(mpg, aes(class, hwy)) + |
| 56 | +#' geom_boxplot(aes(fill = stage(class, after_scale = alpha(fill, 0.4)))) |
| 57 | +NULL |
| 58 | + |
| 59 | +#' @rdname aes_eval |
| 60 | +#' @export |
| 61 | +after_stat <- function(x) { |
| 62 | + x |
| 63 | +} |
| 64 | +#' @rdname aes_eval |
| 65 | +#' @usage NULL |
| 66 | +#' @export |
| 67 | +stat <- function(x) { |
| 68 | + x |
| 69 | +} |
| 70 | +#' @rdname aes_eval |
| 71 | +#' @export |
| 72 | +after_scale <- function(x) { |
| 73 | + x |
| 74 | +} |
| 75 | +#' @rdname aes_eval |
| 76 | +#' @export |
| 77 | +stage <- function(start = NULL, after_stat = NULL, after_scale = NULL) { |
| 78 | + start |
| 79 | +} |
| 80 | +stage_calculated <- function(start = NULL, after_stat = NULL, after_scale = NULL) { |
| 81 | + after_stat |
| 82 | +} |
| 83 | +stage_scaled <- function(start = NULL, after_stat = NULL, after_scale = NULL) { |
| 84 | + after_scale |
| 85 | +} |
| 86 | + |
| 87 | +# Regex to determine if an identifier refers to a calculated aesthetic |
| 88 | +match_calculated_aes <- "^\\.\\.([a-zA-Z._]+)\\.\\.$" |
| 89 | + |
| 90 | +is_dotted_var <- function(x) { |
| 91 | + grepl(match_calculated_aes, x) |
| 92 | +} |
| 93 | + |
| 94 | +# Determine if aesthetic is calculated |
| 95 | +is_calculated_aes <- function(aesthetics) { |
| 96 | + vapply(aesthetics, is_calculated, logical(1), USE.NAMES = FALSE) |
| 97 | +} |
| 98 | +is_scaled_aes <- function(aesthetics) { |
| 99 | + vapply(aesthetics, is_scaled, logical(1), USE.NAMES = FALSE) |
| 100 | +} |
| 101 | +is_staged_aes <- function(aesthetics) { |
| 102 | + vapply(aesthetics, is_staged, logical(1), USE.NAMES = FALSE) |
| 103 | +} |
| 104 | +is_calculated <- function(x) { |
| 105 | + if (is_call(get_expr(x), "after_stat")) { |
| 106 | + return(TRUE) |
| 107 | + } |
| 108 | + # Support of old recursive behaviour |
| 109 | + if (is.atomic(x)) { |
| 110 | + FALSE |
| 111 | + } else if (is.symbol(x)) { |
| 112 | + is_dotted_var(as.character(x)) |
| 113 | + } else if (is_quosure(x)) { |
| 114 | + is_calculated(quo_get_expr(x)) |
| 115 | + } else if (is.call(x)) { |
| 116 | + if (identical(x[[1]], quote(stat))) { |
| 117 | + TRUE |
| 118 | + } else { |
| 119 | + any(vapply(x, is_calculated, logical(1))) |
| 120 | + } |
| 121 | + } else if (is.pairlist(x)) { |
| 122 | + FALSE |
| 123 | + } else { |
| 124 | + stop("Unknown input:", class(x)[1]) |
| 125 | + } |
| 126 | +} |
| 127 | +is_scaled <- function(x) { |
| 128 | + is_call(get_expr(x), "after_scale") |
| 129 | +} |
| 130 | +is_staged <- function(x) { |
| 131 | + is_call(get_expr(x), "stage") |
| 132 | +} |
| 133 | + |
| 134 | +# Strip dots from expressions |
| 135 | +strip_dots <- function(expr) { |
| 136 | + if (is.atomic(expr)) { |
| 137 | + expr |
| 138 | + } else if (is.name(expr)) { |
| 139 | + expr_ch <- as.character(expr) |
| 140 | + if (nchar(expr_ch) > 0) { |
| 141 | + as.name(gsub(match_calculated_aes, "\\1", expr_ch)) |
| 142 | + } else { |
| 143 | + expr |
| 144 | + } |
| 145 | + } else if (is_quosure(expr)) { |
| 146 | + # strip dots from quosure and reconstruct the quosure |
| 147 | + expr <- new_quosure( |
| 148 | + strip_dots(quo_get_expr(expr)), |
| 149 | + quo_get_env(expr) |
| 150 | + ) |
| 151 | + } else if (is.call(expr)) { |
| 152 | + if (identical(expr[[1]], quote(stat))) { |
| 153 | + strip_dots(expr[[2]]) |
| 154 | + } else { |
| 155 | + expr[-1] <- lapply(expr[-1], strip_dots) |
| 156 | + expr |
| 157 | + } |
| 158 | + } else if (is.pairlist(expr)) { |
| 159 | + # In the unlikely event of an anonymous function |
| 160 | + as.pairlist(lapply(expr, strip_dots)) |
| 161 | + } else if (is.list(expr)) { |
| 162 | + # For list of aesthetics |
| 163 | + lapply(expr, strip_dots) |
| 164 | + } else { |
| 165 | + stop("Unknown input:", class(expr)[1]) |
| 166 | + } |
| 167 | +} |
| 168 | + |
| 169 | +strip_stage <- function(expr) { |
| 170 | + uq_expr <- get_expr(expr) |
| 171 | + if (is_call(uq_expr, c("after_stat", "after_scale"))) { |
| 172 | + uq_expr[[2]] |
| 173 | + } else if (is_call(uq_expr, "stage")) { |
| 174 | + # Prefer stat mapping if present, otherwise original mapping (fallback to |
| 175 | + # scale mapping) but there should always be two arguments to stage() |
| 176 | + uq_expr$after_stat %||% uq_expr$start %||% (if (is.null(uq_expr$after_scale)) uq_expr[[3]]) %||% uq_expr[[2]] |
| 177 | + } else { |
| 178 | + expr |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +# Convert aesthetic mapping into text labels |
| 183 | +make_labels <- function(mapping) { |
| 184 | + default_label <- function(aesthetic, mapping) { |
| 185 | + # e.g., geom_smooth(aes(colour = "loess")) or aes(y = NULL) |
| 186 | + if (is.atomic(mapping)) { |
| 187 | + return(aesthetic) |
| 188 | + } |
| 189 | + mapping <- strip_stage(mapping) |
| 190 | + mapping <- strip_dots(mapping) |
| 191 | + if (is_quosure(mapping) && quo_is_symbol(mapping)) { |
| 192 | + name <- as_string(quo_get_expr(mapping)) |
| 193 | + } else { |
| 194 | + name <- quo_text(mapping) |
| 195 | + name <- gsub("\n.*$", "...", name) |
| 196 | + } |
| 197 | + name |
| 198 | + } |
| 199 | + Map(default_label, names(mapping), mapping) |
| 200 | +} |
0 commit comments