Animations with gganimate

· 7 min read · Updated March 28, 2026 · intermediate
r ggplot2 gganimate visualization animation

gganimate adds a temporal dimension to your ggplot2 charts. Instead of a static image, you get a sequence of frames that reveal change over time, categories that morph into one another, or data that accumulates step by step. If you already know ggplot2, the learning curve is gentle—you keep everything you know and layer on animation.

How gganimate Extends ggplot2

gganimate works by inserting animation-specific layers between your data and the rendered output. The package adds two pseudo-aesthetics that ggplot2 does not normally know about:

  • frame maps data to individual animation frames. Points sharing the same frame value render together.
  • group defines which data points belong to the same entity across frames. Without it, gganimate cannot track a line or bar across time steps.

The critical insight is that you always start with a valid ggplot2 plot. gganimate only takes effect once you add a transition_*() function and call animate().

library(ggplot2)
library(gganimate)

# Build a static plot first—this must be valid before animation
p <- ggplot(economics, aes(x = date, y = psavert)) +
  geom_line(color = "#3366cc", linewidth = 0.8) +
  labs(title = "Personal Savings Rate", x = "Date", y = "PSAVERT (%)")

# Now add animation
p + transition_reveal(date)

The transition_reveal(date) call tells gganimate to render the line progressively as the date variable increases. Each frame adds the next portion of data.

Choosing a Transition Function

transition_*() functions are the core of gganimate. They determine how the plot data is segmented into frames.

transition_states()

Use this for discrete categories or time steps. gganimate interpolates smoothly between each state.

p <- ggplot(mtcars, aes(x = wt, y = mpg, color = factor(cyl))) +
  geom_point(size = 3) +
  labs(title = "Cylinders: {closest_state}") +
  transition_states(cyl, transition_length = 2, state_length = 1) +
  enter_fade() + exit_shrink()

animate(p, nframes = 60, fps = 10)

{closest_state} is a gganimate label expression that inserts the current state value into the title. Other useful placeholders include {frame} for the frame number and {transitioning} for a boolean.

transition_time()

For continuous time variables, transition_time() generates a number of frames proportional to the number of unique time points in your data. It handles the interpolation automatically.

p <- ggplot(economics_long, aes(x = date, y = value01, color = variable)) +
  geom_line() +
  transition_time(date) +
  shadow_wake(wake_length = 0.3)

animate(p, nframes = 80, fps = 15)

shadow_wake() leaves a fading trail of previous frames behind the current data, which helps viewers see the direction of change.

transition_reveal()

This reveals data progressively along a single variable—typically time on the x-axis. Unlike transition_time(), it does not interpolate between missing time points. It simply shows more of the line as time advances.

p <- ggplot(gss_laboured, aes(x = year, y = hours, color = degree)) +
  geom_line(size = 1) +
  geom_point(size = 2) +
  transition_reveal(year)

animate(p, renderer = gifski_renderer("reveal.gif"))

transition_layers()

Animates the plot layer by layer, building up from a blank canvas. Each frame adds one geom layer.

p <- ggplot(mtcars, aes(x = factor(cyl), y = mpg)) +
  geom_jitter(width = 0.2, color = "#d35400") +
  geom_boxplot(fill = NA, color = "#2c3e50", outlier.shape = NA) +
  geom_violin(fill = NA, color = "#8e44ad", trim = FALSE) +
  transition_layers(layer_order = c("jitter", "boxplot", "violin"))

animate(p, nframes = 30)

transition_filter()

Keeps only data matching a condition, then transitions to the next matching condition. Useful for filtering animations where data “zooms in” based on a threshold.

p <- ggplot(iris, aes(x = Petal.Length, y = Petal.Width, color = Species)) +
  geom_point(size = 3) +
  transition_filter(Sepal.Length > 4, Sepal.Length > 5, Sepal.Length > 6) +
  enter_fade() + exit_fade()

Enter and Exit Transitions

How data points appear and disappear between states is controlled by enter_*() and exit_*() functions. By default, gganimate removes departing points instantly; adding enter/exit transitions makes the motion feel natural.

p <- ggplot(mtcars, aes(x = reorder(rownames(mtcars), mpg), y = mpg, fill = factor(cyl))) +
  geom_bar(stat = "identity") +
  coord_flip() +
  transition_states(cyl, transition_length = 1, state_length = 0.5) +
  enter_fade() +     # new bars fade in
  exit_fade() +      # departing bars fade out
  enter_shrink()     # bars shrink from full height

