112

I'm using ggplot and have two graphs that I want to display on top of each other. I used grid.arrange from gridExtra to stack them. The problem is I want the left edges of the graphs to align as well as the right edges regardless of axis labels. (the problem arises because the labels of one graph are short while the other is long).

The Question:
How can I do this? I am not married to grid.arrange but the ggplot2 is a must.

What I've tried:
I tried playing with widths and heights as well as ncol and nrow to make a 2 x 2 grid and place the visuals in opposite corners and then play with the widths but I couldn't get the visuals in opposite corners.

require(ggplot2);require(gridExtra)
A <- ggplot(CO2, aes(x=Plant)) + geom_bar() +coord_flip() 
B <- ggplot(CO2, aes(x=Type)) + geom_bar() +coord_flip() 
grid.arrange(A, B, ncol=1)

enter image description here

zx8754
  • 52,746
  • 12
  • 114
  • 209
Tyler Rinker
  • 108,132
  • 65
  • 322
  • 519
  • 2
    Here are two possible options: [here](http://stackoverflow.com/q/9198637/324364) and [here](http://stackoverflow.com/q/12448428/324364). – joran Nov 08 '12 at 18:18
  • @Joran I'm looking for the left axes to be aligned. I don't think these will do it. I'd like to be wrong though. – Tyler Rinker Nov 08 '12 at 18:27

9 Answers9

150

Try this,

 gA <- ggplotGrob(A)
 gB <- ggplotGrob(B)
 maxWidth = grid::unit.pmax(gA$widths[2:5], gB$widths[2:5])
 gA$widths[2:5] <- as.list(maxWidth)
 gB$widths[2:5] <- as.list(maxWidth)
 grid.arrange(gA, gB, ncol=1)

Edit

Here's a more general solution (works with any number of plots) using a modified version of rbind.gtable included in gridExtra

gA <- ggplotGrob(A)
gB <- ggplotGrob(B)
grid::grid.newpage()
grid::grid.draw(rbind(gA, gB))
baptiste
  • 75,767
  • 19
  • 198
  • 294
  • 3
    Beautiful and really pretty straight forward. Thank you for the solution. – Tyler Rinker Nov 08 '12 at 21:37
  • 1
    Perfect solution! I've been looking for something like this to align multiple separate time-series plots that I can't do with faceting because of the major customization in each plot. – wahalulu Jan 20 '13 at 17:47
  • Would you be so kind as to provide what would be the way to match the heigh if we have two columns? gA$heights[2:3] does not seem to work. Do I have to select another element of the grob than 2:3? Thank you! – Etienne Low-Décarie Jun 28 '13 at 16:23
  • I was surprised to see that `length(gA$heights)!=length(gA$widths)` – Etienne Low-Décarie Jun 28 '13 at 16:51
  • why would the two be equal? A plot has a title, legend, strips, etc. besides the plot panel and axes, and the gtable layout has widths/heights for those elements. – baptiste Jun 28 '13 at 16:54
  • specifically, look at `gA$layout` to get a sense of what viewports heights are defined and which grobs they will contain. – baptiste Jun 28 '13 at 17:05
  • this solution is great but it fails if the misalignment is on the right side(one legend being wider than the other). Just do 2:5 rather than 2:3 and that should do it – CarrKnight Jul 03 '13 at 13:46
  • @CarrKnight good point, I've updated the code. The best solution, in the long-term, will always be to use gtable's rbind and cbind methods. – baptiste Jul 04 '13 at 13:41
  • Just a note to anyone who happens to find this great answer -- for some reason when I test this on my machine, the `grid.arrange()` call produces `NULL` -- I have to use `arrangeGrob()` instead. – KarateSnowMachine Sep 17 '13 at 01:00
  • Hi baptiste, is it correct that rbind_gtable_max() equalizes the widths AND heights of the individual plots (or is something in my plots causing it)? – JBJ Dec 05 '14 at 14:54
  • @JBJ the short answer is that it shouldn't, but you should post a separate question with a MSCRWE if you find it's not the case – baptiste Dec 05 '14 at 15:33
  • `arrangeGrob()` has the nice `heights` argument. Could you show how to expand your function `rbind_gtable_max` to such an argument as well? (And analogously for `cbind_gtable_max` and `widths`?). With the first version that was straight forward to do. Relatedly, Is there any information on when `gtable` does not fail this comparison any more? – Andreas Sep 02 '15 at 10:49
  • @Andreas rbind()ing two gtables will produce a new gtable with an arbitrary number of heights (the sum of both), while for grid.arrange it is clear from the number of objects passed to the function. It is trivial to set the heights, but it's very unlikely that users would want to set them *all*. Think of rbind two ggplots, would you want to set the dozen of heights explicitly? Regarding release and improvements, you should ask the developers. – baptiste Sep 02 '15 at 20:06
  • Thanks for following that up. I am after something that would allow to set the ratio of the heights of tables I passed to `rbind_gtable_max` -- regardless of how many rows each of the passed tables already has. That would be the same behaviour as `grid.arrange` would give. I agree that this is probably not trivial, I was just hoping for a drop in replacement of the now broken `grid.arrange` solution. – Andreas Sep 02 '15 at 20:21
  • @baptiste Regarding release and improvements, I feel like this issue is way above my head to even write a proper bug report. But I just installed gtable from github and according to very few tests I ran, the issues seem to have gone. I am back to the `grid.arrange` based solution, which works quite well for me. – Andreas Sep 02 '15 at 21:34
  • what do you mean by broken grid.arrange solution? – baptiste Sep 02 '15 at 21:59
  • 4
    Thank you for your solution Baptiste. However, I do not get this to work when one of the plots is a `tableGrob`. The `gtable::cbind` gives me a disappointing error: `nrow(x) == nrow(y) is not TRUE`. Any suggestions? – Gabra Sep 11 '15 at 08:14
  • 2
    This solution worked for me, put I'm trying to understand it. What does the `[2:5]` stand for? – Hurlikus Feb 13 '19 at 14:05
  • How would you do the same for height? – JAQuent May 06 '20 at 12:02
41

I wanted to generalize this for any number of plots. Here is a step-by-step solution using the approach by Baptiste:

plots <- list(A, B, C, D)
grobs <- list()
widths <- list()

collect the widths for each grob of each plot

for (i in 1:length(plots)){
    grobs[[i]] <- ggplotGrob(plots[[i]])
    widths[[i]] <- grobs[[i]]$widths[2:5]
}

use do.call to get the max width

maxwidth <- do.call(grid::unit.pmax, widths)

asign the max width to each grob

for (i in 1:length(grobs)){
     grobs[[i]]$widths[2:5] <- as.list(maxwidth)
}

plot

do.call("grid.arrange", c(grobs, ncol = 1))
slizb
  • 5,742
  • 4
  • 25
  • 22
35

Using cowplot package:

A <- ggplot(CO2, aes(x = Plant)) + geom_bar() + coord_flip() 
B <- ggplot(CO2, aes(x = Type)) + geom_bar() + coord_flip() 

library(cowplot)

plot_grid(A, B, ncol = 1, align = "v")

enter image description here

zx8754
  • 52,746
  • 12
  • 114
  • 209
12

On http://rpubs.com/MarkusLoew/13295 is a really easy solution available (last item) Applied to this problem:

require(ggplot2);require(gridExtra)
A <- ggplot(CO2, aes(x=Plant)) + geom_bar() +coord_flip() 
B <- ggplot(CO2, aes(x=Type)) + geom_bar() +coord_flip() 
grid.draw(rbind(ggplotGrob(A), ggplotGrob(B), size="first"))

you can also use this for both width and height:

require(ggplot2);require(gridExtra)
A <- ggplot(CO2, aes(x=Plant)) + geom_bar() +coord_flip() 
B <- ggplot(CO2, aes(x=Type)) + geom_bar() +coord_flip() 
C <- ggplot(CO2, aes(x=conc)) + geom_bar() +coord_flip()
D <- ggplot(CO2, aes(x=uptake)) + geom_bar() +coord_flip() 
grid.draw(cbind(
            rbind(ggplotGrob(A), ggplotGrob(B), size="first"),
            rbind(ggplotGrob(C), ggplotGrob(D), size="first"),
            size='first'))
Wilbert
  • 121
  • 1
  • 3
  • 2
    using `size="first"` means that the alignment won't look very good if the second plot is bigger than the first – baptiste Nov 05 '14 at 16:28
10

The egg package wraps ggplot objects into a standardised 3x3 gtable, enabling the alignment of plot panels between arbitrary ggplots, including facetted ones.

library(egg) # devtools::install_github('baptiste/egg')
library(ggplot2)

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)

