Text rendering and font use

Thomas Lin Pedersen

Key takeaway today:

Text rendering is hard (for Thomas), but doesn’t have to be (for me)

Today

  • Selecting the right typeface
    • The systemfonts package and why graphics devices matters
    • How it just works
    • Using web fonts
  • Apply rich formatting
    • The marquee package
    • marquee-flavoured markdown
    • Using it in ggplot2

Selecting the right typeface

ggplot(penguins) +
  geom_point(aes(x = bill_len, y = body_mass)) +
  theme_minimal(base_family = "FANCY FONT")
Warning: Removed 2 rows containing missing values or values outside the scale range
(`geom_point()`).
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, : font
family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, : font
family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, : font
family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, : font
family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, : font
family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, : font
family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, : font
family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_stringMetric, as.graphicsAnnot(x$label)): font family
'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, : font
family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, : font
family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family 'FANCY FONT' not found in PostScript font database
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family 'FANCY FONT' not found in PostScript font database

The impossible life of a graphics device

Here is a family name and some text — go fetch

  • Where are the font files (OS dependent)
  • How do I read them once I find them
  • How do I determine which font to use
  • How do I even render text???

The impossible life of a graphics device

systemfonts

It (should) just work

library(systemfonts)

dplyr::glimpse(system_fonts())
Rows: 181
Columns: 9
$ path      <chr> "/tmp/RtmpqhYFuq/Barrio-regular.ttf", "/tmp/RtmpqhYFuq/Monte…
$ index     <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ name      <chr> "Barrio-Regular", "MonteCarlo-Regular", "Datalegreya-Thin", …
$ family    <chr> "Barrio", "MonteCarlo", "Datalegreya", "Datalegreya", "Datal…
$ style     <chr> "Regular", "Regular", "Thin", "Gradient", "Dot", "ExtraLight…
$ weight    <ord> normal, normal, light, normal, bold, ultralight, ultralight,…
$ width     <ord> normal, normal, normal, normal, normal, normal, normal, norm…
$ italic    <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, TRUE,…
$ monospace <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALS…

systemfonts

It (should) just work

ggplot(penguins) +
  geom_point(
    aes(
      x = bill_len,
      y = body_mass
    )
  ) +
  theme_minimal(
    base_family = "Barrio"
  )

systemfonts

It (should) just work

ggplot(penguins) +
  geom_point(
    aes(
      x = bill_len,
      y = body_mass
    )
  ) +
  theme_minimal(
    base_family = "Barrio"
  ) +
  labs(
    title = "I speak emoji 🤓"
  )

systemfonts

Problem:

  • I’m not allowed to install fonts on my computer

Solution

  • Use add_fonts() to register local font files

systemfonts

Problem:

  • I don’t want to call add_fonts() every time I start R

Solution

  • Place font files in ~/fonts or ./fonts. These two locations will automatically get scanned when systemfonts is loaded

systemfonts

Problem:

  • I don’t like using a web browser to find fonts

Solution

  • Download and add fonts from web repositories directly using get_from_google_fonts() and get_from_font_squirrel()

systemfonts

Problem:

  • I don’t know if the font is available on the computer my script will be running on

Solution

  • Use require_font() to ensure the font is installed from a web repository if missing (or error if it can’t be found there)

    (this slideshow has require_font("Barrio") in the beginning so it can render everywhere)

Exercise

Apply rich formatting

 

The limitations of R

  • font selection is limited.
    • Two levels of boldness (normal and bold)
    • Italic on/off
  • All text need to share the same font

Apply rich formatting

  • ggtext by Claus Wilke
    • Works within the bounds of the old graphics engine limits
    • Uses a mix of basic markdown and a subset of HTML
    • Doesn’t have a hex logo
  • marquee by me
    • Uses very new and fancy functionality I got Paul to add to R
    • Uses a superset of CommonMark syntax
    • Does have a logo

marquee

  • Input text is treated as markdown and used for formatting
  • CommonMark (basically what you expect) is supported
  • Custom spans are supported

marquee

library(marquee)

text <- "
Now you too can **sound like an llm**

_Here is what you need:_

* Add boldness to text at random
* Put everything in lists  
✅ Make those lists use emojis

Remember to delve into the intricate 
words that **underscore your potential**
"

grid::grid.draw(marquee_grob(text))

Custom spans

