Post #9. Arrange multiple ggplots on one graphic device

2021

Come and learn two methods to arrange multiple ggplots on one graphic device.

Gen-Chang Hsu
12-20-2021

Feel excited to re-blog after taking a one-and-a-half months break. Quite shocked at my rusty writing skills. I think I really need to brush up a bit! This time, I will just do a simple post as a warm-up.

Introduction

Sometimes we have a set of related plots and we would like to arrange them as a single graph. Instead of doing this manually (e.g., using MS PowerPoint), we can actually do it in R. This not only streamlines the figure generation process, but also makes it fully reproducible so that others can get the exact figure as yours with your code.

In this post, I will show you two methods for laying out multiple ggplots: (1) using the function ggarrange() in the package ggpubr, and (2) using the function draw_plot() and its families in the package cowplot. Let’s jump right in!

Method 1. Using ggarrange() in the package ggpubr

ggarrange() is a convenient function for arranging multiple ggplots on a same graphic device. It also allows the users to make some adjustments, for example, adding panel labels and aligning the plots by axis. Moreover, the package offers a function for adding text annotations to the figure. We’ll go through these features one by one in the following.

To begin with, let’s create some plots using the famous iris data set:

library(tidyverse)

### 1. A scatterplot of Sepal.Length vs. Sepal.Width
P1 <- ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, color = Species)) + 
  geom_point() + 
  scale_color_brewer(palette = "Set1", name = "") + 
  theme_classic(base_size = 14)

### 2. Density plot of Sepal.Length
P2 <- ggplot(iris, aes(x = Sepal.Length, color = Species)) + 
  geom_density(aes(y = ..scaled..)) + 
  scale_color_brewer(palette = "Set1", name = "") + 
  theme_classic(base_size = 14) + 
  scale_y_continuous(breaks = seq(0, 1, 0.2))

### 3. Density plot of Sepal.Width
P3 <- ggplot(iris, aes(x = Sepal.Width, color = Species)) + 
  geom_density(aes(y = ..scaled..)) + 
  scale_color_brewer(palette = "Set1", name = "") + 
  theme_classic(base_size = 14) + 
  scale_y_continuous(breaks = seq(0, 1, 0.2)) +
  coord_flip()
  
P1
P2
P3

1. Basic layout

Now, say, we would like to place the density plots at the top (for Sepal.Length) and right (for Sepal.Width) of the scatterplot to show their distributions. We can do this by passing the above ggplot objects (P1, P2 and P3) into ggarrange() and specify the layout designs (the numbers of rows and columns along with their widths and heights).

library(ggpubr)

P_arrg_1 <- ggarrange(P2 + labs(x = "", y = ""),  # remove the axis labels
                      NULL,  # you can add an empty plot using "NULL" to create a white space in the graphic device
                      P1, 
                      P3 + labs(x = "", y = ""), 
                      nrow = 2,  # split the graphic device into two rows
                      ncol = 2,  # split the graphic device into two columns
                      widths = c(0.65, 0.35),  # the widths of the two columns
                      heights = c(0.35, 0.65),  # the heights of the two rows
                      common.legend = T)  # use the same legend for all plots

P_arrg_1

2. Panel labels

After getting our basic layout done, we can then add panel labels to the plots by specifying their x and y positions for each individual plots using values in the range of 0 to 1 (e.g., (x, y) = (0, 0) means bottom-left corner of the plot; values outside this range are also allowed and it will draw the labels outside the plot area).

P_arrg_2 <- ggarrange(P2 + labs(x = "", y = ""),  
                      NULL,
                      P1, 
                      P3 + labs(x = "", y = ""), 
                      nrow = 2, 
                      ncol = 2,
                      widths = c(0.65, 0.35),  
                      heights = c(0.35, 0.65),  
                      common.legend = T,
                      labels = c("(a)", "", "(b)", "(c)"),  # the panel labels for each plot (also an empty string for the "NULL" plot)
                      label.x = c(0, 0, 0, 0),  # the x positions of the labels
                      label.y = c(1.1, 1.1, 1.1, 1.1))  # the y positions of the labels

