Search code examples
rggplot2legend

R, ggplot2, patchwork: combine legends with comparable (but different) sets of values


I've reviewed other questions along these sames lines about combining plot legends with the patchwork package, but my question is different in that 3 of the 4 combined graphs have overlapping values. I.e., the three plots share some values, but some plots have unique values. And none of the plots has the complete set of values.

Thus, showing three different legends is redundant, because they share much of the same information. I would like to combine the three "character" legends somehow, or else find some way to make a legend independent of a particular plot.

Now you might think that it would be better to simply combine the first three plots into a single plot with three facets. I did that too, but for some reason, it put a lot of blank space between the facted triple plot and the 4th plot, they also don't scale together very well. I've spent all messing with that, so I'm thinking that one plot per data set is the best way to go.

Now, in this reproducible example, the legends of the first three plots have different names "character1, character2, etc." because I had to manually create the data here. In my real data, it all comes from an external csv file, and is then subsetted. So when I run the plot, the three Legends all have the same "character" title.

So my question here is, can I combine the first three character legends, while leaving the "episode" legend separate?

code:

library(ggplot2)

#dataset 1
channel1 <- c("Channel 1", "Channel 1", "Channel 1", "Channel 1", "Channel 1", "Channel 1", "Channel 1", "Channel 1", "Channel 1", "Channel 1")
start_time1 <- c(0.000, 9.719, 11.735, 14.183, 16.554, 18.482, 0.000, 11.693, 12.310, 13.912)
stop_time1 <- c(9.719, 11.735, 14.183, 16.554, 18.482, 19.553, 11.693, 12.310, 13.912, 15.406)
character1 <- c("A", "B", "C", "C", "B", "A", "A", "B", "C", "B")

df1 <- data.frame(channel1, start_time1, stop_time1, character1)

#dataset1 plot
plot1 <- ggplot(df1, aes(fill = character1, color = character1)) +
  geom_rect(color = NA, aes(xmin = start_time1, xmax = stop_time1, ymin = -0.5, ymax = 0.5)) +
  scale_x_continuous(breaks = scales::pretty_breaks(n = 10)) +
  facet_grid(channel1 ~ .) +
  theme(axis.text.x=element_blank(), 
        axis.ticks.x=element_blank(), 
        axis.text.y=element_blank(), 
        axis.ticks.y=element_blank()) +
  theme(axis.text.y=element_blank(),
        axis.ticks.y=element_blank() ) +
  scale_fill_manual(values=c("#F8766D", "#871282", "#ffe119", "#0072B2", "#00C094", "#00A9FF", "#C77CFF"))


#dataset 2
channel2 <- c("Channel 2", "Channel 2", "Channel 2", "Channel 2", "Channel 2", "Channel 2", "Channel 2", "Channel 2", "Channel 2", "Channel 2")
start_time2 <- c(0, 3, 4, 7, 9, 10, 12, 15, 17, 19)
stop_time2 <- c(3, 4, 6, 8, 11, 12, 14, 17, 18, 20)
character2 <- c("A", "D", "C", "E", "B", "A", "C", "D", "C", "B")

df2 <- data.frame(channel2, start_time2, stop_time2, character2)

#dataset2 plot

plot2 <- ggplot(df2, aes(fill = character2, color = character2)) +
  geom_rect(color = NA, aes(xmin = start_time2, xmax = stop_time2, ymin = -0.5, ymax = 0.5)) +
  scale_x_continuous(breaks = scales::pretty_breaks(n = 10)) +
  facet_grid(channel2 ~ .) +
  theme(axis.text.x=element_blank(), 
        axis.ticks.x=element_blank(), 
        axis.text.y=element_blank(), 
        axis.ticks.y=element_blank()) +
  theme(axis.text.y=element_blank(),
        axis.ticks.y=element_blank() ) +
  scale_fill_manual(values=c("#F8766D", "#871282", "#ffe119", "#0072B2", "#00C094", "#00A9FF", "#C77CFF"))

#dataset 3
channel3 <- c("Channel 3", "Channel 3", "Channel 3", "Channel 3", "Channel 3", "Channel 3")
start_time3 <- c(0, 3, 6, 9, 15,18)
stop_time3 <- c(3, 6, 9, 12, 17, 20)
character3 <- c("A", "D", "C", "G", "B", "F")

df3 <- data.frame(channel3, start_time3, stop_time3, character3)

#dataset3 plot
plot3 <- ggplot(df3, aes(fill = character3, color = character3)) +
  geom_rect(color = NA, aes(xmin = start_time3, xmax = stop_time3, ymin = -0.5, ymax = 0.5)) +
  scale_x_continuous(name = "Time (sec)", breaks = scales::pretty_breaks(n = 10)) +
  facet_grid(channel3 ~ .) +
  theme(axis.text.y=element_blank(),
        axis.ticks.y=element_blank() ) + theme(legend.position="bottom") +
  scale_fill_manual(values=c("#F8766D", "#871282", "#ffe119", "#0072B2", "#00C094", "#00A9FF", "#C77CFF"))

