Making animated charts with gganimate

What’s better than making charts? Making them animated, of course! While animation isn’t always (ok, rarely) necessary, it can help tell a story static charts can’t.

Recently there has been a lot of discussion in Canada related to equalization. This is a federal program to transfer funds to provinces with below average fiscal capacity. What is “fiscal capacity”? Others can tell you a lot more about it, but for simplicity, we can think of it like GDP.

So I wanted to take a look at GDP per capita, by province, going back several years. A little learning, a lot of online digging, and I was able to create this nice chart:

People seemed to like it and some wanted to know more about how it was made. This post explains how to make an animated chart like the tweeted one above using gganimate.

Let’s go!

To begin, let’s load our usual packages, but add in the latest version of gganimate from David Robinson which has been updated by Thomas Lin Pedersen. Thomas has added some new syntax which allows for really nice transitions to the animation.

library(tidyverse)
library(lubridate)
library(ggthemes)
library(ggrepel)

# Package to pull in cansim data, courtesy of Jens von Bergman and Dmitry Shkolnik
library(cansim) 

# Now update to the newest gganimate version
#install.packages("devtools")
#devtools::install_github('thomasp85/gganimate')
library(gganimate)

With packages loaded, it’s time to wrangle the data. I could probably do this cleaner, but meh - it works! I wrote a function below to change the full province names to their 2 character abbreviations that we will use in the plots.

Let’s start by pulling in annual GDP data by province and to test it’s working do a quick plot.

provlist <- c("British Columbia","Alberta","Saskatchewan","Manitoba","Ontario","Quebec","New Brunswick","Nova Scotia","Prince Edward Island","Newfoundland and Labrador")

short_provs <- function(df){
  df <- df %>%
    filter(GEO %in% c("British Columbia","Alberta","Saskatchewan","Manitoba","Ontario","Quebec","New Brunswick","Nova Scotia","Prince Edward Island","Newfoundland and Labrador"))
  df <- df %>%
    mutate(GEO.short = case_when(
      GEO=="British Columbia" ~ "BC",
      GEO=="Alberta" ~ "AB",
      GEO=="Saskatchewan" ~ "SK",
      GEO=="Manitoba" ~ "MB",
      GEO=="Ontario" ~ "ON",
      GEO=="Quebec" ~ "QC",
      GEO=="New Brunswick" ~ "NB",
      GEO=="Prince Edward Island" ~ "PE",
      GEO=="Nova Scotia" ~ "NS",
      GEO=="Newfoundland and Labrador" ~ "NL"
    ))
  df$GEO.short <- factor(df$GEO.short, levels=c("BC","AB","SK","MB","ON","QC","NB","PE","NS","NL"))
}

GDP <- get_cansim(36100222) %>% normalize_cansim_values()
df.GDP <- GDP %>%
  mutate(year=as.integer(REF_DATE)) %>%
  filter(GEO %in% provlist,
         Prices=="Chained (2012) dollars",
         Estimates=="Gross domestic product at market prices") %>%
  dplyr::select(GEO,year,GDP=VALUE)
df.GDP$GEO.short<-short_provs(df.GDP)

ggplot(df.GDP, aes(GEO.short, GDP/1e9, fill = GEO.short, label=GEO.short)) +
  geom_col()+
  geom_text(vjust=-.1)+
  scale_fill_viridis_d(name="")+
  scale_y_continuous(labels=scales::dollar)+
  theme_tufte(14,"Avenir")+
  theme(legend.position = "none",
        axis.text.x = element_blank(),
        axis.ticks = element_blank())+
  # Here comes the gganimate specific bits
  labs(title = "Year: {frame_time}", x = "", y = "GDP (2012$ billions") +
  transition_time(year) +
  ease_aes('linear')        

Ok. Good! We have an animated chart! But it’s not too informative given the BIG and small size of provinces. So to look at per capita GDP we need to pull in annual population data and merge it with our GDP data.

pop <- get_cansim(17100005) %>% normalize_cansim_values()
df.pop <- pop %>%
  mutate(year=as.integer(REF_DATE)) %>%
  filter(GEO %in% provlist,
         Sex=="Both sexes",
         `Age group`=="All ages") %>%
  dplyr::select(GEO,year,pop=VALUE)

df.GDPpercap <-left_join(df.GDP,df.pop, by=c("year","GEO")) %>%
  mutate(GDPpercap=GDP/pop)

df.GDPpercap$GEO.short<-short_provs(df.GDPpercap)

Now, let’s create our plot.

I’m going to use the animate function here so that I can change the speed of the animation a bit, and add a pause at the end.

