40

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.

Henrik
  • 14,202
  • 10
  • 68
  • 91
  • 1
    Couple of points on your edited Q; i) `par()` will *only* produce a blank plot if there isn't a currently open device. This is harmless as you are about to plot on it, ii) You are wrong about the `'what'` argument - it can be character or a function, iii) You don't need the `list()` wrapping in `list("bottomleft", "bar")`, because everything is coerced to a common type --- witness `c("foo", 1:10, list(a = 1:10, b = letters[1:3]))`. Nice Q and follow-up mind, but good to clean up your edits for accuracy. – Gavin Simpson Nov 10 '10 at 17:56
  • 1
    @Gavin Please see my update for a response to your points. – Henrik Nov 11 '10 at 09:04
  • 2
    good update and nice to point out the corner cases like this. Bravo! On the function name /variable name clash - you could always be more defensive and use `graphics::plot` and `graphics::legend` to be doubly sure the correct function is found. – Gavin Simpson Nov 11 '10 at 14:21

6 Answers6

26

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.

Carl Witthoft
  • 20,573
  • 9
  • 43
  • 73
VitoshKa
  • 8,387
  • 3
  • 35
  • 59
  • 1
    Nice solution, but you need quite a lot more filtering than just pch. There are a number of parameters that can be valid for both plot and legend. How do you go about them? – Joris Meys Nov 09 '10 at 10:54
  • 1
    @Joris, If a parameter is valid for both functions it will be passed to both. I am not really getting what you mean. One needs to filter pch to avoid duplication in the call to legend. – VitoshKa Nov 09 '10 at 12:41
  • 1
    @VitoshKa : suppose you want to set that parameter only in plot, or only in legend. Say pch for example. That won't be possible with the function as is, there's nowhere you can specify for which function exactly you want to set the parameter. – Joris Meys Nov 09 '10 at 12:50
  • @Joris, I see what you mean. In this case your solution with legend.args would be more appropriate of course. – VitoshKa Nov 09 '10 at 12:59
  • 1
    Yeah, it is a great way. However, there were minor issues, which I corrected in my question. – Henrik Nov 09 '10 at 21:16
  • @Henrik, You are right there with par creating new plot. I completely forgot about that:(. In do.call though, "what" argument can be a function directly, or a character, and the second argument in my code is a list because `dots` is a list and "c" will make the whole thing a list. So, my code was right there. People might get a different impressiong from your update. – VitoshKa Nov 10 '10 at 08:28
  • @Vitoshka: (1) When i used the function instead of the character I always got a warning, so I used the character (but as you said it did work). (2) However, when not using c(list(foo), ...) horrible things happened, because all elements that were not part of the ... statement were coerced into one list element (I think). This was kind of tricky to find out. – Henrik Nov 10 '10 at 09:27
  • @Henrik, Strange indeed, I don't experience either of those problems. – VitoshKa Nov 10 '10 at 10:46
  • @Vitoshka Please see my second update on why I had these problems. Sorry for making you responsible for my problems :) – Henrik Nov 11 '10 at 09:05
18

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.

Gavin Simpson
  • 170,508
  • 25
  • 396
  • 453
  • That's more than a handful to digest at once ;-) Yet, thank you very much for your elaborative answer, it really taught me a thing or two on argument passing. Indeed, I got the idea from lme -and other- functions partly taking lists as arguments for something specific. Thx for showing how to get around the "matched by multiple actual arguments" problem. – Joris Meys Nov 08 '10 at 16:10
  • 1
    @Joris: Thanks. Prof. Ripley educated me on the `localFoo()` method and we've used it in one of the packages I contribute to. – Gavin Simpson Nov 08 '10 at 16:21
6

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.

