93

This is cross-posted on the ggplot2 google group

My situation is that I'm working on a function that outputs an arbitrary number of plots (depending upon the input data supplied by the user). The function returns a list of n plots, and I'd like to lay those plots out in 2 x 2 formation. I'm struggling with the simultaneous problems of:

  1. How can I allow the flexibility to be handed an arbitrary (n) number of plots?
  2. How can I also specify I want them laid out 2 x 2

My current strategy uses grid.arrange from the gridExtra package. It's probably not optimal, especially since, and this is key, it totally doesn't work. Here's my commented sample code, experimenting with three plots:

library(ggplot2)
library(gridExtra)

x <- qplot(mpg, disp, data = mtcars)
y <- qplot(hp, wt, data = mtcars)
z <- qplot(qsec, wt, data = mtcars)

# A normal, plain-jane call to grid.arrange is fine for displaying all my plots
grid.arrange(x, y, z)

# But, for my purposes, I need a 2 x 2 layout. So the command below works acceptably.
grid.arrange(x, y, z, nrow = 2, ncol = 2)

# The problem is that the function I'm developing outputs a LIST of an arbitrary
# number plots, and I'd like to be able to plot every plot in the list on a 2 x 2
# laid-out page. I can at least plot a list of plots by constructing a do.call()
# expression, below. (Note: it totally even surprises me that this do.call expression
# DOES work. I'm astounded.)
plot.list <- list(x, y, z)
do.call(grid.arrange, plot.list)

# But now I need 2 x 2 pages. No problem, right? Since do.call() is taking a list of
# arguments, I'll just add my grid.layout arguments to the list. Since grid.arrange is
# supposed to pass layout arguments along to grid.layout anyway, this should work.
args.list <- c(plot.list, "nrow = 2", "ncol = 2")

# Except that the line below is going to fail, producing an "input must be grobs!"
# error
do.call(grid.arrange, args.list)

As I am wont to do, I humbly huddle in the corner, eagerly awaiting the sagacious feedback of a community far wiser than I. Especially if I'm making this harder than it needs to be.

briandk
  • 6,749
  • 8
  • 36
  • 46

3 Answers3

45

You're ALMOST there! The problem is that do.call expects your args to be in a named list object. You've put them in the list, but as character strings, not named list items.

I think this should work:

args.list <- c(plot.list, 2,2)
names(args.list) <- c("x", "y", "z", "nrow", "ncol")

as Ben and Joshua pointed out in the comments, I could have assigned names when I created the list:

args.list <- c(plot.list,list(nrow=2,ncol=2))

or

args.list <- list(x=x, y=y, z=x, nrow=2, ncol=2)
JD Long
  • 59,675
  • 58
  • 202
  • 294
  • So, if I'm understanding you correctly: even though arg.list IS a list, it contains two elements (the two strings I passed it) which are character vectors, and thus, violates do.call's expectation? – briandk Jul 13 '11 at 15:30
  • 1
    I changed the code a couple of times. Sorry for the edits. does it make sense now? When I said they were a vector earlier, I misspoke. Sorry about that. – JD Long Jul 13 '11 at 15:37
  • 2
    You can name the args during list creation: `args.list <- list(x=x, y=y, z=x, nrow=2, ncol=2)` – Joshua Ulrich Jul 13 '11 at 15:37
  • And they don't _have_ to be named, e.g. `do.call("+", list(1,2))` works just as well as `do.call("+", list(e1=1,e2=2))`. – Joshua Ulrich Jul 13 '11 at 15:45
  • 2
    Not exactly. Yours is of the proper length. The structure of your list is different than the structure of JD's list. Use str() and names(). All of your list elements are unnamed, so for the `do.call` to succeed, there would have needed to be exact positional matching. – IRTFM Jul 13 '11 at 15:45
  • or even `c(plot.list,list(nrow=2,ncol=2))` – Ben Bolker Jul 13 '11 at 15:45
  • This probably risks revealing something of my personality type, but positional mapping makes me nervous. I'm always afraid the function syntax will change and add a position or I won't be able to understand what I did when I read the code later. So I tend to be overly verbose with naming and explicit arguments. – JD Long Jul 13 '11 at 15:48
  • 2
    @JD Long; I heartily agree. And ven if it doesn't prevent all the errors, you still get much better error messages and `traceback()` information if you use named arguments. – IRTFM Jul 13 '11 at 16:05
  • To be clear, I wasn't suggesting that you _shouldn't_ name arguments to `do.call`; just that it's not _strictly_ required. I agree that naming arguments is generally good practice. – Joshua Ulrich Jul 13 '11 at 16:12
  • 1
    I don't quite follow the discussion here; since the first argument to `grid.arrange()` is `...` positional matching is probably irrelevant. Each input must be either a grid object (with or without name), a named parameter for `grid.layout`, or a named parameter for the remaining arguments. – baptiste Jul 14 '11 at 02:47
