问题
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

(note that "Very Good" slipped in the second column/group)
回答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:

回答2:
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)
回答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()
回答4:
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)
)
回答5:
This question is a few years old but there are new packages that appeared since this question was asked that can help here.
1) ggnewscale This CRAN package supplies new_scale_fill
such that any fill geom after it appears gets a separate scale.
library(ggplot2)
library(dplyr)
library(ggnewscale)
cut.levs <- levels(diamonds$cut)
cut.values <- setNames(rainbow(length(cut.levs)), cut.levs)
ggplot(diamonds, aes(color)) +
geom_bar(aes(fill = cut)) +
scale_fill_manual(aesthetics = "fill", values = cut.values,
breaks = cut.levs[1:2], name = "First Grouop:") +
new_scale_fill() +
geom_bar(aes(fill2 = cut)) %>% rename_geom_aes(new_aes = c(fill = "fill2")) +
scale_fill_manual(aesthetics = "fill2", values = cut.values,
breaks = cut.levs[-(1:2)], name = "Second Group:") +
guides(fill=guide_legend(order = 1)) +
theme(legend.position="bottom")
2) relayer The relayer (on github) package allows one to define new aesthetics so here we draw the bars twice, once with a fill
aesthetic and once with a fill2
aesthetic, generating a separate legend for each using scale_fill_manual
.
library(ggplot2)
library(dplyr)
library(relayer)
cut.levs <- levels(diamonds$cut)
cut.values <- setNames(rainbow(length(cut.levs)), cut.levs)
ggplot(diamonds, aes(color)) +
geom_bar(aes(fill = cut)) +
geom_bar(aes(fill2 = cut)) %>% rename_geom_aes(new_aes = c(fill = "fill2")) +
guides(fill=guide_legend(order = 1)) + ##
theme(legend.position="bottom") +
scale_fill_manual(aesthetics = "fill", values = cut.values,
breaks = cut.levs[1:2], name = "First Grouop:") +
scale_fill_manual(aesthetics = "fill2", values = cut.values,
breaks = cut.levs[-(1:2)], name = "Second Group:")
I think the horizontal legend looks a bit better here as it does not take up so much space but if you want two side by side vertical legends use this line in place of the guides
line marked ## :
guides(fill = guide_legend(order = 1, ncol = 1),
fill2 = guide_legend(ncol = 1)) +
来源:https://stackoverflow.com/questions/27803710/ggplot2-divide-legend-into-two-columns-each-with-its-own-title