Level up your plots

NHS-R Conference 2023 | 4th October 2023

Hi there 👋 !

👩 Cara Thompson

👩‍💻 Love for patterns in music & language, and a fascination with the human brain |> Psychology PhD |> Analysis of postgraduate medical examinations |> Data Visualisation Consultant


💙 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 colour and text to create compelling and memorable visualisations.

  • Explore how we can use colour to be less dependent on annotations
  • Illustrate ways in which we can use colour and fonts to add text hierarchy
  • Apply all of the above to create story-enhancing annotations
  • Package up reusable bits of R code
  • Introduce you to {ggtext} and {geomtextpath}
  • Give feedback on your own plots

A bit of housekeeping

  • Please ask for help!
  • Regular breaks to apply what we’re learning to your own plots
    • if you get stuck / want to share something for feedback: hello@cararthompson.com
  • Namespacing (package::function("blah"))
  • Choose your own pipe
  • Reuse as much of the code as you like!

“Intuitive colour palettes?”

Which one is Bouba, and which one is Kiki?
“Sound Symbolism” - Wolfgang Köhler 1929

“Intuitive colour palettes?”

Which one is Bouba, and which one is Kiki?
Predicted by sound properties - Passi & Arun, 2022

Let’s play a quick game…

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 unripe banana in their mix. Each island chose a different quantity.

The Great Penguin Bake Off

They decided to go on a retreat to plan their bakes in different locations

The Great Penguin Bake Off

Each species was allowed to invite a different mentor…

The Great Penguin Bake Off

… and to choose a type of snack between practice bakes

The Great Penguin Bake Off - Bonus round!

The penguins also baked their cakes for different amounts of time. Here are the mean durations per species.

The Great Penguin Bake Off - Bonus round!

The penguins also baked their cakes for different amounts of time. Here are the mean durations per species.

Compelling and memorable visualisations

#1 - Use colour and orientation purposefully

#2 - Add text hierarchy

#3 - Reduce unnecessary eye movement

#4 - Highlight important patterns

Let’s get coding!

Setting up our first plot

Using the ToothGrowth dataset

  • Build into R for easy “codealongability”
  • Intriguing dataset (?ToothGrowth)
  • Excuse for a cute GIF –>

Setting up our first plot

With a few tips along the way

library(tidyverse)

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",
           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",
           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()

Setting up our first plot

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

#1 - Use colour 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 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 purposefully

Creating a named vector

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 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",
           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 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",
           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 = c("#fab909", 
                               "#E93603")) +
  facet_wrap(supplement ~ ., ncol = 1) +
  theme_minimal()

#1 - Use colour purposefully

Add in our colours - wait, what?

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),
         supplement = 
           factor(supplement, 
                  levels = c("Vitamin C", 
                             "Orange Juice"))) %>%
  ggplot(aes(x = categorical_dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(stat = "identity",
           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 = c("#fab909", 
                               "#E93603")) +
  facet_wrap(supplement ~ ., ncol = 1) +
  theme_minimal()

#1 - Use colour purposefully

Add in our colours - named vector to the rescue!

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),
         supplement = 
           factor(supplement, 
                  levels = c("Vitamin C", 
                             "Orange Juice"))) %>%
  ggplot(aes(x = categorical_dose,
             y = mean_length,
             fill = supplement)) +
  geom_bar(stat = "identity",
           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 purposefully

Add in our colours - named vector to the rescue!

Key advantages

  • Know the colours are applied to the right data points!
  • Keep colour-data pairings consistent throughout the project
  • Package up a default palette
  • Easily reuse colours in the text
    • ggtext::element_markdown() later in this workshop

#1 - Use colour purposefully

Get rid of unused 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",
           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 purposefully

Get rid of unused 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",
           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, 
                    limits = force) +
  facet_wrap(supplement ~ ., ncol = 1) +
  theme_minimal()

#1 - Use colour 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",
           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, limits = force) +
  facet_wrap(supplement ~ ., ncol = 1) +
  theme_minimal()

#1 - Use colour 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",
           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, limits = force) +
  scale_alpha(range = c(0.33, 1)) +
  facet_wrap(supplement ~ ., ncol = 1) +
  theme_minimal()

