Skip to content

arranging ggplot

baptiste edited this page Mar 27, 2017 · 10 revisions

The plots produced by ggplot2 are self-contained graphical objects that may be displayed at arbitrary positions on a page. Notably, it is possible to combine multiple plots, and other graphical objects on a single page of output, although to do so you will need to learn a little bit of grid, the underlying graphics system used by ggplot2, or use helper functions provided by other packages such as gridExtra.

An individual ggplot object contains multiple pieces -- axes, plot panel(s), titles, legends --, and their layout is defined and enforced via the gtable package, itself built around the lower-level grid package. The following schematic illustrates the relation between these packages.

Schematic illustration of the links between packages ggplot2, gtable, grid, and gridExtra.

Arranging multiple plots on a page

To begin, we'll create four example plots that we can experiment with. When arranging multiple elements on a page, it will usually be easiest to create them, assign them to variables and, ultimately, render them on the graphics device. This strategy also makes it easier to experiment with plot placement independently of content, e.g. using dummy rectangles (rectGrob()) in place of the actual plots.

library(ggplot2)
p1 <- qplot(mpg, wt, data=mtcars, colour=cyl)
p2 <- qplot(mpg, data = mtcars) + ggtitle("title")
p3 <- qplot(mpg, data = mtcars, geom = "dotplot")
p4 <- p1 + facet_wrap(~carb, nrow=1) + theme(legend.position="none") +
  ggtitle("facetted plot")

A few plots that we want to organise on a page.

Basic strategy

The easiest approach to assemble multiple plots on a page is to use the grid.arrange() function from the gridExtra package; in fact, that's what we used for the previous figure. With grid.arrange(), one can reproduce the behaviour of the base functions par(mfrow=c(r,c)), specifying either the number of rows or columns,

grid.arrange(p1, p2, nrow = 1)

Basic usage of grid.arrange()

If layout parameters are ommitted altogether, grid.arrange() will calculate a default number of rows and columns to organise the plots.

More complex layouts can be achieved by passing specific dimensions (widths or heights), or a layout matrix defining the position of each plot in a rectangular grid. For the sake of clarity, we'll use a list gl of dummy rectangles, but the process is identical for plots.

grid.arrange(grobs = gl, widths = c(2, 1, 1),
             layout_matrix = rbind(c(1,2,NA), 
                                   c(3,3,4)))

Illustrating further arguments of grid.arrange(), namely layout_matrix and relative widths.

Further examples are available in a dedicated gridExtra vignette.

Plot insets

A special case of layouts is where one of the plots is to be placed within another, typically as an inset of the plot panel. In this case, grid.arrange() cannot help, as it only provides rectangular layouts with non-overlapping cells. Instead, a simple solution is to convert the plot into a grob, and place it using annotation_custom() within the plot panel.

g <- ggplotGrob(qplot(1, 1) +
                  theme(plot.background = element_rect(colour = "black")))
qplot(1:10, 1:10) + 
  annotation_custom(grob = g, xmin = 1, xmax = 5, ymin = 5, ymax = 10) +
  annotation_custom(grob = rectGrob(gp=gpar(fill="white")), xmin = 7.5, 
                    xmax = Inf, ymin = -Inf, ymax =5)

Plot inset.

In the second annotation, we used the convenient shorthand +/-Inf to signify the edge of the plot, irrespective of the data range.

An alternative way to place custom annotations within the plots is to use raw grid commands, which we will present at the end of this document. However, note that an advantage of using annotation_custom is that the inset plot is embedded in the main plot, therefore the whole layout can be saved with ggsave(), which will not be the case for plot modifications at the grid level.

Aligning plot panels

A common request for presenting multiple plots on a single page is to align the plot panels. Often, facetting the plot solves this issue, with a flexible syntax, and in the true spirit of the Grammar of Graphics that inspired the ggplot2 design. However, in some situations, the various plot panels cannot easily be combined in a unique plot; for instance when using different geoms, or different colour scales.

grid.arrange() makes no attempt at aligning the plot panels; instead, it merely places the objects into a rectangular grid, where they fit each cell according to the varying size of plot elements. The following figure illustrates the typical structure of ggplots.

Colour-coded structure of examplar ggplot layouts. Note how the panels (red) vary in size from plot to plot, as they accommodate the other graphical components.

As we can readily appreciate, each plot panel stretches or shrinks according to the other plot elements, e.g. guides, axes, titles, etc. This often results in misaligned plot panels.

In this situation, instead of using grid.arrange(), we recommend to switch to the more powerful gtable package. In particular, the rbind(), cbind(), and join functions can provide a better alignment. The plots must first be converted to grobs (more specifically, gtables), using the ggplotGrob() function. The second step is to bind the two gtables, using the sizes from the first object, then assigning them to the maximum. Finally, the resulting object, a gtable, can be displayed using grid.draw() (it is no longer a ggplot, so print() no longer renders it on a device).

