4

Since I will need to make a lot of different plots in R I'm trying to put some more logic in preparing the data (add column names corresponding to the aesthetics) and less logic in the plot itself.

Consider the following default iris plot:

library(ggplot2)
library(data.table)
scatter <- ggplot(data=iris, aes(x = Sepal.Length, y = Sepal.Width)) 
scatter + geom_point(aes(color=Species, shape=Species))

Now I make a modified iris data with column names matching to the desired aesthetics:

iris2 <- as.data.table(iris)
iris2 <- iris2[,.(x=Sepal.Length, y=Sepal.Width, color=Species,
                  shape=Species)]

That I want to plot in a function in such a way that it basically builds the following command only slightly more dynamic, so you use all the aesthetics supplied in the data.

ggplot(data, aes(x=x, y=y)) + geom_point(aes(color=color, shape=shape))

It has been a long time since I read anything about nonstandard evaluation, expressions and quotation and I noticed that there are quite some developments with rlang and quosures (cheatsheet). [This] question was kind of helpful, but it did not resolve the fact that I want to infer the aesthetics from the data.

In the end I have tried a lot of stuff, and looked inside aes. In there I see:

exprs <- rlang::enquos(x = x, y = y, ...)

and I think this is the reason that all attempts that I made like:

ggplot(iris2, aes(x=x, y=y)) +
    geom_point(aes(rlang::quo(expr(color=color))))

did not work out since aes is trying to 'enquos' my quosure(s).

QUESTION Is there any way to supply arguments to aes in a dynamic way based on the contents of the data (so you do not know in advance which aesthetics you will need?

If my question is not clear enough, in the end I made something that works, only I have a feeling that this totally not necessary because I don't know/understand the right way to do it. So the stuff below works and is what I have in mind, but what I e.g. don't like is that I had to modify aes:

The block below is stand alone and can be executed without the code chunks above.

library(data.table)
library(ggplot2)
library(rlang)
iris2 <- as.data.table(iris)
iris2 <- iris2[,.(x=Sepal.Length, y=Sepal.Width, color=Species, shape=Species)]
myaes <- function (x, y, myquo=NULL, ...) {    
    exprs <- rlang::enquos(x = x, y = y, ...)    
    exprs <- c(exprs, myquo)
    is_missing <- vapply(exprs, rlang::quo_is_missing, logical(1))
    aes <- ggplot2:::new_aes(exprs[!is_missing], env = parent.frame())
    ggplot2:::rename_aes(aes)
}

generalPlot <- function(data, f=geom_point,
                        knownaes=c('color'=expr(color), 'shape'=expr(shape))){
    myquo  <- list()
    for(i in names(knownaes)){
        if(i %in% names(data)){
            l <- list(rlang::quo(!!knownaes[[i]]))
            names(l) <- i
            myquo <- c(myquo, l)
        }
    }    

    ggplot(data, aes(x=x, y=y)) +
        f(myaes(myquo=myquo))   
}

generalPlot(iris2[,.(x, y, color)])
generalPlot(iris2[,.(x, y, color, shape)])
pogibas
  • 27,303
  • 19
  • 84
  • 117
Martin
  • 1,084
  • 9
  • 15

2 Answers2

2

You can use this custom function that parses input data colnames and generates an aes text string that is passed to eval().

generateAES <- function(foo) {
    eval(parse(text = paste0("aes(", 
        paste(
            lapply(foo, function(i) paste(i, "=", i)), 
        collapse = ","), 
        ")"
    )))
}

You can use it with:

ggplot(iris2, generateAES(colnames(iris2))) +
    geom_point()

Or with pipes:

library(magrittr)
iris2 %>%
    ggplot(generateAES(colnames(.))) +
        geom_point()

generateAES output is aes like:

Aesthetic mapping: 
* `x`      -> `x`
* `y`      -> `y`
* `colour` -> `color`
* `shape`  -> `shape`

That is generated from text string "aes(x = x,y = y,color = color,shape = shape)"

pogibas
  • 27,303
  • 19
  • 84
  • 117
  • This was actually something I considered going with, however, based on the drawbacks mentioned [here](http://adv-r.had.co.nz/Computing-on-the-language.html#nse-downsides) I think the other answer is more viable, but this definitely works as well! – Martin Jan 21 '19 at 19:42
1

So if your data as a "color" or "shape" column, you just want to map that to the color or shape aesthetic? I think a simpler way to do that would be

generalPlot <- function(data, f=geom_point, knownaes=c('color', 'shape')) {
  match_aes <- intersect(names(data), knownaes)
  my_aes_list <- purrr::set_names(purrr::map(match_aes, rlang::sym), match_aes)
  my_aes <- rlang::eval_tidy(quo(aes(!!!my_aes_list)))
  ggplot(data, aes(x=x, y=y)) +
        f(mapping=my_aes)

}

Then you can do

generalPlot(iris2[,.(x, y)])
generalPlot(iris2[,.(x, y, color)])
generalPlot(iris2[,.(x, y, color, shape)])

and it doesn't require the additional myaes function.

I'm kind of surprised I had to use eval_tidy but for some reason you can't seem to use !!! with aes().

x <- list(color=sym("color"))
ggplot(iris2, aes(x,y)) + geom_point(aes(!!!x))
# Error: Can't use `!!!` at top level

(Tested with ggplot2_3.1.0)

MrFlick
  • 195,160
  • 17
  • 277
  • 295
  • Ok, this is great! First of all it works, but this solution pointed out some things I missed initially (1) I missed how you can create a symbol with rlang::sym (but also with as.symbol), (2) the eval_tidy with the [unquote splicing](https://dplyr.tidyverse.org/articles/programming.html#unquote-splicing) – Martin Jan 21 '19 at 19:29