#1 - Use colour 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",
           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, limits = force) +
  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 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",
           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, limits = force) +
  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 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",
           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, limits = force) +
  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!

#1 Over to you - but first…

It’s all about making it easier to remember what is what

Picking colours is hard! Let others help you!

#1 Use colour purposefully

#1 Use colour purposefully

machine <- "#061939"
human <- "#e25470"
monochromeR::generate_palette(machine, 
                              blend_colour = human, 
                              n_colours = 3, 
                              view_palette = TRUE)




#1 Use colour purposefully

#1 Use colour purposefully

#1 Use colour purposefully

  • Your department brand guidelines
  • A photo + something like imagecolorpicker.com to pick out colours
  • Take inspiration from photos/ other dataviz / art you like

#1 Use colour purposefully



#1 Use colour purposefully

#1 Use colour purposefully

  • Your department brand guidelines
  • A photo + something like imagecolorpicker.com to pick out colours
  • Take inspiration from other dataviz / art you like
  • Google images and “[whatever you like] palette”

#1 Use colour purposefully

#1 Use colour purposefully

  • Your department brand guidelines
  • A photo + something like imagecolorpicker.com to pick out colours
  • Take inspiration from other dataviz / art you like
  • Google images and “[whatever you like] palette”
  • Or… start from the colour wheel and read around how best to use it

#1 Use colour purposefully

Quick tip: Viewing your colours

penguin_locations <- c("#b0a798",
                       "#afba49",
                       "#d8ebfa")

monochromeR::view_palette(penguin_locations)

#1 Use colour purposefully

Quick tip: Naming and viewing your colours

penguin_locations <- c("Adelie" = "#b0a798",
                       "Chinstrap" = "#afba49",
                       "Gentoo" = "#d8ebfa")

monochromeR::view_palette(penguin_locations)

Manipulating colours - Terminology

  • Hue: “what colour”?
  • Saturation: “how colourful?”
  • Value: “how light?”

Manipulating colours | Amounts

Back to the bake-off… Value / transparency (blank background!)

Manipulating colours | Cumulative effect

Transparency with outlines

Manipulating colours | Recency

Which line shows the most recent data?

Manipulating colours | Recency


Manipulating colours | Recency

Playing with value (a “lighter” green)

Manipulating colours | Recency

Playing with saturation (a “less green” green)

Manipulating colours | Recency (“Going grey”)

Combining the two

Manipulating colours | Recency (“Going grey”)

[1] "#3D6946" "#597C60" "#76907B" "#92A496" "#AFB8B1"

Now also a shiny app! cararthompson.shinyapps.io/monochromeR

#1 Use colour purposefully

Final colour hack: Test it out with text and background


#1 Use colour purposefully

Final colour hack: Test it out with text and background


#1 Use colour purposefully

Final colour hack: Test it out with text and background


{monochromeR} can help!

Over to you!

  • Think about the concepts in your own data
  • Look for images related to those concepts
  • Extract the colour codes you need - imagecolorpicker.com
  • Name them & check what they look like together - monochromeR::view_palette()

You have 10 minutes! 📊 🎨 ☕

10:00

#1 Use colour purposefully

A few extra things to bear in mind

  • Accessibility
    • colorblindr::cvd_grid()
    • remotes::install_github("clauswilke/colorblindr")

#1 Use colour purposefully

A few things to bear in mind

#1 Use colour purposefully

A few things to bear in mind

  • Accessibility
  • Race/Ethnicity - avoid stereotypes and be mindful of unintended messages
  • Colour intensity - “more is more”

More than green and red!

More than green and red!

  • Colour perception differences
    • Green / Red / Blue / Combinations
  • Neurodivergent audiences - 10-15% of global population
    • Behavioural / emotional disorders
    • ADHD
    • Learning disabilities
    • Autism

The case for designing with accessibility first

  • It makes it better for everyone
    • Less visually overwhelming (colour minimalism)
    • Easier to read (text & fonts next time!)
    • Make the main thing the main thing (visual hierarchy)
  • It makes it more environment-proof
    • Poor quality screenshot / zoom pixelation
    • Printing in black and white
    • Projector quality for slides at conferences / demos