enter image description here

baptiste
  • 75,767
  • 19
  • 198
  • 294
  • for me this could properly arrange horizontally a simple heatmap (`geom_tile`) with legend at bottom and a multifaceted heatmap (`facet_grid` with `geom_tile`), but failed to align the height of the third plot, which was a dendrogram (`geom_segment`). however, cowplot or `gridExtra::grid.arrange` were not able to do even the former, so this works the best so far – deeenes Jan 27 '17 at 13:16
8

Here is another possible solution using melt from the reshape2 package, and facet_wrap:

library(ggplot2)
library(reshape2)

dat = CO2[, c(1, 2)]
dat$id = seq(nrow(dat))
mdat = melt(dat, id.vars="id")

head(mdat)
#   id variable value
# 1  1    Plant   Qn1
# 2  2    Plant   Qn1
# 3  3    Plant   Qn1
# 4  4    Plant   Qn1
# 5  5    Plant   Qn1
# 6  6    Plant   Qn1

plot_1 = ggplot(mdat, aes(x=value)) + 
         geom_bar() + 
         coord_flip() +
         facet_wrap(~ variable, nrow=2, scales="free", drop=TRUE)

ggsave(plot=plot_1, filename="plot_1.png", height=4, width=6)

enter image description here

bdemarest
  • 14,397
  • 3
  • 53
  • 56
  • This solution assumes you have equal number of rows in each column. In my MRWE that's true but not in reality. – Tyler Rinker Nov 08 '12 at 21:36
  • I'm not sure I understand: Do you mean that CO2$Plant and CO2$Type happen to be the same length, but that your actual data isn't like that? – bdemarest Nov 08 '12 at 21:54
  • It's two different data sets that shares one variable so number of rows is not the same. – Tyler Rinker Nov 08 '12 at 22:56
4

The patchwork package handles this by default:

library(ggplot2)
library(patchwork)

A <- ggplot(CO2, aes(x = Plant)) + geom_bar() + coord_flip() 
B <- ggplot(CO2, aes(x = Type)) + geom_bar() + coord_flip() 

A / B

Created on 2019-12-08 by the reprex package (v0.3.0)

MSR
  • 2,731
  • 1
  • 14
  • 24
1

I know this is an old post, and that it has already been answered, but may I suggest combining @baptiste's approach with purrr to make it nicer-looking:

library(purrr)
list(A, B) %>% 
  map(ggplotGrob) %>% 
  do.call(gridExtra::gtable_rbind, .) %>% 
  grid::grid.draw()
Felipe Gerard
  • 1,552
  • 13
  • 23
0

At best this is a hack:

library(wq)
layOut(list(A, 1, 2:16),  list(B, 2:3, 1:16))

It feels really wrong though.

Tyler Rinker
  • 108,132
  • 65
  • 322
  • 519