From a2fda96c02da1a0b7e5710df028eb59c0a905905 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Sat, 9 Dec 2017 16:36:47 -0600 Subject: [PATCH] Improved robustness of converting plots to grobs and of calling ggsave in various unusual environments. --- R/plot_to_gtable.R | 33 +-- R/save.R | 54 +++- inst/doc/axis_position.html | 330 ++++++++++++++++++----- inst/doc/introduction.R | 13 - inst/doc/introduction.Rmd | 17 -- inst/doc/introduction.html | 465 +++++++++++++++++++++++---------- inst/doc/plot_annotations.html | 356 +++++++++++++++++++------ inst/doc/plot_grid.html | 408 +++++++++++++++++++++-------- inst/doc/shared_legends.html | 440 ++++++++++++++++++++++--------- 9 files changed, 1550 insertions(+), 566 deletions(-) diff --git a/R/plot_to_gtable.R b/R/plot_to_gtable.R index ab69ebc..e81711a 100644 --- a/R/plot_to_gtable.R +++ b/R/plot_to_gtable.R @@ -20,7 +20,12 @@ plot_to_gtable <- function(plot){ gt } else { + # we convert the captured plot or output plot into a grob + # to be safe, we have to save and restore the current graphics device + cur_dev <- dev.cur() tree <- grid::grid.grabExpr(gridGraphics::grid.echo(plot)) + dev.set(cur_dev) + u <- grid::unit(1, "null") gt <- gtable::gtable_col(NULL, list(tree), u, u) # fix gtable clip setting @@ -29,23 +34,21 @@ plot_to_gtable <- function(plot){ } } else if (methods::is(plot, "ggplot")){ - ## ggplotGrob must open a device and when a multiple page capable device (e.g. PDF) is open this will save a blank page - ## in order to avoid saving this blank page to the final target device a NULL device is opened and closed here to *absorb* the blank plot - - # catch_blank <- identical(names(grDevices::dev.cur()), 'pdf') # is the current device `pdf()` - - ## this problem arises even when the current device is not pdf, e.g. in R studio notebooks. Let's try to set - ## catch_blank to TRUE always and see if this causes other issues. - - catch_blank <- TRUE - - if (catch_blank) - grDevices::pdf(NULL) + ## ggplotGrob must open a device and when a multiple page capable device (e.g. PDF) is open it will + ## save a blank page. To avoid this blank page, we open a NULL pdf device that will *absorb* the + ## blank plot. However, as part of this procedure, the wrong device can end up being the current one, + ## so to be absolutely sure, we also save the previous device and then restore it. + # save currently active device + cur_dev <- dev.cur() + # open NULL pdf device + grDevices::pdf(NULL) + # convert plot to grob plot <- ggplot2::ggplotGrob(plot) - - if (catch_blank) - grDevices::dev.off() + # close pdf device + grDevices::dev.off() + # restore previously active device + dev.set(cur_dev) plot } diff --git a/R/save.R b/R/save.R index fd62dbc..3348cfe 100644 --- a/R/save.R +++ b/R/save.R @@ -1,16 +1,9 @@ -# Much of the code in this file was copied verbatim from the ggplot2 source. Ugly, but -# necessary to fix the dingbats issue. - -# ************************************************* -# Output -# ************************************************* - #' Cowplot reimplementation of ggsave. #' #' This function should behave just like \code{ggsave} from ggplot2, #' with the main difference being that by default it doesn't use the Dingbats #' font for pdf output. If you ever have trouble with this function, you can -#' try using \code{ggplot2::ggsave()} instead. +#' use \code{ggplot2::ggsave()} instead. #' @param filename Filename of plot #' @param plot Plot to save, defaults to last plot displayed. #' @param device Device to use, automatically extract from file name extension. @@ -26,7 +19,47 @@ #' specifying dimensions in pixels. #' @param ... Other arguments to be handed to the plot device. #' @export -ggsave <- function(filename, plot = ggplot2::last_plot(), +ggsave <- function(filename, plot = ggplot2::last_plot(), device = NULL, path = NULL, scale = 1, + width = NA, height = NA, units = c("in", "cm", "mm"), dpi = 300, limitsize = TRUE, ...) { + + # arguments we want to hand off to ggplot2::ggsave only if explicitly provided + ggsave_args <- c("plot", "path", "scale", "width", "height", "units", "dpi", "limitsize") + + # match ggsave_args to args provided + args <- as.list(match.call()) + args[[1]] <- NULL # remove the function call + args <- args[na.omit(match(ggsave_args, names(args)))] # remove other args + + args_dotdotdot <- list(...) + + # if device isn't provided, try to infer from filename + if (is.null(device)) { + device <- tolower(tools::file_ext(filename)) + } + + if (identical(device, "pdf") || identical(device, grDevices::pdf)) { + # pdf device specified either by filename extension or by character string + # set useDingbats option unless provided + + if (!"useDingbats" %in% names(args_dotdotdot)) + args_dotdotdot <- append(args_dotdotdot, list(useDingbats = FALSE)) + + } + + # combine all the args + args <- c(list(filename = filename), args, device = device, args_dotdotdot) + + # call ggplot2::ggsave while saving and afterwards restoring the current graphics device + cur_dev <- grDevices::dev.cur() + x <- do.call(ggplot2::ggsave, args, envir = parent.frame()) + grDevices::dev.set(cur_dev) + invisible(x) +} + + +# This was the previous implementation copying much of the ggplot2::ggsave +# code over. This seems unnecessary. +ggsave_old <- function(filename, plot = ggplot2::last_plot(), device = NULL, path = NULL, scale = 1, width = NA, height = NA, units = c("in", "cm", "mm"), dpi = 300, limitsize = TRUE, ...) { @@ -200,6 +233,7 @@ save_plot <- function(filename, plot, ncol = 1, nrow = 1, base_width <- base_height * base_aspect_ratio } - ggsave(filename = filename, plot = plot, width = base_width*cols, height = base_height*rows, ...) + # make clear we're using the cowplot function, not the ggplot2 one + cowplot::ggsave(filename = filename, plot = plot, width = base_width*cols, height = base_height*rows, ...) } diff --git a/inst/doc/axis_position.html b/inst/doc/axis_position.html index 67c6adc..81e53ea 100644 --- a/inst/doc/axis_position.html +++ b/inst/doc/axis_position.html @@ -12,7 +12,7 @@ - + Changing the axis positions @@ -20,46 +20,244 @@ - + @@ -70,44 +268,44 @@

Changing the axis positions

Claus O. Wilke

-

2017-10-20

+

2017-12-09

As of ggplot2 2.2.0, the package now natively supports secondary axes and axes on either side of the plot. Therefore, the cowplot function switch_axis_position() has been deprecated, since it always was an ugly hack and difficult to maintain. Here are some examples of how alternative axes can be created with ggplot2.

-
require(cowplot)
-require(grid) # for unit()
-theme_set(theme_cowplot(font_size=12)) # reduce default font size
-p1 <- ggplot(mtcars, aes(mpg, disp)) + geom_line(colour = "blue")
-
-# the following code only works for ggplot2 2.2.0 or later
-if (packageVersion("ggplot2")>"2.1.0")
-  p1 + theme_gray() + scale_y_continuous(position = "right")
+
require(cowplot)
+
require(grid) # for unit()
+
theme_set(theme_cowplot(font_size=12)) # reduce default font size
+
p1 <- ggplot(mtcars, aes(mpg, disp)) + geom_line(colour = "blue")
+
+
# the following code only works for ggplot2 2.2.0 or later
+
if (packageVersion("ggplot2")>"2.1.0")
+
p1 + theme_gray() + scale_y_continuous(position = "right")

-
if (packageVersion("ggplot2")>"2.1.0")
-  p1 + theme_bw() + scale_x_continuous(sec.axis = dup_axis())
+
if (packageVersion("ggplot2")>"2.1.0")
+
p1 + theme_bw() + scale_x_continuous(sec.axis = dup_axis())

-
if (packageVersion("ggplot2")>"2.1.0")
-  p1 + scale_x_continuous(sec.axis = dup_axis()) + scale_y_continuous(sec.axis = dup_axis())
+
if (packageVersion("ggplot2")>"2.1.0")
+
p1 + scale_x_continuous(sec.axis = dup_axis()) + scale_y_continuous(sec.axis = dup_axis())

-
if (packageVersion("ggplot2")>"2.1.0")
-  p1 + theme(axis.ticks.length = unit(0.3, "cm"),
-           axis.text.x = element_text(margin = margin(0.2, unit = "cm"))) +
-     scale_x_continuous(sec.axis = dup_axis()) + scale_y_continuous(sec.axis = dup_axis())
+
if (packageVersion("ggplot2")>"2.1.0")
+
p1 + theme(axis.ticks.length = unit(0.3, "cm"),
+
axis.text.x = element_text(margin = margin(0.2, unit = "cm"))) +
+
scale_x_continuous(sec.axis = dup_axis()) + scale_y_continuous(sec.axis = dup_axis())

It is also possible to use alternate discrete axes:

-
mtcars2 <- mtcars[1:15, ]
-mtcars2$name <- row.names(mtcars2)
-
-# the following code only works for ggplot2 2.2.0 or later
-if (packageVersion("ggplot2")>"2.1.0"){
-ggplot(mtcars2, aes(x = name, y = mpg, fill = name)) + 
-  geom_bar(stat = 'identity', position = "identity") + 
-  scale_y_reverse() +
-  guides(fill = FALSE) +
-  theme(axis.text.x.top = element_text(angle = 90, vjust=0.5, hjust=0)) +
-  scale_x_discrete(position = "top")
-}
+
mtcars2 <- mtcars[1:15, ]
+
mtcars2$name <- row.names(mtcars2)
+
+
# the following code only works for ggplot2 2.2.0 or later
+
if (packageVersion("ggplot2")>"2.1.0"){
+
ggplot(mtcars2, aes(x = name, y = mpg, fill = name)) +
+
geom_bar(stat = 'identity', position = "identity") +
+
scale_y_reverse() +
+
guides(fill = FALSE) +
+
theme(axis.text.x.top = element_text(angle = 90, vjust=0.5, hjust=0)) +
+
scale_x_discrete(position = "top")
+
}

diff --git a/inst/doc/introduction.R b/inst/doc/introduction.R index 2109eae..7859c05 100644 --- a/inst/doc/introduction.R +++ b/inst/doc/introduction.R @@ -139,19 +139,6 @@ ggdraw() + draw_image("http://jeroen.github.io/images/tiger.svg") + draw_plot(p) -## ----message=FALSE, results="hide"--------------------------------------- -img <- magick::image_read("http://jeroen.github.io/images/tiger.svg") -img <- magick::image_transparent(img, color = "white") -img2 <- magick::image_charcoal(img) -img2 <- magick::image_transparent(img2, color = "white") - -## ----message=FALSE------------------------------------------------------- -ggplot(data.frame(x=1:3, y=1:3), aes(x, y)) + - geom_point(size = 3) + - geom_abline(slope = 1, intercept = 0, linetype = 2, color = "blue") + - draw_image(img, x=1, y=1, scale = .9) + - draw_image(img2, x=2, y=2, scale = .9) - ## ----message=FALSE, fig.width=7, fig.height=2.5-------------------------- p <- ggplot(iris, aes(x = Sepal.Length, fill = Species)) + geom_density(alpha = 0.7) p2 <- ggdraw() + draw_image("http://jeroen.github.io/images/tiger.svg", scale = 0.9) diff --git a/inst/doc/introduction.Rmd b/inst/doc/introduction.Rmd index ed6e647..b3d4659 100644 --- a/inst/doc/introduction.Rmd +++ b/inst/doc/introduction.Rmd @@ -221,23 +221,6 @@ ggdraw() + draw_plot(p) ``` -Or we can manipulate images and draw them in plot coordinates: - -```{r message=FALSE, results="hide"} -img <- magick::image_read("http://jeroen.github.io/images/tiger.svg") -img <- magick::image_transparent(img, color = "white") -img2 <- magick::image_charcoal(img) -img2 <- magick::image_transparent(img2, color = "white") -``` - -```{r message=FALSE} -ggplot(data.frame(x=1:3, y=1:3), aes(x, y)) + - geom_point(size = 3) + - geom_abline(slope = 1, intercept = 0, linetype = 2, color = "blue") + - draw_image(img, x=1, y=1, scale = .9) + - draw_image(img2, x=2, y=2, scale = .9) -``` - We can also make a plot grid with a regular plot and an image: ```{r message=FALSE, fig.width=7, fig.height=2.5} diff --git a/inst/doc/introduction.html b/inst/doc/introduction.html index df23a6e..f3a1e3c 100644 --- a/inst/doc/introduction.html +++ b/inst/doc/introduction.html @@ -12,7 +12,7 @@ - + Introduction to cowplot @@ -20,46 +20,244 @@ - + @@ -70,147 +268,134 @@

Introduction to cowplot

Claus O. Wilke

-

2017-10-20

+

2017-12-09

The cowplot package is a simple add-on to ggplot2. It is meant to provide a publication-ready theme for ggplot2, one that requires a minimum amount of fiddling with sizes of axis labels, plot backgrounds, etc. Its primary purpose is to give my students and postdocs an easy way to make figures that I will approve of. Thus, this package meets my personal needs and tastes. Yours may be different.

In addition to providing a modified plot theme, this package also offers functionality for custom annotations to ggplot2 plots. It turns out that the easiest way to offer this functionality was to implement a general-purpose drawing canvas on top of ggplot2. As a result, you can achieve quite unusual effects with this package (see more below).

The cowplot source code is available on github: https://github.com/wilkelab/cowplot

-
+

Plot design

I don’t find the default ggplot2 design particularly elegant. In particular, I don’t like the gray background grid. I feel it often distracts from the data. For example, see this ggplot2 visualization of the mpg data set:

-
library(ggplot2)
-ggplot(mpg, aes(x = cty, y = hwy, colour = factor(cyl))) + 
-   geom_point(size = 2.5)
+
library(ggplot2)
+
ggplot(mpg, aes(x = cty, y = hwy, colour = factor(cyl))) +
+
geom_point(size = 2.5)

I prefer a clean and sparse layout for publication. I also prefer the approach of building a graph by adding elements rather than taking them away. Therefore, the default design of cowplot has no grid at all. It looks similar to ggplot2’s theme_classic(), but there are a few important but subtle differences, mostly with respect to font sizes.

-
library(cowplot)
-ggplot(mpg, aes(x = cty, y = hwy, colour = factor(cyl))) + 
-   geom_point(size = 2.5)
+
library(cowplot)
+
ggplot(mpg, aes(x = cty, y = hwy, colour = factor(cyl))) +
+
geom_point(size = 2.5)

In particular, the cowplot default theme works nicely in conjunction with the save_plot() function the package provides, such that the output pdfs are nicely formatted and scaled and don’t require any additional parameter fiddling:

-
library(cowplot)
-plot.mpg <- ggplot(mpg, aes(x = cty, y = hwy, colour = factor(cyl))) + 
-  geom_point(size=2.5)
-# use save_plot() instead of ggsave() when using cowplot
-save_plot("mpg.png", plot.mpg,
-          base_aspect_ratio = 1.3 # make room for figure legend
-)
-

+
library(cowplot)
+
plot.mpg <- ggplot(mpg, aes(x = cty, y = hwy, colour = factor(cyl))) +
+
geom_point(size=2.5)
+
# use save_plot() instead of ggsave() when using cowplot
+
save_plot("mpg.png", plot.mpg,
+
base_aspect_ratio = 1.3 # make room for figure legend
+
)

The resulting figure:

By default, cowplot disables grid lines on the plot. In many cases, this is the cleanest and most elegant way to display the data. However, sometimes gridlines may be useful, and thus cowplot provides a simple way of adding gridlines, via the function background_grid():

-
plot.mpg + background_grid(major = "xy", minor = "none")
+
plot.mpg + background_grid(major = "xy", minor = "none")

While the same result could be obtained using the function theme(), the function background_grid() makes the most commonly used option easily accessible. See the reference documentation for details.

Note that if you ever want to use the default ggplot2 theme while using the cowplot package, simply add theme_gray() to your plot or call theme_set(theme_gray()) to set this theme for all subsequent plots:

-
plot.mpg + theme_gray() # create plot with default ggplot2 theme
-theme_set(theme_gray()) # switch to default ggplot2 theme for good
-
-
+
plot.mpg + theme_gray() # create plot with default ggplot2 theme
+
theme_set(theme_gray()) # switch to default ggplot2 theme for good
+ +

Arranging graphs into a grid

One limitation of ggplot2 is that it doesn’t make it easy to add labels and other annotations to a plot. ggplot2 strictly separates the plot panel (the part inside the axes) from the rest of the plot, and while it’s generally straightforward to modify one or the other we cannot easily change both. To solve this issue in a generic way, cowplot implements a generic drawing layer on top of ggplot2. In this drawing layer, you can add arbitrary graphical elements on top of a graph. This concept will be discussed in detail in the next section. For now, let’s discuss how this capability allows us to make nice compound plots.

The need for compound plots arises frequently when writing scientific publications. For example, let’s assume we have created the following two plots, and we would like to combine them into a two-part figure with parts A and B. In our case, the plots are the following:

-
plot.mpg <- ggplot(mpg, aes(x = cty, y = hwy, colour = factor(cyl))) + 
-  geom_point(size=2.5)
-plot.mpg
+
plot.mpg <- ggplot(mpg, aes(x = cty, y = hwy, colour = factor(cyl))) +
+
geom_point(size=2.5)
+
plot.mpg

-
plot.diamonds <- ggplot(diamonds, aes(clarity, fill = cut)) + geom_bar() +
-  theme(axis.text.x = element_text(angle=70, vjust=0.5))
-plot.diamonds
+
plot.diamonds <- ggplot(diamonds, aes(clarity, fill = cut)) + geom_bar() +
+
theme(axis.text.x = element_text(angle=70, vjust=0.5))
+
plot.diamonds

cowplot allows us to combine them into one graph via the function plot_grid():

-
plot_grid(plot.mpg, plot.diamonds, labels = c("A", "B"))
-

+
plot_grid(plot.mpg, plot.diamonds, labels = c("A", "B"))
+

By default, the plots are simply placed into the grid as they are, and so the axes are not aligned. If axis alignment is required, it can be switched on via the align option:

-
plot_grid(plot.mpg, plot.diamonds, labels = c("A", "B"), align = "h")
-

+
plot_grid(plot.mpg, plot.diamonds, labels = c("A", "B"), align = "h")
+

The function plot_grid() will attempt to achieve a reasonable layout of the plots provided. However, you can precisely manipulate the layout by specifying the number of rows or columns or both:

-
plot_grid(plot.mpg, NULL, NULL, plot.diamonds, labels = c("A", "B", "C", "D"), ncol = 2)
-

-
plot_grid(plot.mpg, plot.diamonds, labels = c("A", "B"), nrow = 2, align = "v")
-

+
plot_grid(plot.mpg, NULL, NULL, plot.diamonds, labels = c("A", "B", "C", "D"), ncol = 2)
+

+
plot_grid(plot.mpg, plot.diamonds, labels = c("A", "B"), nrow = 2, align = "v")
+

The function plot_grid() works nicely in combination with the function save_plot(), which can be told about the grid layout. For example, if we want to save a 2-by-2 figure, we might use this code:

-
plot2by2 <- plot_grid(plot.mpg, NULL, NULL, plot.diamonds,
-                      labels=c("A", "B", "C", "D"), ncol = 2)
-save_plot("plot2by2.png", plot2by2,
-          ncol = 2, # we're saving a grid plot of 2 columns
-          nrow = 2, # and 2 rows
-          # each individual subplot should have an aspect ratio of 1.3
-          base_aspect_ratio = 1.3
-          )
-

-

The resulting figure:

+
plot2by2 <- plot_grid(plot.mpg, NULL, NULL, plot.diamonds,
+
labels=c("A", "B", "C", "D"), ncol = 2)
+
save_plot("plot2by2.png", plot2by2,
+
ncol = 2, # we're saving a grid plot of 2 columns
+
nrow = 2, # and 2 rows
+
# each individual subplot should have an aspect ratio of 1.3
+
base_aspect_ratio = 1.3
+
)
+

The resulting figure:

The advantage of saving figures in this way is that you can first develop the code for individual figures, and once each individual figure looks the way you want it to you can easily combine the figures into a grid. save_plot() will make sure to scale the overall figure size such that the individual figures look the way they do when saved individually (as long as they all have the same base_aspect_ratio).

-
-
+ +

Generic plot annotations

Finally, let’s discuss how we can use cowplot to create more unusual plot designs. For example, let’s take the mpg image from the previous section, label it with an A in the top-left corner, and mark it as a draft:

-
ggdraw(plot.mpg) + 
-  draw_plot_label("A", size = 14) + 
-  draw_label("DRAFT!", angle = 45, size = 80, alpha = .2)
-

+
ggdraw(plot.mpg) +
+
draw_plot_label("A", size = 14) +
+
draw_label("DRAFT!", angle = 45, size = 80, alpha = .2)
+

The function ggdraw() sets up the drawing layer, and functions that are meant to operate on this drawing layer all start with draw_. The resulting object is again a standard ggplot2 object, and you can do with it whatever you might do with a regular ggplot2 plot, such as save it with ggsave(). [However, as mentioned before, I recommend using save_plot() instead.]

In fact, because ggdraw() produces a standard ggplot2 object, we can draw on it with standard geoms if we want to. For example:

-
t <- (0:1000)/1000
-spiral <- data.frame(x = .45+.55*t*cos(t*15), y = .55-.55*t*sin(t*15), t)
-ggdraw(plot.mpg) + 
-  geom_path(data = spiral, aes(x = x, y = y, colour = t), size = 6, alpha = .4)
-

+
t <- (0:1000)/1000
+
spiral <- data.frame(x = .45+.55*t*cos(t*15), y = .55-.55*t*sin(t*15), t)
+
ggdraw(plot.mpg) +
+
geom_path(data = spiral, aes(x = x, y = y, colour = t), size = 6, alpha = .4)
+

I don’t know if this is useful in any way, but it shows the power of the approach.

Importantly, though, in all cases discussed so far, the main plot was below all other elements. Sometimes, you might want the plot on top. In this case, you can initialize an empty drawing canvas by calling ggdraw() without any parameters. You then place the plot by calling draw_plot(). Notice the difference in the two plots produced by the following code:

-
boxes <- data.frame(
-  x = sample((0:33)/40, 40, replace = TRUE),
-  y = sample((0:33)/40, 40, replace = TRUE)
-)
-# plot on top of annotations
-ggdraw() + 
-  geom_rect(data = boxes, aes(xmin = x, xmax = x + .15, ymin = y, ymax = y + .15),
-            colour = "gray60", fill = "gray80") +
-  draw_plot(plot.mpg) +
-  draw_label("Plot is on top of the grey boxes", x = 1, y = 1,
-            vjust = 1, hjust = 1, size = 10, fontface = 'bold')
-# plot below annotations
-ggdraw(plot.mpg) + 
-  geom_rect(data = boxes, aes(xmin = x, xmax = x + .15, ymin = y, ymax = y + .15),
-            colour = "gray60", fill = "gray80") + 
-  draw_label("Plot is underneath the grey boxes", x = 1, y = 1,
-            vjust = 1, hjust = 1, size = 10, fontface = 'bold')
-

+
boxes <- data.frame(
+
x = sample((0:33)/40, 40, replace = TRUE),
+
y = sample((0:33)/40, 40, replace = TRUE)
+
)
+
# plot on top of annotations
+
ggdraw() +
+
geom_rect(data = boxes, aes(xmin = x, xmax = x + .15, ymin = y, ymax = y + .15),
+
colour = "gray60", fill = "gray80") +
+
draw_plot(plot.mpg) +
+
draw_label("Plot is on top of the grey boxes", x = 1, y = 1,
+
vjust = 1, hjust = 1, size = 10, fontface = 'bold')
+
# plot below annotations
+
ggdraw(plot.mpg) +
+
geom_rect(data = boxes, aes(xmin = x, xmax = x + .15, ymin = y, ymax = y + .15),
+
colour = "gray60", fill = "gray80") +
+
draw_label("Plot is underneath the grey boxes", x = 1, y = 1,
+
vjust = 1, hjust = 1, size = 10, fontface = 'bold')
+

Note that placing a plot on top of annotations only makes sense if the plot background is transparent. This is one of the main differences between theme_cowplot() and theme_classic(). If you tried the same example with theme_classic(), the gray boxes underneath the plot would not show.

The draw_plot() function also allows us to place graphs at arbitrary locations and at arbitrary sizes onto the canvas. This is useful for combining subplots into a layout that is not a simple grid, e.g. with an inset plotted inside a larger graph.

-
# plot.mpg and plot.diamonds were defined earlier
-library(viridis)
-ggdraw() +
-  draw_plot(plot.diamonds + theme(legend.justification = "bottom"), 0, 0, 1, 1) +
-  draw_plot(plot.mpg + scale_color_viridis(discrete = TRUE) + 
-              theme(legend.justification = "top"), 0.5, 0.52, 0.5, 0.4) +
-  draw_plot_label(c("A", "B"), c(0, 0.5), c(1, 0.92), size = 15)
-

+
# plot.mpg and plot.diamonds were defined earlier
+
library(viridis)
+
ggdraw() +
+
draw_plot(plot.diamonds + theme(legend.justification = "bottom"), 0, 0, 1, 1) +
+
draw_plot(plot.mpg + scale_color_viridis(discrete = TRUE) +
+
theme(legend.justification = "top"), 0.5, 0.52, 0.5, 0.4) +
+
draw_plot_label(c("A", "B"), c(0, 0.5), c(1, 0.92), size = 15)
+

We can also combine plots and images, using the function draw_image(). This function, which requires the magick package to be installed, can take images in many different formats and combine them with ggplot2 plots. For example, we can use an image as a plot background:

-
p <- ggplot(iris, aes(x=Sepal.Length, fill=Species)) + geom_density(alpha = 0.7)
-ggdraw() +
-  draw_image("http://jeroen.github.io/images/tiger.svg") +
-  draw_plot(p)
-

-

Or we can manipulate images and draw them in plot coordinates:

-
img <- magick::image_read("http://jeroen.github.io/images/tiger.svg")
-img <- magick::image_transparent(img, color = "white")
-img2 <- magick::image_charcoal(img)
-img2 <- magick::image_transparent(img2, color = "white")
-
ggplot(data.frame(x=1:3, y=1:3), aes(x, y)) +
-  geom_point(size = 3) +
-  geom_abline(slope = 1, intercept = 0, linetype = 2, color = "blue") +
-  draw_image(img, x=1, y=1, scale = .9) +
-  draw_image(img2, x=2, y=2, scale = .9)
-

+
p <- ggplot(iris, aes(x=Sepal.Length, fill=Species)) + geom_density(alpha = 0.7)
+
ggdraw() +
+
draw_image("http://jeroen.github.io/images/tiger.svg") +
+
draw_plot(p)
+

We can also make a plot grid with a regular plot and an image:

-
p <- ggplot(iris, aes(x = Sepal.Length, fill = Species)) + geom_density(alpha = 0.7)
-p2 <- ggdraw() + draw_image("http://jeroen.github.io/images/tiger.svg", scale = 0.9)
-plot_grid(p, p2, labels = "AUTO")
-

-
+
p <- ggplot(iris, aes(x = Sepal.Length, fill = Species)) + geom_density(alpha = 0.7)
+
p2 <- ggdraw() + draw_image("http://jeroen.github.io/images/tiger.svg", scale = 0.9)
+
plot_grid(p, p2, labels = "AUTO")
+

+ diff --git a/inst/doc/plot_annotations.html b/inst/doc/plot_annotations.html index 5bdd50f..95f7556 100644 --- a/inst/doc/plot_annotations.html +++ b/inst/doc/plot_annotations.html @@ -12,7 +12,7 @@ - + Plot annotations @@ -20,46 +20,244 @@ - + @@ -70,68 +268,68 @@

Plot annotations

Claus O. Wilke

-

2017-10-20

+

2017-12-09

The cowplot package provides a variety of functions to annotate plots, including annotations underneath the plot, mathematical expressions inside plots, and joint titles for combined plots. Note that as ggplot2 gains more of this functionality natively (e.g. as of version 2.2.0), we recommend to use the ggplot2 methods rather than the cowplot methods. Any functionality that is duplicated between ggplot2 and cowplot may be removed from future versions of cowplot.

-
+

Annotations with mathematical expressions

We commonly want to annotate plots with mathematical expressions, for example when we want to show the result from a statistical test inside the plot. For this purpose, cowplot defines the function draw_label(), which can add arbitrary test or mathematical expressions to a plot.

Depending on the application, we may want to specify the location of the label either in absolute coordinates (independently of the data plotted) or in coordinates relative to the data shown. Both uses are supported by draw_label(). For the former, we use draw_label() in conjunction with ggdraw():

-
c <- cor.test(mtcars$mpg, mtcars$disp, method='sp')
-label <- substitute(paste("Spearman ", rho, " = ", estimate, ", P = ", pvalue),
-                    list(estimate = signif(c$estimate, 2), pvalue = signif(c$p.value, 2)))
-# adding label via ggdraw, in the ggdraw coordinates
-ggdraw(p1) + draw_label(label, .7, .9)
-

+
c <- cor.test(mtcars$mpg, mtcars$disp, method='sp')
+
label <- substitute(paste("Spearman ", rho, " = ", estimate, ", P = ", pvalue),
+
list(estimate = signif(c$estimate, 2), pvalue = signif(c$p.value, 2)))
+
# adding label via ggdraw, in the ggdraw coordinates
+
ggdraw(p1) + draw_label(label, .7, .9)
+

For the latter, we add draw_label() directly to the plot:

-
# adding label directly to plot, in the data coordinates
-p1 + draw_label(label, 20, 400, hjust = 0, vjust = 0)
+
# adding label directly to plot, in the data coordinates
+
p1 + draw_label(label, 20, 400, hjust = 0, vjust = 0)

-
-
+ +

Joint plot titles

When we combine plots with plot_grid(), we may want to add a title that spans the entire combined figure. While there is no specific function in cowplot to achieve this effect, it can be simulated easily with a few lines of code:

-
# make a plot grid consisting of two panels
-p1 <- ggplot(mtcars, aes(x=disp, y=mpg)) + geom_point(colour = "blue") + background_grid(minor='none')
-p2 <- ggplot(mtcars, aes(x=hp, y=mpg)) + geom_point(colour = "green") + background_grid(minor='none')
-p <- plot_grid(p1, p2, labels=c('A', 'B'))
-# now add the title
-title <- ggdraw() + draw_label("MPG declines with displacement and horsepower", fontface='bold')
-plot_grid(title, p, ncol=1, rel_heights=c(0.1, 1)) # rel_heights values control title margins
-

In the final plot_grid line, the values of rel_heights need to be chosen appropriately so that the margins around the title look correct. With the values chosen here, the title takes up 9% (i.e., 0.1/1.1) of the total plot height.

-
-
+
# make a plot grid consisting of two panels
+
p1 <- ggplot(mtcars, aes(x=disp, y=mpg)) + geom_point(colour = "blue") + background_grid(minor='none')
+
p2 <- ggplot(mtcars, aes(x=hp, y=mpg)) + geom_point(colour = "green") + background_grid(minor='none')
+
p <- plot_grid(p1, p2, labels=c('A', 'B'))
+
# now add the title
+
title <- ggdraw() + draw_label("MPG declines with displacement and horsepower", fontface='bold')
+
plot_grid(title, p, ncol=1, rel_heights=c(0.1, 1)) # rel_heights values control title margins
+

In the final plot_grid line, the values of rel_heights need to be chosen appropriately so that the margins around the title look correct. With the values chosen here, the title takes up 9% (i.e., 0.1/1.1) of the total plot height.

+ +

Annotations underneath plots

The function add_sub() can be used to add annotation text underneath a plot. This functionality is now mostly superseded by the caption argument to the labs function in base ggplot2, but add_sub() is retained in cowplot for backwards compatibility. For new plots, I suggest you try caption first and use add_sub() only when caption doesn’t provide the desired result.

To demonstrate how add_sub() is used, we first make a plot:

-
p1 <- ggplot(mtcars, aes(mpg, disp)) + geom_line(colour = "blue") + background_grid(minor='none')
-p1
+
p1 <- ggplot(mtcars, aes(mpg, disp)) + geom_line(colour = "blue") + background_grid(minor='none')
+
p1

Now we add an annotation underneath:

-
p2 <- add_sub(p1, "This is an annotation.\nAnnotations can span multiple lines.")
-ggdraw(p2)
-

+
p2 <- add_sub(p1, "This is an annotation.\nAnnotations can span multiple lines.")
+
ggdraw(p2)
+

Note that p2 is not a ggplot object but a gtable. It needs to be drawn with ggdraw().

We can also do this repeatedly, and we can use mathematical expressions instead of plain text.

-
p2 <- add_sub(p1, "This formula has no relevance here:", y  = 0, vjust = 0)
-p3 <- add_sub(p2, expression(paste(a^2+b^2, " = ", c^2)), size=12)
-ggdraw(p3)
-

+
p2 <- add_sub(p1, "This formula has no relevance here:", y = 0, vjust = 0)
+
p3 <- add_sub(p2, expression(paste(a^2+b^2, " = ", c^2)), size=12)
+
ggdraw(p3)
+

This code also works with faceted plots:

-
plot.iris <- ggplot(iris, aes(Sepal.Length, Sepal.Width)) +
-  geom_point() + facet_grid(. ~ Species) + stat_smooth(method = "lm") +
-  background_grid(major = 'y', minor = "none") + # add thin horizontal lines
-  panel_border() # and a border around each panel
-p2 <- add_sub(plot.iris, "Annotation underneath a faceted plot, left justified.", x = 0, hjust = 0)
-ggdraw(p2)
-

+
plot.iris <- ggplot(iris, aes(Sepal.Length, Sepal.Width)) +
+
geom_point() + facet_grid(. ~ Species) + stat_smooth(method = "lm") +
+
background_grid(major = 'y', minor = "none") + # add thin horizontal lines
+
panel_border() # and a border around each panel
+
p2 <- add_sub(plot.iris, "Annotation underneath a faceted plot, left justified.", x = 0, hjust = 0)
+
ggdraw(p2)
+

Finally, it is possible to move the annotation inside of the plot if desired. Note that the coordinate x is measured relative to the left border of the plot panel but the coordinate y is measured relative to the space that has been added underneath the plot. Neither x nor y are measured in the units of the data plotted. This guarantees that the annotation can be placed in the same location in different plots, regardless of the data shown.

-
ggdraw(add_sub(p1, "Annotation inside plot", vpadding=grid::unit(0, "lines"),
-       y = 6, x = 0.03, hjust = 0))
-

-
+
ggdraw(add_sub(p1, "Annotation inside plot", vpadding=grid::unit(0, "lines"),
+
y = 6, x = 0.03, hjust = 0))
+

+ diff --git a/inst/doc/plot_grid.html b/inst/doc/plot_grid.html index 6bf75ca..e88ca19 100644 --- a/inst/doc/plot_grid.html +++ b/inst/doc/plot_grid.html @@ -12,7 +12,7 @@ - + Arranging plots in a grid @@ -20,46 +20,244 @@ - + @@ -70,100 +268,100 @@

Arranging plots in a grid

Claus O. Wilke

-

2017-10-20

+

2017-12-09

-
+

Basic use of plot_grid()

The plot_grid() function provides a simple interface for arranging plots into a grid and adding labels to them:

-
require(cowplot)
-theme_set(theme_cowplot(font_size=12)) # reduce default font size
-plot.mpg <- ggplot(mpg, aes(x = cty, y = hwy, colour = factor(cyl))) + 
-  geom_point(size=2.5)
-plot.diamonds <- ggplot(diamonds, aes(clarity, fill = cut)) + geom_bar() +
-  theme(axis.text.x = element_text(angle=70, vjust=0.5))
-plot_grid(plot.mpg, plot.diamonds, labels = c('A', 'B'))
-

+
require(cowplot)
+
theme_set(theme_cowplot(font_size=12)) # reduce default font size
+
plot.mpg <- ggplot(mpg, aes(x = cty, y = hwy, colour = factor(cyl))) +
+
geom_point(size=2.5)
+
plot.diamonds <- ggplot(diamonds, aes(clarity, fill = cut)) + geom_bar() +
+
theme(axis.text.x = element_text(angle=70, vjust=0.5))
+
plot_grid(plot.mpg, plot.diamonds, labels = c('A', 'B'))
+

If you specify the labels as labels="AUTO" or labels="auto" then labels will be auto-generated in upper or lower case, respectively:

-
plot_grid(plot.mpg, plot.diamonds, labels = "AUTO")
-

-
plot_grid(plot.mpg, plot.diamonds, labels = "auto")
-

+
plot_grid(plot.mpg, plot.diamonds, labels = "AUTO")
+

+
plot_grid(plot.mpg, plot.diamonds, labels = "auto")
+

By default, the plots are not aligned, but in many cases they can be aligned via the align option:

-
plot_grid(plot.mpg, plot.diamonds, labels = "AUTO", align = 'h')
-

-
plot_grid(plot.mpg, plot.diamonds, labels = "AUTO", ncol = 1, align = 'v')
-

+
plot_grid(plot.mpg, plot.diamonds, labels = "AUTO", align = 'h')
+

+
plot_grid(plot.mpg, plot.diamonds, labels = "AUTO", ncol = 1, align = 'v')
+

For more complex combinations of plots that don’t have the same number of visual elements, alignment becomes more tricky. In those cases, you also have to specify the margin along which you want to align, via the axis option. For example, to align a faceted and a non-faceted plot such that the left axes are aligned, we can use the following:

-
plot.iris <- ggplot(iris, aes(Sepal.Length, Sepal.Width)) + 
-  geom_point() + facet_grid(. ~ Species) + stat_smooth(method = "lm") +
-  background_grid(major = 'y', minor = "none") + # add thin horizontal lines 
-  panel_border() # and a border around each panel
-plot_grid(plot.iris, plot.mpg, labels = "AUTO", ncol = 1, 
-          align = 'v', axis = 'l') # aligning vertically along the left axis
-

-
-
+
plot.iris <- ggplot(iris, aes(Sepal.Length, Sepal.Width)) +
+
geom_point() + facet_grid(. ~ Species) + stat_smooth(method = "lm") +
+
background_grid(major = 'y', minor = "none") + # add thin horizontal lines
+
panel_border() # and a border around each panel
+
plot_grid(plot.iris, plot.mpg, labels = "AUTO", ncol = 1,
+
align = 'v', axis = 'l') # aligning vertically along the left axis
+

+ +

Supported plot formats

The function plot_grid() can handle several different plot formats, including the classes ggplot (created by the ggplot function), gtable (created from ggplot or from other grid graphics objects such as grob and gTree objects) and, if the package gridGraphics is installed, recordedplot (returned by recordPlot()). Since the code to create a recordedPlot object would ordinarily create an unwanted plot or file, a function creating a graphics based plot (plot, image, …) can be passed as well.

For example, the following creates a recordedPlot object by recording a previous plot (plot(sqrt)):

-
par(xpd = NA, # switch off clipping, necessary to always see axis labels
-    bg = "transparent", # switch off background to avoid obscuring adjacent plots
-    oma = c(2, 2, 0, 0), # move plot to the right and up
-    mgp = c(2, 1, 0) # move axis labels closer to axis
-  ) 
-plot(sqrt) # plot the square root function
+
par(xpd = NA, # switch off clipping, necessary to always see axis labels
+
bg = "transparent", # switch off background to avoid obscuring adjacent plots
+
oma = c(2, 2, 0, 0), # move plot to the right and up
+
mgp = c(2, 1, 0) # move axis labels closer to axis
+
)
+
plot(sqrt) # plot the square root function

-
recordedplot <- recordPlot() # record the previous plot
+
recordedplot <- recordPlot() # record the previous plot

Next we define a function that creates a plot:

-
plotfunc <- function() image(volcano) # define the function
-plotfunc() # call the function to make the plot
+
plotfunc <- function() image(volcano) # define the function
+
plotfunc() # call the function to make the plot

And finally a plot (a circle) drawn directly with grid:

-
gcircle <- grid::circleGrob()
-ggdraw(gcircle)
+
gcircle <- grid::circleGrob()
+
ggdraw(gcircle)

Now we combine all these plots with plot_grid():

-
plot_grid(plot.mpg, recordedplot, plotfunc, gcircle, labels = "AUTO", hjust = 0, vjust = 1,
-          scale = c(1., 1., 0.9, 0.9))
-

Note that the various alignment functions of plot_grid() only work with plots generated by ggplot, not with any of the other supported plot types.

-
-
+
plot_grid(plot.mpg, recordedplot, plotfunc, gcircle, labels = "AUTO", hjust = 0, vjust = 1,
+
scale = c(1., 1., 0.9, 0.9))
+

Note that the various alignment functions of plot_grid() only work with plots generated by ggplot, not with any of the other supported plot types.

+ +

Fine-tuning the plot appearance

You can adjust the label size via the label_size option. Default is 14, so larger values will make the labels larger and smaller values will make them smaller:

-
plot_grid(plot.mpg, plot.diamonds, labels = "AUTO", align = 'h', label_size = 12)
-

+
plot_grid(plot.mpg, plot.diamonds, labels = "AUTO", align = 'h', label_size = 12)
+

You can also adjust the font family, font face, and color of the labels:

-
plot_grid(plot.mpg, plot.diamonds, labels = "AUTO", align = 'h', label_fontfamily = "serif",
-          label_fontface = "plain", label_colour = "blue")
-

+
plot_grid(plot.mpg, plot.diamonds, labels = "AUTO", align = 'h', label_fontfamily = "serif",
+
label_fontface = "plain", label_colour = "blue")
+

Labels can be moved via the label_x and label_y arguments, and justified via the hjust and vjust arguments. For example, to place labels into the bottom left corner, you can write:

-
plot_grid(plot.mpg, plot.diamonds, labels = "AUTO", align = 'h', label_size = 12,
-          label_x = 0, label_y = 0, hjust = -0.5, vjust = -0.5 )
-

+
plot_grid(plot.mpg, plot.diamonds, labels = "AUTO", align = 'h', label_size = 12,
+
label_x = 0, label_y = 0, hjust = -0.5, vjust = -0.5 )
+

It is possible to adjust individual labels one by one by passing vectors of adjustment values to the options label_x, label_y, hjust, and vjust (example not shown).

You can also adjust the relative widths and heights of rows and columns:

-
plot_grid(plot.mpg, plot.diamonds, labels = "AUTO", align = 'h', rel_widths = c(1, 1.3))
-

-
-
+
plot_grid(plot.mpg, plot.diamonds, labels = "AUTO", align = 'h', rel_widths = c(1, 1.3))
+

+ +

Nested plot grids

If you want to generate a plot arrangement that is not a simple grid, you may insert one plot_grid() plot into another:

-
bottom_row <- plot_grid(plot.mpg, plot.diamonds, labels = c('B', 'C'), align = 'h', rel_widths = c(1, 1.3))
-plot_grid(plot.iris, bottom_row, labels = c('A', ''), ncol = 1, rel_heights = c(1, 1.2))
-

+
bottom_row <- plot_grid(plot.mpg, plot.diamonds, labels = c('B', 'C'), align = 'h', rel_widths = c(1, 1.3))
+
plot_grid(plot.iris, bottom_row, labels = c('A', ''), ncol = 1, rel_heights = c(1, 1.2))
+

(Notice how we used rel_heights to make to bottom row higher than the top row. Also, we can’t auto-generate the labels in this case.) Alignment is a bit tricky in this case. However, it can be achieved using the align_plots() function. The trick is to first align the top-row plot (plot.iris) and the first botton-row plot (plot.mpg) vertically along the left axis, using the align_plots() function. Then these aligned plots can be passed to plot_grid(). Note that this vertical alignment does not interfere with the horizontal alignment of the bottom row.

-
# first align the top-row plot (plot.iris) with the left-most plot of the
-# bottom row (plot.mpg)
-plots <- align_plots(plot.mpg, plot.iris, align = 'v', axis = 'l')
-# then build the bottom row
-bottom_row <- plot_grid(plots[[1]], plot.diamonds, 
-                        labels = c('B', 'C'), align = 'h', rel_widths = c(1, 1.3))
-# then combine with the top row for final plot
-plot_grid(plots[[2]], bottom_row, labels = c('A', ''), ncol = 1, rel_heights = c(1, 1.2))
-

-
+
# first align the top-row plot (plot.iris) with the left-most plot of the
+
# bottom row (plot.mpg)
+
plots <- align_plots(plot.mpg, plot.iris, align = 'v', axis = 'l')
+
# then build the bottom row
+
bottom_row <- plot_grid(plots[[1]], plot.diamonds,
+
labels = c('B', 'C'), align = 'h', rel_widths = c(1, 1.3))
+
# then combine with the top row for final plot
+
plot_grid(plots[[2]], bottom_row, labels = c('A', ''), ncol = 1, rel_heights = c(1, 1.2))
+

+ diff --git a/inst/doc/shared_legends.html b/inst/doc/shared_legends.html index 7e6a7f6..aea3d13 100644 --- a/inst/doc/shared_legends.html +++ b/inst/doc/shared_legends.html @@ -12,7 +12,7 @@ - + Shared legends @@ -20,46 +20,244 @@ - + @@ -70,98 +268,98 @@

Shared legends

Claus O. Wilke

-

2017-10-20

+

2017-12-09

This vignette demonstrates how to make compound plots with a shared legend.

-
# down-sampled diamonds data set
-dsamp <- diamonds[sample(nrow(diamonds), 1000), ]
-
-# Make three plots.
-# We set left and right margins to 0 to remove unnecessary spacing in the
-# final plot arrangement.
-p1 <- qplot(carat, price, data=dsamp, colour=clarity) +
-  theme(plot.margin = unit(c(6,0,6,0), "pt"))
-p2 <- qplot(depth, price, data=dsamp, colour=clarity) +
-  theme(plot.margin = unit(c(6,0,6,0), "pt")) + ylab("")
-p3 <- qplot(color, price, data=dsamp, colour=clarity) +
-  theme(plot.margin = unit(c(6,0,6,0), "pt")) + ylab("")
-
-# arrange the three plots in a single row
-prow <- plot_grid( p1 + theme(legend.position="none"),
-           p2 + theme(legend.position="none"),
-           p3 + theme(legend.position="none"),
-           align = 'vh',
-           labels = c("A", "B", "C"),
-           hjust = -1,
-           nrow = 1
-           )
-prow
-

+
# down-sampled diamonds data set
+
dsamp <- diamonds[sample(nrow(diamonds), 1000), ]
+
+
# Make three plots.
+
# We set left and right margins to 0 to remove unnecessary spacing in the
+
# final plot arrangement.
+
p1 <- qplot(carat, price, data=dsamp, colour=clarity) +
+
theme(plot.margin = unit(c(6,0,6,0), "pt"))
+
p2 <- qplot(depth, price, data=dsamp, colour=clarity) +
+
theme(plot.margin = unit(c(6,0,6,0), "pt")) + ylab("")
+
p3 <- qplot(color, price, data=dsamp, colour=clarity) +
+
theme(plot.margin = unit(c(6,0,6,0), "pt")) + ylab("")
+
+
# arrange the three plots in a single row
+
prow <- plot_grid( p1 + theme(legend.position="none"),
+
p2 + theme(legend.position="none"),
+
p3 + theme(legend.position="none"),
+
align = 'vh',
+
labels = c("A", "B", "C"),
+
hjust = -1,
+
nrow = 1
+
)
+
prow
+

Legend to the side:

-
# extract the legend from one of the plots
-# (clearly the whole thing only makes sense if all plots
-# have the same legend, so we can arbitrarily pick one.)
-legend <- get_legend(p1)
-
-# add the legend to the row we made earlier. Give it one-third of the width
-# of one plot (via rel_widths).
-p <- plot_grid( prow, legend, rel_widths = c(3, .3))
-p
-

+
# extract the legend from one of the plots
+
# (clearly the whole thing only makes sense if all plots
+
# have the same legend, so we can arbitrarily pick one.)
+
legend <- get_legend(p1)
+
+
# add the legend to the row we made earlier. Give it one-third of the width
+
# of one plot (via rel_widths).
+
p <- plot_grid( prow, legend, rel_widths = c(3, .3))
+
p
+

Legend at the bottom:

-
# extract the legend from one of the plots
-# (clearly the whole thing only makes sense if all plots
-# have the same legend, so we can arbitrarily pick one.)
-legend_b <- get_legend(p1 + theme(legend.position="bottom"))
-
-# add the legend underneath the row we made earlier. Give it 10% of the height
-# of one plot (via rel_heights).
-p <- plot_grid( prow, legend_b, ncol = 1, rel_heights = c(1, .2))
-p
-

+
# extract the legend from one of the plots
+
# (clearly the whole thing only makes sense if all plots
+
# have the same legend, so we can arbitrarily pick one.)
+
legend_b <- get_legend(p1 + theme(legend.position="bottom"))
+
+
# add the legend underneath the row we made earlier. Give it 10% of the height
+
# of one plot (via rel_heights).
+
p <- plot_grid( prow, legend_b, ncol = 1, rel_heights = c(1, .2))
+
p
+

Legend between plots:

-
# arrange the three plots in a single row, leaving space between plot B and C
-prow <- plot_grid( p1 + theme(legend.position="none"),
-           p2 + theme(legend.position="none"),
-           NULL,
-           p3 + theme(legend.position="none"),
-           align = 'vh',
-           labels = c("A", "B", "", "C"),
-           hjust = -1,
-           nrow = 1,
-           rel_widths = c(1, 1, .3, 1)
-           )
-
-prow + draw_grob(legend, 2/3.3, 0, .3/3.3, 1)
-

+
# arrange the three plots in a single row, leaving space between plot B and C
+
prow <- plot_grid( p1 + theme(legend.position="none"),
+
p2 + theme(legend.position="none"),
+
NULL,
+
p3 + theme(legend.position="none"),
+
align = 'vh',
+
labels = c("A", "B", "", "C"),
+
hjust = -1,
+
nrow = 1,
+
rel_widths = c(1, 1, .3, 1)
+
)
+
+
prow + draw_grob(legend, 2/3.3, 0, .3/3.3, 1)
+

One more example, now with a more complex plot arrangement:

-
# plot A
-p1 <- ggplot(iris, aes(Sepal.Length, Sepal.Width, color = Species)) + 
-  geom_point() + facet_grid(. ~ Species) + stat_smooth(method = "lm") +
-  background_grid(major = 'y', minor = "none") + 
-  panel_border() + theme(legend.position = "none")
-
-# plot B
-p2 <- ggplot(iris, aes(Sepal.Length, fill = Species)) +
-  geom_density(alpha = .7) + theme(legend.justification = "top")
-p2a <- p2 + theme(legend.position = "none")
-
-# plot C
-p3 <- ggplot(iris, aes(Sepal.Width, fill = Species)) +
-  geom_density(alpha = .7) + theme(legend.position = "none")
-
-# legend
-legend <- get_legend(p2)
-
-# align all plots vertically
-plots <- align_plots(p1, p2a, p3, align = 'v', axis = 'l')
-
-# put together bottom row and then everything
-bottom_row <- plot_grid(plots[[2]], plots[[3]], legend, labels = c("B", "C"), rel_widths = c(1, 1, .3), nrow = 1)
-plot_grid(plots[[1]], bottom_row, labels = c("A"), ncol = 1)
-

+
# plot A
+
p1 <- ggplot(iris, aes(Sepal.Length, Sepal.Width, color = Species)) +
+
geom_point() + facet_grid(. ~ Species) + stat_smooth(method = "lm") +
+
background_grid(major = 'y', minor = "none") +
+
panel_border() + theme(legend.position = "none")
+
+
# plot B
+
p2 <- ggplot(iris, aes(Sepal.Length, fill = Species)) +
+
geom_density(alpha = .7) + theme(legend.justification = "top")
+
p2a <- p2 + theme(legend.position = "none")
+
+
# plot C
+
p3 <- ggplot(iris, aes(Sepal.Width, fill = Species)) +
+
geom_density(alpha = .7) + theme(legend.position = "none")
+
+
# legend
+
legend <- get_legend(p2)
+
+
# align all plots vertically
+
plots <- align_plots(p1, p2a, p3, align = 'v', axis = 'l')
+
+
# put together bottom row and then everything
+
bottom_row <- plot_grid(plots[[2]], plots[[3]], legend, labels = c("B", "C"), rel_widths = c(1, 1, .3), nrow = 1)
+
plot_grid(plots[[1]], bottom_row, labels = c("A"), ncol = 1)
+