The case for designing with accessibility first

  • Colour
    • Shades of the same colour rather than lots of different colours
    • Prefer muted colours over stark contrasts
  • Typography (readability)
  • Visual hierarchy (where next?)
  • Patterns (predictable repetition)

Distinguishable colours

“Web Content Accessibility Guidelines (WACG) 2.1 standards call for a contrast ratio of at least 3:1 for ‘meaningful graphics’. Achieving this is a mathematical impossibility for any palette containing just a small handful of colours, let alone one as a comprehensive as a data design system requires.”

  • Ben Willers, Power of Colour: Creating a new data design system for Economist Impact (Nightingale magazine, Issue 03, p 88)


So we do the best we can to make sure colours, chosen with additional accessibility criteria in mind, are visibly distinct from each other for a range of audiences.

Distinguishable colours

R: {monochromeR} + {colorblindr}

Distinguishable colours

Distinguishable colours

www.vis4.net/palettes

Distinguishable colours

www.vis4.net/palettes

  • Interpolate between “anchor colours”
  • Check for “friendliness”
  • Tweak colours as required to make them work


Top tip: Go for light/dark contrasts as well as hue (“colour”) variation


Testing with real graphs

projects.susielu.com/viz-palette

Testing with (your!) real graphs

daltonlens.org/colorblindness-simulator

Text and colour contrast

monochromeR::view_palette(c("celebrate!" = "#3D6946", 
                            "relax..." = "#D7EDC4", 
                            "no comment" = "#FFFFFF", 
                            "watch out..." = "#E5BFC6",
                            "PANIC!" = "#97001D"))

Text and colour contrast

Text and colour contrast

Text and colour contrast

Text and colour contrasts

colourcontrast.cc

Over to you

  • Check the colours you have come up with
    • Viz palette checker
  • What colour text will you use with them?
    • Color contrast checker
  • Try implementing these in your own plot and uploading a screenshot to the colourblindness simulator or using colorblindr::cvd_grid()
    • Any surprises?
    • How can you address them?

Links to resources: cararthompson.com/talks - first one

You have 5 minutes! 📊 🎨

05:00

Compelling and memorable visualisations

#1 - Use colour and orientation purposefully

#2 - Add text hierarchy

#3 - Reduce unnecessary eye movement

#4 - Highlight important patterns

#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", 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, limits = force) +
  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)

basic_plot

#2 - Add text hierarchy

Time to start playing with theme()!

basic_plot +
  theme(legend.position = "none")

#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, restart R Studio + 📦 {systemfonts} ({ragg} + {textshaping}) + Set graphics device to “AGG” + 🤞



knitr::opts_chunk$set(dev = “ragg_png”)



#2 - Add text hierarchy

  • set of fonts + set of colours + rules…

A Dataviz Design System: A simple set of rules to follow, to effortlessly make your visualisations look on brand, every time you go to create a new plot.

  • Accessibility
  • Meaningful colours within your context
  • “Visual identity”
  • Implement as a R package
  • Wednesday 11th October 2023 @ 14.05

#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"),
        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

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 + colour!

Wait, that yellow… #fab909

#2 - Add text hierarchy + colour!

Wait, that yellow…

colourcontrast.cc

#2 - Add text hierarchy + colour!

