Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to use two '...' statements in a function in R?

I want to write a function that calls both plot() and legend() and it would be ideal if the user could specify a number of additional arguments that are then passed through to either plot() or legend(). I know I can achieve this for one of the two functions using ...:

foo.plot <- function(x,y,...) {     plot(x,y,...)     legend("bottomleft", "bar", pch=1) }  foo.plot(1,1, xaxt = "n") 

This passes xaxt = "n" to plot. But is there a way for example to pass e.g. title = "legend" to the legend() call without prespecifying the arguments in the function header?


Update from the accepted answer: I thought that VitoshKa's way was the most elegant to accomplish what I wanted. However, there were some minor issues that I had to get around with until it worked as I wanted.

At first, I checked which of the parameters I want to pass to legend and which to plot. First step to this end was to see which arguments of legend are unique to legend and not part of plot and/or par:

legend.args <- names(formals(legend)) plot.args <- c(names(formals(plot.default)), names(par())) dput(legend.args[!(legend.args %in% plot.args)]) 

I use dput() here, because the line plot.args <- c(names(formals(plot.default)), names(par())) always calls a new empty plot which I did not want. So, I used the output of dput in the following function.

Next, I had to deal with the overlapping arguments (get them via dput(largs.all[(largs.all %in% pargs.all)])). For some this was trivial (e.g., x, y) others get passed to both functions (e.g., pch). But, in my real application I even use other strategies (e.g., different variable names for adj, but not implemented in this example).

Finally, the do.call function had to be changed in two ways. First, the what part (i.e., called functions) needs to be a character (i.e., 'plot' instead of plot). And the argument list must be constructed slightly different.

foo.plot <- function(x,y,...) {     leg.args.unique <- c("legend", "fill", "border", "angle", "density", "box.lwd", "box.lty", "box.col", "pt.bg", "pt.cex", "pt.lwd", "xjust", "yjust", "x.intersp", "y.intersp", "text.width", "text.col", "merge", "trace", "plot", "ncol", "horiz", "title", "inset", "title.col", "title.adj")     leg.args.all <- c(leg.args.unique, "col", "lty", "lwd", "pch", "bty", "bg", "cex", "adj", "xpd")     dots <- list(...)     do.call('plot', c(list(x = x, y = x), dots[!(names(dots) %in% leg.args.unique)]))     do.call('legend', c(list("bottomleft", "bar"), dots[names(dots) %in% leg.args.all])) }   foo.plot(1,1,pch = 4, title = "legendary", ylim = c(0, 5)) 

In this example, pch is passed to both plot and legend, title is only passed to legend, and ylim only to plot.


