Code

ggtime: Visualizing time with a grammar of temporal graphics
5th August 2025 @ JSM, Nashville TN
Mitchell O’Hara-Wild, Monash University
Cynthia Huang, Monash University
Matthew Kay, Northwestern University
Rob Hyndman, Monash University

Several types of data require unique care.
🍀 uncertainty
{distributional} / {ggdist}
🕸️ graph
{tidygraph} / {ggraph}
🗺️ space
{sf} / {ggplot2}
⌛ time
{mixtime} / {ggtime}
Time is comparatively simple dimension, it is a continuous and ordered variable.
Why do we need specialised tools?
📆 Calendrical time
While time itself is simple, calendrical patterns are complex!
Temporal data/patterns align with calendars.
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 do we need ggtime?
{ggtime} visualises time with calendrical patterns.
ggplot2 extensions can be categorised into:
🖼️ Plot helpers
Functions which are used to quickly create a specific plot.
autoplot() / autolayer()ggtime::gg_season()ggtime::gg_subseries()🎨 Grammar extensions
Functions which add new features to the ggplot2’s grammar.
ggtime::geom_time_line()ggtime::scale_x_mixtime()ggtime::facet_calendar()These extension types are both useful:
🖼️ Plot helpers
Plot helpers focus on what is being plotted.
🎨 Grammar extensions
Grammar extensions are about how something is plotted.
These ‘visual idioms’ of time series plots can be categorised into two core groups:
Changing how time is plotted
Linear time plots show time continuously on the x/y axis.
Circular time plots loop time over specific calendar units.
These plot styles highlight different patterns in the data.
Maps time to the x/y-axis.
They reveal trends, seasons, cycles, outliers, and more.
🎨 Grammar elements
These are simply time plots.
They just happen to show forecasts, or uncertain values.
🎨 Grammar elements
{ggtime} re-expresses these common plots with a composable ggplot2-like grammar.
🎯 Goals
The ggplot2 grammar of graphics identifies compositional elements involved in plotting.
Each of these elements are extensible.
| Element | Description |
|---|---|
| Data | The original data to plot |
| Aesthetics | Map graphical dimensions |
| Geometries | Graphical objects to plot |
| Scales | Transform data to visuals |
| Positions | Adjust placement of values |
| Statistics | Data summary statistics |
| Facets | Split plots into subplots |
| Coordinates | Plotting reference system |
| Guides | Explain aesthetic mappings |
| Themes | Non-data visual style |
| Element | Description |
|---|---|
| Data | The original data to plot |
| Aesthetics | Map graphical dimensions |
| Geometries | Graphical objects to plot |
| Scales | Transform data to visuals |
| Positions | Adjust placement of values |
| Statistics * | Data summary statistics |
| Facets | Split plots into subplots |
| Coordinates * | Plotting reference system |
| Guides * | Explain aesthetic mappings |
| Themes | Non-data visual style |
Data for ggplot2 needs to be rectangular.
Most time series datasets look like this:
# A tibble: 24,320 × 5
Quarter Region State Purpose Trips
<date> <chr> <chr> <chr> <dbl>
1 1998-01-01 Adelaide South Australia Business 135.
2 1998-04-01 Adelaide South Australia Business 110.
3 1998-07-01 Adelaide South Australia Business 166.
4 1998-10-01 Adelaide South Australia Business 127.
5 1999-01-01 Adelaide South Australia Business 137.
6 1999-04-01 Adelaide South Australia Business 200.
7 1999-07-01 Adelaide South Australia Business 169.
8 1999-10-01 Adelaide South Australia Business 134.
9 2000-01-01 Adelaide South Australia Business 154.
10 2000-04-01 Adelaide South Australia Business 169.
# ℹ 24,310 more rows
Existing time vectors are limited
A better solution?
The {mixtime} package works with:
Many calendars via {calcal}
Gregorian, Chinese, Islamic, Hebrew, …
Custom calendars / granularities
Business hours, trading days, trimesters, …
Mixed temporal granularities in the same tsibble
Useful for related series at different frequencies!

mixtime vectors works nicely in rectangular data structures for ggplot2.
# A tsibble: 18 x 5 [1Y, 1Q, 1M]
Time Region State Purpose Trips
<mixtime> <chr> <chr> <chr> <dbl>
1 2020 Melbourne Victoria Holiday 7784.
2 2021 Melbourne Victoria Holiday 7543.
3 2020 Q1 Melbourne Victoria Holiday 1880.
4 2020 Q2 Melbourne Victoria Holiday 1965.
5 2020 Q3 Melbourne Victoria Holiday 1929.
6 2020 Q4 Melbourne Victoria Holiday 2010.
7 2020 Jan Melbourne Victoria Holiday 735.
8 2020 Feb Melbourne Victoria Holiday 573.
9 2020 Mar Melbourne Victoria Holiday 571.
10 2020 Apr Melbourne Victoria Holiday 607.
11 2020 May Melbourne Victoria Holiday 747.
12 2020 Jun Melbourne Victoria Holiday 611.
13 2020 Jul Melbourne Victoria Holiday 663.
14 2020 Aug Melbourne Victoria Holiday 632.
15 2020 Sep Melbourne Victoria Holiday 634.
16 2020 Oct Melbourne Victoria Holiday 582.
17 2020 Nov Melbourne Victoria Holiday 622.
18 2020 Dec Melbourne Victoria Holiday 806.

geom_time_line()
A time-aware version of geom_line(). Shows timezone offsets with dashed lines from the [x/y]_time_offset aesthetic.
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)) +
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 value changes over time periods (e.g. daily, weekly, …) which are calculated using the stat_candle statistic.
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),
colour_up = "#1FB974", fill_up = "#1FB974", colour_down = "#F4375D", fill_down = "#F4375D"
) +
scale_x_date(date_labels = "%d %b %Y") +
labs(y = "GOOG Stock")

The x/y position of time is timezone adjusted:
position_time_absolute() positions time at its exact global location.
pedestrian |>
filter(Sensor == "Southern Cross Station") |>
filter(year(Date_Time) == 2015) |>
mutate(Date_Time = force_tz(make_datetime(year(Date), month(Date), day(Date), Time), "Australia/Melbourne")) |>
ggplot(aes(x = Date_Time - as.POSIXct(Date), y = Count, group = Date)) +
geom_line(alpha = 0.2) +
theme(axis.text.x = element_blank()) +
labs(x = "Time", title = "Hourly pedestrians passing Southern Cross Station")
Timezone differences (e.g. daylight savings) misaligns seasonal patterns.
position_time_civil() positions time as it appears locally in each timezone.

The scales in ggplot2 provide:
scale_*_date() for Date,scale_*_datetime() for POSIXct,scale_*_time() for hms.Extension packages (e.g. tsibble) add:
scale_*_yearquarter() for yearquarter,scale_*_yearmonth() for yearmonth,scale_*_yearweek() for yearweek.Unified scales for time series
mixtime has many calendars and granularities.
ggtime unifies them all with scale_*_mixtime().
The mixtime scales support temporal labels and breaks, much like ggplot2:
time_labels (strftime, e.g. "%Y %b")time_breaks (duration, e.g. "3 months")These scales also handle the alignment of:
time_align 0-1)warp and time_warp){ggtime} aligns mixed 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.

{ggtime} center aligns granularities.

📅 Aligning temporal granularities
Specify alignment of different granularities with scale_x_mixtime(align_time = <0-1>).

{ggtime} defaults to center alignment.
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
In scale_x_mixtime(), warp time with
warp (stretch time between specific time points)time_warp (stretch time by duration, e.g. "1 month")
Two calendar-plot approaches:
facet_calendar()coord_calendar()📅 Calendar layouts for temporal graphics
Calendars are useful for plotting long time series, they:
Calendars are not limited to the standard weekly layout, but are hierarchical in nature over any calendar structure.
Facets separate each day (or calendar period).


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


Season plots loop time over seasonalities.
coord_time_loop()The time loop points can be specified with:
loops (loop over specific time points)time_loops (loop by duration - "1 week")
Looping continuous time
Looping the x-axis over seasonal granularities (e.g. day, week, or year) clearly shows seasonality.
coord_time_loop(time_loops = "1 year")


Non-cartesian coordinates
Looping conceptually applies to other coordinate spaces too. Combining coord_loop() and coord_polar() shows seasonality in polar coordinates.

Closing remarks
Become a ggplot2 extender!
Start with making plot helpers using ggplot2
Create a new branded theme for your organisation
Extend the grammar with a new Geom and Stat
See how others have written ggplot extensions
(there’s plenty of examples out there!)

Thanks to these Unsplash contributors for their photos