How to Combine Animated Plots in R

Leveraging the power of {gganimate} and {magick} to combine animated plots for your viewers.

October 2019 • 9 minute read


In this tutorial, I'm going to outline the steps necessary to create an animated, faceted plot in R. Although rare, combining animated plots can be a powerful way to showcase different elements of the same data (as you'll see below).

In this example, I'm using weightlifting data from the International Powerlifting Federation. For the purposes of this tutorial, we'll look at differences in top lifts by sex. A faceted, animated plot is a great option because we'd like to observe the magnitude of these differences and how these differences have evolved over time.

We'll be creating this GIF:

A combination of the two aforementioned plots. The top plot is the animated dumbbell plot, and the bottom is a line chart. In combination, they allow the user to see the magnitude of differences between men and women, as well as how these differences have evolved over time.

Environment Setup

These are the packages we'll need to get started. In my case, I use a custom theme I've developed for stylistic purposes. Feel free to instead run theme_set(theme_minimal()) rather than use my theme!

library(ggplot2)
library(tidyverse)
library(ggtext)
library(gifski)
library(gganimate)
library(cr)

set_cr_theme(font = "Proxima Nova")
# theme_set(theme_minimal())

Load and Clean Data

I've already done a lot of the data cleaning for you. If you'd like to follow along, here's the process (or, skip ahead!).

Here, we'll do some minor cleaning and then reshape the three lifts into one column:

ipf_lifts <- readr::read_csv("data/ipf_lifts.csv") %>%
  mutate(year = lubridate::year(date))

ipf_lifts_reshape <- ipf_lifts %>%
  tidyr::pivot_longer(cols = c("best3squat_kg", "best3bench_kg", "best3deadlift_kg"), names_to = "lift") %>%
  select(name, sex, year, lift, value)

For my visualization, I’m only concerned with the heaviest lifts from each year:

ipf_lifts_maxes <- ipf_lifts_reshape %>%
  group_by(year, sex, lift) %>%
  top_n(1, value) %>%
  ungroup %>%
  distinct(year, lift, value, .keep_all = TRUE)

The first visualization we'll create for the final output is a dumbbell plot. Curious what that is, or how to make it in R? Check out my other post on the topic.

In order to construct a dumbbell plot, we need both male and female observations in the same row. For this, we use the spread function.

max_pivot <- ipf_lifts_maxes %>%
  spread(sex, value)

Now, let’s construct a dataframe for each sex:

male_lifts <- max_pivot %>%
  select(-name) %>%
  filter(!is.na(M)) %>%
  group_by(year, lift) %>%
  summarise(male = mean(M))

female_lifts <- max_pivot %>%
  select(-name) %>%
  filter(!is.na(F)) %>%
  group_by(year, lift) %>%
  summarise(female = mean(F))

And join them:

max_lifts <- merge(male_lifts, female_lifts)

max_lifts_final <- max_lifts %>%
  group_by(year, lift) %>%
  mutate(diff = male - female)

Not following along, or want to check your progress? Here's what our data looks like in its final form:

yearliftmalefemalediff
1980best3bench_kg262.5120142.5
1980best3deadlift_kg395205190
1980best3squat_kg417.5230187.5
1981best3bench_kg24515095
1981best3deadlift_kg367.5230137.5
1981best3squat_kg427.5215212.5

Visualize!

Finally, we can construct the visualization.

First, we can create a static visualization using ggalt (again, my blog post covers the details of this step).

You can fast forward the creation of individual plots if you're only interested in the combination of the two. You'll find that at the end of this post!

max_lifts_final %>%
  filter(year == 2019) %>%
  ggplot() +
  ggalt::geom_dumbbell(aes(y = lift,
                    x = female, xend = male),
                colour = "grey", size = 5,
                colour_x = "#D6604C", colour_xend = "#395B74") +
  labs(y = element_blank(),
       x = "Top Lift Recorded (kg)",
       title =  "How <span style='color:#D6604C'>Women</span> and <span style='color:#395B74'>Men</span> Differ in Top Lifts",
                       subtitle = "In 2019") +
  theme(plot.title = element_markdown(lineheight = 1.1, size = 20),
        plot.subtitle = element_text(size = 15)) +
  scale_y_discrete(labels = c("Bench", "Deadlift", "Squat")) +
  drop_axis(axis = "y") +
  geom_text(aes(x = female, y = lift, label = paste(female, "kg")),
            color = "#D6604C", size = 4, vjust = -2) +
  geom_text(aes(x = male, y = lift, label = paste(male, "kg")),
            color = "#395B74", size = 4, vjust = -2) +
  geom_rect(aes(xmin=430, xmax=470, ymin=-Inf, ymax=Inf), fill="grey80") +
  geom_text(aes(label=diff, y=lift, x=450), size=4) +
  geom_text(aes(x=450, y=3, label="Difference"),
            color="grey20", size=4, vjust=-3)
