8

Is there a way to use ggplot2 to create divergent stacked bar charts like the one on the right-hand side of the image below?

enter image description here

Data for reproducible example

library(ggplot2)
library(scales)
library(reshape)

dat <- read.table(text = "    ONE TWO THREE
                  1   23  234 324
                  2   34  534 12
                  3   56  324 124
                  4   34  234 124
                  5   123 534 654",sep = "",header = TRUE)

# reshape data
datm <- melt(cbind(dat, ind = rownames(dat)), id.vars = c('ind'))

# plot
ggplot(datm,aes(x = variable, y = value,fill = ind)) + 
  geom_bar(position = "fill",stat = "identity") +
  coord_flip()
rafa.pereira
  • 13,251
  • 6
  • 71
  • 109
  • Possible duplicate of [Simpler population pyramid in ggplot2](https://stackoverflow.com/questions/14680075/simpler-population-pyramid-in-ggplot2) – Mako212 Oct 08 '19 at 16:12

2 Answers2

8

Sure, positive values stack positively, negative values stack negatively. Don't use position fill. Just define what you want as negative values, and actually make them negative. Your example only has positive scores. E.g.

ggplot(datm, aes(x = variable, y = ifelse(ind %in% 1:2, -value, value), fill = ind)) + 
    geom_col() +
    coord_flip()

enter image description here

If you want to also scale to 1, you need some preprocessing:

library(dplyr)
datm %>% 
  group_by(variable) %>% 
  mutate(value = value / sum(value)) %>% 
  ggplot(aes(x = variable, y = ifelse(ind %in% 1:2, -value, value), fill = ind)) + 
  geom_col() +
  coord_flip()

enter image description here

Axeman
  • 32,068
  • 8
  • 81
  • 94
3

An extreme approach might be to calculate the boxes yourself. Here's one method

dd <- datm %>% group_by(variable) %>% 
  arrange(desc(ind)) %>% 
  mutate(pct = value/sum(value), right = cumsum(pct), left=lag(right, default=0))

then you can plot with

ggplot(dd) + 
  geom_rect(aes(xmin=right, xmax=left, ymin=as.numeric(variable)-.4, ymax=as.numeric(variable)+.4, fill=ind)) + 
  scale_y_continuous(labels=levels(dd$variable), breaks=1:nlevels(dd$variable))

to get the left plot. and to get the right, you just shift the boxes a bit. This will line up all the right edges of the ind 3 boxes.

ggplot(dd %>% group_by(variable) %>% mutate(left=left-right[ind==3], right=right-right[ind==3])) + 
  geom_rect(aes(xmin=right, xmax=left, ymin=as.numeric(variable)-.4, ymax=as.numeric(variable)+.4, fill=ind)) + 
  scale_y_continuous(labels=levels(dd$variable), breaks=1:nlevels(dd$variable))

enter image description here

So maybe overkill here, but you have a lot of control this way.

MrFlick
  • 195,160
  • 17
  • 277
  • 295
  • Yes, if the goal is to both fill to 1 and stack pos and neg, I'm pretty sure you need to pre-calculate no matter what. But you should still be able to just once scale all levels to 1, then change to negative and plot with position stack, I think. No need for `geom_rect`? – Axeman Mar 07 '18 at 22:36
  • @Axeman that seems right. I honestly didn’t know how ggplot would handle stacking negative valued bars. I just wanted to show that you could basically draw whatever you wanted and what that might look like. – MrFlick Mar 07 '18 at 22:45
  • To be fair, IIRC the negative stacking has changed at some point, maybe a year ago or so? Agreed that whenever things don't turn out the way you want, you can draw primitives like `rect`! – Axeman Mar 07 '18 at 22:47