16

Try this,

require(ggplot2)
require(gridExtra)
plots <- lapply(1:11, function(.x) qplot(1:10,rnorm(10), main=paste("plot",.x)))

params <- list(nrow=2, ncol=2)

n <- with(params, nrow*ncol)
## add one page if division is not complete
pages <- length(plots) %/% n + as.logical(length(plots) %% n)

groups <- split(seq_along(plots), 
  gl(pages, n, length(plots)))

pl <-
  lapply(names(groups), function(g)
         {
           do.call(arrangeGrob, c(plots[groups[[g]]], params, 
                                  list(main=paste("page", g, "of", pages))))
         })

class(pl) <- c("arrangelist", "ggplot", class(pl))
print.arrangelist = function(x, ...) lapply(x, function(.x) {
  if(dev.interactive()) dev.new() else grid.newpage()
   grid.draw(.x)
   }, ...)

## interactive use; open new devices
pl

## non-interactive use, multipage pdf
ggsave("multipage.pdf", pl)
baptiste
  • 75,767
  • 19
  • 198
  • 294
4

I'm answering a bit late, but stumbled on a solution at the R Graphics Cookbook that does something very similar using a custom function called multiplot. Perhaps it will help others who find this question. I'm also adding the answer as the solution may be newer than the other answers to this question.

Multiple graphs on one page (ggplot2)

Here's the current function, though please use the above link, as the author noted that it's been updated for ggplot2 0.9.3, which indicates it may change again.

# Multiple plot function
#
# ggplot objects can be passed in ..., or to plotlist (as a list of ggplot objects)
# - cols:   Number of columns in layout
# - layout: A matrix specifying the layout. If present, 'cols' is ignored.
#
# If the layout is something like matrix(c(1,2,3,3), nrow=2, byrow=TRUE),
# then plot 1 will go in the upper left, 2 will go in the upper right, and
# 3 will go all the way across the bottom.
#
multiplot <- function(..., plotlist=NULL, file, cols=1, layout=NULL) {
  require(grid)

  # Make a list from the ... arguments and plotlist
  plots <- c(list(...), plotlist)

  numPlots = length(plots)

  # If layout is NULL, then use 'cols' to determine layout
  if (is.null(layout)) {
    # Make the panel
    # ncol: Number of columns of plots
    # nrow: Number of rows needed, calculated from # of cols
    layout <- matrix(seq(1, cols * ceiling(numPlots/cols)),
                    ncol = cols, nrow = ceiling(numPlots/cols))
  }

 if (numPlots==1) {
    print(plots[[1]])

  } else {
    # Set up the page
    grid.newpage()
    pushViewport(viewport(layout = grid.layout(nrow(layout), ncol(layout))))

    # Make each plot, in the correct location
    for (i in 1:numPlots) {
      # Get the i,j matrix positions of the regions that contain this subplot
      matchidx <- as.data.frame(which(layout == i, arr.ind = TRUE))

      print(plots[[i]], vp = viewport(layout.pos.row = matchidx$row,
                                      layout.pos.col = matchidx$col))
    }
  }
}

One creates plot objects:

p1 <- ggplot(...)
p2 <- ggplot(...)
# etc.

And then passes them to multiplot:

multiplot(p1, p2, ..., cols = n)
Hendy
  • 10,182
  • 15
  • 65
  • 71