So much more than pretty graphs

The value of creating and implementing a dataviz design system in R
NHSR 2023 | 11th 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

Hi there 👋 !

Just in case…

@cararthompson

Hi there 👋 !

Just in case…

@cararthompson

Hi there 👋 !

Dataviz Design System, implemented as an R package

Hi there 👋 !

Dataviz Design System, implemented as an R package

What is a Dataviz Design System?

Dataviz-friendly brand guidelines

  • Colours
  • Fonts
  • Simple rules

What is a Dataviz Design System?

Dataviz-friendly brand guidelines

  • Purposeful colour semantics
  • Dataviz-friendly text formatting
  • Preferred geoms
  • Accessibility baked in

🎨 Graphs, tables, presentations, documents, Quarto slides…

Why have a Dataviz Design System?

“So much more than pretty graphs”

  • Clear visual identity
  • Decision-making & trial-and-error energy saver
  • #rstats 💙 Automate the “boring stuff” to focus on the stuff that needs your expertise

Building a dataviz design system


Functional

  • Text hierarchy
  • Number of colours + symbolism
  • Accessibility

Aesthetic

  • On-brand
  • Personality
  • Nice extras (background, toggles)


Implementation

  • Documentation
  • Automation
  • Flexibility

Let’s go!

Gathering specific requirements

  • What kind of colours would you like to be associated with this project?
  • How many colours do you need?
  • Any colour semantics we should include?

  • What types of plots do you use a lot?
  • How much personality would you like to convey in the text formatting?

Colour inspiration

Colour inspiration

Colour inspiration 🥳

Colours & Accessibility

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

Colours & Accessibility

Top tips

  • Shades of the same colour rather than lots of different colours
  • Prefer muted colours over stark contrasts
  • Less is more
  • Light/dark variation as well as hue variation

Colours & Accessibility

R: {monochromeR} + {colorblindr}

monochromeR::view_palette(ophelia::ophelia_palettes$default)

Colours & Accessibility

R: {monochromeR} + {colorblindr}

colorblindr::cvd_grid()

Colours & Accessibility

R: {monochromeR} + {colorblindr}

library(tidyverse)

palmerpenguins::penguins |>
  ggplot() +
  geom_point(aes(x = bill_length_mm,
                 y = flipper_length_mm,
                 size = body_mass_g,
                 color = species),
             alpha = 0.8) +
  labs(title = "Perfectly proportional penguins",
       subtitle = "Look at them go!",
       x = "Bill length (mm)",
       y = "Flipper length (mm)",
       caption = "Demo plot, built with {palmerpenguins}") +
  ophelia::scale_colour_ophelia(palette = "warm_colours",
                                continuous = FALSE) +
  ophelia::theme_ophelia(background = FALSE) +
  theme(legend.position = "none")

Colours & Accessibility

R: {monochromeR} + {colorblindr}

colorblindr::cvd_grid()

Font inspiration

  • Choosing fonts with easily distinguishable characters
    • The 1Il test
    • Are these letters disctinct enough? ceo
    • Looking for lack of symmetry db qp
  • Any key letters you know will be used a lot?
  • Check what the numbers look like!

Font inspiration

  • Make it easy for the end user!
  • Easy to find & install
  • .ttf
  • Italics & bold
  • Numbers & special characters
  • Easy to read





Font inspiration

  • Make it easy for the end user!
  • Easy to find & install
  • .ttf
  • Italics & bold
  • Numbers & special characters
  • Easy to read





Fonts & usability

  • Choosing fonts you can use
    • Licence
    • Need bold and italics
  • Pairing fonts so that they scale nicely together
    • or hack a solution!


Here is some text
in Nunito Sans, 35pt!


Here is some text
in Crimson Pro, 35pt!

Fonts & usability

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”)



Implementation

Colours

Anchor colours

ophelia_colours <- list(
  deep_purple = "#2B1725",
  dark_red = "#56242b",
  purple =  "#4f3c78",
  gold = "#d3970a",
  pink = "#e0b5ba",
  light_blue = "#7691b1",
  dark_green = "#375248",
  na_value = "#6A686F",
  ... # and a few more derived from these 
)

Colours

