Make it your own - Tips and tricks for stand-out dataviz with R and ggplot2

data-visualisation
r-how-to
parameterised-plots
dataviz-design-system

Lunchtime workshop for the Manchester Uni useR group, looking at making the most of ggplot2 (including a fun new feature!) for clear, engaging data visualisations which always look like your own.

Published

February 19, 2026

Recording

Coming soon!

Slides

View full screen

Final code

Code
library(ggplot2)

# A tiny bit of tidying up
penguin_df <- palmerpenguins::penguins_raw |>
  janitor::clean_names() |>
  dplyr::filter(!is.na(culmen_length_mm))

# Setting up our colours
penguin_colours <- c(
  "Adelie Penguin (Pygoscelis adeliae)" = "#E18C1C",
  "Chinstrap penguin (Pygoscelis antarctica)" = "#E8A9C2",
  "Gentoo penguin (Pygoscelis papua)" = "#2A483E"
)

# Our theme
theme_chester_penguins <- function(base_text_size = 20) {
  theme_minimal(base_size = base_text_size) +
    theme(
      text = element_text(family = "Open Sans", colour = "#534959"),
      axis.text = element_text(colour = "#534959"),
      legend.position = "none",
      axis.title = element_blank(),
      plot.title.position = "plot",
      plot.title = ggtext::element_textbox_simple(
        family = "Domine",
        face = "bold",
        size = rel(1.5),
        colour = "#15081D",
        margin = margin(0, 0, base_text_size, 0)
      ),
      panel.grid = element_line(colour = "#FFFFFF"),
      plot.background = element_rect(fill = "#f9f5fc", colour = "#f9f5fc"),
      plot.margin = margin_auto(base_text_size * 1.5),
      geom = element_geom(ink = "#6b2c91")
    )
}

# Our parameterised interactive plotting function
make_beak_off_plot <- function(df = penguin_df, palette = penguin_colours) {
  beak_means_df <- df |>
    dplyr::group_by(species) |>
    dplyr::summarise(mean_length = mean(culmen_length_mm, na.rm = TRUE))

  beak_range_df <- df |>
    dplyr::filter(
      culmen_length_mm == max(culmen_length_mm, na.rm = TRUE) |
        culmen_length_mm == min(culmen_length_mm, na.rm = TRUE)
    )

  interactive_plot <- df |>
    ggplot(aes(x = culmen_length_mm, y = species)) +
    geom_vline(
      data = beak_range_df,
      aes(xintercept = culmen_length_mm),
      linetype = 3,
      colour = "#333333"
    ) +
    geom_segment(
      data = beak_means_df,
      aes(x = mean_length, xend = mean_length, y = -Inf, yend = species),
      linetype = 3
    ) +
    ggiraph::geom_jitter_interactive(
      aes(
        x = culmen_length_mm,
        y = species,
        fill = species,
        size = body_mass_g,
        tooltip = paste0("<b>", individual_id, "</b> from ", island)
      ),
      shape = 21,
      width = 0,
      height = 0.15,
      colour = "#333333",
      stroke = 0.5
    ) +
    ggtext::geom_textbox(
      data = beak_range_df,
      aes(
        y = max(df$species),
        label = dplyr::case_when(
          culmen_length_mm == min(culmen_length_mm) ~
            paste0("🞀 ", culmen_length_mm, "mm"),
          TRUE ~ paste0(culmen_length_mm, "mm", " 🞂")
        ),
        hjust = dplyr::case_when(
          culmen_length_mm == min(culmen_length_mm) ~ 0,
          TRUE ~ 1
        ),
        halign = dplyr::case_when(
          culmen_length_mm == min(culmen_length_mm) ~ 0,
          TRUE ~ 1
        )
      ),
      family = "Open Sans",
      colour = "#333333",
      fontface = "bold",
      fill = NA,
      box.padding = unit(0, "pt"),
      size = 5,
      box.colour = NA,
      nudge_y = 0.33
    ) +
    ggtext::geom_textbox(
      data = beak_means_df,
      aes(
        x = mean_length,
        y = species,
        label = paste0(
          species,
          " mean<br>**",
          janitor::round_half_up(mean_length),
          "mm**"
        )
      ),
      hjust = 0,
      nudge_y = -0.4,
      box.colour = NA,
      family = "Open Sans",
      colour = "#333333",
      fill = "#f9f5fc"
    ) +
    labs(title = "Beak lengths by species") +
    scale_fill_manual(values = palette) +
    scale_x_continuous(
      label = function(x) paste0(x, "mm"),
      limits = range(
        palmerpenguins::penguins_raw$`Culmen Length (mm)`,
        na.rm = TRUE
      )
    ) +
    scale_y_discrete(labels = function(x) gsub("(.)( )(.*)", "\\1", x)) +
    theme_chester_penguins() +
    theme(axis.text.y = element_blank())

  ggiraph::girafe(
    ggobj = interactive_plot,
    options = list(ggiraph::opts_tooltip(
      css = "background-color:#333333;color:#f9f5fc;padding:7.5px;letter-spacing:0.025em;line-height:1.3;border-radius:5px;font-family:Open Sans;"
    ))
  )
}

make_beak_off_plot()

Reuse

Citation

For attribution, please cite this work as:
“Make It Your Own - Tips and Tricks for Stand-Out Dataviz with R and Ggplot2.” 2026. February 19, 2026. https://www.cararthompson.com/talks/manchester_useR/.