I have three different subplotts, each with their own legend. I want to combine each of these 3 legends into one common legend at the bottom of the plot. I have found many similar questions combining the legends of different sub plots into one common legend when all the subplots had the same legend. Yet, not when the legends are different. Attempts to change the code were not succesful.
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))
}
data = read.table("fermentation_run.csv", header=TRUE, sep=",", fileEncoding="UTF-8-BOM")
p1 <- ggplot(data, aes(x = time)) +
geom_line(aes(y = cdw*5, colour = "CDW"), size=1) +
geom_line(aes(y = glucose, colour = "glucose"), size=1) +
geom_step(aes(y = substrate, colour = "substrate"), size=1) +
theme_classic() + ylab("Concentration (g/l)") +
xlab("Time (h)") +
scale_colour_manual(values = c("grey", "red", "black"))
theme(legend.position="bottom", legend.title=element_blank())
p2 <- ggplot(data, aes(x=time)) +
geom_line(aes(y = alkyl, colour = "alkyl SS"), size=1) +
geom_line(aes(y = oleyl, colour = "oleyl alcohol"), size=1) +
theme_classic() +
xlab("Time (h)") +
ylab("Concentration (g/l)") +
scale_colour_manual(values = c("green", "blue"))
theme(legend.position="bottom", legend.title=element_blank())
p3 <- ggplot(data, aes(x=time)) +
geom_step(aes(y = aeration, colour="aeration"), size=1) +
geom_line(aes(y = do/2, colour="dissolved oxygen"), size=1) +
theme_classic() +
xlab("Time (h)") +
ylab("Aeration (lpm)") +
scale_y_continuous(sec.axis = sec_axis(~.*2, name = "Dissolved oxygen (%)")) +
theme(legend.position="bottom", legend.title=element_blank())
grid_arrange_shared_legend(p1, p2,p3)
This returns only the legend of the first plot and not of the three plots combined.
I think the key is to add all the legends in your first plot. To achieve this, you could add some fake rows in your data and label them according to your legends for all plots. Let's assume those legends are "a", "b", "c", "d", "e", and "f" in the following:
library(tidyverse)
# insert several rows with values outside your plot range
data <- add_row(mtcars,am=c(2, 3, 4, 5), mpg = 35, disp = 900)
data1<-data %>%
mutate (
by1 = factor(am, levels = c(0, 1, 2, 3, 4, 5),
labels = c("a", "b","c","d", "e","f")))
p1 <- ggplot(data1, aes(x = mpg, y=disp, col=by1)) +
geom_point() +
ylim(50,500)
You will get all the legends you need, and grid_arrange_shared_legend(p1, p2,p3) will pick up this. As you can see only "a" and "b" are for the first plot, and the rest are for other plots.
I don't have your data so I'll illustrate it with some basic datasets. The method isn't perfect with respect to some whitespace around the legends, but maybe someone in the comments knows a solution.
The answer I'm proposing is getting dirty with gtables and patchwork and internal functions thereof.
library(ggplot2)
library(grid)
library(patchwork) #https://github.com/thomasp85/patchwork
# Make plots as usual
g1 <- ggplot(iris, aes(Sepal.Width, Sepal.Length)) +
geom_point(aes(colour = Species))
g2 <- ggplot(mtcars, aes(mpg, disp)) +
geom_point(aes(colour = as.factor(cyl)))
# specify a legend position and a orientation for plots
position <- "bottom"
orientation <- "vertical"
# Add as many plots as you want to this list
plots <- list(g1, g2)
# Grab legends from plots in list
legends <- lapply(plots, function(p) {
p <- ggplotGrob(p + theme(legend.position = position))$grobs
p[[which(sapply(p, function(x) x$name) == "guide-box")]]
})
# Combine the legends
legend <- switch(position,
"bottom" = do.call(gtable:::cbind.gtable, legends),
"right" = do.call(gtable:::rbind.gtable, legends))
# Now make versions of the plots without the legend
stripped <- lapply(plots, function(p) p + theme(legend.position = "none"))
# Combine all the plots
stripped <- switch(orientation,
"horizontal" = do.call(patchwork:::ggplot_add.ggplot, stripped),
"vertical" = do.call(patchwork:::`/.ggplot`, stripped))
# Combine plots with legend
out <- switch(position,
"bottom" = stripped / legend,
"right" = stripped + legend)
out
Created on 2019-08-17 by the reprex package (v0.3.0)
If the whitespace really is a problem, you could supply a plot layout, but this would have to be a manual judgement to make:
out + plot_layout(heights = c(1,1,0.2))
Related
There might be a simple way to do this, but I am not sure what it is. I am trying to make it so that the text in the legend matches up with the color box next to it. I have been trying to do this for a while and have not found a way to use the element_text function to add multiple colors to the legend. I've had no problem making every label the same color, but is there a way to make each legend label a different color?
data<-data.frame(count=c(39,36,19,6), category=c("a","b","c","d"))
data$fraction = data$count / sum(data$count)
data = data[order(data$fraction), ]
data$ymax = cumsum(data$fraction)
data$ymin = c(0, head(data$ymax, n=-1))
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#Create Plot
fill <- c("blue3","cyan3","darkgrey","forestgreen")
library(ggplot2)
p1 = ggplot(data, aes(fill=category, ymax=ymax, ymin=ymin, xmax=4, xmin=3.5)) +
geom_rect(colour="White") +
coord_polar(theta="y") +
scale_fill_manual(values=fill)+
theme_bw()+
geom_label(aes(label=paste(data$fraction*100,"%"),x=4,y=
(ymin+ymax)/2),inherit.aes = F)+
theme(panel.grid=element_blank())+
theme(axis.ticks=element_blank()) +
xlim(c(0, 4)) +
theme(axis.text=element_blank()) +
theme(legend.text=element_text(color=fill,size=12))+
theme(legend.key.size=unit(2,'lines'))+
theme(legend.key=element_rect(size=5))+
labs(title="donut plot")
print(p1)
With a couple of modifications to this answer, match-legend-text-color-in-geom-text-to-symbol, you get what you want. But note, the answer uses grid's editing functions.
# Your data and plot
data<-data.frame(count=c(39,36,19,6), category=c("a","b","c","d"))
data$fraction = data$count / sum(data$count)
data = data[order(data$fraction), ]
data$ymax = cumsum(data$fraction)
data$ymin = c(0, head(data$ymax, n=-1))
fill <- c("blue3","cyan3","darkgrey","forestgreen")
library(ggplot2)
p1 = ggplot(data, aes(fill=category, ymax=ymax, ymin=ymin, xmax=4, xmin=3.5)) +
geom_rect(colour="White") +
coord_polar(theta="y") +
scale_fill_manual(values=fill)+
theme_bw()+
geom_label(aes(label=paste(data$fraction*100,"%"),x=4,y=
(ymin+ymax)/2),inherit.aes = F)+
theme(panel.grid=element_blank())+
theme(axis.ticks=element_blank()) +
xlim(c(0, 4)) +
theme(axis.text=element_blank()) +
theme(legend.text=element_text(color=fill,size=12))+
theme(legend.key.size=unit(2,'lines'))+
theme(legend.key=element_rect(size=5))+
labs(title="donut plot")
# Get the ggplot grob
g <- ggplotGrob(p1)
# Check out the grobs
library(grid)
grid.ls(grid.force(g))
Look through the list of grobs. The grobs you want to edit are towards the bottom of the list, in the 'guide-box' set of grobs - with names that begin with "label". There are four grobs:
label-3-3.4-4-4-4
label-4-3.5-4-5-4
label-5-3.6-4-6-4
label-6-3.7-4-7-4
# Get names of 'label' grobs.
names.grobs <- grid.ls(grid.force(g))$name
labels <- names.grobs[which(grepl("^label", names.grobs))]
# Edit the 'label' grobs - change their colours
# Use the `editGrob` function
for(i in seq_along(labels)) {
g <- editGrob(grid.force(g), gPath(labels[i]), grep = TRUE,
gp = gpar(col = fill[i]))
}
# Draw it
grid.newpage()
grid.draw(g)
It's possible to achieve this without editing grobs, by using the ggtext package. Specify the legend text labels as element_markdown and wrap them in <span> tags that use the colors you want.
data<-data.frame(count=c(39,36,19,6), category=c("a","b","c","d"))
data$fraction = data$count / sum(data$count)
data = data[order(data$fraction), ]
data$ymax = cumsum(data$fraction)
data$ymin = c(0, head(data$ymax, n=-1))
fill <- c("blue3","cyan3","darkgrey","forestgreen")
library(ggplot2)
library(ggtext)
ggplot(data, aes(fill=category, ymax=ymax, ymin=ymin, xmax=4, xmin=3.5)) +
geom_rect(colour="White") +
coord_polar(theta="y") +
scale_fill_manual(labels = paste("<span style='color:",
fill,
"'>",
unique(data$category),
"</span>"),
values = fill)+
theme_bw()+
geom_label(aes(label=paste(data$fraction*100,"%"),x=4,y=
(ymin+ymax)/2),inherit.aes = F)+
theme(panel.grid=element_blank())+
theme(axis.ticks=element_blank()) +
xlim(c(0, 4)) +
theme(axis.text=element_blank()) +
theme(legend.text=element_markdown(size=12))+
theme(legend.key.size=unit(2,'lines'))+
theme(legend.key=element_rect(size=5))+
labs(title="donut plot")
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
I have been using the plot_grid command from cowplot to arrange my plots. I use the labeling feature, and my plots all look the same in that regard. However, when I 'hv' align some plots that have very different y-axis limits, such as the one below, it appears the height of the plot with shortest range of y is used.
If I just 'v' align the plot it looks better in some respects, but it is hard to resize the plot and have the labels looking good. I'd prefer the plot height not consider the x-axis labels, etc, like above.
Using gtables, I can get the desired width/height (below), but these leaves me without the consistent labels across all the figures in a document. Can I use the 'hv' alignment with cowplot and specify which plot height to use?
library(ggplot2)
library(dplyr)
library(scales)
library(grid)
library(cowplot)
data(iris)
iris <- iris %>% mutate(Petal.Width2 = ifelse(Species == "setosa", Petal.Width * 75, Petal.Width))
p1 <- ggplot(data=iris, aes(x = factor(Species), y=Sepal.Width)) +
geom_bar(stat="identity") +
labs(x = NULL, y = "Plot One") +
scale_y_continuous(labels = percent) +
theme(axis.text.x = element_blank(),
axis.title.y = element_text(vjust=1), plot.margin=unit(c(2,2,0,2),"mm"))
p2 <- ggplot(data=iris, aes(x = factor(Species), y=Petal.Width2)) + geom_bar(stat="identity") +
labs(x = NULL, y = "Plot Two") +
scale_y_continuous(labels = percent) +
theme(axis.text.x = element_blank(),
axis.title.y = element_text(vjust=1), plot.margin=unit(c(0,2,0,2),"mm"))
p3 <- ggplot(data=iris, aes(x = factor(Species), y=Petal.Length*0+.01)) + geom_bar(stat="identity") +
labs(x = "SPECIES", y = "The Third plot") +
scale_y_continuous(labels = percent) +
theme( axis.title.y = element_text(vjust=1, color="blue"), plot.margin=unit(c(0,2,0,2),"mm"),
axis.text.x = element_text(angle = 90, hjust=1, vjust=1,face ="italic", size=10))
plot_grid(p1,p2,p3,ncol=1, align="v", labels=c("A", "B", "C"))
# https://stackoverflow.com/a/27408589/1670053
plots <- list(p1, p2, p3)
grobs = lapply(plots, ggplotGrob)
g = do.call(rbind, c(grobs, size="first"))
g$widths = do.call(unit.pmax, lapply(grobs, "[[", "widths"))
grid.newpage()
grid.draw(g)
it's easy as to add labels,
plots <- list(p1, p2, p3)
grobs = lapply(plots, ggplotGrob)
library(gridExtra)
g = do.call(rbind, grobs) # uses gridExtra::rbind.gtable
panels <- g$layout[g$layout$name=="panel",]
g <- gtable::gtable_add_grob(g, lapply(LETTERS[1:nrow(panels)],
textGrob, vjust=1, y=1,
gp=gpar(fontface=2)),
t=panels$t, l=2)
grid.newpage()
grid.draw(g)
I have made a plot like the one described by the code below resulting in the posted image. I can not figure out how to set the entire background to the same "grey80"-color I have used when defining the subplots. Ie. I want to color the white areas between the plots and on the sides of the legend in the same color.
Is this possible to achieve, perhaps with some fancy gridgrob-magic?
This question is similar to change the background color of grid.arrange output but I would like a solution without using the png() function, if possible
library(ggplot2)
library(gridExtra)
library(ggthemes)
library(grid)
p1 <- ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width,
colour = Species)) +
ggtitle('Sepal') +
geom_point() + theme_bw() +
theme(rect = element_rect(fill = 'grey80'))
p2 <- ggplot(iris, aes(x = Petal.Length, y = Petal.Width,
colour = Species)) +
ggtitle('Petal') +
geom_point() + theme_bw() +
theme(rect = element_rect(fill = 'grey80'))
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))
}
grid_arrange_shared_legend(p1,p2)
upgrade comment
You can do this by adding a grey background to the graphics window and
then adding the plots on top. As your legend function uses grid.arrange this generates a newpage; so either add newpage=FALSE or change to arrangeGrob to your function.
Your example
library(ggplot2)
library(gridExtra)
library(ggthemes)
library(grid)
p1 <- ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, colour = Species)) +
ggtitle('Sepal') +
geom_point() + theme_bw() +
# by adding colour=grey removes the white border of the plot and
# so removes the lines between the plots
# add panel.background = element_rect(fill = "grey80")
# if you want the plot panel grey aswell
theme(plot.background=element_rect(fill="grey80", colour="grey80"),
rect = element_rect(fill = 'grey80'))
p2 <- ggplot(iris, aes(x = Petal.Length, y = Petal.Width, colour = Species)) +
ggtitle('Petal') +
geom_point() + theme_bw() +
theme(plot.background=element_rect(fill="grey80", colour="grey80"),
rect = element_rect(fill = 'grey80'))
Tweal your function
# Change grid.arrange to arrangeGrob
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)
arrangeGrob( # change here
do.call(arrangeGrob, lapply(plots, function(x)
x + theme(legend.position="none"))),
legend,
ncol = 1,
heights = unit.c(unit(1, "npc") - lheight, lheight))
}
Plot
grid.draw(grobTree(rectGrob(gp=gpar(fill="grey80", lwd=0)),
grid_arrange_shared_legend(p1,p2)))
Which gives
I think you could take advantage of the ggdraw() function from the cowplot package as showed here.
In your case, you would just need to add plot.background = element_rect(fill="grey80", color = NA) in the theme() of each plot p1 and p2, stitch them together with your function grid_arrange_shared_legend() and then call ggdraw() on its output:
g <- grid_arrange_shared_legend(p1,p2)
g2 <- cowplot::ggdraw(g) +
# same plot.background should be in the theme of p1 and p2 as mentioned above
theme(plot.background = element_rect(fill="grey80", color = NA))
plot(g2)
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)