hadley / ggplot2-book

ggplot2: elegant graphics for data analysis
https://ggplot2-book.org/
1.57k stars 683 forks source link

Example creating new theme elements #322

Open teunbrand opened 2 years ago

teunbrand commented 2 years ago

It was pointed out to me that the ggplot2 book might look for an example of extending theme elements,

https://github.com/hadley/ggplot2-book/blob/01734fa9361eb1ce6ff954d77fa2d6c01f10430b/extensions.Rmd#L21

I think I found a nice example that can be considered for the book. From R 4.2+, the {grid} package has added the option to do outline strokes for text. The example defines a new text element that draws outlined text instead of regular text.

For the class constructor, it shows how to (1) allow alternate spellings of color/colour (2) how to structure a class and (3) how to inherit from element_text.

library(ggplot2)
library(grid)
library(rlang)

element_text_outline <- function(
  family = NULL, face = NULL, colour = NULL, fill = NULL,
  size = NULL, hjust = NULL, vjust = NULL, angle = NULL, 
  lineheight = NULL, linewidth = NULL,
  color = NULL, margin = NULL, inherit.blank = FALSE
) {
  # Allow both spellings of colo(u)r
  if (!is.null(color)) {
    colour <- color
  }

  structure(
    # The core of the class is a list of parameters
    list(
      family = family,
      face   = face,
      colour = colour,
      fill   = fill,
      size   = size,
      hjust  = hjust,
      vjust  = vjust,
      angle  = angle,
      lineheight = lineheight,
      linewidth  = linewidth,
      margin = margin,
      inherit.blank = inherit.blank
    ),
    # The class should inherit from one of the existing classes
    class = c("element_text_outline", "element_text", "element")
  )
}

The grob drawing code avoids getting into the nitty {grid}dy of margins and such by recycling the element_text's code with NextMethod(). It also shows the override rules that arguments provided to element_grob() should override those provided by the element itself.

element_grob.element_text_outline <- function(
    element, 
    # If we're using the parent's method, we can save ourselves
    # some space by using the ellipsis.
    ..., 
    fill = NULL,
    linewidth = NULL
) {
  # You can use the parent class' method to draw the basic grob
  text <- NextMethod()

  # Provided arguments should override the element's settings
  fill <- fill %||% element$fill
  linewidth <- linewidth %||% element$linewidth

  gp <- gpar(fill = fill, lwd = linewidth)

  # Wrap every textGrob inside a fillStrokeGrob
  text$children <- lapply(text$children, fillStrokeGrob, gp = gp)
  text
}

Finally, an example plot to show that the new element can be used in various places.

ggplot(mpg, aes(displ, cty, colour = factor(cyl))) +
  geom_point() +
  theme(
    axis.title.x = element_text_outline(size = 18),
    legend.text  = element_text_outline(
      size = 12, linewidth = 0.5,
      colour = "red", fill = "gold"
    )
  )

Created on 2022-08-10 by the reprex package (v2.0.1)

One drawback is that {ragg} does not (yet) support these R 4.2 features, so this reprex was rendered with:

#+ setup, include=FALSE
knitr::opts_chunk$set(dev.args = list(png = list(type = "cairo")))