Reputation: 79
I would like to know if it's possible to manually position the labels in a legend in ggplot?
My example is this: I have a data on countries, and I'm doing a 100% stacked bar for each continent so I have:
dt <- data.table(continent = c(rep('Africa', 2), rep('Asia', 3), rep('Europe', 4)),
country = c('Nigeria', 'Kenya',
'China', 'India', 'Japan',
'Germany', 'Sweden', 'Spain', 'Croatia'),
value = runif(9, 0, 10),
number=(1:9))
ggplot(data=dt,
aes(x = continent, y = value, fill = as.factor(number))) +
geom_bar(stat = "identity", position = "fill", color='white', width=0.3 ) +
labs(x = '', y = 'Percentage') +
scale_y_continuous(expand=c(0,0)) +
scale_fill_manual('Country',
labels = dt[, country],
values = (grDevices::colorRampPalette(c('#BB16A3', '#f8e7f5')))(9)) +
theme(legend.position='bottom', aspect.ratio = 1) +
guides(fill = guide_legend(title.position="top", title.hjust = 0.5, reverse=T)) +
coord_flip()
So my question is, is it possible to re-position the labels in the legend so that countries of each continent are in a separate column? Or a separate row?
Thanks!
Upvotes: 4
Views: 1180
Reputation: 29085
I notice you have a different number of countries for each continent. ggplot()
can fill a legend matrix by row or by column, but I've never seen a jagged matrix with different number of cells in each row / column.
It's possible to hack something that looks like a jagged legend matrix, though. Here are some implementations. You may wish to tweak the parameters if you want to sort the continent / country labels in a specific order, or vary the spacing between legend keys, etc.
Prep work:
# define fill mapping so that it can be re-used for both top plot & legend
scale_fill_country <-
scale_fill_manual(labels = dt[, country],
values = (grDevices::colorRampPalette(c('#BB16A3', '#f8e7f5')))(9))
# create top plot (without any legend)
gg.plot <- ggplot(data = dt,
aes(x = continent, y = value, fill = as.factor(number))) +
#note: geom_col is equivalent to geom_bar(stat = "identity")
geom_col(position = "fill", color='white', width=0.3 ) +
labs(x = '', y = 'Percentage') +
scale_y_continuous(expand=c(0,0)) +
scale_fill_country +
theme_classic() +
theme(legend.position = "none") +
coord_flip()
Modify data source for legend:
library(dplyr)
dt.legend <- dt %>%
# pad with empty rows so that there are equal number of countries under
# each continent
group_by(continent) %>%
arrange(country) %>%
mutate(country.id = seq(1, n())) %>%
ungroup() %>%
tidyr::complete(continent, country.id, fill = list(country = " ")) %>%
# make each empty row distinct (within the same continent), & sort them
# after the original rows
rowwise() %>%
mutate(country = ifelse(country == " ",
paste0(rep.int(" ", country.id), collapse = ""),
country)) %>%
ungroup() %>%
mutate(country = forcats::fct_reorder(country, country.id))
> dt.legend
# A tibble: 12 x 5
continent country.id country value number
<chr> <int> <fct> <dbl> <int>
1 Africa 1 Kenya 2.02 2
2 Africa 2 Nigeria 7.17 1
3 Africa 3 " " NA NA
4 Africa 4 " " NA NA
5 Asia 1 China 3.21 3
6 Asia 2 India 5.59 4
7 Asia 3 Japan 9.31 5
8 Asia 4 " " NA NA
9 Europe 1 Croatia 0.0131 9
10 Europe 2 Germany 0.0775 6
11 Europe 3 Spain 3.98 8
12 Europe 4 Sweden 0.703 7
Version 1: each continent in one row, labels below legend key (add axis.text.y = element_blank()
to theme()
if you don't want the continent label associated with each row to be shown)
gg.legend.rows1 <- ggplot(data = dt.legend,
aes(x = country, y = continent,
fill = as.factor(number))) +
geom_tile(color = "white", size = 2) +
facet_wrap(~ continent, scales = "free", ncol = 1) +
scale_y_discrete(expand = c(0, 0)) +
scale_fill_country +
theme_minimal() +
theme(axis.title = element_blank(),
strip.text = element_blank(),
panel.grid = element_blank(),
legend.position = "none")
cowplot::plot_grid(gg.plot, gg.legend.rows1,
ncol = 1,
rel_heights = c(1, 0.3))
Version 2: each continent in one row, labels to the right of legend key (I couldn't think of a way to get the continent labels in as well for this approach, but I don't think that was required in the question anyway...)
gg.legend.rows2 <- ggplot(data = dt.legend,
aes(x = "", y = country, fill = as.factor(number))) +
geom_tile() +
scale_y_discrete(position = "right", expand = c(0, 0)) +
facet_wrap(~ interaction(continent, country, lex.order = TRUE),
scales = "free") +
scale_fill_country +
theme_minimal() +
theme(axis.title = element_blank(),
axis.text.x = element_blank(),
strip.text = element_blank(),
panel.grid = element_blank(),
panel.spacing = unit(0, "pt"),
legend.position = "none")
cowplot::plot_grid(gg.plot, gg.legend.rows2,
axis = "l", align = "v",
ncol = 1,
rel_heights = c(1, 0.2))
Version 3: each continent in one column, labels to the right of legend key (add axis.text.x = element_blank()
to theme()
if you don't want the continent label associated with each column to be shown)
gg.legend.columns <- ggplot(data = dt.legend,
aes(x = continent, y = forcats::fct_rev(country),
fill = as.factor(number))) +
geom_tile(color = "white", size = 2) +
facet_wrap(~ continent, scales = "free", nrow = 1) +
scale_x_discrete(position = "top", expand = c(0, 0)) +
scale_y_discrete(position = "right", expand = c(0, 0)) +
scale_fill_country +
theme_minimal() +
theme(axis.title = element_blank(),
strip.text = element_blank(),
panel.grid = element_blank(),
legend.position = "none")
cowplot::plot_grid(gg.plot, gg.legend.columns,
axis = "l", align = "v",
ncol = 1,
rel_heights = c(1, 0.3))
Upvotes: 5