A static dumbbell plot which represents male and female ‘top lifts' in 2019.

Finally, we animate, using Thomas Pedersen’s wonderful gganimate package. This is a relatively easy step, because gganimate only requires two extra lines of code: transition_states and ease_aes. Then, we pass it into the animate function!

animation <- max_lifts_final %>%
  ggplot() +
  ggalt::geom_dumbbell(aes(y = lift,
                    x = female, xend = male),
                colour = "grey", size = 5,
                colour_x = "#D6604C", colour_xend = "#395B74") +
  labs(y = element_blank(),
       x = "Top Lift Recorded (kg)",
       title =  "How <span style='color:#D6604C'>Women</span> and <span style='color:#395B74'>Men</span> Differ in Top Lifts",
  subtitle='\nThis plot depicts the difference between the heaviest lifts for each sex at International Powerlifting Federation\nevents over time. \n \n{closest_state}') +
  theme(plot.title = element_markdown(lineheight = 1.1, size = 25, margin=margin(0,0,0,0)),
        plot.subtitle = element_text(size = 15, margin=margin(8,0,-30,0))) +
  scale_y_discrete(labels = c("Bench", "Deadlift", "Squat")) +
  drop_axis(axis = "y") +
  geom_text(aes(x = female, y = lift, label = paste(female, "kg")),
            color = "#D6604C", size = 4, vjust = -2) +
  geom_text(aes(x = male, y = lift, label = paste(male, "kg")),
            color = "#395B74", size = 4, vjust = -2) +
  transition_states(year, transition_length = 4, state_length = 1) +
  ease_aes('cubic-in-out')

a_gif <- animate(animation,
                 fps = 10,
                 duration = 25,
                 width = 800, height = 400,
                 renderer = gifski_renderer("outputs/animation.gif"))

a_gif
An animated dumbbell plot which represents male and female ‘top lifts' over time. Each tick of the animation represents a new year.

But in our case, we'd like to include another GIF: a line chart of differences over time.

animation2 <- max_lifts_final %>%
  ungroup %>%
  mutate(lift = case_when(lift == "best3bench_kg" ~ "Bench",
                          lift == "best3squat_kg" ~ "Squat",
                          lift == "best3deadlift_kg" ~ "Deadlift")) %>%
  ggplot(aes(year, diff, group = lift, color = lift)) +
  geom_line(show.legend = FALSE) +
  geom_segment(aes(xend = 2019.1, yend = diff), linetype = 2, colour = 'grey', show.legend = FALSE) +
  geom_point(size = 2, show.legend = FALSE) +
  geom_text(aes(x = 2019.1, label = lift, color = "#000000"), hjust = 0, show.legend = FALSE) +
  drop_axis(axis = "y") +
  transition_reveal(year) +
  coord_cartesian(clip = 'off') +
  theme(plot.title = element_text(size = 20)) +
  labs(title = 'Difference over time',
       y = 'Difference (kg)',
       x = element_blank()) +
  theme(plot.margin = margin(5.5, 40, 5.5, 5.5))

b_gif <- animate(animation2,
                 fps = 10,
                 duration = 25,
        width = 800, height = 200,
        renderer = gifski_renderer("outputs/animation2.gif"))

b_gif
An animated line chart that shows, year by year, the difference in male and female top lifts on the Y axis. Each tick of the chart shows a new year.

Finally, we'll combine them using magick (thanks to this post):

library(magick)
a_mgif <- image_read(a_gif)
b_mgif <- image_read(b_gif)

new_gif <- image_append(c(a_mgif[1], b_mgif[1]), stack = TRUE)
for(i in 2:250){
  combined <- image_append(c(a_mgif[i], b_mgif[i]), stack = TRUE)
  new_gif <- c(new_gif, combined)
}

What’s happening here? Essentially, we’re using the power of magick to:

  1. Read in all of the individual images (image_read) from each GIF (after all, a GIF is just a series of images!).
  2. For the first frame, stack the two images on top of each other (image_append), so plot 1 is above plot 2.
  3. For the rest of the frames (in my case, the next 249, because my GIF had 250 frames), replicate this and combine it with the first frame (this is the for loop).

Here, we specify stack = TRUE so that one plot is above the other. If you'd like to place them side-by-side, specify stack = FALSE.

In combination, the process results in our final output:

A combination of the two aforementioned plots. The top plot is the animated dumbbell plot, and the bottom is a line chart. In combination, they allow the user to see the magnitude of differences between men and women, as well as how these differences have evolved over time.

In this view, we can see the magnitude of the differences both relatively and absolutely (top chart), and we can see how these differences change over time (bottom chart). The power of an animated, combined chart!