ggplot2: Divide Legend into Two Columns, Each with Its Own Title
Solution 1:
You can shift the "Very Good" category to the second column of the legend by adding a dummy factor level and setting its colour to white in the legend, so that it can't be seen. In the code below, we add a blank factor level between "Good" and "Very Good", so now we have six levels. Then, we use scale_fill_manual
to set the color of this blank level to "white". drop=FALSE
forces ggplot
to keep the blank level in the legend. There might be a more elegant way to control where ggplot
places the legend values, but at least this will get the job done.
diamonds$cut = factor(diamonds$cut, levels=c("Fair","Good"," ","Very Good",
"Premium","Ideal"))
ggplot(diamonds, aes(color, fill=cut)) + geom_bar() +
scale_fill_manual(values=c(hcl(seq(15,325,length.out=5), 100, 65)[1:2],
"white",
hcl(seq(15,325,length.out=5), 100, 65)[3:5]),
drop=FALSE) +
guides(fill=guide_legend(ncol=2)) +
theme(legend.position="bottom")
UPDATE: I'm hoping there's a better way to add titles to each group in the legend, but the only option I can come up with for now is to resort to grobs, which always gives me a headache. The code below is adapted from the answer to this SO question. It adds two text grobs, one for each label, but the labels have to be positioned by hand, which is a huge pain. The code for the plot also has to be modified to create more room for the legend. In addition, even though I've turned off clipping for all grobs, the labels are still clipped by the legend grob. You can position the labels outside of the clipped area, but then they're too far from the legend. I'm hoping someone who really knows how to work with grobs can fix this and more generally improve upon the code below (@baptiste, are you out there?).
library(gtable)
p = ggplot(diamonds, aes(color, fill=cut)) + geom_bar() +
scale_fill_manual(values=c(hcl(seq(15,325,length.out=5), 100, 65)[1:2],
"white",
hcl(seq(15,325,length.out=5), 100, 65)[3:5]),
drop=FALSE) +
guides(fill=guide_legend(ncol=2)) +
theme(legend.position=c(0.5,-0.26),
plot.margin=unit(c(1,1,7,1),"lines")) +
labs(fill="")
# Add two text grobs
p = p + annotation_custom(
grob = textGrob(label = "First\nGroup",
hjust = 0.5, gp = gpar(cex = 0.7)),
ymin = -2200, ymax = -2200, xmin = 3.45, xmax = 3.45) +
annotation_custom(
grob = textGrob(label = "Second\nGroup",
hjust = 0.5, gp = gpar(cex = 0.7)),
ymin = -2200, ymax = -2200, xmin = 4.2, xmax = 4.2)
# Override clipping
gt <- ggplot_gtable(ggplot_build(p))
gt$layout$clip <- "off"
grid.draw(gt)
And here's the result:
Solution 2:
Using cowplot
, you just need to construct the legends separately and then stitch things back together. It does require using scale_fill_manual
to make sure that the colors match across the plots, and there is lots of room for fiddling with the legend positioning, etc.
Save the colors to use (here, using RColorBrewer
)
cut_colors <-
setNames(brewer.pal(5, "Set1")
, levels(diamonds$cut))
Make the base plot -- without a legend:
full_plot <-
ggplot(diamonds, aes(color, fill=cut)) + geom_bar() +
scale_fill_manual(values = cut_colors) +
theme(legend.position="none")
Make two separate plots, limited to the cuts within the grouping that we want. We are not planning to plot these; we are just going to use the legends that they generate. Note that I am using dplyr
for ease of filtering, but that is not strictly necessary. If you are doing this for more than two groups, it may be worth the effort to use split
and lapply
to generate a list of the plots instead of doing each manually.
for_first_legend <-
diamonds %>%
filter(cut %in% c("Fair", "Good")) %>%
ggplot(aes(color, fill=cut)) + geom_bar() +
scale_fill_manual(values = cut_colors
, name = "First Group")
for_second_legend <-
diamonds %>%
filter(cut %in% c("Very Good", "Premium", "Ideal")) %>%
ggplot(aes(color, fill=cut)) + geom_bar() +
scale_fill_manual(values = cut_colors
, name = "Second Group")
Finally, stitch the plot and the legends together using plot_grid
. Note that I used theme_set(theme_minimal())
before running the plot to get the theme that I personally like.
plot_grid(
full_plot
, plot_grid(
get_legend(for_first_legend)
, get_legend(for_second_legend)
, nrow = 1
)
, nrow = 2
, rel_heights = c(8,2)
)
Solution 3:
Following the idea of @eipi10, you can add the name of the titles as labels, with white
values:
diamonds$cut = factor(diamonds$cut, levels=c("Title 1 ","Fair","Good"," ","Title 2","Very Good",
"Premium","Ideal"))
ggplot(diamonds, aes(color, fill=cut)) + geom_bar() +
scale_fill_manual(values=c("white",hcl(seq(15,325,length.out=5), 100, 65)[1:2],
"white","white",
hcl(seq(15,325,length.out=5), 100, 65)[3:5]),
drop=FALSE) +
guides(fill=guide_legend(ncol=2)) +
theme(legend.position="bottom",
legend.key = element_rect(fill=NA),
legend.title=element_blank())
I introduce some white spaces after "Title 1 "
to separate the columns and improve the design, but there might be an option to increase the space.
The only problem is that I have no idea how to change the format of the "title" labels (I tried bquote
or expression
but it didn't work).
_____________________________________________________________
Depending on the graph you are attempting, a right alignment of the legend might be a better alternative, and this trick looks better (IMHO). It separates the legend into two, and uses the space better. All you have to do is change the ncol
back to 1
, and "bottom"
(legend.position
) to "right"
:
diamonds$cut = factor(diamonds$cut, levels=c("Title 1","Fair","Good"," ","Title 2","Very Good","Premium","Ideal"))
ggplot(diamonds, aes(color, fill=cut)) + geom_bar() +
scale_fill_manual(values=c("white",hcl(seq(15,325,length.out=5), 100, 65)[1:2],
"white","white",
hcl(seq(15,325,length.out=5), 100, 65)[3:5]),
drop=FALSE) +
guides(fill=guide_legend(ncol=1)) +
theme(legend.position="bottom",
legend.key = element_rect(fill=NA),
legend.title=element_blank())
In this case, it might make sense to leave the title in this version, by removing legend.title=element_blank()
Solution 4:
This adds the titles to the legend's gtable. It uses @eipi10's technique for moving the "very good" category into the second column of the legend (thanks).
The method extracts the legend from the plot. The legend's gtable can be manipulated. Here, an additional row is added to the gtable, and the titles are added to the new row. The legend (after a little fine-tuning) is then put back into the plot.
library(ggplot2)
library(gtable)
library(grid)
diamonds$cut = factor(diamonds$cut, levels=c("Fair","Good"," ","Very Good",
"Premium","Ideal"))
p = ggplot(diamonds, aes(color, fill = cut)) +
geom_bar() +
scale_fill_manual(values =
c(hcl(seq(15, 325, length.out = 5), 100, 65)[1:2],
"white",
hcl(seq(15, 325, length.out = 5), 100, 65)[3:5]),
drop = FALSE) +
guides(fill = guide_legend(ncol = 2, title.position = "top")) +
theme(legend.position = "bottom",
legend.key = element_rect(fill = "white"))
# Get the ggplot grob
g = ggplotGrob(p)
# Get the legend
leg = g$grobs[[which(g$layout$name == "guide-box")]]$grobs[[1]]
# Set up the two sub-titles as text grobs
st = lapply(c("First group", "Second group"), function(x) {
textGrob(x, x = 0, just = "left", gp = gpar(cex = 0.8)) } )
# Add a row to the legend gtable to take the legend sub-titles
leg = gtable_add_rows(leg, unit(1, "grobheight", st[[1]]) + unit(0.2, "cm"), pos = 3)
# Add the sub-titles to the new row
leg = gtable_add_grob(leg, st,
t = 4, l = c(2, 6), r = c(4, 8), clip = "off")
# Add a little more space between the two columns
leg$widths[[5]] = unit(.6, "cm")
# Move the legend to the right
leg$vp = viewport(x = unit(.95, "npc"), width = sum(leg$widths), just = "right")
# Put the legend back into the plot
g$grobs[[which(g$layout$name == "guide-box")]] = leg
# Draw the plot
grid.newpage()
grid.draw(g)