204

I have two ggplots which I align horizontally with grid.arrange. I have looked through a lot of forum posts, but everything I try seem to be commands that are now updated and named something else.

My data looks like this;

# Data plot 1                                   
        axis1     axis2   
group1 -0.212201  0.358867
group2 -0.279756 -0.126194
group3  0.186860 -0.203273
group4  0.417117 -0.002592
group1 -0.212201  0.358867
group2 -0.279756 -0.126194
group3  0.186860 -0.203273
group4  0.186860 -0.203273

# Data plot 2   
        axis1     axis2
group1  0.211826 -0.306214
group2 -0.072626  0.104988
group3 -0.072626  0.104988
group4 -0.072626  0.104988
group1  0.211826 -0.306214
group2 -0.072626  0.104988
group3 -0.072626  0.104988
group4 -0.072626  0.104988

#And I run this:
library(ggplot2)
library(gridExtra)


groups=c('group1','group2','group3','group4','group1','group2','group3','group4')

x1=data1[,1]
y1=data1[,2]

x2=data2[,1]
y2=data2[,2]

p1=ggplot(data1, aes(x=x1, y=y1,colour=groups)) + geom_point(position=position_jitter(w=0.04,h=0.02),size=1.8)

p2=ggplot(data2, aes(x=x2, y=y2,colour=groups)) + geom_point(position=position_jitter(w=0.04,h=0.02),size=1.8)

#Combine plots
p3=grid.arrange(
p1 + theme(legend.position="none"), p2+ theme(legend.position="none"), nrow=1, widths = unit(c(10.,10), "cm"), heights = unit(rep(8, 1), "cm")))

How would I extract the legend from any of these plots and add it to the bottom/centre of the combined plot?

Henrik
  • 65,555
  • 14
  • 143
  • 159
jO.
  • 3,384
  • 7
  • 28
  • 38
  • 4
    I occasionally have this problem. If you don't want to facet the plot the easiest solution I know is just to save one with a legend then use Photoshop/Ilustrator to paste it onto the blank legend plots. Inelegant I know -- but practical quick and easy. – Stephen Henderson Nov 30 '12 at 17:04
  • 1
    @StephenHenderson That's an answer. Facet or post-process with gfx editor. – Brandon Bertelsen Nov 30 '12 at 18:00

9 Answers9

186

You may also use ggarrange from ggpubr package and set "common.legend = TRUE":

library(ggpubr)

dsamp <- diamonds[sample(nrow(diamonds), 1000), ]
p1 <- qplot(carat, price, data = dsamp, colour = clarity)
p2 <- qplot(cut, price, data = dsamp, colour = clarity)
p3 <- qplot(color, price, data = dsamp, colour = clarity)
p4 <- qplot(depth, price, data = dsamp, colour = clarity) 

ggarrange(p1, p2, p3, p4, ncol=2, nrow=2, common.legend = TRUE, legend="bottom")

enter image description here

Huiyan Wan
  • 1,951
  • 1
  • 8
  • 7
  • 1
    Is it possible that this doesn't work inside a shiny application (or flexdashboard) with renderPlot()? It works perfectly fine in a normal R script with normal plots. But when I do the exact same thing with plots made with renderPlot() in my flexdashboard, nothing appears. – Tingolfin Jan 26 '18 at 14:14
  • 1
    @Tingolfin I have had to sometimes wrap `print(ggarrangeobject)` around one of my `ggarrange` objects when I needed it to be plotted by some other function, that may be similar to the solution for your `renderPlot()`? – Brandon Feb 11 '20 at 04:21
  • `common.legend = TRUE` is all that I need! – Aryo Jun 02 '20 at 01:17
  • Great. If I wanted to make the common legend a single horizontal line, would that be possible? So instead of having two rows for clarity, just a single one? – Miranda Jul 09 '20 at 17:35
  • How do you make the legend displayed into several rows? – Aryo Aug 13 '20 at 05:09
  • what if I have two legends: one that is common to all plots (linetype) and another that is not (color)? I just want the linetype legend to be common to all. – Alvaro Morales Jul 13 '21 at 21:25
124

Update 2021-Mar

This answer has still some, but mostly historic, value. Over the years since this was posted better solutions have become available via packages. You should consider the newer answers posted below.

Update 2015-Feb

See Steven's answer below


