Self-documenting plots in ggplot2

Including plotting code as an annotation on a plot
r
rlang
ggplot2
nonstandard evaluation
Photo credit: Slava Kuzminsk
Author

TJ Mahr

Published

March 10, 2022

Modified

July 28, 2023

When I am showing off a plotting technique in ggplot2, I sometimes like to include the R code that produced the plot as part of the plot. Here is an example I made to demonstrate the debug parameter in element_text():

library(ggplot2)

self_document(
  ggplot(mtcars, aes(x = mpg)) +
    geom_histogram(bins = 20, color = "white") +
    labs(title = "A basic histogram") +
    theme(axis.title = element_text(debug = TRUE))
)

A ggplot2 plot of a histogram with the plotting code above the image. The plot theme includes yellow shading and points in the x and y axis titles.

Let’s call these “self-documenting plots”. If we’re feeling nerdy, we might also call them “qquines”, although they are not true quines.

In this post, we will build up a self_document() function from scratch. Here are the problems we need to sort out:

Creating the code annotation

As a first step, let’s just treat our plotting code as a string that is ready to use for annotation.

p_text <- 'ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(bins = 20, color = "white") +
  labs(title = "A basic histogram")'

p_plot <- ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(bins = 20, color = "white") +
  labs(title = "A basic histogram")

In order to have a titled plot along with this annotation, we need some way to combine these two graphical objects together (the code and the plot produced by ggplot2). I like the patchwork package for this job. Here we use wrap_elements() to capture the plot into a “patch” that patchwork can annotate.

library(patchwork)
wrap_elements(p_plot) + 
  plot_annotation(title = p_text)

A ggplot2 plot of a histogram with the plotting code above the image. Here the title is in the default font.

Let’s style this title to use a monospaced font. I use Windows and like Consolas, so I will use that font.

# Use default mono font if "Consolas" is not available
extrafont::loadfonts(device = "win", quiet = TRUE)
monofont <- ifelse(
  extrafont::choose_font("Consolas") == "", 
  "mono", 
  "Consolas"
)

title_theme <- theme(
  plot.title = element_text(
    family = monofont, hjust = 0, size = rel(.9), 
    margin = margin(0, 0, 5.5, 0, unit = "pt")
  )
)

wrap_elements(p_plot) + 
  plot_annotation(title = p_text, theme = title_theme)  

A ggplot2 plot of a histogram with the plotting code above the image. Here the title is in Consolas.

One problem with this setup is that the plotting code has to be edited in two places: the plot p_plot and the title p_text. As a result, it’s easy for these two pieces of code to fall out of sync with each other, turning our self-documenting plot into a lying liar plot.

The solution is pretty easy: Tell R that p_text is code with parse() and evaluate the code with eval():

wrap_elements(eval(parse(text = p_text))) + 
  plot_annotation(title = p_text, theme = title_theme)  

A ggplot2 plot of a histogram with the plotting code above the image.

This works. It gets the job done. But we find ourselves in a clumsy workflow, either having to edit R code inside of quotes or editing the plot interactively and then having to wrap it in quotes. Let’s do better.

Capturing plotting code as a string

Time for some nonstandard evaluation. I will use the rlang package, although in principle we could use functions in base R to accomplish these goals.

First, we are going to use rlang::expr() to capture/quote/defuse the R code as an expression. We can print the code as code, print it as text, and use eval() to show the plot.

p_code <- rlang::expr(
  ggplot(mtcars, aes(x = mpg)) +
    geom_histogram(bins = 20, color = "white") +
    labs(title = "A basic histogram")
)

# print the expressions
p_code
#> ggplot(mtcars, aes(x = mpg)) + geom_histogram(bins = 20, color = "white") + 
#>     labs(title = "A basic histogram")

# expression => text
rlang::expr_text(p_code)
#> [1] "ggplot(mtcars, aes(x = mpg)) + geom_histogram(bins = 20, color = \"white\") + \n    labs(title = \"A basic histogram\")"

eval(p_code)

A ggplot2 plot of a histogram with the plotting code above the image.

Then, it should be straightforward to make the self-documenting plot, right?

p_code <- rlang::expr(
  ggplot(mtcars, aes(x = mpg)) +
    geom_histogram(bins = 20, color = "white") +
    labs(title = "A basic histogram")
)

wrap_elements(eval(p_code)) + 
  plot_annotation(title = rlang::expr_text(p_code), theme = title_theme)  

A ggplot2 plot of a histogram with the plotting code above the image. In this case, the title is mostly on one line and some text is cut off from the image.

