Adding the gap between positive and negative categories is actually pretty tricky. To do that, I had to build up shapes from scratch with geom_rect
. I followed some of the advice from this answer. One of the problems I ran into was getting the categories to come out in the right order—I kept having "disagree" and "strongly disagree" reversed until I added a "strength" measure to make sure "strongly agree" and "strongly disagree" would both be placed at the extremes.
The main variation was to then add an offset to shift all positive values up by some amount and all negative values down by that same amount. I'd recommend you take the data manipulation steps apart line by line to get the hang of them—I certainly had to just to write it.
library(dplyr)
library(ggplot2)
test_dat <- data.frame(question = rep(c('test1', 'test2'), each = 4),
value = rep(c('Strongly disagree', 'Disagree', 'Agree', 'Strongly agree'), 2),
percentage = c(10, 20, 5, 40, 15, 24, 30, 10),
stringsAsFactors = FALSE)
test_dat$value <- factor(test_dat$value, levels = c('Strongly disagree', 'Disagree', 'Agree', 'Strongly agree')[4:1])
gap <- 0.5
width <- 0.35
test_likert <- test_dat %>%
mutate(question = forcats::as_factor(question),
direction = ifelse(grepl("D|disagree", value), -1, 1),
xmin = as.numeric(question) - width,
xmax = as.numeric(question) + width,
strength = as.numeric(grepl("Strongly", value))) %>%
group_by(question, direction) %>%
arrange(strength, desc(value)) %>%
mutate(ymax = cumsum(percentage) + gap,
ymin = lag(ymax, default = gap)) %>%
mutate_at(vars(ymin, ymax), ~. * direction)
head(test_likert)
#> # A tibble: 6 x 9
#> # Groups: question, direction [4]
#> question value percentage direction xmin xmax strength ymax ymin
#> <fct> <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 test1 Disagree 20 -1 0.65 1.35 0 -20.5 -0.5
#> 2 test2 Disagree 24 -1 1.65 2.35 0 -24.5 -0.5
#> 3 test1 Agree 5 1 0.65 1.35 0 5.5 0.5
#> 4 test2 Agree 30 1 1.65 2.35 0 30.5 0.5
#> 5 test1 Strongly … 10 -1 0.65 1.35 1 -30.5 -20.5
#> 6 test2 Strongly … 15 -1 1.65 2.35 1 -39.5 -24.5
To get the plot, you now have your x & y positions for geom_rect
. The x-scale is a little awkward in order to get text labels (geom_rect
needs that scale to be continuous as far as I can tell).
Originally I'd left the y-scale alone, but having the gap will be misleading to readers (@MatiasAndina mentions the readability issue). You'd be placing bars ending at e.g. 30.5 where their values should actually be 30. One way to handle that is to manually set the scale breaks and label them with the offset taken out. That then puts two values labeled as 0, which is weird, but you do want a clear baseline position.
ggplot(test_likert, aes(fill = value)) +
geom_rect(aes(ymin = ymin, ymax = ymax, xmin = xmin, xmax = xmax)) +
coord_flip() +
scale_x_continuous(labels = levels(test_likert$question),
breaks = unique(as.numeric(test_likert$question))) +
scale_y_continuous(labels = c(seq(45, 0, by = -15), seq(0, 45, by = 15)),
breaks = c(seq(-45, 0, by = 15) - gap, seq(0, 45, by = 15) + gap),
limits = c(-48, 48))

A better way to do the y-scale (and all around more legible for a stacked bar chart), which I'll let you handle, would be to forgo the y-scale breaks and put direct labels on each bar to show their actual values.