From a9bdb16c96fea525f057e81ef00f5c3563e6786e Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:16:40 +0200 Subject: [PATCH 1/9] eval infrastructure --- R/aes-evaluation.R | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/R/aes-evaluation.R b/R/aes-evaluation.R index 380cce10a8..720bb0d60b 100644 --- a/R/aes-evaluation.R +++ b/R/aes-evaluation.R @@ -204,6 +204,12 @@ stage_scaled <- function(start = NULL, after_stat = NULL, after_scale = NULL) { after_scale } +#' @rdname aes_eval +#' @export +ignore <- function(x) { + x +} + # Regex to determine if an identifier refers to a calculated aesthetic match_calculated_aes <- "^\\.\\.([a-zA-Z._]+)\\.\\.$" @@ -221,6 +227,10 @@ is_scaled_aes <- function(aesthetics) { is_staged_aes <- function(aesthetics) { vapply(aesthetics, is_staged, logical(1), USE.NAMES = FALSE) } +is_ignored_aes <- function(aesthetics) { + vapply(aesthetics, is_ignored, logical(1), USE.NAMES = FALSE) +} + is_calculated <- function(x, warn = FALSE) { if (is_call(get_expr(x), "after_stat")) { return(TRUE) @@ -263,6 +273,9 @@ is_scaled <- function(x) { is_staged <- function(x) { is_call(get_expr(x), "stage") } +is_ignored <- function(x) { + is_call(get_expr(x), "ignore") +} # Strip dots from expressions strip_dots <- function(expr, env, strip_pronoun = FALSE) { From de668cf1f2c7b056a9d098c9bde96cf6439b5412 Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:18:48 +0200 Subject: [PATCH 2/9] skip ignored variables --- R/layer.R | 3 ++- R/scales-.R | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/R/layer.R b/R/layer.R index 98e89540cd..aff3262554 100644 --- a/R/layer.R +++ b/R/layer.R @@ -344,7 +344,8 @@ Layer <- ggproto("Layer", NULL, aesthetics <- defaults(aesthetics, self$stat$default_aes) aesthetics <- compact(aesthetics) - new <- strip_dots(aesthetics[is_calculated_aes(aesthetics) | is_staged_aes(aesthetics)]) + new <- aesthetics[!is_ignored_aes(aesthetics)] + new <- strip_dots(new[is_calculated_aes(new) | is_staged_aes(new)]) if (length(new) == 0) return(data) # data needs to be non-scaled diff --git a/R/scales-.R b/R/scales-.R index 73c490c8a2..2aa6b4673c 100644 --- a/R/scales-.R +++ b/R/scales-.R @@ -145,6 +145,7 @@ ScalesList <- ggproto("ScalesList", NULL, if (is.null(aesthetics)) { return() } + aesthetics <- aesthetics[is_ignored_aes(aesthetics)] names(aesthetics) <- unlist(lapply(names(aesthetics), aes_to_scale)) new_aesthetics <- setdiff(names(aesthetics), self$input()) From 11b21aa6803485d53a0c3a786c4bf2ec0065a684 Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:19:18 +0200 Subject: [PATCH 3/9] Try renaming strategy --- R/layer.R | 16 ++++++++++++++++ R/plot-build.R | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/R/layer.R b/R/layer.R index aff3262554..dcd8af33b7 100644 --- a/R/layer.R +++ b/R/layer.R @@ -430,6 +430,22 @@ Layer <- ggproto("Layer", NULL, data <- self$geom$handle_na(data, self$computed_geom_params) self$geom$draw_layer(data, self$computed_geom_params, layout, layout$coord) + }, + + apply_ignore = function(self, data) { + aesthetics <- self$computed_mapping + ignored <- names(aesthetics)[is_ignored_aes(aesthetics)] + ignored <- names(data) %in% ignored + names(data)[ignored] <- paste0(".ignored_", names(data)[ignored]) + data + }, + + undo_ignore = function(self, data) { + aesthetics <- self$computed_mapping + ignored <- names(aesthetics)[is_ignored_aes(aesthetics)] + ignored <- names(data) %in% paste0(".ignored_", ignored) + names(data)[ignored] <- gsub("^\\.ignored_", "", names(data)[ignored]) + data } ) diff --git a/R/plot-build.R b/R/plot-build.R index 2c1695e350..d23045c3c1 100644 --- a/R/plot-build.R +++ b/R/plot-build.R @@ -51,6 +51,7 @@ ggplot_build.ggplot <- function(plot) { # Compute aesthetics to produce data with generalised variable names data <- by_layer(function(l, d) l$compute_aesthetics(d, plot), layers, data, "computing aesthetics") + data <- by_layer(function(l, d) l$apply_ignore(d), layers, data, "ignoring aesthetics") # Transform all scales data <- lapply(data, scales$transform_df) @@ -62,6 +63,7 @@ ggplot_build.ggplot <- function(plot) { layout$train_position(data, scale_x(), scale_y()) data <- layout$map_position(data) + data <- by_layer(function(l, d) l$undo_ignore(d), layers, data, "unignoring aesthetics") # Apply and map statistics data <- by_layer(function(l, d) l$compute_statistic(d, layout), layers, data, "computing stat") @@ -79,6 +81,7 @@ ggplot_build.ggplot <- function(plot) { # Reset position scales, then re-train and map. This ensures that facets # have control over the range of a plot: is it generated from what is # displayed, or does it include the range of underlying data + data <- by_layer(function(l, d) l$apply_ignore(d), layers, data, "ignoring aesthetics") layout$reset_scales() layout$train_position(data, scale_x(), scale_y()) layout$setup_panel_params() @@ -90,6 +93,7 @@ ggplot_build.ggplot <- function(plot) { lapply(data, npscales$train_df) data <- lapply(data, npscales$map_df) } + data <- by_layer(function(l, d) l$undo_ignore(d), layers, data, "unignoring aesthetics") # Fill in defaults etc. data <- by_layer(function(l, d) l$compute_geom_2(d), layers, data, "setting up geom aesthetics") From c8ae01aabe35a1419e6e31de7898d45bf30064b8 Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:45:59 +0200 Subject: [PATCH 4/9] fix typo --- R/scales-.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/scales-.R b/R/scales-.R index 2aa6b4673c..6a912654eb 100644 --- a/R/scales-.R +++ b/R/scales-.R @@ -145,7 +145,7 @@ ScalesList <- ggproto("ScalesList", NULL, if (is.null(aesthetics)) { return() } - aesthetics <- aesthetics[is_ignored_aes(aesthetics)] + aesthetics <- aesthetics[!is_ignored_aes(aesthetics)] names(aesthetics) <- unlist(lapply(names(aesthetics), aes_to_scale)) new_aesthetics <- setdiff(names(aesthetics), self$input()) From b5c83461c7859a5cc850a1858f2adec35216d55a Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:46:25 +0200 Subject: [PATCH 5/9] directly derive from gglobal_global --- R/position-.R | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/R/position-.R b/R/position-.R index e9ea2ddf6f..2cf27dc940 100644 --- a/R/position-.R +++ b/R/position-.R @@ -78,13 +78,14 @@ transform_position <- function(df, trans_x = NULL, trans_y = NULL, ...) { # Treat df as list during transformation for faster set/get oldclass <- class(df) df <- unclass(df) - scales <- aes_to_scale(names(df)) if (!is.null(trans_x)) { - df[scales == "x"] <- lapply(df[scales == "x"], trans_x, ...) + is_x <- names(df) %in% ggplot_global$x_aes + df[is_x] <- lapply(df[is_x], trans_x, ...) } if (!is.null(trans_y)) { - df[scales == "y"] <- lapply(df[scales == "y"], trans_y, ...) + is_y <- names(df) %in% ggplot_global$y_aes + df[is_y] <- lapply(df[is_y], trans_y, ...) } class(df) <- oldclass From 8cee896c6d310504ab0cb6534b3d5484594fdb91 Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:47:14 +0200 Subject: [PATCH 6/9] helper for ignored aesthetics --- R/layer.R | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/R/layer.R b/R/layer.R index dcd8af33b7..fefc8ca263 100644 --- a/R/layer.R +++ b/R/layer.R @@ -432,17 +432,20 @@ Layer <- ggproto("Layer", NULL, self$geom$draw_layer(data, self$computed_geom_params, layout, layout$coord) }, - apply_ignore = function(self, data) { + ignored_aesthetics = function(self) { aesthetics <- self$computed_mapping - ignored <- names(aesthetics)[is_ignored_aes(aesthetics)] + names(aesthetics)[is_ignored_aes(aesthetics)] + }, + + apply_ignore = function(self, data) { + ignored <- self$ignored_aesthetics() ignored <- names(data) %in% ignored names(data)[ignored] <- paste0(".ignored_", names(data)[ignored]) data }, undo_ignore = function(self, data) { - aesthetics <- self$computed_mapping - ignored <- names(aesthetics)[is_ignored_aes(aesthetics)] + ignored <- self$ignored_aesthetics() ignored <- names(data) %in% paste0(".ignored_", ignored) names(data)[ignored] <- gsub("^\\.ignored_", "", names(data)[ignored]) data From 17cef66ea4a1095efe162b6ee385dd8ee46af44b Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:48:21 +0200 Subject: [PATCH 7/9] overrule ggplot_global temporarily --- R/layer.R | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/R/layer.R b/R/layer.R index fefc8ca263..d194871dd7 100644 --- a/R/layer.R +++ b/R/layer.R @@ -429,6 +429,20 @@ Layer <- ggproto("Layer", NULL, } data <- self$geom$handle_na(data, self$computed_geom_params) + + ignored <- self$ignored_aesthetics() + if (any(ignored %in% c(ggplot_global$x_aes, ggplot_global$y_aes))) { + # Temporarily override global x/y aesthetics + old_x <- ggplot_global$x_aes + old_y <- ggplot_global$y_aes + on.exit({ + ggplot_global$x_aes <- old_x + ggplot_global$y_aes <- old_y + }, add = TRUE) + ggplot_global$x_aes <- setdiff(old_x, ignored) + ggplot_global$y_aes <- setdiff(old_y, ignored) + } + self$geom$draw_layer(data, self$computed_geom_params, layout, layout$coord) }, From 5cba616d133457e89c59ada46ea6e847cf73fc25 Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Wed, 13 Sep 2023 13:17:01 +0200 Subject: [PATCH 8/9] Ignored aesthetics in `annotation()` --- R/annotation.R | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/R/annotation.R b/R/annotation.R index ca1530462b..8d0f3aa6d1 100644 --- a/R/annotation.R +++ b/R/annotation.R @@ -70,8 +70,17 @@ annotate <- function(geom, x = NULL, y = NULL, xmin = NULL, xmax = NULL, details <- paste0(names(aesthetics)[bad], " (", lengths[bad], ")") cli::cli_abort("Unequal parameter lengths: {details}") } - data <- data_frame0(!!!position, .size = n) + + # Re-inject potential `ignore()` expressions + mapping <- aes_all(names(data)) + call <- call_match(fn = annotate) + aesthetics <- intersect(names(call), names(mapping)) + for (aes in aesthetics[is_ignored_aes(call[aesthetics])]) { + expr <- quo_get_expr(mapping[[aes]]) + mapping[[aes]] <- quo(ignore(!!expr)) + } + layer( geom = geom, params = list( @@ -81,7 +90,7 @@ annotate <- function(geom, x = NULL, y = NULL, xmin = NULL, xmax = NULL, stat = StatIdentity, position = PositionIdentity, data = data, - mapping = aes_all(names(data)), + mapping = mapping, inherit.aes = FALSE, show.legend = FALSE ) From 49b275a706d62319a5ad3727b85e42cb714545e6 Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Wed, 13 Sep 2023 14:15:33 +0200 Subject: [PATCH 9/9] Prefer `local_bindings()` over `on.exit()` --- R/layer.R | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/R/layer.R b/R/layer.R index d194871dd7..38adf11d7b 100644 --- a/R/layer.R +++ b/R/layer.R @@ -432,15 +432,13 @@ Layer <- ggproto("Layer", NULL, ignored <- self$ignored_aesthetics() if (any(ignored %in% c(ggplot_global$x_aes, ggplot_global$y_aes))) { - # Temporarily override global x/y aesthetics - old_x <- ggplot_global$x_aes - old_y <- ggplot_global$y_aes - on.exit({ - ggplot_global$x_aes <- old_x - ggplot_global$y_aes <- old_y - }, add = TRUE) - ggplot_global$x_aes <- setdiff(old_x, ignored) - ggplot_global$y_aes <- setdiff(old_y, ignored) + # We temporarily redefine x/y aesthetics to have coords skip + # transformation of ignored aesthetics + local_bindings( + x_aes = setdiff(ggplot_global$x_aes, ignored), + y_aes = setdiff(ggplot_global$y_aes, ignored), + .env = ggplot_global + ) } self$geom$draw_layer(data, self$computed_geom_params, layout, layout$coord)