Clarity, Creativity, Accessibility with R and ggplot2

RMedicine | 6th May 2026

Hello 👋

👩 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

My happy place

💙 Teaching new stuff 💙 Learning new stuff 💙

Chat-based introductions

  • Where are you joining us from?
  • What time is it where you are?
  • What emoji would you use to describe your relationship with ggplot?
  • What type of data do you work with?
  • Do you like licorice / liquorice?

What’s the plan?

  • Make some graphs
  • Make them look good!
  • Create reusable components
  • Equip you to do all this at home
  • New(ish) ggplot tricks 💙
  • medicaldata::licorice_gargle

Housekeeping

  • All packages available on CRAN
  • Namespacing (package::function) apart from ggplot2
  • Code along + adapt to your own setting if you want to
  • Slides, recording and final code will be made available
  • No such thing as a silly question!
    • But the focus is on the graphs, not the stats 😅
  • We’ll take a break half way through

Let’s make a graph!

Our starting point

medicaldata::licorice_gargle |>
  dplyr::glimpse()
Rows: 235
Columns: 19
$ preOp_gender           <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ preOp_asa              <dbl> 3, 2, 2, 2, 1, 2, 3, 2, 3, 3, 2, 2, 2, 2, 3, 2,…
$ preOp_calcBMI          <dbl> 32.98, 23.66, 26.83, 28.39, 30.45, 35.49, 25.50…
$ preOp_age              <dbl> 67, 76, 58, 59, 73, 61, 66, 61, 83, 69, 31, 72,…
$ preOp_mallampati       <dbl> 2, 2, 2, 2, 1, 3, 1, 2, 1, 2, 1, 2, 2, 1, 2, 2,…
$ preOp_smoking          <dbl> 1, 2, 1, 1, 2, 1, 1, 1, 1, 3, 1, 2, 3, 3, 3, 1,…
$ preOp_pain             <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ treat                  <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
$ intraOp_surgerySize    <dbl> 2, 1, 2, 3, 2, 3, 3, 1, 1, 2, 2, 1, 1, 2, 2, 2,…
$ extubation_cough       <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ pacu30min_cough        <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ pacu30min_throatPain   <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ pacu30min_swallowPain  <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ pacu90min_cough        <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ pacu90min_throatPain   <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ postOp4hour_cough      <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ postOp4hour_throatPain <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ pod1am_cough           <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ pod1am_throatPain      <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…

Let’s make a graph!

Our starting point

?medicaldata::licorice_gargle

This study enrolled 236 adult patients undergoing elective thoracic surgery requiring a double-lumen endotracheal tube. Gender, physical status, BMI, age, Mallampati score, smoking status, preoperative pain, surgery size, intervention and the outcomes (cough, sore throat and pain swallowing at various time points) are provided.

  • preOp_gender Gender, numeric, 0 = Male; 1 = Female
  • treat Intervention, 0 = Sugar 5g; 1 = Licorice 0.5g
  • pod1am_cough Amount of coughing on the first postoperative morning, 0 = No cough; 1 = Mild; 2 = Moderate; 3 = Severe, numeric, range: 0-3

Let’s make a graph

We could do this…

library(ggplot2)

medicaldata::licorice_gargle |>
  ggplot(aes(
    x = preOp_gender,
    y = pod1am_cough,
    colour = treat
  )) +
  geom_boxplot()

Let’s make a graph

Hmm, let’s sort out the data

tidied_data <- medicaldata::licorice_gargle |>
  dplyr::mutate(
    gender = factor(
      dplyr::case_when(
        preOp_gender == 0 ~ "Male",
        preOp_gender == 1 ~ "Female"
      )
    ),
    treatment = factor(dplyr::case_when(
      treat == 0 ~ "Sugar",
      treat == 1 ~ "Licorice"
    ))
  ) |>
  dplyr::filter(!is.na(pod1am_cough))

Let’s make a graph

Hmm, let’s fix the data

dplyr::glimpse(tidied_data)
Rows: 233
Columns: 21
$ preOp_gender           <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ preOp_asa              <dbl> 3, 2, 2, 2, 1, 2, 3, 2, 3, 3, 2, 2, 2, 2, 3, 2,…
$ preOp_calcBMI          <dbl> 32.98, 23.66, 26.83, 28.39, 30.45, 35.49, 25.50…
$ preOp_age              <dbl> 67, 76, 58, 59, 73, 61, 66, 61, 83, 69, 31, 72,…
$ preOp_mallampati       <dbl> 2, 2, 2, 2, 1, 3, 1, 2, 1, 2, 1, 2, 2, 1, 2, 2,…
$ preOp_smoking          <dbl> 1, 2, 1, 1, 2, 1, 1, 1, 1, 3, 1, 2, 3, 3, 3, 1,…
$ preOp_pain             <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ treat                  <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
$ intraOp_surgerySize    <dbl> 2, 1, 2, 3, 2, 3, 3, 1, 1, 2, 2, 1, 1, 2, 2, 2,…
$ extubation_cough       <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ pacu30min_cough        <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ pacu30min_throatPain   <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ pacu30min_swallowPain  <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ pacu90min_cough        <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ pacu90min_throatPain   <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ postOp4hour_cough      <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ postOp4hour_throatPain <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ pod1am_cough           <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ pod1am_throatPain      <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ gender                 <fct> Male, Male, Male, Male, Male, Male, Male, Male,…
$ treatment              <fct> Licorice, Licorice, Licorice, Licorice, Licoric…

Let’s make a graph

Let’s try that again…

ggplot(tidied_data, aes(x = gender, y = pod1am_cough, colour = treatment)) +
  geom_boxplot()

Let’s make a graph

But let’s make it a bit easier to understand

ggplot(tidied_data, aes(x = gender, y = pod1am_cough, colour = treatment)) +
  geom_jitter()

Let’s make a graph

But let’s make it a bit easier to understand

ggplot(tidied_data, aes(x = gender, y = pod1am_cough, colour = treatment)) +
  geom_jitter(height = 0.1, width = 0.1, alpha = 0.8)

Let’s make a graph

But let’s make it a bit easier to understand - 💙

ggplot(tidied_data, aes(x = gender, y = pod1am_cough, colour = treatment)) +
  geom_jitter(height = 0.1, width = 0.1, alpha = 0.8) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y)),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  )

Let’s make a graph

But let’s make it a bit easier to understand - 💙

ggplot(tidied_data, aes(x = gender, y = pod1am_cough, colour = treatment)) +
  geom_jitter(height = 0.1, width = 0.1, alpha = 0.8) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y)),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2)
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.05)
  )

Let’s make a graph

But let’s make it a bit easier to understand - 💙

ggplot(tidied_data, aes(x = gender, y = pod1am_cough, colour = treatment)) +
  geom_jitter(height = 0.1, width = 0.1, alpha = 0.8) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y)),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2)
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.05),
    colour = "black"
  )

Let’s make a graph

But let’s make it a bit easier to understand - 💙

ggplot(tidied_data, aes(x = gender, y = pod1am_cough, colour = treatment)) +
  geom_jitter(height = 0.1, width = 0.1, alpha = 0.8) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y)),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.05),
    colour = "black"
  )

Let’s make a graph

Let’s fix the text size

ggplot(tidied_data, aes(x = gender, y = pod1am_cough, colour = treatment)) +
  geom_jitter(height = 0.1, width = 0.1, alpha = 0.8) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y)),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.05),
    colour = "black"
  ) +
  theme_minimal(base_size = 20)

Let’s make a graph

Let’s fix the text size

ggplot(tidied_data, aes(x = gender, y = pod1am_cough, colour = treatment)) +
  geom_jitter(height = 0.1, width = 0.1, alpha = 0.8) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y)),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.05),
    colour = "black",
    size = 20
  ) +
  theme_minimal(base_size = 20)

Let’s make a graph

Let’s fix the text size - 💙 size.unit = "pt"

ggplot(tidied_data, aes(x = gender, y = pod1am_cough, colour = treatment)) +
  geom_jitter(height = 0.1, width = 0.1, alpha = 0.8) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y)),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 20,
    size.unit = "pt"
  ) +
  theme_minimal(base_size = 20)

Let’s make a graph

Let’s fix the text size - 💙

ggplot(tidied_data, aes(x = gender, y = pod1am_cough, colour = treatment)) +
  geom_jitter(height = 0.1, width = 0.1, alpha = 0.8) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y)),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20)

Give it a go

Gist 1: Our basic graph!

  • install.packages("medicaldata")
  • Tidying up the data
  • stat_summary()

Style it

Style it

  • Licorice
  • Sugar
  • Highlight colour

Style it

  • Licorice - #39496a
  • Sugar - #c592c7
  • Highlight colour - #f8f28b

Your turn 🧑‍🎨🎨

What colours work for your own topic?

Add a bit more style

Starting point: Licorice - #39496a

  • Dark text      
  • Light text      
  • Background      

Implementation

Add colours

licorice_gargle_colours <- c(
  "Licorice" = "#39496a",
  "Sugar" = "#c592c7",
  "Highlight" = "#f8f28b",
  "Dark text" = "#1A2231",
  "Light text" = "#383F4C",
  "Background" = "#F8F1F8"
)

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, colour = treatment)
) +
  geom_jitter(height = 0.1, width = 0.1, alpha = 0.8) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y)),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20)

Implementation

Add colours - and shuffle the dots! 💙

ggplot(
  tidied_data |> dplyr::slice_sample(prop = 1),
  aes(x = gender, y = pod1am_cough, colour = treatment)
) +
  geom_jitter(height = 0.1, width = 0.1, alpha = 0.8) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y)),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20)

Implementation

Or jitter + dodge - position_jitterdodge() 💙

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, colour = treatment)
) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    )
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y)),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20)

Accessibility check

  • Are the colours different from each other?

Accessibility check

  • Do they stand out enough from the background?

colourcontrast.cc

Accessibility fix

Change the shape, to add a contour

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, colour = treatment)
) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y)),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20)

Accessibility fix

Change the shape, to add a contour

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y)),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20)

Set default styles for contours

theme(geom = element_geom()) 💙

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20) +
  theme(
    geom = element_geom(
      ink = licorice_gargle_colours["Light text"],
      size = 5,
      borderwidth = 0.5, # not `stroke`
      linewidth = 0.2
    )
  )

Add a background colour

theme() - we’ll do more in a minute!

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21,
    size = 5
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      ymin = after_stat(y),
      ymax = after_stat(y),
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    width = 0.5,
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20) +
  theme(
    geom = element_geom(
      ink = licorice_gargle_colours["Light text"],
      borderwidth = 0.5,
      linewidth = 0.2
    ),
    plot.background = element_rect(
      fill = licorice_gargle_colours["Background"],
      colour = licorice_gargle_colours["Background"]
    ),
    panel.grid = element_line(colour = "white")
  )

Give it a go

Gist 2 - Our colourful graph!

  • Add you own colours
  • Check for accessibility
  • Change the shapes / play around with theme(geom = element_geom()) defaults

Use gradients for emphasis

Bars with gradient fill for semantic emphasis! 💙

Use gradients for emphasis

First, we need to create a gradient

blue_to_red <- grid::linearGradient(
  colours = c("blue", "red"),
  x1 = 0,
  y1 = 0,
  x2 = 0,
  y2 = 1,
  group = FALSE
)

ggplot(tidied_data) +
  geom_bar(
    data = tidied_data |>
      dplyr::group_by(gender) |>
      dplyr::filter(pod1am_cough == max(pod1am_cough, na.rm = TRUE)) |>
      dplyr::select(gender, pod1am_cough) |>
      unique(),
    aes(x = gender, y = pod1am_cough + 0.2),
    stat = "identity",
    position = "dodge",
    fill = blue_to_red,
    width = 0.5,
    alpha = 1
  )

Use gradients for emphasis

First, we need to create a gradient

blue_to_red <- grid::linearGradient(
  colours = c("blue", "red"),
  x1 = 0,
  y1 = 0,
  x2 = 0,
  y2 = 1,
  group = TRUE # default
)

ggplot(tidied_data) +
  geom_bar(
    data = tidied_data |>
      dplyr::group_by(gender) |>
      dplyr::filter(pod1am_cough == max(pod1am_cough, na.rm = TRUE)) |>
      dplyr::select(gender, pod1am_cough) |>
      unique(),
    aes(x = gender, y = pod1am_cough + 0.2),
    stat = "identity",
    position = "dodge",
    fill = blue_to_red,
    width = 0.5,
    alpha = 1
  )

Use gradients for emphasis

And now, some subtlety!

bg_to_red <- grid::linearGradient(
  colours = c(licorice_gargle_colours["Background"], "red"),
  x1 = 0,
  y1 = 0,
  x2 = 0,
  y2 = 1,
  group = TRUE
)

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  geom_bar(
    data = tidied_data |>
      dplyr::group_by(gender) |>
      dplyr::filter(pod1am_cough == max(pod1am_cough, na.rm = TRUE)) |>
      dplyr::select(gender, pod1am_cough) |>
      unique(),
    aes(x = gender, y = pod1am_cough + 0.2),
    stat = "identity",
    position = "dodge",
    fill = bg_to_red,
    width = 0.5,
    alpha = 0.2
  ) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21,
    size = 5
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20) +
  theme(
    geom = element_geom(
      ink = licorice_gargle_colours["Light text"],
      borderwidth = 0.5,
      linewidth = 0.2
    ),
    plot.background = element_rect(
      fill = licorice_gargle_colours["Background"],
      colour = licorice_gargle_colours["Background"]
    ),
    panel.grid = element_line(colour = "white")
  )

Add annotations for clarity

And to demonstrate the use of I()! 💙

Add annotations for clarity

… and position them relative to the graph!

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  geom_bar(
    data = tidied_data |>
      dplyr::group_by(gender) |>
      dplyr::filter(pod1am_cough == max(pod1am_cough, na.rm = TRUE)) |>
      dplyr::select(gender, pod1am_cough) |>
      unique(),
    aes(x = gender, y = pod1am_cough + 0.2),
    stat = "identity",
    position = "dodge",
    fill = bg_to_red,
    width = 0.5,
    alpha = 0.2
  ) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21,
    size = 5
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  geom_text(
    data = data.frame(),
    aes(
      x = I(0.5),
      y = 3,
      label = "These people did\na lot of coughing",
      fill = NULL
    )
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20) +
  theme(
    geom = element_geom(
      ink = licorice_gargle_colours["Light text"],
      borderwidth = 0.5,
      linewidth = 0.2
    ),
    plot.background = element_rect(
      fill = licorice_gargle_colours["Background"],
      colour = licorice_gargle_colours["Background"]
    ),
    panel.grid = element_line(colour = "white")
  )

Add annotations for clarity

Sort out the legend position.

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  geom_bar(
    data = tidied_data |>
      dplyr::group_by(gender) |>
      dplyr::filter(pod1am_cough == max(pod1am_cough, na.rm = TRUE)) |>
      dplyr::select(gender, pod1am_cough) |>
      unique(),
    aes(x = gender, y = pod1am_cough + 0.2),
    stat = "identity",
    position = "dodge",
    fill = bg_to_red,
    width = 0.5,
    alpha = 0.2
  ) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21,
    size = 5
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  geom_text(
    data = tibble::tibble(
      y_coord = c(0, 1, 2, 3),
      severity = c("No cough", "Mild", "Moderate", "Severe cough")
    ),
    aes(
      x = I(0.5),
      y = y_coord,
      label = severity,
      fill = NULL
    )
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20) +
  theme(
    geom = element_geom(
      ink = licorice_gargle_colours["Light text"],
      borderwidth = 0.5,
      linewidth = 0.2
    ),
    plot.background = element_rect(
      fill = licorice_gargle_colours["Background"],
      colour = licorice_gargle_colours["Background"]
    ),
    panel.grid = element_line(colour = "white"),
    legend.position = "bottom"
  )

Add annotations for clarity

Wait a minute… 👀

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  geom_bar(
    data = tidied_data |>
      dplyr::group_by(gender) |>
      dplyr::filter(pod1am_cough == max(pod1am_cough, na.rm = TRUE)) |>
      dplyr::select(gender, pod1am_cough) |>
      unique(),
    # Culprit is here!
    aes(x = gender, y = pod1am_cough + 0.2),
    stat = "identity",
    position = "dodge",
    fill = bg_to_red,
    width = 0.5,
    alpha = 0.2
  ) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21,
    size = 5
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  geom_text(
    data = tibble::tibble(
      y_coord = c(0, 1, 2, 3),
      severity = c("No cough", "Mild", "Moderate", "Severe cough")
    ),
    aes(
      x = I(0.5),
      y = y_coord,
      label = severity,
      fill = NULL
    )
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20) +
  theme(
    geom = element_geom(
      ink = licorice_gargle_colours["Light text"],
      borderwidth = 0.5,
      linewidth = 0.2
    ),
    plot.background = element_rect(
      fill = licorice_gargle_colours["Background"],
      colour = licorice_gargle_colours["Background"]
    ),
    panel.grid = element_line(colour = "white"),
    legend.position = "bottom"
  )

Add annotations for clarity

Declutter! What else?

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  geom_bar(
    data = tidied_data |>
      dplyr::group_by(gender) |>
      dplyr::filter(pod1am_cough == max(pod1am_cough, na.rm = TRUE)) |>
      dplyr::select(gender, pod1am_cough) |>
      unique(),
    # Culprit is here!
    aes(x = gender, y = pod1am_cough + 0.2),
    stat = "identity",
    position = "dodge",
    fill = bg_to_red,
    width = 0.5,
    alpha = 0.2
  ) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21,
    size = 5
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  geom_text(
    data = tibble::tibble(
      y_coord = c(0, 1, 2, 3),
      severity = c("No cough", "Mild", "Moderate", "Severe cough")
    ),
    aes(
      x = I(0.5),
      y = y_coord,
      label = severity,
      fill = NULL
    )
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20) +
  theme(
    geom = element_geom(
      ink = licorice_gargle_colours["Light text"],
      borderwidth = 0.5,
      linewidth = 0.2
    ),
    plot.background = element_rect(
      fill = licorice_gargle_colours["Background"],
      colour = licorice_gargle_colours["Background"]
    ),
    panel.grid = element_line(colour = "white"),
    legend.position = "bottom",
    axis.title.y = element_blank(),
    axis.text.y = element_blank()
  )

Add annotations for clarity

Declutter! What else?

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  geom_bar(
    data = tidied_data |>
      dplyr::group_by(gender) |>
      dplyr::filter(pod1am_cough == max(pod1am_cough, na.rm = TRUE)) |>
      dplyr::select(gender, pod1am_cough) |>
      unique(),
    # Culprit is here!
    aes(x = gender, y = pod1am_cough + 0.5),
    stat = "identity",
    position = "dodge",
    fill = bg_to_red,
    width = 0.5,
    alpha = 0.2
  ) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21,
    size = 5
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  geom_text(
    data = tibble::tibble(
      y_coord = c(0, 1, 2, 3),
      severity = c("No cough", "Mild", "Moderate", "Severe cough")
    ),
    aes(
      x = I(0.5),
      y = y_coord,
      label = severity,
      fill = NULL
    )
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20) +
  theme(
    geom = element_geom(
      ink = licorice_gargle_colours["Light text"],
      borderwidth = 0.5,
      linewidth = 0.2
    ),
    plot.background = element_rect(
      fill = licorice_gargle_colours["Background"],
      colour = licorice_gargle_colours["Background"]
    ),
    panel.grid = element_line(colour = "white"),
    legend.position = "bottom",
    axis.title.y = element_blank(),
    axis.text.y = element_blank(),
    axis.title.x = element_blank(),
    legend.title = element_blank(),
    panel.grid.major.x = element_blank(),
    panel.grid.major.y = element_blank(),
    axis.line.x = element_line(
      colour = licorice_gargle_colours["Light text"],
      linewidth = 0.2
    )
  )

What am I finding confusing?

Declutter! What else?

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  geom_bar(
    data = tidied_data |>
      dplyr::group_by(gender) |>
      dplyr::filter(pod1am_cough == max(pod1am_cough, na.rm = TRUE)) |>
      dplyr::select(gender, pod1am_cough) |>
      unique(),
    # Culprit is here!
    aes(x = gender, y = pod1am_cough + 0.5),
    stat = "identity",
    position = "dodge",
    fill = bg_to_red,
    width = 0.5,
    alpha = 0.2
  ) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21,
    size = 5
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  geom_text(
    data = tibble::tibble(
      y_coord = c(0, 1, 2, 3),
      severity = c("No cough", "Mild", "Moderate", "Severe cough")
    ),
    aes(
      x = I(0.5),
      y = y_coord,
      label = severity,
      fill = NULL
    )
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20) +
  theme(
    geom = element_geom(
      ink = licorice_gargle_colours["Light text"],
      borderwidth = 0.5,
      linewidth = 0.2
    ),
    plot.background = element_rect(
      fill = licorice_gargle_colours["Background"],
      colour = licorice_gargle_colours["Background"]
    ),
    panel.grid = element_line(colour = "white"),
    legend.position = "bottom",
    axis.title.y = element_blank(),
    axis.text.y = element_blank(),
    axis.title.x = element_blank(),
    legend.title = element_blank(),
    panel.grid.major.x = element_blank(),
    panel.grid.major.y = element_blank(),
    axis.line.x = element_line(
      colour = licorice_gargle_colours["Light text"],
      linewidth = 0.2
    ),
    axis.text.x = element_text(size = rel(1.2), face = "bold")
  )

What am I finding confusing?

Declutter! What else?

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  geom_bar(
    data = tidied_data |>
      dplyr::group_by(gender) |>
      dplyr::filter(pod1am_cough == max(pod1am_cough, na.rm = TRUE)) |>
      dplyr::select(gender, pod1am_cough) |>
      unique(),
    # Culprit is here!
    aes(x = gender, y = pod1am_cough + 0.5),
    stat = "identity",
    position = "dodge",
    fill = bg_to_red,
    width = 0.5,
    alpha = 0.2
  ) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21,
    size = 5
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  geom_text(
    data = tibble::tibble(
      y_coord = c(0, 1, 2, 3),
      severity = c("No cough", "Mild", "Moderate", "Severe cough")
    ),
    aes(
      x = I(0.5),
      y = y_coord,
      label = severity,
      fill = NULL
    )
  ) +
  scale_x_discrete(position = "top") +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20) +
  theme(
    geom = element_geom(
      ink = licorice_gargle_colours["Light text"],
      borderwidth = 0.5,
      linewidth = 0.2
    ),
    plot.background = element_rect(
      fill = licorice_gargle_colours["Background"],
      colour = licorice_gargle_colours["Background"]
    ),
    panel.grid = element_line(colour = "white"),
    legend.position = "bottom",
    axis.title.y = element_blank(),
    axis.text.y = element_blank(),
    axis.title.x = element_blank(),
    legend.title = element_blank(),
    panel.grid.major.x = element_blank(),
    panel.grid.major.y = element_blank(),
    axis.text.x = element_text(size = rel(1.2), face = "bold")
  )

What am I finding confusing?

Declutter! What else?

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  # Annotate doesn't support the gradient fill, so we need geom_rect
  geom_rect(
    # To get the structure of the data but only one layer of rectangle
    data = tidied_data |> dplyr::sample_n(1),
    aes(xmin = I(0.1), xmax = I(0.9), ymin = 0, ymax = 3.5),
    fill = bg_to_red,
    alpha = 0.1
  ) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21,
    size = 5
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  geom_text(
    data = tibble::tibble(
      y_coord = c(0, 1, 2, 3),
      severity = c("No cough", "Mild", "Moderate", "Severe cough")
    ),
    aes(
      x = I(0.5),
      y = y_coord,
      label = severity,
      fill = NULL
    )
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20) +
  theme(
    geom = element_geom(
      ink = licorice_gargle_colours["Light text"],
      borderwidth = 0.5,
      linewidth = 0.2
    ),
    plot.background = element_rect(
      fill = licorice_gargle_colours["Background"],
      colour = licorice_gargle_colours["Background"]
    ),
    panel.grid = element_line(colour = "white"),
    legend.position = "bottom",
    axis.title.y = element_blank(),
    axis.text.y = element_blank(),
    axis.title.x = element_blank(),
    legend.title = element_blank(),
    panel.grid.major.x = element_blank(),
    panel.grid.major.y = element_blank(),
    axis.line.x = element_line(
      colour = licorice_gargle_colours["Light text"],
      linewidth = 0.2
    ),
    axis.text.x = element_text(size = rel(1.2), face = "bold")
  )

Give it a go

Gist 3 - Our graph with annotations

  • gradient fill
  • I()
  • adding the axis line back in

Time for a break ☕️

Grab a coffee / decaf option / some time outdoors.

See you in 15 mins!

Favourite “aha!” so far?

  • What was new?
  • What can you already see a use for in your own graph?

Optimise the text

  • What? How? Oh no! Ooh…

Optimise the text content

Descriptive vs. declarative title?

licorice_cough_plot <- ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  # Annotate doesn't support the gradient fill, so we need geom_rect
  geom_rect(
    # To get the structure of the data but only one layer of rectangle
    data = tidied_data |> dplyr::sample_n(1),
    aes(xmin = I(0.1), xmax = I(0.9), ymin = 0, ymax = 3.5),
    fill = bg_to_red,
    alpha = 0.1
  ) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21,
    size = 5
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  geom_text(
    data = tibble::tibble(
      y_coord = c(0, 1, 2, 3),
      severity = c("No cough", "Mild", "Moderate", "Severe cough")
    ),
    aes(
      x = I(0.5),
      y = y_coord,
      label = severity,
      fill = NULL
    )
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  labs(title = "Severity of cough on the morning after operation") +
  theme_minimal(base_size = 20) +
  theme(
    geom = element_geom(
      ink = licorice_gargle_colours["Light text"],
      borderwidth = 0.5, #
      linewidth = 0.2
    ),
    plot.background = element_rect(
      fill = licorice_gargle_colours["Background"],
      colour = licorice_gargle_colours["Background"]
    ),
    panel.grid = element_line(colour = "white"),
    legend.position = "bottom",
    axis.title.y = element_blank(),
    axis.text.y = element_blank(),
    axis.title.x = element_blank(),
    legend.title = element_blank(),
    panel.grid.major.x = element_blank(),
    panel.grid.major.y = element_blank(),
    axis.line.x = element_line(
      colour = licorice_gargle_colours["Light text"],
      linewidth = 0.2
    ),
    axis.text.x = element_text(size = rel(1.2), face = "bold")
  )

licorice_cough_plot

Optimise the text content

Declarative title

licorice_cough_plot +
  labs(
    title = "Patients who gargled Licorice reported a slightly less severe cough than those who gargled Sugar",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  )

Optimise the font

Personality without compromising on Accessibility

licorice_cough_plot +
  labs(
    title = "Patients who gargled Licorice reported a slightly less severe cough than those who gargled Sugar",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  theme(
    text = element_text(family = "Atkinson Hyperlegible")
  )

Optimise the font

theme(geom = element_geom()) has a family argument! 💙

licorice_cough_plot +
  labs(
    title = "Patients who gargled Licorice reported a slightly less severe cough than those who gargled Sugar",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  theme(
    text = element_text(family = "Atkinson Hyperlegible"),
    geom = element_geom(family = "Atkinson Hyperlegible")
  )

Optimise the text hierarchy

Optimise the text hierarchy

Starting point

licorice_cough_plot +
  labs(
    title = "Patients who gargled Licorice reported a slightly less severe cough than those who gargled Sugar",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  theme(
    text = element_text(family = "Atkinson Hyperlegible"),
    geom = element_geom(family = "Atkinson Hyperlegible")
  )

Optimise the text hierarchy

rel() is your friend!

licorice_cough_plot +
  labs(
    title = "Patients who gargled Licorice reported a slightly less severe cough than those who gargled Sugar",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  theme(
    text = element_text(family = "Atkinson Hyperlegible"),
    geom = element_geom(family = "Atkinson Hyperlegible"),
    plot.title = element_text(size = rel(1.5))
  )

Optimise the text hierarchy

rel() is your friend!

licorice_cough_plot +
  labs(
    title = "Patients who gargled Licorice reported a slightly less severe cough than those who gargled Sugar",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  theme(
    text = element_text(family = "Atkinson Hyperlegible"),
    geom = element_geom(family = "Atkinson Hyperlegible"),
    plot.title = element_text(size = rel(1.5), face = "bold")
  )

Optimise the text hierarchy

marquee::element_marquee or ggtext::element_textbox_simple wrap text for you

licorice_cough_plot +
  labs(
    title = "Patients who gargled Licorice reported a slightly less severe cough than those who gargled Sugar",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  theme(
    text = element_text(family = "Atkinson Hyperlegible"),
    geom = element_geom(family = "Atkinson Hyperlegible"),
    plot.title = marquee::element_marquee(
      size = rel(1.5),
      width = 1,
      style = marquee::classic_style(weight = "bold")
    ),
    plot.subtitle = ggtext::element_textbox_simple()
  )

Optimise the text hierarchy

Optimise with colours

licorice_cough_plot +
  labs(
    title = "Patients who gargled Licorice reported a slightly less severe cough than those who gargled Sugar",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  theme(
    text = element_text(family = "Atkinson Hyperlegible"),
    geom = element_geom(family = "Atkinson Hyperlegible"),
    plot.title = marquee::element_marquee(
      size = rel(1.5),
      colour = licorice_gargle_colours["Dark text"],
      width = 1,
      style = marquee::classic_style(weight = "bold")
    ),
    plot.subtitle = ggtext::element_textbox_simple(
      colour = licorice_gargle_colours["Light text"]
    )
  )

Optimise the signposting

Doing the legend a bit differently…

highlight_style <- marquee::classic_style(weight = "bold") |>
  marquee::modify_style(
    "sugar",
    background = licorice_gargle_colours["Sugar"],
    # US spelling of colour/color required!
    color = licorice_gargle_colours["Dark text"],
    padding = marquee::trbl(marquee::em(0.1))
  ) |>
  marquee::modify_style(
    "licorice",
    background = licorice_gargle_colours["Licorice"],
    color = "white",
    padding = marquee::trbl(marquee::em(0.1))
  )

Optimise the signposting

Doing the legend a bit differently…

licorice_cough_plot +
  labs(
    title = "Patients who gargled {.licorice Licorice} reported a slightly less severe cough than those who gargled {.sugar Sugar}",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  theme(
    text = element_text(family = "Atkinson Hyperlegible"),
    geom = element_geom(family = "Atkinson Hyperlegible"),
    plot.title = marquee::element_marquee(
      size = rel(1.5),
      colour = licorice_gargle_colours["Dark text"],
      width = 1,
      style = highlight_style
    ),
    plot.subtitle = ggtext::element_textbox_simple(
      colour = licorice_gargle_colours["Light text"]
    )
  )

Optimise the signposting

Doing the legend a bit differently…

licorice_cough_plot +
  labs(
    title = "Patients who gargled {.licorice Licorice} reported a slightly less severe cough than those who gargled {.sugar Sugar}",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  theme(
    text = element_text(family = "Atkinson Hyperlegible"),
    geom = element_geom(family = "Atkinson Hyperlegible"),
    plot.title = marquee::element_marquee(
      size = rel(1.5),
      colour = licorice_gargle_colours["Dark text"],
      width = 1,
      style = highlight_style
    ),
    plot.subtitle = ggtext::element_textbox_simple(
      colour = licorice_gargle_colours["Light text"]
    ),
    legend.position = "none"
  )

Give everything some space to breathe

Lineheight 👀

licorice_cough_plot +
  labs(
    title = "Patients who gargled {.licorice Licorice} reported a slightly less severe cough than those who gargled {.sugar Sugar}",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  theme(
    text = element_text(family = "Atkinson Hyperlegible"),
    geom = element_geom(family = "Atkinson Hyperlegible"),
    plot.title = marquee::element_marquee(
      size = rel(1.5),
      colour = licorice_gargle_colours["Dark text"],
      width = 1,
      style = highlight_style,
      lineheight = 1.3
    ),
    plot.subtitle = ggtext::element_textbox_simple(
      colour = licorice_gargle_colours["Light text"],
      lineheight = 1.3
    ),
    legend.position = "none"
  )

Give everything space to breathe

“Eyeball” it!

licorice_cough_plot +
  labs(
    title = "Patients who gargled {.licorice Licorice} reported a slightly less severe cough than those who gargled {.sugar Sugar}",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  theme(
    text = element_text(family = "Atkinson Hyperlegible"),
    geom = element_geom(family = "Atkinson Hyperlegible"),
    plot.title = marquee::element_marquee(
      size = rel(1.5),
      colour = licorice_gargle_colours["Dark text"],
      width = 1,
      style = highlight_style,
      lineheight = 1.05
    ),
    plot.subtitle = ggtext::element_textbox_simple(
      colour = licorice_gargle_colours["Light text"],
      lineheight = 1.3
    ),
    legend.position = "none"
  )

Give everything space to breathe

Margins!

licorice_cough_plot +
  labs(
    title = "Patients who gargled {.licorice Licorice} reported a slightly less severe cough than those who gargled {.sugar Sugar}",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  theme(
    text = element_text(family = "Atkinson Hyperlegible"),
    geom = element_geom(family = "Atkinson Hyperlegible"),
    plot.title = marquee::element_marquee(
      size = rel(1.5),
      colour = licorice_gargle_colours["Dark text"],
      width = 1,
      style = highlight_style,
      lineheight = 1.05
    ),
    plot.subtitle = ggtext::element_textbox_simple(
      colour = licorice_gargle_colours["Light text"],
      lineheight = 1.3,
      margin = margin(15, 0, 20, 0)
    ),
    legend.position = "none",
    plot.margin = margin_auto(30)
  )

Give everything space to breathe

Margins!

licorice_cough_plot +
  labs(
    title = "Patients who gargled {.licorice Licorice} reported a slightly less severe cough than those who gargled {.sugar Sugar}",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  theme(
    text = element_text(family = "Atkinson Hyperlegible"),
    geom = element_geom(family = "Atkinson Hyperlegible"),
    plot.title = marquee::element_marquee(
      size = rel(1.5),
      colour = licorice_gargle_colours["Dark text"],
      width = 1,
      vjust = 0,
      margin = margin(30, 0, 0, 0),
      style = highlight_style,
      lineheight = 1.05
    ),
    plot.subtitle = ggtext::element_textbox_simple(
      colour = licorice_gargle_colours["Light text"],
      lineheight = 1.3,
      margin = margin(15, 0, 20, 0)
    ),
    legend.position = "none",
    plot.margin = margin_auto(30)
  )

Let’s tidy up all those theme elements!

Making the theme reusable

We’ve done a lot of theme modification!

highlight_style <- marquee::classic_style(weight = "bold") |>
  marquee::modify_style(
    "sugar",
    background = licorice_gargle_colours["Sugar"],
    # US spelling of colour/color required!
    color = licorice_gargle_colours["Dark text"],
    padding = marquee::trbl(marquee::em(0.1))
  ) |>
  marquee::modify_style(
    "licorice",
    background = licorice_gargle_colours["Licorice"],
    color = "white",
    padding = marquee::trbl(marquee::em(0.1))
  )

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  geom_rect(
    data = tidied_data |> tail(1),
    aes(xmin = I(0.1), xmax = I(0.9), ymin = 0, ymax = 3.5),
    fill = bg_to_red,
    alpha = 0.1
  ) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21,
    size = 5
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  geom_text(
    data = tibble::tibble(
      y_coord = c(0, 1, 2, 3),
      severity = c("No cough", "Mild", "Moderate", "Severe cough")
    ),
    aes(
      x = I(0.5),
      y = y_coord,
      label = severity,
      fill = NULL
    )
  ) +
  labs(
    title = "Patients who gargled {.licorice Licorice} reported a slightly less severe cough than those who gargled {.sugar Sugar}",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  theme_minimal(base_size = 20) +
  theme(
    text = element_text(family = "Atkinson Hyperlegible"),
    geom = element_geom(
      ink = licorice_gargle_colours["Light text"],
      borderwidth = 0.5,
      linewidth = 0.2,
      family = "Atkinson Hyperlegible"
    ),
    plot.background = element_rect(
      fill = licorice_gargle_colours["Background"],
      colour = licorice_gargle_colours["Background"]
    ),
    panel.grid = element_line(colour = "white"),
    axis.title.y = element_blank(),
    axis.text.y = element_blank(),
    axis.title.x = element_blank(),
    legend.title = element_blank(),
    panel.grid.major.x = element_blank(),
    panel.grid.major.y = element_blank(),
    axis.line.x = element_line(
      colour = licorice_gargle_colours["Light text"],
      linewidth = 0.2
    ),
    axis.text.x = element_text(size = rel(1.2), face = "bold"),
    plot.title = marquee::element_marquee(
      size = rel(1.5),
      colour = licorice_gargle_colours["Dark text"],
      width = 1,
      vjust = 0,
      margin = margin(30, 0, 0, 0),
      style = highlight_style,
      lineheight = 1.05
    ),
    plot.subtitle = ggtext::element_textbox_simple(
      colour = licorice_gargle_colours["Light text"],
      lineheight = 1.3,
      margin = margin(15, 0, 20, 0)
    ),
    legend.position = "none",
    plot.margin = margin_auto(30)
  )

Making the theme reusable

What should we exclude?

theme_licorice <- function() {
  theme_minimal(base_size = 20) +
    theme(
      text = element_text(family = "Atkinson Hyperlegible"),
      geom = element_geom(
        ink = licorice_gargle_colours["Light text"],
        borderwidth = 0.5,
        linewidth = 0.2,
        family = "Atkinson Hyperlegible"
      ),
      plot.background = element_rect(
        fill = licorice_gargle_colours["Background"],
        colour = licorice_gargle_colours["Background"]
      ),
      panel.grid = element_line(colour = "white"),
      #   axis.title.y = element_blank(),
      #   axis.text.y = element_blank(),
      #   axis.title.x = element_blank(),
      #   legend.title = element_blank(),
      #   panel.grid.major.x = element_blank(),
      #   panel.grid.major.y = element_blank(),
      axis.line.x = element_line(
        colour = licorice_gargle_colours["Light text"],
        linewidth = 0.2
      ),
      axis.text.x = element_text(size = rel(1.2), face = "bold"),
      plot.title = marquee::element_marquee(
        size = rel(1.5),
        colour = licorice_gargle_colours["Dark text"],
        width = 1,
        vjust = 0,
        margin = margin(30, 0, 0, 0),
        style = highlight_style,
        lineheight = 1.05
      ),
      plot.subtitle = ggtext::element_textbox_simple(
        colour = licorice_gargle_colours["Light text"],
        lineheight = 1.3,
        margin = margin(15, 0, 20, 0)
      ),
      legend.position = "none",
      plot.margin = margin_auto(30)
    )
}

Making the theme reusable

Let’s be kinder to our future selves

theme_licorice <- function(base_text_size = 20) {
  theme_minimal(base_size = base_text_size) +
    theme(
      text = element_text(family = "Atkinson Hyperlegible"),
      geom = element_geom(
        ink = licorice_gargle_colours["Light text"],
        borderwidth = 0.5,
        linewidth = 0.2,
        family = "Atkinson Hyperlegible"
      ),
      plot.background = element_rect(
        fill = licorice_gargle_colours["Background"],
        colour = licorice_gargle_colours["Background"]
      ),
      panel.grid = element_line(colour = "white"),
      #   axis.title.y = element_blank(),
      #   axis.text.y = element_blank(),
      #   axis.title.x = element_blank(),
      #   legend.title = element_blank(),
      #   panel.grid.major.x = element_blank(),
      #   panel.grid.major.y = element_blank(),
      axis.line.x = element_line(
        colour = licorice_gargle_colours["Light text"],
        linewidth = 0.2
      ),
      axis.text.x = element_text(size = rel(1.2), face = "bold"),
      plot.title = marquee::element_marquee(
        size = rel(1.5),
        colour = licorice_gargle_colours["Dark text"],
        width = 1,
        vjust = 0,
        margin = margin(base_text_size * 1.5, 0, 0, 0),
        style = highlight_style,
        lineheight = 1.05
      ),
      plot.subtitle = ggtext::element_textbox_simple(
        colour = licorice_gargle_colours["Light text"],
        lineheight = 1.3,
        margin = margin(base_text_size * 0.75, 0, base_text_size, 0)
      ),
      legend.position = "none",
      plot.margin = margin_auto(base_text_size * 1.5)
    )
}

Making the theme reusable

Give it a go!

penguins |>
  ggplot() +
  geom_bar(aes(x = species), stat = "count") +
  labs(
    title = "Do penguins like licorice?",
    subtitle = "We don't have data on that, but we can count how many penguins are in each species instead!"
  )

Making the theme reusable

Give it a go!

penguins |>
  ggplot() +
  geom_bar(aes(x = species), stat = "count") +
  labs(
    title = "Do penguins like licorice?",
    subtitle = "We don't have data on that, but we can count how many penguins are in each species instead!"
  ) +
  theme_licorice()

Making the theme reusable

Give it a go!

penguins |>
  ggplot() +
  geom_bar(aes(x = species), stat = "count") +
  labs(
    title = "Do penguins like licorice?",
    subtitle = "We don't have data on that, but we can count how many penguins are in each species instead!"
  ) +
  theme_licorice() +
  theme(
    axis.title.y = element_blank(),
    axis.title.x = element_blank(),
    panel.grid.major.x = element_blank()
  )

Making the theme resusable

How much do we bundle in?

theme_extra <- function(base_size = 12) {
  list(
    theme_minimal(base_size = base_size) +
      theme(...),
    scale_colour_manual(values = c("Sugar" = "pink", "Licorice" = "black")),
    scale_fill_manual(values = c("Sugar" = "pink", "Licorice" = "black"))
  )
}

Make it interactive

I ❤️ {ggiraph}

Make it interactive

“Here’s one I made earlier”

tidied_data <- medicaldata::licorice_gargle |>
  dplyr::mutate(
    gender = factor(
      dplyr::case_when(
        preOp_gender == 0 ~ "Male",
        preOp_gender == 1 ~ "Female"
      )
    ),
    id = dplyr::row_number(),
    treatment = factor(dplyr::case_when(
      treat == 0 ~ "Sugar",
      treat == 1 ~ "Licorice"
    ))
  ) |>
  dplyr::left_join(
    babynames::babynames |>
      dplyr::group_by(sex) |>
      dplyr::mutate(id = dplyr::row_number()) |>
      dplyr::select(sex, name, id) |>
      dplyr::mutate(
        sex = factor(dplyr::case_when(
          sex == "F" ~ "Female",
          sex == "M" ~ "Male"
        ))
      ),
    by = dplyr::join_by(gender == sex, id == id)
  ) |>
  dplyr::rowwise() |>
  dplyr::mutate(
    praise = paste(
      sample(
        praise::praise_parts$adverb_manner,
        1
      ),
      sample(praise::praise_parts$adjective, 1)
    )
  ) |>
  dplyr::ungroup()

Make it interactive

“Here’s one I made earlier”

tidied_data |>
  dplyr::select(name, preOp_age, praise) |>
  dplyr::sample_n(size = 10)
# A tibble: 10 × 3
   name    preOp_age praise                
   <chr>       <dbl> <chr>                 
 1 Irvin          73 bravely kickass       
 2 Hubert         45 sharply amazing       
 3 Eula           53 kindly doozie         
 4 Arthur         30 correctly stellar     
 5 Ollie          84 powerfully great      
 6 Nell           84 openly super-excellent
 7 Francis        64 deliberately fine     
 8 Sophia         68 swiftly luminous      
 9 Lee            57 calmly brilliant      
10 Myrtie         55 swiftly best          

Make it interactive

“Here’s one I made earlier”

tidied_data |>
  dplyr::select(name, preOp_age, praise) |>
  dplyr::sample_n(size = 10)
# A tibble: 10 × 3
   name    preOp_age praise                
   <chr>       <dbl> <chr>                 
 1 Irvin          73 bravely kickass       
 2 Hubert         45 sharply amazing       
 3 Eula           53 kindly doozie         
 4 Arthur         30 correctly stellar     
 5 Ollie          84 powerfully great      
 6 Nell           84 openly super-excellent
 7 Francis        64 deliberately fine     
 8 Sophia         68 swiftly luminous      
 9 Lee            57 calmly brilliant      
10 Myrtie         55 swiftly best          

Make it interactive

Let’s modify our graph…

ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  geom_rect(
    data = tidied_data |> tail(1),
    aes(xmin = I(0.1), xmax = I(0.9), ymin = 0, ymax = 3.5),
    fill = bg_to_red,
    alpha = 0.1
  ) +
  geom_point(
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21,
    size = 5
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  geom_text(
    data = tibble::tibble(
      y_coord = c(0, 1, 2, 3),
      severity = c("No cough", "Mild", "Moderate", "Severe cough")
    ),
    aes(
      x = I(0.5),
      y = y_coord,
      label = severity,
      fill = NULL
    )
  ) +
  labs(
    title = "Patients who gargled {.licorice Licorice} reported a slightly less severe cough than those who gargled {.sugar Sugar}",
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  theme_licorice() +
  theme(
    axis.title.y = element_blank(),
    axis.text.y = element_blank(),
    axis.title.x = element_blank(),
    legend.title = element_blank(),
    panel.grid.major.x = element_blank(),
    panel.grid.major.y = element_blank()
  )

Make it interactive

Let’s modify our graph…

interactive_cough_plot <- ggplot(
  tidied_data,
  aes(x = gender, y = pod1am_cough, fill = treatment)
) +
  geom_rect(
    data = tidied_data |> tail(1),
    aes(xmin = I(0.1), xmax = I(0.9), ymin = 0, ymax = 3.5),
    fill = bg_to_red,
    alpha = 0.1
  ) +
  ggiraph::geom_point_interactive(
    aes(
      tooltip = paste0("<b>", name, "</b>, ", preOp_age, ", is ", praise),
      data_id = name,
      group = treatment
    ),
    alpha = 0.8,
    position = position_jitterdodge(
      jitter.height = 0.05,
      jitter.width = 0.2,
      dodge.width = 0.25
    ),
    shape = 21,
    size = 5
  ) +
  stat_summary(
    aes(ymin = after_stat(y), ymax = after_stat(y), colour = treatment),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "crossbar",
    width = 0.5
  ) +
  stat_summary(
    aes(
      label = janitor::round_half_up(after_stat(y), 2),
      group = treatment
    ),
    fun = function(x) mean(x, na.rm = TRUE),
    geom = "text",
    position = position_nudge(y = 0.06),
    colour = "black",
    size = 16,
    size.unit = "pt"
  ) +
  geom_text(
    data = tibble::tibble(
      y_coord = c(0, 1, 2, 3),
      severity = c("No cough", "Mild", "Moderate", "Severe cough")
    ),
    aes(
      x = I(0.5),
      y = y_coord,
      label = severity,
      fill = NULL
    )
  ) +
  labs(
    title = paste0(
      "Patients who gargled <span style='color:",
      licorice_gargle_colours["Licorice"],
      "'>Licorice</span> reported a slightly less severe cough than those who gargled <span style='color:",
      licorice_gargle_colours["Sugar"],
      "'>Sugar</span>"
    ),
    subtitle = "The effect was similar across Female and Male patients, on the morning after surgery."
  ) +
  scale_fill_manual(values = licorice_gargle_colours) +
  scale_colour_manual(values = licorice_gargle_colours) +
  theme_licorice() +
  theme(
    plot.title = ggtext::element_textbox_simple(
      size = rel(1.5),
      face = "bold",
      margin = margin(30, 0, 15, 0),
      vjust = 0
    ),
    axis.title.y = element_blank(),
    axis.text.y = element_blank(),
    axis.title.x = element_blank(),
    legend.title = element_blank(),
    panel.grid.major.x = element_blank(),
    panel.grid.major.y = element_blank()
  )

ggiraph::girafe(
  ggobj = interactive_cough_plot,
  height_svg = 9.5,
  options = list(
    ggiraph::opts_hover(
      css = "stroke:#f8f28b;stroke-width:1.5px;stroke-opacity:1;fill:none;opacity:1;"
    ),
    ggiraph::opts_tooltip(
      css = "background-color:#383F4C;color:#F8F1F8;padding:7.5px;letter-spacing:0.025em;line-height:1.3;border-radius:5px;font-family:'Atkinson Hyperlegible';",
      opacity = 0.9,
      offy = 10
    ),
    ggiraph::opts_sizing(width = 1),
    ggiraph::opts_hover_inv(css = "opacity:0.5")
  )
)

Let’s start again!

Building a second graph, using the reusable bits!

  • What’s the effect on throat pain?
  • How does the effect change over time?
  • Are there notable differences between Male and Female?

Let’s start again

Fast track - you know how to do all this!

timeline_data <- tidied_data |>
  dplyr::select(
    gender,
    name,
    praise,
    treatment,
    preOp_age,
    pacu30min_throatPain,
    pacu90min_throatPain,
    postOp4hour_throatPain,
    pod1am_throatPain
  ) |>
  tidyr::pivot_longer(
    -c(gender, name, praise, treatment, preOp_age),
    names_to = "timeline",
    values_to = "pain"
  ) |>
  dplyr::mutate(
    timeline = factor(
      timeline,
      levels = c(
        "pacu30min_throatPain",
        "pacu90min_throatPain",
        "postOp4hour_throatPain",
        "pod1am_throatPain"
      ),
      labels = c("30mins", "90mins", "4hours", "Next morning"),
      ordered = TRUE
    )
  )

head(timeline_data, 10)
# A tibble: 10 × 7
   gender name    praise              treatment preOp_age timeline      pain
   <fct>  <chr>   <chr>               <fct>         <dbl> <ord>        <dbl>
 1 Male   John    honestly smashing   Licorice         67 30mins           0
 2 Male   John    honestly smashing   Licorice         67 90mins           0
 3 Male   John    honestly smashing   Licorice         67 4hours           0
 4 Male   John    honestly smashing   Licorice         67 Next morning     0
 5 Male   William speedily cat's meow Licorice         76 30mins           0
 6 Male   William speedily cat's meow Licorice         76 90mins           0
 7 Male   William speedily cat's meow Licorice         76 4hours           0
 8 Male   William speedily cat's meow Licorice         76 Next morning     0
 9 Male   James   brightly great      Licorice         58 30mins           0
10 Male   James   brightly great      Licorice         58 90mins           0

Let’s start again

Data for some annotation rectangles

mean_pain <- timeline_data |>
  dplyr::group_by(timeline, treatment, gender) |>
  dplyr::summarise(mean_pain = mean(pain, na.rm = TRUE)) |>
  dplyr::ungroup()

mean_pain
# A tibble: 16 × 4
   timeline     treatment gender mean_pain
   <ord>        <fct>     <fct>      <dbl>
 1 30mins       Licorice  Female     0.163
 2 30mins       Licorice  Male       0.353
 3 30mins       Sugar     Female     0.818
 4 30mins       Sugar     Male       1.15 
 5 90mins       Licorice  Female     0.102
 6 90mins       Licorice  Male       0.162
 7 90mins       Sugar     Female     0.75 
 8 90mins       Sugar     Male       0.861
 9 4hours       Licorice  Female     0.245
10 4hours       Licorice  Male       0.426
11 4hours       Sugar     Female     1.07 
12 4hours       Sugar     Male       0.819
13 Next morning Licorice  Female     0.286
14 Next morning Licorice  Male       0.338
15 Next morning Sugar     Female     0.682
16 Next morning Sugar     Male       0.625

Another graph

The main idea

ggplot(timeline_data, aes(x = timeline, y = pain)) +
  geom_rect(
    data = mean_pain,
    aes(
      xmin = as.numeric(timeline) - 0.5,
      xmax = as.numeric(timeline) + 0.5,
      ymin = -Inf,
      ymax = Inf,
      fill = "red",
      alpha = mean_pain
    ),
    inherit.aes = FALSE
  ) +
  scale_alpha(range = c(0.05, 0.25)) +
  # Move lines to back for easier interaction
  ggiraph::geom_line_interactive(
    aes(group = name, colour = treatment, data_id = name, tooltip = name),
    alpha = 0.3
  ) +
  ggiraph::geom_jitter_interactive(
    aes(
      x = timeline,
      y = pain,
      colour = treatment,
      data_id = name,
      tooltip = name
    ),
    height = 0.1,
    width = 0.1,
    alpha = 0.6
  ) +
  scale_colour_manual(values = c("Sugar" = "#c592c7", "Licorice" = "#39496a")) +
  facet_grid(gender ~ treatment) +
  theme_minimal() +
  theme(legend.position = "none")

Another graph

Styled!

ggplot(timeline_data, aes(x = timeline, y = pain)) +
  geom_rect(
    data = mean_pain,
    aes(
      xmin = as.numeric(timeline) - 0.5,
      xmax = as.numeric(timeline) + 0.5,
      ymin = -Inf,
      ymax = Inf,
      fill = "red",
      alpha = mean_pain
    ),
    inherit.aes = FALSE
  ) +
  scale_alpha(range = c(0.05, 0.25)) +
  # Move lines to back for easier interaction
  ggiraph::geom_line_interactive(
    aes(group = name, colour = treatment, data_id = name, tooltip = name),
    alpha = 0.3
  ) +
  ggiraph::geom_jitter_interactive(
    aes(
      x = timeline,
      y = pain,
      colour = treatment,
      data_id = name,
      tooltip = name
    ),
    height = 0.1,
    width = 0.1,
    alpha = 0.6
  ) +
  scale_colour_manual(values = c("Sugar" = "#c592c7", "Licorice" = "#39496a")) +
  facet_grid(gender ~ treatment) +
  theme_licorice() +
  theme(legend.position = "none")

Another graph

Tweaked!

ggplot(timeline_data, aes(x = timeline, y = pain)) +
  geom_rect(
    data = mean_pain,
    aes(
      xmin = as.numeric(timeline) - 0.5,
      xmax = as.numeric(timeline) + 0.5,
      ymin = -Inf,
      ymax = Inf,
      fill = "red",
      alpha = mean_pain
    ),
    inherit.aes = FALSE
  ) +
  scale_alpha(range = c(0.05, 0.25)) +
  # Move lines to back for easier interaction
  ggiraph::geom_line_interactive(
    aes(
      group = name,
      colour = treatment,
      data_id = name,
      tooltip = paste0("<b>", name, "</b>, ", preOp_age, ", is ", praise)
    ),
    alpha = 0.3
  ) +
  ggiraph::geom_jitter_interactive(
    aes(
      x = timeline,
      y = pain,
      colour = treatment,
      data_id = name,
      tooltip = name
    ),
    height = 0.1,
    width = 0.1,
    alpha = 0.6
  ) +
  scale_colour_manual(values = c("Sugar" = "#c592c7", "Licorice" = "#39496a")) +
  scale_x_discrete(labels = function(x) {
    # Thank you, Claude!
    stringr::str_wrap(gsub("([0-9])([a-zA-Z])", "\\1 \\2", x), 5)
  }) +
  facet_grid(treatment ~ gender) +
  labs(
    title = "The Licorice effect on pain reduction was most visible within the first 4 hours after surgery",
    subtitle = "Pain was rated 0 = no pain to 10 = worst pain, but the maximum pain reported was 6."
  ) +
  theme_licorice() +
  theme(
    legend.position = "none",
    axis.title.y = element_blank(),
    axis.title.x = element_blank(),
    strip.text.y = element_text(angle = 0)
  )

Facets with highlights

The penultimate trick!

Facets with highlights

  • 📦 {gghighlight}
  • 💔 {ggiraph}
  • default_geom()
update_geom_defaults(
  geom = "point",
  aes(
    colour = "white",
    fill = licorice_gargle_colours["Light text"],
    shape = 21,
    size = 4,
    alpha = 0.5
  )
)

Facets with highlights

Add the default geom in every facet

ggplot(timeline_data, aes(x = timeline, y = pain)) +
  geom_rect(
    data = mean_pain,
    aes(
      xmin = as.numeric(timeline) - 0.5,
      xmax = as.numeric(timeline) + 0.5,
      ymin = -Inf,
      ymax = Inf,
      fill = "red",
      alpha = mean_pain
    ),
    inherit.aes = FALSE
  ) +
  scale_alpha(range = c(0.05, 0.25)) +
  # Slight fudge!
  geom_point(
    layout = "fixed",
    alpha = 0.1
  ) +
  # Move lines to back for easier interaction
  ggiraph::geom_line_interactive(
    aes(
      group = name,
      colour = treatment,
      data_id = name,
      tooltip = paste0("<b>", name, "</b>, ", preOp_age, ", is ", praise)
    ),
    alpha = 0.3
  ) +
  ggiraph::geom_jitter_interactive(
    aes(
      x = timeline,
      y = pain,
      colour = treatment,
      data_id = name,
      tooltip = name
    ),
    height = 0.1,
    width = 0.1,
    alpha = 0.6
  ) +
  scale_colour_manual(values = c("Sugar" = "#c592c7", "Licorice" = "#39496a")) +
  scale_x_discrete(labels = function(x) {
    # Thank you, Claude!
    stringr::str_wrap(gsub("([0-9])([a-zA-Z])", "\\1 \\2", x), 5)
  }) +
  facet_grid(treatment ~ gender) +
  labs(
    title = "The Licorice effect on pain reduction was most visible within the first 4 hours after surgery",
    subtitle = "Pain was rated 0 = no pain to 10 = worst pain, but the maximum pain reported was 6."
  ) +
  theme_licorice() +
  theme(
    legend.position = "none",
    axis.title.y = element_blank(),
    axis.title.x = element_blank(),
    strip.text.y = element_text(angle = 0)
  )

Facets with highlights

But it works with interactivity!

timeline_graph <- ggplot(timeline_data, aes(x = timeline, y = pain)) +
  geom_rect(
    data = mean_pain,
    aes(
      xmin = as.numeric(timeline) - 0.5,
      xmax = as.numeric(timeline) + 0.5,
      ymin = -Inf,
      ymax = Inf,
      fill = "red",
      alpha = mean_pain
    ),
    inherit.aes = FALSE
  ) +
  scale_alpha(range = c(0.05, 0.25)) +
  # Slight fudge!
  geom_point(
    layout = "fixed",
    alpha = 0.1
  ) +
  # Move lines to back for easier interaction
  ggiraph::geom_line_interactive(
    aes(
      group = name,
      colour = treatment,
      data_id = name,
      tooltip = paste0("<b>", name, "</b>, ", preOp_age, ", is ", praise)
    ),
    alpha = 0.3
  ) +
  ggiraph::geom_jitter_interactive(
    aes(
      x = timeline,
      y = pain,
      colour = treatment,
      data_id = name,
      tooltip = paste0("<b>", name, "</b>, ", preOp_age, ", is ", praise)
    ),
    height = 0.1,
    width = 0.1,
    alpha = 0.6
  ) +
  scale_colour_manual(values = c("Sugar" = "#c592c7", "Licorice" = "#39496a")) +
  scale_x_discrete(labels = function(x) {
    # Thank you, Claude!
    stringr::str_wrap(gsub("([0-9])([a-zA-Z])", "\\1 \\2", x), 5)
  }) +
  facet_grid(treatment ~ gender) +
  labs(
    title = "The Licorice effect on pain reduction was most visible within the first 4 hours after surgery",
    subtitle = "Pain was rated 0 = no pain to 10 = worst pain, but the maximum pain reported was 6."
  ) +
  theme_licorice() +
  theme(
    legend.position = "none",
    axis.title.y = element_blank(),
    axis.title.x = element_blank(),
    strip.text.y = element_text(angle = 0)
  )

ggiraph::girafe(
  ggobj = timeline_graph,
  height_svg = 9.5,
  options = list(
    ggiraph::opts_hover(
      css = "stroke:#f8f28b;stroke-width:1.5px;stroke-opacity:1;opacity:1;fill:#f8f28b;"
    ),
    ggiraph::opts_tooltip(
      css = "background-color:#383F4C;color:#F8F1F8;padding:7.5px;letter-spacing:0.025em;line-height:1.3;border-radius:5px;font-family:'Atkinson Hyperlegible';",
      opacity = 0.9,
      offy = 10
    ),
    ggiraph::opts_sizing(width = 1),
    ggiraph::opts_hover_inv(css = "opacity:0.5")
  )
)

Tie the two graphs together

Last trick for today!

Tie the two graphs together

library(patchwork)

timeline_graph <- timeline_graph +
  theme(
    plot.title = ggtext::element_textbox_simple(
      size = rel(1.5),
      face = "bold",
      margin = margin(30, 0, 15, 0),
      vjust = 0
    )
  )

ggiraph::girafe(
  ggobj = interactive_cough_plot +
    timeline_graph +
    plot_layout(heights = c(1, 1), widths = c(1, 1), nrow = 1),
  height_svg = 15,
  width_svg = 25,
  options = list(
    ggiraph::opts_hover(
      css = "stroke:#f8f28b;stroke-width:5px;stroke-opacity:1;opacity:1;fill:#f8f28b;"
    ),
    ggiraph::opts_tooltip(
      css = "background-color:#383F4C;color:#F8F1F8;padding:7.5px;letter-spacing:0.025em;line-height:1.3;border-radius:5px;font-family:'Atkinson Hyperlegible';",
      opacity = 0.9,
      offy = 10
    ),
    ggiraph::opts_sizing(width = 1),
    ggiraph::opts_hover_inv(css = "opacity:0.5")
  )
)

Tie the two graphs together

Over to you!