Build an Animated Chart with gganimate
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 yearenter_fade()andexit_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 Length | Effect |
|---|---|
| Short (0.5-1) | Quick, energetic changes |
| Medium (2-3) | Smooth, readable transitions |
| Long (4+) | Dramatic, emphasizes each state |
| Frame Rate | Best For |
|---|---|
| 10-15 fps | Simple transitions, web GIFs |
| 20-30 fps | Smoother, video exports |
| 30+ fps | Very 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:
- Interactive animations: Combine gganimate with plotly for hover information
- Custom easing: Try different
ease_aes()functions for varied feels - Multiple transitions: Use
transition_time()for continuous data - Pause states: Add
pause()to hold on specific frames
See Also
r-ggplot2-extensions— Extend ggplot2 with patchwork, ggrepel, and gganimateggplot2-publication-chart— Create publication-ready charts with ggplot2interactive-map-leaflet— Interactive visualizations with leaflet in R