Skip to contents

The 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:

  1. Create a temporary directory for the PNG frames.
  2. Loop, building one ggplot per frame and writing it with ggsave().
  3. 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_seq one month after date_begin. If date_end equals date_begin on 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)

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_end so the x-axis stays fixed across frames.
  • Spiral plots. spiral_points_plot(), spiral_heatmap_plot(), and spiral_horizon_plot() draw with the grid/spiralize graphics system rather than returning ggplot objects. They are already compatible with the ggsave() + gifski::gifski() approach used here — save each frame with grDevices::png() / dev.off() and pass the resulting file list directly to gifski::gifski().

Embedding in your README

Drop the rendered GIF into your profile README.Rmd the same way as any image — typically right above the badge block so it plays as soon as someone loads your profile:

![](cumulative.gif)

Re-knit README.Rmd to refresh README.md, then commit the GIF alongside it.