P_arrg_2

3. Text annotations

Sometimes we might want to add some text annotations to the figure. ggpubr provides a handy function annotate_figure() for this: simply create a grob object using text_grob() (in which you can specify the x and y positions of the text as well as its horizontal and vertical adjustments) and pass it to annotate_figure()

P_arrg_3 <- annotate_figure(P_arrg_2,
                            top = text_grob("Visualizing Iris Data", 
                                            color = "blue", 
                                            face = "bold.italic", 
                                            size = 14, 
                                            x = 0.825, 
                                            y = 1, 
                                            vjust = 10))

P_arrg_3

4. Plot alignment

Another nice functionality of ggarrange() is that it can automatically align the plot panels by axis (vertically and/or horizontally). This is particularly useful when the plots have different margin sizes (e.g., due to different label lengths). Sounds a bit abstract right? Let’s take a look at an example:

### A scatterplot of Sepal.Length vs. Sepal.Width
P4 <- ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, color = Species)) + 
  geom_point(show.legend = F) + 
  scale_color_brewer(palette = "Set1", name = "") + 
  theme_classic(base_size = 14)

### Density plot of Sepal.Length
P5 <- ggplot(iris, aes(x = Sepal.Length, color = Species)) + 
  geom_density(aes(y = ..scaled..), show.legend = F) + 
  scale_color_brewer(palette = "Set1", name = "") +
  labs(x = NULL, y = NULL) +
  theme_classic(base_size = 14) 

### The y-axes of the two plots are not aligned by default
ggarrange(P5, 
          P4,
          nrow = 2,
          heights = c(0.3, 0.7))

### The y-axes of the two plots are now vertically aligned
ggarrange(P5, 
          P4,
          nrow = 2,
          heights = c(0.3, 0.7),
          align = "v")


Method 2. Using draw_plot() in the package cowplot

The package cowplot provides a family of functions for adding plots/images/text to the graphic device. We will see how to make use of them shortly. Again, let’s first create some example plots:

library(tidyverse)

### 1. A scatterplot of Sepal.Length vs. Sepal.Width
P1 <- ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, color = Species)) + 
  geom_point(show.legend = F) + 
  scale_color_brewer(palette = "Set1", name = "") + 
  theme_classic(base_size = 14)

### 2. Density plot of Sepal.Length
P2 <- ggplot(iris, aes(x = Sepal.Length, color = Species)) + 
  geom_density(aes(y = ..scaled..)) + 
  scale_color_brewer(palette = "Set1", name = "") + 
  theme_classic(base_size = 14) + 
  scale_y_continuous(breaks = seq(0, 1, 0.2))

### 3. Density plot of Sepal.Width
P3 <- ggplot(iris, aes(x = Sepal.Width, color = Species)) + 
  geom_density(aes(y = ..scaled..), show.legend = F) + 
  scale_color_brewer(palette = "Set1", name = "") + 
  theme_classic(base_size = 14) + 
  scale_y_continuous(breaks = seq(0, 1, 0.2)) +
  coord_flip()
  
P1
P2
P3

1. Basic layout

The first method ggarrange works by “cutting” the graphic device into grids of various widths and heights and placing the corresponding plots into those grids.

Here, the second method draw_plot(), is a bit different though. Basically, we will first create a drawing canvas (using the function ggdraw()), which has the x- and y-coordinates of 0 to 1. Next, we use draw_plot() to add the plots to this canvas by specifying their x and y positions using values in the range of 0 to 1 (again, (x, y) = (0, 0) corresponds to the bottom-left corner of the plot) as well as the widths and heights.

library(cowplot)

ggdraw() + 
  draw_plot(P2 + labs(x = "", y = ""), x = 0, y = 0.65, width = 0.925, height = 0.3) + 
  draw_plot(P1, x = 0, y = 0, width = 0.65, height = 0.65) +
  draw_plot(P3 + labs(x = "", y = ""), x = 0.65, y = 0, width = 0.3, height = 0.65)