library(gtable)
g2 <- ggplotGrob(p2)
g3 <- ggplotGrob(p3)
g <- rbind(g2, g3, size="first")
g$widths <- unit.pmax(g2$widths, g3$widths)
grid.newpage()
grid.draw(g)

Aligning plot panels. Note that the two y axes have different widths.

Fix the panel size

In some cases, having ggplot2 expand the plot panel to best fit the available space isn't ideal: for instance, we may want to produce multiple plots to appear on different pages of a beamer/powerpoint presentation, and the successive pages should have the exact same layout for smoother visual transition. Another use-case is to embed multiple separate graphics in a drawing/page layout software, such as Inkscape, Illustrator, etc. In this situation the plot alignement will be made manually, but the plots should not be rescaled (otherwise the fonts will be distorted). For such situations, the easiest solution is to set fixed dimensions to the gtable produced by ggplot2. This is illustrated below using a helper function.

set_panel_size <- function(p=NULL, g=ggplotGrob(p), file=NULL, 
                           margin = unit(1,"mm"),
                           width=unit(4, "cm"), 
                           height=unit(4, "cm")){

  panels <- grep("panel", g$layout$name)
  panel_index_w<- unique(g$layout$l[panels])
  panel_index_h<- unique(g$layout$t[panels])
  nw <- length(panel_index_w)
  nh <- length(panel_index_h)

if(getRversion() < "3.3.0"){

   # the following conversion is necessary
   # because there is no `[<-`.unit method
   # so promoting to unit.list allows standard list indexing
   g$widths <- grid:::unit.list(g$widths)
   g$heights <- grid:::unit.list(g$heights)

   g$widths[panel_index_w] <-  rep(list(width),  nw)
   g$heights[panel_index_h] <- rep(list(height), nh)

} else {

   g$widths[panel_index_w] <-  rep(width,  nw)
   g$heights[panel_index_h] <- rep(height, nh)

}

  if(!is.null(file))
    ggsave(file, g, 
           width = convertWidth(sum(g$widths) + margin, 
                                unitTo = "in", valueOnly = TRUE),
           height = convertHeight(sum(g$heights) + margin,  
                                  unitTo = "in", valueOnly = TRUE))

  g
}

Note that the total size is now fixed, therefore when exporting the plot on a device it can be useful to query the size and set the width and height accordingly, to avoid clipping or white margins. This is taken into account in this example, working with ggsave. This function is available in the egg package (on github).

Aligning complex plots

We can extend the previous code to align complex facetted plots. One possible strategy, illustrated below, is to take the following steps:

  • decompose each plot into a 3x3 layout, where the central cell corresponds to the core panels, surrounded by axes, legends, etc.
  • set the core width and height to a fixed dimension
  • align the individual 3x3 gtables using rbind/cbind
gtable_frame <- function(g, width=unit(1,"null"), height=unit(1,"null")){
  panels <- g[["layout"]][grepl("panel", g[["layout"]][["name"]]), ]
  ll <- unique(panels$l)
  tt <- unique(panels$t)
  
  fixed_ar <- g$respect
  if(fixed_ar) { # there lies madness, we want to align with aspect ratio constraints
    ar <- as.numeric(g$heights[tt[1]]) / as.numeric(g$widths[ll[1]])
    print(ar)
    height <- width * ar
    g$respect <- FALSE
  }
  
  core <- g[seq(min(tt), max(tt)), seq(min(ll), max(ll))]
  top <- g[seq(1, min(tt)-1), ]
  bottom <- g[seq(max(tt)+1, nrow(g)), ]
  left <- g[, seq(1, min(ll)-1)]
  right <- g[, seq(max(ll)+1, ncol(g))]
  
  fg <- nullGrob()
  lg <-  if(length(left))  g[seq(min(tt), max(tt)), seq(1, min(ll)-1)] else fg
  rg <- if(length(right)) g[seq(min(tt), max(tt)), seq(max(ll)+1,ncol(g))] else fg
  grobs = list(fg, g[seq(1, min(tt)-1), seq(min(ll), max(ll))], fg, 
               lg, g[seq(min(tt), max(tt)), seq(min(ll), max(ll))], rg, 
               fg, g[seq(max(tt)+1, nrow(g)), seq(min(ll), max(ll))], fg)
  widths <- unit.c(sum(left$widths), width, sum(right$widths))
  heights <- unit.c(sum(top$heights), height, sum(bottom$heights))
  all <- gtable_matrix("all", grobs = matrix(grobs, ncol=3, nrow=3, byrow = TRUE), 
                       widths = widths, heights = heights)
  all[["layout"]][5,"name"] <- "panel" # make sure knows where the panel is
  if(fixed_ar)  all$respect <- TRUE
  all
}

p1 <- ggplot(mtcars, aes(mpg, wt, colour = factor(cyl))) +
  geom_point() 

p2 <- ggplot(mtcars, aes(mpg, wt, colour = factor(cyl))) +
  geom_point() + facet_wrap( ~ cyl, ncol=2, scales = "free") +
  guides(colour="none") +
  theme()

