Set legend width to be 100% plot width

Viewed 365

How do I set the legend height or width to be 100% of the plot height/width regardless of the actual dimensions?

library(ggplot2)
ggplot(iris, aes(Petal.Width, Sepal.Width, color=Petal.Length))+
  geom_point()+
  theme(
    legend.title=element_blank(),
    legend.position="bottom",
    legend.key.width=unit(0.1,"npc"))

Created on 2022-02-11 by the reprex package (v2.0.1)

Session info
sessioninfo::session_info()
#> ─ Session info ───────────────────────────────────────────────────────────────
#>  setting  value
#>  version  R version 4.1.0 (2021-05-18)
#>  os       Ubuntu 20.04.3 LTS
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  ggplot2     * 3.3.5   2021-06-25 [1] CRAN (R 4.1.0)
#> 
#> ──────────────────────────────────────────────────────────────────────────────
4 Answers

The only way I know of is to manually adjust the grid objects in the gtable of the plot. AFAIK, the guides are mostly defined in cm (rather than relative units), so getting them adapted to the panels is a bit of a pain. I'd also love to know a better way to do this.

library(ggplot2)

g <- ggplot(iris, aes(Petal.Width, Sepal.Width, color=Petal.Length))+
  geom_point()+
  theme(
    legend.title=element_blank(),
    legend.position="bottom",
    legend.key.width=unit(0.1,"npc"),
    legend.margin = margin(), # pre-emptively set zero margins
    legend.spacing.x = unit(0, "cm"))

gt <- ggplotGrob(g) 

# Extract legend
is_legend <- which(gt$layout$name == "guide-box")
legend <- gt$grobs[is_legend][[1]]
legend <- legend$grobs[legend$layout$name == "guides"][[1]]

# Set widths in guide gtable
width <- as.numeric(legend$widths[4]) # save bar width (assumes 'cm' unit) 
legend$widths[4] <- unit(1, "null") # replace bar width

# Set width/x of bar/labels/ticks. Assumes everything is 'cm' unit.
legend$grobs[[2]]$width <- unit(1, "npc")
legend$grobs[[3]]$children[[1]]$x <- unit(
  as.numeric(legend$grobs[[3]]$children[[1]]$x) / width, "npc"
)
legend$grobs[[5]]$x0 <- unit(as.numeric(legend$grobs[[5]]$x0) / width, "npc")
legend$grobs[[5]]$x1 <- unit(as.numeric(legend$grobs[[5]]$x1) / width, "npc")

# Replace legend
gt$grobs[[is_legend]] <- legend

# Draw new plot
grid::grid.newpage()
grid::grid.draw(gt)

Created on 2022-02-11 by the reprex package (v2.0.1)

Please forgive me for double answers, but I believe this to be a totally different approach, and the credits for the idea go to @benson23.

We can use ggh4x::force_panelsizes() to set an absolute size for the panel and match the width of the bar. Upside is that it is reasonably easy to do, downside is that your plot's width doesn't automagically adapts to the window size anymore.

library(ggplot2)
library(ggh4x)

width <- unit(10, "cm")

ggplot(iris, aes(Petal.Width, Sepal.Width, color=Petal.Length))+
  geom_point() +
  guides(colour = guide_colorbar(barwidth = width)) +
  force_panelsizes(cols = width) +
  theme(
    legend.title=element_blank(),
    legend.position="bottom",
    legend.spacing.x = unit(0, "cm"))

The process becomes slightly more complicated if a plot has multiple panels, but it is not undoable.

ncol         <- 3
total_width  <- unit(10, "cm")
# Optionally: replace `theme_get()` with actual theme you're using
spacing      <- calc_element("panel.spacing.x", theme_get())
panel_widths <- (total_width - spacing * (ncol - 1)) / ncol

ggplot(iris, aes(Petal.Width, Sepal.Width, color=Petal.Length))+
  geom_point() +
  guides(colour = guide_colorbar(barwidth = total_width)) +
  facet_wrap(~ Species, ncol = ncol) +
  force_panelsizes(cols = panel_widths) +
  theme(
    legend.title=element_blank(),
    legend.position="bottom",
    legend.spacing.x = unit(0, "cm"))

Created on 2022-02-11 by the reprex package (v2.0.1)

Disclaimer: I'm the author of {ggh4x}

Here I'll use a very manual way to set the legend width, which is guides(colour = guide_colorbar(barwidth = 25)). Just try out the barwidth parameter, you'll get one that fits the plot.

I'd also love to know a way to easily get the width of the plot, so that we don't need to manually set the barwidth.

UPDATE: As you can see in the comment under this answer, both @teunbrand and I agree that we can use a barwidth value close to the value of fig.width in Rmarkdown or width in ggsave if you are using these utilities to display your graph

ggplot(iris, aes(Petal.Width, Sepal.Width, color=Petal.Length))+
  geom_point()+
  guides(colour = guide_colorbar(barwidth = 25)) +
  theme(
    legend.title=element_blank(),
    legend.position="bottom")

legend_barwidth

Adapted @teunbrand's ggplotGrob answer to work as a function. Added ggplotify to convert gtable back to ggplot. Note that it only works specifically for horizontal colorbars.

Original plot:

library(ggplot2)
library(ggplotify)

g <- ggplot(iris, aes(Petal.Width, Sepal.Width, color=Sepal.Width))+
  geom_point()+
  theme(legend.position="bottom")
g

Plot with full width colorbar legend:

#' @param x A ggplot object with colorbar legend along the width.
#'
full_width_legend <- function(x){
  gt <- ggplotGrob(x)
  
  # Extract legend
  is_legend <- which(gt$layout$name == "guide-box")
  legend <- gt$grobs[is_legend][[1]]
  legend <- legend$grobs[legend$layout$name == "guides"][[1]]
  
  # Set widths in guide gtable
  width <- as.numeric(legend$widths[4]) # save bar width (assumes 'cm' unit) 
  legend$widths[4] <- unit(1, "null") # replace bar width
  
  # Set width/x of bar/labels/ticks. Assumes everything is 'cm' unit.
  legend$grobs[[2]]$width <- unit(1, "npc")
  legend$grobs[[3]]$children[[1]]$x <- unit(
    as.numeric(legend$grobs[[3]]$children[[1]]$x) / width, "npc"
  )
  legend$grobs[[5]]$x0 <- unit(as.numeric(legend$grobs[[5]]$x0) / width, "npc")
  legend$grobs[[5]]$x1 <- unit(as.numeric(legend$grobs[[5]]$x1) / width, "npc")
  
  # Replace legend
  gt$grobs[[is_legend]] <- legend
  return(ggplotify::as.ggplot(gt))
}

g1 <- full_width_legend(g)
g1

Created on 2022-02-11 by the reprex package (v2.0.1)

Related