2. Panel labels

We can add panel labels to the figure using the function draw_text(), with the same principle as draw_plot():

ggdraw() + 
  draw_plot(P2 + labs(x = "", y = ""), x = 0, y = 0.65, width = 0.925, height = 0.3) + 
  draw_plot(P1, x = 0, y = 0, width = 0.65, height = 0.65) +
  draw_plot(P3 + labs(x = "", y = ""), x = 0.65, y = 0, width = 0.3, height = 0.65) + 
  draw_text(c("(a)", "(b)", "(c)"), x = c(0.075, 0.075, 0.725), y = c(0.975, 0.675, 0.675)) + 
  draw_text("Visualizing Iris Data", x = 0.5, y = 0.975, color = "blue", size = 14, fontface = "italic") 

3. Add external images

Another cool thing when working with cowplot is that you can add external images to the graphic device. Use draw_image() to do this (note that you need to have the package magick installed beforehand for this function to work):

# install.packages("magick")
ggdraw() + 
  draw_plot(P2 + labs(x = "", y = ""), x = 0, y = 0.65, width = 0.925, height = 0.3) + 
  draw_plot(P1, x = 0, y = 0, width = 0.65, height = 0.65) +
  draw_plot(P3 + labs(x = "", y = ""), x = 0.65, y = 0, width = 0.3, height = 0.65) + 
  draw_text(c("(a)", "(b)", "(c)"), x = c(0.075, 0.075, 0.725), y = c(0.975, 0.675, 0.675)) + 
  draw_text("Visualizing Iris Data", x = 0.5, y = 0.975, color = "blue", size = 14, fontface = "italic") +
  draw_image("https://archive.ics.uci.edu/ml/assets/MLimages/Large53.jpg", x = 0.75, y = 0.9, width = 0.1, height = 0.1)

4. Insets and legends

We can even do more tricks in the figure, for instance, adding inset plots. Additionally, the package has a function called get_legend(), which allows one to extract the legend from a plot (as a grob object) for further use. See the example below to get an idea of how they work:

### 1. A scatterplot of Sepal.Length vs. Sepal.Width
P1 <- ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, color = Species)) + 
  geom_point(show.legend = F) + 
  scale_color_brewer(palette = "Set1", name = "") + 
  theme_classic(base_size = 14) + 
  scale_x_continuous(limits = c(3, NA)) + 
  scale_y_continuous(limits = c(1, NA))

### 2. Density plot of Sepal.Length
P2 <- ggplot(iris, aes(x = Sepal.Length, color = Species)) + 
  geom_density(aes(y = ..scaled..), show.legend = F) + 
  scale_color_brewer(palette = "Set1", name = "") + 
  theme_classic(base_size = 14) + 
  scale_x_continuous(limits = c(3, NA)) + 
  scale_y_continuous(breaks = seq(0, 1, 0.2))

### 3. Density plot of Sepal.Width
P3 <- ggplot(iris, aes(x = Sepal.Width, color = Species)) + 
  geom_density(aes(y = ..scaled..), show.legend = F) + 
  scale_color_brewer(palette = "Set1", name = "") + 
  theme_classic(base_size = 14) + 
  scale_x_continuous(limits = c(1, NA)) + 
  scale_y_continuous(breaks = seq(0, 1, 0.2)) +
  coord_flip()

### 4. Extract the legend from the scatterplot
P1_legned <- ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, color = Species)) + 
  geom_point() + 
  scale_color_brewer(palette = "Set1", name = "") + 
  theme_classic(base_size = 14) + 
  scale_x_continuous(limits = c(3, NA)) + 
  scale_y_continuous(limits = c(1, NA))

P_legend <- get_legend(P1_legned)

