Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stretching legends #5515

Merged
merged 14 commits into from
Dec 11, 2023
Merged

Stretching legends #5515

merged 14 commits into from
Dec 11, 2023

Conversation

teunbrand
Copy link
Collaborator

@teunbrand teunbrand commented Nov 9, 2023

This PR aims to fix #4896.

Briefly it allows keywidth/keyheight and barwidth/barheight to be 'null' units, which fills the legends to the available space.

In somewhat more detail, 'null' in grid units are typically (to my understanding) interpreted as 'calculate all other units first, then divide the remaining space among the null units'. In this PR, I propagate that behaviour for legend key sizes. This works in several steps:

  1. Guides as calculated as before, it is just in the Guide$assemble_drawing() stage that the key sizes are replaced with the provided 'null' units if provided by the user in the guide_*() function (i.e., we don't propagate 'null' units set in the theme).
  2. During the guide box assembly, it is checked if any legend's size contains a 'null' unit and if so, interpret the null units and translate these to 'npc' units. This step is necessary because the interpretation in grid of 'null' unit nested in some 'sum' unit is different than that in a plain simple unit.
  3. During ggplot_gtable(), the legend size is set to 1npc if 'sum' units are encountered in the legend size. These 'sum' units should only appear when a 'null' had been set somewhere, because everything else in legends is casted to centimeters. This aligns the stretched legends to the panel.

I didn't want to mess with the guts of <unit> objects too much, so at several places I just delay taking the sum of units to look into their types. I had to improvise a unitType function for R <4.0, which works, but might not be great. In any case, in 2024 the minimum supported version of R should be R 4.0, so this might not matter for much longer.

I've marked this option as 'experimental' in the documentation, just to prepare users for the possibility that there might be unexpected behaviour that I cannot foresee at this point.

On to examples: if we want to implement #4896, we want to might want to start like this:

devtools::load_all("~/packages/ggplot2")
#> ℹ Loading ggplot2

ggplot(mtcars, aes(mpg, wt, colour = as.factor(am))) +
  geom_point() +
  guides(colour = guide_legend(
    keywidth = unit(1, "null"), 
    label.position = "bottom")
  ) +
  theme(legend.position = "top")

This isn't quite what the result should be, so we'd have to set the legend margins to 0 and remove the title to get full alignment.

last_plot() + theme(
  legend.title = element_blank(),
  legend.margin = margin()
)

This shows a stretched colourbar:

ggplot(mtcars, aes(disp, mpg)) +
  geom_point(aes(colour = drat)) +
  guides(colour = guide_colourbar(barheight = unit(1, "null")))

This shows a stretched legend:

ggplot(mtcars, aes(disp, mpg)) +
  geom_point(aes(shape = factor(cyl))) +
  guides(shape = guide_legend(keyheight = unit(1, "null")))

The behaviour when you combine an absolute legend and relative legend follows the 'null' heuristics described above: first absolute units are honoured, then remainder is divided among null units.

ggplot(mtcars, aes(disp, mpg)) +
  geom_point(aes(shape = factor(cyl), colour = drat)) +
  guides(shape = guide_legend(keyheight = unit(1, "null")))

When you just have 1 legend, the value of the 'null' unit doesn't matter, but it can come in handy when combining legends and you want to give them a different size.

ggplot(mtcars, aes(disp, mpg)) +
  geom_point(aes(shape = factor(cyl), colour = factor(vs))) +
  guides(shape  = guide_legend(keyheight = unit(1, "null")),
         colour = guide_legend(keyheight = unit(0.5, "null"))) +
  theme(legend.key = element_rect(colour = "black"))

It was not really clear to me what should happen when the opposite size (that isn't parallel to the panel side) is set as a null unit, since there isn't any 'available space' to stretch into. With the current code, the 'available space' is 0, so the guide is shrunk. Note that this exactly the same as it works in CRAN ggplot2, so this isn't a regression.

ggplot(mtcars, aes(disp, mpg)) +
  geom_point(aes(colour = drat)) +
  guides(colour = guide_colourbar(barwidth = unit(1, "null")))

Created on 2023-11-09 with reprex v2.0.2

R/backports.R Show resolved Hide resolved
Comment on lines +462 to +476
width = 1,
height = 1,
default.units = "npc",
gp = gpar(col = NA),
interpolate = TRUE
)
} else{
if (params$direction == "horizontal") {
width <- elements$key.width / nrow(decor)
height <- elements$key.height
width <- 1 / nrow(decor)
height <- 1
x <- (seq(nrow(decor)) - 1) * width
y <- 0
} else {
width <- elements$key.width
height <- elements$key.height / nrow(decor)
width <- 1
height <- 1 / nrow(decor)
Copy link
Collaborator Author

@teunbrand teunbrand Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting these sizes to npcs is benign because the actual size is set in Guide$measure_grobs().

Comment on lines +650 to +662
widths <- unit(c(sizes$padding[4], sizes$widths, sizes$padding[2]), "cm")
if (is.unit(params$keywidth) && unitType(params$keywidth) == "null") {
i <- unique(layout$layout$key_col)
widths[i] <- params$keywidth
}

gt <- gtable(
widths = unit(c(sizes$padding[4], sizes$widths, sizes$padding[2]), "cm"),
heights = unit(c(sizes$padding[1], sizes$heights, sizes$padding[3]), "cm")
)
heights <- unit(c(sizes$padding[1], sizes$heights, sizes$padding[3]), "cm")
if (is.unit(params$keyheight) && unitType(params$keyheight) == "null") {
i <- unique(layout$layout$key_row)
heights[i] <- params$keyheight
}

gt <- gtable(widths = widths, heights = heights)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is step 1

R/guides-.R Outdated Show resolved Hide resolved
R/plot-build.R Outdated Show resolved Hide resolved
@teunbrand teunbrand added this to the ggplot2 3.5.0 milestone Nov 9, 2023
Merge branch 'main' into flex_legend_size

# Conflicts:
#	R/guide-legend.R
#	R/guides-.R
#	R/plot-build.R
@teunbrand
Copy link
Collaborator Author

What I previously called step 2 and 3 are now all part of Guides$package_box(), so there isn't really a need to discern these steps anymore.

Copy link
Member

@thomasp85 thomasp85 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@teunbrand
Copy link
Collaborator Author

teunbrand commented Dec 11, 2023

Thanks for the review Thomas! I sneaked in a little fix related to an interaction with #5570 I ran into, but I'll merge anyway because it I'm pretty sure the fix can't harm anything.

@teunbrand teunbrand merged commit 5ed2d88 into tidyverse:main Dec 11, 2023
12 checks passed
@teunbrand teunbrand deleted the flex_legend_size branch December 11, 2023 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Feature request - Align horizontal legend width and plotting area
2 participants