#dataset 4
ep_Channel <- c("Episode", "Episode", "Episode", "Episode")
ep_start_time <- c(0, 5, 10, 15)
ep_stop_time <- c(5, 10, 15, 20)
Episode <- c("ep1", "ep2", "ep3", "ep4")

ep_df <- data.frame(ep_Channel, ep_start_time, ep_stop_time, Episode)

#dataset4 plot
plot4 <- ggplot(ep_df, aes(fill = Episode, color = Episode)) +
  geom_rect(color = NA, aes(xmin = ep_start_time, xmax = ep_stop_time, ymin = -0.5, ymax = 0.5)) +
  scale_x_continuous(name = "Time (sec)", breaks = scales::pretty_breaks(n = 10)) +
  facet_grid(ep_Channel ~ .) +
  theme(axis.text.y=element_blank(),
        axis.ticks.y=element_blank() ) + theme(legend.position="bottom") +
  scale_fill_manual(values=c("#F8F8F8", "#F5F5F5", "#F0F0F0", "#E8E8E8"))

#combine plots
library(patchwork)
(plot1 / plot2 / plot3 / plot4) + 
  plot_annotation(title = 'TITLE GOES HERE', theme = theme(plot.title = element_text(hjust = 0.5))) + 
  plot_layout(guides = "collect") & theme(legend.position = 'bottom')

Plot:

big ol' plot


Solution

  • To merge the three legends when the categories differ use the limits= argument to display all categories in each legend and set the same name= for each legend. Doing so you also ensure that each letter is always assigned the same color in each plot, which is not the case in your original code. Additionally, to avoid duplicating the code for each plot I use a plotting function, which could be simplified even further if all the datasets shared the same column names.

    UPDATE Due to a change in the guide system introduced with ggplot2 3.5.0 one now has to add show.legend = TRUE to the geom_rects to ensure that a key is displayed for missing categories and to ensure that the legends get merged.

    library(patchwork)
    library(ggplot2)
    
    # Plotting function
    
    plot_fun <- function(.data, i = 1) {
      fill <- color <- paste0("character", i)
      xmin <- paste0("start_time", i)
      xmax <- paste0("stop_time", i)
      facet <- paste0("channel", i)
      drop_x <- if (i %in% 1:2) {
        theme(
          axis.text.x = element_blank(),
          axis.ticks.x = element_blank()
        )
      }
      ggplot(.data, aes(fill = .data[[fill]], color = .data[[color]])) +
        geom_rect(
          color = NA,
          aes(xmin = .data[[xmin]], xmax = .data[[xmax]], ymin = -0.5, ymax = 0.5),
          show.legend = TRUE
        ) +
        scale_x_continuous(breaks = scales::pretty_breaks(n = 10)) +
        scale_fill_manual(
          name = "Character",
          values = c(
            "#F8766D", "#871282", "#ffe119", "#0072B2",
            "#00C094", "#00A9FF", "#C77CFF"
          ),
          limits = LETTERS[1:7]
        ) +
        facet_grid(rows = vars(.data[[facet]])) +
        theme(
          axis.text.y = element_blank(),
          axis.ticks.y = element_blank()
        ) +
        drop_x
    }
    
    # plots
    plot1 <- plot_fun(df1, 1)
    plot2 <- plot_fun(df2, 2)
    plot3 <- plot_fun(df3, 3)
    
    # dataset4 plot
    plot4 <- ggplot(ep_df, aes(fill = Episode, color = Episode)) +
      geom_rect(
        color = NA,
        aes(xmin = ep_start_time, xmax = ep_stop_time, ymin = -0.5, ymax = 0.5),
        show.legend = TRUE
      ) +
      scale_x_continuous(name = "Time (sec)", breaks = scales::pretty_breaks(n = 10)) +
      facet_grid(ep_Channel ~ .) +
      theme(
        axis.text.y = element_blank(),
        axis.ticks.y = element_blank()
      ) +
      theme(legend.position = "bottom") +
      scale_fill_manual(values = c("#F8F8F8", "#F5F5F5", "#F0F0F0", "#E8E8E8"))
    
    # combine plots
    
    (plot1 / plot2 / plot3 / plot4) +
      plot_annotation(title = "TITLE GOES HERE", theme = theme(plot.title = element_text(hjust = 0.5))) +
      plot_layout(guides = "collect") &
      theme(legend.position = "bottom")
    

    enter image description here