Plotting with markdown text

The ggtext package defines two new geoms, geom_richtext() and geom_textbox(), which can be used to plot with markdown text. They draw simple text labels (without word wrap) and textboxes (with word wrap), respectively.

Simple text labels

Markdown-formatted text labels can be placed into a plot with geom_richtext(). This geom is mostly a drop-in replacement for geom_label() (or geom_text()), with added capabilities.

As a first example, we will annotate a plot of linear regressions with their r2 values. We will use the iris dataset for this demonstration. In our first iteration, we will not yet use any ggtext features, and instead plot the text with geom_text().

library(ggplot2)
library(dplyr)
library(glue)

iris_cor <- iris %>% 
  group_by(Species) %>%
  summarize(r_square = cor(Sepal.Length, Sepal.Width)^2) %>%
  mutate(
    # location of each text label in data coordinates
    Sepal.Length = 8, Sepal.Width = 4.5,
    # text label containing r^2 value 
    label = glue("r^2 = {round(r_square, 2)}")
  )

iris_cor
#> # A tibble: 3 × 5
#>   Species    r_square Sepal.Length Sepal.Width label     
#>   <fct>         <dbl>        <dbl>       <dbl> <glue>    
#> 1 setosa        0.551            8         4.5 r^2 = 0.55
#> 2 versicolor    0.277            8         4.5 r^2 = 0.28
#> 3 virginica     0.209            8         4.5 r^2 = 0.21

iris_facets <- ggplot(iris, aes(Sepal.Length, Sepal.Width)) +
  geom_point() + 
  geom_smooth(method = "lm", formula = y ~ x) +
  facet_wrap(~Species) +
  theme_bw()

iris_facets + 
  geom_text(
    data = iris_cor,
    aes(label = label),
    hjust = 1, vjust = 1
  )

This code works, but the result is not fully satisfying. First, because r is a mathematical variable, it should be typeset in italics. Second, it would be nicer to have a superscript 2 instead of ^2. We can achieve both results by creating a markdown label and plotting it with geom_richtext().

library(ggtext)

iris_cor_md <- iris_cor %>% 
  mutate(
    # markdown version of text label
    label = glue("*r*<sup>2</sup> = {round(r_square, 2)}")
  )

iris_cor_md
#> # A tibble: 3 × 5
#>   Species    r_square Sepal.Length Sepal.Width label                 
#>   <fct>         <dbl>        <dbl>       <dbl> <glue>                
#> 1 setosa        0.551            8         4.5 *r*<sup>2</sup> = 0.55
#> 2 versicolor    0.277            8         4.5 *r*<sup>2</sup> = 0.28
#> 3 virginica     0.209            8         4.5 *r*<sup>2</sup> = 0.21

iris_facets + 
  geom_richtext(
    data = iris_cor_md,
    aes(label = label),
    hjust = 1, vjust = 1
  )

By default, geom_richtext() puts a box around the text it draws. We can suppress the box by setting the fill and outline colors to transparent (fill = NA, label.colour = NA).

iris_facets + 
  geom_richtext(
    data = iris_cor_md,
    aes(label = label),
    hjust = 1, vjust = 1,
    # remove label background and outline
    fill = NA, label.color = NA,
    # remove label padding, since we have removed the label outline
    label.padding = grid::unit(rep(0, 4), "pt") 
  )

We can separately choose the colors of label outline, label fill, and label text, and we can assign them via aesthetic mapping as well as by direct specification, as is usual in ggplot2.

iris_facets + 
  aes(colour = Species) +
  geom_richtext(
    data = iris_cor_md,
    aes(
      label = label,
      fill = after_scale(alpha(colour, .2))
    ),
    text.colour = "black",
    hjust = 1, vjust = 1
  ) +
  theme(legend.position = "none")

Rotated labels are also possible, though in most cases it is not recommended to use them.

iris_facets + 
  aes(colour = Species) +
  geom_richtext(
    data = iris_cor_md,
    aes(
      x = 7.5,
      label = label,
      fill = after_scale(alpha(colour, .2))
    ),
    text.colour = "black",
    hjust = 1, vjust = 1,
    angle = 30
  ) +
  theme(legend.position = "none")

Text boxes

Markdown-formatted text boxes (with word wrap) can be placed into a plot with geom_textbox(). It is generally necessary to specify a width for the box. Widths are specified in grid units, and both absolute (e.g., "cm", "pt", or "in") and relative ("npc", Normalised Parent Coordinates) units are possible.

df <- data.frame(
  x = 0.1,
  y = 0.8,
  label = "*Lorem ipsum dolor sit amet,* consectetur adipiscing
elit. Quisque tincidunt eget arcu in pulvinar. Morbi varius leo
vel consectetur luctus. **Morbi facilisis justo non fringilla.**
Vivamus sagittis sem felis, vel lobortis risus mattis eget. Nam
quis imperdiet felis, in convallis elit."
)

p <- ggplot() +
  geom_textbox(
    data = df,
    aes(x, y, label = label),
    width = grid::unit(0.73, "npc"), # 73% of plot panel width
    hjust = 0, vjust = 1
  ) +
  xlim(0, 1) + ylim(0, 1)

p

If we specify a relative width, then changing the size of the plot will change the size of the textbox. The text will reflow to accommodate this change.

p

The parameters hjust and vjust align the box relative to the reference point specified by x and y, but they do not affect the alignment of text inside the box. To specify how text is aligned inside the box, use halign and valign. For example, halign = 0.5 generates centered text.

ggplot() +
  geom_textbox(
    data = df,
    aes(x, y, label = label),
    width = grid::unit(0.73, "npc"), # 73% of plot panel width
    hjust = 0, vjust = 1,
    halign = 0.5 # centered text
  ) +
  xlim(0, 1) + ylim(0, 1)

While text boxes cannot be rotated arbitrarily, they can be placed in four distinct orientations, corresponding to rotations by multiples of 90 degrees. Note that hjust and vjust are specified relative to this orientation.

df <- data.frame(
  x = 0.5,
  y = 0.5,
  label = "The quick brown fox jumps over the lazy dog.",
  orientation = c("upright", "left-rotated", "inverted", "right-rotated")
)

ggplot() +
  geom_textbox(
    data = df,
    aes(x, y, label = label, orientation = orientation),
    width = grid::unit(1.5, "in"),
    height = grid::unit(1.5, "in"),
    box.margin = grid::unit(rep(0.25, 4), "in"),
    hjust = 0, vjust = 1
  ) +
  xlim(0, 1) + ylim(0, 1) +
  scale_discrete_identity(aesthetics = "orientation")

The previous example uses the box.margin argument to create some space between the reference point given by x, y and the box itself. This margin is part of the size calculation for the box, so that a width of 1.5 inches with 0.25 inch margins yields an actual box of 1 inch in width.