How to show seperate legend for two geom_col with same aestetics

I’m trying to plot overlapping stacked bar charts with the same aestetics and different widths of the bars.

My current solution is by adding separate geom_cols, but the same aestetics. Just different filters on the data set. But this gives the same fill colours two both. I can change this by adding alpha to one of them, which looks OK for me.

The problem here is then, that I still get only one legend for both bars as you can see in the following example:

library(tidyverse)

data <- tribble(
  ~status , ~category , ~month , ~value ,
  'plan' , 'A' , 'Jan' , 5000 ,
  'plan' , 'A' , 'Feb' , 4900 ,
  'plan' , 'A' , 'Mar' , 4900 ,
  'plan' , 'B' , 'Jan' , 0 ,
  'plan' , 'B' , 'Feb' , 499 ,
  'plan' , 'B' , 'Mar' , 600 ,
  'actual' , 'A' , 'Jan' , 5200 ,
  'actual' , 'A' , 'Feb' , 4950 ,
  'actual' , 'A' , 'Mar' , 4700 ,
  'actual' , 'B' , 'Jan' , 0 ,
  'actual' , 'B' , 'Feb' , 380 ,
  'actual' , 'B' , 'Mar' , 400
) |>
  mutate(month = factor(month , levels = month.abb[1:3]))

data |>
  ggplot(mapping = ggplot2::aes(x = month , y = value)) +
  geom_col(data = ~ . |> filter(status == 'plan') ,
                    mapping = ggplot2::aes(fill = category) ,
                    alpha = 0.5 ,
                    position = position_stack(reverse = TRUE)) +
  geom_col(data = ~ . |> filter(status == 'actual') ,
                    mapping = aes(fill = category) ,
                    position = position_stack(reverse = TRUE) ,
                    width = 0.5) +
  scale_fill_manual(name = 'Category' ,
                             values = c('A' = '#628395' , 'B' = '#95A472') ,
                             labels = c('A' = 'one' , 'B' = 'two'))

I can put alpha in the aes() of both bars and add a scale_alpha_manual, this gives an additional legend, but with wrong colours.

So, does anyone have an idea, how I can achieve a similar plot with two legends?

>Solution :

You can use fill= on an "interaction" of status and category.

data |>
  ggplot(mapping = ggplot2::aes(x = month , y = value)) +
  geom_col(data = ~ . |> filter(status == 'plan') ,
                    mapping = ggplot2::aes(fill = interaction(status, category)) ,
                    position = position_stack(reverse = TRUE)) +
  geom_col(data = ~ . |> filter(status == 'actual') ,
                    mapping = aes(fill = interaction(status, category)) ,
                    position = position_stack(reverse = TRUE) ,
                    width = 0.5) +
  scale_fill_manual(name = 'Category' ,
                    values = c('actual.A' = '#628395' , 'actual.B' = '#95A472',
                               'plan.A' = '#62839588' , 'plan.B' = '#95A47288') ,
                    labels = c('A' = 'one' , 'B' = 'two'))

ggplot using interaction of two fields for fill and alpha

The labels actual.A and such may not be inspiring. All interaction is doing is creating a concatenated string collapsed with ., so you can create your own new field that combines category and status in a way that you prefer.

Leave a Reply