basic_plot +
  labs(title = 
         paste0("In smaller doses, **<span style='color:",
                vit_c_palette["Orange Juice text"], "'>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 + colour!

See for yourselves!

Quiz time!


… but do we?

Do we need a legend?

  • ?

Do we need a legend?

  • No Colour = Y axis labels, no additional information

Do we need a legend?

  • ?

Do we need a legend?

  • Yes: No other way of telling; also, nicely in the right order

Do we need a legend?

  • ?

Do we need a legend?

  • Yes - or line-end annotations… or a different plot type? (👀 small multiples)

Do we need a legend?

  • ?

Do we need a legend?

  • No - the annotations say it all (and are much clearer)

Do we need a legend?

  • ?

Do we need a legend?

  • No - But legend adds more information… Consider turning the legend into a table

Does it need a legend?

  • ?

Does it need a legend?

  • Yes, but I’d rethink it

Over to you!

  • Look at all your text: do you need it?
  • See what fonts are available on your device - systemfonts::systemfonts() |> View()
    • If you want to, install a new one!
  • Apply one to the title, and one to the rest of the text using theme()
  • Create some text colour variants using monochromeR::generate_palette()
  • Apply those colours to the title and the other text elements
  • Play around with (relative) text sizes
  • Give me a shout if you get stuck!

You have 10 minutes! 📊 🎨 ☕

10:00

Packaging up

  • Package development is a whole other workshop (but it’s easier than you think!)
    • 📦 {usethis}
  • Any function or object you create can be added to a package
carartemplates::carar_colours()
     green       blue  dark_text light_text 
 "#28A569"  "#2C3D4F"  "#1A242F"  "#808A95" 
monochromeR::view_palette(
  carartemplates::carar_colours()
)

Packaging up

The current status:

plot +
  theme_minimal() +
  theme(...,
        ...,
        ...,
        ...,
        ...,
        ...,
        ...,
        ...) # ~ 20+ lines of code

What we want:

plot +
  theme_guineapigs() # 1 line of code!

Packaging up

theme_guineapigs <- function(base_text_size = 15,
                             palette = vit_c_palette) {
  
  theme_minimal(base_size = base_text_size) +
    theme(legend.position = "none",
          text = element_text(colour = palette["light_text"],
                              family = "Cabin"),
          axis.title.y = element_blank(),
          plot.title = ggtext::element_textbox_simple(colour = 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 = palette["light_text"],
                                    size = rel(1.1), face = "bold",
                                    margin = margin(2, 0, 0.5, 0, "lines")),
          axis.text = element_text(colour = palette["light_text"]),
          plot.margin = margin(rep(base_text_size, 4)),
          plot.title.position = "plot") 
}

Packaging up

theme_guineapigs <- function(base_text_size = 15,
                             palette = vit_c_palette) {
  
  theme_minimal(base_size = base_text_size) +
    theme(legend.position = "none",
          text = element_text(colour = palette["light_text"],
                              family = "Cabin"),
          axis.title.y = element_blank(),
          plot.title = ggtext::element_textbox_simple(colour = 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 = palette["light_text"],
                                    size = rel(1.1), face = "bold",
                                    margin = margin(2, 0, 0.5, 0, "lines")),
          axis.text = element_text(colour = palette["light_text"])) 
}

Packaging up

palmerpenguins::penguins %>%
  ggplot() +
  geom_point(aes(x = flipper_length_mm,
                 y = bill_length_mm, 
                 colour = sex)) +
  labs(title = "Perfectly proportional penguins",
       subtitle = "Look at them go!",
       x = "Flipper length (mm)",
       y = "Bill length(mm)")

Packaging up

palmerpenguins::penguins %>%
  ggplot() +
  geom_point(aes(x = flipper_length_mm,
                 y = bill_length_mm, 
                 colour = sex)) +
  labs(title = "Perfectly proportional penguins",
       subtitle = "Look at them go!",
       x = "Flipper length (mm)",
       y = "Bill length(mm)") +
  theme_guineapigs()

Level up your plots

#1 - Use colour purposefully

#2 - Add text hierarchy

#3 - Reduce unnecessary eye movement

#4 - Highlight important patterns

#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(hjust = 0.03))

#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(hjust = 0.03)) +
  # 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(hjust = 0.03)) +
  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(hjust = 0.03)) +
  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(hjust = 0.03)) +
  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(hjust = 0.03)) +
  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(hjust = 0.03)) +
  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(hjust = 0.03)) +
  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(hjust = 0.03)) +
  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(hjust = 0.03)) +
  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")

