Skip to content

Instantly share code, notes, and snippets.

@tomhopper
Last active June 25, 2023 17:36
Show Gist options
  • Save tomhopper/faa24797bb44addeba79 to your computer and use it in GitHub Desktop.
Save tomhopper/faa24797bb44addeba79 to your computer and use it in GitHub Desktop.
Align multiple ggplot2 graphs with a common x axis and different y axes, each with different y-axis labels.
#' When plotting multiple data series that share a common x axis but different y axes,
#' we can just plot each graph separately. This suffers from the drawback that the shared axis will typically
#' not align across graphs due to different plot margins.
#' One easy solution is to reshape2::melt() the data and use ggplot2's facet_grid() mapping. However, there is
#' no way to label individual y axes.
#' facet_grid() and facet_wrap() were designed to plot small multiples, where both x- and y-axis ranges are
#' shared acros all plots in the facetting. While the facet_ calls allow us to use different scales with
#' the \code{scales = "free"} argument, they should not be used this way.
#' A more robust approach is to the grid package grid.draw(), rbind() and ggplotGrob() to create a grid of
#' individual plots where the plot axes are properly aligned within the grid.
#' Thanks to https://rpubs.com/MarkusLoew/13295 for the grid.arrange() idea.
library(ggplot2)
library(grid)
library(dplyr)
library(lubridate)
#' Create some data to play with. Two time series with the same timestamp.
df <- data.frame(DateTime = ymd("2010-07-01") + c(0:8760) * hours(2), series1 = rnorm(8761), series2 = rnorm(8761, 100))
#' Create the two plots.
plot1 <- df %>%
select(DateTime, series1) %>%
na.omit() %>%
ggplot() +
geom_point(aes(x = DateTime, y = series1), size = 0.5, alpha = 0.75) +
ylab("Red dots / m") +
theme_minimal() +
theme(axis.title.x = element_blank())
plot2 <- df %>%
select(DateTime, series2) %>%
na.omit() %>%
ggplot() +
geom_point(aes(x = DateTime, y = series2), size = 0.5, alpha = 0.75) +
ylab("Blue drops / L") +
theme_minimal() +
theme(axis.title.x = element_blank())
grid.newpage()
grid.draw(rbind(ggplotGrob(plot1), ggplotGrob(plot2), size = "last"))
# To plot two series vertically aligned with only one labelled x-axis,
# we can remove the axes from the top plot and then plot the two graphs
# together using either the egg package or the cowplot package
library(ggplot2) # 3.2.1
library(dplyr)
library(lubridate)
library(cowplot) # 1.0.0
library(egg) # 0.4.5
#' Create some data to play with. Two time series with the same timestamp.
df <- data.frame(DateTime = ymd("2010-07-01") + c(0:8760) * hours(2),
series1 = rnorm(8761),
series2 = rnorm(8761, 100))
#' Create the two plots.
plot1 <- df %>%
select(DateTime, series1) %>%
na.omit() %>%
ggplot() +
geom_point(aes(x = DateTime, y = series1), size = 0.5, alpha = 0.75) +
ylab("Red dots / m") +
theme_minimal() +
theme(axis.title.x = element_blank(),
axis.text.x = element_blank())
plot2 <- df %>%
select(DateTime, series2) %>%
na.omit() %>%
ggplot() +
geom_point(aes(x = DateTime, y = series2), size = 0.5, alpha = 0.75) +
ylab("Blue drops / L") +
theme_minimal() +
theme(axis.title.x = element_blank())
# Draw the two plot aligned vertically, with the top plot 1/3 of the height
# of the bottom plot
cowplot::plot_grid(plot1, plot2, align = "v", ncol = 1, rel_heights = c(0.25, 0.75))
egg::ggarrange(plot1, plot2, heights = c(0.25, 0.75))
@dschneiderch
Copy link

use this to get different heights (all credit goes to an answer on stack overflow):

library(gridExtra)
gA=ggplot_gtable(ggplot_build(plot1))
gB=ggplot_gtable(ggplot_build(plot2))
maxWidth = grid::unit.pmax(gA$widths[2:3], gB$widths[2:3])
gA$widths[2:3] <- as.list(maxWidth)
gB$widths[2:3] <- as.list(maxWidth)
grid.newpage()

pdf('graphs/test.pdf',
     width=8,
     height=6)
grid.arrange(
    arrangeGrob(gA,gB,nrow=2,heights=c(.8,.3))
    )
dev.off()

you'll also want to play with

theme(plot.margin=unit(c(.2,1,.1,1),"cm"))

in each of your plots to control the distance between them.

@rmariscal
Copy link

I note that in the last version the different heights code doesn't work properly. You have to get rid of the brackets:

maxWidth = grid::unit.pmax(gA$widths, gB$widths)
gA$widths <- as.list(maxWidth)
gB$widths <- as.list(maxWidth)

@smasongarrison
Copy link

Note: I had to install an extra library to get this code to work. I had to add the library(lubridate) for the ymd function to work.

@anmolsethy
Copy link

Very neat. Thanks a lot, this was really helpful. I was struggling to align the y-axis in multiple graphs.

@vc1492a
Copy link

vc1492a commented Dec 16, 2016

Very neat and tidy.

@meniluca
Copy link

Thank you Tom!

@mgm-cincy
Copy link

