Build an Animated Chart with gganimate

· 6 min read · Updated March 13, 2026 · intermediate
gganimate ggplot2 animation data-viz visualization r

Animated charts bring your data to life. Whether you are showing stock prices moving over time, survey responses changing across demographics, or population trends evolving across decades, animation helps your audience see the story unfold. This project walks you through building animated visualizations with gganimate, the ggplot2 extension for animations.

What You Will Build

By the end of this project, you will have created a polished animated visualization showing the growth of tech companies stock performance over a 10-year period. You will start with a static ggplot, add animation layers, customize transitions, and export the final product as a GIF or video.

The techniques you learn apply to any time series or categorical data. You will understand how to control animation timing, add smooth transitions between states, and create animations that are informative without being distracting.

Setting Up Your Environment

First, install and load the required packages:

install.packages(c("gganimate", "tidyverse", "lubridate"))

library(gganimate)
library(tidyverse)
library(lubridate)

You also need ImageMagick installed on your system for GIF export. On macOS, install it with Homebrew:

brew install imagemagick

On Ubuntu or Debian:

sudo apt-get install imagemagick

Preparing Your Data

Create a sample dataset representing tech company stock prices over time:

set.seed(42)

# Generate sample stock data
companies <- c("Apple", "Google", "Microsoft", "Amazon", "Meta")
n_days <- 2520  # ~10 years of trading days

stock_data <- tibble(
  date = rep(seq(as.Date("2014-01-01"), by = "day", length.out = n_days), each = 5),
  company = rep(companies, times = n_days),
  price = NA
)

# Generate realistic-looking stock paths
for (co in companies) {
  start_price <- switch(co, "Apple" = 50, "Google" = 40, "Microsoft" = 30, 
                       "Amazon" = 25, "Meta" = 60)
  returns <- rnorm(n_days, mean = 0.0003, sd = 0.02)
  prices <- start_price * cumprod(1 + returns)
  stock_data$price[stock_data$company == co] <- prices
}

# Add year column for animation grouping
stock_data <- stock_data %>%
  mutate(year = year(date)) %>%
  filter(company %in% c("Apple", "Google", "Microsoft", "Amazon"))

This creates a realistic-looking dataset with multiple companies tracked over approximately 10 years. The random seed ensures reproducibility.

Building Your First Animated Chart

Start with a basic line chart and add animation:

p <- ggplot(stock_data, aes(x = date, y = price, color = company)) +
  geom_line(size = 1.2) +
  geom_point(size = 2) +
  scale_y_continuous(labels = scales::dollar) +
  labs(
    title = "Tech Stock Prices",
    subtitle = "2014-2024",
    x = "Date",
    y = "Stock Price ($)",
    color = "Company"
  ) +
  theme_minimal()

# Add animation
animate(p, nframes = 100, fps = 10)

This works, but it shows all points appearing at once. The real power of gganimate comes from transition functions that control how data states transition.

Using Transitions for Smooth Animation

The transition_states() function splits your data into states and animates between them:

p_animated <- ggplot(stock_data, aes(x = date, y = price, color = company)) +
  geom_line(size = 1.2) +
  geom_point(size = 2, aes(group = company)) +
  scale_y_continuous(labels = scales::dollar, limits = c(0, 400)) +
  labs(
    title = "Tech Stock Prices: {closest_state}",
    subtitle = "2014-2024",
    x = "Date",
    y = "Stock Price ($)",
    color = "Company"
  ) +
  theme_minimal() +
  transition_states(year, transition_length = 2, state_length = 1) +
  enter_fade() +
  exit_fade()

animate(p_animated, nframes = 150, fps = 15, width = 800, height = 500)

Key additions here:

  • transition_states(year, ...) splits the data by year and animates between states
  • {closest_state} in the title shows the current year
  • enter_fade() and exit_fade() add smooth opacity transitions
  • Fixed y-axis limits ensure the chart does not jump around

Adding Annotations and Visual Polish

Make your animation more informative by adding a running annotation:

