Beautifully annotated: enhancing your ggplots with text

RLadies Cambridge | 23rd February 2023

Hi there đź‘‹ !

đź‘© Cara Thompson

👩‍💻 Psychology PhD |> Analysis of postgraduate medical examinations |> Freelance data consultant specialising in dataviz and “enhanced” reproducible outputs

đź’™ Helping others maximise the impact of their expertise


Today’s goal

To equip you with some design tips and coding tricks to make the most of text in your plots.

  • Explore how to be less dependent on annotations (I know… trust me!)
  • Illustrate ways in which we can use colour and fonts to add text hierarchy and story-enhancing annotations
  • Provide you with reusable code to implement these tips, introducing {ggtext}, {geomtextpath}
  • Point to additional resources you can explore in your own time (links below slides)
  • Give feedback on your own plots

But first, please suspend all disbelief…

The Great Penguin Bake Off

The penguins had a baking competition to see which species could make the best banana loaf. Each species was given bananas of a different level of ripeness.

The Great Penguin Bake Off

The penguins had a baking competition to see which species could make the best banana loaf. Each species was given bananas of a different level of ripeness.

The Great Penguin Bake Off

The Adelie penguins decided to experiment with different quantities of banana in their mix. Each island chose a different quantity.

The Great Penguin Bake Off

The Adelie penguins decided to experiment with different quantities of banana in their mix. Each island chose a different quantity.

The Great Penguin Bake Off

The penguins also baked their cakes for different amounts of time. Here are the mean durations per species. Which species left their cakes in the oven for longest?

The Great Penguin Bake Off

The penguins also baked their cakes for different amounts of time. Here are the mean durations per species. Which species left their cakes in the oven for longest?

Five tips for beautiful annotations

#1 - Use colour and orientation purposefully

#2 - Add text hierarchy

#3 - Reduce unnecessary eye movement

#4 - Highlight important patterns

#5 - See how much you can declutter

Bonus track - {gghighlight}, Tee pipe and curved arrows

Let’s get coding!

Setting up our first plot

Using the ToothGrowth dataset

  • Build into R for easy “codealongability”
  • Namespacing
    • package::function() 🕵️
  • Intriguing dataset (?ToothGrowth)
  • Research question with a pattern to visualise and annotate

(Feel free to munch along!)

Setting up our first plot

With a few tips along the way

ToothGrowth %>%
  group_by(supp, dose) %>%
  summarise(mean_length = mean(len)) %>%
  ggplot(aes(x = dose,
             y = mean_length,
             fill = supp)) +
  geom_bar(stat = "identity")

Setting up our first plot

With a few tips along the way

ToothGrowth %>%
  group_by(supp, dose) %>%
  summarise(mean_length = mean(len)) %>%
  ggplot(aes(x = dose,
             y = mean_length,
             fill = supp)) +
  geom_bar(stat = "identity",
           position = "dodge")

Setting up our first plot

With a few tips along the way

ToothGrowth %>%
  group_by(supp, dose) %>%
  summarise(mean_length = mean(len)) %>%
  ggplot(aes(x = dose,
             y = mean_length,
             fill = supp)) +
  geom_bar(stat = "identity",
           position = "dodge", 
           colour = "#FFFFFF",
           size = 2)

Setting up our first plot

Mini tip: get rid of abbreviations