Thanks, this is very helpful. I have a question. I ran grid.draw and my plot with the two aligned graphs appeared in the plot frame of RStudio. I then tried this assignment:
pdbox_spc_cms <- grid.draw(rbind(ggplotGrob(pdbox1), ggplotGrob(pdbox2), size = "last")) so that I then could use ggsave to save in the file format, dimensions and dpi that I needed. But, that assignment returned as NULL. Any suggestions as to how should I have made that assignment?

@mattecologist
Copy link

Nice! Cheers for this.

@wsgroves
Copy link

wsgroves commented Jun 8, 2017

In response to mgm-cincy on March 16:

ggplot2's ggsave() function has the ability to save grid.draw().

eg.:
ggsave("File name.png", path = "File path", plot = grid.draw(rbind(ggplotGrob(plot1), ggplotGrob(plot2), size = "last")), ... )

Hope this helps!

@tdjames1
Copy link

Thanks, this is just what I needed.

@LukasBDF
Copy link

Splendid! Is there a way to supress the printing of the same x axis label on each subplot except the last one?

@julian-lemos
Copy link

Great solution, thanks!

@lker5lker5
Copy link

Awesome! but a little bit extension: in this case, how can I seamlessly combine these two plots with only eaxcatly ONE x-axis (i.e. there is no space left between these two rows)?

@sunxm19
Copy link

sunxm19 commented Jan 31, 2018

Just beautiful code!

@KanikaNahata
Copy link

Thank you! This helps a lot!

@naiieandrade
Copy link

Thannk you!! Easy and help me a lot too! 😁

@MattChristmas
Copy link

Exactly what I was looking for, thank you! So neat and saves me fiddling around with aligning plots outside of R.

r9_onp_depth_plot copy

@ADam-Z514
Copy link

Can both graphs have differents names?
I have managed to put 2 graphs in the same plot using grid.arrange : grid.arrange(graph_weight_ori, graph_weight_imp, nrow = 1)
how can I give them different names?

@DatenBergwerker
Copy link

@ADam-Z514
It should work the same with grid.arrange. You should have to convert to Grob first and bind them together with rbind.

grid.arrange(rbind(ggplotGrob(graph_weight_ori), ggplotGrob(bpgraph_weight_imp), size = "first"))

@bggalvenmon
Copy link

This code is great, but is there a way that you can bind both plots together so that x-axis for the upper plot is only separated by a line so as not to show the numbers. Basically, I want two plots directly on top of each other with the same scales for the y-axis but two different variables. Is this possible? I am having trouble finding any code to do this exactly.

@bggalvenmon
Copy link

for some reason I got this plot to work and now the select command is giving me an error: "Error in select(., d34S, d13C) : unused arguments (d34S, d13C)". The libraries I am running in the background are:

library(tidyverse)
library(ggplot2)
library(broom)
library(sjstats)
library(plotly)
library(fastDummies)
library(MASS)
library(AICcmodavg)
library(car)
library(MASS)
library(grid)
library(dplyr)
library(lubridate)

Can anybody help me?

@gorgitko
Copy link

@bggalvenmon Some package is masking dplyr's select: use dplyr::select

@jmbarbone
Copy link

@bggalvenmon Some package is masking dplyr's select: use dplyr::select

I think there is a select function loaded through the MASS package - I recently had a script failing because of this issue.

If you don't want to keep calling the function direction you can add the following to the beginning of your script: select <- dplyr::select

@norakirkizh
Copy link

did something like:

p1 <- ggplot(...) + geom_histogram()
p2 <- ggplot(...) + geom_boxplot() + geom_point() + coord_flip()
grid.newpage()
grid.draw(...)

and the axis were close, but not right on.

edit: was able to get them to align by adding:

p1 <- p1 + scale_x_continuous(limits=c(x_min, x_max))
p2 <- p2 + scale_y_continuous(limits=c(x_min, x_max))

thanks!

very nice solution, especially if one wants to compile 2-by-2 histograms from separate data frames.

@Sarikhan
Copy link

Sarikhan commented Dec 8, 2019

Thanks!

@lars3033
Copy link

Great help, thanks very much for posting!!

@lyoussar
Copy link

Many thanks! Solved my issue.

@darwinalexander
Copy link

Really nice solution!!

Thank you very much for your great help

@mjsobrep
Copy link

mjsobrep commented Jan 1, 2022

If you want to take this:

p1 <- p1 + scale_x_continuous(limits=c(x_min, x_max))
p2 <- p2 + scale_y_continuous(limits=c(x_min, x_max))

a step further, you can do something like:

x_min=min(c(layer_scales(p1)$x$range$range[[1]],layer_scales(p2)$x$range$range[[1]]))
x_max=max(c(layer_scales(p1)$x$range$range[[2]],layer_scales(p2)$x$range$range[[2]]))
p1<-p1+scale_x_continuous(limits=c(x_min, x_max))
p2<-p2+scale_x_continuous(limits=c(x_min, x_max))

@mjsobrep
Copy link

mjsobrep commented Jan 1, 2022

If you want to take this:

p1 <- p1 + scale_x_continuous(limits=c(x_min, x_max))
p2 <- p2 + scale_y_continuous(limits=c(x_min, x_max))

a step further, you can do something like:

x_min=min(c(layer_scales(p1)$x$range$range[[1]],layer_scales(plt_mod_cond)$x$range$range[[1]]))
x_max=max(c(layer_scales(plt_mod_all)$x$range$range[[2]],layer_scales(plt_mod_cond)$x$range$range[[2]]))
p1<-p1+scale_x_continuous(limits=c(x_min, x_max))
p2<-p2+scale_x_continuous(limits=c(x_min, x_max))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment