How to combine two separate legends side by side?

ggplot2

#1

Hello everyone,

Can anyone show me how to replace the color scale in p1 with the color bar p2 generated by plot_discrete_cbar function?

Thanks for any suggestions!


library(ggplot2)
library(cowplot)

# discrete colorbar
plot_discrete_cbar = function (
  # Vector of breaks. If +-Inf are used, triangles will be added to the sides of the color bar      
  breaks, 
  palette = "Greys", # RColorBrewer palette to use
  # Alternatively, manually set colors
  colors = RColorBrewer::brewer.pal(length(breaks) - 1, palette), 
  direction = 1, # Flip colors? Can be 1 or -1
  spacing = "natural", # Spacing between labels. Can be "natural" or "constant"
  border_color = NA, # NA = no border color
  legend_title = NULL,
  legend_direction = "horizontal", # Can be "horizontal" or "vertical"
  font_size = 5,
  expand_size = 1, # Controls spacing around legend plot
  spacing_scaling = 1, # Multiplicative factor for label and legend title spacing
  width = 0.1, # Thickness of color bar
  triangle_size = 0.1 # Relative width of +-Inf triangles
) {
  require(ggplot2)
  if (!(spacing %in% c("natural", "constant"))) stop("Spacing must be either 'natural' or 'constant'")
  if (!(direction %in% c(1, -1))) stop("Direction must be either 1 or -1")
  if (!(legend_direction %in% c("horizontal", "vertical"))) { 
    stop("Legend_direction must be either 'horizontal' or 'vertical'")
  }
  breaks = as.numeric(breaks)
  new_breaks = sort(unique(breaks))
  if (any(new_breaks != breaks)) warning("Wrong order or duplicated breaks")
  breaks = new_breaks
  if (class(colors) == "function") colors = colors(length(breaks) - 1)
  if (length(colors) != length(breaks) - 1) {
    stop("Number of colors (", length(colors), ") must be equal to number of breaks (", 
         length(breaks), ") minus 1")
  }
  if (!missing(colors)) {
    warning("Ignoring RColorBrewer palette '", palette, "', since colors were passed manually")
  }
  if (direction == -1) colors = rev(colors)
  
  inf_breaks = which(is.infinite(breaks))
  if (length(inf_breaks) != 0) breaks = breaks[-inf_breaks]
  plotcolors = colors
  
  n_breaks = length(breaks)
  
  labels = breaks
  
  if (spacing == "constant") {
    breaks = 1:n_breaks
  }
  
  r_breaks = range(breaks)
  
  cbar_df = data.frame(stringsAsFactors = FALSE,
                       y = breaks,
                       yend = c(breaks[-1], NA),
                       color = as.character(1:n_breaks)
  )[-n_breaks,]
  
  xmin = 1 - width/2
  xmax = 1 + width/2
  
  cbar_plot = ggplot(cbar_df, aes(xmin = xmin, xmax = xmax, 
                                  ymin = y, ymax = yend, fill = color)) +
    geom_rect(show.legend = FALSE,
              color=border_color)
  
  if (any(inf_breaks == 1)) { # Add < arrow for -Inf
    firstv = breaks[1]
    polystart = data.frame(
      x = c(xmin, xmax, 1),
      y = c(rep(firstv, 2), firstv - diff(r_breaks) * triangle_size)
    )
    plotcolors = plotcolors[-1]
    cbar_plot = cbar_plot +
      geom_polygon(data = polystart, aes(x = x, y = y),
                   show.legend = FALSE,
                   inherit.aes = FALSE,
                   fill = colors[1],
                   color = border_color)
  }
  if (any(inf_breaks > 1)) { # Add > arrow for +Inf
    lastv = breaks[n_breaks]
    polyend = data.frame(
      x = c(xmin, xmax, 1),
      y = c(rep(lastv, 2), lastv + diff(r_breaks) * triangle_size)
    )
    plotcolors = plotcolors[-length(plotcolors)]
    cbar_plot = cbar_plot +
      geom_polygon(data = polyend, aes(x = x, y = y),
                   show.legend = FALSE,
                   inherit.aes = FALSE,
                   fill = colors[length(colors)],
                   color = border_color)
  }
  
  if (legend_direction == "horizontal") { # horizontal legend
    mul = 1
    x = xmin
    xend = xmax
    cbar_plot = cbar_plot + coord_flip()
    angle = 0
    legend_position = xmax + 0.1 * spacing_scaling
  } else { # vertical legend
    mul = -1
    x = xmax
    xend = xmin
    angle = -90
    legend_position = xmax + 0.2 * spacing_scaling
  }
  
  cbar_plot = cbar_plot +
    geom_segment(data = data.frame(y = breaks, yend = breaks),
                 aes(y = y, yend = yend),
                 x = x - 0.05 * mul * spacing_scaling, xend = xend,
                 inherit.aes = FALSE) +
    annotate(geom = 'text', x = x - 0.1 * mul * spacing_scaling, y = breaks,
             label = labels,
             size = font_size) +
    scale_x_continuous(expand = c(expand_size, expand_size)) +
    scale_fill_manual(values = plotcolors) +
    theme_void()
  
  if (!is.null(legend_title)) { # Add legend title
    cbar_plot = cbar_plot +
      annotate(geom = 'text', x = legend_position, y = mean(r_breaks),
               label = legend_title,
               angle = angle,
               size = font_size)
  }
  
  return(cbar_plot)
}



