grouped and stacked bar chart without faceting

I don't have a good solution, but a few comments and pointers.

If I'm not mistaken your example df doesn't really work for this, as category2 and category3 are fully confounded. I think the question becomes more obvious by doubling the examples (each time one in test1 and one in test2)?

Looking at it with faceting:

library(ggplot2)

# Your example df
df <- data.frame(category1 =c("A", "A", "B", "B", "C", "C"),
                 category2= letters[1:6], 
                 category3 = c("test1", "test2", "test2", "test1", "test2", "test1"),
                 num = 1:6)
df
#>   category1 category2 category3 num
#> 1         A         a     test1   1
#> 2         A         b     test2   2
#> 3         B         c     test2   3
#> 4         B         d     test1   4
#> 5         C         e     test2   5
#> 6         C         f     test1   6

ggplot(df, aes(category1, y= num))+
  geom_col(position = "stack", aes(fill = category2)) +
  facet_wrap(~category3)

# a more complex df
df <- data.frame(category1 =c("A", "B", "C") |> rep(each = 4),
                 category2= letters[1:6] |> rep(each = 2), 
                 category3 = c("test1", "test2") |> rep(times = 6),
                 num = 1:12)

df
#>    category1 category2 category3 num
#> 1          A         a     test1   1
#> 2          A         a     test2   2
#> 3          A         b     test1   3
#> 4          A         b     test2   4
#> 5          B         c     test1   5
#> 6          B         c     test2   6
#> 7          B         d     test1   7
#> 8          B         d     test2   8
#> 9          C         e     test1   9
#> 10         C         e     test2  10
#> 11         C         f     test1  11
#> 12         C         f     test2  12

ggplot(df, aes(category1, y= num))+
  geom_col(position = "stack", aes(fill = category2)) +
  facet_wrap(~category3)

So from how I understand your question, this second df is better suited?


Anyway, I don't have a perfect answer, but maybe this approaches what you want? This is still not really dodged, the spaces within category1 bars being the same as between different category1.

ggplot(df) +
  geom_col(aes(x = interaction(category3, category1), y = num, fill = category2),
           position = "stack")

You can kind of make it better with a complete hack, but ending up with a continuous x axis, which is probably not what you want:

df |>
  dplyr::mutate(x_pos = dplyr::case_match(category1,
                                          "A" ~ 0,
                                          "B" ~ 1,
                                          "C" ~ 2) +
                  0.4 * dplyr::case_match(category3,
                                          "test1" ~ 1,
                                          "test2" ~ 2)) |>
  ggplot() +
  geom_col(aes(x = x_pos, y = num, fill = category2),
           position = "stack")

Or another hack that keeps the x axis discrete, but adds an empty category3 variable to make sure we are spacing the groups:

library(dplyr)
df |> 
  select(category1, category2) |>
  mutate(category3 = "", num = NA_integer_) |>
  bind_rows(df) |>
  ggplot() +
  geom_col(aes(x = interaction(category3, category1), y = num, fill = category2),
           position = "stack")

Note, the interaction idea comes from Stack Overflow posts, and judging by the answers, there is not a well-known way to do that:

1 Like