Joris Meys
  • 106,551
  • 31
  • 221
  • 263
  • this reminds me of the `control` arguments in functions like `lme()`, which uses arguments like `control = lme.control()`. `lme.control()` contains the defaults and exists to populate a list of suitable arguments. I'll add an example to my answer as it is too complicated for a comment. – Gavin Simpson Nov 08 '10 at 15:51
  • Thanks, this seems to be a good way to do it. I will wait some more time before accepting, perhaps some other good answer pops up. But, I am already willing to accept this. – Henrik Nov 08 '10 at 15:52
  • You can use this approach with `modifyList`, to be able to merge legend.args with your default set of legend arguments. – Charles Nov 08 '10 at 16:39
  • 1
    Also another way you can do it is to match on eg names(formals(legend)), to segment the arguments to be used for each function. – Charles Nov 08 '10 at 16:42
  • @Charles: thing is, some arguments will match both formals(legend) and formals(plot). That's the problem Gavin illustrated. – Joris Meys Nov 08 '10 at 17:00
4

We could build a formalize function that will make any function compatible with dots :

formalize <- function(f){
  # add ... to formals
  formals(f) <- c(formals(f), alist(...=))
  # release the ... in the local environment
  body(f)    <- substitute({x;y},list(x = quote(list2env(list(...))),y = body(f)))
  f
}

foo.plot <- function(x,y,...) {
  legend <- formalize(legend) # definition is changed locally
  plot(x,y,...)
  legend("bottomleft", "bar", pch = 1, ...)
}    

foo.plot(1,1, xaxt = "n", title = "legend")

It shows warnings though, because plot is receiving arguments it doesn't expect.

We could use suppressWarnings on the plot call (but it will suppress all warnings obviously), or we could design another function to make the plot function (or any function) locally more tolerant to dots :

casualize <- function(f){
  f_name <- as.character(substitute(f))
  f_ns   <- getNamespaceName(environment(f))
  body(f) <- substitute({
      # extract all args
      args <- as.list(match.call()[-1]);
      # relevant args only
      args <- args[intersect(names(formals()),names(args))]
      # call initial fun with relevant args
      do.call(getExportedValue(f_ns, f_name),args)})
  f
}

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

foo.plot(1,1, xaxt = "n", title = "legend")

Now it works fine!

moodymudskipper
  • 46,417
  • 11
  • 121
  • 167
2

A similar solution to the accepted answer, but this one uses list2() and the splice operator !!! (plus its auxiliary function inject()) from the rlang package. It has the advantage of not needing to concatenate named or hardcoded arguments to the "dots" list.

foo.plot <- function(x, y, leg.pos='bottomleft', legend='bar', ...) {
    #.Pars.readonly <- c("cin", "cra", "csi", "cxy", "din", "page")  # from par()
    eval(body(par)[[2]])  # .Pars.readonly <- c(...)

    this_params <- names(formals())
    par_params <- setdiff(graphics:::.Pars, .Pars.readonly)
    plot_params <- setdiff(union(formalArgs(plot.default), par_params), this_params)
    legend_params <- setdiff(formalArgs(graphics::legend), this_params)

    dots <- rlang::list2(...)
    plot_args <- dots[names(dots) %in% plot_params]
    legend_args <- dots[names(dots) %in% legend_params]

    rlang::inject(plot.default(x, y, !!!plot_args))
    rlang::inject(graphics::legend(leg.pos, legend=legend, !!!legend_args))
}
leogama
  • 898
  • 9
  • 13
1

Just wanted to add a general solution that I've implemented. I wrote a function dots_parser which takes the function you want to call, any named arguments, and the ellipsis, and then runs the function with only the appropriate arguments.

dots_parser <- function(FUN, ...) {
  argnames <- names(formals(FUN))
  dots <- list(...)
  return(do.call(FUN, dots[names(dots) %in% argnames]))
}

So for instance, if code previously would've looked like this:

func1(a = myvaluea, b = myvalueb, ...)

But that returns an error because ... includes arguments not accepted by func1, this now works:

dots_parser(func1, a = myvaluea, b = myvalueb, ...)