Related
I am trying to align three plots (with different scales on the y-axis) on the left y-axis. In other words, I would like the red axis to be aligned:
However, the y-axis of the first plot does not align with the y-axis of the bottom left plot.
Code
# Libraries
library(tidyverse)
library(cowplot)
df1 <- data.frame(x = seq(0, 100, 1),
y = seq(100, 0, -1))
df2 <- data.frame(x = seq(0, 10, 0.1),
y = seq(1, 10^9, length.out = 101 ) )
p1 <- ggplot(data = df1) +
geom_line(aes(x = x, y = y))
p2 <- ggplot(data = df2) +
geom_line(aes(x = x, y = y))
combi_p2 <- plot_grid(p2, p2, nrow = 1)
plot_grid(p1, combi_p2, ncol = 1, axis = "l", align = "v")
Attempt to fix it
Using the information provided here, I rewrote the last part of the code:
require(grid) # for unit.pmax()
p1 <- ggplotGrob(p1) # convert to gtable
combi_p2 <- ggplotGrob(combi_p2) # convert to gtable
p1.widths <- p1$widths[1:3] # extract the first three widths,
# corresponding to left margin, y lab, and y axis
combi_p2.widths <- combi_p2$widths[1:3] # same for combi_p2 plot
max.widths <- unit.pmax(p1.widths, combi_p2.widths) # calculate maximum widths
p1$widths[1:3] <- max.widths # assign max. widths to p1 gtable
combi_p2$widths[1:3] <- max.widths # assign max widths to combi_p2 gtable
# plot_grid() can work directly with gtables, so this works
plot_grid(p1, combi_p2, labels = "AUTO", ncol = 1)
Sadly, I was not able to fix the alignment:
Question
How do I align the y-axis of the top plot with the left bottom plot using cowplot in R?
I think you can use ggplotGrob and put them together with gtable_rbind and gtable_cbind. Finally, you can draw the plot with grid.draw()
df1 <- data.frame(x = seq(0, 100, 1),
y = seq(100, 0, -1))
df2 <- data.frame(x = seq(0, 10, 0.1),
y = seq(1, 10^9, length.out = 101 ) )
p1 <- ggplot(data = df1) +
geom_line(aes(x = x, y = y))
p2 <- ggplot(data = df2) +
geom_line(aes(x = x, y = y))
g1 <- ggplotGrob(p1)
g2 <- ggplotGrob(p2)
frame_g2 <- gtable_frame(g2 , debug = TRUE)
frame_combi <- gtable_frame(gtable_cbind(frame_g2,frame_g2),
width = unit(2, "null"),
height = unit(1, "null"))
frame_g1 <-
gtable_frame(
g1,
width = unit(1, "null"),
height = unit(1, "null"),
debug = TRUE
)
grid.newpage()
all_frames <- gtable_rbind(frame_g1, frame_combi)
grid.draw(all_frames)
And this is how the plot looks.
A cowplot solution by Claus O. Wilke is presented here.
It is based on the align_plot function, which first aligns the top plot with the left bottom plot along the y-axis. Then the aligned plots are passed to the plot_grid function.
# Libraries
library(tidyverse)
library(cowplot)
df1 <- data.frame(x = seq(0, 100, 1),
y = seq(100, 0, -1))
df2 <- data.frame(x = seq(0, 10, 0.1),
y = seq(1, 10^9, length.out = 101 ) )
p1 <- ggplot(data = df1) +
geom_line(aes(x = x, y = y))
p2 <- ggplot(data = df2) +
geom_line(aes(x = x, y = y))
plots <- align_plots(p1, p2, align = 'v', axis = 'l')
bottom_row <- plot_grid(plots[[2]], p2, nrow = 1)
plot_grid(plots[[1]], bottom_row, ncol = 1)
I'm trying to make my multipanel ggplot with a shared legend more flexible in a ShinyApp by allowing the user to choose how many panels to plot.
Currently my code writes out the panel objects 1 at a time like this.
grid_arrange_shared_legend(p1,p2,p3,p4, ncol = 4, nrow = 1)
I do not fully understand why I can not find a way to tell the grid_arrange_shared_legend to accept a list of plots (list object) rather than writing them out 1 after the other.
It throws this error:
Error in UseMethod("ggplot_build") :
no applicable method for 'ggplot_build' applied to an object of class "NULL"
library(ggplot2)
library(lemon)
plotlist <- list()
dsamp <- diamonds[sample(nrow(diamonds), 300), ]
plotlist$p1 <- qplot(carat, price, data = dsamp, colour = clarity)
plotlist$p2 <- qplot(cut, price, data = dsamp, colour = clarity)
plotlist$p3 <- qplot(color, price, data = dsamp, colour = clarity)
plotlist$p4 <- qplot(depth, price, data = dsamp, colour = clarity)
grid_arrange_shared_legend(plotlist, ncol = 4, nrow = 1)
with the use of a list, it would not matter how many plots are in the list, and I would calculate ncol or nrow based on the length of the list...
My homebrew version of the function gets that by adding a plotlist parameter, and adding the plots <- c(list(...), plotlist) line as the first line of code. That way it can take both a list of plots or separate plot objects.
grid_arrange_shared_legend_plotlist <- function(...,
plotlist=NULL,
ncol = length(list(...)),
nrow = NULL,
position = c("bottom", "right")) {
plots <- c(list(...), plotlist)
if (is.null(nrow)) nrow = ceiling(length(plots)/ncol)
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, ncol = ncol, nrow = nrow)
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)
# return gtable invisibly
invisible(combined)
}
Using your example:
library(gridExtra)
library(grid)
library(ggplot2)
plots <- list()
dsamp <- diamonds[sample(nrow(diamonds), 300), ]
plots$p1 <- qplot(carat, price, data = dsamp, colour = clarity)
plots$p2 <- qplot(cut, price, data = dsamp, colour = clarity)
plots$p3 <- qplot(color, price, data = dsamp, colour = clarity)
plots$p4 <- qplot(depth, price, data = dsamp, colour = clarity)
grid_arrange_shared_legend_plotlist(plotlist = plots, ncol = 4)
The ugly text string paste solution:
Since the answers provided do not seem to work, or are not suitable (rebuilding an entirely different set of plots than the list of plot objects I already have from extensive code, I played around a bit with eval(parse(text = ....) and paste0 to dynamically generate a text string that ends up being the fully written out code (which works) without actually writing it out
nplots = 4
nrow = 2
ncol = ceiling(nplots/nrow)
eval(parse( text = paste0("grid_arrange_shared_legend(", paste0("plotlist", "[[", c(1:nplots), "]]", sep = '', collapse = ','), ",ncol =", ncol, ",nrow =", nrow, ", position = 'right', top=grid::textGrob('My title', gp=grid::gpar(fontsize=18)))", sep = '')))
which produces:
[1]
"grid_arrange_shared_legend(plotlist[[1]],plotlist[[2]],plotlist[[3]],plotlist[[4]],ncol
=2,nrow =2, position = 'right', top=grid::textGrob('My title', gp=grid::gpar(fontsize=18)))"
I would like to combine vertically two figures using viewport. Figures are created with ggplot and facet_grid().
The problem which arises is that the legend of the categorical variable differ in lengths. This result in plots with different width since
the legend takes more places. I would like that the width of the plots are identically.
How can I solve this problem?
Here is an example of a figures with not aligned plots:
Here is the code to produce the figure:
# dataframe
x <- rep(1:10,2)
y <- x + rep(c(0,2),each=10)
sex <- rep(c("f","m"), each=10)
sex2 <- rep(c("fffffffff","mmmmmmmmm"), each=10)
df0 <- data.frame(x = x, y = y, sex = sex, sex2 = sex2)
# libraries
library("grid")
library("gridExtra")
library("ggplot2")
# Viewport
Layout <- grid.layout(nrow = 2, ncol = 1, heights = unit(c(1,1), c("null","null")))
vplayout <- function(x,y) {
viewport(layout.pos.row=x, layout.pos.col=y)
}
# plot object
p1 <- ggplot(df0,aes(x = x, y = y,linetype=sex)) +
geom_line()
p2 <- ggplot(df0,aes(x = x, y = y,linetype=sex2)) +
geom_line()
# figures
tiff("test0.tiff", width=5, height=10, units="cm", res=300, compression = 'lzw')
grid.newpage()
pushViewport(viewport(layout= Layout))
print(p1 + theme_bw(base_size=5), vp = vplayout(1,1))
print(p2 + theme_bw(base_size=5), vp = vplayout(2,1))
dev.off()
You can use cowplot::plot_grid
# figures
library(cowplot)
tiff("test0.tiff", width=5, height=10, units="cm", res=300, compression = 'lzw')
grid.newpage()
plot_grid(p1, p2, align = "v", nrow = 2, rel_heights = c(1/2, 1/2))
dev.off()
Note: I don't know how you set up df0 so cannot present exported plot.
I have used the method indicated here to align graphs sharing the same abscissa.
But I can't make it work when some of my graphs have a legend and others don't.
Here is an example:
library(ggplot2)
library(reshape2)
library(gridExtra)
x = seq(0, 10, length.out = 200)
y1 = sin(x)
y2 = cos(x)
y3 = sin(x) * cos(x)
df1 <- data.frame(x, y1, y2)
df1 <- melt(df1, id.vars = "x")
g1 <- ggplot(df1, aes(x, value, color = variable)) + geom_line()
print(g1)
df2 <- data.frame(x, y3)
g2 <- ggplot(df2, aes(x, y3)) + geom_line()
print(g2)
gA <- ggplotGrob(g1)
gB <- ggplotGrob(g2)
maxWidth <- grid::unit.pmax(gA$widths[2:3], gB$widths[2:3])
gA$widths[2:3] <- maxWidth
gB$widths[2:3] <- maxWidth
g <- arrangeGrob(gA, gB, ncol = 1)
grid::grid.newpage()
grid::grid.draw(g)
Using this code, I have the following result:
What I would like is to have the x axis aligned and the missing legend being filled by a blank space. Is this possible?
Edit:
The most elegant solution proposed is the one by Sandy Muspratt below.
I implemented it and it works quite well with two graphs.
Then I tried with three, having different legend sizes, and it doesn't work anymore:
library(ggplot2)
library(reshape2)
library(gridExtra)
x = seq(0, 10, length.out = 200)
y1 = sin(x)
y2 = cos(x)
y3 = sin(x) * cos(x)
y4 = sin(2*x) * cos(2*x)
df1 <- data.frame(x, y1, y2)
df1 <- melt(df1, id.vars = "x")
g1 <- ggplot(df1, aes(x, value, color = variable)) + geom_line()
g1 <- g1 + theme_bw()
g1 <- g1 + theme(legend.key = element_blank())
g1 <- g1 + ggtitle("Graph 1", subtitle = "With legend")
df2 <- data.frame(x, y3)
g2 <- ggplot(df2, aes(x, y3)) + geom_line()
g2 <- g2 + theme_bw()
g2 <- g2 + theme(legend.key = element_blank())
g2 <- g2 + ggtitle("Graph 2", subtitle = "Without legend")
df3 <- data.frame(x, y3, y4)
df3 <- melt(df3, id.vars = "x")
g3 <- ggplot(df3, aes(x, value, color = variable)) + geom_line()
g3 <- g3 + theme_bw()
g3 <- g3 + theme(legend.key = element_blank())
g3 <- g3 + scale_color_discrete("This is indeed a very long title")
g3 <- g3 + ggtitle("Graph 3", subtitle = "With legend")
gA <- ggplotGrob(g1)
gB <- ggplotGrob(g2)
gC <- ggplotGrob(g3)
gB = gtable::gtable_add_cols(gB, sum(gC$widths[7:8]), 6)
maxWidth <- grid::unit.pmax(gA$widths[2:5], gB$widths[2:5], gC$widths[2:5])
gA$widths[2:5] <- maxWidth
gB$widths[2:5] <- maxWidth
gC$widths[2:5] <- maxWidth
g <- arrangeGrob(gA, gB, gC, ncol = 1)
grid::grid.newpage()
grid::grid.draw(g)
This results in the following figure:
My main problem with the answers found here and in other questions regarding the subject is that people "play" quite a lot with the vector myGrob$widths without actually explaining why they are doing it. I have seen people modify myGrob$widths[2:5] others myGrob$widths[2:3] and I just can't find any documentation explaining what those columns are.
My objective is to create a generic function such as:
AlignPlots <- function(...) {
# Retrieve the list of plots to align
plots.list <- list(...)
# Initialize the lists
grobs.list <- list()
widths.list <- list()
# Collect the widths for each grob of each plot
max.nb.grobs <- 0
longest.grob <- NULL
for (i in 1:length(plots.list)){
if (i != length(plots.list)) {
plots.list[[i]] <- plots.list[[i]] + theme(axis.title.x = element_blank())
}
grobs.list[[i]] <- ggplotGrob(plots.list[[i]])
current.grob.length <- length(grobs.list[[i]])
if (current.grob.length > max.nb.grobs) {
max.nb.grobs <- current.grob.length
longest.grob <- grobs.list[[i]]
}
widths.list[[i]] <- grobs.list[[i]]$widths[2:5]
}
# Get the max width
maxWidth <- do.call(grid::unit.pmax, widths.list)
# Assign the max width to each grob
for (i in 1:length(grobs.list)){
if(length(grobs.list[[i]]) < max.nb.grobs) {
grobs.list[[i]] <- gtable::gtable_add_cols(grobs.list[[i]],
sum(longest.grob$widths[7:8]),
6)
}
grobs.list[[i]]$widths[2:5] <- as.list(maxWidth)
}
# Generate the plot
g <- do.call(arrangeGrob, c(grobs.list, ncol = 1))
return(g)
}
Expanding on #Axeman's answer, you can do all of this with cowplot without ever needing to use draw_plot directly. Essentially, you just make the plot in two columns -- one for the plots themselves and one for the legends -- and then place them next to each other. Note that, because g2 has no legend, I am using an empty ggplot object to hold the place of that legend in the legends column.
library(cowplot)
theme_set(theme_minimal())
plot_grid(
plot_grid(
g1 + theme(legend.position = "none")
, g2
, g3 + theme(legend.position = "none")
, ncol = 1
, align = "hv")
, plot_grid(
get_legend(g1)
, ggplot()
, get_legend(g3)
, ncol =1)
, rel_widths = c(7,3)
)
Gives
The main advantage here, in my mind, is the ability to set and skip legends as needed for each of the subplots.
Of note is that, if all of the plots have a legend, plot_grid handles the alignment for you:
plot_grid(
g1
, g3
, align = "hv"
, ncol = 1
)
gives
It is only the missing legend in g2 that causes problems.
Therefore, if you add a dummy legend to g2 and hide it's elements, you can get plot_grid to do all of the alignment for you, instead of worrying about manually adjusting rel_widths if you change the size of the output
plot_grid(
g1
, g2 +
geom_line(aes(color = "Test")) +
scale_color_manual(values = NA) +
theme(legend.text = element_blank()
, legend.title = element_blank())
, g3
, align = "hv"
, ncol = 1
)
gives
This also means that you can easily have more than one column, but still keep the plot areas the same. Simply removing , ncol = 1 from above yields a plot with 2 columns, but still correctly spaced (though you'll need to adjust the aspect ratio to make it useable):
As #baptiste suggested, you can also move the legends over so that they are all aligned to the left of in the "legend" portion of the plot by adding theme(legend.justification = "left") to the plots with the legends (or in theme_set to set globally), like this:
plot_grid(
g1 +
theme(legend.justification = "left")
,
g2 +
geom_line(aes(color = "Test")) +
scale_color_manual(values = NA) +
theme(legend.text = element_blank()
, legend.title = element_blank())
, g3 +
theme(legend.justification = "left")
, align = "hv"
, ncol = 1
)
gives
The patchwork package by Thomas Lin Pedersen does this all automagically:
library(patchwork)
g1 + g2 + plot_layout(ncol = 1)
Can hardly get any easier than that.
There might now be easier ways to do this, but your code was not far wrong.
After you have ensured that the widths of columns 2 and 3 in gA are the same as those in gB, check the widths of the two gtables: gA$widths and gB$widths. You will notice that the gA gtable has two additional columns not present in the gB gtable, namely widths 7 and 8. Use the gtable function gtable_add_cols() to add the columns to the gB gtable:
gB = gtable::gtable_add_cols(gB, sum(gA$widths[7:8]), 6)
Then proceed with arrangeGrob() ....
Edit: For a more general solution
Package egg (available on github) is experimental and fragile, but works nicely with your revised set of plots.
# install.package(devtools)
devtools::install_github("baptiste/egg")
library(egg)
grid.newpage()
grid.draw(ggarrange(g1,g2,g3, ncol = 1))
Thanks to this and that, posted in the comments (and then removed), I came up with the following general solution.
I like the answer from Sandy Muspratt and the egg package seems to do the job in a very elegant manner, but as it is "experimental and fragile", I preferred using this method:
#' Vertically align a list of plots.
#'
#' This function aligns the given list of plots so that the x axis are aligned.
#' It assumes that the graphs share the same range of x data.
#'
#' #param ... The list of plots to align.
#' #param globalTitle The title to assign to the newly created graph.
#' #param keepTitles TRUE if you want to keep the titles of each individual
#' plot.
#' #param keepXAxisLegends TRUE if you want to keep the x axis labels of each
#' individual plot. Otherwise, they are all removed except the one of the graph
#' at the bottom.
#' #param nb.columns The number of columns of the generated graph.
#'
#' #return The gtable containing the aligned plots.
#' #examples
#' g <- VAlignPlots(g1, g2, g3, globalTitle = "Alignment test")
#' grid::grid.newpage()
#' grid::grid.draw(g)
VAlignPlots <- function(...,
globalTitle = "",
keepTitles = FALSE,
keepXAxisLegends = FALSE,
nb.columns = 1) {
# Retrieve the list of plots to align
plots.list <- list(...)
# Remove the individual graph titles if requested
if (!keepTitles) {
plots.list <- lapply(plots.list, function(x) x <- x + ggtitle(""))
plots.list[[1]] <- plots.list[[1]] + ggtitle(globalTitle)
}
# Remove the x axis labels on all graphs, except the last one, if requested
if (!keepXAxisLegends) {
plots.list[1:(length(plots.list)-1)] <-
lapply(plots.list[1:(length(plots.list)-1)],
function(x) x <- x + theme(axis.title.x = element_blank()))
}
# Builds the grobs list
grobs.list <- lapply(plots.list, ggplotGrob)
# Get the max width
widths.list <- do.call(grid::unit.pmax, lapply(grobs.list, "[[", 'widths'))
# Assign the max width to all grobs
grobs.list <- lapply(grobs.list, function(x) {
x[['widths']] = widths.list
x})
# Create the gtable and display it
g <- grid.arrange(grobs = grobs.list, ncol = nb.columns)
# An alternative is to use arrangeGrob that will create the table without
# displaying it
#g <- do.call(arrangeGrob, c(grobs.list, ncol = nb.columns))
return(g)
}
One trick is to plot and align the graphs without any legends, and then plotting the legend separately next to it. cowplot has a convenience function for quickly getting the legend from a plot, and plot_grid allows for automatic allignment.
library(cowplot)
theme_set(theme_grey())
l <- get_legend(g1)
ggdraw() +
draw_plot(plot_grid(g1 + theme(legend.position = 'none'), g2, ncol = 1, align = 'hv'),
width = 0.9) +
draw_plot(l, x = 0.9, y = 0.55, width = 0.1, height = 0.5)
Using grid.arrange
library(ggplot2)
library(reshape2)
library(gridExtra)
x = seq(0, 10, length.out = 200)
y1 = sin(x)
y2 = cos(x)
y3 = sin(x) * cos(x)
df1 <- data.frame(x, y1, y2)
df1 <- melt(df1, id.vars = "x")
g1 <- ggplot(df1, aes(x, value, color = variable)) + geom_line()
df2 <- data.frame(x, y3)
g2 <- ggplot(df2, aes(x, y3)) + geom_line()
#extract the legend from the first graph
temp <- ggplotGrob(g1)
leg_index <- which(sapply(temp$grobs, function(x) x$name) == "guide-box")
legend <- temp$grobs[[leg_index]]
#remove the legend of the first graph
g1 <- g1 + theme(legend.position="none")
#define position of each grobs/plots and width and height ratio
grid_layout <- rbind(c(1,3),
c(2,NA))
grid_width <- c(5,1)
grid_heigth <- c(1,1)
grid.arrange(
grobs=list(g1, g2,legend),
layout_matrix = grid_layout,
widths = grid_width,
heights = grid_heigth)
I can plot a UK map with ggmap with a point like this:
library(ggmap)
UK_map <- get_map(location = c(-2.65, 53.7), zoom = 5, maptype = "hybrid")
UK_map <- ggmap(ggmap=UK_map, extent = "device", legend = "right")
UK_map + geom_point(data = data.frame(x = -1.81, y = 55.655), aes(x, y), size = 5)
However, if I try to use Winston Chang's multiplot function, the point disappears.
multiplot <- function(..., plotlist=NULL, cols) {
require(grid)
# Make a list from the ... arguments and plotlist
plots <- c(list(...), plotlist)
numPlots = length(plots)
# Make the panel
plotCols = cols # Number of columns of plots
plotRows = ceiling(numPlots/plotCols) # Number of rows needed, calculated from # of cols
# Set up the page
grid.newpage()
pushViewport(viewport(layout = grid.layout(plotRows, plotCols)))
vplayout <- function(x, y)
viewport(layout.pos.row = x, layout.pos.col = y)
# Make each plot, in the correct location
for (i in 1:numPlots) {
curRow = ceiling(i/plotCols)
curCol = (i-1) %% plotCols + 1
print(plots[[i]], vp = vplayout(curRow, curCol ))
}
}
multiplot(UK_map, UK_map, cols = 2)
Why is the point disappearing and how can I get point to appear when using multiplot?
The multiplot function doesn't know about the point, since you only pass it your UK_map object, which does not include the point. To have it plot the point, you would need to add the geom_point call to assignment of UK_map, like so:
UK_map_with_point <- UK_map +
geom_point(data = data.frame(x = -1.81, y = 55.655), aes(x, y), size = 5)
multiplot(UK_map_with_point, UK_map, cols = 2)
Or, alternatively, add the point on the fly within the call to multiplot:
multiplot(UK_map + geom_point(data = data.frame(x = -1.81, y = 55.655),
aes(x, y), size = 5),
UK_map + geom_point(data = data.frame(x = -2.81, y = 56.655),
aes(x, y), size = 5), cols = 2)