Est-il possible d'utiliser deux '...' états dans une fonction dans R?

Je veux écrire une fonction qui appelle à la fois plot() et legend() et il serait idéal que l'utilisateur puisse spécifier un certain nombre d'arguments supplémentaires qui sont ensuite transmis à plot() ou legend(). Je sais que je peux y parvenir pour l'une des deux fonctions en utilisant ...:

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

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

Cela passe {[9] } à tracer. Mais existe-t-il un moyen par exemple de passer par exemple title = "legend" à l'appel legend() sans spécifier les arguments dans l'en-tête de la fonction?


Mise à Jour de la réponse acceptée: je pensais que le chemin de VitoshKa était le plus élégant pour accomplir ce que je voulais. Cependant, il y avait quelques problèmes mineurs que je devais me déplacer jusqu'à ce que cela fonctionne comme je le voulais.

Au début, j'ai vérifié lequel des paramètres que je veux passer à legend et qui à plot. La première étape à cette fin était de voir quels arguments de legend sont uniques à legend et ne font pas partie de l'intrigue et / ou du par:

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

J'utilise dput() ici, car la ligne plot.args <- c(names(formals(plot.default)), names(par())) appelle toujours un nouveau terrain vide que je ne voulais pas. Donc, j'ai utilisé la sortie de dput dans la fonction suivante.

Ensuite, j'ai dû faire face aux arguments qui se chevauchent (les obtenir via dput(largs.all[(largs.all %in% pargs.all)])). Pour certains, c'était trivial (par exemple, x, y) d'autres sont transmis aux deux fonctions (par exemple, pch). Mais, dans mon application réelle, j'utilise même d'autres stratégies (par exemple, différents noms de variables pour adj, mais pas implémentés dans cet exemple).

Enfin, la fonction do.call a dû être modifiée de deux façons. Premier, la partie quelle (c'est-à-dire les fonctions appelées) doit être un caractère (c'est-à-dire 'plot' au lieu de plot). Et la liste d'arguments doit être construite légèrement différente.

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))

Dans cet exemple, pch est passé à deux plot et legend, title sont uniquement transmises à legend, et ylim seulement plot.


Mise à jour 2 basée sur un commentaire de Gavin Simpson (voir aussi les commentaires à la réponse de Vitoshka):
(I) c'est exact.
(ii) Il peut toujours être un personnage. Mais si vous disposez d'une variable avec le même nom que la fonction, alors vous devez citer le nom de la fonction 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) vous pouvez utiliser c(x = 1, y = 1, list()) et cela fonctionne bien. Cependant, ce que j'ai vraiment fait (pas dans l'exemple que j'ai donné mais dans ma fonction réelle) est le suivant: c(x = 1, y = 1, xlim = c(0, 2), list(bla='foo'))
Veuillez comparer cela avec: c(list(x = 1, y = 1, xlim = c(0, 2)), list(bla='foo'))
Dans le premier cas, la liste contient deux éléments xlim, xlim1 et xlim2 (chacun étant un scalaire), dans ce dernier cas, la liste n'a qu' xlim (qui est un vecteur de longueur 2, qui est ce que je voulais).

Donc, vous avez raison dans tous vos points pour mon exemple. Mais, pour ma vraie fonction (avec beaucoup plus de variables), j'ai rencontré ces problèmes et je voulais les documenter ici. Désolé d'être imprécis.

30
demandé sur Henrik 2010-11-08 17:49:49

4 réponses

Une manière automatique:

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 doit être filtré à partir des lnames pour éviter la duplication dans l'appel legend au cas où l'utilisateur fournirait 'pch', mais vous avez eu l'idée. Edité Jan 2012 par Carl W: "do.call " ne fonctionne qu'avec les fonctions entre guillemets, comme dans les mises à jour de Henrik. J'ai édité ici pour éviter toute confusion.

19
répondu VitoshKa 2013-01-22 14:17:45

Ces choses deviennent délicates, et il n'y a pas de solutions faciles sans spécifier d'arguments supplémentaires dans votre fonction. Si vous aviez ... dans les appels plot et legend, vous finiriez par recevoir des avertissements lors de la transmission d'arguments spécifiques à legend. Par exemple, avec:

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

Vous obtenez les avertissements suivants:

> 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

Il existe des façons de contourner ce problème, Voir plot.default et ses fonctions locales définies comme des wrappers autour de fonctions comme axis, box etc. où vous auriez quelque chose comme un localPlot() wrapper, fonction en ligne et appelez cela plutôt que plot() directement.

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, ...)
}

(tout à fait pourquoi l'argument 'plot' a besoin d'une valeur par défaut est au-delà de moi, mais cela ne fonctionnera pas sans lui donner la valeur par défaut TRUE.)

Maintenant, cela fonctionne sans avertissements:

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

La façon dont vous gérez les paramètres graphiques comme bty par exemple dépendra de vous - bty affectera le type de zone de tracé et le type de zone de légende. Notez également que j'ai traité 'pch' différemment parce que si quelqu'un l'utilise argument dans l'appel bar.plot(), vous seriez i) en utilisant différents caractères dans la légende / intrigue et vous obtiendriez un avertissement ou une erreur sur 'pch' correspondant deux fois.

Comme vous pouvez le voir, cela commence à devenir assez délicat...


La réponse de Joris fournit une solution intéressante, que j'ai commentée m'a rappelé des arguments de listes de contrôle dans des fonctions comme lme(). Voici ma version de la réponse de Joris implémentant l'idée de cette idée de liste de contrôle:

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)
}

Qui fonctionne comme ceci, en utilisant le deuxième exemple d'appel de Jori, convenablement Modifié:

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

Vous pouvez être aussi complet que vous le souhaitez lors de la configuration de la fonction la.args() - ici, Je ne définis que les paramètres par défaut pour les arguments mis en place par Joris et concaténer les autres. Ce serait plus facile si la.args() contenait tous les arguments de légende avec des valeurs par défaut.

18
répondu Gavin Simpson 2010-11-08 15:57:06

Une solution consiste à utiliser des listes d'arguments en combinaison avec do.call. Ce n'est pas la plus belle solution, mais ça marche.

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"))

Un inconvénient est que vous ne pouvez pas spécifier par exemple pch=2 par exemple dans la légende.liste args. Vous pouvez contourner cela avec quelques clauses if, je vous laisse le soin de jouer avec elle.


Edit : voir la réponse de M. Simpson pour une meilleure version de cette idée.

6
répondu Joris Meys 2010-11-08 16:13:24

Nous pourrions construire une fonction formalize qui rendra n'importe quelle fonction compatible avec les points:

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")

Il montre cependant des avertissements, car plot reçoit des arguments qu'il n'attend pas.

Nous pourrions utiliser suppressWarnings sur l'appel de tracé (mais il supprimera tous les Avertissements évidemment), ou nous pourrions concevoir une autre fonction pour rendre la fonction plot (ou n'importe quelle fonction) localement plus tolérante aux points:

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")

Maintenant, cela fonctionne très bien!

2
répondu Moody_Mudskipper 2018-09-19 12:36:40