John Gagnon
John Gagnon

Reputation: 915

How to provide multiple fill values using the same aesthetic

My goal is to pass separate values to change the colors used for fill aesthetics in different geoms.

For example:

ggplot(iris, aes(x = Species, y = Sepal.Length)) + 
  stat_summary(aes(fill = Species), color = 'black', geom = 'bar', fun.y = mean) +
  geom_point(aes(fill = Species), color = 'black', shape = 21) + 
  scale_fill_manual(values = c('royal blue', 'red2', 'limegreen'))

In this plot, I would like to be able to use separate colors to fill the bars and points. Is this possible? I'm aware of using scale_fill_manual() to set the colors to whatever values I want, but this will change the fills of both the bars and the points to the same colors.

Here is a semi-working example of what I am trying to do, however, the legend is off...

iris_j <- iris %>%
  mutate(Species_bar = factor(paste0(Species, '_bar')))

color.groups <- c('royal blue', 'red2', 'limegreen', NA, 'royal blue', 'white')
names(color.groups) <- c(levels(iris_j$Species), levels(iris_j$Species_bar))

ggplot(iris_j, aes(x = Species, y = Sepal.Length)) + 
  stat_summary(aes(fill = Species_bar), color = 'black', geom = 'bar', fun.y = mean) +
  geom_point(aes(fill = Species), color = 'black', shape = 21) + 
  scale_fill_manual(values = color.groups)

enter image description here

Upvotes: 1

Views: 3581

Answers (3)

alistaire
alistaire

Reputation: 43344

This is one of the limitations of ggplot—an aesthetic can only be mapped to a single variable. Generally speaking, I find it a reasonable limitation, as it forestalls a lot of confusing and hard-to-read graphs. That said, with some creativity, it can be worked around, e.g. by coloring the points with the color aesthetic, and then overplotting to add a stroke:

library(ggplot2)

ggplot(iris, aes(x = Species, y = Sepal.Length)) + 
    stat_summary(aes(fill = Species), color = 'black', geom = 'bar', fun.y = mean) +
    geom_point(aes(color = Species)) +    # add colored points
    geom_point(color = 'black', shape = 21, show.legend = TRUE) +    # add point strokes (including in legend)
    scale_color_manual(values = c('royal blue', 'red2', 'limegreen')) +    # define point colors
    scale_fill_manual(values = c(NA, 'royal blue', 'white'))    # define bar colors

To separate the legends, specify a different name for each. To add a stroke to the points in the legend, you'll need to effectively rebuild it in guide_legend. (According to the docs, supplying a named vector to show.legend should work, but in practice it fails.)

ggplot(iris, aes(x = Species, y = Sepal.Length)) + 
    stat_summary(aes(fill = Species), color = 'black', geom = 'bar', fun.y = mean) +
    geom_point(aes(color = Species)) + 
    geom_point(color = 'black', shape = 21) +
    scale_color_manual('points', values = c('royal blue', 'red2', 'limegreen'), 
                       guide = guide_legend(override.aes = list(shape = 21, color = 'black', 
                                                                fill = c('royal blue', 'red2', 'limegreen')))) + 
    scale_fill_manual('bars', values = c(NA, 'royal blue', 'white'))

Such an approach will not generalize to a plot where color is already being used otherwise.

Upvotes: 4

camille
camille

Reputation: 16842

Here are a few little things to try that you could build off of.

First up, if you don't need to use a filled shape, you can just map color to the species in geom_point, so you have a color scale and a fill scale. In this case, I changed the label for fill to mark it as being the means, to show how you can split these into two legends.

library(tidyverse)
light_colors <- c("#87CEEB", "#FFB6C1", "#FF8C69")
dark_colors <- c("#22A0D6", "#E33650", "#BF411B")

ggplot(iris, aes(x = Species, y = Sepal.Length)) +
  stat_summary(aes(fill = Species), geom = "bar", fun.y = mean) +
  geom_point(aes(color = Species)) +
  scale_fill_manual(values = light_colors) +
  scale_color_manual(values = dark_colors) +
  labs(fill = "Mean by Species")

Second, if you do need a filled shape, let geom_point get a fill scale and hack the bars to have a color instead. One way to do that is by making what look like bars but are actually really big geom_segments. I changed the size in the legend to make the legend keys not ridiculously huge.

ggplot(iris, aes(x = Species, y = Sepal.Length)) +
  stat_summary(aes(xend = Species, yend = 0, color = Species), geom = "segment", fun.y = mean, size = 30, lineend = "butt") +
  geom_point(aes(fill = Species), color = "black", shape = 21) +
  scale_fill_manual(values = light_colors) +
  scale_color_manual(values = dark_colors, guide = guide_legend(override.aes = list(size = 4)))

Third way, make a data frame of averages and give it a variable to denote that it's got averages, then add a variable to the original data frame to denote that it's observations. Then you can map the interaction of type with species to get separate colors in one fill scale.

avgs <- iris %>%
  group_by(Species) %>%
  summarise(Sepal.Length = mean(Sepal.Length)) %>%
  mutate(type = "Mean")

iris %>%
  select(Species, Sepal.Length) %>%
  mutate(type = "Observation") %>%
  ggplot(aes(x = Species, y = Sepal.Length, fill = interaction(Species, type))) +
  geom_col(data = avgs) +
  geom_point(color = "black", shape = 21)

Upvotes: 3

A Duv
A Duv

Reputation: 403

Not quite a perfect solution, but it may be a sufficient workaround

     cols_1 <- c("red", "green", "blue")
     cols_2 <- c("orange", "purple", "yellow")
     ggplot(iris, aes(x = Species, y = Sepal.Length)) +  
       geom_point(aes(color = Species)) + # Using color instead of fill 
       stat_summary(aes(fill = Species), color = 'black', geom = 'bar', fun.y = mean, alpha = c(0.5, 0.05, 1)) +  
     scale_color_manual(values = cols_1) + # colors your points 
     scale_fill_manual(values = cols_2) # fills your Summary Bars

Adjust the colors, alpha, and other graphical parameters as you see fit.

Upvotes: 2

Related Questions