Race Charts in R: How to Visualize and Compare Change Over Time with gganimate

Reading time:
time
min
By:
Dario Radečić
June 18, 2024

So, you’ve mastered the basics of ggplot2 animation and are now looking for a real-world challenge? You’re in the right place. After reading this one, you’ll know how to download and visualize stock data change through something known as race charts.

You can think of race charts as dynamic visualizations (typically bar charts) that display a ranking of different items over time. In our case, we’ll build a stock price comparison animation that shows the average monthly stock price for 7 tickers for the last 10 years.

Let’s start by gathering data. The best part - you can do it straight from R!

Want to use R to process huge volumes of data? Read our detailed comparison of tools and packages for efficiently handling 1 billion rows.

Table of contents:

  • Data Gathering: How to Download and Organize Stock Data in R
  • Step 1: Build and Style a Bar Chart for a Single Time Period
  • Step 2: Chart Animation with R gganimate
  • Summing up Race Charts in R

Data Gathering: How to Download and Organize Stock Data in R

The process of gathering data typically involves downloading CSV/Excel files from the web or connecting to a database of some sort. That couldn’t be further from the truth when it comes to financial data.

You can download data for any ticker and any time period straight from R! Just make sure you have the `quantmod` package installed. And speaking of packages, these are the ones you’ll need today:

library(quantmod)
library(dplyr)
library(lubridate)
library(gganimate)

If you don’t have them installed, simply run `install.packages(“<package-name>”)` from the R console.

Moving on, we’ll declare a vector of 7 tickers for which we want to download daily stock prices: Amazon, Apple, Google, Microsoft, Meta, Tesla, and Nvidia. For each, we’ll grab the last 10 years’ worth of data, indicated with `start_date` and `end_date`.

To demonstrate how this approach works, we’ll call the `quantmod::getSymbols()` function to display the first 10 rows of a returned dataframe for the Nvidia stock. It will make adequate API calls to Yahoo Finance for you:

# Define stock tickers and the time period
tickers <- c("AMZN", "AAPL", "GOOGL", "MSFT", "META", "TSLA", "NVDA")
start_date <- Sys.Date() - years(10)
end_date <- Sys.Date()

# The getSymbols() function returns a data.frame of stock data for a given ticker
head(getSymbols("NVDA", src = "yahoo", from = start_date, to = end_date, auto.assign = FALSE), 10)

This is the output you’ll see:

Image 1 - Sample of Nvidia stock prices

We only care about the Adjusted price and the date, which is represented by the dataframe index. Let’s see how to get the data for all tickers next.

How to Get Historical Data for All Tickers

The `get_monthly_averages()` is a custom function that will download stock data for a provided ticker. It will then rename the dataframe columns so that the ticker name is removed from them (needed for the aggregation later).

As mentioned earlier, only the Adjusted price column is relevant, so we’ll construct a new dataframe with its value and the index (time period). We’ll then remove the day information from the date since we only care for monthly averages.

Finally, the mean value for each time period is calculated and returned:

# Function to download and aggregate stock data
get_monthly_averages <- function(ticker) {
  # Download stock data from Yahoo Finance
  stock_data <- getSymbols(ticker, src = "yahoo", from = start_date, to = end_date, auto.assign = FALSE) 
  # Remove ticker prefix from column names
  colnames(stock_data) <- gsub(paste0(ticker, "\\."), "", colnames(stock_data)) 
  # Keep only the 'Adjusted' column from the data
  stock_data <- stock_data[, "Adjusted"] 
  # Convert the data to a data frame with Date and Adjusted columns
  stock_data <- data.frame(Date = index(stock_data), Adjusted = coredata(stock_data))
  # Add a YearMonth column by flooring the Date to the nearest month
  stock_data$YearMonth <- floor_date(stock_data$Date, "month") 
  
  # Group the data by month and calcualte the mean adjusted price for each period
  monthly_data <- stock_data %>% 
    group_by(YearMonth) %>%
    summarize(Value = mean(Adjusted, na.rm = TRUE)) %>%
    ungroup() %>%
    mutate(Ticker = ticker)
  
  return(monthly_data)
}

With the function to get single stock data out of the way, you can now use the `lapply()` magic to apply the function to each of our 7 tickers. Here, we’ll also arrange the data by the time period (ascending) and the average stock value (descending):

data <- lapply(tickers, get_monthly_averages)
data <- bind_rows(data)
data <- data %>%
  arrange(YearMonth, desc(Value))

head(data, 10)
Image 2 - Downloaded stock data for all 7 tickers

You could say the following to interpret this data: Microsoft’s average stock price for the month June of the year 2014 was USD 35.5.

You now have the data, so the only thing left to do is to build some cool visualizations!

Step 1: Build and Style a Bar Chart for a Single Time Period

Rendering an animated chart takes time, so a good piece of advice is to start small by building a visualization for a single time period. This way, you’ll know everything looks exactly the way you want to.

Race charts need one thing to work properly, and that is the rank. In our case, rank represents the position of the ticker’s value when compared to other tickers. In other words, it determines the position the column will have in a bar chart.

While here, we’ll also add a label column (you’ll see why in a bit):

chart_data <- data %>%
  group_by(YearMonth) %>%
  mutate(
    Rank = rank(-Value),
    Label = paste0("$", round(Value, 2))
  )

head(chart_data, 10)

Here’s what your dataframe should look like now:

