I have a legend for a ggplot chart that spans two rows with several short values and one long value: legend over two rows
Currently legend 'fills' by column then by row, causing the wrapping to mess with the proportion. Is there a way to have it 'fill' by row first? So it reads like so:
Strongly disagree | Disagree | Slightly disagree |
Slightly agree | Agree | Strongly agree |
No opportunities to observe/Prefer not to say/I dont know
or even
Strongly disagree | Disagree | Slightly disagree | Slightly agree | Agree | Strongly agree |
No opportunities to observe/Prefer not to say/I dont know
Chart code below:
temp_chart <- ggplot(data=dat_chart, aes(x=category, y=percentage, group=question, fill = factor(answer, levels = bar_order1))) +
geom_bar(stat = "identity", position = "stack") +
scale_x_discrete(limits=rev, labels = wrap_format(25)) +
scale_y_continuous(labels = function(x) paste0(x, "%")) +
scale_fill_manual(labels = ~ stringr::str_wrap(.x, width = 20), values = culture_col1) +
geom_vline(xintercept = (0:10)+0.5) +
coord_flip() +
facet_wrap(~question, ncol = 1) +
guides(fill = guide_legend(nrow = 3, ncol = 3)) +
theme_minimal() +
theme(strip.text = element_text(hjust = 0),
axis.title.x=element_blank(),
axis.title.y=element_blank(),
legend.title=element_blank(),
legend.position="bottom",
panel.grid.minor = element_blank(),
panel.grid.major = element_blank(),
panel.border = element_rect(color = "black", fill = NA, size = 2),
panel.spacing = unit(0.75, "lines"))
simplified data:
dput(dat_chart)
structure(list(question = c("1. Question1",
"1. Question1", "1. Question1",
"1. Question1", "1. Question1",
"1. Question1", "1. Question1",
"1. Question1", "1. Question1",
"1. Question1", "1. Question1",
"1. Question1", "1. Question1",
"1. Question1", "1. Question1",
"1. Question1", "1. Question1",
"1. Question1", "1. Question1",
"1. Question1", "1. Question1"
), percentage = c(3.35731414868106, 4.55635491606715, 5.03597122302158,
13.189448441247, 47.242206235012, 26.378896882494, 0.239808153477218,
0, 7.69230769230769, 11.5384615384615, 26.9230769230769, 30.7692307692308,
23.0769230769231, 0, 2.02020202020202, 7.07070707070707, 13.1313131313131,
20.2020202020202, 32.3232323232323, 25.2525252525253, 0), category = c("a",
"a", "a", "a", "a", "a", "a", "a",
"b", "b", "b", "b", "b", "b", "c",
"c", "c",
"c", "c",
"c", "c"
), answer = c("Strongly disagree", "Disagree", "Slightly disagree",
"Slightly agree", "Agree", "Strongly agree", "No opportunities to observe/Prefer not to say/I dont know",
"Strongly disagree", "Disagree", "Slightly disagree", "Slightly agree",
"Agree", "Strongly agree", "No opportunities to observe/Prefer not to say/I dont know",
"Strongly disagree", "Disagree", "Slightly disagree", "Slightly agree",
"Agree", "Strongly agree", "No opportunities to observe/Prefer not to say/I dont know"
)), row.names = c(NA, -21L), class = c("tbl_df", "tbl", "data.frame"
))
First of all, load the required packages and define a custom theme. The custom theme will simplify the plot code and can be reused if you need it for other plots, it can give your plots a common graphic look.
I simplified the theme, to take advantage of inheritance of theme elements. From the documentation:
Theme elements are documented together according to inheritance, read more about theme inheritance below.
And below, section Arguments:
axis.title.*.* inherits from axis.title.* which inherits from axis.title, which in turn inherits from text
and
panel.grid.*.* inherits from panel.grid.* which inherits from panel.grid, which in turn inherits from line
axis.title
substitutes for the two elements axis.title.x
and axis.title.y
;panel.grid
substitutes for panel.grid.minor
and panel.grid.major
.suppressPackageStartupMessages({
library(tidyverse)
library(scales)
})
theme_so_q79721367 <- function(){
theme_minimal() %+replace%
theme(
# theme elements in alphabetical order
axis.title = element_blank(),
legend.title = element_blank(),
legend.position = "bottom",
panel.grid = element_blank(),
panel.border = element_rect(color = "black", fill = NA, size = 2),
panel.spacing = unit(0.75, "lines"),
strip.text = element_text(hjust = 0)
)
}
Then, I define the answers' order and the bars colors variables. I have changed bar_order1
to a more descriptive variable name, culture_order1
.
culture_order1 <- c("Strongly disagree", "Disagree","Slightly disagree",
"Slightly agree", "Agree", "Strongly agree",
"No opportunities to observe/Prefer not to say/I dont know")
culture_col1 <- setNames(
c("#c23728", "#de6e56", "#f7dbd4", "#d1ecfa", "#63bff0", "#1984c5", "#737373"),
culture_order1)
Now the plot.
To answer the question, if you want to have the legend in row-first order, set byrow = TRUE
in guide_legend
.
ggplot(data = dat_chart, aes(x = category, y = percentage, group = question, fill = factor(answer, levels = culture_order1))) +
geom_bar(stat = "identity", position = "stack") +
scale_x_discrete(limits = rev, labels = wrap_format(25)) +
scale_y_continuous(labels = function(x) paste0(x, "%")) +
scale_fill_manual(labels = ~ stringr::str_wrap(.x, width = 20), values = culture_col1) +
geom_vline(xintercept = (0:10)+0.5) +
coord_flip() +
facet_wrap(~ question, ncol = 1) +
guides(fill = guide_legend(nrow = 3, ncol = 3, byrow = TRUE)) +
theme_so_q79721367()
#> Warning: The `size` argument of `element_rect()` is deprecated as of ggplot2 3.4.0.
#> ℹ Please use the `linewidth` argument instead.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
Created on 2025-08-01 with reprex v2.1.1