# Cut data into bins
iris2 <- iris 
values <- c(0, 1, 2, 5, 10) 
iris2$cuts <- cut(iris2$Petal.Length, values) 

p1 <- ggplot(iris2, aes(Sepal.Length, y = Sepal.Width, 
                        size = Petal.Width,
                        color = cuts))+ 
  geom_point() +
  facet_grid(~ Species) +
  scale_color_brewer("Petal.Length", palette = "RdYlBu") +
  theme(legend.position = "bottom") +
  guides(color = guide_legend(title.position = "top",
                              title.hjust = 0.5)) +
  guides(size = guide_legend(label.position = "bottom",
                             title.position = "top",
                             title.hjust = 0.5,
                             override.aes = list(shape = c("circle open"),
                                                 color = c("grey50"))))
p1


p2 <- plot_discrete_cbar(c(-Inf, 0, 1, 2, 5, 10, Inf),
                         palette = "RdYlBu", 
                         legend_title = "Petal.Length",
                         spacing = "natural")
p2


# Combine
plot_grid(p1, 
          p2,
          nrow = 2,
          rel_heights = c(1.5, 1.0))

Created on 2018-06-07 by the reprex package (v0.2.0).


#2

People liked but didn't help


#3

Hi,

Noted a couple of things:

  • Use the same breaks for the plot and the colourbar. -Inf seems to be obsolete as there are no negative lengths here
  • Hide legends with guides(colour = "none")
  • Keep factor levels with scale...(drop = F)
  • Replace color = as.character(1:n_breaks) with color = as.factor(1:n_breaks) in the function plot_discrete_cbar. Avoids malsorted colours.

The easiest solution would be to put the native ggplot legend to the side of the panel and the additional colourbar below (or vice-versa). Note that the function plot_discrete_cbar is very sensitive to margins. I'd assign an absolute (instead of relative) height to get the same look across differents plots.

Best, Matt

library(ggplot2)
library(gridExtra)
library(grid)

data(iris)

# Make sure that you use the same breaks for the plot and the colour bar!
values <- c(0, 1, 2, 5, 10, Inf) 
iris$cuts <- cut(iris$Petal.Length, values, include.lowest = T) 

# Show levels
levels(iris$cuts)
#> [1] "[0,1]"    "(1,2]"    "(2,5]"    "(5,10]"   "(10,Inf]"

# drop = F makes sure, you don't lose any levels in the scale
# guides(colour = "none") hides the scale
p1 <- ggplot(iris, aes(Sepal.Length, y = Sepal.Width, size = Petal.Width, color = cuts)) +
  geom_point() +
  scale_color_discrete(drop = F) +
  facet_grid(~ Species) +
  guides(colour = "none", size = guide_legend())
    
# Blank rectangle, here would be your colourbar
p2 <- grid.rect(gp=gpar(fill="green"), draw = F)

# I use gridExtra to arrange the plots, cowplots should work similarly
grid.arrange(p1, p2, nrow = 2, ncol = 1, heights = unit(c(100, 10), "mm"))

Created on 2018-06-10 by the reprex package (v0.2.0).


#4

Thank you Matt! Those are great points.

I tried your code but have another two issues now:

  • The color bar is too small. If I increase its size, its margin also grows making it too far from p1 above.
  • The color bar expands to the whole width of the plot area. Can we limit it to the plot area of p1 (minus its legend) only?
p2 <- plot_discrete_cbar(c(0, 1, 2, 5, 10, Inf),
                         font_size = 4,
                         palette = "RdYlBu", 
                         legend_title = "Petal.Length",
                         spacing = "natural")

grid.arrange(p1, p2, nrow = 2, ncol = 1, heights = unit(c(100, 10), "mm"))


#5

Or I can extract the legend from p1, combine it with p2 then combine with p1 (without legend). However the legend still occupies a lot of plot space

### Combine legend first
p1_leg <- get_legend(p1)
all_leg <- plot_grid(p2, p1_leg, ncol = 2)

### Remove p1 legend
p1_ed <- p1 + 
  theme(legend.position = "none")
  
### Plot together
plot_grid(p1_ed, 
          all_leg,
          nrow = 2,
          align = "v",
          axis = "lr",
          rel_heights = c(1.8, 1.0))


#6

Hi,

Looks nice. Function plot_discrete_cbar has a margin argument which adds space to the right and left of the bar. Change theme(plot.margin = margin(0, margin, 0, margin) within the function code if you need left/right orientation.

You might close this topic now.
Best, Matt


#7

Will try it later. Thanks again!