p<-ggplot(df.GDPpercap, aes(GEO.short, GDPpercap, fill=GEO.short, label=GEO.short)) +
  geom_col()+
  geom_text(vjust=-.1)+
  scale_fill_viridis_d(name="")+
  theme_tufte(14,"Avenir")+
  theme(legend.position = "none",
        axis.text.x = element_blank(),
        axis.ticks = element_blank())+
  # Here comes the gganimate specific bits
  labs(title = 'Year: {frame_time}', x = '', y = 'GDP per capita ($2012)') +
  transition_time(year) +
  ease_aes('linear')  
animate(p, fps=5, end_pause = 30) # Use anim_save(filename) to save the plot

Not bad. But we can do better! It would be fun to get the provinces to rank themselves every frame. And let’s flip it horizontally too to look like they’re climbing over one another.

A little digging on stackoverflow took me to this post by Jon Spring: (https://stackoverflow.com/questions/52623722/how-does-gganimate-order-an-ordered-bar-time-series)

Basically, we add a rank variable, grouping first by year and ordering the provinces. We will use this as our x axis (vertical axis in the flipped coordinates). We will also use geom_tile instead of geom_col because this object creates smoother transitions in the size of the bars.

The tweeted chart!

plotdata <- df.GDPpercap %>%
  group_by(year) %>%
  mutate(ordering = rank(GDPpercap)) %>%
  ungroup() 

p<-ggplot(plotdata,
       aes(ordering, group = GEO.short,color=GEO.short,fill=GEO.short)) +
  geom_tile(aes(y = GDPpercap/2, 
                height = GDPpercap,
                width = 0.9), alpha = 0.9) +
  # text on top of bars
  geom_text(aes(y = GDPpercap, label = GEO.short), hjust = -0.4) +
  # text in x-axis (requires clip = "off" in coord_cartesian)
  geom_text(aes(y = 0, label = GEO.short), hjust = 1.4) +
  coord_flip(clip = "off", expand = FALSE) +
  scale_color_viridis_d(name="")+
  scale_fill_viridis_d(name="")+
  scale_y_continuous(labels=scales::dollar)+
  theme_tufte(14,"Avenir")+
  guides(color=F,fill=F)+
  labs(title='{frame_time}', x = "",y="GDP per capita ($2012)") +
  theme(plot.title = element_text(hjust = 1, size = 22),
        axis.ticks.y = element_blank(),
        axis.text.y  = element_blank()) + 
  transition_time(year)+
  ease_aes('cubic-in-out')

animate(p, nframes = 100, fps = 5, end_pause = 20) #again, use anim_save(filename) to save

Cool. If i do say so myself :-)

But wait, we can do more! Rather than bars, let’s plot points as in a scatter plot and watch them float over time. We can start by looking at the co-movement of GDP per capita and unemployment rates across provinces over time. I pull in the annual LFS data from NDM 14-10-0090, merging the annual unemployment rate values with our GDP per capita data.

Let’s chart it!

p<-ggplot(df.full, 
       aes(GDPpercap,Unemp_Rate,
           color=GEO.short, label=GEO.short)) +
  geom_point(aes(size=pop))+
  geom_text(hjust=-.2,vjust=-.2)+
  scale_color_viridis_d(name="")+
  scale_y_continuous(labels=scales::percent)+
  scale_x_continuous(labels=scales::dollar)+
  theme_tufte(14,"Avenir")+
  theme(legend.position = "none",
        plot.caption = element_text(size=9,face="italic"))+
  guides(size=F,color=F)+
  # Here comes the gganimate specific bits
  labs(title = 'Year: {frame_time}',
       x = 'GDP per capita ($2012)',
       y = "Unemployment Rate",
       caption="Bubble size proportional to population\nChart by @bcshaffer") +
  transition_time(year) +
  ease_aes('linear')  
animate(p, nframes = 100, fps = 10, width = 600, height = 400, end_pause = 30)

Lastly, we can do the same with the employment rate. This time leads add some bread crumbs behind our roving points to get some history.

p<-ggplot(df.full, 
       aes(GDPpercap,Emp_Rate,
           color=GEO.short, label=GEO.short)) +
  geom_point(aes(size=pop))+
  geom_text(hjust=-.2,vjust=-.2)+
  scale_color_viridis_d(name="")+
  scale_y_continuous(labels=scales::percent)+
  scale_x_continuous(labels=scales::dollar)+
  theme_tufte(14,"Avenir")+
  theme(legend.position = "none",
        plot.caption = element_text(size=9,face="italic"))+
  guides(size=F,color=F)+
  # Here comes the gganimate specific bits
  labs(title = 'Year: {frame_time}',
       x = 'GDP per capita ($2012)',
       y = "Employment Rate",
       caption="Bubble size proportional to population\nChart by @bcshaffer") +
  transition_time(year) +
  shadow_wake(wake_length = 1, exclude_layer = 2)+
  ease_aes('cubic-in-out')
  #ease_aes('linear')  
animate(p, nframes = 100, fps = 10, width = 600, height = 400, end_pause = 30)

Happy charting!

-Blake