There’s been some great animated maps in the data viz world of late. Most notably this stunner by John Muyskens for the Washington Post, showing the diverted flight paths of planes getting themselves into the line of the recent solar eclipse. What’s more it was made with R and ggplot2! Have a look here:

The beauty of animation in any viz is it’s ability to show time series as a literal time series (you can watch it unfold before your very eyes), removing the need to show it moving along the x-axis. This frees up the x-axis for something else, how liberating! As a map obviously requires both the x + y to plot lat/lon coordinates, animated mapping is our only option of combining cartography with time-series.

So let’s see how it can be done utilising an R toolkit of ggplot2, the wonderful Simple Features (sf) package, and gganimate to create great looking maps with minimal amounts of code.

As a football (soccer) fan, I’ve chosen a simple dataset of FIFA World Cup winners, runners-up and final locations to work with. The only amendments I made to the wikipedia table were due to a couple countries in the dataset no longer existing: West Germany and Czechoslovakia.

For simplicity of matching these to closest modern day equivalent (which is required to match with polygons in our shapefile) West Germany becomes Germany and Czechoslovakia becomes Czech Rep. (sorry if this causes offence to any Slovaks or East Germans!). The cleaned up dataset is downloadable in the code below.

I’ve put the code in one big block below with comments. It should be fully reproduceable with the right packages installed so you can copy it into R studio and make the map yourself, or even try it with a different dataset if you’re feeling adventurous!


library(tidyverse) # dev ggplot version required: devtools::install_github("hadley/ggplot2")
library(gganimate) # devtools::install_github("dgrtwo/gganimate")
library(hrbrthemes) # devtools::install_github("hrbrmstr/hrbrthemes")

# download the natural earth shapefile we need into your working directory
URL <- ""
temp <- tempfile()
download.file(URL, temp)

# read in shapefile as an sf object and set the projection
# this will be our base world map for plot sans Antarctica
world <- st_read("ne_110m_admin_0_map_units.shp") %>% 
  st_transform(crs = "+proj=longlat +datum=WGS84") %>% 
  filter(!name %in% c("Fr. S. Antarctic Lands", "Antarctica"))

# download dataset into your working directory
url <- ""
GET(url, write_disk("wc.xlsx", overwrite=TRUE))

# read in our the massive 20 rows of data and get the winner/runner-up variable in 1 column #tidyafdata
# setting factor for winner to show first in the legend
winners <- read_excel("wc.xlsx") %>% 
  gather(w_l, country, winner:runner_up) %>% 
  mutate(w_l = factor(w_l, levels = c("winner", "runner_up")))

# merge our world shape file with our main dataset
# this will add the polygon for the appropriate country to each row of our winners dataset
# and remove any countries that haven't won or come 2nd in the WC
wc_geo <- merge(world, winners, by.x = "name", by.y = "country")

# get the lon/lat coordinates of the world cup final locations via ggmap::geocode
# add year and final placenames from winners data then transorm into an sf object with same proj as basemap
# finally mutate 2 columns for lon/lat (x + y) to use for plotting text
locations_sf <- geocode(winners[1:20,]$Location) %>% 
  cbind(winners[1:20, 1:2]) %>% 
  st_as_sf(coords = c("lon", "lat"), crs = "+proj=longlat +datum=WGS84", agr = "constant") %>% 
  mutate(x = sapply(geometry, "[[", 1), y = sapply(geometry, "[[", 2))

# plot base map + filtered map with fill on winner/runner-up variable and frame as year for animation
# then a point at each final location along with the place name text
# set the projection and all the theme commands to give it a dark and mysterious aesthetic
wc_map <- ggplot() +
  geom_sf(data = world, colour = "#ffffff20", fill = "#2d2d2d60", size = .5) +
  geom_sf(data = wc_geo, aes(fill = w_l, frame = Year)) +
  geom_sf(data = locations_sf, aes(frame = Year), size = .2, colour = "#ffffff90") +
  geom_text(data = locations_sf, aes(x, y, label = Location, frame = Year), 
            colour = "white", fill = "#00000040", nudge_y = -5) +
  coord_sf(crs = st_crs(world), datum = NA) +
  labs(title = "FIFA World Cup Winners, Runners Up & Final Locations", x=NULL, y=NULL,
       caption = "Culture of Insight / @paulcampbell91 / Source: Wikipedia") +
  theme_modern_rc(axis = FALSE, base_size = 16, caption_size = 18) +
  scale_fill_manual(values = c("#D9A441", "#A8A8A8"), name = NULL, labels = c("Winner", "Runner-Up")) +
  theme(legend.position = c(0.9, 1.01), legend.direction = "horizontal", axis.text = element_blank(), 
        panel.grid.minor = element_blank(), panel.grid.major = element_blank())

# set animation interval as 2 seconds, create the gif, and Robert's your Father's brother
animation::ani.options(interval = 2)
gganimate(wc_map, ani.width =  1250, ani.height = 585, "wc.gif", title_frame = TRUE)

If all went to plan we should have a moving map showing the footballing dominance of the South American and European continents:


Mad respect to Edzer Pebesma and the whole r-spatial crew for all their work. What a time to be alive and making maps with R.

Also to David Robinson for making ggplot animation so easy with the gganimate package.

Drop a note in the comments or on twitter if you have any questions about the code. I learnt a lot of it myself to make this map but I’ll try my best to explain.