#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(hjust = 0.03)) +
  scale_colour_identity() +
  ggtext::geom_textbox(aes(
    label = paste0("<span style=font-size:12pt>", 
                   dose, "mg/day</span><br>", 
                   janitor::round_half_up(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?

Wait, but why?

new_data %>% guinea_pig_plot()

#3 - Reduce unnecessary eye movement

Let’s fix that alignment…

themed_plot +
  scale_y_continuous(expand = c(0, 0.5)) +
  scale_colour_identity() +
  ggtext::geom_textbox(aes(
    label = paste0("<span style=font-size:12pt>", 
                   dose, "mg/day</span><br>", 
                   janitor::round_half_up(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") +
  theme(strip.text = element_text(hjust = 0.03),
        plot.title.position = "plot")

#3 - Reduce unnecessary eye movement

And add some breathing space around the plot…

themed_plot +
  scale_y_continuous(expand = c(0, 0.5)) +
  scale_colour_identity() +
  ggtext::geom_textbox(aes(
    label = paste0("<span style=font-size:12pt>", 
                   dose, "mg/day</span><br>", 
                   janitor::round_half_up(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") +
  theme(strip.text = element_text(hjust = 0.03),
        plot.title.position = "plot",
        plot.margin = margin(rep(15, 4)))

#3 - Reduce unnecessary eye movement

Easier than you think and makes a big difference! 🦸

Over to you!

  • Add a textbox (or several!) using ggtext::geom_textbox()
  • Add in some styling (text hierarchy / colour) and conditional alignments
  • See what happens when you apply your plot code to random subsets of your data

You have 10 minutes! 📊 🎨 ☕

10:00

Level up your plots

#1 - Use colour purposefully

#2 - Add text hierarchy

#3 - Reduce unnecessary eye movement

#4 - Highlight important patterns

#4 - Highlight important patterns

How did those penguins get on anyway…?

  • Means, trends, key data points

#4 - Highlight important patterns | means

Consider text boxes instead of a legend…

# Create a new tibble
penguin_summaries <- palmerpenguins::penguins %>%
  group_by(species) %>%
  summarise(bill_depth_mm = mean(bill_depth_mm, na.rm = TRUE),
            bill_length_mm = mean(bill_length_mm, na.rm = TRUE)) %>%
  mutate(commentary = case_when(
    species == "Adelie" ~
      "The Adelie penguins tried varying the amount of banana in the mix.
       Turns out, even a hint of green banana is detrimental to yumminess!",
    species == "Gentoo" ~
      "Over-ripe bananas and typically shorter baking times.",
    TRUE ~ "Ripe bananas and slightly longer cooking times."))

#4 - Highlight important patterns | means

Consider text boxes instead of a legend…

penguin_summaries
# A tibble: 3 x 4
  species   bill_depth_mm bill_length_mm commentary                             
  <fct>             <dbl>          <dbl> <chr>                                  
1 Adelie             18.3           38.8 "The Adelie penguins tried varying the~
2 Chinstrap          18.4           48.8 "Ripe bananas and slightly longer cook~
3 Gentoo             15.0           47.5 "Over-ripe bananas and typically short~

#4 - Highlight important patterns | means

Consider text boxes instead of a legend…

penguins_themed_plot +
  ggtext::geom_textbox(data = penguin_summaries,
                       aes(label = paste0(
                         "**Team ", species, "**",
                         "<br><span style = \"color:",
                         banana_colours$light_text,
                         "\">", commentary, "</span>")))

#4 - Highlight important patterns | means

Consider text boxes instead of a legend…

penguins_themed_plot +
  ggtext::geom_textbox(data = penguin_summaries,
                       aes(label = paste0(
                         "**Team ", species, "**",
                         "<br><span style = \"color:",
                         banana_colours$light_text,
                         "\">", commentary, "</span>")),
                       family = "DM Sans",
                       size = 3.5,
                       width = unit(9, "line"),
                       alpha = 0.9,
                       box.colour = NA)

#4 - Highlight important patterns | points

What about individual penguins?

penguin_highlights <- palmerpenguins::penguins_raw %>%
  # Housekeeping
  janitor::clean_names() %>%
  rename(bill_depth_mm = culmen_depth_mm,
         bill_length_mm = culmen_length_mm) %>%
  # Find star baker, runner up and lowest score
  filter(bill_length_mm %in% c(max(bill_length_mm, na.rm = TRUE),
                               sort(bill_length_mm, decreasing = TRUE)[2],
                               min(bill_length_mm, na.rm = TRUE))) 

#4 - Highlight important patterns | points

First, a bit of text manipulation!

penguin_highlights <- palmerpenguins::penguins_raw %>%
  janitor::clean_names() %>%
  rename(bill_depth_mm = culmen_depth_mm,
         bill_length_mm = culmen_length_mm) %>%
  filter(bill_length_mm %in% c(max(bill_length_mm, na.rm = TRUE),
                               sort(bill_length_mm, decreasing = TRUE)[2],
                               min(bill_length_mm, na.rm = TRUE))) %>%
  # More housekeeping
  mutate(species = gsub("(.) (.*)", "\\1", species))


raw_species <- unique(palmerpenguins::penguins_raw$Species)
raw_species
[1] "Adelie Penguin (Pygoscelis adeliae)"      
[2] "Gentoo penguin (Pygoscelis papua)"        
[3] "Chinstrap penguin (Pygoscelis antarctica)"
gsub("(.) (.*)", "\\1", raw_species)
[1] "Adelie"    "Gentoo"    "Chinstrap"

#4 - Highlight important patterns | points

First, a bit of text manipulation!

penguin_highlights <- palmerpenguins::penguins_raw %>%
  janitor::clean_names() %>%
  rename(bill_depth_mm = culmen_depth_mm,
         bill_length_mm = culmen_length_mm) %>%
  filter(bill_length_mm %in% c(max(bill_length_mm, na.rm = TRUE),
                               sort(bill_length_mm, decreasing = TRUE)[2],
                               min(bill_length_mm, na.rm = TRUE))) %>%
  mutate(species = gsub("(.) (.*)", "\\1", species),
         # Add commentary!
         commentary = case_when(
           bill_length_mm == max(bill_length_mm) ~
             paste0("Our star baker is **", individual_id,
                    "**, a ", species, " from ", island,
                    ". Congratulations, ", individual_id, "!"),
           bill_length_mm == sort(bill_length_mm, decreasing = TRUE)[2] ~
             paste0("Our runner up is a ", species,
                    " from ", island, ": **", individual_id,
                    "**, proving that ripe and over-ripe bananas are both good options!"),
           TRUE ~ paste0("**", individual_id,
                         "**, did not have a good baking day. The combination of short cooking time and green bananas probably didn't help!")))

#4 - Highlight important patterns | points

First, a bit of text manipulation!

penguin_highlights %>% 
  select(individual_id,
         species,
         island,
         commentary)
# A tibble: 3 x 4
  individual_id species   island commentary                                     
  <chr>         <chr>     <chr>  <chr>                                          
1 N81A1         Adelie    Dream  **N81A1**, did not have a good baking day. The~
2 N56A2         Gentoo    Biscoe Our star baker is **N56A2**, a Gentoo from Bis~
3 N71A2         Chinstrap Dream  Our runner up is a Chinstrap from Dream: **N71~

#4 - Highlight important patterns | points

Next, let’s work out where we want our labels…

#4 - Highlight important patterns | points

… and add box coordinates and text alignment to our data

penguin_highlights <- palmerpenguins::penguins_raw %>%
  janitor::clean_names() %>%
  rename(bill_depth_mm = culmen_depth_mm,
         bill_length_mm = culmen_length_mm) %>%
  filter(bill_length_mm %in% c(max(bill_length_mm, na.rm = TRUE),
                               sort(bill_length_mm, decreasing = TRUE)[2],
                               min(bill_length_mm, na.rm = TRUE))) %>%
  # more housekeeping!
  arrange(bill_length_mm) %>%
  mutate(species = gsub("(.) (.*)", "\\1", species),
         commentary = case_when(
           bill_length_mm == max(bill_length_mm) ~
             paste0("Our star baker is **", individual_id, "**, a ", species, " from ", island, ". Congratulations, ", individual_id, "!"),
           bill_length_mm == sort(bill_length_mm, decreasing = TRUE)[2] ~
             paste0("Our runner up is a ", species, " from ", island, ": **", individual_id, "**, proving that ripe and over-ripe bananas are both good options!"),
           TRUE ~ paste0("**", individual_id, "**, did not have a good baking day. The combination of short cooking time and green bananas probably didn't help!")),
         # Add label and arrow coordinates
         label_x = c(15, 18.15, 16.45),
         label_y = c(34, 57, 59),
         left_to_right = case_when(label_x < bill_depth_mm ~ 1,
                             TRUE ~ 0),
         arrow_x_end = case_when(label_x < bill_depth_mm ~ bill_depth_mm - 0.1,
                                 TRUE ~ bill_depth_mm + 0.1),
         arrow_y_end = case_when(label_y < bill_length_mm ~ bill_length_mm - 0.1,
                                 TRUE ~ bill_length_mm + 0.1))

#4 - Highlight important patterns | points

Let’s add the annotations…

penguins_themed_plot +
  ggtext::geom_textbox(data = penguin_summaries,
               aes(label = paste0("**Team ", species, "**", "<br><span style = \"color:", banana_colours$light_text, "\">", commentary, "</span>")),
               family = "DM Sans", size = 3.5, width = unit(9, "line"), alpha = 0.9, box.colour = NA)

#4 - Highlight important patterns | points

Let’s add the annotations…

penguins_themed_plot +
  ggtext::geom_textbox(data = penguin_summaries,
               aes(label = paste0("**Team ", species, "**", "<br><span style = \"color:", banana_colours$light_text, "\">", commentary, "</span>")),
               family = "DM Sans", size = 3.5, width = unit(9, "line"), alpha = 0.9, box.colour = NA) +
  ggtext::geom_textbox(data = penguin_highlights,
               aes(label = commentary,
                   x = label_x,
                   y = label_y,
                   hjust = left_to_right),
               family = "DM Sans", 
               size = 3,
               fill = NA,
               box.colour = NA)

#4 - Highlight important patterns | points

… using arrows and alignments to emphasise the story

penguins_themed_plot +
  ggtext::geom_textbox(data = penguin_summaries,
               aes(label = paste0("**Team ", species, "**", "<br><span style = \"color:", banana_colours$light_text, "\">", commentary, "</span>")),
               family = "DM Sans", size = 3.5, width = unit(9, "line"), alpha = 0.9, box.colour = NA) +
  ggtext::geom_textbox(data = penguin_highlights,
               aes(label = commentary, x = label_x, y = label_y, hjust = left_to_right),
               family = "DM Sans", size = 3, fill = NA, box.colour = NA) +
  geom_curve(data = penguin_highlights,
             aes(x = label_x, xend = arrow_x_end,
                 y = label_y, yend = arrow_y_end,
                 hjust = left_to_right),
             arrow = arrow(length = unit(0.1, "cm")),
             curvature = list(0.15),
             alpha = 0.5)

#4 - Highlight important patterns | points

… using arrows and alignments to emphasise the story

penguins_themed_plot +
  ggtext::geom_textbox(data = penguin_summaries,
               aes(label = paste0("**Team ", species, "**", "<br><span style = \"color:", banana_colours$light_text, "\">", commentary, "</span>")),
               family = "DM Sans", size = 3.5, width = unit(9, "line"), alpha = 0.9, box.colour = NA) +
  ggtext::geom_textbox(data = penguin_highlights,
               aes(label = commentary, x = label_x, y = label_y, hjust = left_to_right,
                   halign = left_to_right),
               family = "DM Sans", size = 3, fill = NA, box.colour = NA) +
  geom_curve(data = penguin_highlights,
             aes(x = label_x, xend = arrow_x_end,
                 y = label_y, yend = arrow_y_end),
             arrow = arrow(length = unit(0.1, "cm")),
             curvature = list(0.15),
             alpha = 0.5)

#4 - Highlight important patterns

Wait, but why again?

new_data %>% penguin_plot()

Wait, but why again?

new_data %>% time_comparisons_plot()

Level up your plots

The possibilities are endless!

Level up your plots

The possibilities are endless!

Level up your plots

The possibilities are endless!

Level up your plots

The possibilities are endless!

We made it!

Over to you!


hello@cararthompson.com

Tw/Li: cararthompson


Dataviz Design Systems | Dataviz Commissions | Training & Consultations