Skip to content

Enable margins settings for guide titles #2556

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

Merged
merged 10 commits into from
May 8, 2018

Conversation

clauswilke
Copy link
Member

This pull request, while not completely solving #2465, makes it much easier to work around it, by enabling margin, hjust, and vjust for the theme element legend.title. This enables fine-grained justification control over the location of the legend title, which is needed for plots with more than one legend.

Examples:

library(ggplot2)
p <- ggplot(mtcars, aes(hp, disp, color = mpg, shape = factor(cyl))) +
  geom_point(size = 2) +
  theme(legend.position = "top",
        legend.title = element_text(vjust = 1, margin = margin(t = 4.5)),
        legend.spacing.y = grid::unit(3, "pt"),
        legend.text.align = 0.5)
p

screen shot 2018-05-06 at 9 39 21 pm

p + guides(shape = guide_legend(label.position = "bottom"))

screen shot 2018-05-06 at 9 39 35 pm

For comparison, with the default settings but legend on top, this plot looks as follows:

ggplot(mtcars, aes(hp, disp, color = mpg, shape = factor(cyl))) +
  geom_point(size = 2) +
  theme(legend.position = "top")

screen shot 2018-05-06 at 9 49 27 pm

(Unfortunately, there is no good way at this time to adjust the default settings depending on whether the legend sits on top or at the side. This will have to wait for later.)

Note that none of the visual tests except one had to be changed. Things look exactly as they always did if the default settings are not changed. The visual test that had to be changed had the problem that the two legends in that plot switched order. I have now fixed the order, so this won't happen again.

@clauswilke
Copy link
Member Author

I have updated the pull request to cover legend text margins as well. I realize some visual tests still fail. Those are related to the default hjust setting of legend text and I will fix the issue.

@clauswilke
Copy link
Member Author

Examples with both types of legends:

ggplot(mtcars, aes(hp, disp, color = mpg, shape = factor(cyl))) +
  geom_point(size = 2) +
  theme(legend.text = element_text(margin = margin(l = 20)))

screen shot 2018-05-07 at 12 37 52 am

ggplot(mtcars, aes(hp, disp, color = mpg, shape = factor(cyl))) +
  geom_point(size = 2) +
  guides(shape = guide_legend(label.position = "bottom")) +
  theme(legend.position = "top",
        legend.title = element_text(vjust = 1, margin = margin(t = 4.5)),
        legend.spacing.y = grid::unit(3, "pt"),
        legend.text = element_text(margin = margin(t = 10)))

screen shot 2018-05-07 at 12 38 09 am

@hadley
Copy link
Member

hadley commented May 7, 2018

My main concern with enabling this is that if we do later fix the alignment of titles to align "correctly" by default, then all these smaller fixes will break. OTOH, it's not totally clear what "correct" means, because when I think about aligning the title only with the keys, not the text, that's clearly not correct for a vertical legend.

@clauswilke
Copy link
Member Author

The way I see it, legend.title = element_text(vjust = 1, margin = margin(t = 4.5)) simply states that the legend title should be aligned to the top of the legend box, with a 4.5pt margin. This should continue working in the future, as long as the key height doesn't change. In fact, one way to get the alignment right automatically in the future would be to use exactly this approach but auto-calculate the margin based on the key height.

@clauswilke
Copy link
Member Author

Margins and alignment work now for labels, as far as I can tell. I'm quite happy with the results.

First, manual specification of justification and margins:

df <- data.frame(x = c(1, 10, 100))
p <- ggplot(df, aes(x, x, color = x, size = x)) + geom_point() + theme_gray(7)

p + theme(legend.text = element_text(hjust = 1, margin = margin(l = 5, t = 10, b = 10)))

screen shot 2018-05-07 at 11 40 41 am

Second, defaults (for labels) make sense in all arrangements:

cowplot::plot_grid(
  p,
  p + guides(size = guide_legend(label.position = "bottom")), 
  p + guides(size = guide_legend(label.position = "left"), color = guide_colorbar(label.position = "left")), 
  p + theme(legend.position = "top"),
  p + theme(legend.position = "top") + 
    guides(size = guide_legend(label.position = "bottom")),
  p + theme(legend.position = "top") + 
    guides(size = guide_legend(label.position = "top"), color = guide_colorbar(label.position = "top")),
  nrow = 2
)

screen shot 2018-05-07 at 11 40 03 am

Two issues remain. None are regressions:

  1. I could set more sophisticated defaults for the legend title depending on the legend layout. E.g., set vjust = 1 for horizontal legends.

  2. The code contains (from a long time ago) a check for whether a label is an expression. This check is not reliable and doesn't work for lists of expressions, e.g. as returned by scales::math_format(). The original line in question is this one:

    if (any(is.expression(guide$key$.label))) 1 else 0

@karawoo
Copy link
Member

karawoo commented May 7, 2018

The results of this look really good @clauswilke! This issue with legend title/text margins has plagued me for some time 😅

The desired behavior we're aiming for is so similar to facet strip labels that it seems like there should be a way to fold the placement calculations into existing functions rather than having the legend titles go through title_spec() -> add_margins() -> titleGrob() and then overriding the placement with justify_grobs(). But I haven't figured out where/how to accomplish that.

R/guide-legend.r Outdated
@@ -677,7 +693,7 @@ guide_gengrob.legend <- function(guide, theme) {
)
gt <- gtable_add_grob(
gt,
grob.title,
justify_grobs(grob.title, hjust = title.hjust, vjust = title.vjust),
Copy link
Member

Choose a reason for hiding this comment

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

justify_grobs() takes a debugging argument, should that get passed in here and elsewhere when justify_grobs() is called, or is that not intended to be a user-facing debugging option?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think passing it in makes sense. I'll update my patch. I ran into a problem with switching debugging on for labels and I wasn't sure whether it was my fault or a preexisting problem. I'll see if I can resolve that issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

It was a preexisting problem but it's fixed now.

@clauswilke
Copy link
Member Author

@karawoo I think the problem arises because you don't know ahead of time whether the legend title and text will be integrated in a horizontal or vertical setting. In cases where you do know it, justify_grobs() is not needed, such as for the labels in guide_colorbar().

Also, I'd like to point out that justify_grobs() was de facto invented by @thomasp85 in the context of plot tags here:
https://github.com/thomasp85/ggplot2/blob/1e61f9dea5a5205cc865d1c43690240d4159bebb/R/plot-build.r#L282
I just copied this code and put it into a function.

if (is.null(guide$label.theme$vjust) && is.null(theme$legend.text$vjust)) label.theme$vjust <- NULL

# label.theme in param of guide_legend() > theme$legend.text.align > default
hjust <- guide$label.hjust %||% theme$legend.text.align %||% label.theme$hjust %||%
Copy link
Member

Choose a reason for hiding this comment

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

Is it necessary to have both of theme$legend.text.align and label.theme$hjust? Shouldn't label.theme inherit from legend.text?

Copy link
Member Author

Choose a reason for hiding this comment

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

In the current (released) ggplot2, there are two special theme elements for legend alignment, legend.text.align and legend.title.align. I think both are obsolete and have issues, for example that they don't apply in a vertical context. I only left them in for backwards compatibility, but I'm happy to take them out. label.theme does inherit from legend.text.

Did you also have a question about me overriding inheritance of hjust and vjust for label.theme? I'm happy to explain more. It's needed to make guides behave intuitively under default settings, so that, e.g., a horizontal guide with labels underneath has the correct label alignments (hjust = 0.5 instead of hjust = 0 and vjust = 1 instead of vjust = 0.5).

vjust <- y <- guide$label.vjust %||% 0.5
switch(guide$direction, horizontal = {x <- label_pos; y <- vjust}, "vertical" = {x <- hjust; y <- label_pos})

switch(
Copy link
Member

Choose a reason for hiding this comment

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

Since you're rewriting this, do you mind moving the assignment into the individual blocks of the if statement? I think that will make it easier to read.

Copy link
Member Author

Choose a reason for hiding this comment

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

Just so I understand what you're requesting. Patterns such as:

switch(
  guide$direction,
  "horizontal" = {
    ...
  },
  "vertical" = {
  }
)

are used all over the place in this code. Do you want me to replace those with the following?

if (guide$direction == "horizontal") {
  ...
}
else {
  ...
}

Copy link
Member

Choose a reason for hiding this comment

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

Yes please!

Copy link
Member Author

Choose a reason for hiding this comment

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

I had originally not understood the "move assignment" part of the request, but I think I got it now. The code is easier to read now.

switch(
guide$direction,
"horizontal" = {
x <- label_pos
Copy link
Member

Choose a reason for hiding this comment

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

Similarly, it would be great to move these assignments out.

@hadley hadley merged commit 6b09d17 into tidyverse:master May 8, 2018
@hadley
Copy link
Member

hadley commented May 8, 2018

Thanks!

@clauswilke clauswilke deleted the guide-title-margins branch May 10, 2018 15:50
@lock
Copy link

lock bot commented Nov 6, 2018

This old issue has been automatically locked. If you believe you have found a related problem, please file a new issue (with reprex) and link to this issue. https://reprex.tidyverse.org/

@lock lock bot locked and limited conversation to collaborators Nov 6, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants