How to combine two separate legends side by side?

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 Likes

People liked but didn't help

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).

2 Likes

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"))

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))

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

1 Like

Will try it later. Thanks again!

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.