How to sensibly align two legends when using cowplot in R? - r

A similar question was asked here, however I cant adapt the answer to my issue.
I am trying to correctly align two legends when using cowplot. For example, if I create some data and a cowplot with two legends like so:
library(cowplot)
library(ggplot2)
# create some data
dat <- NULL
for(i in 1:20){
x <- LETTERS[1:5]
y <- paste0("var", seq(1,5))
dat[[i]] <- expand.grid(X=x, Y=y)
dat[[i]]$Z <- runif(25, 0, 1)
}
# plotting function
plotFun <- function(data){
ggplot(data, aes(X, Y, fill= Z)) +
geom_tile() +
theme(aspect.ratio = 1,
legend.justification = c(0,1),
axis.text.x=element_blank(),
axis.ticks.x=element_blank(),
axis.text.y=element_blank(),
axis.ticks.y=element_blank()) +
xlab("") + ylab("")
}
# set up to plot on a grid
allPlots <- lapply(dat, plotFun)
allPlotsAlter <- lapply(allPlots, function(x) x + theme(legend.position = "none"))
n <- length(allPlotsAlter)
nRow <- floor(sqrt(n))
plotGrid <- gridExtra::arrangeGrob(grobs=allPlotsAlter, nrow=nRow)
# create a different type of legend
newPlot <- ggplot(iris, aes(x = Sepal.Length, y = Petal.Length, fill = Species)) +
geom_bar(stat = 'identity') + theme(legend.justification = c(0,1))
# get both legends and combine
legend <- cowplot::get_legend(allPlots[[1]])
legend1 <- cowplot::get_legend(newPlot)
combineLegend <- cowplot::plot_grid(
legend,
legend1,
nrow = 2)
# now make plot
cowplot::plot_grid(plotGrid,
combineLegend,
rel_widths = c(0.9, 0.11),
ncol = 2)
That creates this type of plot:
As you can see, the two legends have quite a bit of vertical space between them and they are not centred with the plot.
Is there a way to align the two legends so they look something like this:
I'm not sure if it is possible using cowplot... or is there a way to maybe use ggplot's annotate to place the legends?

I would probably go for patchwork, as Stefan suggests, but within cowplot you probably need to adjust the legend margins:
theme_margin <- theme(legend.box.margin = margin(100, 10, 100, 10))
legend <- cowplot::get_legend(allPlots[[1]] + theme_margin)
legend1 <- cowplot::get_legend(newPlot + theme_margin)
combineLegend <- cowplot::plot_grid(
legend,
legend1,
nrow = 2)
# now make plot
cowplot::plot_grid(plotGrid,
combineLegend,
rel_widths = c(0.9, 0.11),
ncol = 2)

If switching to another package is an option for you I would suggest to use patchwork to glue your plots together. One feature offered by patchwork is that using plot_spacer you could easily add some empty panels above and below your legends to "move" them to the center and thereby getting rid of the empty space. Depending on your final result or the height of your final plot you probably have to play a bit around with the heights and/or widths arguments:
library(cowplot)
library(ggplot2)
library(patchwork)
set.seed(123)
# create some data
dat <- NULL
for (i in 1:20) {
x <- LETTERS[1:5]
y <- paste0("var", seq(1, 5))
dat[[i]] <- expand.grid(X = x, Y = y)
dat[[i]]$Z <- runif(25, 0, 1)
}
# plotting function
plotFun <- function(data) {
ggplot(data, aes(X, Y, fill = Z)) +
geom_tile() +
theme(
aspect.ratio = 1,
legend.justification = c(0, 1),
axis.text.x = element_blank(),
axis.ticks.x = element_blank(),
axis.text.y = element_blank(),
axis.ticks.y = element_blank()
) +
labs(x = NULL, y = NULL)
}
# set up to plot on a grid
allPlots <- lapply(dat, plotFun)
allPlotsAlter <- lapply(allPlots, function(x) x + theme(legend.position = "none"))
n <- length(allPlotsAlter)
nRow <- floor(sqrt(n))
plotGrid <- wrap_plots(grobs = allPlotsAlter, nrow = nRow)
# create a different type of legend
newPlot <- ggplot(iris, aes(x = Sepal.Length, y = Petal.Length, fill = Species)) +
geom_bar(stat = "identity") +
theme(legend.justification = c(0, 1))
# get both legends and combine
legend <- cowplot::get_legend(allPlots[[1]])
legend1 <- cowplot::get_legend(newPlot)
combineLegend <- plot_spacer() + legend + legend1 + plot_spacer() + plot_layout(ncol = 1, heights = c(.5, 1, 1, .5))
wrap_elements(plotGrid) + combineLegend + plot_layout(widths = c(4, 1))