df1 <- read.table(text="group   x     y   
group1 -0.212201  0.358867
group2 -0.279756 -0.126194
group3  0.186860 -0.203273
group4  0.417117 -0.002592
group1 -0.212201  0.358867
group2 -0.279756 -0.126194
group3  0.186860 -0.203273
group4  0.186860 -0.203273",header=TRUE)

df2 <- read.table(text="group   x     y   
group1  0.211826 -0.306214
group2 -0.072626  0.104988
group3 -0.072626  0.104988
group4 -0.072626  0.104988
group1  0.211826 -0.306214
group2 -0.072626  0.104988
group3 -0.072626  0.104988
group4 -0.072626  0.104988",header=TRUE)


library(ggplot2)
library(gridExtra)

p1 <- ggplot(df1, aes(x=x, y=y,colour=group)) + geom_point(position=position_jitter(w=0.04,h=0.02),size=1.8) + theme(legend.position="bottom")

p2 <- ggplot(df2, aes(x=x, y=y,colour=group)) + geom_point(position=position_jitter(w=0.04,h=0.02),size=1.8)

#extract legend
#https://github.com/hadley/ggplot2/wiki/Share-a-legend-between-two-ggplot2-graphs
g_legend<-function(a.gplot){
  tmp <- ggplot_gtable(ggplot_build(a.gplot))
  leg <- which(sapply(tmp$grobs, function(x) x$name) == "guide-box")
  legend <- tmp$grobs[[leg]]
  return(legend)}

mylegend<-g_legend(p1)

p3 <- grid.arrange(arrangeGrob(p1 + theme(legend.position="none"),
                         p2 + theme(legend.position="none"),
                         nrow=1),
             mylegend, nrow=2,heights=c(10, 1))

Here is the resulting plot: 2 plots with common legend

Roland
  • 127,288
  • 10
  • 191
  • 288
  • 3
    both answers point to the [same wiki page](https://github.com/hadley/ggplot2/wiki/Share-a-legend-between-two-ggplot2-graphs) which can be updated as new versions of ggplot2 break the code. – baptiste Jun 23 '16 at 20:53
  • More than six year later this answer solved my problem. Thanks! – SPK.z Jan 10 '19 at 08:47
  • 1
    This may be straightforward for some/most people, but I didn't get it right away so thought I'd comment. If you want the common legend on top of the plot (rather than below), all you need to do is switch the arguments. In the above example, mylegend goes before ```arrangeGrob()```. You also need to reverse the heights (i.e. ```heights=c(1,10)``` – ljh2001 Jul 09 '20 at 15:56
  • Is there a way to center the bottom legend or shift it to the right a bit? – wolfsatthedoor Mar 25 '21 at 19:24
  • @wolfsatthedoor `theme(legend.position="bottom", legend.justification = c(0.75, 0.5))` A value of 0.5 is the center, larger values shift to right/bottom. – Roland Mar 26 '21 at 06:04
  • I am trying to implement this code but I am getting an error on g_legend() : "Error in tmp$grobs[[leg]] : attempt to select less than one element in get1index". What is the possible issue? – user1916067 Apr 12 '21 at 17:47
90

A new, attractive solution is to use patchwork. The syntax is very simple:

library(ggplot2)
library(patchwork)

p1 <- ggplot(df1, aes(x = x, y = y, colour = group)) + 
  geom_point(position = position_jitter(w = 0.04, h = 0.02), size = 1.8)
p2 <- ggplot(df2, aes(x = x, y = y, colour = group)) + 
  geom_point(position = position_jitter(w = 0.04, h = 0.02), size = 1.8)

combined <- p1 + p2 & theme(legend.position = "bottom")
combined + plot_layout(guides = "collect")

Created on 2019-12-13 by the reprex package (v0.2.1)

MSR
  • 2,731
  • 1
  • 14
  • 24
  • 15
    If you slightly change the order of commands you can do this in one line: `combined <- p1 + p2 + plot_layout(guides = "collect") & theme(legend.position = "bottom")` – mlcyo Apr 23 '20 at 23:20
  • Why won’t patchwork place the legend between the plots now? It seems to have done it with this example but when I try this using the development version of patchwork it keeps placing the legend at the left margin and not the center. Thanks. – jaydoc Feb 07 '21 at 17:02
  • Using the latest CRAN version of patchwork (1.1.1), I still get the same output as in the answer here. As for the development version, I am not sure, but I suppose you could open an issue in the [repo](https://github.com/thomasp85/patchwork/issues) with a reprex to ask if what you're getting is intended. I don't see anything about this in the NEWS file, so I'm thinking it might not be. – MSR Feb 08 '21 at 13:16
  • It is possible to combine the x axis labs into a common one for two plot within patchwork? – Nip Oct 28 '21 at 19:27
  • 1
    @Nip: Not in the same simple way as for legends. See [this issue](https://github.com/thomasp85/patchwork/issues/150). – MSR Oct 29 '21 at 09:02
66

Roland's answer needs updating. See: https://github.com/hadley/ggplot2/wiki/Share-a-legend-between-two-ggplot2-graphs

This method has been updated for ggplot2 v1.0.0.

library(ggplot2)
library(gridExtra)
library(grid)


grid_arrange_shared_legend <- function(...) {
    plots <- list(...)
    g <- ggplotGrob(plots[[1]] + theme(legend.position="bottom"))$grobs
    legend <- g[[which(sapply(g, function(x) x$name) == "guide-box")]]
    lheight <- sum(legend$height)
    grid.arrange(
        do.call(arrangeGrob, lapply(plots, function(x)
            x + theme(legend.position="none"))),
        legend,
        ncol = 1,
        heights = unit.c(unit(1, "npc") - lheight, lheight))
}

dsamp <- diamonds[sample(nrow(diamonds), 1000), ]
p1 <- qplot(carat, price, data=dsamp, colour=clarity)
p2 <- qplot(cut, price, data=dsamp, colour=clarity)
p3 <- qplot(color, price, data=dsamp, colour=clarity)
p4 <- qplot(depth, price, data=dsamp, colour=clarity)
grid_arrange_shared_legend(p1, p2, p3, p4)

Note the lack of ggplot_gtable and ggplot_build. ggplotGrob is used instead. This example is a bit more convoluted than the above solution but it still solved it for me.

Mateusz1981
  • 1,817
  • 17
  • 33
Steven Lockton
  • 761
  • 5
  • 3
  • 10
    Hi, I have 6 plots, and I would like to arrange the 6 plots as 2 row × 3 col and draw the legend on the top, so how to change the function `grid_arrange_shared_legend`? Thank you! – just_rookie May 09 '15 at 09:15
  • 4
    @just_rookie did you find a solution how to change the function so that one can use different ncol and nrow arrangements instead of only `ncol = 1`? – Giuseppe Jan 06 '16 at 09:57
  • Hi, i tried this solution, it works good, however when printing it, i got 2 pdf pages instead of only 1, the first is blank while the later contains my plot, why i got such behavior ? thanks, – HanniBaL90 Jun 20 '17 at 22:02
  • for anyone how get the same isse as me, here is a workaround: https://stackoverflow.com/questions/12481267/in-r-how-to-prevent-blank-page-in-pdf-when-using-gridbase-to-embed-subplot-insi – HanniBaL90 Jun 20 '17 at 22:25
  • 1
    Great stuff here. Any idea how one can add a single title for all plots? – Pertinax May 13 '18 at 12:29
  • @Giuseppe the code to do this while permitting you to define the number of rows and cols is at https://github.com/tidyverse/ggplot2/wiki/Share-a-legend-between-two-ggplot2-graphs – carbocation Aug 01 '19 at 19:02
  • what if the plots share some of the legend values and the others are different ? I don't want to use one legend for all of them since others might have different ones – Mariya Apr 08 '21 at 10:41
28

I suggest using cowplot. From their R vignette:

# load cowplot
library(cowplot)

# 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
           )

# 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

combined plots with legend at bottom

Gregor Sturm
  • 2,792
  • 1
  • 25
  • 34
  • This was only way, that maked a possibled to put a **manual** legend in my plot with `annotate_figure(ggarrange())`, using a legend_b(). **Thank you so much, god blesse you!** – Jean Karlos Jun 07 '20 at 23:07
13

@Giuseppe, you may want to consider this for a flexible specification of the plots arrangement (modified from here):

library(ggplot2)
library(gridExtra)
library(grid)

grid_arrange_shared_legend <- function(..., nrow = 1, ncol = length(list(...)), position = c("bottom", "right")) {

  plots <- list(...)
  position <- match.arg(position)
  g <- ggplotGrob(plots[[1]] + theme(legend.position = position))$grobs
  legend <- g[[which(sapply(g, function(x) x$name) == "guide-box")]]
  lheight <- sum(legend$height)
  lwidth <- sum(legend$width)
  gl <- lapply(plots, function(x) x + theme(legend.position = "none"))
  gl <- c(gl, nrow = nrow, ncol = ncol)

  combined <- switch(position,
                     "bottom" = arrangeGrob(do.call(arrangeGrob, gl),
                                            legend,
                                            ncol = 1,
                                            heights = unit.c(unit(1, "npc") - lheight, lheight)),
                     "right" = arrangeGrob(do.call(arrangeGrob, gl),
                                           legend,
                                           ncol = 2,
                                           widths = unit.c(unit(1, "npc") - lwidth, lwidth)))
  grid.newpage()
  grid.draw(combined)

}

Extra arguments nrow and ncol control the layout of the arranged plots:

dsamp <- diamonds[sample(nrow(diamonds), 1000), ]
p1 <- qplot(carat, price, data = dsamp, colour = clarity)
p2 <- qplot(cut, price, data = dsamp, colour = clarity)
p3 <- qplot(color, price, data = dsamp, colour = clarity)
p4 <- qplot(depth, price, data = dsamp, colour = clarity)
grid_arrange_shared_legend(p1, p2, p3, p4, nrow = 1, ncol = 4)
grid_arrange_shared_legend(p1, p2, p3, p4, nrow = 2, ncol = 2)

enter image description here enter image description here

epsilone
  • 745
  • 11
  • 23
  • Same as for the other solution, i tried it, it works good, however when printing it, i got 2 pdf pages instead of only 1, the first is blank while the later contains my plot, why i got such behavior ? thanks, – HanniBaL90 Jun 20 '17 at 22:03
  • for anyone how get the same isse as me, here is a workaround: https://stackoverflow.com/questions/12481267/in-r-how-to-prevent-blank-page-in-pdf-when-using-gridbase-to-embed-subplot-insi – HanniBaL90 Jun 20 '17 at 22:25
  • Can someone explain me the solution ? How can i a place the legend at the top instead of the bottom ? Thanks – HanniBaL90 Jun 23 '17 at 00:27
9

If you are plotting the same variables in both plots, the simplest way would be to combine the data frames into one, then use facet_wrap.

For your example:

big_df <- rbind(df1,df2)

big_df <- data.frame(big_df,Df = rep(c("df1","df2"),
times=c(nrow(df1),nrow(df2))))

ggplot(big_df,aes(x=x, y=y,colour=group)) 
+ geom_point(position=position_jitter(w=0.04,h=0.02),size=1.8) 
+ facet_wrap(~Df)

Plot 1

Another example using the diamonds data set. This shows that you can even make it work if you have only one variable common between your plots.

diamonds_reshaped <- data.frame(price = diamonds$price,
independent.variable = c(diamonds$carat,diamonds$cut,diamonds$color,diamonds$depth),
Clarity = rep(diamonds$clarity,times=4),
Variable.name = rep(c("Carat","Cut","Color","Depth"),each=nrow(diamonds)))

ggplot(diamonds_reshaped,aes(independent.variable,price,colour=Clarity)) + 
geom_point(size=2) + facet_wrap(~Variable.name,scales="free_x") + 
xlab("")

Plot 2

Only tricky thing with the second example is that the factor variables get coerced to numeric when you combine everything into one data frame. So ideally, you will do this mainly when all your variables of interest are the same type.

Prradep
  • 5,506
  • 5
  • 43
  • 84
hmgeiger
  • 121
  • 1
  • 4
4

If the legend is the same for both plots, there is a simple solution using grid.arrange(assuming you want your legend to align with both plots either vertically or horizontally). Simply keep the legend for the bottom-most or right-most plot while omitting the legend for the other. Adding a legend to just one plot, however, alters the size of one plot relative to the other. To avoid this use the heights command to manually adjust and keep them the same size. You can even use grid.arrange to make common axis titles. Note that this will require library(grid) in addition to library(gridExtra). For vertical plots:

y_title <- expression(paste(italic("E. coli"), " (CFU/100mL)"))

grid.arrange(arrangeGrob(p1, theme(legend.position="none"), ncol=1), 
   arrangeGrob(p2, theme(legend.position="bottom"), ncol=1), 
   heights=c(1,1.2), left=textGrob(y_title, rot=90, gp=gpar(fontsize=20)))

Here is the result for a similar graph for a project I was working on: enter image description here

kangaroo_cliff
  • 6,067
  • 3
  • 29
  • 42
3

@Guiseppe:

I have no idea of Grobs etc whatsoever, but I hacked together a solution for two plots, should be possible to extend to arbitrary number but its not in a sexy function:

plots <- list(p1, p2)
g <- ggplotGrob(plots[[1]] + theme(legend.position="bottom"))$grobs
legend <- g[[which(sapply(g, function(x) x$name) == "guide-box")]]
lheight <- sum(legend$height)
tmp <- arrangeGrob(p1 + theme(legend.position = "none"), p2 + theme(legend.position = "none"), layout_matrix = matrix(c(1, 2), nrow = 1))
grid.arrange(tmp, legend, ncol = 1, heights = unit.c(unit(1, "npc") - lheight, lheight))
Jack
  • 195
  • 2
  • 10