diff --git a/DESCRIPTION b/DESCRIPTION index 3ed478b51b..1f73c1fa96 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 @@ -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 abeb5f4906..befca1d703 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,11 +2,13 @@ S3method("$",ggproto) S3method("$",ggproto_parent) +S3method("$<-",uneval) S3method("+",gg) S3method("[",uneval) +S3method("[<-",uneval) S3method("[[",ggproto) +S3method("[[<-",uneval) S3method(.DollarNames,ggproto) -S3method(as.character,uneval) S3method(as.list,ggproto) S3method(autolayer,default) S3method(autoplot,default) @@ -107,12 +109,12 @@ 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) export("%+%") export("%+replace%") +export(.data) export(.pt) export(.stroke) export(AxisSecondary) @@ -274,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) @@ -382,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) @@ -513,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) @@ -546,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/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-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..6004fad8f2 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,13 +67,58 @@ 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::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], env = parent.frame()) rename_aes(aes) } + +# 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") +} + #' @export print.uneval <- function(x, ...) { cat("Aesthetic mapping: \n") @@ -71,23 +126,35 @@ 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 = "") } + + invisible(x) } #' @export -str.uneval <- function(object, ...) utils::str(unclass(object), ...) -#' @export -"[.uneval" <- function(x, i, ...) structure(unclass(x)[i], class = "uneval") +"[.uneval" <- function(x, i, ...) { + new_aes(NextMethod()) +} +# If necessary coerce replacements to quosures for compatibility +#' @export +"[[<-.uneval" <- function(x, i, value) { + new_aes(NextMethod()) +} +#' @export +"$<-.uneval" <- function(x, i, value) { + # Can't use NextMethod() because of a bug in R 3.1 + x <- unclass(x) + x[[i]] <- value + new_aes(x) +} #' @export -as.character.uneval <- function(x, ...) { - char <- as.character(unclass(x)) - names(char) <- names(x) - char +"[<-.uneval" <- function(x, i, value) { + new_aes(NextMethod()) } # Rename American or old-style aesthetics name @@ -131,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()] @@ -157,17 +231,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 + new_aesthetic(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") } @@ -178,13 +254,14 @@ 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]] - } else { - x + x <- rlang::parse_expr(x) } + new_aesthetic(x, env = caller_env) }) + structure(rename_aes(mapping), class = "uneval") } @@ -204,8 +281,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" ) } @@ -243,7 +322,10 @@ aes_auto <- function(data = NULL, ...) { } mapped_aesthetics <- function(x) { + if (is.null(x)) { + return(NULL) + } + is_null <- vapply(x, is.null, logical(1)) names(x)[!is_null] - } 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..c8fb934072 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)) @@ -139,6 +132,7 @@ layer <- function(geom = NULL, stat = NULL, } + subset <- rlang::enquo(subset) ggproto("LayerInstance", Layer, geom = geom, @@ -155,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(mapping) +} + Layer <- ggproto("Layer", NULL, geom = NULL, geom_params = NULL, @@ -210,16 +221,17 @@ 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), ] + 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] } scales_add_defaults(plot$scales, data, aesthetics, plot$plot_env) # 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) { @@ -271,7 +283,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 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/R/plot.r b/R/plot.r index 9d1b6b768e..c6246cde54 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 diff --git a/R/quick-plot.r b/R/quick-plot.r index d426db40e5..5dab73193e 100644 --- a/R/quick-plot.r +++ b/R/quick-plot.r @@ -62,22 +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) { + 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") + + mapping <- new_aes(exprs[!is_missing & !is_constant], env = parent.frame()) + consts <- exprs[is_constant] - aesthetics <- compact(arguments[.all_aesthetics]) - aesthetics <- aesthetics[!is.constant(aesthetics)] - aes_names <- names(aesthetics) - aesthetics <- rename_aes(aesthetics) - class(aesthetics) <- "uneval" + 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 @@ -87,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 } @@ -96,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 = caller_env) if (is.null(facets)) { p <- p + facet_null() @@ -125,13 +137,9 @@ 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()) - - p <- p + do.call(paste0("geom_", g), params) + # We reevaluate constants once per geom for historical reasons? + params <- lapply(consts, rlang::eval_tidy) + 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 9aa0fbec21..6b292b0e18 100644 --- a/R/scale-type.R +++ b/R/scale-type.R @@ -1,4 +1,4 @@ -find_scale <- function(aes, x, env = parent.frame()) { +find_scale <- function(aes, x, env) { type <- scale_type(x) candidates <- paste("scale", aes, type, sep = "_") diff --git a/R/scales-.r b/R/scales-.r index 2330d8ae61..79a3344cae 100644 --- a/R/scales-.r +++ b/R/scales-.r @@ -96,10 +96,7 @@ 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)) 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/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) } 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) diff --git a/man/ggplot.Rd b/man/ggplot.Rd index ec9a6caa07..dc85733d93 100644 --- a/man/ggplot.Rd +++ b/man/ggplot.Rd @@ -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 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/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} 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))) ) }) diff --git a/tests/testthat/test-aes.r b/tests/testthat/test-aes.r index d948d11a74..c2f6c76ead 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, 0.4) }) test_that("aes_q() & aes_string() preserves explicit NULLs", { @@ -59,6 +59,40 @@ test_that("aes evaluated in environment where plot created", { expect_equal(layer_data(f())$x, 10) }) +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), globalenv())) + + mapping[[2]] <- quote(bar) + expect_identical(mapping[[2]], rlang::new_quosure(quote(bar), globalenv())) + + mapping$c <- quote(baz) + 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") +}) + +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 ------------------------------------------------------------ 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))) }) 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")) +})