p_polished <- ggplot(stock_data, aes(x = date, y = price, color = company)) +
  # Current values as points
  geom_point(size = 3, aes(group = company)) +
  # Connecting line
  geom_line(size = 1.2) +
  # Add current price label
  geom_text(aes(label = paste0("$", round(price, 0)), 
                group = company),
            hjust = -0.3, size = 4, fontface = "bold") +
  scale_y_continuous(labels = scales::dollar, limits = c(0, 450)) +
  scale_color_brewer(palette = "Set1") +
  labs(
    title = "Tech Stock Performance",
    subtitle = "Year: {closest_state}",
    x = "Date",
    y = "Stock Price ($)",
    color = "Company"
  ) +
  theme_minimal(base_size = 14) +
  theme(
    plot.title = element_text(face = "bold", size = 18),
    legend.position = "bottom"
  ) +
  transition_states(year, transition_length = 2, state_length = 1) +
  enter_fade() +
  exit_fade() +
  ease_aes("cubic-in-out")

animate(p_polished, nframes = 200, fps = 20, width = 900, height = 600)

The ease_aes("cubic-in-out") makes the animation start slow, speed up, then slow down again—this feels more natural than linear motion.

Creating a Bar Chart Race

Another popular animation style is the bar chart race, showing how rankings change over time:

# Prepare data for bar race
bar_data <- stock_data %>%
  filter(month(date) == 1) %>%  # Annual snapshots
  select(date, company, price) %>%
  mutate(rank = rank(-price)) %>%
  filter(rank <= 4)

p_race <- ggplot(bar_data, aes(x = rank, y = price, fill = company, group = company)) +
  geom_col(width = 0.8) +
  geom_text(aes(y = price, label = paste0("$", round(price, 0))),
            hjust = -0.2, size = 5, fontface = "bold") +
  scale_x_reverse() +
  scale_y_continuous(limits = c(0, 400)) +
  scale_fill_brewer(palette = "Set1") +
  coord_flip() +
  labs(
    title = "Top Tech Stocks by Price",
    subtitle = "January {closest_state}",
    x = "",
    y = "Stock Price ($)",
    fill = "Company"
  ) +
  theme_minimal(base_size = 14) +
  theme(
    axis.text.x = element_blank(),
    axis.ticks.x = element_blank()
  ) +
  transition_states(date, transition_length = 1, state_length = 2) +
  enter_grow() +
  ease_aes("back-out")

animate(p_race, nframes = 100, fps = 10, width = 700, height = 500)

Saving Your Animation

Export your animation in multiple formats:

# Save as GIF
anim_save("tech-stocks.gif", animation = p_polished)

# Save as MP4 (requires ffmpeg)
anim_save("tech-stocks.mp4", animation = p_polished, renderer = ffmpeg_renderer())

# Save with specific settings
anim_save(
  "custom-animation.gif",
  animation = p_polished,
  nframes = 150,
  fps = 15,
  width = 900,
  height = 600,
  res = 100
)

The GIF format works everywhere but can be large. For web use, consider reducing frames or using a video embed.

Understanding Animation Timing

The timing of your animation significantly affects how it is perceived:

Transition LengthEffect
Short (0.5-1)Quick, energetic changes
Medium (2-3)Smooth, readable transitions
Long (4+)Dramatic, emphasizes each state
Frame RateBest For
10-15 fpsSimple transitions, web GIFs
20-30 fpsSmoother, video exports
30+ fpsVery smooth, but larger files

Start with 15 fps and adjust based on your content complexity.

Common Issues and Solutions

Animation appears jerky

Increase the number of frames:

animate(p, nframes = 200, fps = 20)

File size is too large

Reduce frames or dimensions, or export as video instead of GIF.

Labels overlap

Adjust hjust values or reduce the number of elements shown.

Chart jumps between states

Fix the axis limits explicitly:

scale_y_continuous(limits = c(0, max_value))

Taking It Further

Now that you have the basics, try these enhancements:

  1. Interactive animations: Combine gganimate with plotly for hover information
  2. Custom easing: Try different ease_aes() functions for varied feels
  3. Multiple transitions: Use transition_time() for continuous data
  4. Pause states: Add pause() to hold on specific frames

See Also