p3 <- ggplot(mtcars, aes(mpg, wt, colour = factor(cyl))) +
  geom_point() + facet_grid(. ~ cyl, scales = "free")

g1 <- ggplotGrob(p1);
g2 <- ggplotGrob(p2);
g3 <- ggplotGrob(p3);
fg1 <- gtable_frame(g1)
fg2 <- gtable_frame(g2)
fg12 <- gtable_frame(rbind(fg1,fg2), width=unit(2,"null"), height=unit(1,"null"))
fg3 <- gtable_frame(g3, width=unit(1,"null"), height=unit(1,"null"))
grid.newpage()
combined <- cbind(fg12, fg3)
combined <- gtable_add_grob(combined, rectGrob(gp=gpar(fill="grey98", alpha=0.5, lty=2, lwd=1.5)),
                            l=2, r=5, t=2, b=2, z=Inf, name="debug")
grid.draw(combined)

This strategy is implemented in the experimental egg package available on github. Aligning plots is achieved simply as follows,

library(egg)
p1 <- ggplot(mtcars, aes(mpg, wt, colour = factor(cyl))) +
geom_point() 
p2 <- ggplot(mtcars, aes(mpg, wt, colour = factor(cyl))) +
geom_point() + facet_wrap( ~ cyl, ncol=2, scales = "free") +
guides(colour="none") +
theme()

ggarrange(p1, p2, widths = 1:2)

Multiple pages?

Plots produced by ggplot2, including those with facets, and those combined with grid.arrange(), are always displayed on a single page. Sometimes, however, there isn't enough room to display all the information, and it becomes necessary to split the output on multiple pages. A convenient approach consists in storing all the plots in a list, and plotting subsets of them on subsequent pages. The gridExtra package can simplify this process with the helper function marrangeGrob(), sharing a common syntax with grid.arrange(), but outputting as many pages as required by the total number of plots and per-page layout.

Mixed graphical components

Titles and subtitles

Adding a global title and/or subtitle to a page with multiple plots is easy with grid.arrange(): use the top, bottom, left, or right parameters to pass either a text string, or a grob for finer control.

grid.arrange(p3, p3, p3, nrow=1,
             top="Title of the page", 
             bottom = textGrob("this footnote is right-justified",
                               gp = gpar(fontface=3, fontsize=9),
                               hjust=1, x=1))

Recent versions of ggplot2 have added built-in options to add a subtitle and a caption; the two stategies are somewhat complementary (grid.arrange aligns elements with respect to the entire plot, whereas ggplot2 places them with respect to the plot panel area).

Legends

When arranging multiple plots, one may wish to share a legend between some of them (although in general this is a clear hint that facetting might be a better option). The procedure involves extracting the legend from one graph, creating a custom layout, and inserting the plots and legend in their corresponding cell.

arrange_2x1_shared_legend <- function(p1, p2) {
   
    g1 <- ggplotGrob(p1)
    id.legend <- grep("guide", g1$layout$name)
    legend <- g1[["grobs"]][[id.legend]]
    lwidth <- sum(legend$width)
    
    grid.arrange(p1 + theme(legend.position="none"), 
                 p2 + theme(legend.position="none"), 
                 legend, 
                 layout_matrix = rbind(c(1,3), c(2,3)),
                 widths = unit.c(unit(1, "npc") - lwidth, lwidth))
}
arrange_2x1_shared_legend(p1, p2)

Tables and other grobs

As we've seen in the previous examples, ggplots are grobs, which can be placed and manipulated. Likewise, other grobs can be added to the mix. For instance, one may wish to add a small table next to the plot, as produced by the tableGrob function in gridExtra.

grid.arrange(tableGrob(mtcars[1:4, 1:4]), p2, 
             ncol=2, widths=c(1.5, 1), clip=FALSE)

Alternative options

We've focused on grid.arrange() for simplicity, but there are numerous alternatives to achieve similar arrangements of plots. We list below some of the most common options (in order of appearance).

Package Function(s) ggsave compat. alignment
grid viewport no no
gridExtra grid.arrange yes no
(r cookbook) multiplot no no
gtable rbind, cbind yes yes
cowplot plot_grid yes* yes*
egg ggarrange yes yes

grid viewports and layouts

Underlying all these other packages is the grid package, included in the core R distribution. This package provides the low-level functions used for drawing and placing objects on a page (device). The key concept for object placeement is that of a viewport: in grid terminology, this represents a rectangular subregion of the page or display. The default viewport takes up the entire page, and by customising the viewport's location, size, and even orientation you can arrange a set of plots in just about any way you can imagine.

pushViewport(viewport(layout = grid.layout(2, 2)))
vplayout <- function(x, y) viewport(layout.pos.row = x, layout.pos.col = y)
print(p1, vp = vplayout(1, 1:2))
print(p2, vp = vplayout(2, 1))
print(p3, vp = vplayout(2, 2))