ggplot2 icon indicating copy to clipboard operation
ggplot2 copied to clipboard

More aesthetics for `geom_label()`

Open steveharoz opened this issue 2 years ago • 16 comments

Updates to geom_label()'s aesthetics and parameters

  1. The legend keys for geom_label() now have the same rounded rectangle shape and aesthetics as the labels. (See questions below)

Old: image New: image

  1. Deprecated label.size. Use linewidth instead, which is more consistent with the rest of ggplot. ~~If linewidth is not set, it'll fall back to label.size, so nothing should break.~~ Update: setting up a soft deprecation fallback was too messy when going from an argument to an aesthetic, so I fully deprecated label.size.

  2. New aesthetic: linetype for the label border. Default: "solid". Addresses #5365

  3. New parameter: border_colour is the color of the label's border and is decoupled from the text color. If the value is NULL or not set, it will fall back to the text color, which is how it worked previously. Hopefully, that will avoid any breaking changes.

  4. I added three examples to the docs. The mtcars graphs are too crowded to show off all the new aesthetics, so I made a dummy dataset for the latter two:

ggplot(mtcars, aes(wt, mpg, label = rownames(mtcars))) +
geom_label(
  aes(fill = factor(cyl), linetype = qsec < 15),
  border_colour = "black", color = "white", linewidth = 1) +
scale_linetype_manual(values=c("solid", "blank"), limits = TRUE, labels = "1/4 mi < 15s", name = NULL)

image

# Add aesthetics to the border for geom_label
data.frame(x = 1:10, y = 1:10) |>
ggplot() +
  geom_label(aes(
    label=month.abb[x],
    x=x,
    y=y,
    color = factor(x%%3),
    linewidth = x%%2,
    linetype = factor(x%%3)),
    fill = NA) +
scale_linewidth(range=c(0.5, 1.5)) +
scale_linetype_manual(values=c("solid", "blank", "dotted"))

image


# Override the border color
data.frame(x = 1:10, y = 1:10) |>
ggplot() +
  geom_label(aes(
    label=month.abb[x],
    x=x,
    y=y,
    color = factor(x%%3),
    linewidth=x%%2,
    linetype=factor(x%%3)),
    border_color = "red",
    fill=NA) +
scale_linewidth(range=c(0.5, 1.5)) +
scale_linetype_manual(values=c("solid", "blank", "dotted"))

image

Questions:

  1. ~~Does the deprecated messaging look right for label.size?~~
  2. There is a bug where the key_glyph is cropped at the bottom. Can anyone help with that?
  3. ~~Are the aliases border_colour/border_color dealt with appropriately?~~

steveharoz avatar Oct 04 '23 20:10 steveharoz

I've updated the comment at the beginning. See the two remaining questions.

steveharoz avatar Oct 05 '23 18:10 steveharoz

I was looking for that exactly! Thank you very much.

I guess that before this PR, it was not possible to only apply the color to the label box?

After this PR, some work will be required in ggrepel to make geom_label_repel() understand that?

olivroy avatar Oct 06 '23 14:10 olivroy

After this PR, some work will be required in ggrepel to make geom_label_repel() understand that?

The ggrepel package has always been free to do whatever it wants with labels, so there'd be no need to wait on this PR for that.

steveharoz requested a review from teunbrand 20 hours ago

I have to probe Thomas' wisdom on some points, I'll get back on this on tuesday/wednesday.

As a quick comment though: I don't think you'd need all the examples to show all the aesthetics. Most people should be familiar with what 'linewidth' is doing and how this might apply to textboxes. The border_colour and how it interacts with colour probably is worth having an example about.

teunbrand avatar Oct 06 '23 14:10 teunbrand

@teunbrand Thanks. Fair point about the examples. How about these three? The last could be simpler, but I think it's a use case some people may want to copy.

p = ggplot(mtcars, aes(wt, mpg, label = rownames(mtcars)))

# If border.colour is NULL or not set, the border will use the text color
p + geom_label(aes(color = factor(cyl)))

image

# Alternatively, border.colour can have a static value
p + geom_label(aes(color = factor(cyl)), border.colour = "black")

image

# Use linetype and linewidth aesthetics to add highlights
p + geom_label(
  aes(fill = factor(cyl), linetype = qsec < 15),
  border.colour = "black", color = "white", linewidth = 1) +
scale_linetype_manual(values=c("solid", "blank"), limits = TRUE, labels = "1/4 mi < 15s", name = NULL)

image

steveharoz avatar Oct 06 '23 14:10 steveharoz

Thanks @teunbrand!

steveharoz avatar Oct 10 '23 10:10 steveharoz

If #5465 gets merged, we can fix the key glyph cropping issue.

teunbrand avatar Oct 10 '23 13:10 teunbrand

