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

Solution 1:

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.

Solution 2:

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.

Solution 3:

One way around is using lists of arguments in combination with do.call. It's not the most beautiful solution, but it does work.

foo.plot <- function(x,y,legend.args,...) {
    la <- list(
        x="bottomleft",
        legend="bar",
        pch=1
    )
    if (!missing(legend.args)) la <- c(la,legend.args)
    plot(x,y,...)
    do.call(legend,la)
}
foo.plot(1,1, xaxt = "n")    
foo.plot(1,1, xaxt = "n",legend.args=list(bg="yellow",title="legend"))

One drawback is that you cannot specify eg pch=2 for example in the legend.args list. You can get around that with some if clauses, I'll leave it to you to further fiddle around with it.


Edit : see the answer of Gavin Simpson for a better version of this idea.