Related

How to add y axis title for each facet row in ggplot?

I am doing a scatterplot with a facet_grid() like that:
library(ggplot2)
ggplot(df, aes(x, y)) +
geom_point() +
facet_grid(group1 ~ group2)
I want the y axis title y to be in the middle of each row like this (paint solution):
The numbers of facet rows is two in this example because df$group2 has two different values. For my actual use case there may be more than two rows depending on the used facet variable; the y axis title is supposed to be in the middle of each facet row.
Best solution so far is adding spaces which is a mess since using y axis titles of different length shifts the text away from the middle of the rows. It must be with ggplot2, i.e. without the usage of additional packages. I make a package and do not want to rely on/ include too many packages.
Data used here:
df <- data.frame(x= rnorm(100), y= rnorm(100),
group1= rep(0:1, 50), group2= rep(2:3, each= 50))
Without using another package, I felt that the best method would be to build upon the spaces solution you linked in the original question. So I wrote a function to make the label spacing a little bit more robust.
ylabel <- function(label1,label2){
L1 <- nchar(label1)
L2 <- nchar(label2)
scaler <- ifelse(L1 + L2 > 8, 4, 0)
space1 = paste0(rep("",27 - (L1/2)),collapse = " ")
space2 = paste0(rep("",44 - (L1/2 + L2/2) - scaler), collapse = " ")
space3 = paste0(rep("",22 - (L2/2)), collapse = " ")
paste0(space1,label1,space2,label2,space3)
}
Application:
test <- ylabel("automobiles", "trucks")
ggplot(df, aes(x, y)) +
geom_point() +
facet_grid(group1 ~ group2) +
ylab(test)
Still playing around with the scaler parameter, it's not perfect:
test2 <- ylabel("super long label", "a")
ggplot(df, aes(x, y)) +
geom_point() +
facet_grid(group1 ~ group2) +
ylab(test2)
Will continue to refine the function/parameters, but am thinking this will get you close to what you're looking for.
You can copy the axis labels into new grobs in the gtable. Note that although this uses the grid and gtable packages, these are already imported by ggplot2, so this does not add any new dependencies that are not already available and used internally by ggplot.
library(grid)
library(gtable)
g = ggplot(df, aes(x, y)) +
geom_point() +
facet_grid(group1 ~ group2)
gt = ggplot_gtable(ggplot_build(g))
which.ylab = grep('ylab-l', gt$layout$name)
gt = gtable_add_grob(gt, gt$grobs[which.ylab], 8, 3)
gt = gtable_add_grob(gt, gt$grobs[which.ylab], 10, 3)
gt = gtable_filter(gt, 'ylab-l', invert = TRUE) # remove the original axis title
grid.draw(gt)
The above works for OP's example with just two facets. If we want to generalise this for an arbitrary number of facets we can do that simply enough by searching the gtable to see which rows contain y-axes.
gt = ggplot_gtable(ggplot_build(g))
which.ylab = grep('ylab-l', gt$layout$name)
which.axes = grep('axis-l', gt$layout$name)
axis.rows = gt$layout$t[which.axes]
label.col = gt$layout$l[which.ylab]
gt = gtable::gtable_add_grob(gt, rep(gt$grobs[which.ylab], length(axis.rows)), axis.rows, label.col)
gt = gtable::gtable_filter (gt, 'ylab-l', invert = TRUE)
grid::grid.draw(gt)
In the version above, I also use :: to explicitly specify the namespace for the functions from the grid and gtable packages. This will allow the code to work without even loading the additional packages into the search path.
Demonstrating this code with another example with four facet rows:
df <- data.frame(x= rnorm(100), y= rnorm(100),
group1= rep(1:4, 25), group2= rep(1:2, each= 50))
You may consider switching to library(cowplot) for more control
The following code could be added to a function, but I left it long for clarity. Create 4 dataframes and feed them to four plots. Then arrange the plots
library(tidyverse)
df <- data.frame(x= rnorm(100), y= rnorm(100),
group1= rep(0:1, 50), group2= rep(2:3, each= 50))
library(cowplot)
df1 <- df %>%
filter(group2 == 2) %>%
filter(group1 == 0)
df2 <- df %>%
filter(group2 == 3) %>%
filter(group1 == 0)
df3 <- df %>%
filter(group2 == 2) %>%
filter(group1 == 1)
df4 <- df %>%
filter(group2 == 3) %>%
filter(group1 == 1)
plot1 <- ggplot(df1, aes(x, y)) +
geom_point() +
facet_grid(group1 ~ group2)+
xlim(c(-3, 3))+
ylim(c(-3, 2))+
theme(strip.text.y = element_blank(),
axis.title.x = element_blank(),
axis.text.x = element_blank(),
axis.ticks.x = element_blank()
)
plot1
plot2 <- ggplot(df2, aes(x, y)) +
geom_point() +
facet_grid(group1 ~ group2)+
xlim(c(-3, 3))+
ylim(c(-3, 2))+
theme(axis.title.y = element_blank(),
axis.text.y = element_blank(),
axis.ticks.y = element_blank(),
axis.title.x = element_blank(),
axis.text.x = element_blank(),
axis.ticks.x = element_blank()
)
plot2
plot3 <- ggplot(df3, aes(x, y)) +
geom_point() +
facet_grid(group1 ~ group2)+
xlim(c(-3, 3))+
ylim(c(-3, 2))+
theme(strip.text.x = element_blank(),
strip.text.y = element_blank())
plot3
plot4 <- ggplot(df4, aes(x, y)) +
geom_point() +
facet_grid(group1 ~ group2)+
xlim(c(-3, 3))+
ylim(c(-3, 2))+
theme(axis.title.y = element_blank(),
strip.text.x = element_blank(),
axis.text.y = element_blank(),
axis.ticks.y = element_blank())
plot4
plot_grid(plot1, plot2, plot3, plot4)
Here is a version with annotation, using ggplot2 only. It should be scalable.
No messing with grobs. The disadvantage is that the x positioning and the plot margins need to be semi-manually defined and this might not be very robust.
library(ggplot2)
df <- data.frame(x= rnorm(100), y= rnorm(100),
group1= rep(0:1, 50), group2= rep(2:3, each= 50))
## define a new data frame based on your groups, so this is scalable
annotate_ylab <- function(df, x, y, group1, group2, label = "label") {
## make group2 a factor, so you know which column will be to the left
df[[group2]] <- factor(df[[group2]])
lab_df <- data.frame(
## x positioning is a bit tricky,
## I think a moderately robust method is to
## set it relativ to the range of your values
x = min(df[[x]]) - 0.2 * diff(range(df[[x]])),
y = mean(df[[y]]),
g1 = unique(df[[group1]]),
## draw only on the left column
g2 = levels(df[[group2]])[1],
label = label
)
names(lab_df) <- c(x, y, group1, group2, "label")
lab_df
}
y_df <- annotate_ylab(df, "x", "y", "group1", "group2", "y")
ggplot(df, aes(x, y)) +
geom_point() +
geom_text(data = y_df, aes(x, y, label = label), angle = 90) +
facet_grid(group1 ~ group2) +
coord_cartesian(xlim = range(df$x), clip = "off") +
theme(axis.title.y = element_blank(),
plot.margin = margin(5, 5, 5, 20))
y_df_mtcars <- annotate_ylab(mtcars, "mpg", "disp", "carb", "vs", "y")
ggplot(mtcars, aes(mpg, disp)) +
geom_point() +
geom_text(data = y_df_mtcars, aes(mpg, disp, label = label), angle = 90) +
facet_grid(carb ~ vs) +
coord_cartesian(xlim = range(mtcars$mpg), clip = "off") +
theme(axis.title.y = element_blank(),
plot.margin = margin(5, 5, 5, 20))
Created on 2021-11-24 by the reprex package (v2.0.1)

