I was recently working on a fun project using gganimate and had one of those breakthroughs that made me think “This is what it feels like to steal fire from the gods!”1 I thought I would share the epiphany as a blog post for anyone else who would find it helpful.

An introduction to frame variables

One of gganimate’s cool features is frame variables. They allow your labels to include data about the animation using glue-style syntax. For example:

library(tidyverse)
library(cowplot) # for theme_minimal_hgrid()
library(gganimate)
diamond_summary <- diamonds %>% 
  group_by(color, cut) %>% 
  summarise(ave_price = mean(price), max_price = max(price))

ggplot(diamond_summary, aes(color, ave_price)) + 
  geom_col() +
  theme_minimal_hgrid() +
  transition_states(cut, transition_length = 3, state_length = 1) +
  ggtitle("Going from {previous_state} to {next_state}")

We’re using the frame variable "{previous_state}" to show that last value of cut we stopped at and "{next_state}" to show where we’re going2.

But what if we wanted a title like “Max price for [{next_state}] is [price] with color: [color]”? This is the part where we get to play Prometheus. Thankfully, "{next_state}" is a string we can pass to functions:

ggplot(diamond_summary, aes(color, ave_price)) + 
  geom_col() +
  theme_minimal_hgrid() +
  transition_states(cut, transition_length = 3, state_length = 1) +
  # cut is factor in diamonds, but is being coerced into a character
  ggtitle("next_state is a string: {is.character(next_state)}",
          subtitle = "next_state is a factor: {is.factor(next_state)}")

Note: Depending on the transition, type might be preserved instead of being coerced to character. Experiment to figure out how the transition you’re using handles the data before writing any functions.

Helper functions for frame variables

So now we just need a function to find which color has the highest price for a given next_state.

create_max_price_title <- function(frame_var){
  max_price_row <- diamond_summary %>% 
    filter(cut == frame_var) %>% 
    group_by(cut) %>% 
    filter(max_price == max(max_price))
  
  max_price <- pull(max_price_row, max_price)
  
  max_color <- pull(max_price_row, color)
  
  paste( "Max price for", frame_var,"is", max_price, "with color:", max_color)
}

Now we’re good to go.

ggplot(diamond_summary, aes(color, ave_price)) + 
  geom_col() +
  theme_minimal_hgrid() +
  transition_states(cut, transition_length = 3, state_length = 1) +
  ggtitle("{create_max_price_title(next_state)}")

We did it!

A couple of notes about transition_reveal() and transition_time()

First, unlike transition_state(), both transition_reveal() and transition_time() preserve the type of their frame variables. If your helper function relies on the fact that you are transitioning over a Date, for example, you don’t have to convert it back from a character.

On the other hand, the frame variables for transition_reveal() and transition_time() often won’t correspond to an actual value in your data the same way next_state does. gganimate creates artificial times evenly spaced across your data set and interpolates the appropriate values for your other variables3. The frame variables are those artificial times. As result, your helper function will need to replace filter(transition_col == frame_var) with a step like filter(transition_col <= frame_var). Depending on the helper function, you may want to use slice() or additional filter() calls to get just the most “recent” data.


  1. Minus the part where I get my liver repeatedly ripped out.

  2. Admittedly, it’s a little awkward when both frame variables are the same because we’re not in an in-between frame.

  3. This is an oversimplification of gganimate’s internals, not helped by the fact that I’m not an expert on tweening.