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.
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.
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.
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.
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!