Hey, it reformatted the title! Indeed, in the process of capturing the code, the code formatting was lost. To get something closer to the source code we provided, we have to reformat the captured code before we print it.

The styler package provides a suite of functions for reformatting code. We can define our own coding styles/formatting rules to customize how styler works. I like the styler rules used by Garrick Aden-Buie in his grkstyle package, so I will use grkstyle::grk_style_text() to reformat the code.

p_code <- rlang::expr(
  ggplot(mtcars, aes(x = mpg)) +
    geom_histogram(bins = 20, color = "white") +
    labs(title = "A basic histogram")
)

wrap_elements(eval(p_code)) + 
  plot_annotation(
    title = rlang::expr_text(p_code) |> 
      grkstyle::grk_style_text() |> 
      # reformatting returns a vector of lines,
      # so we have to combine them
      paste0(collapse = "\n"), 
    theme = title_theme
  ) 

A ggplot2 plot of a histogram with the plotting code above the image.

Putting it all together

When we write our self_document() function, the only change we have to make is using rlang::enexpr() instead rlang::expr(). The en-variant is used when we want to en-quote exactly what the user provided. Aside from that change, our self_document() function just bundles together all of the code we developed above:

self_document <- function(expr) {
  monofont <- ifelse(
    extrafont::choose_font("Consolas") == "", 
    "mono", 
    "Consolas"
  )
  
  p <- rlang::enexpr(expr)
  title <- rlang::expr_text(p) |> 
    grkstyle::grk_style_text() |> 
    paste0(collapse = "\n")
  
  patchwork::wrap_elements(eval(p)) + 
    patchwork::plot_annotation(
      title = title, 
      theme = theme(
        plot.title = element_text(
          family = monofont, hjust = 0, size = rel(.9), 
          margin = margin(0, 0, 5.5, 0, unit = "pt")
        )
      )
    )
}

And let’s confirm that it works.

library(ggplot2)
self_document(
  ggplot(mtcars, aes(x = mpg)) +
    geom_histogram(bins = 20, color = "white") +
    labs(title = "A basic histogram")
)

A ggplot2 plot of a histogram with the plotting code above the image.

Because we developed this function on top of rlang, we can do some tricks like injecting a variable’s value when capturing the code. For example, here I use !! color to replace the color variable with the actual value.

color <- "white"
self_document(
  ggplot(mtcars, aes(x = mpg)) +
    geom_histogram(bins = 20, color = !! color) +
    labs(title = "A basic histogram")
)

A ggplot2 plot of a histogram with the plotting code above the image.

And if you are wondering, yes, we can self_document() a self_document() plot.

self_document(
  self_document(
    ggplot(mtcars, aes(x = mpg)) +
      geom_histogram(bins = 20, color = "white") +
      labs(title = "A basic histogram")
  )
)

A self_document() plot of a plot of a histogram with the plotting code above the image. There are two sets of code on top of each other.

Alas, comments are lost

One downside of this approach is that helpful comments are lost.

self_document(
  ggplot(mtcars, aes(x = mpg)) +
    geom_histogram(bins = 20, color = !! color) +
    # get rid of that grey
    theme_minimal() +
    labs(title = "A basic histogram")
)

A ggplot2 plot of a histogram with the plotting code above the image.

I am not sure how to include comments. One place where comments are stored and printed is in function bodies:

f <- function() {
ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(bins = 20, color = !! color) +
  # get rid of that grey
  theme_minimal() +
  labs(title = "A basic histogram")
}

print(f, useSource = TRUE)
#> function() {
#> ggplot(mtcars, aes(x = mpg)) +
#>   geom_histogram(bins = 20, color = !! color) +
#>   # get rid of that grey
#>   theme_minimal() +
#>   labs(title = "A basic histogram")
#> }

I have no idea how to go about exploiting this feature for self-documenting plots, however.


Session info
─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.3.0 (2023-04-21 ucrt)
 os       Windows 11 x64 (build 22621)
 system   x86_64, mingw32
 ui       RTerm
 language (EN)
 collate  English_United States.utf8
 ctype    English_United States.utf8
 tz       America/Chicago
 date     2023-07-28
 pandoc   3.1.1 @ C:/Program Files/RStudio/resources/app/bin/quarto/bin/tools/ (via rmarkdown)
 quarto   1.3.353

