From 0da5bf3497f00f826dc78c4cabf1ba41fbe8f60f Mon Sep 17 00:00:00 2001 From: hadley Date: Fri, 19 Jan 2018 15:52:59 -0600 Subject: [PATCH 01/19] First pass at tidyeval #2412 --- NAMESPACE | 2 -- R/aes-calculated.r | 2 +- R/aes.r | 29 +++++++++++++++-------------- R/geom-.r | 6 ++++-- R/layer.r | 12 +++++++----- R/plot.r | 21 +++++++++++++-------- R/scale-type.R | 4 ++-- R/scales-.r | 9 +++------ man/ggplot.Rd | 6 ++---- 9 files changed, 47 insertions(+), 44 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index abeb5f4906..6de694cbe6 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -6,7 +6,6 @@ S3method("+",gg) S3method("[",uneval) S3method("[[",ggproto) S3method(.DollarNames,ggproto) -S3method(as.character,uneval) S3method(as.list,ggproto) S3method(autolayer,default) S3method(autoplot,default) @@ -107,7 +106,6 @@ S3method(scale_type,logical) S3method(scale_type,numeric) S3method(scale_type,ordered) S3method(scale_type,sfc) -S3method(str,uneval) S3method(summary,ggplot) S3method(widthDetails,titleGrob) S3method(widthDetails,zeroGrob) diff --git a/R/aes-calculated.r b/R/aes-calculated.r index 6a4736116d..94dec22d84 100644 --- a/R/aes-calculated.r +++ b/R/aes-calculated.r @@ -91,7 +91,7 @@ make_labels <- function(mapping) { if (is.atomic(mapping)) { aesthetic } else { - x <- deparse(strip_dots(mapping)) + x <- rlang::quo_text(strip_dots(mapping)) if (length(x) > 1) { x <- paste0(x[[1]], "...") } diff --git a/R/aes.r b/R/aes.r index 475b191642..f147676f81 100644 --- a/R/aes.r +++ b/R/aes.r @@ -58,12 +58,18 @@ NULL #' # Aesthetics supplied to ggplot() are used as defaults for every layer #' # you can override them, or supply different aesthetics for each layer aes <- function(x, y, ...) { - exprs <- rlang::enexprs(x = x, y = y, ...) - is_missing <- vapply(exprs, rlang::is_missing, logical(1)) + exprs <- rlang::enquos(x = x, y = y, ...) + is_missing <- vapply(exprs, rlang::quo_is_missing, logical(1)) - aes <- structure(exprs[!is_missing], class = "uneval") + aes <- new_aes(exprs[!is_missing]) rename_aes(aes) } + +new_aes <- function(x) { + stopifnot(is.list(x)) + structure(x, class = "uneval") +} + #' @export print.uneval <- function(x, ...) { cat("Aesthetic mapping: \n") @@ -71,23 +77,18 @@ print.uneval <- function(x, ...) { if (length(x) == 0) { cat("\n") } else { - values <- vapply(x, deparse2, character(1)) - bullets <- paste0("* ", format(names(x)), " -> ", values, "\n") + values <- vapply(x, rlang::quo_label, character(1)) + bullets <- paste0("* `", format(names(x)), "` -> ", values, "\n") cat(bullets, sep = "") } -} -#' @export -str.uneval <- function(object, ...) utils::str(unclass(object), ...) -#' @export -"[.uneval" <- function(x, i, ...) structure(unclass(x)[i], class = "uneval") + invisible(x) +} #' @export -as.character.uneval <- function(x, ...) { - char <- as.character(unclass(x)) - names(char) <- names(x) - char +"[.uneval" <- function(x, i, ...) { + new_aes(NextMethod()) } # Rename American or old-style aesthetics name diff --git a/R/geom-.r b/R/geom-.r index 8748155c40..cae0a47be6 100644 --- a/R/geom-.r +++ b/R/geom-.r @@ -108,10 +108,12 @@ Geom <- ggproto("Geom", use_defaults = function(self, data, params = list()) { # Fill in missing aesthetics with their defaults missing_aes <- setdiff(names(self$default_aes), names(data)) + + missing_eval <- lapply(self$default_aes[missing_aes], rlang::eval_tidy) if (empty(data)) { - data <- plyr::quickdf(self$default_aes[missing_aes]) + data <- plyr::quickdf(missing_eval) } else { - data[missing_aes] <- self$default_aes[missing_aes] + data[missing_aes] <- missing_eval } # Override mappings with params diff --git a/R/layer.r b/R/layer.r index 5a5a3b15ea..bfe8763110 100644 --- a/R/layer.r +++ b/R/layer.r @@ -139,6 +139,7 @@ layer <- function(geom = NULL, stat = NULL, } + subset <- rlang::enquos(subset) ggproto("LayerInstance", Layer, geom = geom, @@ -211,15 +212,16 @@ Layer <- ggproto("Layer", NULL, # Old subsetting method if (!is.null(self$subset)) { - include <- data.frame(plyr::eval.quoted(self$subset, data, plot$env)) - data <- data[rowSums(include, na.rm = TRUE) == ncol(include), ] + res <- rlang::eval_tidy(self$subset, data = data) + res <- res & !is.na(res) + data <- data[res, , drop = FALSE] } - scales_add_defaults(plot$scales, data, aesthetics, plot$plot_env) + scales_add_defaults(plot$scales, data, aesthetics) # Evaluate and check aesthetics aesthetics <- compact(aesthetics) - evaled <- lapply(aesthetics, eval, envir = data, enclos = plot$plot_env) + evaled <- lapply(aesthetics, rlang::eval_tidy, data = data) n <- nrow(data) if (n == 0) { @@ -275,7 +277,7 @@ Layer <- ggproto("Layer", NULL, names(stat_data) <- names(new) # Add any new scales, if needed - scales_add_defaults(plot$scales, data, new, plot$plot_env) + scales_add_defaults(plot$scales, data, new) # Transform the values, if the scale say it's ok # (see stat_spoke for one exception) if (self$stat$retransform) { diff --git a/R/plot.r b/R/plot.r index 9d1b6b768e..7e475dd157 100644 --- a/R/plot.r +++ b/R/plot.r @@ -32,9 +32,7 @@ #' @param mapping Default list of aesthetic mappings to use for plot. #' If not specified, must be supplied in each layer added to the plot. #' @param ... Other arguments passed on to methods. Not currently used. -#' @param environment If a variable defined in the aesthetic mapping is not -#' found in the data, ggplot will look for it in this environment. It defaults -#' to using the environment in which `ggplot()` is called. +#' @param environment DEPRECATED. Used prior to tidy evaluation. #' @export #' @examples #' # Generate some sample data, then compute mean and standard deviation @@ -72,23 +70,30 @@ #' width = 0.4 #' ) ggplot <- function(data = NULL, mapping = aes(), ..., - environment = parent.frame()) { + environment = NULL) { UseMethod("ggplot") } #' @export ggplot.default <- function(data = NULL, mapping = aes(), ..., - environment = parent.frame()) { + environment = NULL) { ggplot.data.frame(fortify(data, ...), mapping, environment = environment) } #' @export ggplot.data.frame <- function(data, mapping = aes(), ..., - environment = parent.frame()) { + environment = NULL) { if (!missing(mapping) && !inherits(mapping, "uneval")) { stop("Mapping should be created with `aes() or `aes_()`.", call. = FALSE) } + if (!is.null(environment)) { + stop( + "`environment` is deprecated: environments are now captured by `aes()`", + call. = FALSE + ) + } + p <- structure(list( data = data, layers = list(), @@ -97,7 +102,7 @@ ggplot.data.frame <- function(data, mapping = aes(), ..., theme = list(), coordinates = coord_cartesian(default = TRUE), facet = facet_null(), - plot_env = environment + plot_env = parent.frame() ), class = c("gg", "ggplot")) p$labels <- make_labels(mapping) @@ -108,7 +113,7 @@ ggplot.data.frame <- function(data, mapping = aes(), ..., #' @export ggplot.grouped_df <- function(data, mapping = aes(), ..., - environment = parent.frame()) { + environment = NULL) { data$.group <- dplyr::group_indices(data) mapping$group <- mapping$group %||% quote(.group) diff --git a/R/scale-type.R b/R/scale-type.R index 9aa0fbec21..c7b54267dd 100644 --- a/R/scale-type.R +++ b/R/scale-type.R @@ -1,9 +1,9 @@ -find_scale <- function(aes, x, env = parent.frame()) { +find_scale <- function(aes, x) { type <- scale_type(x) candidates <- paste("scale", aes, type, sep = "_") for (scale in candidates) { - scale_f <- find_global(scale, env, mode = "function") + scale_f <- find_global(scale, globalenv(), mode = "function") if (!is.null(scale_f)) return(scale_f()) } diff --git a/R/scales-.r b/R/scales-.r index 2330d8ae61..3107bb2e52 100644 --- a/R/scales-.r +++ b/R/scales-.r @@ -88,7 +88,7 @@ scales_transform_df <- function(scales, df) { # @param aesthetics A list of aesthetic-variable mappings. The name of each # item is the aesthetic, and the value of each item is the variable in data. -scales_add_defaults <- function(scales, data, aesthetics, env) { +scales_add_defaults <- function(scales, data, aesthetics) { if (is.null(aesthetics)) return() names(aesthetics) <- unlist(lapply(names(aesthetics), aes_to_scale)) @@ -96,13 +96,10 @@ scales_add_defaults <- function(scales, data, aesthetics, env) { # No new aesthetics, so no new scales to add if (is.null(new_aesthetics)) return() - datacols <- plyr::tryapply( - aesthetics[new_aesthetics], eval, - envir = data, enclos = env - ) + datacols <- lapply(aesthetics[new_aesthetics], rlang::eval_tidy, data = data) for (aes in names(datacols)) { - scales$add(find_scale(aes, datacols[[aes]], env)) + scales$add(find_scale(aes, datacols[[aes]])) } } diff --git a/man/ggplot.Rd b/man/ggplot.Rd index ec9a6caa07..10551e10ca 100644 --- a/man/ggplot.Rd +++ b/man/ggplot.Rd @@ -4,7 +4,7 @@ \alias{ggplot} \title{Create a new ggplot} \usage{ -ggplot(data = NULL, mapping = aes(), ..., environment = parent.frame()) +ggplot(data = NULL, mapping = aes(), ..., environment = NULL) } \arguments{ \item{data}{Default dataset to use for plot. If not already a data.frame, @@ -16,9 +16,7 @@ If not specified, must be supplied in each layer added to the plot.} \item{...}{Other arguments passed on to methods. Not currently used.} -\item{environment}{If a variable defined in the aesthetic mapping is not -found in the data, ggplot will look for it in this environment. It defaults -to using the environment in which \code{ggplot()} is called.} +\item{environment}{DEPRECATED. Used prior to tidy evaluation.} } \description{ \code{ggplot()} initializes a ggplot object. It can be used to From 4012ae4bbe07d293997f8011c5bd9afefba56a80 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 22 Feb 2018 16:48:06 +0100 Subject: [PATCH 02/19] Fix `subset` tidy eval compat --- R/layer.r | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/layer.r b/R/layer.r index bfe8763110..01e1dfd466 100644 --- a/R/layer.r +++ b/R/layer.r @@ -139,7 +139,7 @@ layer <- function(geom = NULL, stat = NULL, } - subset <- rlang::enquos(subset) + subset <- rlang::enquo(subset) ggproto("LayerInstance", Layer, geom = geom, @@ -211,7 +211,7 @@ Layer <- ggproto("Layer", NULL, } # Old subsetting method - if (!is.null(self$subset)) { + if (!rlang::quo_is_null(self$subset)) { res <- rlang::eval_tidy(self$subset, data = data) res <- res & !is.na(res) data <- data[res, , drop = FALSE] From 2ac926773dad8aa6744252d85189dce78ad5918d Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 22 Feb 2018 16:56:26 +0100 Subject: [PATCH 03/19] Evaluate statistics with eval_tidy() --- R/layer.r | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/layer.r b/R/layer.r index 01e1dfd466..5ca0afb154 100644 --- a/R/layer.r +++ b/R/layer.r @@ -273,7 +273,7 @@ Layer <- ggproto("Layer", NULL, env <- new.env(parent = baseenv()) env$calc <- calc - stat_data <- plyr::quickdf(lapply(new, eval, data, env)) + stat_data <- plyr::quickdf(lapply(new, rlang::eval_tidy, data, env)) names(stat_data) <- names(new) # Add any new scales, if needed From a1531d4b368e3c26d5aeb1c83868f564e56881ea Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 22 Feb 2018 17:35:09 +0100 Subject: [PATCH 04/19] Handle quosures in detection of mapped aesthetics --- R/aes.r | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/R/aes.r b/R/aes.r index f147676f81..2f4a2ce1b1 100644 --- a/R/aes.r +++ b/R/aes.r @@ -244,7 +244,10 @@ aes_auto <- function(data = NULL, ...) { } mapped_aesthetics <- function(x) { - is_null <- vapply(x, is.null, logical(1)) - names(x)[!is_null] + if (is.null(x)) { + return(NULL) + } + is_null <- vapply(x, rlang::quo_is_null, logical(1)) + names(x)[!is_null] } From 9625379221ad8d2916f953af8dc1dd157a33b15c Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 22 Feb 2018 17:49:13 +0100 Subject: [PATCH 05/19] Port SE versions of aes() to tidy eval --- R/aes.r | 20 +++++++++++++------- tests/testthat/test-aes.r | 12 ++++++------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/R/aes.r b/R/aes.r index 2f4a2ce1b1..47a32ff7ee 100644 --- a/R/aes.r +++ b/R/aes.r @@ -158,17 +158,19 @@ aes_ <- function(x, y, ...) { if (!missing(x)) mapping["x"] <- list(x) if (!missing(y)) mapping["y"] <- list(y) - as_call <- function(x) { + caller_env <- parent.frame() + + as_quosure_aes <- function(x) { if (is.formula(x) && length(x) == 2) { - x[[2]] + rlang::as_quosure(x) } else if (is.call(x) || is.name(x) || is.atomic(x)) { - x + rlang::new_quosure(x, caller_env) } else { stop("Aesthetic must be a one-sided formula, call, name, or constant.", call. = FALSE) } } - mapping <- lapply(mapping, as_call) + mapping <- lapply(mapping, as_quosure_aes) structure(rename_aes(mapping), class = "uneval") } @@ -179,11 +181,13 @@ aes_string <- function(x, y, ...) { if (!missing(x)) mapping["x"] <- list(x) if (!missing(y)) mapping["y"] <- list(y) + caller_env <- parent.frame() + mapping <- lapply(mapping, function(x) { if (is.character(x)) { - parse(text = x)[[1]] + rlang::parse_quo(x, env = caller_env) } else { - x + rlang::new_quosure(x, env = caller_env) } }) structure(rename_aes(mapping), class = "uneval") @@ -205,8 +209,10 @@ aes_all <- function(vars) { names(vars) <- vars vars <- rename_aes(vars) + # Quosure the symbols in the empty environment because they can only + # refer to the data mask structure( - lapply(vars, as.name), + lapply(vars, function(x) rlang::new_quosure(as.name(x), emptyenv())), class = "uneval" ) } diff --git a/tests/testthat/test-aes.r b/tests/testthat/test-aes.r index d948d11a74..903705553c 100644 --- a/tests/testthat/test-aes.r +++ b/tests/testthat/test-aes.r @@ -2,25 +2,25 @@ context("Creating aesthetic mappings") test_that("aes() captures input expressions", { out <- aes(mpg, wt + 1) - expect_equal(out$x, quote(mpg)) - expect_equal(out$y, quote(wt + 1)) + expect_identical(out$x, rlang::quo(mpg)) + expect_identical(out$y, rlang::quo(wt + 1)) }) test_that("aes_q() uses quoted calls and formulas", { out <- aes_q(quote(mpg), ~ wt + 1) - expect_equal(out$x, quote(mpg)) - expect_equal(out$y, quote(wt + 1)) + expect_identical(out$x, rlang::quo(mpg)) + expect_identical(out$y, rlang::quo(wt + 1)) }) test_that("aes_string() parses strings", { - expect_equal(aes_string("a + b")$x, quote(a + b)) + expect_equal(aes_string("a + b")$x, rlang::quo(a + b)) }) test_that("aes_string() doesn't parse non-strings", { old <- options(OutDec = ",") on.exit(options(old)) - expect_equal(aes_string(0.4)$x, 0.4) + expect_identical(aes_string(0.4)$x, rlang::new_quosure(0.4)) }) test_that("aes_q() & aes_string() preserves explicit NULLs", { From d6355ad8ab8e7936426c9e170bca0fbf0b045382 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 22 Feb 2018 18:22:47 +0100 Subject: [PATCH 06/19] Fix tests with tidy eval aes() --- tests/testthat/test-aes-calculated.r | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/testthat/test-aes-calculated.r b/tests/testthat/test-aes-calculated.r index 8e35a1059e..4a62688912 100644 --- a/tests/testthat/test-aes-calculated.r +++ b/tests/testthat/test-aes-calculated.r @@ -16,11 +16,11 @@ test_that("call to calc() is calculated", { }) test_that("strip_dots remove dots around calculated aesthetics", { - expect_equal(strip_dots(aes(..density..))$x, quote(density)) - expect_equal(strip_dots(aes(mean(..density..)))$x, quote(mean(density))) + expect_identical(strip_dots(aes(..density..))$x, rlang::quo(density)) + expect_identical(strip_dots(aes(mean(..density..)))$x, rlang::quo(mean(density))) expect_equal( strip_dots(aes(sapply(..density.., function(x) mean(x)))$x), - quote(sapply(density, function(x) mean(x))) + rlang::quo(sapply(density, function(x) mean(x))) ) }) From 1851d3b2fed6117e2bf10b81329d5e424b47d4c2 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 22 Feb 2018 20:30:27 +0100 Subject: [PATCH 07/19] Coerce `uneval` replacements to quosure for compatibility --- NAMESPACE | 3 +++ R/aes.r | 20 ++++++++++++++++++++ R/utilities.r | 11 +++++++++++ tests/testthat/test-aes.r | 12 ++++++++++++ 4 files changed, 46 insertions(+) diff --git a/NAMESPACE b/NAMESPACE index 6de694cbe6..9ac3573b9b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,9 +2,12 @@ S3method("$",ggproto) S3method("$",ggproto_parent) +S3method("$<-",uneval) S3method("+",gg) S3method("[",uneval) +S3method("[<-",uneval) S3method("[[",ggproto) +S3method("[[<-",uneval) S3method(.DollarNames,ggproto) S3method(as.list,ggproto) S3method(autolayer,default) diff --git a/R/aes.r b/R/aes.r index 47a32ff7ee..9b30f552e3 100644 --- a/R/aes.r +++ b/R/aes.r @@ -91,6 +91,26 @@ print.uneval <- function(x, ...) { new_aes(NextMethod()) } +# If necessary coerce replacements to quosures for compatibility +#' @export +"[[<-.uneval" <- function(x, i, value) { + x <- unclass(x) + x[[i]] <- ensure_quosure(value) + new_aes(x) +} +#' @export +"$<-.uneval" <- function(x, i, value) { + i <- rlang::as_string(i) + x[[i]] <- value + x +} +#' @export +"[<-.uneval" <- function(x, i, value) { + x <- unclass(x) + x[i] <- lapply(value, ensure_quosure) + new_aes(x) +} + # Rename American or old-style aesthetics name rename_aes <- function(x) { # Convert prefixes to full names diff --git a/R/utilities.r b/R/utilities.r index 01a551412f..482c778357 100644 --- a/R/utilities.r +++ b/R/utilities.r @@ -397,3 +397,14 @@ with_seed_null <- function(seed, code) { # Needed to trigger package loading #' @importFrom tibble tibble NULL + +ensure_quosure <- function(x, env = baseenv()) { + if (rlang::is_quosure(x)) { + return(x) + } + + if (!rlang::is_symbolic(x)) { + env <- emptyenv() + } + rlang::new_quosure(x, env) +} diff --git a/tests/testthat/test-aes.r b/tests/testthat/test-aes.r index 903705553c..3259faa46f 100644 --- a/tests/testthat/test-aes.r +++ b/tests/testthat/test-aes.r @@ -59,6 +59,18 @@ test_that("aes evaluated in environment where plot created", { expect_equal(layer_data(f())$x, 10) }) +test_that("assignment methods create quosures", { + mapping <- aes(a, b, c = c) + mapping[1] <- list(quote(foo)) + expect_identical(mapping[[1]], rlang::new_quosure(quote(foo), baseenv())) + + mapping[[2]] <- quote(bar) + expect_identical(mapping[[2]], rlang::new_quosure(quote(bar), baseenv())) + + mapping$c <- quote(baz) + expect_identical(mapping[[3]], rlang::new_quosure(quote(baz), baseenv())) +}) + # Visual tests ------------------------------------------------------------ From 2059a442b5e00f91932fa005993393a504a72260 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 22 Feb 2018 20:39:55 +0100 Subject: [PATCH 08/19] Ensure `mapping` argument of layer() contains quosures --- R/layer.r | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/R/layer.r b/R/layer.r index 5ca0afb154..9a3bdfc893 100644 --- a/R/layer.r +++ b/R/layer.r @@ -84,16 +84,9 @@ layer <- function(geom = NULL, stat = NULL, } data <- fortify(data) - if (!is.null(mapping) && !inherits(mapping, "uneval")) { - msg <- paste0("`mapping` must be created by `aes()`") - if (inherits(mapping, "ggplot")) { - msg <- paste0( - msg, "\n", - "Did you use %>% instead of +?" - ) - } - stop(msg, call. = FALSE) + if (!is.null(mapping)) { + mapping <- validate_mapping(mapping) } if (is.character(geom)) @@ -156,6 +149,23 @@ layer <- function(geom = NULL, stat = NULL, ) } +validate_mapping <- function(mapping) { + if (!inherits(mapping, "uneval")) { + msg <- paste0("`mapping` must be created by `aes()`") + if (inherits(mapping, "ggplot")) { + msg <- paste0( + msg, "\n", + "Did you use %>% instead of +?" + ) + } + + stop(msg, call. = FALSE) + } + + # For backward compatibility with pre-tidy-eval layers + new_aes(lapply(mapping, ensure_quosure)) +} + Layer <- ggproto("Layer", NULL, geom = NULL, geom_params = NULL, From 76e809cccf9e2fe3681b325b9e10a10fe395aef6 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 22 Feb 2018 21:19:40 +0100 Subject: [PATCH 09/19] Account for quosures in summarise_layers() tests --- tests/testthat/test-plot-summary-api.R | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/testthat/test-plot-summary-api.R b/tests/testthat/test-plot-summary-api.R index 7720ef2066..4fa3e44073 100644 --- a/tests/testthat/test-plot-summary-api.R +++ b/tests/testthat/test-plot-summary-api.R @@ -120,10 +120,12 @@ test_that("coord summary - coord_flip", { test_that("summarise_layers", { l <- summarise_layers(ggplot_build(p)) - expect_identical(l$mapping[[1]], list(x = quote(displ), y = quote(hwy))) + expect_equal(l$mapping[[1]], list(x = rlang::quo(displ), y = rlang::quo(hwy))) p2 <- p + geom_point(aes(x = displ/2, y = hwy/2)) l2 <- summarise_layers(ggplot_build(p2)) - expect_identical(l2$mapping[[1]], list(x = quote(displ), y = quote(hwy))) - expect_identical(l2$mapping[[2]], list(x = quote(displ/2), y = quote(hwy/2))) + expect_equal(l2$mapping[[1]], list(x = rlang::quo(displ), y = rlang::quo(hwy))) + + # Here use _identical because the quosures are supposed to be local + expect_identical(l2$mapping[[2]], list(x = rlang::quo(displ/2), y = rlang::quo(hwy/2))) }) From 1270bcfc7e49b9b48acb4b3d05b20e5f4f861f38 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 22 Feb 2018 22:30:30 +0100 Subject: [PATCH 10/19] Disable qplot() tests --- R/quick-plot.r | 2 ++ 1 file changed, 2 insertions(+) diff --git a/R/quick-plot.r b/R/quick-plot.r index d426db40e5..2ade3d11bb 100644 --- a/R/quick-plot.r +++ b/R/quick-plot.r @@ -65,6 +65,8 @@ qplot <- function(x, y = NULL, ..., data, facets = NULL, margins = FALSE, xlab = deparse(substitute(x)), ylab = deparse(substitute(y)), asp = NA, stat = NULL, position = NULL) { + testthat::skip("qplot") + if (!missing(stat)) warning("`stat` is deprecated", call. = FALSE) if (!missing(position)) warning("`position` is deprecated", call. = FALSE) if (!is.character(geom)) stop("`geom` must be a character vector", call. = FALSE) From 1f39e138391259ebef95e35e39fa1d3579f1984c Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Wed, 28 Feb 2018 15:27:45 +0100 Subject: [PATCH 11/19] Unwrap constants from quosures in aes objects --- R/aes.r | 45 ++++++++++++++++++++++++--------------- R/layer.r | 2 +- R/utilities.r | 11 ---------- tests/testthat/test-aes.r | 27 ++++++++++++++++++----- 4 files changed, 51 insertions(+), 34 deletions(-) diff --git a/R/aes.r b/R/aes.r index 9b30f552e3..7ae55c9931 100644 --- a/R/aes.r +++ b/R/aes.r @@ -61,12 +61,30 @@ aes <- function(x, y, ...) { exprs <- rlang::enquos(x = x, y = y, ...) is_missing <- vapply(exprs, rlang::quo_is_missing, logical(1)) - aes <- new_aes(exprs[!is_missing]) + aes <- new_aes(exprs[!is_missing], env = parent.frame()) rename_aes(aes) } -new_aes <- function(x) { +# Wrap symbolic objects in quosures but pull out constants out of +# quosures for backward-compatibility +new_aesthetic <- function(x, env = globalenv()) { + if (rlang::is_quosure(x)) { + if (!rlang::quo_is_symbolic(x)) { + x <- rlang::quo_get_expr(x) + } + return(x) + } + + if (rlang::is_symbolic(x)) { + x <- rlang::new_quosure(x, env = env) + return(x) + } + + x +} +new_aes <- function(x, env = globalenv()) { stopifnot(is.list(x)) + x <- lapply(x, new_aesthetic, env = env) structure(x, class = "uneval") } @@ -94,21 +112,15 @@ print.uneval <- function(x, ...) { # If necessary coerce replacements to quosures for compatibility #' @export "[[<-.uneval" <- function(x, i, value) { - x <- unclass(x) - x[[i]] <- ensure_quosure(value) - new_aes(x) + new_aes(NextMethod()) } #' @export "$<-.uneval" <- function(x, i, value) { - i <- rlang::as_string(i) - x[[i]] <- value - x + new_aes(NextMethod()) } #' @export "[<-.uneval" <- function(x, i, value) { - x <- unclass(x) - x[i] <- lapply(value, ensure_quosure) - new_aes(x) + new_aes(NextMethod()) } # Rename American or old-style aesthetics name @@ -184,7 +196,7 @@ aes_ <- function(x, y, ...) { if (is.formula(x) && length(x) == 2) { rlang::as_quosure(x) } else if (is.call(x) || is.name(x) || is.atomic(x)) { - rlang::new_quosure(x, caller_env) + new_aesthetic(x, caller_env) } else { stop("Aesthetic must be a one-sided formula, call, name, or constant.", call. = FALSE) @@ -202,14 +214,13 @@ aes_string <- function(x, y, ...) { if (!missing(y)) mapping["y"] <- list(y) caller_env <- parent.frame() - mapping <- lapply(mapping, function(x) { if (is.character(x)) { - rlang::parse_quo(x, env = caller_env) - } else { - rlang::new_quosure(x, env = caller_env) + x <- rlang::parse_expr(x) } + new_aesthetic(x, env = caller_env) }) + structure(rename_aes(mapping), class = "uneval") } @@ -274,6 +285,6 @@ mapped_aesthetics <- function(x) { return(NULL) } - is_null <- vapply(x, rlang::quo_is_null, logical(1)) + is_null <- vapply(x, is.null, logical(1)) names(x)[!is_null] } diff --git a/R/layer.r b/R/layer.r index 9a3bdfc893..d6b3cddb02 100644 --- a/R/layer.r +++ b/R/layer.r @@ -163,7 +163,7 @@ validate_mapping <- function(mapping) { } # For backward compatibility with pre-tidy-eval layers - new_aes(lapply(mapping, ensure_quosure)) + new_aes(mapping) } Layer <- ggproto("Layer", NULL, diff --git a/R/utilities.r b/R/utilities.r index 482c778357..01a551412f 100644 --- a/R/utilities.r +++ b/R/utilities.r @@ -397,14 +397,3 @@ with_seed_null <- function(seed, code) { # Needed to trigger package loading #' @importFrom tibble tibble NULL - -ensure_quosure <- function(x, env = baseenv()) { - if (rlang::is_quosure(x)) { - return(x) - } - - if (!rlang::is_symbolic(x)) { - env <- emptyenv() - } - rlang::new_quosure(x, env) -} diff --git a/tests/testthat/test-aes.r b/tests/testthat/test-aes.r index 3259faa46f..9aed612ab8 100644 --- a/tests/testthat/test-aes.r +++ b/tests/testthat/test-aes.r @@ -20,7 +20,7 @@ test_that("aes_string() doesn't parse non-strings", { old <- options(OutDec = ",") on.exit(options(old)) - expect_identical(aes_string(0.4)$x, rlang::new_quosure(0.4)) + expect_identical(aes_string(0.4)$x, 0.4) }) test_that("aes_q() & aes_string() preserves explicit NULLs", { @@ -59,16 +59,33 @@ test_that("aes evaluated in environment where plot created", { expect_equal(layer_data(f())$x, 10) }) -test_that("assignment methods create quosures", { +test_that("constants are not wrapped in quosures", { + aes <- aes(1L, "foo", 1.5) + expect_identical(unclass(aes), list(x = 1L, y = "foo", 1.5)) +}) + +test_that("assignment methods wrap symbolic objects in quosures", { mapping <- aes(a, b, c = c) mapping[1] <- list(quote(foo)) - expect_identical(mapping[[1]], rlang::new_quosure(quote(foo), baseenv())) + expect_identical(mapping[[1]], rlang::new_quosure(quote(foo), globalenv())) mapping[[2]] <- quote(bar) - expect_identical(mapping[[2]], rlang::new_quosure(quote(bar), baseenv())) + expect_identical(mapping[[2]], rlang::new_quosure(quote(bar), globalenv())) mapping$c <- quote(baz) - expect_identical(mapping[[3]], rlang::new_quosure(quote(baz), baseenv())) + expect_identical(mapping[[3]], rlang::new_quosure(quote(baz), globalenv())) +}) + +test_that("assignment methods pull unwrap constants from quosures", { + mapping <- aes(a, b, c = c) + mapping[1] <- list(rlang::quo("foo")) + expect_identical(mapping[[1]], "foo") + + mapping[[2]] <- rlang::quo("bar") + expect_identical(mapping[[2]], "bar") + + mapping$c <- rlang::quo("baz") + expect_identical(mapping[[3]], "baz") }) From 84a354431465c08dd87a08bb93b9cab3fb6005a3 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 1 Mar 2018 00:40:52 +0100 Subject: [PATCH 12/19] Port qplot() to tidy eval --- R/quick-plot.r | 48 +++++++++++++++++++++---------------- man/qplot.Rd | 8 +++---- tests/testthat/test-qplot.r | 14 +++++++---- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/R/quick-plot.r b/R/quick-plot.r index 2ade3d11bb..adeeb1304f 100644 --- a/R/quick-plot.r +++ b/R/quick-plot.r @@ -62,24 +62,33 @@ qplot <- function(x, y = NULL, ..., data, facets = NULL, margins = FALSE, geom = "auto", xlim = c(NA, NA), ylim = c(NA, NA), log = "", main = NULL, - xlab = deparse(substitute(x)), ylab = deparse(substitute(y)), + xlab = NULL, ylab = NULL, asp = NA, stat = NULL, position = NULL) { - testthat::skip("qplot") + caller_env <- parent.frame() if (!missing(stat)) warning("`stat` is deprecated", call. = FALSE) if (!missing(position)) warning("`position` is deprecated", call. = FALSE) if (!is.character(geom)) stop("`geom` must be a character vector", call. = FALSE) - argnames <- names(as.list(match.call(expand.dots = FALSE)[-1])) - arguments <- as.list(match.call()[-1]) - env <- parent.frame() + exprs <- rlang::enquos(x = x, y = y, ...) + is_missing <- vapply(exprs, rlang::quo_is_missing, logical(1)) + is_constant <- vapply(exprs, rlang::quo_is_call, logical(1), name = "I") - aesthetics <- compact(arguments[.all_aesthetics]) - aesthetics <- aesthetics[!is.constant(aesthetics)] - aes_names <- names(aesthetics) - aesthetics <- rename_aes(aesthetics) - class(aesthetics) <- "uneval" + mapping <- new_aes(exprs[!is_missing & !is_constant], env = parent.frame()) + consts <- exprs[is_constant] + + aes_names <- names(mapping) + mapping <- rename_aes(mapping) + + + xlab <- rlang::quo_name(exprs$x) + # Work around quo_name() bug: https://github.com/r-lib/rlang/issues/430 + if (rlang::quo_is_null(exprs$y)) { + ylab <- "NULL" + } else { + ylab <- rlang::quo_name(exprs$y) + } if (missing(data)) { # If data not explicitly specified, will be pulled from workspace @@ -89,7 +98,8 @@ qplot <- function(x, y = NULL, ..., data, facets = NULL, margins = FALSE, facetvars <- all.vars(facets) facetvars <- facetvars[facetvars != "."] names(facetvars) <- facetvars - facetsdf <- as.data.frame(mget(facetvars, envir = env)) + # FIXME? + facetsdf <- as.data.frame(mget(facetvars, envir = caller_env)) if (nrow(facetsdf)) data <- facetsdf } @@ -98,22 +108,22 @@ qplot <- function(x, y = NULL, ..., data, facets = NULL, margins = FALSE, if ("sample" %in% aes_names) { geom[geom == "auto"] <- "qq" } else if (missing(y)) { - x <- eval(aesthetics$x, data, env) + x <- rlang::eval_tidy(mapping$x, data, caller_env) if (is.discrete(x)) { geom[geom == "auto"] <- "bar" } else { geom[geom == "auto"] <- "histogram" } - if (missing(ylab)) ylab <- "count" + if (is.null(ylab)) ylab <- "count" } else { if (missing(x)) { - aesthetics$x <- bquote(seq_along(.(y)), aesthetics) + mapping$x <- rlang::quo(seq_along(!!mapping$y)) } geom[geom == "auto"] <- "point" } } - p <- ggplot(data, aesthetics, environment = env) + p <- ggplot(data, mapping, environment = NULL) if (is.null(facets)) { p <- p + facet_null() @@ -127,12 +137,8 @@ qplot <- function(x, y = NULL, ..., data, facets = NULL, margins = FALSE, # Add geoms/statistics for (g in geom) { - # Arguments are unevaluated because some are aesthetics. Need to evaluate - # params - can't do in correct env because that's lost (no lazyeval) - # so do the best we can by evaluating in parent frame. - params <- arguments[setdiff(names(arguments), c(aes_names, argnames))] - params <- lapply(params, eval, parent.frame()) - + # We reevaluate constants once per geom for historical reasons? + params <- lapply(consts, rlang::eval_tidy) p <- p + do.call(paste0("geom_", g), params) } diff --git a/man/qplot.Rd b/man/qplot.Rd index 804870ec9a..83d9aa73a9 100644 --- a/man/qplot.Rd +++ b/man/qplot.Rd @@ -7,13 +7,13 @@ \usage{ qplot(x, y = NULL, ..., data, facets = NULL, margins = FALSE, geom = "auto", xlim = c(NA, NA), ylim = c(NA, NA), log = "", - main = NULL, xlab = deparse(substitute(x)), - ylab = deparse(substitute(y)), asp = NA, stat = NULL, position = NULL) + main = NULL, xlab = NULL, ylab = NULL, asp = NA, stat = NULL, + position = NULL) quickplot(x, y = NULL, ..., data, facets = NULL, margins = FALSE, geom = "auto", xlim = c(NA, NA), ylim = c(NA, NA), log = "", - main = NULL, xlab = deparse(substitute(x)), - ylab = deparse(substitute(y)), asp = NA, stat = NULL, position = NULL) + main = NULL, xlab = NULL, ylab = NULL, asp = NA, stat = NULL, + position = NULL) } \arguments{ \item{x, y, ...}{Aesthetics passed into each layer} diff --git a/tests/testthat/test-qplot.r b/tests/testthat/test-qplot.r index 732e1b1500..8e5e57fb23 100644 --- a/tests/testthat/test-qplot.r +++ b/tests/testthat/test-qplot.r @@ -14,14 +14,18 @@ test_that("qplot works with variables in data frame and parent env", { }) test_that("qplot works in non-standard environments", { - env <- new.env(parent = globalenv()) - expr <- quote({ + p <- local({ `-1-` <- 10 x <- 1:10 qplot(x, breaks = 0:`-1-`) }) - - expect_is(eval(expr, env), "ggplot") - + expect_is(p, "ggplot") }) +test_that("qplot() evaluates constants in the right place", { + p <- local({ + foo <- "d" + qplot(1, 1, colour = I(paste0("re", foo))) + }) + expect_identical(layer_data(p)$colour, I("red")) +}) From 4c1964bc650028acaa8c606de341dcbd6cfb2e78 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 1 Mar 2018 00:43:09 +0100 Subject: [PATCH 13/19] Restore notion of plot environment to find default scales --- R/layer.r | 4 ++-- R/plot.r | 17 +++++------------ R/quick-plot.r | 4 ++-- R/scale-type.R | 4 ++-- R/scales-.r | 4 ++-- man/ggplot.Rd | 2 +- 6 files changed, 14 insertions(+), 21 deletions(-) diff --git a/R/layer.r b/R/layer.r index d6b3cddb02..c8fb934072 100644 --- a/R/layer.r +++ b/R/layer.r @@ -227,7 +227,7 @@ Layer <- ggproto("Layer", NULL, data <- data[res, , drop = FALSE] } - scales_add_defaults(plot$scales, data, aesthetics) + scales_add_defaults(plot$scales, data, aesthetics, plot$plot_env) # Evaluate and check aesthetics aesthetics <- compact(aesthetics) @@ -287,7 +287,7 @@ Layer <- ggproto("Layer", NULL, names(stat_data) <- names(new) # Add any new scales, if needed - scales_add_defaults(plot$scales, data, new) + scales_add_defaults(plot$scales, data, new, plot$plot_env) # Transform the values, if the scale say it's ok # (see stat_spoke for one exception) if (self$stat$retransform) { diff --git a/R/plot.r b/R/plot.r index 7e475dd157..c6246cde54 100644 --- a/R/plot.r +++ b/R/plot.r @@ -70,30 +70,23 @@ #' width = 0.4 #' ) ggplot <- function(data = NULL, mapping = aes(), ..., - environment = NULL) { + environment = parent.frame()) { UseMethod("ggplot") } #' @export ggplot.default <- function(data = NULL, mapping = aes(), ..., - environment = NULL) { + environment = parent.frame()) { ggplot.data.frame(fortify(data, ...), mapping, environment = environment) } #' @export ggplot.data.frame <- function(data, mapping = aes(), ..., - environment = NULL) { + environment = parent.frame()) { if (!missing(mapping) && !inherits(mapping, "uneval")) { stop("Mapping should be created with `aes() or `aes_()`.", call. = FALSE) } - if (!is.null(environment)) { - stop( - "`environment` is deprecated: environments are now captured by `aes()`", - call. = FALSE - ) - } - p <- structure(list( data = data, layers = list(), @@ -102,7 +95,7 @@ ggplot.data.frame <- function(data, mapping = aes(), ..., theme = list(), coordinates = coord_cartesian(default = TRUE), facet = facet_null(), - plot_env = parent.frame() + plot_env = environment ), class = c("gg", "ggplot")) p$labels <- make_labels(mapping) @@ -113,7 +106,7 @@ ggplot.data.frame <- function(data, mapping = aes(), ..., #' @export ggplot.grouped_df <- function(data, mapping = aes(), ..., - environment = NULL) { + environment = parent.frame()) { data$.group <- dplyr::group_indices(data) mapping$group <- mapping$group %||% quote(.group) diff --git a/R/quick-plot.r b/R/quick-plot.r index adeeb1304f..5dab73193e 100644 --- a/R/quick-plot.r +++ b/R/quick-plot.r @@ -123,7 +123,7 @@ qplot <- function(x, y = NULL, ..., data, facets = NULL, margins = FALSE, } } - p <- ggplot(data, mapping, environment = NULL) + p <- ggplot(data, mapping, environment = caller_env) if (is.null(facets)) { p <- p + facet_null() @@ -139,7 +139,7 @@ qplot <- function(x, y = NULL, ..., data, facets = NULL, margins = FALSE, for (g in geom) { # We reevaluate constants once per geom for historical reasons? params <- lapply(consts, rlang::eval_tidy) - p <- p + do.call(paste0("geom_", g), params) + p <- p + do.call(paste0("geom_", g), params, envir = caller_env) } logv <- function(var) var %in% strsplit(log, "")[[1]] diff --git a/R/scale-type.R b/R/scale-type.R index c7b54267dd..6b292b0e18 100644 --- a/R/scale-type.R +++ b/R/scale-type.R @@ -1,9 +1,9 @@ -find_scale <- function(aes, x) { +find_scale <- function(aes, x, env) { type <- scale_type(x) candidates <- paste("scale", aes, type, sep = "_") for (scale in candidates) { - scale_f <- find_global(scale, globalenv(), mode = "function") + scale_f <- find_global(scale, env, mode = "function") if (!is.null(scale_f)) return(scale_f()) } diff --git a/R/scales-.r b/R/scales-.r index 3107bb2e52..79a3344cae 100644 --- a/R/scales-.r +++ b/R/scales-.r @@ -88,7 +88,7 @@ scales_transform_df <- function(scales, df) { # @param aesthetics A list of aesthetic-variable mappings. The name of each # item is the aesthetic, and the value of each item is the variable in data. -scales_add_defaults <- function(scales, data, aesthetics) { +scales_add_defaults <- function(scales, data, aesthetics, env) { if (is.null(aesthetics)) return() names(aesthetics) <- unlist(lapply(names(aesthetics), aes_to_scale)) @@ -99,7 +99,7 @@ scales_add_defaults <- function(scales, data, aesthetics) { datacols <- lapply(aesthetics[new_aesthetics], rlang::eval_tidy, data = data) for (aes in names(datacols)) { - scales$add(find_scale(aes, datacols[[aes]])) + scales$add(find_scale(aes, datacols[[aes]], env)) } } diff --git a/man/ggplot.Rd b/man/ggplot.Rd index 10551e10ca..dc85733d93 100644 --- a/man/ggplot.Rd +++ b/man/ggplot.Rd @@ -4,7 +4,7 @@ \alias{ggplot} \title{Create a new ggplot} \usage{ -ggplot(data = NULL, mapping = aes(), ..., environment = NULL) +ggplot(data = NULL, mapping = aes(), ..., environment = parent.frame()) } \arguments{ \item{data}{Default dataset to use for plot. If not already a data.frame, From e522314d34aadae760a95d5ac6d1987cb97a4eed Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 1 Mar 2018 11:59:22 +0100 Subject: [PATCH 14/19] Fix error on R 3.1 --- R/aes.r | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/R/aes.r b/R/aes.r index 7ae55c9931..0ed873998f 100644 --- a/R/aes.r +++ b/R/aes.r @@ -116,7 +116,10 @@ print.uneval <- function(x, ...) { } #' @export "$<-.uneval" <- function(x, i, value) { - new_aes(NextMethod()) + # Can't use NextMethod() because of a bug in R 3.1 + x <- unclass(x) + x[[i]] <- value + new_aes(x) } #' @export "[<-.uneval" <- function(x, i, value) { From 27fa93117f184a13a4c88f85f741ee47add97167 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Wed, 21 Mar 2018 16:41:13 +0100 Subject: [PATCH 15/19] Bump rlang dev dep --- DESCRIPTION | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 3ed478b51b..080a0d733f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -20,7 +20,7 @@ Imports: MASS, plyr (>= 1.7.1), reshape2, - rlang (>= 0.1.6.9002), + rlang (>= 0.2.0.9001), scales (>= 0.4.1.9002), stats, tibble, @@ -52,7 +52,7 @@ Suggests: Remotes: hadley/scales, hadley/svglite, jimhester/withr, - tidyverse/rlang + r-lib/rlang Enhances: sp License: GPL-2 | file LICENSE URL: http://ggplot2.tidyverse.org, https://github.com/tidyverse/ggplot2 From c0a99a878ad1848b877bf873e81ce2c2a76db02f Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Wed, 21 Mar 2018 22:54:25 +0100 Subject: [PATCH 16/19] Reexport tidy eval tools --- DESCRIPTION | 1 + NAMESPACE | 28 ++++++++++++++++++++++++++++ R/utilities-tidy-eval.R | 39 +++++++++++++++++++++++++++++++++++++++ man/tidyeval.Rd | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 R/utilities-tidy-eval.R create mode 100644 man/tidyeval.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 080a0d733f..1f73c1fa96 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -232,6 +232,7 @@ Collate: 'utilities-matrix.r' 'utilities-resolution.r' 'utilities-table.r' + 'utilities-tidy-eval.R' 'zxx.r' 'zzz.r' VignetteBuilder: knitr diff --git a/NAMESPACE b/NAMESPACE index 9ac3573b9b..befca1d703 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -114,6 +114,7 @@ S3method(widthDetails,titleGrob) S3method(widthDetails,zeroGrob) export("%+%") export("%+replace%") +export(.data) export(.pt) export(.stroke) export(AxisSecondary) @@ -275,8 +276,16 @@ export(element_grob) export(element_line) export(element_rect) export(element_text) +export(enexpr) +export(enexprs) +export(enquo) +export(enquos) +export(ensym) +export(ensyms) export(expand_limits) export(expand_scale) +export(expr) +export(exprs) export(facet_grid) export(facet_null) export(facet_wrap) @@ -383,6 +392,9 @@ export(position_nudge) export(position_stack) export(qplot) export(quickplot) +export(quo) +export(quo_name) +export(quos) export(rel) export(remove_missing) export(render_axes) @@ -514,6 +526,8 @@ export(stat_ydensity) export(summarise_coord) export(summarise_layers) export(summarise_layout) +export(sym) +export(syms) export(theme) export(theme_bw) export(theme_classic) @@ -547,6 +561,20 @@ import(scales) importFrom(lazyeval,f_eval) importFrom(plyr,as.quoted) importFrom(plyr,defaults) +importFrom(rlang,.data) +importFrom(rlang,enexpr) +importFrom(rlang,enexprs) +importFrom(rlang,enquo) +importFrom(rlang,enquos) +importFrom(rlang,ensym) +importFrom(rlang,ensyms) +importFrom(rlang,expr) +importFrom(rlang,exprs) +importFrom(rlang,quo) +importFrom(rlang,quo_name) +importFrom(rlang,quos) +importFrom(rlang,sym) +importFrom(rlang,syms) importFrom(stats,setNames) importFrom(tibble,tibble) importFrom(utils,.DollarNames) diff --git a/R/utilities-tidy-eval.R b/R/utilities-tidy-eval.R new file mode 100644 index 0000000000..1cba1ec9a7 --- /dev/null +++ b/R/utilities-tidy-eval.R @@ -0,0 +1,39 @@ +#' Tidy eval helpers +#' +#' @description +#' +#' * \code{\link[rlang]{sym}()} creates a symbol from a string and +#' \code{\link[rlang]{syms}()} creates a list of symbols from a +#' character vector. +#' +#' * \code{\link[rlang]{expr}()} and \code{\link[rlang]{quo}()} quote +#' one expression. `quo()` wraps the quoted expression in a quosure. +#' +#' The plural variants \code{\link[rlang]{exprs}()} and +#' \code{\link[rlang]{quos}()} return a list of quoted expressions or +#' quosures. +#' +#' * \code{\link[rlang]{enexpr}()} and \code{\link[rlang]{enquo}()} +#' capture the expression supplied as argument by the user of the +#' current function (`enquo()` wraps this expression in a quosure). +#' +#' \code{\link[rlang]{enexprs}()} and \code{\link[rlang]{enquos}()} +#' capture multiple expressions supplied as arguments, including +#' `...`. +#' +#' @md +#' @name tidyeval +#' @keywords internal +#' @aliases quo quos enquo enquos quo_name +#' sym ensym syms ensyms +#' expr exprs enexpr enexprs +#' .data +#' @export quo quos enquo enquos quo_name +#' @export sym ensym syms ensyms +#' @export expr exprs enexpr enexprs +#' @export .data +NULL + +#' @importFrom rlang quo quos enquo enquos quo_name sym ensym syms +#' ensyms expr exprs enexpr enexprs .data +NULL diff --git a/man/tidyeval.Rd b/man/tidyeval.Rd new file mode 100644 index 0000000000..b2f1b2edae --- /dev/null +++ b/man/tidyeval.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utilities-tidy-eval.R +\name{tidyeval} +\alias{tidyeval} +\alias{quo} +\alias{quos} +\alias{enquo} +\alias{enquos} +\alias{quo_name} +\alias{sym} +\alias{ensym} +\alias{syms} +\alias{ensyms} +\alias{expr} +\alias{exprs} +\alias{enexpr} +\alias{enexprs} +\alias{.data} +\title{Tidy eval helpers} +\description{ +\itemize{ +\item \code{\link[rlang]{sym}()} creates a symbol from a string and +\code{\link[rlang]{syms}()} creates a list of symbols from a +character vector. +\item \code{\link[rlang]{expr}()} and \code{\link[rlang]{quo}()} quote +one expression. \code{quo()} wraps the quoted expression in a quosure. + +The plural variants \code{\link[rlang]{exprs}()} and +\code{\link[rlang]{quos}()} return a list of quoted expressions or +quosures. +\item \code{\link[rlang]{enexpr}()} and \code{\link[rlang]{enquo}()} +capture the expression supplied as argument by the user of the +current function (\code{enquo()} wraps this expression in a quosure). + +\code{\link[rlang]{enexprs}()} and \code{\link[rlang]{enquos}()} +capture multiple expressions supplied as arguments, including +\code{...}. +} +} +\keyword{internal} From db9bd66c09e9c721199a7bc61c9e1b0bd4abaee8 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 22 Mar 2018 12:37:06 +0100 Subject: [PATCH 17/19] Document tidy eval for mappings with aes() --- R/aes.r | 37 ++++++++++++++++++++++++++++++++++--- man/aes.Rd | 37 +++++++++++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/R/aes.r b/R/aes.r index 0ed873998f..d45bb180b3 100644 --- a/R/aes.r +++ b/R/aes.r @@ -30,13 +30,23 @@ NULL #' #' This function also standardise aesthetic names by performing partial #' matching, converting color to colour, and translating old style R names to -#' ggplot names (eg. pch to shape, cex to size) +#' ggplot names (eg. pch to shape, cex to size). +#' +#' +#' @section Quasiquotation: +#' +#' `aes()` is a [quoting function][rlang::quotation]. This means that +#' its inputs are quoted to be evaluated in the context of the +#' data. This makes it easy to work with variables from the data frame +#' because you can name those directly. The flip side is that you have +#' to use [quasiquotation][rlang::quasiquotation] to program with +#' `aes()`. See a tidy evaluation tutorial such as the [dplyr +#' programming vignette](http://dplyr.tidyverse.org/articles/programming.html) +#' to learn more about these techniques. #' #' @param x,y,... List of name value pairs giving aesthetics to map to #' variables. The names for x and y aesthetics are typically omitted because #' they are so common; all other aesthetics must be named. -#' @seealso See [aes_()] for a version of `aes` that is -#' more suitable for programming with. #' @export #' @examples #' aes(x = mpg, y = wt) @@ -57,6 +67,27 @@ NULL #' #' # Aesthetics supplied to ggplot() are used as defaults for every layer #' # you can override them, or supply different aesthetics for each layer +#' +#' +#' # aes() is a quoting function, so you need to use tidy evaluation +#' # techniques to create wrappers around ggplot2 pipelines. The +#' # simplest case occurs when your wrapper takes dots: +#' scatter_by <- function(data, ...) { +#' ggplot(data) + geom_point(aes(...)) +#' } +#' scatter_by(mtcars, disp, drat) +#' +#' # If your wrapper has a more specific interface with named arguments, +#' # you need to use the "enquote and unquote" technique: +#' scatter_by <- function(data, x, y) { +#' ggplot(data) + geom_point(aes(!!enquo(x), !!enquo(y))) +#' } +#' scatter_by(mtcars, disp, drat) +#' +#' # Note that users of your wrapper can use their own functions in the +#' # quoted expressions and all will resolve as it should! +#' cut3 <- function(x) cut_number(x, 3) +#' scatter_by(mtcars, cut3(disp), drat) aes <- function(x, y, ...) { exprs <- rlang::enquos(x = x, y = y, ...) is_missing <- vapply(exprs, rlang::quo_is_missing, logical(1)) diff --git a/man/aes.Rd b/man/aes.Rd index d56cabd5be..e980e6e682 100644 --- a/man/aes.Rd +++ b/man/aes.Rd @@ -19,8 +19,20 @@ properties (aesthetics) of geoms. Aesthetic mappings can be set in \details{ This function also standardise aesthetic names by performing partial matching, converting color to colour, and translating old style R names to -ggplot names (eg. pch to shape, cex to size) +ggplot names (eg. pch to shape, cex to size). } +\section{Quasiquotation}{ + + +\code{aes()} is a \link[rlang:quotation]{quoting function}. This means that +its inputs are quoted to be evaluated in the context of the +data. This makes it easy to work with variables from the data frame +because you can name those directly. The flip side is that you have +to use \link[rlang:quasiquotation]{quasiquotation} to program with +\code{aes()}. See a tidy evaluation tutorial such as the \href{http://dplyr.tidyverse.org/articles/programming.html}{dplyr programming vignette} +to learn more about these techniques. +} + \examples{ aes(x = mpg, y = wt) aes(mpg, wt) @@ -40,8 +52,25 @@ ggplot(mpg) + geom_point(aes(displ, hwy)) # Aesthetics supplied to ggplot() are used as defaults for every layer # you can override them, or supply different aesthetics for each layer + + +# aes() is a quoting function, so you need to use tidy evaluation +# techniques to create wrappers around ggplot2 pipelines. The +# simplest case occurs when your wrapper takes dots: +scatter_by <- function(data, ...) { + ggplot(data) + geom_point(aes(...)) +} +scatter_by(mtcars, disp, drat) + +# If your wrapper has a more specific interface with named arguments, +# you need to use the "enquote and unquote" technique: +scatter_by <- function(data, x, y) { + ggplot(data) + geom_point(aes(!!enquo(x), !!enquo(y))) } -\seealso{ -See \code{\link[=aes_]{aes_()}} for a version of \code{aes} that is -more suitable for programming with. +scatter_by(mtcars, disp, drat) + +# Note that users of your wrapper can use their own functions in the +# quoted expressions and all will resolve as it should! +cut3 <- function(x) cut_number(x, 3) +scatter_by(mtcars, cut3(disp), drat) } From e311f9a6c1e6c69d0d568f0409201ce1b1bd8b3d Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 22 Mar 2018 16:33:48 +0100 Subject: [PATCH 18/19] Fix deparsing of quosures when adding mappings --- R/plot-construction.r | 2 +- tests/testthat/test-aes.r | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/R/plot-construction.r b/R/plot-construction.r index 16efd60bc7..bbfbb3f19f 100644 --- a/R/plot-construction.r +++ b/R/plot-construction.r @@ -117,7 +117,7 @@ ggplot_add.uneval <- function(object, plot, object_name) { # defaults() doesn't copy class, so copy it. class(plot$mapping) <- class(object) - labels <- lapply(object, deparse) + labels <- lapply(object, rlang::quo_name) names(labels) <- names(object) update_labels(plot, labels) } diff --git a/tests/testthat/test-aes.r b/tests/testthat/test-aes.r index 9aed612ab8..c2f6c76ead 100644 --- a/tests/testthat/test-aes.r +++ b/tests/testthat/test-aes.r @@ -88,6 +88,11 @@ test_that("assignment methods pull unwrap constants from quosures", { expect_identical(mapping[[3]], "baz") }) +test_that("quosures are squashed when creating default label for a mapping", { + p <- ggplot(mtcars) + aes(!!quo(identity(!!quo(cyl)))) + expect_identical(p$labels$x, "identity(cyl)") +}) + # Visual tests ------------------------------------------------------------ From bab8ac687891a12695e126adde77a53bf5b86bd8 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 22 Mar 2018 16:50:07 +0100 Subject: [PATCH 19/19] Soft-deprecate SE versions of aes() --- NEWS.md | 8 +++----- R/aes.r | 7 +++++++ man/aes_.Rd | 8 ++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/NEWS.md b/NEWS.md index ae3820b320..28b3afe78f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,11 +2,9 @@ ## New features -* `aes()` now supports quasiquotation so that you can use `!!`, `!!!`, and - `:=`. (ggplot2 does not currently support full tidy evaluation because - when I wrote ggplot2 my understanding of NSE was quite flawed, and hence - ggplot2 only captures one environment per plot, not one environment - per aesthetic. We will fix this in a future release.) +* `aes()` now supports quasiquotation so that you can use `!!`, `!!!`, + and `:=`. This replaces `aes_()` and `aes_string()` which are now + soft-deprecated (but will remain around for a long time). * ggplot2 now works on R 3.1 onwards, and uses the [vdiffr](https://github.com/lionel-/vdiffr) package for visual testing. diff --git a/R/aes.r b/R/aes.r index d45bb180b3..6004fad8f2 100644 --- a/R/aes.r +++ b/R/aes.r @@ -198,6 +198,13 @@ is_position_aes <- function(vars) { #' `aes(colour = "my colour")` or \code{aes{x = `X$1`}} #' with `aes_string()` is quite clunky. #' +#' +#' @section Life cycle: +#' +#' All these functions are soft-deprecated. Please use tidy evaluation +#' idioms instead (see the quasiquotation section in +#' [aes()] documentation). +#' #' @param x,y,... List of name value pairs. Elements must be either #' quoted calls, strings, one-sided formulas or constants. #' @seealso [aes()] diff --git a/man/aes_.Rd b/man/aes_.Rd index 9df79060e7..9f8d26b65a 100644 --- a/man/aes_.Rd +++ b/man/aes_.Rd @@ -35,6 +35,14 @@ I recommend using \code{aes_()}, because creating the equivalents of \code{aes(colour = "my colour")} or \code{aes{x = `X$1`}} with \code{aes_string()} is quite clunky. } +\section{Life cycle}{ + + +All these functions are soft-deprecated. Please use tidy evaluation +idioms instead (see the quasiquotation section in +\code{\link[=aes]{aes()}} documentation). +} + \examples{ # Three ways of generating the same aesthetics aes(mpg, wt, col = cyl)