Update 2 based on a comment by Gavin Simpson (see also the comments at Vitoshka's answer):
(i) That's correct.
(ii) It can always be a character. But if you have a variable with the same name as the function, then you need to quote the function name in do.call:

min.plot <- function(x,y,plot=TRUE) if(plot == TRUE) do.call(plot, list(x = x, y = y)) min.plot(1,1) Error in do.call(plot, list(x = x, y = y)) :    'what' must be a character string or a function 

(iii) You can use c(x = 1, y = 1, list()) and it works fine. However, what I really did (not in the example I gave but in my real function) is the following: c(x = 1, y = 1, xlim = c(0, 2), list(bla='foo'))
Please compare this with: c(list(x = 1, y = 1, xlim = c(0, 2)), list(bla='foo'))
In the first case, the list contains two elements xlim, xlim1 and xlim2 (each a scalar), in the latter case the list has only xlim (which is vector of length 2, which is what I wanted).

So, you are right in all your points for my example. But, for my real function (with a lot more variables), I encountered these problems and wanted to document them here. Sorry for being imprecise.

like image 920
Henrik Avatar asked Nov 08 '10 14:11

Henrik


People also ask

How do you do multiple If statements in R?

Multiple Conditions To join two or more conditions into a single if statement, use logical operators viz. && (and), || (or) and ! (not). && (and) expression is True, if all the conditions are true.

Can an R function return multiple objects?

You can only return a single object from a function in R; if you need to return multiple objects, you need to return a list containing those objects, and extract them from the list when you return to the calling environment.

Can you combine functions in R?

Concatenating Objects in R Programming – combine() Function Moreover, combine() function is used to combine factors in R programming.


2 Answers

An automatic way:

foo.plot <- function(x,y,...) {     lnames <- names(formals(legend))     pnames <- c(names(formals(plot.default)), names(par()))     dots <- list(...)     do.call('plot', c(list(x = x, y = x), dots[names(dots) %in% pnames]))     do.call('legend', c("bottomleft", "bar", pch = 1, dots[names(dots) %in% lnames])) } 

pch must be filtered from the lnames to avoid duplication in the legend call in case the user supplies 'pch', but you got the idea. Edited Jan 2012 by Carl W: "do.call" only works with the functions in quotes, as in the updates by Henrik. I edited it here to avoid future confusion.

like image 162
VitoshKa Avatar answered Sep 19 '22 20:09

VitoshKa


These things get tricky, and there aren't easy solutions without specifying extra arguments in your function. If you had ... in both the plot and legend calls you'd end up getting warnings when passing in legend-specific arguments. For example, with:

foo.plot <- function(x,y,...) {     plot(x,y,...)     legend("bottomleft", "bar", pch = 1, ...) } 

You get the following warnings:

> foo.plot(1, 1, xjust = 0.5) Warning messages: 1: In plot.window(...) : "xjust" is not a graphical parameter 2: In plot.xy(xy, type, ...) : "xjust" is not a graphical parameter 3: In axis(side = side, at = at, labels = labels, ...) :   "xjust" is not a graphical parameter 4: In axis(side = side, at = at, labels = labels, ...) :   "xjust" is not a graphical parameter 5: In box(...) : "xjust" is not a graphical parameter 6: In title(...) : "xjust" is not a graphical parameter 

There are ways round this problem, see plot.default and its local functions defined as wrappers around functions like axis, box etc. where you'd have something like a localPlot() wrapper, inline function and call that rather than plot() directly.

bar.plot <- function(x, y, pch = 1, ...) {     localPlot <- function(..., legend, fill, border, angle, density,                           xjust, yjust, x.intersp, y.intersp,                           text.width, text.col, merge, trace, plot = TRUE, ncol,                           horiz, title, inset, title.col, box.lwd,                           box.lty, box.col, pt.bg, pt.cex, pt.lwd) plot(...)     localPlot(x, y, pch = pch, ...)     legend(x = "bottomleft", legend = "bar", pch = pch, ...) } 

(Quite why the 'plot' argument needs a default is beyond me, but it won't work without giving it the default TRUE.)

Now this works without warnings:

bar.plot(1, 1, xjust = 0.5, title = "foobar", pch = 3) 

How you handle graphical parameters like bty for example will be up to you - bty will affect the plot box type and the legend box type. Note also that I've handled 'pch' differently because if someone use that argument in the bar.plot() call, you would be i) using different characters in the legend/plot and you'd get a warning or error about 'pch' matching twice.

As you can see, this starts getting quite tricky...


Joris' Answer provides an interesting solution, which I commented reminded me of control lists arguments in functions like lme(). Here is my version of Joris' Answer implementing the idea this control-list idea:

la.args <- function(x = "bottomleft", legend = "bar", pch = 1, ...)     c(list(x = x, legend = legend, pch = pch), list(...))  foo.plot <- function(x,y, legend.args = la.args(), ...) {     plot(x, y, ...)     do.call(legend, legend.args) } 

Which works like this, using Jori's second example call, suitably modified:

foo.plot(1,1, xaxt = "n", legend.args=la.args(bg = "yellow", title = "legend")) 

You can be as complete as you like when setting up the la.args() function - here I only set defaults for the arguments Joris set up, and concatenate any others. It would be easier if la.args() contained all the legend arguments with defaults.

like image 21
Gavin Simpson Avatar answered Sep 19 '22 20:09

Gavin Simpson