### 5. Inset boxplots 
P5 <- ggplot(iris, aes(x = Species, y = Sepal.Length, color = Species)) + 
  geom_boxplot(show.legend = F, outlier.shape = NA) + 
  scale_y_continuous(limits = c(3, NA)) + 
  scale_color_brewer(palette = "Set1", name = "") + 
  coord_flip() + 
  theme_classic(base_size = 14) + 
  scale_x_discrete(labels = c("1", "2", "3")) + 
  theme(axis.title = element_text(color = "transparent"),
        axis.text = element_text(color = "transparent"),
        axis.line = element_line(color = "transparent"),
        axis.ticks = element_line(color = "transparent"),
        panel.background = element_blank(),
        plot.background = element_blank())
  
P6 <- ggplot(iris, aes(x = Species, y = Sepal.Width, color = Species)) + 
  geom_boxplot(show.legend = F, outlier.shape = NA) + 
  scale_y_continuous(limits = c(1, NA)) + 
  scale_color_brewer(palette = "Set1", name = "") +
  theme_classic(base_size = 14) + 
  scale_x_discrete(labels = c("1", "2", "3")) + 
  theme(axis.title = element_text(color = "transparent"),
        axis.text = element_text(color = "transparent"),
        axis.line = element_line(color = "transparent"),
        axis.ticks = element_line(color = "transparent"),
        panel.background = element_blank(),
        plot.background = element_blank())

### Put everything together
ggdraw() + 
  draw_plot(P2 + labs(x = "", y = ""), x = -0.025, y = 0.65, width = 0.7, height = 0.3) + 
  draw_plot(P1, x = 0, y = 0, width = 0.65, height = 0.65) +
  draw_plot(P3 + labs(x = "", y = ""), x = 0.65, y = 0, width = 0.3, height = 0.65) + 
  draw_text(c("(a)", "(b)", "(c)"), x = c(0.075, 0.075, 0.725), y = c(0.975, 0.675, 0.675)) + 
  draw_text("Visualizing Iris Data", x = 0.5, y = 0.975, color = "blue", size = 14, fontface = "italic") +
  draw_grob(P_legend, x = 0.75, y = 0.75, width = 0.1, height = 0.1) + 
  draw_image("https://archive.ics.uci.edu/ml/assets/MLimages/Large53.jpg", x = 0.75, y = 0.85, width = 0.1, height = 0.1) + 
  draw_plot(P5, x = 0, y = 0.025, width = 0.65, height = 0.2) + 
  draw_plot(P6, x = 0.025, y = 0, width = 0.2, height = 0.65)

Note that you might need to experiment a bit so that the inset boxplots align with the axes of the main panel. A tip here is to use exactly the same axis settings (labels, ticks, ranges, font size, etc.) for the main panel and the insets as well as the same x and y positions in draw_plot() (so basically the insets will overlay the main panel). After making sure that the main panel and the insets are nicely aligned, you can remove those unnecessary theme elements from the inset plot and then re-draw them on the main panel.

Summary

To recap, we’ve walked through two methods to arrange multiple ggplots on one graphic device. The first one, ggarrange() in the package ggpubr, is straightforward and easy-to-use. Just pass the individual plots into the function, tell it your layout designs, and you’re done. The package also offers a function for adding text annotations to the figure. So if you simply just want to combine several ggplots together without complex layout, this is perhaps the go-to method and will save you lots of time.

The second method, draw_plot() and its family functions in the package cowplot, create a drawing canvas that allows you to put any plots/text/images at any places on it. You can also create plot insets as well as extract the legend from the plot to customize its position. This method provides much greater flexibility in terms of plot layout, but at a cost that it might take some time to experiment a bit so that the objects are at the exact positions you want with the desired sizes. This second method is better-suited for complex plot layout, or if you really want to tweak the plot positions to your heart’s content.

Whatever method you use, the most important thing is to think about what your final figure should look like. Maybe sketch it out on a paper first. After getting sort of an idea, you can then proceed to preparing the individual plots, determining their positions and sizes, doing some decorations like adding panel labels and text annotations, and finally fine-tuning the objects before printing it out.

Hope you enjoy the reading and don’t forget to leave your comments and suggestions below if you have any!

Corrections

If you see mistakes or want to suggest changes, please create an issue on the source repository.