marquee uses cli-esque syntax for custom spans.

  • {.red color me red} will change the color of text to red
  • {#00FF00 color me green} will change the color to green
  • {.30 make me big} will set the size to 30
  • {.class put a class on it} will add the classclass` to the text
    • this will become relevant when we talk about styling
  • {.sub supscript} and {.sup superscript} are predefined custom spans

Custom spans

text <- "
Some {.red *red* text} and some {#00FF00 *green* text}
walked into a {.30 huge} bar and said to the {.person barman}:
”Can I get a glass of H{.sub 2}O?”
"

grid::grid.draw(marquee_grob(text))

Exercise

Supercharged images

p <- last_plot()
point <- grid::pointsGrob(0.5, 0.5)

text <- "
We made a *plot* earlier. Let's have a look:

![](p)

It can even work with grobs: ![](point)
"

grid::grid.draw(marquee_grob(text))

Tables

No markdown table support, but…

table <- gt::gt(penguins[1:6, 1:4])

text <- "
Why use markdown when you have gt

![](table)
"

grid::grid.draw(marquee_grob(text))

Styling

  • So far we have used the default style which matches standard (GitHub) markdown rendering

  • marquee provides a simple but powerful styling system

    classic_style()
    <marquee_style_set[1]>
    [1] <base, body, ul, ol, li, hr, h1, h2, h3, h4, h5, h6, cb, p, qb, em, str, a, code, u, del, img, sub, sup, out>

Styling

  • Each tag can be styled with 37 different properties

    names(classic_style()[[1]]$base)
     [1] "size"               "background"         "color"             
     [4] "family"             "weight"             "italic"            
     [7] "width"              "features"           "lineheight"        
    [10] "align"              "tracking"           "indent"            
    [13] "hanging"            "margin_top"         "margin_right"      
    [16] "margin_bottom"      "margin_left"        "padding_top"       
    [19] "padding_right"      "padding_bottom"     "padding_left"      
    [22] "border"             "border_size_top"    "border_size_right" 
    [25] "border_size_bottom" "border_size_left"   "border_radius"     
    [28] "outline"            "outline_width"      "outline_join"      
    [31] "outline_mitre"      "bullets"            "underline"         
    [34] "strikethrough"      "baseline"           "img_asp"           
    [37] "text_direction"    

Styling

  • Styling is powered by direct inheritance
  • Any NULL value will get it’s value from the parent
  • Any rel() value will multiply the parent value with the rel value
  • Use em() to make a value dependent on the text size of the element
  • Use rem() to make a value dependent on the text size of the root element
  • Use skip_inherit() to make a value uninheritable

Don’t roll your own — modify classic_style()

Styling

new_style <- classic_style(base_size = 30) |> 
  modify_style(
    "fancy",
    family = "MonteCarlo",
    weight = "light",
    features = font_feature(
      ligatures = c("contextual", "discretionary"),
      letters = c("swash", "stylistic"),
      swsh = 1
    )
  ) |> 
  modify_style(
    "plot",
    family = "Datalegreya",
    weight = "bold"
  )

Styling

text <- "
{.fancy Readability is overrated}

{.plot Even when it  
c|2o|3n|2t|3[++]a|1i|2n|0[--]s|1[high [low  ]     data}
"

grid::grid.draw(
  marquee_grob(text, new_style)
)

Exercise

What about ggplot2

  • Apart from the low-level grob that package developers can use, marquee also include high-level ggplot2 functions
    • geom_marquee()
    • element_marquee()
    • guide_marquee()
  • These will eventually be included directly in ggplot2

geom_marquee()

  • Replacement for geom_text()/geom_label()
  • How do you reconcile the style aesthetic with the other text-related aesthetics?
  • Be aware of markdown (single linebreak is ignored)

geom_marquee()

penguins$outlier <- penguins$bill_len > 55 & 
  penguins$flipper_len < 190
penguins$text <- paste0(
  "Suspecious *Pygoscelis ", 
  tolower(penguins$species), 
  "*"
)
style <- classic_style() |> 
  modify_style("body", outline = "white")
ggplot(
  penguins, 
  aes(x = bill_len, y = flipper_len)
) + 
  geom_point(
    aes(colour = I(ifelse(outlier, "red", "black")))
  ) + 
  geom_marquee(
    aes(label = text), 
    data = penguins[penguins$outlier,],
    style = style,
    hjust = 1,
    vjust = 1
  )

geom_marquee()

ggplot(
  penguins, 
  aes(x = bill_len, y = flipper_len)
) + 
  geom_point(
    aes(colour = I(ifelse(outlier, "red", "black")))
  ) + 
  geom_marquee(
    aes(label = text), 
    data = penguins[penguins$outlier,],
    style = style,
    hjust = "right-ink",
    vjust = 1,
    width = grid::unit(5, "cm")
  )

element_marquee()

  • Replacement for `element_text()``
  • Properties are added to the base element in style
  • Automatic text wrapping (with some caveats)

element_marquee()

ggplot(
  penguins, 
  aes(x = bill_len, y = flipper_len)
) + 
  geom_point() +
  labs(
    title = "An overview of various *Pygoscelis* (penguin) species"
  ) + 
  theme(
    plot.title = element_marquee(family = "Spectral")
  )

guide_marquee()

  • A special guide for mixing long free-form text with legends
  • Conceptually a mix between a subtitle and a guide

guide_marquee()

ggplot(penguins) + 
  geom_point(aes(x = bill_len, y = flipper_len, colour = species)) + 
  scale_color_discrete(
    name = "
Overview of how bill length and flipper length relate to each other
in the penguin species *Adelie* <<1>>, *Chinstrap* <<2>>, and 
*Gentoo* <<3>>.",
    guide = guide_marquee(width = grid::unit(2.75, "in"))
  )

guide_marquee()

ggplot(penguins) + 
  geom_point(aes(x = bill_len, y = flipper_len, colour = species)) + 
  scale_color_discrete(
    name = "
Overview of how bill length and flipper length relate to each other
in the penguin species {.1 *Adelie*}, {.2 *Chinstrap*}, and 
{.3 *Gentoo*}.",
    guide = guide_marquee(width = grid::unit(2.75, "in"))
  )

[Exercise](../exercises/3_text_fonts.qmd#using-geom_marquee(-in-ggplot2)

Next session: Styling your plot