Remove white space in ggarrange, ggplot2

Say I have three SQUARE plots p1 p2 p3 made from the ggplot2 package. I applied ggarrange command under ggpubr package to make a 1*3 plot after that. Here are the commands:
library(ggplot2)
library(ggpubr)
library(gridExtra)
data <- data.frame(matrix(seq(1, 30, 1), 2))
for(i in 1:5){
plot1 <- ggplot(data = data, aes(y = data[, (i*3-2)])) +
theme(aspect.ratio = 1)
plot2 <- ggplot(data = data, aes(y = data[, (i*3-1)])) +
theme(aspect.ratio = 1)
plot3 <- ggplot(data = data, aes(y = data[, (i*3)])) +
theme(aspect.ratio = 1)
p <- ggarrange(plot1, plot2, plot3,
ncol = 3, common.legend = TRUE, legend = "bottom")
grid.arrange(p, top = paste(colnames(data)[i]), heights = c(1, 1))
}
The rmarkdown output (below) shows a large white space below the arranged plots:
Is there any way to remove it? What I thought is to change the default size of canvas..

Decrease margins between plots when using cowplot

I would like to combine some graphs together using cowplot. But I cannot change the margin sizes. I want to use only one y-axes, but than the margin is still quite large, which I want to decrease. I have used the plot.margin code from ggplot, although that works when I look at the single plot, it doesn't seem to work when the plots are combined.
I have made some example code:
library(ggplot2)
library(cowplot)
x <- c("a", "b")
y1 <- c(3,6)
y2 <- c(10,15)
data1 <- data.frame(x,y1)
data2 <- data.frame(x, y2)
ylab1 <- ylab("Very nice y values")
xlab1 <- xlab("Very nice factors")
plot1 <- ggplot(data1, aes(x=x, y = y1)) +
geom_bar(stat ="identity", position=position_dodge(), fill = "grey")+
theme(plot.margin = unit(c(0.5,0.5,0.5,0.5), "cm")) + xlab1 + ylab1
plot1
ylab2 <- ylab("")
xlab2 <- xlab("Very nice factors")
plot2 <- ggplot(data2, aes(x=x, y = y2)) +
geom_bar(stat = "identity",position=position_dodge(), fill = "grey")+
theme(plot.margin = unit(c(0.5,0.5,0.5,-0.5), "cm")) + xlab2 + ylab2
plot2
plot3 <- plot_grid(plot1, plot2, labels = c("A", "B"), align = "hv",nrow = 1, ncol = 2)
plot3 # Quite large margin between the two plots
I am aware that I could avoid this problem by using facets, however my real plot is rather more complicated than this graph.
Increasing the space between plots in plot_grid was also addressed in this issue.
An extra interesting solution is the one suggested in this comment - try to add an extra empty plot between the two plots and adjust the relative columns widths:
plot4 <- plot_grid(plot1, NULL, plot2, rel_widths = c(1, 0, 1), align = "hv",
labels = c("A", "B"), nrow = 1)
plot4
Can even try negative values in rel_widths, which gives better results:
plot5 <- plot_grid(plot1, NULL, plot2, rel_widths = c(1, -0.1, 1), align = "hv",
labels = c("A", "B"), nrow = 1)
plot5
So, try a combination of adjusting the plot.margin (as answered by #J.Con) and adding an extra empty plot with tweaking rel_widths.
EDIT 2019-12-11
Also check out this comment of the author of cowplot (Claus Wilke):
For those kinds of problems I would now recommend the patchwork library. It's inherently difficult with plot_grid(), due to its underlying design
So, a fast example with patchwork based on their vignette Adding Annotation and Style goes like this:
library(patchwork)
plot3 <- plot1 + plot2 +
plot_annotation(tag_levels = 'A') &
theme(plot.tag = element_text(size = 8))
plot3
Created on 2019-12-11 by the reprex package (v0.3.0)
Your plot.margins were actually working against you. Set them to zero to fill up that white space.
plot1 <- ggplot(data1, aes(x=x, y = y1)) +
geom_bar(stat ="identity", position=position_dodge(), fill = "grey")+
theme(plot.margin = unit(c(0,0,0,0), "cm")) + xlab1 + ylab1
plot1
ylab2 <- ylab("")
xlab2 <- xlab("Very nice factors")
plot2 <- ggplot(data2, aes(x=x, y = y2)) +
geom_bar(stat = "identity",position=position_dodge(), fill = "grey")+
theme(plot.margin = unit(c(0,0,0,0), "cm")) + xlab2 + ylab2
plot2
plot3 <- plot_grid(plot1, plot2, labels = c("A", "B"), align = "hv",nrow = 1, ncol = 2)
plot3

Annotate below a ggplot2 graph

I would like plot a line below a ggplot2 graph with text above it, something like this:
where the starting and ending point of the gene on the x axis can be specified.
My attempt so far:
require(ggplot2)
require(grid)
require(gridExtra)
data = data.frame(y = -log10(runif(100)), x = 1:100)
p = ggplot(data=data, aes(x, y)) + geom_point()
p = p + theme(plot.margin=unit(c(1, 1, 5, 1), "lines"))
t1 = textGrob("Gene1")
p1 = p + annotation_custom(grob=t1, xmin=0, ymin=0, xmax = 3, ymax=-.1)
print(p1)
which gives:
If I try to move the text down by adjusting ymax, then it disappears.
In my answer, I have changed a couple things:
1 - I changed the name of your data to "df", as "data" can cause confusion between objects and arguments.
2 - I removed the extra panel space around the main data plot, so that the annotation wasn't so far away.
require(ggplot2)
require(grid)
require(gridExtra)
# make the data
df <- data.frame(y = -log10(runif(100)), x = 1:100)
p <- ggplot(data=df, aes(x, y)) + geom_point()
# remove this line of code:
# p <- p + theme(plot.margin=unit(c(1, 1, 5, 1), "lines"))
# set up the plot theme for the annotation
blank_axes_and_thin_margin <- theme(axis.text = element_text(color="white"),
axis.title = element_text(color="white"),
axis.ticks = element_blank(),
panel.grid = element_blank(),
panel.border = element_blank(),
plot.margin=unit(c(0, 2, 0,2),"mm"))
# define the position of the arrow (you would change this part)
arrow_start <- min(df$x)
arrow_end <- mean(c(min(df$x), max(df$x)))
arrow_height <- 1
# here's the rectangle with the arrow
t2 <- ggplot(df, aes(x,y))+
theme_bw()+
geom_rect(aes(xmin=min(x), xmax = max(x)),
ymin=0, ymax=4,fill="gray50")+
coord_cartesian(ylim=c(0,4))+
annotate(geom="text", label="Gene1",
x=20, y=2, size=6, color="black")+
geom_segment(x=arrow_start, xend=arrow_end,
y=arrow_height, yend=arrow_height,
color="black", arrow=arrow(ends="both"))+
blank_axes_and_thin_margin
t2
# arrange the graphic objects here
# I use arrangeGrob because it allows you to use ggsave(), unlike grid.arrange
plot_both <- arrangeGrob(p, t2, nrow=2, heights=unit(c(0.75,0.25), "null"))
plot_both
# ta-da !
you can turn clipping off,
g <- ggplotGrob(p1)
g$layout$clip[g$layout$name == "panel"] <- "off"
grid.newpage()
grid.draw(g)

Top to bottom alignment of two ggplot2 figures

I realize that the align.plots function from the ggExtra package has been deprecated and removed. However, I am using my own version as it seems to provide the specific functionality I need. I have looked into faceting to solve my problem but I don't think it will work for my particular issue. What seems to be the problem is that the top-to-bottom images don't align when I use coord_equal on one of them. This doesn't seem to affect left-to-right though. Here is a simplified (or at least as simple as I can make it) version of what I am trying to achieve.
Create some dummy data frames:
source('https://raw.github.com/jbryer/multilevelPSA/master/r/align.R')
require(psych)
df = data.frame(x=rnorm(100, mean=50, sd=10),
y=rnorm(100, mean=48, sd=10),
group=rep(letters[1:10], 10))
dfx = describe.by(df$x, df$group, mat=TRUE)[,c('group1', 'mean', 'n', 'min', 'max')]
names(dfx) = c('group', 'x', 'x.n', 'x.min', 'x.max')
dfy = describe.by(df$y, df$group, mat=TRUE)[,c('group1', 'mean', 'n', 'min', 'max')]
names(dfy) = c('group', 'y', 'y.n', 'y.min', 'y.max')
df2 = cbind(dfx, dfy[,2:ncol(dfy)])
range = c(0,100)
This will setup the three plots:
p1a = ggplot(df2, aes(x=x, y=y, colour=group)) + geom_point() +
opts(legend.position='none') +
scale_x_continuous(limits=range) + scale_y_continuous(limits=range)
p1 = p1a + coord_equal(ratio=1)
p2 = ggplot(df, aes(x=x, y=group, colour=group)) + geom_point() +
scale_x_continuous(limits=range) + opts(legend.position='none')
p3 = ggplot(df, aes(x=group, y=y, colour=group)) + geom_point() +
scale_y_continuous(limits=range) + opts(legend.position='none')
The alignment top to bottom does not work with coord_equal
grid_layout <- grid.layout(nrow=2, ncol=2, widths=c(1,2), heights=c(2,1))
grid.newpage()
pushViewport( viewport( layout=grid_layout, width=1, height=1 ) )
align.plots(grid_layout, list(p1, 1, 2), list(p3, 1, 1), list(p2, 2, 2))
Broken Plot http://bryer.org/alignplots1.png
The fix is to add respect=TRUE to the grid.layout call:
grid_layout <- grid.layout(nrow=2, ncol=2, widths=c(1,2), heights=c(2,1), respect=TRUE)
But if I don't use coord_equal the alignment works fine:
grid_layout <- grid.layout(nrow=2, ncol=2, widths=c(1,2), heights=c(2,1))
grid.newpage()
pushViewport( viewport( layout=grid_layout, width=1, height=1 ) )
align.plots(grid_layout, list(p1a, 1, 2), list(p3, 1, 1), list(p2, 2, 2))
Working Plot http://bryer.org/alignplots2.png
ggplot2 now has ggplotGrob(), which may help with this.
First, we need to update the code used to generate the plots:
p1a = ggplot(df2, aes(x=x, y=y, colour=group)) + geom_point() +
scale_x_continuous(limits=range) + scale_y_continuous(limits=range)
p1 = p1a + coord_equal(ratio=1) + theme_minimal() + theme(legend.position='none')
p2 = ggplot(df, aes(x=x, y=group, colour=group)) + geom_point() +
scale_x_continuous(limits=range) + theme_minimal() + theme(legend.position='none')
p3 = ggplot(df, aes(x=group, y=y, colour=group)) + geom_point() +
scale_y_continuous(limits=range) + theme_minimal() + theme(legend.position='none')
p4 <- ggplot(df, aes(x = group, y = y)) +
geom_blank() +
theme(line = element_blank(),
rect = element_blank(),
text = element_blank(),
title = element_blank())
p4 will be blank; we just need the grob to draw.
Then we load the grid package and draw the grobs in a list arranged in rows and columns using cbind() and rbind().
library(grid)
grid.newpage()
grid.draw(
cbind(
rbind(ggplotGrob(p3), ggplotGrob(p4), size = "first"),
rbind(ggplotGrob(p1), ggplotGrob(p2), size = "first"),
size = "first"))
I'm not sure if this method will let you plot p3 in a different width and p2 in a different height, as you have them in the original example; I normally need a grid of similarly-sized graphs, and haven't needed to figure out different sizes.
This answer is partially based on partially based on https://stackoverflow.com/a/17463184/393354
Here is an example:
m <- matrix(c(3, 1, 0, 2), 2, byrow = T)
lay <- gglayout(m, widths = c(1, 3), heights = c(3, 1))
ggtable(p1, p2, p3, layout = lay)
you can use this by
install.packages('devtools')
library(devtools)
dev_mode()
install_github("ggplot2", "kohske", "cutting-edge")
library(ggplot2)
note that this branch is experimental, so maybe there are bugs.
To solve the problem using the align.plots method, specify respect=TRUE on the layout call:
grid_layout <- grid.layout(nrow=2, ncol=2, widths=c(1,2), heights=c(2,1), respect=TRUE)

Resources