ToothGrowth %>%
  mutate(supplement = 
           case_when(supp == "OJ" ~ "Orange Juice",
                     supp == "VC" ~ "Vitamin C",
                     TRUE ~ as.character(supp))) %>%
  group_by(supplement, dose) %>%
  summarise(mean_length = mean(len)) %>%
  ggplot(aes(x = dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(stat = "identity",
           position = "dodge", 
           colour = "#FFFFFF",
           size = 2)

Setting up our first plot

Mini tip: theme_minimal()

ToothGrowth %>%
  mutate(supplement = 
           case_when(supp == "OJ" ~ "Orange Juice", supp == "VC" ~ "Vitamin C", TRUE ~ as.character(supp))) %>%
  group_by(supplement, dose) %>%
  summarise(mean_length = mean(len)) %>%
  ggplot(aes(x = dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(stat = "identity",
           position = "dodge", 
           colour = "#FFFFFF",
           size = 2) +
  theme_minimal()

Setting up our first plot

Turning Dose into a categorical variable (fear not!)

ToothGrowth %>%
  mutate(supplement = case_when(supp == "OJ" ~ "Orange Juice", supp == "VC" ~ "Vitamin C", TRUE ~ as.character(supp))) %>%
  group_by(supplement, dose) %>%
  summarise(mean_length = mean(len)) %>%
  mutate(categorical_dose = factor(dose)) %>%
  ggplot(aes(x = categorical_dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(stat = "identity",
           position = "dodge",
           colour = "#FFFFFF", 
           size = 2) +
  theme_minimal()

Setting up our first plot

Turning Dose into a categorical variable (fear not!) + facetting

ToothGrowth %>%
  mutate(supplement = case_when(supp == "OJ" ~ "Orange Juice", supp == "VC" ~ "Vitamin C", TRUE ~ as.character(supp))) %>%
  group_by(supplement, dose) %>%
  summarise(mean_length = mean(len)) %>%
  mutate(categorical_dose = factor(dose)) %>%
  ggplot(aes(x = categorical_dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(stat = "identity",
           position = "dodge",
           colour = "#FFFFFF", 
           size = 2) + 
  facet_wrap(supplement ~ ., ncol = 1) +
  theme_minimal()

Setting up our first plot

Adding some text (finally!)

ToothGrowth %>%
  mutate(supplement = case_when(supp == "OJ" ~ "Orange Juice", supp == "VC" ~ "Vitamin C", TRUE ~ as.character(supp))) %>%
  group_by(supplement, dose) %>%
  summarise(mean_length = mean(len)) %>%
  mutate(categorical_dose = factor(dose)) %>%
  ggplot(aes(x = categorical_dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(stat = "identity",
           position = "dodge",
           colour = "#FFFFFF", 
           size = 2) + 
  labs(x = "Dose",
       y = "Mean length (mm)",
       title = "In smaller doses, Orange Juice was associated with greater mean tooth growth,
compared to equivalent doses of Vitamin C",
       subtitle = "With the highest dose, the mean recorded length was almost identical.") +
  facet_wrap(supplement ~ ., ncol = 1) +
  theme_minimal()

#1 - Use colour and orientation purposefully

Legend + facet strip + colour + title… Wait, which one is which?

#1 - Use colour and orientation purposefully

  • Orange juice is… orange!
  • Vitamin C is… also orange, but more red and “aggressive”
  • Those green leaves look nice with those colours…
  • imagecolorpicker.com

#1 - Use colour and orientation purposefully

Generating a colour palette, starting with orange juice! #fab909

monochromeR::generate_palette("#db5a05", blend_colour = "red", n_colours = 3, view_palette = TRUE)

[1] "#DB5A05" "#E93603" "#F71201"
monochromeR::generate_palette("#3c6b30", "go_darker", n_colours = 2, view_palette = TRUE)

[1] "#3C6B30" "#0C1509"
monochromeR::generate_palette("#0C1509", "go_lighter", n_colours = 6, view_palette = TRUE)

[1] "#0C1509" "#323A30" "#595F57" "#80857F" "#A7AAA6" "#CED0CD"

#1 - Use colour and orientation purposefully

Creating a named vector, for ease later

vit_c_palette <- c("Orange Juice" = "#fab909", 
                   "Vitamin C" = "#E93603",
                   light_text = "#323A30",
                   dark_text =  "#0C1509")

monochromeR::view_palette(vit_c_palette)

#1 - Use colour and orientation purposefully

Back to the plot!

ToothGrowth %>%
  mutate(supplement = case_when(supp == "OJ" ~ "Orange Juice", supp == "VC" ~ "Vitamin C", TRUE ~ as.character(supp))) %>%
  group_by(supplement, dose) %>%
  summarise(mean_length = mean(len)) %>%
  mutate(categorical_dose = factor(dose)) %>%
  ggplot(aes(x = categorical_dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(stat = "identity",
           position = "dodge",
           colour = "#FFFFFF", 
           size = 2) + 
  labs(x = "Dose",
       y = "Mean length (mm)",
       title = "In smaller doses, Orange Juice was associated with greater mean tooth growth,
compared to equivalent doses of Vitamin C",
       subtitle = "With the highest dose, the mean recorded length was almost identical.") +
  facet_wrap(supplement ~ ., ncol = 1) +
  theme_minimal()

#1 - Use colour and orientation purposefully

Add in our colours

ToothGrowth %>%
  mutate(supplement = case_when(supp == "OJ" ~ "Orange Juice", supp == "VC" ~ "Vitamin C", TRUE ~ as.character(supp))) %>%
  group_by(supplement, dose) %>%
  summarise(mean_length = mean(len)) %>%
  mutate(categorical_dose = factor(dose)) %>%
  ggplot(aes(x = categorical_dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(stat = "identity",
           position = "dodge",
           colour = "#FFFFFF", 
           size = 2) + 
  labs(x = "Dose",
       y = "Mean length (mm)",
       title = "In smaller doses, Orange Juice was associated with greater mean tooth growth,
compared to equivalent doses of Vitamin C",
       subtitle = "With the highest dose, the mean recorded length was almost identical.") +
  scale_fill_manual(values = vit_c_palette) +
  facet_wrap(supplement ~ ., ncol = 1) +
  theme_minimal()

#1 - Use colour and orientation purposefully

Use transparency to indicate dose

ToothGrowth %>%
  mutate(supplement = case_when(supp == "OJ" ~ "Orange Juice", supp == "VC" ~ "Vitamin C", TRUE ~ as.character(supp))) %>%
  group_by(supplement, dose) %>%
  summarise(mean_length = mean(len)) %>%
  mutate(categorical_dose = factor(dose)) %>%
  ggplot(aes(x = categorical_dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(aes(alpha = dose),
           stat = "identity",
           position = "dodge",
           colour = "#FFFFFF", 
           size = 2) + 
  labs(x = "Dose",
       y = "Mean length (mm)",
       title = "In smaller doses, Orange Juice was associated with greater mean tooth growth,
compared to equivalent doses of Vitamin C",
       subtitle = "With the highest dose, the mean recorded length was almost identical.") +
  scale_fill_manual(values = vit_c_palette) +
  facet_wrap(supplement ~ ., ncol = 1) +
  theme_minimal()

#1 - Use colour and orientation purposefully

Use transparency to indicate dose - within limits

ToothGrowth %>%
  mutate(supplement = case_when(supp == "OJ" ~ "Orange Juice", supp == "VC" ~ "Vitamin C", TRUE ~ as.character(supp))) %>%
  group_by(supplement, dose) %>%
  summarise(mean_length = mean(len)) %>%
  mutate(categorical_dose = factor(dose)) %>%
  ggplot(aes(x = categorical_dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(aes(alpha = dose),
           stat = "identity",
           position = "dodge",
           colour = "#FFFFFF", 
           size = 2) + 
  labs(x = "Dose",
       y = "Mean length (mm)",
       title = "In smaller doses, Orange Juice was associated with greater mean tooth growth,
compared to equivalent doses of Vitamin C",
       subtitle = "With the highest dose, the mean recorded length was almost identical.") +
  scale_fill_manual(values = vit_c_palette) +
  scale_alpha(range = c(0.33, 1)) +
  facet_wrap(supplement ~ ., ncol = 1) +
  theme_minimal()

#1 - Use colour and orientation purposefully

What is the dose unit again? ?ToothGrowth

ToothGrowth %>%
  mutate(supplement = case_when(supp == "OJ" ~ "Orange Juice", supp == "VC" ~ "Vitamin C", TRUE ~ as.character(supp))) %>%
  group_by(supplement, dose) %>%
  summarise(mean_length = mean(len)) %>%
  mutate(categorical_dose = factor(dose)) %>%
  ggplot(aes(x = categorical_dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(aes(alpha = dose),
           stat = "identity",
           position = "dodge",
           colour = "#FFFFFF", 
           size = 2) + 
  labs(x = "Dose",
       y = "Mean length (mm)",
       title = "In smaller doses, Orange Juice was associated with greater mean tooth growth,
compared to equivalent doses of Vitamin C",
       subtitle = "With the highest dose, the mean recorded length was almost identical.") +
  scale_fill_manual(values = vit_c_palette) +
  scale_alpha(range = c(0.33, 1)) +
  scale_x_discrete(breaks = c("0.5", "1", "2"), 
                   labels = function(x) 
                     paste0(x, " mg/day")) +
  facet_wrap(supplement ~ ., ncol = 1) +
  theme_minimal()

#1 - Use colour and orientation purposefully

Legend has always been redundant!

ToothGrowth %>%
  mutate(supplement = case_when(supp == "OJ" ~ "Orange Juice", supp == "VC" ~ "Vitamin C", TRUE ~ as.character(supp))) %>%
  group_by(supplement, dose) %>%
  summarise(mean_length = mean(len)) %>%
  mutate(categorical_dose = factor(dose)) %>%
  ggplot(aes(x = categorical_dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(aes(alpha = dose),
           stat = "identity",
           position = "dodge",
           colour = "#FFFFFF", 
           size = 2) + 
  labs(x = "Dose",
       y = "Mean length (mm)",
       title = "In smaller doses, Orange Juice was associated with greater mean tooth growth,
compared to equivalent doses of Vitamin C",
       subtitle = "With the highest dose, the mean recorded length was almost identical.") +
  scale_fill_manual(values = vit_c_palette) +
  scale_alpha(range = c(0.33, 1)) +
  facet_wrap(supplement ~ ., ncol = 1) +
  scale_x_discrete(breaks = c("0.5", "1", "2"), labels = function(x) paste0(x, " mg/day")) +
  theme_minimal() +
  theme(legend.position = "none")

#1 - Use colour and orientation purposefully

And I find this so much less confusing!

ToothGrowth %>%
  mutate(supplement = case_when(supp == "OJ" ~ "Orange Juice", supp == "VC" ~ "Vitamin C", TRUE ~ as.character(supp))) %>%
  group_by(supplement, dose) %>%
  summarise(mean_length = mean(len)) %>%
  mutate(categorical_dose = factor(dose)) %>%
  ggplot(aes(x = categorical_dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(aes(alpha = dose),
           stat = "identity",
           position = "dodge",
           colour = "#FFFFFF", 
           size = 2) + 
  labs(x = "Dose",
       y = "Mean length (mm)",
       title = "In smaller doses, Orange Juice was associated with greater mean tooth growth,
compared to equivalent doses of Vitamin C",
       subtitle = "With the highest dose, the mean recorded length was almost identical.") +
  scale_fill_manual(values = vit_c_palette) +
  scale_alpha(range = c(0.4, 1)) +
  scale_x_discrete(breaks = c("0.5", "1", "2"), labels = function(x) paste0(x, " mg/day")) +
  coord_flip() +
  facet_wrap(supplement ~ ., ncol = 1) +
  theme_minimal() +
  theme(legend.position = "none")

#1 - Use colour and orientation purposefully

So much clearer, and we haven’t even done any annotating!

Five tips for beautiful annotations

#1 - Use colour and orientation purposefully

#2 - Add text hierarchy

#3 - Reduce unnecessary eye movement

#4 - Highlight important patterns

#5 - See how much you can declutter

Bonus track - {gghighlight}, Tee pipe and curved arrows

#2 - Add text hierarchy

#2 - Add text hierarchy

Time to start playing with theme()!

basic_plot <- ToothGrowth %>%
  mutate(supplement = case_when(supp == "OJ" ~ "Orange Juice", supp == "VC" ~ "Vitamin C", TRUE ~ as.character(supp))) %>%
  group_by(supplement, dose) %>%
  summarise(mean_length = mean(len)) %>%
  mutate(categorical_dose = factor(dose)) %>%
  ggplot(aes(x = categorical_dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(aes(alpha = dose),
           stat = "identity",
           position = "dodge",
           colour = "#FFFFFF", 
           size = 2) + 
  labs(x = "Dose",
       y = "Mean length (mm)",
       title = "In smaller doses, Orange Juice was associated with greater mean tooth growth,
compared to equivalent doses of Vitamin C",
       subtitle = "With the highest dose, the mean recorded length was almost identical.") +
  scale_fill_manual(values = vit_c_palette) +
  scale_alpha(range = c(0.4, 1)) +
  scale_x_discrete(breaks = c("0.5", "1", "2"), labels = function(x) paste0(x, " mg/day")) +
  coord_flip() +
  facet_wrap(supplement ~ ., ncol = 1) +
  theme_minimal(base_size = 15) +
  theme(legend.position = "none")

basic_plot

#2 - Add text hierarchy

Time to start playing with theme()!

basic_plot +
  theme(legend.position = "none",
        text = element_text(colour = vit_c_palette["light_text"]))

#2 - Add text hierarchy

Time to start playing with theme()!

basic_plot +
  theme(legend.position = "none",
        text = element_text(colour = vit_c_palette["light_text"]),
        plot.title = element_text(colour = vit_c_palette["dark_text"]))

#2 - Add text hierarchy

Time to start playing with theme()!

basic_plot +
  theme(legend.position = "none",
        text = element_text(colour = vit_c_palette["light_text"]),
        plot.title = element_text(colour = vit_c_palette["dark_text"], 
                                  size = rel(1.5), 
                                  face = "bold"))

#2 - Add text hierarchy

Move away from the default fonts

basic_plot +
  theme(legend.position = "none",
        text = element_text(colour = vit_c_palette["light_text"],
                            family = "Cabin"),
        plot.title = element_text(colour = vit_c_palette["dark_text"], 
                                  size = rel(1.5), 
                                  face = "bold",
                                  family = "Enriqueta"))

#2 - Add text hierarchy

Move away from the default fonts

basic_plot +
  theme(legend.position = "none",
        text = element_text(colour = vit_c_palette["light_text"],
                            family = "Cabin"),
        plot.title = element_text(colour = vit_c_palette["dark_text"], 
                                  size = rel(1.5), 
                                  face = "bold",
                                  family = "Enriqueta"),
        strip.text = element_text(family = "Enriqueta",
                                  colour = vit_c_palette["light_text"], 
                                  size = rel(1.1), face = "bold"),
        axis.text = element_text(colour = vit_c_palette["light_text"]))

#2 - Add text hierarchy

Choosing fonts can be tricky!

  • Brand guidelines
  • Datawrapper guidance - avoid fonts that are too wide/narrow!
  • Websites + inspector tool
  • Oliver Schöndorfer’s exploration of the Font Matrix

#2 - Add text hierarchy

Getting custom fonts to work can be frustrating!

Install fonts locally + {ragg} + {systemfonts} + {textshaping} + Set graphics device to “AGG” + 🤞


#2 - Add text hierarchy

Give everything some space to breathe

basic_plot +
  theme(legend.position = "none",
        text = element_text(colour = vit_c_palette["light_text"], 
                            family = "Cabin"),
        plot.title = element_text(colour = vit_c_palette["dark_text"], 
                                  size = rel(1.5), 
                                  face = "bold",
                                  family = "Enriqueta",
                                  lineheight = 1.3,
                                  margin = margin(0.5, 0, 1, 0, "lines")),
        plot.subtitle = element_text(size = rel(1.1), lineheight = 1.3,
                                     margin = margin(0, 0, 1, 0, "lines")),
        strip.text = element_text(family = "Enriqueta",
                                  colour = vit_c_palette["light_text"], 
                                  size = rel(1.1), face = "bold",
                                  margin = margin(2, 0, 0.5, 0, "lines")),
        axis.text = element_text(colour = vit_c_palette["light_text"]))

#2 - Add text hierarchy

Remove unnecessary text

basic_plot +
  theme(legend.position = "none",
        text = element_text(colour = vit_c_palette["light_text"], 
                            family = "Cabin"),
        axis.title.y = element_blank(),
        plot.title = element_text(colour = vit_c_palette["dark_text"], 
                                  size = rel(1.5), 
                                  face = "bold",
                                  family = "Enriqueta",
                                  lineheight = 1.3,
                                  margin = margin(0.5, 0, 1, 0, "lines")),
        plot.subtitle = element_text(size = rel(1.1), lineheight = 1.3,
                                     margin = margin(0, 0, 1, 0, "lines")),
        strip.text = element_text(family = "Enriqueta",
                                  colour = vit_c_palette["light_text"], 
                                  size = rel(1.1), face = "bold",
                                  margin = margin(2, 0, 0.5, 0, "lines")),
        axis.text = element_text(colour = vit_c_palette["light_text"]))

#2 - Add text hierarchy

Watch out for that title!

basic_plot +
  labs(title = "In smaller doses, Orange Juice was associated with greater mean tooth growth,
compared to equivalent doses of Vitamin C") +
  theme(legend.position = "none",
        text = element_text(colour = vit_c_palette["light_text"],
                            family = "Cabin"),
        axis.title.y = element_blank(),
        plot.title = element_text(colour = vit_c_palette["dark_text"], 
                                  size = 36, 
                                  face = "bold",
                                  family = "Enriqueta",
                                  lineheight = 1.3,
                                  margin = margin(0.5, 0, 1, 0, "lines")),
        plot.subtitle = element_text(size = rel(1.1), lineheight = 1.3,
                                     margin = margin(0, 0, 1, 0, "lines")),
        strip.text = element_text(family = "Enriqueta",
                                  colour = vit_c_palette["light_text"], 
                                  size = rel(1.1), face = "bold",
                                  margin = margin(2, 0, 0.5, 0, "lines")),
        axis.text = element_text(colour = vit_c_palette["light_text"]))

#2 - Add text hierarchy

Watch out for that title!

basic_plot +
  labs(title = "In smaller doses, Orange Juice was associated with greater mean tooth growth, compared to equivalent doses of Vitamin C") +
  theme(legend.position = "none",
        text = element_text(colour = vit_c_palette["light_text"],
                            family = "Cabin"),
        axis.title.y = element_blank(),
        plot.title = element_text(colour = vit_c_palette["dark_text"], 
                                  size = rel(1.5), 
                                  face = "bold",
                                  family = "Enriqueta",
                                  lineheight = 1.3,
                                  margin = margin(0.5, 0, 1, 0, "lines")),
        plot.subtitle = element_text(size = rel(1.1), lineheight = 1.3,
                                     margin = margin(0, 0, 1, 0, "lines")),
        strip.text = element_text(family = "Enriqueta",
                                  colour = vit_c_palette["light_text"], 
                                  size = rel(1.1), face = "bold",
                                  margin = margin(2, 0, 0.5, 0, "lines")),
        axis.text = element_text(colour = vit_c_palette["light_text"]))

#2 - Add text hierarchy

I ❤️ 📦 {ggtext}

basic_plot +
  labs(title = "In smaller doses, Orange Juice was associated with greater mean tooth growth, compared to equivalent doses of Vitamin C") +
  theme(legend.position = "none",
        text = element_text(colour = vit_c_palette["light_text"],
                            family = "Cabin"),
        axis.title.y = element_blank(),
        plot.title = ggtext::element_textbox_simple(
          colour = vit_c_palette["dark_text"], 
          size = rel(1.5), 
          face = "bold",
          family = "Enriqueta",
          lineheight = 1.3,
          margin = margin(0.5, 0, 1, 0, "lines")),
        plot.subtitle = ggtext::element_textbox_simple(
          size = rel(1.1), 
          lineheight = 1.3,
          margin = margin(0, 0, 1, 0, "lines")),
        strip.text = element_text(family = "Enriqueta",
                                  colour = vit_c_palette["light_text"],
                                  size = rel(1.1), face = "bold",
                                  margin = margin(2, 0, 0.5, 0, "lines")),
        axis.text = element_text(colour = vit_c_palette["light_text"]))

#2 - Add text hierarchy + colour!

Hello, HTML + CSS!

We can make text <span style='color:green'>green</span> and also <span style='color:green; font-size:60pt'>really big</span>! 🤯


We can make text green and also really big! 🤯

#2 - Add text hierarchy + colour!

I ❤️ 📦 {ggtext}

basic_plot +
  labs(title = 
         paste0("In smaller doses, **<span style='color:",
                vit_c_palette["Orange Juice"], "'>Orange Juice</span>**
                      was associated with greater mean tooth growth,
                      compared to equivalent doses of **<span style='color:",
                vit_c_palette["Vitamin C"], "'>Vitamin C</span>**")
  ) +
  theme(legend.position = "none",
        text = element_text(colour = vit_c_palette["light_text"],
                            family = "Cabin"),
        axis.title.y = element_blank(),
        plot.title = ggtext::element_textbox_simple(colour = vit_c_palette["dark_text"], 
                                  size = rel(1.5), 
                                  face = "bold",
                                  family = "Enriqueta",
                                  lineheight = 1.3,
                                  margin = margin(0.5, 0, 1, 0, "lines")),
        plot.subtitle = ggtext::element_textbox_simple(family = "Cabin", size = rel(1.1), lineheight = 1.3,
                                     margin = margin(0, 0, 1, 0, "lines")),
        strip.text = element_text(family = "Enriqueta",
                                  colour = vit_c_palette["light_text"],
                                  size = rel(1.1), face = "bold",
                                  margin = margin(2, 0, 0.5, 0, "lines")),
        axis.text = element_text(colour = vit_c_palette["light_text"]))

#2 - Add text hierarchy

See for yourselves!

Five tips for beautiful annotations

#1 - Use colour and orientation purposefully

#2 - Add text hierarchy

#3 - Reduce unnecessary eye movement

#4 - Highlight important patterns

#5 - See how much you can declutter

Bonus track - {gghighlight} & curved arrows

#3 - Reduce unnecessary eye movement

We’ve made it easy to see what’s what. Now, let’s make it even easier to compare values.

themed_plot

#3 - Reduce unnecessary eye movement

We’ve made it easy to see what’s what. Now, let’s make it even easier to compare values.

themed_plot +
  scale_y_continuous(expand = c(0, 0.5))

#3 - Reduce unnecessary eye movement

We’ve made it easy to see what’s what. Now, let’s make it even easier to compare values.

themed_plot +
  scale_y_continuous(expand = c(0, 0.5)) +
  theme(strip.text = element_text(family = "Enriqueta",
                                  colour = vit_c_palette["light_text"],
                                  size = rel(1.1), face = "bold",
                                  hjust = 0.03,
                                  margin = margin(2, 0, 0.5, 0, "lines")))

#3 - Reduce unnecessary eye movement

Time to add some text boxes!

themed_plot +
  scale_y_continuous(expand = c(0, 0.5)) +
  theme(strip.text = element_text(family = "Enriqueta", colour = vit_c_palette["light_text"], size = rel(1.1), face = "bold", hjust = 0.03, margin = margin(2, 0, 0.5, 0, "lines"))) +
  # x (dose) and y (mean_length) are already 
  # set in the global ggplot() call! 
  ggtext::geom_textbox(aes(label = mean_length))

#3 - Reduce unnecessary eye movement

Time to add some text boxes!

themed_plot +
  scale_y_continuous(expand = c(0, 0.5)) +
  theme(strip.text = element_text(family = "Enriqueta", colour = vit_c_palette["light_text"], size = rel(1.1), face = "bold", hjust = 0.03, margin = margin(2, 0, 0.5, 0, "lines"))) +
  ggtext::geom_textbox(aes(label = mean_length),
                       size = 6,
                       halign = 1, 
                       hjust = 1)

#3 - Reduce unnecessary eye movement

Time to add some text boxes!

themed_plot +
  scale_y_continuous(expand = c(0, 0.5)) +
  theme(strip.text = element_text(family = "Enriqueta", colour = vit_c_palette["light_text"], size = rel(1.1), face = "bold", hjust = 0.03, margin = margin(2, 0, 0.5, 0, "lines"))) +
  ggtext::geom_textbox(aes(label = mean_length),
                       size = 6,
                       halign = 1, 
                       hjust = 1,
                       fill = NA, 
                       box.colour = NA)

#3 - Reduce unnecessary eye movement

Time to add some text boxes!

themed_plot +
  scale_y_continuous(expand = c(0, 0.5)) +
  theme(strip.text = element_text(family = "Enriqueta", colour = vit_c_palette["light_text"], size = rel(1.1), face = "bold", hjust = 0.03, margin = margin(2, 0, 0.5, 0, "lines"))) +
  ggtext::geom_textbox(aes(label = mean_length),
                       size = 6,
                       halign = 1, 
                       hjust = 1,
                       fill = NA,
                       box.colour = NA,
                       family = "Cabin",
                       colour = "#FFFFFF",
                       fontface = "bold")

#3 - Reduce unnecessary eye movement

Now for the fun stuff…

themed_plot +
  scale_y_continuous(expand = c(0, 0.5)) +
  theme(strip.text = element_text(family = "Enriqueta", colour = vit_c_palette["light_text"], size = rel(1.1), face = "bold", hjust = 0.03, margin = margin(2, 0, 0.5, 0, "lines"))) +
  ggtext::geom_textbox(aes(
    label = mean_length,
    hjust = case_when(mean_length < 15 ~ 0,
                      TRUE ~ 1),
    halign = case_when(mean_length < 15 ~ 0,
                       TRUE ~ 1)),
    size = 6,
    fill = NA,
    box.colour = NA,
    family = "Cabin",
    fontface = "bold")

#3 - Reduce unnecessary eye movement

Now for the fun stuff…

themed_plot +
  scale_y_continuous(expand = c(0, 0.5)) +
  theme(strip.text = element_text(family = "Enriqueta", colour = vit_c_palette["light_text"], size = rel(1.1), face = "bold", hjust = 0.03, margin = margin(2, 0, 0.5, 0, "lines"))) +
  ggtext::geom_textbox(aes(
    label = mean_length,
    hjust = case_when(mean_length < 15 ~ 0,
                      TRUE ~ 1),
    halign = case_when(mean_length < 15 ~ 0,
                       TRUE ~ 1),
    colour = case_when(mean_length > 15 ~ "#FFFFFF",
                       TRUE ~ vit_c_palette[supplement])),
    size = 6,
    fill = NA,
    box.colour = NA,
    family = "Cabin",
    fontface = "bold")

#3 - Reduce unnecessary eye movement

??????

themed_plot +
  scale_y_continuous(expand = c(0, 0.5)) +
  theme(strip.text = element_text(family = "Enriqueta", colour = vit_c_palette["light_text"], size = rel(1.1), face = "bold", hjust = 0.03, margin = margin(2, 0, 0.5, 0, "lines"))) +
  ggtext::geom_textbox(aes(
    label = mean_length,
    hjust = case_when(mean_length < 15 ~ 0,
                      TRUE ~ 1),
    halign = case_when(mean_length < 15 ~ 0,
                       TRUE ~ 1),
    colour = case_when(mean_length > 15 ~ "#FFFFFF",
                       TRUE ~ vit_c_palette[supplement])),
    size = 6,
    fill = NA,
    box.colour = NA,
    family = "Cabin",
    fontface = "bold")

#3 - Reduce unnecessary eye movement

scale_colour_identity() required!

themed_plot +
  scale_y_continuous(expand = c(0, 0.5)) +
  theme(strip.text = element_text(family = "Enriqueta", colour = vit_c_palette["light_text"], size = rel(1.1), face = "bold", hjust = 0.03, margin = margin(2, 0, 0.5, 0, "lines"))) +
  scale_colour_identity() +
  ggtext::geom_textbox(aes(
    label = mean_length,
    hjust = case_when(mean_length < 15 ~ 0,
                      TRUE ~ 1),
    halign = case_when(mean_length < 15 ~ 0,
                       TRUE ~ 1),
    colour = case_when(mean_length > 15 ~ "#FFFFFF",
                       TRUE ~ vit_c_palette[supplement])),
    size = 6,
    fill = NA,
    box.colour = NA,
    family = "Cabin",
    fontface = "bold")

#3 - Reduce unnecessary eye movement

We might as well add a bit of extra info (with text hierarchy!) to our labels…

themed_plot +
  scale_y_continuous(expand = c(0, 0.5)) +
  theme(strip.text = element_text(family = "Enriqueta", colour = vit_c_palette["light_text"], size = rel(1.1), face = "bold", hjust = 0.03, margin = margin(2, 0, 0.5, 0, "lines"))) +
  scale_colour_identity() +
  ggtext::geom_textbox(aes(
    label = paste0("<span style=font-size:12pt>", 
                   dose, "mg/day</span><br>", 
                   mean_length, "mm"),
    hjust = case_when(mean_length < 15 ~ 0,
                      TRUE ~ 1),
    halign = case_when(mean_length < 15 ~ 0,
                       TRUE ~ 1),
    colour = case_when(mean_length > 15 ~ "#FFFFFF",
                       TRUE ~ vit_c_palette[supplement])),
    size = 6,
    fill = NA,
    box.colour = NA,
    family = "Cabin",
    fontface = "bold")

Wait, but why?

#3 - Reduce unnecessary eye movement

Easier than you think and makes a big difference! 🦸

Five tips for beautiful annotations

#1 - Use colour and orientation purposefully

#2 - Add text hierarchy

#3 - Reduce unnecessary eye movement

#4 - Highlight important patterns

#5 - See how much you can declutter

Bonus track - {gghighlight}, Tee pipe and curved arrows

#4 - Highlight important patterns

“That’s all well and good, but we all know summary data can be misleading…”

#4 - Highlight important patterns

I ❤️ 📦 {geomtextpath}

themed_scatter_plot +
  geomtextpath::geom_textline(stat = "smooth", 
                              aes(label = supplement))

#4 - Highlight important patterns

I ❤️ 📦 {geomtextpath}

themed_scatter_plot +
  geomtextpath::geom_textline(stat = "smooth", 
                              aes(label = supplement)) +
  scale_colour_manual(values = vit_c_palette)

#4 - Highlight important patterns

I ❤️ 📦 {geomtextpath}

themed_scatter_plot +
  geomtextpath::geom_textline(stat = "smooth", aes(label = supplement),
                              hjust = 0.1,
                              vjust = 0.3) +
  scale_colour_manual(values = vit_c_palette)

#4 - Highlight important patterns

I ❤️ 📦 {geomtextpath}

themed_scatter_plot +
  geomtextpath::geom_textline(stat = "smooth", aes(label = supplement),
                              hjust = 0.1,
                              vjust = 0.3,
                              fontface = "bold",
                              family = "Cabin") +
  scale_colour_manual(values = vit_c_palette)

#4 - Highlight important patterns

More textboxes with markdown and conditional alignment (horizontal and vertical!)

themed_scatter_plot +
  geomtextpath::geom_textline(stat = "smooth", aes(label = supplement),
                              hjust = 0.1,
                              vjust = 0.3,
                              fontface = "bold",
                              family = "Cabin") +
  ggtext::geom_textbox(data = filter(min_max_gps,
                                     dose %in% c(1, 2)),
                       aes(x = case_when(dose < 1.5 ~ dose + 0.05,
                                         TRUE ~ dose - 0.05),
                           y = case_when(min_or_max  == "max"~ len * 1.1,
                                         TRUE ~ len * 0.9),
                           label = paste0("**<span style='font-family:Enriqueta'>", 
                                          guinea_pig_name,
                                          "</span>** - ", len, " mm"),
                           hjust = case_when(dose < 1.5 ~ 0,
                                             TRUE ~ 1),
                           halign = case_when(dose < 1.5 ~ 0,
                                              TRUE ~ 1)),
                       family = "Cabin",
                       size = 4,
                       fill = NA,
                       box.colour = NA) +
  scale_colour_manual(values = vit_c_palette)

#4 - Highlight important patterns

Sometimes less is more!

themed_scatter_plot +
  geomtextpath::geom_textline(stat = "smooth", aes(label = supplement),
                              hjust = 0.1,
                              vjust = 0.3,
                              fontface = "bold",
                              family = "Cabin") +
  ggtext::geom_textbox(data = filter(min_max_gps,
                                     dose == 2),
                       aes(x = case_when(dose < 1.5 ~ dose + 0.05,
                                         TRUE ~ dose - 0.05),
                           y = case_when(min_or_max  == "max"~ len * 1.1,
                                         TRUE ~ len * 0.9),
                           label = paste0("**<span style='font-family:Enriqueta'>", 
                                          guinea_pig_name,
                                          "</span>** - ", len, " mm"),
                           hjust = case_when(dose < 1.5 ~ 0,
                                             TRUE ~ 1),
                           halign = case_when(dose < 1.5 ~ 0,
                                              TRUE ~ 1)),
                       family = "Cabin",
                       size = 4,
                       fill = NA,
                       box.colour = NA) +
  scale_colour_manual(values = vit_c_palette)

#4 - Highlight important patterns

Same principle, let’s add in some arrows!

themed_scatter_plot +
  geomtextpath::geom_textline(stat = "smooth", aes(label = supplement),
                              hjust = 0.1,
                              vjust = 0.3,
                              fontface = "bold",
                              family = "Cabin") +
  ggtext::geom_textbox(data = filter(min_max_gps,
                                     dose == 2),
                       aes(x = case_when(dose < 1.5 ~ dose + 0.05, TRUE ~ dose - 0.05),
                           y = case_when(min_or_max  == "max"~ len * 1.1, TRUE ~ len * 0.9),
                           label = paste0("**<span style='font-family:Enriqueta'>", guinea_pig_name,"</span>** - ", len, " mm"),
                           hjust = case_when(dose < 1.5 ~ 0,TRUE ~ 1),
                           halign = case_when(dose < 1.5 ~ 0, TRUE ~ 1)),
                       family = "Cabin", size = 4, fill = NA, box.colour = NA) +
  geom_curve(data = filter(min_max_gps,
                           dose == 2),
             aes(x = case_when(dose < 1.5 ~ dose + 0.05,
                               TRUE ~ dose - 0.05),
                 y = case_when(min_or_max  == "max"~ len * 1.1,
                               TRUE ~ len * 0.9),
                 xend = case_when(dose < 1.5 ~ dose + 0.02,
                                  TRUE ~ dose - 0.02),
                 yend = case_when(min_or_max  == "max"~ len + 0.5,
                                  TRUE ~ len - 0.5)),
             arrow = arrow(length = unit(0.1, "cm")),
             alpha = 0.5) + 
  scale_colour_manual(values = vit_c_palette)

#4 - Highlight important patterns

Same principle, let’s add in some arrows!

themed_scatter_plot +
  geomtextpath::geom_textline(stat = "smooth", aes(label = supplement),
                              hjust = 0.1,
                              vjust = 0.3,
                              fontface = "bold",
                              family = "Cabin") +
  ggtext::geom_textbox(data = filter(min_max_gps,
                                     dose == 2),
                       aes(x = case_when(dose < 1.5 ~ dose + 0.05, TRUE ~ dose - 0.05),
                           y = case_when(min_or_max  == "max"~ len * 1.1, TRUE ~ len * 0.9),
                           label = paste0("**<span style='font-family:Enriqueta'>", guinea_pig_name,"</span>** - ", len, " mm"),
                           hjust = case_when(dose < 1.5 ~ 0,TRUE ~ 1),
                           halign = case_when(dose < 1.5 ~ 0, TRUE ~ 1)),
                       family = "Cabin", size = 4, fill = NA, box.colour = NA) +
  geom_curve(data = filter(min_max_gps,
                           dose == 2),
             aes(x = case_when(dose < 1.5 ~ dose + 0.05,
                               TRUE ~ dose - 0.05),
                 y = case_when(min_or_max  == "max"~ len * 1.1,
                               TRUE ~ len * 0.9),
                 xend = case_when(dose < 1.5 ~ dose + 0.02,
                                  TRUE ~ dose - 0.02),
                 yend = case_when(min_or_max  == "max"~ len + 0.5,
                                  TRUE ~ len - 0.5)),
             curvature = 0.1,
             arrow = arrow(length = unit(0.1, "cm")),
             alpha = 0.5) + 
  scale_colour_manual(values = vit_c_palette)

#4 - Highlight important patterns

Same principle, let’s add in some arrows!

themed_scatter_plot +
  geomtextpath::geom_textline(stat = "smooth", aes(label = supplement),
                              hjust = 0.1,
                              vjust = 0.3,
                              fontface = "bold",
                              family = "Cabin") +
  ggtext::geom_textbox(data = filter(min_max_gps,
                                     dose == 2),
                       aes(x = case_when(dose < 1.5 ~ dose + 0.05, TRUE ~ dose - 0.05),
                           y = case_when(min_or_max  == "max"~ len * 1.1, TRUE ~ len * 0.9),
                           label = paste0("**<span style='font-family:Enriqueta'>", guinea_pig_name,"</span>** - ", len, " mm"),
                           hjust = case_when(dose < 1.5 ~ 0,TRUE ~ 1),
                           halign = case_when(dose < 1.5 ~ 0, TRUE ~ 1)),
                       family = "Cabin", size = 4, fill = NA, box.colour = NA) +
  geom_curve(data = filter(min_max_gps,
                           dose == 2),
             aes(x = case_when(dose < 1.5 ~ dose + 0.05,
                               TRUE ~ dose - 0.05),
                 y = case_when(min_or_max  == "max"~ len * 1.1,
                               TRUE ~ len * 0.9),
                 xend = case_when(dose < 1.5 ~ dose + 0.02,
                                  TRUE ~ dose - 0.02),
                 yend = case_when(min_or_max  == "max"~ len + 0.5,
                                  TRUE ~ len - 0.5)),
             curvature = 0,
             arrow = arrow(length = unit(0.1, "cm")),
             alpha = 0.5) + 
  scale_colour_manual(values = vit_c_palette)

#4 - Highlight important patterns

Nearly there, folks! Look how far we’ve come!

Five tips for beautiful annotations

#1 - Use colour and orientation purposefully

#2 - Add text hierarchy

#3 - Reduce unnecessary eye movement

#4 - Highlight important patterns

#5 - See how much you can declutter

Bonus track - {gghighlight}, Tee pipe and curved arrows

#5 - See how much you can declutter

Be brave - try theme_void()

  • Do we need the grid?
  • Any text we don’t need?
  • Any colours that don’t fit with our colour scheme?

#5 - See how much you can declutter

Tweak the grid lines, using a matching colour - {monochromeR}

annotated_scatter_plot +
  theme(panel.grid = element_line(colour = "#F0F0EF"))

#5 - See how much you can declutter

And add a bit of white space

annotated_scatter_plot +
  theme(panel.grid = element_line(colour = "#F0F0EF"),
        plot.margin = margin(12, 8, 12, 8))

#5 - See how much you can declutter

Five tips for beautiful annotations

#1 - Use colour and orientation purposefully

#2 - Add text hierarchy

#3 - Reduce unnecessary eye movement

#4 - Highlight important patterns

#5 - See how much you can declutter

Bonus track - {gghighlight}, Tee pipe and curved arrows

Bonus track - {gghighlight}, Tee pipe and curved arrows

{gghighlight}

“But I have way more conditions than this, how can I highlight a more subtle pattern?”

Suppose we have data that has so many series that it is hard to identify them by their colours as the differences are so subtle…


{gghighlight}

Nicola Rennie’s Hollywood Age Gaps plot

Tee pipe

Creating label content on the fly

ToothGrowth %>%
  mutate(guinea_pig_name = sample(unique(bakeoff::bakers$baker), 60),
         supplement = case_when(supp == "OJ" ~ "Orange Juice",
                                supp == "VC" ~ "Vitamin C",
                                TRUE ~ as.character(supp))) %T>%
  {
    {
      # Double assign to jump out of the pipe!
      min_max_gps <<- group_by(., supplement, dose) %>%
        filter(., len == min(len) | len == max(len)) %>%
        mutate(min_or_max = case_when(len == max(len) ~ "max",
                                      TRUE ~ "min"))
    }
  } %>%
  ggplot(aes(x = dose, y = len, fill = supplement,
             colour = supplement)) +
  ...

Curved arrows

Conditional curvatures?

themed_scatter_plot +
  geomtextpath::geom_textline(stat = "smooth", aes(label = supplement), hjust = 0.1, vjust = 0.3, fontface = "bold", family = "Cabin") +
  ggtext::geom_textbox(data = filter(min_max_gps, dose == 2),
                       aes(x = case_when(dose < 1.5 ~ dose + 0.05, TRUE ~ dose - 0.05),
                           y = case_when(min_or_max  == "max"~ len * 1.1, TRUE ~ len * 0.9),
                           label = paste0("**<span style='font-family:Enriqueta'>", guinea_pig_name,"</span>** - ", len, " mm"),
                           hjust = case_when(dose < 1.5 ~ 0,TRUE ~ 1),
                           halign = case_when(dose < 1.5 ~ 0, TRUE ~ 1)),
                       family = "Cabin", size = 4, fill = NA, box.colour = NA) +
  geom_curve(data = filter(min_max_gps,
                           dose == 2 & 
                             min_or_max == "max"),
             aes(x = case_when(dose < 1.5 ~ dose + 0.05, TRUE ~ dose - 0.05),
                 y = case_when(min_or_max  == "max"~ len * 1.1, TRUE ~ len * 0.9),
                 xend = case_when(dose < 1.5 ~ dose + 0.02, TRUE ~ dose - 0.02),
                 yend = case_when(min_or_max  == "max"~ len + 0.5,TRUE ~ len - 0.5)),
             curvature = -0.1,
             arrow = arrow(length = unit(0.1, "cm")),
             alpha = 0.5) + 
  geom_curve(data = filter(min_max_gps,
                           dose == 2 & 
                             min_or_max == "min"),
             aes(x = case_when(dose < 1.5 ~ dose + 0.05, TRUE ~ dose - 0.05),
                 y = case_when(min_or_max  == "max"~ len * 1.1, TRUE ~ len * 0.9),
                 xend = case_when(dose < 1.5 ~ dose + 0.02, TRUE ~ dose - 0.02),
                 yend = case_when(min_or_max  == "max"~ len + 0.5, TRUE ~ len - 0.5)),
             curvature = 0.1,
             arrow = arrow(length = unit(0.1, "cm")),
             alpha = 0.5) +
  scale_colour_manual(values = vit_c_palette)

Curved arrows

To avoid it getting too unwieldy, add the curvatures to your data and iterate.

labelled_plot # plot with everything but the arrows

for(curv in unique(my_data$curvature)) {

  filtered_data <- filter(my_data, 
                          curvature == curv)

  labelled_plot <- labelled_plot +
    annotate(geom = "curve", 
             x = filtered_data$label_x, 
             y = filtered_data$label_y, 
             xend = filtered_data$arrow_end_x, 
             yend = filtered_data$arrow_end_y, 
             size = 0.3,
             colour = case_when(filtered_data$species == "Adelie" ~ penguin_palette$Adelie,
                                filtered_data$species == "Chinstrap" ~ penguin_palette$Chinstrap,
                                filtered_data$species == "Gentoo" ~ penguin_palette$Gentoo),
             curvature = curv,
             arrow = arrow(length = unit(1.5, "mm")))
}

labelled_plot # plot with as many different curvatures as you like!

Over to you!