Intro Thoughts

Status Quo

library(tidyverse)

library(ggplot2)
library(ggtilecal)
make_empty_month_days(c("2024-01-05", "2024-06-30")) |>
  gg_facet_wrap_months(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")))
                       )
## Warning: `aes_string()` was deprecated in ggplot2 3.0.0.
## ℹ Please use tidy evaluation idioms with `aes()`.
## ℹ See also `vignette("ggplot2-in-packages")` for more information.
## ℹ The deprecated feature was likely used in the ggtilecal package.
##   Please report the issue at <https://github.com/cynthiahqy/ggtilecal>.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

make_empty_month_days(c("2024-01-05", "2024-06-30")) |>
  gg_facet_wrap_months(unit_date)

args(gg_facet_wrap_months)
## function (.events_long, date_col, locale = NULL, week_start = NULL, 
##     nrow = NULL, ncol = NULL, .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()) 
## NULL
body(gg_facet_wrap_months)
## {
##     cal_data <- calc_calendar_vars(fill_missing_units(.events_long, 
##         {
##             {
##                 date_col
##             }
##         }), {
##         {
##             date_col
##         }
##     })
##     base_plot <- ggplot2::ggplot(cal_data, 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 + .other
##     base_plot
## }
gg_facet_wrap_months <- function(...){
  ## Data Helpers
  cal_data <- .events_long |>
    fill_missing_units({{ date_col }}) |>
    calc_calendar_vars({{ date_col }})

  ## Plot Construction
  base_plot <- 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

  base_plot
}

Plot Helper Interface

gg_facet_wrap_months(
  .events_long,
  date_col,
  locale = NULL,
  week_start = NULL,
  nrow = NULL,
  ncol = NULL,
  .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()
)
## Error in (function (arg) : object 'date_col' not found

The component list arguments are initialised with sensible defaults, and inherit aesthetic mappings from the “fixed” internal components. The “fixed” components are the “improper” part of the solution, as they could probably be replaced by new ggplot2 Stat and Facet objects.

Internals: Data then Plot

List arguments bind together user inputs that all go into the same place inside the helper function (e.g. elements in .geom are added before .scale_coord, which comes before .theme and the final the catch-all .other). This design allow users to leverage the consistent syntax ggplot2 when specifying customisations. The user specified ggplot2 components are passed directly into the ggplot building portion of the helper function, avoiding the need for ad-hoc and plot-specific customisation arguments.

Plot Helper Body

gg_facet_wrap_months <- function(...){
  ## Data Helpers
  cal_data <- .events_long |>
    fill_missing_units({{ date_col }}) |>
    calc_calendar_vars({{ date_col }})

  ## Plot Construction
  base_plot <- 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

  base_plot
}

The example below shows an example customisation using the component arguments:

library(ggplot2)
library(ggtilecal)
make_empty_month_days(c("2024-01-05", "2024-06-30")) |>
  gg_facet_wrap_months(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")))
                       )
## Error in (function (arg) : object 'date_col' not found

This design also supports using geoms from other packages without depending on those packages directly, (i.e. ggiraph is not a dependency of ggtilecal). Additional layers can be included in the .geom, or added as normal using +:

# remotes::install_github("cynthiahqy/ggtilecal")
library(ggiraph)
library(ggplot2)
library(ggtilecal)

gi <- demo_events_gpt |>
    reframe_events(startDate, endDate) |>
  gg_facet_wrap_months(unit_date) +
  geom_text(aes(label = event_emoji), nudge_y = -0.25, na.rm = TRUE) +
  geom_tile_interactive(
        aes(
            tooltip = paste(event_title),
            data_id = event_id
        ),
        alpha = 0.2,
        fill = "transparent",
        colour = "grey80"
    )
## Error in (function (arg) : object 'date_col' not found
girafe(ggobj = gi)
## Error: object 'gi' not found

Documentation: Exposing Internal Workings

In addition to exposing the ggplot2 components as list arguments, I also document the data preparation steps that the plot helper function is performing:

#' Make Monthly Calendar Facets
#'
#' Generates calendar with monthly facets by:
#' - Padding event list with any missing days via `fill_missing_units()`
#' - Calculating variables for calendar layout via `calc_calendar_vars()`
#' - Returning a ggplot object as per Details.

and how the ggplot2 recipe works:

#' Returns a ggplot with the following fixed components using calculated layout variables:
#' - `aes()` mapping:
#'    - `x` is day of week,
#'    - `y` is week in month,
#'    - `label` is day of month
#' - `facet_wrap()` by month
#' - `labs()` to remove axis labels for calculated layout variables
#'
#' and default customisable components:
#' - `geom_tile()`, `geom_text()` to label each day which inherit calculated variables
#' - `scale_y_reverse()` to order day in month correctly
#' - `scale_x_discrete()` to position weekday labels
#' - `coord_fixed()` to square each tile
#' - `theme_bw_tilecal()` to apply sensible theme defaults
#'

and how to modify the plot:

#' To modify components alter the `.geom` and `.scale_coord`,
#' which inherit the calculate layout mapping by default
#' (via the ggplot2 `inherit.aes` argument).
#'
#' To add components use the ggplot `+` function as normal,
#' or pass components to the `.other` argument.
#' This can be used to add interactive geoms (e.g. from `ggiraph`)
#'
#' To modify the theme, use the ggplot `+` function as normal,
#' or add additional elements to the list in `.theme`.
#'
#' To remove any of the optional components,
#' set the argument to any empty `list()`

Design Principles for ggplot2 Plot Helpers

To summarise my opinionated take on how you should write plot helper functions: