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: