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)

stats <- collect_git_commits(
  user    = "your-username",
  emails  = "you@example.com",
  include = c("commits", "stars", "issues", "prs")
)

Calling collect_git_commits() with no include argument returns the original commits-only tibble shape, which is exactly what the plot functions expect. If you also want star counts, authored issues, or authored pull requests in the same call, see vignette("gh-stats"); the multi-include path returns a named list, in which case the recipes here would use stats$commits in place of the bare commits object.

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 moving the date_end forward one month at a time makes that growth feel kinetic. The 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(stats$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(
    stats$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 honored during filtering, all you need to do is bump date_end to the end of the next year.

years <- sort(unique(stats$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(
    stats$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. The punchcard_plot() graph is rendered once per repo, so the GIF feels like flipping through a deck of cards. The example below ranks repos by commit count; see the “Rank by stars” tip below for an API-backed alternative.

top_repos <- stats$commits |>
  dplyr::count(repo, sort = TRUE) |>
  dplyr::slice_head(n = 6) |>
  dplyr::pull(repo)

# Alternative: rank by stars received (now that stats$stars is available)
# top_repos <- head(stats$stars$repo, 6)

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(stats$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().

  • Rank by stars. The punchcard recipe ranks repos by commit count. To rank by GitHub stars instead, ask collect_git_commits() for both types at once and pull the carousel order from the stars element:

    stats <- collect_git_commits(
      user    = "your-username",
      emails  = "you@example.com",
      include = c("commits", "stars")
    )
    top_repos <- head(stats$stars$repo, 6)
    # then iterate with punchcard_plot(stats$commits, repo = top_repos[[i]], ...)

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. For an end-to-end setup (scaffolding the Rmd, scheduling re-knits via GitHub Actions), see vignette("ghreadme").