Several palettes, derived from the anchor colours

ophelia_palettes <- list(
  default = c(ophelia_colours$deep_purple,
              ophelia_colours$dark_red,
              ophelia_colours$purple,
              ophelia_colours$gold,
              ophelia_colours$pink,
              ophelia_colours$light_blue,
              ophelia_colours$dark_green),
  cool_colours = c(ophelia_colours$dark_green,
                   ophelia_colours$light_blue,
                   ophelia_colours$pale_green),
  warm_colours = c(ophelia_colours$deep_purple,
                   ophelia_colours$pink,
                   ophelia_colours$gold),
  neg_to_pos = c(ophelia_colours$deep_purple,
                 ophelia_colours$pale_pink,
                 ophelia_colours$bright_gold),
  greens = c(ophelia_colours$dark_green,
             ophelia_colours$pale_green),
  purples = c(ophelia_colours$deep_purple,
              ophelia_colours$pale_purple)
)

Colours

Feed the palettes into a bespoke function that uses ggplot scale functions

scale_colour_ophelia <- function(palette = "default",
                                 continuous = FALSE,
                                 .colours = ophelia_colours,
                                 .palettes = ophelia_palettes,
                                 ...) {
  
  if(continuous == FALSE) {
    
    ggplot2::discrete_scale(palette = grDevices::colorRampPalette(.palettes[[palette]]),
                            aesthetics = "colour",
                            scale_name = .palettes[[palette]],
                            na.value = .colours$na_value,
                            ...)
    
  } else {
    
    ggplot2::scale_colour_gradientn(colours = .palettes[[palette]],
                                    na.value = .colours$na_value,
                                    ...)
  }
  
}

Colours

… with a few extra touches

Colours

Text colours: find a “starting” colour the ties in with all the palettes

monochromeR::generate_palette(
  ophelia_colours$dark_red, 
  blend_colour = ophelia_colours$purple, 
  n_colours = 5, 
  view_palette = TRUE)

[1] "#56242B" "#54283A" "#532D49" "#513259" "#503768"

Colours

Text colours: feed it into {monochromeR} to generate a dark text colour

monochromeR::generate_palette(
  "#532D49", 
  "go_darker", 
  n_colours = 5, 
  view_palette = TRUE)

[1] "#532D49" "#42243A" "#311B2B" "#21111D" "#10090E"

Colours

Text colours: feed that dark text colour into {monochromeR} again to generate a light text colour

monochromeR::generate_palette(
  "#10090E", 
  "go_lighter", 
  n_colours = 8, 
  view_palette = TRUE)

[1] "#10090E" "#2B2529" "#464145" "#615D60" "#7D797C" "#989597" "#B3B1B3"
[8] "#CFCDCE"

Plot theme

theme_ophelia()

  • Adding custom fonts and text hierarchy
  • Applying the colour scheme to text, axes, background, etc
  • Removing clutter
  • Those extra bits
    • relative text sizes, programmatic margins, ggtext::element_markdown or ggtext::element_textbox_simple, …
  • Effortlessly achieve a consistent aesthetic beyond just the colour scheme

Plot theme

… with a few option toggles

📦 {ophelia}

Two extra lines

Two extra lines

palmerpenguins::penguins %>%
  ggplot() +
  geom_point(aes(x = bill_length_mm,
                 y = flipper_length_mm,
                 colour = species,
                 size = body_mass_g)) +
  labs(x = "Bill length (mm)",
       y = "Flipper length (mm)",
       title = "Let's try some *italics* in the title",
       subtitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
       caption = "Data from {palmerpenguins}") +
  guides(size = "none")

Two extra lines

palmerpenguins::penguins %>%
  ggplot() +
  geom_point(aes(x = bill_length_mm,
                 y = flipper_length_mm,
                 colour = species,
                 size = body_mass_g)) +
  labs(x = "Bill length (mm)",
       y = "Flipper length (mm)",
       title = "Let's try some *italics* in the title",
       subtitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
       caption = "Data from {palmerpenguins}") +
  guides(size = "none") +
  scale_colour_ophelia() +
  theme_ophelia()

Two extra lines