─ Packages ───────────────────────────────────────────────────────────────────
 package     * version date (UTC) lib source
 cli           3.6.1   2023-03-23 [1] CRAN (R 4.3.0)
 colorspace    2.1-0   2023-01-23 [1] CRAN (R 4.3.0)
 digest        0.6.33  2023-07-07 [1] CRAN (R 4.3.1)
 dplyr         1.1.2   2023-04-20 [1] CRAN (R 4.3.0)
 evaluate      0.21    2023-05-05 [1] CRAN (R 4.3.0)
 extrafont     0.19    2023-01-18 [1] CRAN (R 4.3.0)
 extrafontdb   1.0     2012-06-11 [1] CRAN (R 4.3.0)
 fansi         1.0.4   2023-01-22 [1] CRAN (R 4.3.0)
 farver        2.1.1   2022-07-06 [1] CRAN (R 4.3.0)
 fastmap       1.1.1   2023-02-24 [1] CRAN (R 4.3.0)
 generics      0.1.3   2022-07-05 [1] CRAN (R 4.3.0)
 ggplot2     * 3.4.2   2023-04-03 [1] CRAN (R 4.3.0)
 glue          1.6.2   2022-02-24 [1] CRAN (R 4.3.0)
 grkstyle      0.2.1   2023-04-24 [1] Github (gadenbuie/grkstyle@2dac4d7)
 gtable        0.3.3   2023-03-21 [1] CRAN (R 4.3.0)
 htmltools     0.5.5   2023-03-23 [1] CRAN (R 4.3.0)
 htmlwidgets   1.6.2   2023-03-17 [1] CRAN (R 4.3.0)
 jsonlite      1.8.7   2023-06-29 [1] CRAN (R 4.3.1)
 knitr         1.43    2023-05-25 [1] CRAN (R 4.3.0)
 labeling      0.4.2   2020-10-20 [1] CRAN (R 4.3.0)
 lifecycle     1.0.3   2022-10-07 [1] CRAN (R 4.3.0)
 magrittr      2.0.3   2022-03-30 [1] CRAN (R 4.3.0)
 munsell       0.5.0   2018-06-12 [1] CRAN (R 4.3.0)
 patchwork   * 1.1.2   2022-08-19 [1] CRAN (R 4.3.0)
 pillar        1.9.0   2023-03-22 [1] CRAN (R 4.3.0)
 pkgconfig     2.0.3   2019-09-22 [1] CRAN (R 4.3.0)
 purrr         1.0.1   2023-01-10 [1] CRAN (R 4.3.0)
 R.cache       0.16.0  2022-07-21 [1] CRAN (R 4.3.0)
 R.methodsS3   1.8.2   2022-06-13 [1] CRAN (R 4.3.0)
 R.oo          1.25.0  2022-06-12 [1] CRAN (R 4.3.0)
 R.utils       2.12.2  2022-11-11 [1] CRAN (R 4.3.0)
 R6            2.5.1   2021-08-19 [1] CRAN (R 4.3.0)
 ragg          1.2.5   2023-01-12 [1] CRAN (R 4.3.0)
 rlang         1.1.1   2023-04-28 [1] CRAN (R 4.3.0)
 rmarkdown     2.23    2023-07-01 [1] CRAN (R 4.3.0)
 rprojroot     2.0.3   2022-04-02 [1] CRAN (R 4.3.0)
 rstudioapi    0.15.0  2023-07-07 [1] CRAN (R 4.3.1)
 Rttf2pt1      1.3.12  2023-01-22 [1] CRAN (R 4.3.0)
 scales        1.2.1   2022-08-20 [1] CRAN (R 4.3.0)
 sessioninfo   1.2.2   2021-12-06 [1] CRAN (R 4.3.0)
 styler        1.10.1  2023-06-05 [1] CRAN (R 4.3.1)
 systemfonts   1.0.4   2022-02-11 [1] CRAN (R 4.3.0)
 textshaping   0.3.6   2021-10-13 [1] CRAN (R 4.3.0)
 tibble        3.2.1   2023-03-20 [1] CRAN (R 4.3.0)
 tidyselect    1.2.0   2022-10-10 [1] CRAN (R 4.3.0)
 utf8          1.2.3   2023-01-31 [1] CRAN (R 4.3.0)
 vctrs         0.6.3   2023-06-14 [1] CRAN (R 4.3.1)
 withr         2.5.0   2022-03-03 [1] CRAN (R 4.3.0)
 xfun          0.39    2023-04-20 [1] CRAN (R 4.3.0)
 yaml          2.3.7   2023-01-23 [1] CRAN (R 4.3.0)

 [1] C:/Users/Tristan/AppData/Local/R/win-library/4.3
 [2] C:/Program Files/R/R-4.3.0/library

──────────────────────────────────────────────────────────────────────────────