You can stack multiple enter/exit effects—they are additive:

p + enter_fade() + enter_drift(y_mod = +5) + exit_shrink()

The enter_drift() function shifts data points in a given direction as they enter, which is especially useful for scatter plots where you want new points to float in from above or below.

Controlling Animation Pacing with ease_aes()

Easing controls the speed curve of transitions—linear means constant speed, while cubic-in-out starts and ends slowly with faster movement in the middle.

p <- ggplot(airquality, aes(x = Day, y = Temp, group = Month, color = factor(Month))) +
  geom_line(linewidth = 1) +
  transition_time(Month) +
  ease_aes("cubic-in-out")

animate(p, nframes = 80)

You can apply different easing to each aesthetic:

ease_aes(x = "sine-in-out", y = "elastic-out")

Some easing options create bouncy or spring-like effects. back-in-out overshoots the target slightly before settling. elastic-out creates a spring effect. These are useful for playful or highlight animations but can feel distracting in serious analytical work.

Rendering and Saving Animations

The animate() function renders the animation. It accepts several arguments that control output quality and file size.

animate(
  plot,
  nframes = 100,   # total frames—more = smoother but slower
  fps = 15,         # frames per second
  device = "png",   # image device: "png", "jpeg", or "gif"
  renderer = gifski_renderer("output.gif")  # output format
)

The gifski_renderer() produces high-quality GIFs. For video formats, av_renderer() outputs MP4 or WebM but requires ffmpeg to be installed on your system.

To save an animation after rendering:

anim <- ggplot(...) + transition_*() + ...
anim_save("my_animation.gif", anim)

anim_save() captures last_animation(), so you do not need to store the animation object explicitly. For reproducible scripts, storing the animation object first and then saving it is cleaner:

anim <- p + transition_time(year) + enter_fade() + exit_fade()
anim_save("timeseries.gif", animation = anim)

If you need individual frames for external assembly, use file_renderer():

animate(p, renderer = file_renderer("frames/", overwrite = TRUE))

This writes numbered PNG files to the frames/ directory.

A Practical Example: Bar Chart Race

Bar chart races are a popular animation format. Bars reorder by rank at each time step. The key trick is combining transition_states() with forcats::fct_reorder():

library(gganimate)
library(dplyr)
library(forcats)

# Suppose `data` has columns: country, value, year
p <- data %>%
  mutate(country = fct_reorder(country, value, .fun = last)) %>%
  ggplot(aes(x = value, y = country)) +
  geom_col(fill = "#3498db", width = 0.8) +
  geom_text(aes(label = round(value, 1)), hjust = -0.2, size = 3) +
  coord_cartesian(clip = "off") +
  labs(title = "Year: {closest_state}", x = NULL, y = NULL) +
  theme_minimal() +
  theme(plot.margin = margin(5, 40, 5, 5)) +
  transition_states(year, wrap = FALSE) +
  enter_fade() + exit_fade() +
  ease_aes("cubic-in-out")

animate(p, nframes = 120, fps = 20, width = 800, height = 500,
        renderer = gifski_renderer("barchart-race.gif"))

The coord_cartesian(clip = "off") setting prevents labels from being clipped when they extend past the plot area, which matters when you add text annotations to bars.

Animations in RMarkdown

RMarkdown documents can include animated figures with chunk options:

# ```{r, animation.hook="gifski", fig.show="animate", dev="gif", interval=0.1}
library(gganimate)

p <- ggplot(economics, aes(x = date, y = psavert)) +
  geom_line(color = "#3366cc") +
  transition_reveal(date)

anim_save("savings.gif", p)
# ```

Set fig.show='animate' in the chunk header so knitr knows to render the animation rather than show a static preview. The interval option controls seconds per frame (0.1s = 10fps).

Performance Considerations

Animated plots generate many image frames in memory before assembling them. Large datasets combined with high nframes values can exhaust RAM quickly.

Keep nframes as low as practical for your desired smoothness. For previewing during development, use 20–30 frames. Bump to 60–120 only for final output.

If your animation renders too slowly, consider pre-summarizing your data before plotting rather than relying on ggplot2 to handle millions of rows per frame.

See Also