palmerpenguins::penguins %>%
  ggplot() +
  geom_point(aes(x = bill_length_mm,
                 y = flipper_length_mm,
                 fill = body_mass_g,
                 size = body_mass_g),
             shape = 21,
             colour = "white",
             alpha = 0.8) +
  labs(x = "Bill length (mm)",
       y = "Flipper length (mm)",
       title = "Let's try some *italics* in the title",
       subtitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
       caption = "Data from {palmerpenguins}") +
  guides(size = "none")

Two extra lines

palmerpenguins::penguins %>%
  ggplot() +
  geom_point(aes(x = bill_length_mm,
                 y = flipper_length_mm,
                 fill = body_mass_g,
                 size = body_mass_g),
             shape = 21,
             colour = "white",
             alpha = 0.8) +
  labs(x = "Bill length (mm)",
       y = "Flipper length (mm)",
       title = "Let's try some *italics* in the title",
       subtitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
       caption = "Data from {palmerpenguins}") +
  guides(size = "none") +
  scale_fill_ophelia(continuous = TRUE) +
  theme_ophelia()

Two extra lines

palmerpenguins::penguins %>%
  ggplot() +
  geom_point(aes(x = bill_length_mm,
                 y = flipper_length_mm,
                 fill = body_mass_g,
                 size = body_mass_g),
             shape = 21,
             colour = "white",
             alpha = 0.8) +
  labs(x = "Bill length (mm)",
       y = "Flipper length (mm)",
       title = "Let's try some *italics* in the title",
       subtitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
       caption = "Data from {palmerpenguins}") +
  guides(size = "none") +
  scale_fill_ophelia(continuous = TRUE) +
  theme_ophelia(base_text_size = 18)

Two extra lines

palmerpenguins::penguins %>%
  filter(!is.na(sex)) %>%
  ggplot(aes(x = species,
             fill = island),
         stat = "count") +
  geom_bar() +
  labs(title = "Perfectly proportional penguins",
       subtitle = "Where do they all live?",
       caption = "Data from {palmerpenguins}") +
  facet_grid(. ~ sex) 

Two extra lines

palmerpenguins::penguins %>%
  filter(!is.na(sex)) %>%
  ggplot(aes(x = species,
             fill = island),
         stat = "count") +
  geom_bar() +
  labs(title = "Perfectly proportional penguins",
       subtitle = "Where do they all live?",
       caption = "Data from {palmerpenguins}") +
  facet_grid(. ~ sex) +
  scale_fill_ophelia(palette = "cool_colours") +
  theme_ophelia(background_colour = FALSE,
                base_text_size = 14)

Two extra lines

palmerpenguins::penguins %>%
  ggplot(aes(x = 1,
             fill = species),
         stat = "count") +
  geom_bar() +
  xlim(c(-0.5, 2)) +
  coord_polar(theta = "y") +
  labs(title = "Does anyone know if penguins like donuts?",
       subtitle = "Not sure, but we know there are three species in the dataset",
       caption = "Data from {palmerpenguins}")

Two extra lines

palmerpenguins::penguins %>%
  ggplot(aes(x = 1,
             fill = species),
         stat = "count") +
  geom_bar() +
  xlim(c(-0.5, 2)) +
  coord_polar(theta = "y") +
  labs(title = "Does anyone know if penguins like donuts?",
       subtitle = "Not sure, but we know there are three species in the dataset",
       caption = "Data from {palmerpenguins}") +
  scale_fill_ophelia("warm_colours") +
  theme_ophelia(void = TRUE,
                base_text_size = 14,
                background_colour = FALSE)

Two extra lines

tibble(answer = factor(rep(c("Strongly Disagree", "Disagree", 
                             "Agree", "Strongly Agree"), 2),
                       levels = c("Strongly Disagree", "Disagree", 
                                  "Strongly Agree", "Agree")),
       percent = c(8, 12, 40, 10,
                   6, 18, 34, 18),
       group = sort(rep(c("Male", "Female"), 4))) %>%
  mutate(display_percent = case_when(grepl("Dis|Neutral", answer) ~ -percent,
                                     TRUE ~ percent)) %>%
  ggplot() +
  geom_col(aes(x = group,
               fill = answer,
               y = display_percent),
           width = 0.8) +
  labs(title = "Let's find out!",
       subtitle = "How much do they agree with the statement \"Donuts are delicious\"?",
       caption = "Totally made up data!",
       x = "Percent",
       fill = "Answer") +
  scale_y_continuous(labels = function(x) paste0(abs(x), "%")) +
  coord_flip()

Two extra lines

tibble(answer = factor(rep(c("Strongly Disagree", "Disagree", 
                             "Agree", "Strongly Agree"), 2),
                       levels = c("Strongly Disagree", "Disagree", 
                                  "Strongly Agree", "Agree")),
       percent = c(8, 12, 40, 10,
                   6, 18, 34, 18),
       group = sort(rep(c("Male", "Female"), 4))) %>%
  mutate(display_percent = case_when(grepl("Dis|Neutral", answer) ~ -percent,
                                     TRUE ~ percent)) %>%
  ggplot() +
  geom_col(aes(x = group,
               fill = answer,
               y = display_percent),
           width = 0.8) +
  labs(title = "Let's find out!",
       subtitle = "How much do they agree with the statement \"Donuts are delicious\"?",
       caption = "Totally made up data!",
       x = "Percent",
       fill = "Answer") +
  scale_y_continuous(labels = function(x) paste0(abs(x), "%")) +
  coord_flip() +
  scale_fill_ophelia(
    palette = "neg_to_pos", 
    limits = c("Strongly Disagree", "Disagree", 
               "Agree", "Strongly Agree")) +
  theme_ophelia(background_colour = FALSE,
                base_text_size = 14) +
  theme(axis.title = element_blank(),
        legend.position = "bottom")

Tada!

“Next steps”

  • Add table-styling functions
  • Finalise documentation
  • Share package with the whole team
    • remotes::install_github("cararthompson/ophelia", "TopSecretKey")

Adding a table styling function

Starting point

head(palmerpenguins::penguins, 10)
# A tibble: 10 x 8
   species island    bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
   <fct>   <fct>              <dbl>         <dbl>             <int>       <int>
 1 Adelie  Torgersen           39.1          18.7               181        3750
 2 Adelie  Torgersen           39.5          17.4               186        3800
 3 Adelie  Torgersen           40.3          18                 195        3250
 4 Adelie  Torgersen           NA            NA                  NA          NA
 5 Adelie  Torgersen           36.7          19.3               193        3450
 6 Adelie  Torgersen           39.3          20.6               190        3650
 7 Adelie  Torgersen           38.9          17.8               181        3625
 8 Adelie  Torgersen           39.2          19.6               195        4675
 9 Adelie  Torgersen           34.1          18.1               193        3475
10 Adelie  Torgersen           42            20.2               190        4250
# i 2 more variables: sex <fct>, year <int>

Adding a table styling function

{ophelia} + {reactable}

head(palmerpenguins::penguins, 10) %>% 
  reactable::reactable(
    theme = ophelia::reactable_theme()
  )

Adding a table styling function

{ophelia} + {reactable}

head(palmerpenguins::penguins, 10) %>% 
  reactable::reactable(
    theme = ophelia::reactable_theme("gold"),
    striped = TRUE
  )

Adding a table styling function

{ophelia} + {reactable} + {reactablefmtr}

head(palmerpenguins::penguins, 10) %>% 
  reactable::reactable(
    theme = ophelia::reactable_theme(colour = "dark_green"), 
    pagination = FALSE, 
    striped = TRUE,
    columns = list(
      body_mass_g = reactable::colDef(
        cell = reactablefmtr::data_bars(head(palmerpenguins::penguins, 10), 
                         text_position = "outside-base",
                         fill_color = ophelia::ophelia_colours$light_blue,
                         number_fmt = function(x) format(x, big.mark = ","))
      )
    )
  )

Adding a table styling function

{ophelia} + {reactable} + {reactablefmtr}

Finalise documentation

Using {pkgdown} - cararthompson.github.io/ophelia/

Keep on building…

Set of Quarto slides!

carartemplates::create_slides()


Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Keep on building…

Set of Quarto slides!

ophelia::create_slides()


Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Keep on building…

Interactive graphs with ggiraph tooltip formatting defaults

palmerpenguins::penguins %>%
  ggplot() +
  geom_point(aes(x = bill_length_mm,
                 y = flipper_length_mm,
                 fill = body_mass_g,
                 size = body_mass_g),
             shape = 21,
             colour = "white",
             alpha = 0.8) +
  labs(x = "Bill length (mm)",
       y = "Flipper length (mm)",
       title = "Perfectly proportional penguins",
       caption = "Data from {palmerpenguins}") +
  guides(size = "none") +
  ophelia::scale_fill_ophelia(continuous = TRUE) +
  ophelia::theme_ophelia()

Keep on building…

Interactive graphs with ggiraph tooltip formatting defaults

penguin_plot <- palmerpenguins::penguins %>%
  ggplot() +
  ggiraph::geom_point_interactive(aes(x = bill_length_mm,
                                      y = flipper_length_mm,
                                      fill = body_mass_g,
                                      size = body_mass_g,
                                      tooltip = paste0("Body mass<br><b>", sprintf("%.03f", body_mass_g/1000), "kg</b>")),
                                  shape = 21,
                                  colour = "white",
                                  alpha = 0.8,
                                  show.legend = FALSE) +
  labs(x = "Bill length (mm)",
       y = "Flipper length (mm)",
       title = "Perfectly proportional penguins",
       caption = "Data from {palmerpenguins}") +
  ophelia::scale_fill_ophelia(continuous = TRUE) +
  ophelia::theme_ophelia()

ggiraph::girafe(ggobj = penguin_plot)

Keep on building…

Interactive graphs with ggiraph tooltip formatting defaults

penguin_plot <- palmerpenguins::penguins %>%
  ggplot() +
  ggiraph::geom_point_interactive(aes(x = bill_length_mm,
                                      y = flipper_length_mm,
                                      fill = body_mass_g,
                                      size = body_mass_g,
                                      tooltip = paste0("Body mass<br><b>", sprintf("%.03f", body_mass_g/1000), "kg</b>")),
                                  shape = 21,
                                  colour = "white",
                                  alpha = 0.8,
                                  show.legend = FALSE) +
  labs(x = "Bill length (mm)",
       y = "Flipper length (mm)",
       title = "Perfectly proportional penguins",
       caption = "Data from {palmerpenguins}") +
  ophelia::scale_fill_ophelia(continuous = TRUE) +
  ophelia::theme_ophelia()

ggiraph::girafe(ggobj = penguin_plot, 
                options = list(
                  ggiraph::opts_tooltip(
                    css = "background-color:#2B2529;color:#FEFCF7;padding:7.5px;letter-spacing:0.025em;line-height:1.3;border-radius:5px;font-family:Nunito Sans;")))

Keep on building…

Interactive graphs with ggiraph tooltip formatting defaults

penguin_plot <- palmerpenguins::penguins %>%
  ggplot() +
  ggiraph::geom_point_interactive(aes(x = bill_length_mm,
                                      y = flipper_length_mm,
                                      fill = body_mass_g,
                                      size = body_mass_g,
                                      tooltip = paste0("Body mass<br><b>", sprintf("%.03f", body_mass_g/1000), "kg</b>")),
                                  shape = 21,
                                  colour = "white",
                                  alpha = 0.8,
                                  show.legend = FALSE) +
  labs(x = "Bill length (mm)",
       y = "Flipper length (mm)",
       title = "Perfectly proportional penguins",
       caption = "Data from {palmerpenguins}") +
  ophelia::scale_fill_ophelia(continuous = TRUE) +
  ophelia::theme_ophelia()

ggiraph::girafe(ggobj = penguin_plot, 
                options = list(
                  ggiraph::opts_tooltip(
                    css = ophelia::tooltip_css)))

Building Dataviz Design Systems

  • Colour scheme
  • Set of fonts
  • ggplot theme
  • Interactive tables
  • Interactive plots
  • Quarto slides
  • Word/ppt-friendly tables
  • Commonly used plots

Over to you!



hello@cararthompson.com


Data-to-viz Commissions
Dataviz Design Systems
Training & Consultations