Local Variables Within aes
Asked Answered
O

6

47

I'm trying to use a local variable in aes when I plot with ggplot. This is my problem boiled down to the essence:

xy <- data.frame(x=1:10,y=1:10)

plotfunc <- function(Data,YMul=2){
    ggplot(Data,aes(x=x,y=y*YMul))+geom_line()
}

plotfunc(xy)

This results in the following error:

Error in eval(expr, envir, enclos) : object 'YMul' not found

It seems as if I cannot use local variables (or function arguments) in aes. Could it be that it occurrs due to the content of aes being executed later when the local variable is out of scope? How can I avoid this problem (other than not using the local variable within aes)?

Osteopath answered 18/5, 2012 at 20:15 Comment(3)
I think because it still expects you to pass down the Ymul but you only give plotfunc(xy)Garage
not true, it should use the default valueFrontier
I'm running the code above and not getting any error (23 October 2017), has there been an update to ggplot2 to explain why this would work now?Hispanicize
F
39

I would capture the local environment,

xy <- data.frame(x=1:10,y=1:10)

plotfunc <- function(Data, YMul = 2){
    .e <- environment()
    ggplot(Data, aes(x = x, y = y*YMul), environment = .e) + geom_line()
}

plotfunc(xy)
Frontier answered 19/5, 2012 at 6:33 Comment(6)
Perhaps this is the official (but undocumented) way, I think.Demonolater
to be honest, I think it really should be the default, somehow. Same with plyr, I always get deeply confused when **ply doesn't find a variable that other R functions with usual scoping rules would find.Frontier
+1 -- @Demonolater and @baptiste. I also like this best. Worth noting, though, that it does something different than my solution, as can be seen by: (1) removing y=1:10 from the data.frame xy; (2) putting y<-1:10 in the global environment; and (3) putting y<-10:1 in the function body before the call to ggplot. In essence, my solution allows passing in of selected arguments, without otherwise changing the scoping rules. Yours completely changes the scoping behavior of ggplot() (which is why I like it).Spritsail
@JoshO'Brien yes. I also adhere to the rule that arguments of aes() should always come from a data.frame, in which case the only objects that I may want to cherry-pick from the surrounding environment are parameters like YMu.Frontier
This actually is straight forward - but I agree that it should be the default. I thought that aes within ggplot works like a closure, retaining all used objects from around; obviously it doesn't. Thank you!Osteopath
@babtiste, would you maybe also know of a solution for my related SO question? Many thanks in advance!Sonnier
P
10

Here's an alternative that allows you to pass in any value through the YMul argument without having to add it to the Data data.frame or to the global environment:

plotfunc <- function(Data, YMul = 2){
    eval(substitute(
        expr = {
            ggplot(Data,aes(x=x,y=y*YMul)) + geom_line()
        }, 
        env = list(YMul=YMul)))
    }

plotfunc(xy, YMul=100)

To see how this works, try out the following line in isolation:

substitute({ggplot(Data, aes(x=x, y=y*YMul))}, list(YMul=100))
Patras answered 18/5, 2012 at 20:50 Comment(5)
+1 Thanks! I knew there was a way to do it, but hadn't ever taken the time to sort that out. Very cool.Zebrass
@Zebrass -- Glad that's helpful. What I've never taken the time to sort out is how ggplot's proto-based scoping works. As an example, I just placed a browser() statement inside of the aes() call, and upon typing sys.frames(), I get a list of 23 environments, none of which (??) seem to allow direct access to the value of YMul. Hmm.Spritsail
Yeah... its way beyond me. I had some custom code courtesy of Kohske that broke with the new release and its completely inscrutable to me! Someday I'd love to understand proto...Zebrass
@Zebrass and Josh: Here's a related discussion on github: github.com/hadley/ggplot2/issues/248 Seems to have been discussed but not implemented.Pity
+1 The same with bquote: eval(bquote(ggplot(Data,aes(x=x,y=y*.(YMul)))+geom_line())).Sb
P
5

ggplot()'s aes expects YMul to be a variable within the data data frame. Try including YMull there instead:

Thanks to @Justin: ggplot()'s aes seems to look forYMul in the data data frame first, and if not found, then in the global environment. I like to add such variables to the data frame, as follows, as it makes sense to me conceptually. I also don't have to worry about changes to global variables having unexpected consequences to functions. But all of the other answers are also correct. So, use whichever suits you.

require("ggplot2")
xy <- data.frame(x = 1:10, y = 1:10)
xy <- cbind(xy, YMul = 2)

ggplot(xy, aes(x = x, y = y * YMul)) + geom_line()

Or, if you want the function in your example:

plotfunc <- function(Data, YMul = 2)
{
    ggplot(cbind(Data, YMul), aes(x = x, y = y * YMul)) + geom_line()
}

plotfunc(xy)
Pity answered 18/5, 2012 at 20:35 Comment(3)
YMul doesn't have to be part of the data.frame. It just needs to be defined in the scope where the ggplot object is evaluated, which is global rather than in the function.Zebrass
@ Justin: Thanks. I had not realized that. Interesting that ggplot() seems to look for YMul in the data frame first, then if not found in the global environment, apparently skipping the function's arguments. I haven't found any information on how ggplot() searches the namespaces, but then again I haven't looked very hard.Pity
This is by far the easier option to remember and type. Perhaps not great if your dataframe has a billion rows, but convenient for other situations.Hispanicize
J
4

I am using ggplot2, and your example seems to work fine with the current version.

However, it is easy to come up with variants which still create trouble. I was myself confused by similar behavior, and that's how I found this post (top Google result for "ggplot how to evaluate variables when passed"). For example, if we move ggplot out of plotfunc:

xy <- data.frame(x=1:10,y=1:10)

plotfunc <- function(Data,YMul=2){
  geom_line(aes(x=x,y=y*YMul))
}

ggplot(xy)+plotfunc(xy)
# Error in eval(expr, envir, enclos) : object 'YMul' not found

In the above variant, "capturing the local environment" is not a solution because ggplot is not called from within the function, and only ggplot has the "environment=" argument.

But there is now a family of functions "aes_", "aes_string", "aes_q" which are like "aes" but capture local variables. If we use "aes_" in the above, we still get an error because now it doesn't know about "x". But it is easy to refer to the data directly, which solves the problem:

plotfunc <- function(Data,YMul=2){
  geom_line(aes_(x=Data$x,y=Data$y*YMul))
}
ggplot(xy)+plotfunc(xy)
# works
Jemison answered 7/3, 2016 at 19:39 Comment(0)
F
1

Have you looked at the solution given by @wch (W. Chang)?

https://github.com/hadley/ggplot2/issues/743

I think it is the better one

essentially is like that of @baptiste but include the reference to the environment directly in the call to ggplot

I report it here

g <- function() {
  foo3 <- 4
  ggplot(mtcars, aes(x = wt + foo3, y = mpg),
         environment = environment()) +
    geom_point()
}

g()
# Works
Fairy answered 20/5, 2014 at 17:17 Comment(1)
This is a duplicate of @baptiste's answer, and IMO unnecessarily uses a different example given OP provided a reproducible example. I'd suggest removing (and perhaps commenting baptiste's with the Github issue you linked).Tube
Z
0

If you execute your code outside of the function it works. And if you execute the code within the function with YMul defined globally, it works. I don't fully understand the inner workings of ggplot but this works...

YMul <- 2

plotfunc <- function(Data){
    ggplot(Data,aes(x=x,y=y*YMul))+geom_line()
}

plotfunc(xy)
Zebrass answered 18/5, 2012 at 20:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.