Image 3 - Stock data with added Rank and Label columns

Meta had the highest stock price in June 2014, so it’s ranked number 1. The exact opposite is true for Nvidia.

The last thing we want to do before visualizing this data is to declare a custom color palette. We’ve grabbed the official brand color codes so the chart looks more representative:

chart_colors <- c(
  AMZN = "#FF9900",
  AAPL = "#555555",
  GOOGL = "#0F9D58",
  MSFT = "#FFB900",
  META = "#0081FB",
  TSLA = "#cc0000",
  NVDA = "#76B900"
)

And now, onto the visualization. As mentioned, we’ll create it for a single time period first, and discuss animation in the following section.

We’re creating a horizontal bar chart with the help of the `geom_tile()` function that’s typically used to build heatmaps. The two `geom_text()` calls are used to add ticker names and stock prices, both from their respective side of the chart. The rest of the code is related to styling, changing the theme, and playing around with the overall aesthetics:

ggplot(chart_data %>% filter(YearMonth == "2024-05-01"), aes(Rank, group = Ticker, fill = as.factor(Ticker), color = as.factor(Ticker))) +
  geom_tile(aes(y = Value / 2, height = Value, width = 0.9), alpha = 0.9, color = NA) +
  geom_text(aes(y = 0, label = Ticker), vjust = 0.2, hjust = 1.3) +
  geom_text(aes(y = Value, label = Label, hjust = -0.15)) +
  coord_flip(clip = "off", expand = FALSE) +
  scale_x_reverse() +
  scale_fill_manual(values = chart_colors) +
  scale_color_manual(values = chart_colors) +
  theme_minimal() +
  theme(
    axis.text.x = element_blank(),
    axis.text.y = element_blank(),
    axis.title.x = element_blank(),
    axis.title.y = element_blank(),
    plot.title = element_text(size = 18, face = "bold", color = "#424242"),
    legend.position = "none",
    plot.margin = margin(1, 2, 1, 2, unit = "cm")
  )

In the end, this is the chart you should end up with:

Image 4 - Static chart for a single time period

Now let‘s see how to animate it!

Step 2: Chart Animation with R gganimate

Good news - nothing much changes in the charting code. You only have to remove the data filtering so all time periods are captured, and also store the entire plot into a variable:

p <- ggplot(chart_data, aes(Rank, group = Ticker, fill = as.factor(Ticker), color = as.factor(Ticker))) +
  geom_tile(aes(y = Value / 2, height = Value, width = 0.9), alpha = 0.9, color = NA) +
  geom_text(aes(y = 0, label = Ticker), vjust = 0.2, hjust = 1.3) +
  geom_text(aes(y = Value, label = Label, hjust = -0.15)) +
  coord_flip(clip = "off", expand = FALSE) +
  scale_x_reverse() +
  scale_fill_manual(values = chart_colors) +
  scale_color_manual(values = chart_colors) +
  theme_minimal() +
  theme(
    axis.text.x = element_blank(),
    axis.text.y = element_blank(),
    axis.title.x = element_blank(),
    axis.title.y = element_blank(),
    plot.title = element_text(size = 18, face = "bold", color = "#424242"),
    legend.position = "none",
    plot.margin = margin(1, 2, 1, 2, unit = "cm")
  )

Now to build the animation, we’ll append the `transition_states()` function to our chart to indicate we want the chart to change between different states. Then, the `view_follow()` function allows the chart window to follow the data dynamically, but only on the Y-axis.

The `labs()` function allows us to modify the title through the `{closest_state}` property. In simple terms, this updates the time period in the chart title:

anim <- p + transition_states(YearMonth, transition_length = 4, state_length = 1) +
  view_follow(fixed_x = TRUE) +
  labs(title = "Average monthly stock price in USD ({closest_state})")

Finally, the `animate()` function will render a 1024x768 GIF with 600 frames in total (more info about different rendering options here):

animate(
  anim,
  width = 1024,
  height = 768,
  res = 150,
  nframes = 600,
  fps = 60,
  end_pause = 60,
  renderer = gifski_renderer("stock_race_chart.gif")
)

Once the rendering finishes, you’ll see the following chart saved to your disk:

Image 5 - Rendered race chart

And that’s the basics of race charts for you. Let’s wrap things up next.

Summing up Race Charts in R

Data visualization is a powerful tool. Given the option, almost no one would choose to stare at spreadsheets instead of charts. Animating these charts brings the whole thing to a different dimension.

You now know how to breathe life into static and somewhat boring ggplot2 visualizations, which is an amazing skill to have. Animated charts make it easy to capture trends and shifts over time, ensuring your data stories are more engaging and easier to understand. They are especially useful in presentations and reports where you need to highlight key changes and patterns dynamically, rather than relying on a series of static images.

What are your thoughts on chart animation in R? Do you use gganimate or some other package? Let us know in our Slack community.

Are you hitting the wall with Excel? Here are 5 ways R can help you improve your business workflows.

Have questions or insights?

Engage with experts, share ideas and take your data journey to the next level!

Sign up for ShinyWeekly

Join 4,2k explorers and get the Shiny Weekly Newsletter into your mailbox
for the latest in R/Shiny and Data Science.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Explore Possibilities

Share Your Data Goals with Us

From advanced analytics to platform development and pharma consulting, we craft solutions tailored to your needs.

Talk to our Experts
r
data visualization
tutorials