Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ggplot2: Divide Legend into Two Columns, Each with Its Own Title

Tags:

r

legend

ggplot2

I have these factors

require(ggplot2) names(table(diamonds$cut)) # [1] "Fair"      "Good"      "Very Good" "Premium"   "Ideal"  

which I want to visually divide into two groups in the legend (indicating also the group name):

"First group" -> "Fair", "Good" 

and

"Second group" -> "Very Good", "Premium", "Ideal" 

Starting with this plot

ggplot(diamonds, aes(color, fill=cut)) + geom_bar() +    guides(fill=guide_legend(ncol=2)) +   theme(legend.position="bottom") 

I want to get

enter image description here

(note that "Very Good" slipped in the second column/group)

like image 491
CptNemo Avatar asked Jan 06 '15 17:01

CptNemo


2 Answers

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") 

enter image description here

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:

enter image description here

like image 172
eipi10 Avatar answered Sep 21 '22 14:09

eipi10


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) ) 

enter image description here

like image 32
Mark Peterson Avatar answered Sep 20 '22 14:09

Mark Peterson