Designing ggtime:

A grammar of temporal graphics

12th February 2025 @ ggextenders club

Mitchell O’Hara-Wild, Monash University Cynthia Huang, Monash University

Discussion presentation

This talk is more about designing ggplot2 extensions than temporal visualisation.

The {ggtime} package has a maturing design, and is in early software development.

Today’s presentation

I’ll share some {ggtime} design elements, and prompt discussion questions for some design challenges.

👋 Later discussion

There won’t be enough time to discuss everything today.

I’ll share discussion links in the slides, e.g. discussions#79

Please join the conversation afterwards ❤️

github://ggplot2-extenders/ggplot-extension-club/discussions

Scope of ggplot2 extensions

Tangential to discussions#77

A clear scope for ggplot extensions helps:

  • 🎯 define project goals
  • 📊 highlight usage / applications
  • 🕷️ prevent feature creep
  • 🛠️ ease code maintenance
  • 🧁 promote good design

The scope of ggtime

It’s all about time (and only about time)!

Visualising special dimensions

Several types of data require unique care.

  • 🍀 uncertainty ({distributional} / {ggdist})
  • 🕸️ graph ({tidygraph} / {ggraph})
  • 🗺️ space ({sf} / {ggplot2})
  • time ({mixtime} / {ggtime})
  • 📖 language (NA)

What makes these dimensions “special”?

An open question! Some ideas:

  • They incorporate human abstractions (often cultural).
  • Each have attributes that don’t match the data’s length.

Visualising temporal data

Time is comparatively simple dimension, it is:

  • continuous
  • ordered
  • univariate / singular

ggplot2 already supports temporal data

Simply map a date (Date), datetime (POSIXct), or even time (hms) to any aesthetic. Scales to adjust temporal labels and breaks exist in ggplot2:

  • scale_*_date() for Date,
  • scale_*_datetime() for POSIXct,
  • scale_*_time() for hms.

So why is ggtime needed?

Temporal data/patterns align with calendars.

📆 Calendrical time

Calendars complicate temporal visualisation!

While time itself is simple, calendrical patterns are complex!

  • Calender systems (Gregorian, Islamic, Chinese, Jewish, …)
  • Timezones (local/civil time, absolute time)
  • Granularities (hourly, daily, weekly, monthly, annual, …)
  • Corrections (leap years, leap seconds)
  • Seasonality (weekdays/weekends, financial years, holidays)

ggtime is calendrical

{ggtime} aligns physical time with calendrical structure (e.g. timezones, granularity, cycles, …).

Common temporal graphics

These ‘visual idioms’ of time series plots can be categorised into several groups:

  • Linear time plots
  • Circular time plots
  • and more… (no time to discuss these)

A grammar of temporal graphics

{ggtime} aims to re-express these time series plots with common elements of a grammar.

Those elements can be further remixed to create other useful temporal visualisations.

Linear time plots

These show time as a continuous dimension.

Time plots

Maps time to the x/y-axis.

They reveal trends, seasons, cycles, outliers, and more.

⚛️ Notable grammar elements

  • mapping:
    • x: time
    • y: observations
  • coord: cartesian
Code
library(fpp3)
pbs_scripts <- tsibbledata::PBS |>
  summarise(Scripts = sum(Scripts))
pbs_scripts |>
  autoplot(Scripts)

Forecast plots

These are simply time plots.

They just happen to show forecasts, or uncertain values.

⚛️ Notable grammar elements

  • mapping:
    • x: time
    • y: observations
    • ydist: forecast (❤️ ggdist)
  • coord: cartesian
Code
pbs_scripts |> 
  model(ETS(Scripts)) |> 
  forecast() |> 
  autoplot(pbs_scripts) +
  theme(legend.position = "bottom")

Multiple time plots

Most data is long and across many series.

⚛️ Notable grammar elements

  • mapping:
    • x: time
    • y: observations
    • colour: series
  • coord: cartesian
Code
sugrrants::hourly_peds |> 
  filter(Date < as.Date("2016-05-01")) |> 
  ggplot(aes(x = Date_Time, y = Hourly_Counts, colour = Sensor_Name)) + 
  geom_line() +
  theme_bw() +
  theme(legend.position = "bottom")

Calendar plots

Uses calendar layouts (multiple rows) to partially resolve long series.

⚛️ Notable grammar elements

  • mapping:
    • x: time
    • y: observations
    • colour: series
  • coord: cartesian
  • facet: calendar
Code
library(sugrrants)
hourly_peds %>%
  filter(Date < as.Date("2016-05-01")) %>% 
  ggplot(aes(x = Time, y = Hourly_Counts, colour = Sensor_Name)) +
  geom_line() +
  facet_calendar(~ Date) + # a variable contains dates
  theme_bw() +
  theme(legend.position = "bottom")

Circular time plots

Transforms time to reveal circular patterns.

Seasonal plots

Wraps seasons to align years.

⚛️ Notable grammar elements

  • mapping:
    • x: season (e.g. hms)
    • y: observations
    • colour: time
  • coord: cartesian
Code
pbs_scripts |>
  gg_season(Scripts)

Seasonal plots

They are often shown in polar.

⚛️ Notable grammar elements

  • mapping:
    • x: season
    • y: observations
    • colour: time
  • coord: polar
Code
pbs_scripts |>
  gg_season(Scripts, polar = TRUE)

Seasonal sub-series

facets seasons to show changes over time.

⚛️ Notable grammar elements

  • mapping:
    • x: time
    • y: observations
  • coord: cartesian
  • facet: season
Code
pbs_scripts |>
  gg_subseries(Scripts)

Designing ggtime

🎯 Goals

  • Decompose temporal graphics into modular elements.
  • Provide grammar extensions, and plot helpers using them.
  • Encourage users to experiment beyond standard plots.
  • Interoperability with other ggplot2 extensions (e.g. ggdist)
  • Overcome current limitations of time series graphics.

Data

Date & POSIXct assumes a Gregorian calendar with day or second precision.

{mixtime} directly associates time with a calendar to support:

  • Mixed calendars (Gregorian, astronomical, …)
  • Mixed granularities (daily, weekly, monthly, …)
  • Local/civil time and global/absolute time operations
    • e.g. "2 aweeks" vs "2 weeks" for absolute/civil
  • Censored calendars (trading days, working hours, …)
  • Origin-less time (time of day, month of year, …)
  • and much more…

Geometries / Layers

  • geom_time_line()

    A time-aware version of geom_line(). Shows calendrical jumps with dashed lines and/or open and closed circles.

    Code
    tz_shift <- as_tibble(tsibbledata::gafa_stock) |>
      filter(
        (Symbol == "AAPL" & Date <= "2014-01-15") | 
          (Symbol == "GOOG" & Date <= "2014-01-13")
      ) |>
      mutate(Date = Sys.Date() + hours(c(1:3, 3:9, 1:2, 4:9)), DST = ifelse(Symbol == "AAPL", "DST Ends", "DST Begins")) |> 
      slice(1:3, 3:12, 12:n()) |> 
      mutate(
        open = duplicated(Open),
        closed = c(open[-1], FALSE),
        Date = Date + open*3600*((DST=="DST Begins")*2-1)
      ) 
    
    tz_shift |> 
      ggplot(aes(x = Date, y = Close)) + 
      geom_path(aes(group = cumsum(open))) + 
      geom_path(linetype = "dashed", data = filter(tz_shift, open | closed)) +
      geom_point(aes(shape = closed), data = filter(tz_shift, open | closed), size = 3) +
      facet_wrap(vars(DST), ncol = 2, scales = "free_y") + 
      scale_shape_manual(values = c("TRUE" = 16, "FALSE" = 1)) + 
      guides(shape = "none")

  • geom_time_candle()

    Shows calendrical value changes/ranges (e.g. daily, weekly, monthly, annual).

    Code
    tsibbledata::gafa_stock |> 
      filter(Symbol == "GOOG") |> 
      filter(yearmonth(Date) == yearmonth("2014 Jun")) |> 
      ggplot(aes(x = Date)) + 
      tidyquant::geom_candlestick(aes(open = Open, high = High, low = Low, close = Close)) + 
      scale_x_date(date_labels = "%d %b %Y") +
      labs(y = "GOOG Stock")

Cheeky discussion: are these ‘layers’ or ‘geometries’?

Much like geom_boxplot(), these are compositions of geom, stat, and position.

Following discussions#58, I’m tempted to expose and promote layer_*() aliases for these.

Design discussion 1 (alignment)

Recall the geom_time_line() example handling jumps in time from daylight savings.

This is an alignment of timezone changes (also known as civil or local time).

🕝 Timezone alignment in the grammar

Where should this ‘jumping’ behaviour live in the grammar?

  • directly in geom_time_line()?
  • position_time_civil()/position_time_absolute()?
  • an option of scale_*_mixtime()?
  • somewhere else?

What about other jumps, e.g. working hours?

Design discussion 1 (alignment)

{ggtime} will also align granularities.

Imagine Australian births (annual) compared with total births by state (monthly).

📅 Temporal alignment across granularities

When constrained to Date and POSIXct, left alignment is commonly used for less-frequent granularities.

e.g. 2025-01-01 can be 2025, Jan 2025, or Jan 1 2025.

Consequently, plotting is also often left-aligned.

Design discussion 1 (alignment)

{ggtime} center aligns granularities.

📅 Aligning temporal granularities

A time_align option is for left, center, or right alignment.

Where should this option belong? position? scale?

Design discussion 1 (alignment)

{ggtime} could also align cycles.

Cycles are repeating patterns with an irregular duration (and shape).

Warping cycles to have the same length as “% of cycle” can help compare cycle shapes.

📅 Temporal alignment across cycles

Two arguments are proposed (like breaks and date_breaks):

  • warp (stretch time between specific time points)
  • time_warp (stretch time by calendar, e.g. "1 month")

Does this fit best in scale_*_mixtime()?

Perhaps data pre-processing, a position or stat?

Scales

Much like ggplot2 temporal scales, {ggtime} has a unified scale_*_mixtime() with:

  • time_labels (strftime-based for Gregorian)
  • time_breaks (calendar-based, e.g. "3 months")

The scales (pending discussions#79) may also control calendrical alignment for:

  • timezones (time_local boolean)
  • granularity (time_align 0-1)
  • cycles (warp and time_warp)

Facets / Coordinates

Two calendar-plot options are proposed:

  • facet_calendar()
  • coord_calendar()

📅 Calendar layouts for temporal graphics

Calendars are useful for plotting long time series, they:

  • use a familiar layout for quickly identifying dates.
  • reveal short annual patterns (e.g. holidays and events).
  • have a better aspect-ratio for dense time axis.
  • more effectively use vertical space.

Calendars are not limited to the standard weekly layout, but are hierarchical in nature over any calendar structure.

Calendar facets

Facets separate each day (or calendar period).

Calendar coordinates

Each day (or calendar period) shares the same panel.

Calendar coordinates

Using coordinates offers many benefits:

  • faster to compute / plot
  • dense layout options
  • continuity of time between days

However creating a new coordinate system for calendars is intimidating… 😅

Design discussion 2 (coordinates)

My current design for coord_calendar() is to rearrange an inner coordinate system.

This creates a nested or hierarchical coordinate system – interesting!

📈 Inner cartesian coordinates (default)

p + coord_calendar(coord = coord_cartesian())

  • simplify and replicate coord foreground, background, and axis into calendar layout with gtable.
  • cut and reposition data (or grobs) into calendar layout.
  • adds calendar specific annotations for days, months, etc.

Is this technically feasible in ggplot2?

Sorry @teunbrand - I am being especially… creative… here.

Perhaps there are better design alternatives?

My primary concerns are with data access, and grob cutting.

Design discussion 2 (coordinates)

There are some implementation challenges which could prevent this idea working:

💽 Challenge 1: Data access

The calendar layout requires access to {mixtime} data.

How should the equivalent classed data be recovered after various plot stats and transformations?

✂️ Challenge 2: Grob cutting

Visually cutting grobs seems the safest and best way to relocate layers into a calendar layout.

Is rendering-style grob manipulation feasible in grid/ggplot2?

For example, intersecting a bounding box with the grobs for each week to produce partial lines to the boundary?

Wrapping circular time periods

Recall the seasonal plot:

It features an origin-less time axis (e.g. month within year), which could be achieved with data pre-processing in {mixtime}.

Wrapping continuous time

A better alternative is to retain the origin.

Instead wrap observations over calendar periods.

Wrapping circular time periods

Wrapping continuous time

A better alternative is to retain the origin.

Instead wrap observations over calendar periods.

Simply + coord_polar() for polar coordinates.

Design discussion 3 (wrapping)

Where should temporal wrapping exist?

  • data pre-processing? (relatedly ggperiodic)
  • position data to overplot seasons
    • could this cause issues with geometries between wraps?
  • coordinate like coord_wrapped()
    • very similar to coord_calendar(), but without the calendar layout.
    • possibly relevant for plotting shortest paths in coord_sf()

Design matrix of ggtime

Leveraging calendar structures, ggtime adds

  • alignment of time zones (civil / absolute)
  • alignment of granularities (left / center / right)
  • alignment warping for cycles (ragged / justified)
  • layouts for calendars
  • wrapping for seasons

These elements can be combined to produce familiar plots, or remixed to create more bespoke temporal visualisations.

Design matrix of ggtime

Example: time plot (and forecast plot)

  • alignment of time zones (civil / absolute)
  • alignment of granularities (left / center / right)
  • alignment warping for cycles (ragged / justified)
  • layouts for calendars
  • wrapping for seasons

Design matrix of ggtime

Example: calendar plots

  • alignment of time zones (civil / absolute)
  • alignment of granularities (left / center / right)
  • alignment warping for cycles (ragged / justified)
  • layouts for calendars
  • wrapping for seasons

Design matrix of ggtime

Example: seasonal plots

  • alignment of time zones (civil / absolute)
  • alignment of granularities (left / center / right)
  • alignment warping for cycles (ragged / justified)
  • layouts for calendars
  • wrapping for seasons

Thanks for your time!

Continue the discussions on GitHub!

I’ve created a ggtime discussion thread here: discussions#79

There are reply threads for each discussed topic:

  • Alignment options
  • Calendar coordinates
  • Circular plotting

I appreciate your contributions!

Unsplash credits

Thanks to these Unsplash contributors for their photos