Animating Git visualizations with gifski
viz-git-gif.RmdThe data visualization functions in ghreadme return
ggplot objects, which means each call can be saved as a PNG
frame with ggsave() and then stitched into an animated GIF
with gifski.
This vignette walks through three patterns for turning a static plot
into an animation that highlights how your commit activity evolves over
time.
Prerequisites
install.packages(c("gifski", "ggplot2"))The recipes below also assume the data-collection and ggplot-based
plot helpers from ghreadme:
library(ghreadme)
commits <- collect_git_commits(
user = "your-username",
emails = c("you@example.com", "you@work.example.com")
)How the approach works
Each recipe follows the same pattern:
- Create a temporary directory for the PNG frames.
- Loop, building one ggplot per frame and writing it with
ggsave(). - Call
make_gif()to assemble the frames into a GIF.
make_gif() is exported from ghreadme and
handles collecting the PNGs, building a per-frame delay vector, and
calling gifski::gifski(). The first_duration
and last_duration arguments let you hold the opening and
closing frames longer so readers have time to register the start and end
states.
The three recipes below all follow this pattern and only differ in what is plotted on each frame.
Recipe 1: cumulative commits, sliding the end date
cumulative_line_plot() already shows growth over time,
but stepping the date_end forward one month at a time makes
that growth feel kinetic — lines appear, lengthen, and overtake one
another as your repos accumulate commits.
Note: Start
date_seqone month afterdate_begin. Ifdate_endequalsdate_beginon the first iteration and no commits exist on that exact day,cumulative_line_plot()will error with “No commits after filtering.”
date_seq <- seq(
from = as.Date("2023-02-01"), # one month after date_begin
to = max(commits$date),
by = "1 month"
)
cum_dir <- file.path(tempdir(), "cumulative-frames")
dir.create(cum_dir, showWarnings = FALSE, recursive = TRUE)
for (i in seq_along(date_seq)) {
p <- cumulative_line_plot(
commits,
date_begin = "2023-01-01",
date_end = date_seq[[i]],
top_n = 8
)
ggsave(
filename = file.path(cum_dir, sprintf("%04d.png", i)),
plot = p,
width = 1000, height = 600, units = "px", dpi = 96, bg = "white"
)
}
make_gif(cum_dir, "cumulative.gif",
width = 1000, height = 600,
first_duration = 1, frame_duration = 0.4, last_duration = 4)Recipe 2: calendar heatmap, year by year
calendar_heatmap_plot() is a strong fit for a slow
reveal: each frame adds one more year of history, so the GIF builds
toward the present. Because the plot’s date_begin argument
is honoured during filtering, all you need to do is bump
date_end to the end of the next year.
years <- sort(unique(commits$year))
cal_dir <- file.path(tempdir(), "calendar-frames")
dir.create(cal_dir, showWarnings = FALSE, recursive = TRUE)
for (i in seq_along(years)) {
p <- calendar_heatmap_plot(
commits,
date_begin = paste0(min(years), "-01-01"),
date_end = paste0(years[[i]], "-12-31"),
title = paste("Through", years[[i]])
)
ggsave(
filename = file.path(cal_dir, sprintf("%04d.png", i)),
plot = p,
width = 1100, height = 700, units = "px", dpi = 96, bg = "white"
)
}
make_gif(cal_dir, "calendar.gif",
width = 1100, height = 700,
first_duration = 1, frame_duration = 1.2, last_duration = 4)Recipe 3: punchcard carousel across repos
The third pattern cycles through repositories instead of through time
— punchcard_plot() is rendered once per repo, so the GIF
feels like flipping through a deck of cards.
top_repos <- commits |>
dplyr::count(repo, sort = TRUE) |>
dplyr::slice_head(n = 6) |>
dplyr::pull(repo)
punch_dir <- file.path(tempdir(), "punchcard-frames")
dir.create(punch_dir, showWarnings = FALSE, recursive = TRUE)
for (i in seq_along(top_repos)) {
p <- punchcard_plot(commits, repo = top_repos[[i]], title = top_repos[[i]])
ggsave(
filename = file.path(punch_dir, sprintf("%04d.png", i)),
plot = p,
width = 1000, height = 500, units = "px", dpi = 96, bg = "white"
)
}
make_gif(punch_dir, "punchcard.gif",
width = 1000, height = 500,
first_duration = 1, frame_duration = 1.5, last_duration = 4)Tips
- Frame count vs file size. GIFs are uncompressed across frames, so a monthly cadence over five years (60 frames) renders nicely; a daily cadence produces a multi-megabyte file. Pick a step that yields 20–80 frames.
-
Final-frame hold.
last_duration(in seconds) gives readers time to absorb the end state before the GIF loops. 3–5 seconds is a good range. -
Consistent axes. Some plots auto-scale per frame,
which makes axes jitter as data is added. If that bothers you, pass an
explicit
date_begin/date_endso the x-axis stays fixed across frames. -
Spiral plots.
spiral_points_plot(),spiral_heatmap_plot(), andspiral_horizon_plot()draw with thegrid/spiralizegraphics system rather than returning ggplot objects. They are already compatible with theggsave()+gifski::gifski()approach used here — save each frame withgrDevices::png()/dev.off()and pass the resulting file list directly togifski::gifski().