I'm moving this PR back to a draft to consider to make border.colour into a propper aesthetic (https://blueviewer.pages.dev/view?actor=mjskay.com&rkey=3kbfolzxlpq2s). That would also allow for not needing to worry about the weird special fallback case where NULL uses the text color. Instead, both border.colour and (text)color could be mapped the same way.

steveharoz avatar Oct 10 '23 16:10 steveharoz

Yeah that is exactly what Thomas and I were discussing, but we both thought making border.colour a proper aesthetic opens the floodgates for a new level of complexity that is difficult to understand and teach people. Here, as @bwiernik points out in the thread you linked, one could override a scale's aesthetics argument and that will work out fine. However, in #5423 there would be whisker_linetype aesthetics and linetype scales don't have an aesthetics argument. In fact, I'm planning to roll back the proper aesthetics in #5423 to fixed parameters because of this.

That would also allow for not needing to worry about the weird special fallback case where NULL uses the text color.

The mechanism already has precedence in geom_boxplot() with outlier.colour etc.

teunbrand avatar Oct 10 '23 16:10 teunbrand

Personally, I find fixed parameters extremely frustrating because very quickly collaborators or audiences for plots that use them start to ask for data-dependent customization of them (eg, making the linetype for the whiskers match the linetype for the box or vary depending on whether they cross a threshold).

It would seem that a better solution in the case of #5423 would be to add the aesthetics argument to linetype scales

bwiernik avatar Oct 10 '23 16:10 bwiernik

I agree that customization is key and having a coherent look across geoms would make plots much more readable i.e. linetype of a grouped boxplot follow the same logic as other mapped variable. but yeah this exponentiates possibilities and complexities! (I still liked when I could have the point linetype follow some aesthetics mapping in splus but alas in grid stroke of a point cannot have a different linetype...)

smouksassi avatar Oct 10 '23 17:10 smouksassi

Off topic: @smouksassi You still can, but it is device dependent. With {ragg}:

library(grid)
grid.points(pch = 21, size = unit(2, "char"), gp = gpar(lty = 3), default.units = "npc")

Created on 2023-10-10 with reprex v2.0.2

teunbrand avatar Oct 10 '23 17:10 teunbrand

Here is my proposal for the label key drawing function, now that #5465 has been merged.

draw_key_label <- function(data, params, size) {
  if(is.null(params$label.padding)) params$label.padding <- unit(0.25, "lines")
  if(length(params$label.padding) == 1) params$label.padding[2:4] <- params$label.padding
  
  grob <- labelGrob(
    label = data$label %||% "a",
    x = 0.5,
    y = 0.5,
    padding = params$label.padding,
    r = data$label.r %||% unit(0.1, "snpc"),
    angle = data$angle %||% 0,
    text.gp = gpar(
      col = alpha(data$colour %||% "white", data$alpha),
      fontfamily = data$family %||% "",
      fontface = data$fontface %||% 1,
      fontsize = (data$size %||% 3.88) * .pt
    ),
    rect.gp = gpar(
      col = alpha(params$border.colour %||% params$border.color %||% data$colour %||% "black", data$alpha),
      fill = alpha(data$fill %||% "white", data$alpha),
      lty = data$linetype,
      lwd = len0_null(data$linewidth * .pt)
    ),
    vp = NULL
  )
  # Linewidth as cm, twice because it is on two sides of textbox
  lwd <- (data$linewidth %||% params$label.size) * .pt * 0.2
  # Get width/height of text. Margins are baked in.
  width  <- 
    convertWidth(widthDetails(grob$children[[2]]),  "cm", valueOnly = TRUE)
  height <-
    convertHeight(heightDetails(grob$children[[2]]), "cm", valueOnly = TRUE)
  attr(grob, "width")  <- width  + lwd
  attr(grob, "height") <- height + lwd
  grob
}

teunbrand avatar Nov 22 '23 10:11 teunbrand

Is there any chance this will be merged for this release? or will this get looked at after release?

olivroy avatar Dec 15 '23 13:12 olivroy

@olivroy This PR is currently set as a draft (so we cannot merge it) and the release process has been kicked off. While ideally this PR would have been flagged in the 3.5.0 milestone, it is currently not too late, as we haven't started the reverse dependency check yet (see https://github.com/tidyverse/ggplot2/issues/5588). However, we will start that soon, so I would expect it will not be included in 3.5.0.

teunbrand avatar Dec 15 '23 14:12 teunbrand

Been busy with other things. But I'll try to work on it this weekend.

steveharoz avatar Dec 15 '23 14:12 steveharoz

There is no rush, I'm reasonably sure that the upcoming release would have to be patched afterwards so we can include it in that release. If someone needs these features right this instant, they can extend geom_label() if they want.

teunbrand avatar Dec 15 '23 15:12 teunbrand