Animated cards with {ggplot2} and {gganimate} | R-Bloggers

Animated cards with {ggplot2} and {gganimate} | R-Bloggers

6 minutes, 12 seconds Read



In this blog post we will be information from the
{Gapminder}
R -package, together with global spatial boundaries of
‘Opendatasoft’. We will plott the life expectancy of every country in North and South America to see the changes from 1957 to 2007.

The {gapminder} package that we use is from the
Gap Foundation, an independent educational non-project that covers global misconceptions. The cover problems such as global warming, plastic in the oceans and satisfaction of life.

First we will load the entire data set from the Gapminder package and see what is in it.

data("gapminder_unfiltered", package = "gapminder")
names(gapminder_unfiltered)

## [1] "country" "continent" "year" "lifeExp" "pop" "gdpPercap"

We will then filter the dataset to store the life expectation data for the years from 1952 to 2007 (in steps of 5 years).

A shapefile (*.shp)) With the geographical boundaries of each country, it can be imported using the {sf} R -package.

library(sf)
library(dplyr)
if (getwd() == "/home/osheen/corporate-website"){
 world = st_read("content/blog/2025-animated-map/data/world-administrative-boundaries.shp") |>
 select(-"continent")
} else {
 world = st_read("data/world-administrative-boundaries.shp") |>
 select(-"continent")

}

## Reading layer `world-administrative-boundaries' from data source
## `/home/osheen/corporate-website/content/blog/2025-animated-map/data/world-administrative-boundaries.shp'
## using driver `ESRI Shapefile'
## Simple feature collection with 256 features and 8 fields
## Geometry type: MULTIPOLYGON
## Dimension: XY
## Bounding box: xmin: -180 ymin: -58.49861 xmax: 180 ymax: 83.6236
## Geodetic CRS: WGS 84

head(world)

## Simple feature collection with 6 features and 7 fields
## Geometry type: MULTIPOLYGON
## Dimension: XY
## Bounding box: xmin: -58.43861 ymin: -34.94382 xmax: 148.8519 ymax: 51.09111
## Geodetic CRS: WGS 84
## iso3 status color_code name
## 1 MNP US Territory USA Northern Mariana Islands
## 2  Sovereignty unsettled RUS Kuril Islands
## 3 FRA Member State FRA France
## 4 SRB Member State SRB Serbia
## 5 URY Member State URY Uruguay
## 6 GUM US Non-Self-Governing Territory GUM Guam
## region iso_3166_1_ french_shor
## 1 Micronesia MP Northern Mariana Islands
## 2 Eastern Asia  Kuril Islands
## 3 Western Europe FR France
## 4 Southern Europe RS Serbie
## 5 South America UY Uruguay
## 6 Micronesia GU Guam
## geometry
## 1 MULTIPOLYGON (((145.6333 14...
## 2 MULTIPOLYGON (((146.6827 43...
## 3 MULTIPOLYGON (((9.4475 42.6...
## 4 MULTIPOLYGON (((20.26102 46...
## 5 MULTIPOLYGON (((-53.3743 -3...
## 6 MULTIPOLYGON (((144.7094 13...

One of the fun things of the {sf} The package is that the geographical data stores in a specialized data frame structure with which we can merge our border data with the Gapminder statistics using the same functions that we would use to combine more typical data frames. Here we become a member
DYR left_join function.

joined = left_join(gapminder_unfiltered,
 world,
 by = c("country" = "name")) |>
 st_as_sf()
head(joined)

## Simple feature collection with 6 features and 12 fields
## Geometry type: MULTIPOLYGON
## Dimension: XY
## Bounding box: xmin: 60.50417 ymin: 29.40611 xmax: 74.91574 ymax: 38.47198
## Geodetic CRS: WGS 84
## # A tibble: 6 × 13
## country continent year lifeExp pop gdpPercap iso3 status color_code
##         
## 1 Afghanistan Asia 1952 28.8 8425333 779. AFG Membe… AFG
## 2 Afghanistan Asia 1957 30.3 9240934 821. AFG Membe… AFG
## 3 Afghanistan Asia 1962 32.0 10267083 853. AFG Membe… AFG
## 4 Afghanistan Asia 1967 34.0 11537966 836. AFG Membe… AFG
## 5 Afghanistan Asia 1972 36.1 13079460 740. AFG Membe… AFG
## 6 Afghanistan Asia 1977 38.4 14880372 786. AFG Membe… AFG
## # ℹ 4 more variables: region , iso_3166_1_ , french_shor ,
## # geometry 

I’m going to select the land column and plot it using the basic r
plot Function for fast visualization.

joined |>
 select("country") |>
 plot()

Map of the world where some countries are missing.

Hmmmmmmm that doesn’t look good, right?

The problem here is a common when grabbing a spatial boundaries of the internet. The data sets that are connected have different names for some countries. For example in the world Data We have US as ‘United States’ where as in gapminder It is ‘United States of America’. The dplyr::anti_join Function can be useful to find countries that do not match. I will use fct_recode from {forcats} to the world
land names with gapminder. In the example below I am only repairing the US, but you can see from the above plot that various other countries have to be coded again (19 in total), I do this behind the scenes to prevent you from hiding the page.

library(forcats)
world = world |>
 mutate(name = fct_recode(.data$name,
 "United States" =
 "United States of America"))

Okay, let’s see what this looks like now.

joined |>
 select("country") |>
 plot()

Map of the world with all countries.

That’s better! Now I have the data I want to plot, I can use GGPLOT2 to make the visualization that I will animate. For that I will filter the data to only keep America and I use it
geom_sf To plot the geometry data.

library(ggplot2)

americas = joined |>
 filter(continent == "Americas")

americas_plot = ggplot(americas) +
 geom_sf()

Map of America.

This plot looks good, but I am going to change the coordinate reference system (CRS) into one (“EPSG: 8858”) that is designed for America. I found these CRS Epsg.ioA website that I would recommend if you are looking for different CRSs. st_transform
Can be used to change the CRS to EPSG: 8858. This is what it looks like now:

americas = st_transform(americas, "EPSG:8858")

new_crs_plot = ggplot(americas) +
 geom_sf()

Map of America with EPSG: 8858 CRS.

Okay, so now the plot looks good, we are starting to prepare to be animated.

library(ggplot2)

plot = americas %>%
 filter(year == 2007) %>%
 ggplot() +
 geom_sf(aes(fill = lifeExp)) +
 labs(title = "Year: 2007",
 fill = "Life Expectancy") +
 theme_void() +
 ggplot2::scale_fill_viridis_b() +
 theme(legend.position = c("inside"),
 legend.position.inside = c(0.23, 0.23),
 plot.title = element_text(size = 15,
 hjust = 0.5),
 panel.border = element_rect(color = "black",
 fill = NA))

Map of America to be animated.

This is the plot that we are going to animate now, so we will use {gganimate}. The transition_states function partitions the data using a states
Column (here our ‘year’ column), make a frame of the animation for each previous value in the input data. The next function is
animate who converts these frames into a poison. Please note, make sure you have installed the dependencies or that you can end with 100 PNG files in your workbook instead of a gif!

library(gganimate)

animation = plot +
 ggtitle("Year: {closest_state}") +
 transition_states(states = year)

animate(animation,
 renderer = gifski_renderer("img/map.gif"),
 alt = "Animation with missing values.")

Animation with missing values.

The sharper eyes of you will notice that some countries have no value for each year.

americas |>
 st_drop_geometry() |>
 count(country) |>
 arrange(n)

## # A tibble: 36 × 2
## country n
##  
## 1 French Guiana 1
## 2 Guadeloupe 1
## 3 Martinique 1
## 4 Aruba 8
## 5 Grenada 8
## 6 Netherlands Antilles 8
## 7 Suriname 8
## 8 Bahamas 10
## 9 Barbados 10
## 10 Belize 10
## # ℹ 26 more rows

So 25 countries have 12 observations (the Max), four have 10 and 8 and three respectively have 1. To fill in these spaces, I will use {tidyr} to calculate some fake values using the data set average for each year. The countries with one would continue with one value from 2002.

library(tidyr)

completed = americas |>
 mutate(country = forcats::fct_drop(country)) |>
 complete(year, country) |>
 select(country, lifeExp, year) |>
 group_by(year) |>
 mutate(lifeExp =
 replace_na(lifeExp,
 replace = mean(lifeExp,
 na.rm = TRUE)))

geoms = americas |>
 select(country) |>
 distinct()

plot = left_join(completed,
 geoms,
 by = "country") |>
 st_as_sf() |>
 st_transform("EPSG:8858") |>
 ggplot() +
 geom_sf(aes(fill = lifeExp)) +
 labs(title = "Year: {closest_state}",
 fill = "Life Expectancy") +
 theme_void() +
 ggplot2::scale_fill_viridis_b() +
 theme(legend.position = c("inside"),
 legend.position.inside = c(0.23, 0.23),
 plot.title = element_text(size = 15,
 hjust = 0.5),
 panel.border = element_rect(color = "black",
 fill = NA))

animation = plot +
 transition_states(states = year)

animate(animation,
 renderer = gifski_renderer("img/map2.gif"))

Final animation with all countries.

So that is our last animated card, of course we can add more styling or complexity – perhaps in a future blog. If you want to know more about the subject, view our Spatial data analysis with R -course
or another blog from Jumping Rivers, Think of cards and ice cream

by Nicola Rennie.

See the Original post


#Animated #cards #ggplot2 #gganimate #RBloggers

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *