Reusing ‘ggplot2’ code: how to design better plot helper functions

useR! 2026, Warsaw, Poland, 6-9 Jul 2026

Cynthia A. Huang

About Me!

  • 👩 Post-Doctoral Researcher with Prof. Frauke Kreuter, Social Data and AI Lab, LMU Munich
  • 🎨 Organiser of ‘Grand Unified Grammar of Graphics’ workshop at IEEE VIS 2026 🔗 gugog-vis.github.io/2026
  • 📊 Research Interests
    • 🌰 “alternative” data for empirical social science research
    • 🤖 Leveraging LLMs and genAI in data science
    • 🛠️ Grammar of Graphics visualisation systems

Ways to reuse ggplot code

  • Copy & paste
  • Component objects or lists
  • Complete plot helpers
  • Decorator and combination functions
  • Extending ggproto objects

Copy & Paste

ggplot(
  data = penguins, 
  mapping = aes(
    x = bill_length_mm,
    y = bill_depth_mm,
    color = species)) +
  geom_point()

## -- SPOT THE DIFFERENCE --

ggplot(
  data = penguins, 
  mapping = aes(
    x = bill_length_mm,
    y = flipper_length_mm,
    color = species)) +
  geom_point()

Component Objects

💡 Useful if components are exactly the same each time!

bestfit <- list(
  geom_smooth(
    method = "lm",
    se = FALSE,
    colour = alpha("steelblue", 0.5),
    linewidth = 2
  ))
ggplot(mpg, aes(cty, hwy)) +
  geom_point() +
  bestfit

Decorators and combinations

  • Combine multiple components in a list
  • Specify new parameter options
geom_mean <- function(mean.fill = "grey70") {
  list(
    stat_summary(fun = "mean", geom = "bar", fill = mean.fill),
    stat_summary(fun.data = "mean_cl_normal", geom = "errorbar", width = 0.4)
  )
}
ggplot(mpg, aes(class, cty)) + geom_mean()

New components via ggproto

compute_group_coordinates  <- function(data, scales){
  data |>
    mutate(label = paste0("(", x ", ", y, ")" ))
}
StatCoordinates = 
ggproto(`_class` = "StatCoordinates",
                    `_inherit` = Stat,
                    required_aes = c("x", "y"),
                    compute_group = compute_group_coordinates)
...

Example from: Easy geom_*() recipes, Gina Reynolds

Complete Plot Helpers

  • may take as input:
    • tidy or not-tidy data inputs, and/or
    • multiple parameters arguments
  • usually, transform or create data for plotting,
  • return a complete ggplot2 plot (not just components) as output

Calendars: two data inputs

library(ggcal)

mydate <-
  seq(
    as.Date("2017-02-01"),
    as.Date("2017-07-22"),
    by="1 day")
myfills <-
  rnorm(length(mydate))

print(
  ggcal(mydate, myfills)
  )

  • how many arguments does ggcal() have?
  • what format should dates, fills be?

Calendars: plot specific args

library(ggweekly)
ggweek_planner(
  start_day = "2019-04-01", 
  end_day = "2019-06-30", 
)

What about customisation? e.g. text, colors, font, extra data

ggweekly::ggweek_planner(
    start_day = lubridate::today(),
    end_day = start_day +
        lubridate::weeks(8) - lubridate::days(1),
    highlight_days = NULL,
    week_start = c("isoweek", "epiweek"),
    week_start_label = c("month day", "week", "none"),
    show_day_numbers = TRUE,
    show_month_start_day = TRUE,
    show_month_boundaries = TRUE,
    highlight_text_size = 2,
    month_text_size = 4,
    day_number_text_size = 2,
    month_color = "#f78154",
    day_number_color = "grey80",
    weekend_fill = "#f8f8f8",
    holidays = ggweekly::us_federal_holidays,
    font_base_family = "PT Sans",
    font_label_family = "PT Sans Narrow",
    font_label_text = NULL
)

Calendars: ggplot sub-arguments

calendR(
    year = format(Sys.Date(), "%Y"),
    month = NULL,
    from = NULL,
    to = NULL,
    start = c("S", "M"),
    orientation = c("portrait", "landscape"),
    title,
    title.size = 20,
    title.col = "gray30",
    subtitle = "",
    subtitle.size = 10,
    subtitle.col = "gray30",
    text = NULL,
    text.pos = NULL,
    text.size = 4,
    text.col = "gray30",
    special.days = NULL,
    special.col = "gray90",
    gradient = FALSE,
    low.col = "white",
    col = "gray30",
    lwd = 0.5,
    lty = 1,
    font.family = "sans",
    font.style = "plain",
    day.size = 3,
    days.col = "gray30",
    weeknames,
    weeknames.col = "gray30",
    weeknames.size = 4.5,
    week.number = FALSE,
    week.number.col = "gray30",
    week.number.size = 8,
    monthnames,
    months.size = 10,
    months.col = "gray30",
    months.pos = 0.5,
    mbg.col = "white",
    legend.pos = "none",
    legend.title = "",
    bg.col = "white",
    bg.img = "",
    margin = 1,
    ncol,
    lunar = FALSE,
    lunar.col = "gray60",
    lunar.size = 7,
    pdf = FALSE,
    doc_name = "",
    papersize = "A4"
)
  • Is this using ggplot2?
  • What are the inputs?
  • Internal data preparation?
  • …?

Beware false savings!

Sometimes

“saved” lines of code

turn into

confusing lists of plot specific arguments

Better complete plot helpers

Design goals

  • Quick ‘autoplotting’
  • Minimise plot-specific arguments
  • Retain ggplot syntax & error messages
  • Integrate well with other ggplot2 components

Helpers with Windows

  1. Expose ggplot syntax & recipe
  2. Separate data preparation from plotting
  3. Document grammatical customisation options

Case study: Another Calendar!

very-long-ggplot-script.R
# source(here::here("_utilities/travel_dates_load.R"))
library(ggplot2)
library(ggiraph)

## ---- daily-loc-prep ----

## expand dates: https://stackoverflow.com/a/54728153

travel_days <- travel_dates |>
  mutate(nights = interval(startDate, endDate) / days(1)) |>
  arrange(startDate, desc(nights)) |>
  rowid_to_column() |>
  rowwise() |>
  reframe(rowid, location, details, nights,
    date = seq(startDate, endDate, by = "day")
  ) |>
  group_by(date) |>
  slice_min(order_by = nights, n = 1) |>
  ungroup() |>
  mutate(country = countrycode::countryname(location, "iso2c"))

away_days <- travel_days |>
  summarise(
    startDate = min(date),
    endDate = max(date)
  ) |>
  mutate(rowid = 0, location = "TBC") |>
  reframe(rowid, location, date = seq(startDate, endDate, by = "day"))

daily_loc <- away_days |>
  anti_join(travel_days, by = "date") |>
  bind_rows(travel_days) |>
  mutate(
    flag = countrycode::countrycode(country, "iso2c", "unicode.symbol"),
    continent = countrycode::countrycode(country, "iso2c", "continent")
  ) |>
  mutate(flag = case_when(
    location == "en route" ~ "\u2708",
    location == "TBC" ~ "❔",
    TRUE ~ flag
  ))

## ---- prep-full-calendar-data ----

# based on: https://github.com/nrennie/tidytuesday/blob/9dbe69d696f6c1edad41a72d157a42f3b5a63a81/2023/2023-03-07/20230307.R

# calculate extra months to make plot rectangular
cal_ncol <- 3
startMonth <- month(floor_date(min(travel_days$date), "month"))
endMonth <- month(ceiling_date(max(travel_days$date), "month") - 1)
n_extra_month <- cal_ncol - ((endMonth - startMonth + 1) %% cal_ncol)
padding_days <- summarise(
  travel_days,
  startDate = floor_date(min(date), "month"),
  endDate = ceiling_date(max(date) %m+% months(n_extra_month), "month") - 1
) |>
  reframe(date = seq(startDate, endDate, by = "day"))

# prepare calendar data
full_calendar <- padding_days |>
  anti_join(daily_loc, by = "date") |>
  bind_rows(daily_loc) |>
  mutate(
    Year = year(date),
    Month = month(date, label = TRUE),
    Day = wday(date, label = TRUE, week_start = 1),
    mday = mday(date),
    Month_week = (5 + day(date) +
      wday(floor_date(date, "month"), week_start = 1)) %/% 7
  )

## ---- travel-dates-calendar-display ----

base_data_aes <- full_calendar |>
  ggplot(aes(x = Day, y = Month_week))

layers_geom_text <- list(
  geom_text(aes(label = mday), nudge_y = 0.25),
  geom_text(aes(label = flag), nudge_y = -0.25)
)
layers_scale_coord <- list(
  scale_y_reverse(),
  coord_fixed(expand = TRUE)
)
layers_facet_labs <- list(
  facet_wrap(vars(Month),
    ncol = cal_ncol
  ),
  labs(y = NULL, x = NULL)
)
layers_theme <- list(
  theme_bw(),
  theme(
    plot.margin = margin(0, 0, 0, 0),
    panel.grid.major = element_blank(),
    panel.grid.minor = element_blank(),
    # panel.spacing = unit(0, "lines"),
    panel.border = element_blank(),
    axis.ticks = element_blank(),
    axis.text.y = element_blank(),
    strip.background = element_rect(fill = "grey95"),
    strip.text = element_text(hjust = 0)
  )
)

## ---- tooltip-travel-calendar ----

p_cal_girafe <- base_data_aes +
  geom_tile_interactive(
    aes(
      tooltip = paste(details, "@", location),
      data_id = location
    ),
    alpha = 0.2,
    fill = "transparent",
    colour = "grey80"
  ) +
  layers_geom_text +
  layers_scale_coord +
  coord_fixed(expand = FALSE) +
  layers_facet_labs +
  layers_theme

# girafe(ggobj = p_cal_girafe)
  • data wrangling:
    • reshaping via reframe()
    • creating new variables
    • fill in missing days
    • calculating facet and layout (e.g. Month, Month_week)
  • ggplot2 components:
    • scale, coord and facet to layout the calendar
    • theme modification to style the plot as a calendar
    • interactive geom from {ggiraph}

Convenient autoplot

library(ggtilecal)
dates <- 
ggtilecal::make_empty_month_days(
  c("2024-01-05", "2024-06-30"))
head(dates, 2)
# A tibble: 2 × 1
  unit_date 
  <date>    
1 2024-01-01
2 2024-01-02
ggtilecal::gg_facet_wrap_months(
  .events_long = dates, 
  date_col = unit_date)

List arguments as ‘window’ interface

function-interface
gg_facet_wrap_months(
  ## --- data ----
  .events_long, 
  date_col,
  locale = NULL,
  ## --- facets / layout ----
  week_start = NULL, 
  nrow = NULL, 
  ncol = NULL,
  ## --- default components ---
  .geom = list(
    geom_tile(color = "grey70", fill = "transparent"),
    geom_text(nudge_y = 0.25)
  ),
  .scale_coord = list(
    scale_y_reverse(),
    scale_x_discrete(position = "top"),
    coord_fixed(expand = TRUE)),
  .theme = list(theme_bw_tilecal()),
  .other = list()
)

Modular internals

internals
gg_facet_wrap_months <- function(...){
  ## Data Helpers
  cal_data <- .events_long |>
    fill_missing_units({{ date_col }}) |>
    calc_calendar_vars({{ date_col }})
  ## Plot Construction
  cal_data |>
    ggplot2::ggplot(mapping = aes_string(
      x = "TC_wday_label",
      y = "TC_month_week",
      label = "TC_mday" )) +
    facet_wrap(c("TC_month_label"), axes = "all_x", nrow = nrow, ncol = ncol) +
    labs(y = NULL, x = NULL) +
    .geom +
    .scale_coord +
    .theme +
    .others
}

Grammatical customisation

Reuses existing ggplot2 syntax knowledge for customisations instead of plot-specific arguments!

gg_facet_wrap_months(dates, unit_date,
    .geom = list(
      geom_tile(color = "grey70", fill = "transparent"),
      geom_text(nudge_y = 0.25,
                color = "#6a329f")),
    .theme = list(theme_bw_tilecal(),
      theme(
        strip.background = element_rect(fill = "#d9d2e9")))
    )

Grammatical customisation

Documentation support

Function docs explains:

  1. Data preparation steps
  2. How the ggplot2 recipe works
  3. How to modify the plot

Window plot helpers

  1. Expose ggplot syntax & recipe
  2. Separate functionality
  3. Document grammatical customisation

🔗 Read more at cynthiahqy.com

Share your ggplot2 helper habits!

  • 5-10 minute survey
  • Share what strategies you use for re-using ggplot2 code
  • Contribute to research on teaching and developing ggplot2 extensions
  • Optional EOI for follow-up interview