In 2006, Hans Rosling gave a TED talk that changed how people think about global development. His animated bubble chart showed 200 years of data in 4 minutes, revealing that the world is far less divided between “rich” and “poor” than most people assume.
This tutorial shows you how to recreate that chart in R using freely available packages — no API keys, no paid data, just a few lines of code.
# install.packages(c("gapminder", "ggplot2", "gganimate", "gifski", "scales"))
library(gapminder) # the data
library(ggplot2) # plotting
library(gganimate) # animation
library(gifski) # GIF renderer
library(scales) # axis formattingThe gapminder package ships with a clean, ready-to-use
dataset. No downloading required.
## # A tibble: 12 × 6
## country continent year lifeExp pop gdpPercap
## <fct> <fct> <int> <dbl> <int> <dbl>
## 1 Afghanistan Asia 1952 28.8 8425333 779.
## 2 Afghanistan Asia 1957 30.3 9240934 821.
## 3 Afghanistan Asia 1962 32.0 10267083 853.
## 4 Afghanistan Asia 1967 34.0 11537966 836.
## 5 Afghanistan Asia 1972 36.1 13079460 740.
## 6 Afghanistan Asia 1977 38.4 14880372 786.
## 7 Afghanistan Asia 1982 39.9 12881816 978.
## 8 Afghanistan Asia 1987 40.8 13867957 852.
## 9 Afghanistan Asia 1992 41.7 16317921 649.
## 10 Afghanistan Asia 1997 41.8 22227415 635.
## 11 Afghanistan Asia 2002 42.1 25268405 727.
## 12 Afghanistan Asia 2007 43.8 31889923 975.
It covers 142 countries across 5 continents, with observations every 5 years from 1952 to 2007.
Each row is one country in one year, with:
| Column | Description |
|---|---|
country |
Country name |
continent |
Continent |
year |
Year (every 5 years) |
lifeExp |
Life expectancy at birth |
pop |
Population |
gdpPercap |
GDP per capita (inflation-adjusted USD) |
Start with a single year to get the layout right before adding animation.
gapminder_2007 <- gapminder[gapminder$year == 2007, ]
ggplot(gapminder_2007,
aes(x = gdpPercap, y = lifeExp, size = pop, color = continent)) +
geom_point(alpha = 0.7) +
scale_x_log10() +
labs(title = "2007", x = "GDP per capita", y = "Life expectancy") +
theme_minimal()Three things are already encoded: wealth (x), health (y), and population (bubble size). Continent gets color.
GDP per capita spans from ~$200 to ~$100,000. A log scale is essential — on a linear scale, poor countries are crushed into the left margin and you lose all the variation that matters.
ggplot(gapminder_2007,
aes(x = gdpPercap, y = lifeExp, size = pop, color = continent)) +
geom_point(alpha = 0.75) +
scale_x_log10(
breaks = c(500, 1000, 2000, 5000, 10000, 50000),
labels = label_dollar(scale_cut = cut_short_scale())
) +
scale_size(range = c(2, 24), guide = "none") +
labs(title = "2007", x = "GDP per capita (log scale)", y = "Life expectancy") +
theme_minimal()range = c(2, 24) in scale_size maps the
smallest population to a 2px bubble and the largest (China, India) to
24px. The guide = "none" removes the size legend — it’s not
readable anyway.
big_countries <- c("China", "India", "United States", "Brazil", "Nigeria")
ggplot(gapminder_2007,
aes(x = gdpPercap, y = lifeExp, size = pop, color = continent)) +
geom_point(alpha = 0.75) +
geom_text(
data = \(d) d[d$country %in% big_countries, ],
aes(label = country),
size = 3.2, vjust = -1.1, show.legend = FALSE
) +
scale_x_log10(
breaks = c(500, 1000, 2000, 5000, 10000, 50000),
labels = label_dollar(scale_cut = cut_short_scale())
) +
scale_size(range = c(2, 24), guide = "none") +
labs(title = "2007", x = "GDP per capita (log scale)", y = "Life expectancy") +
theme_minimal()continent_colors <- c(
Africa = "#E74C3C",
Americas = "#F39C12",
Asia = "#3498DB",
Europe = "#2ECC71",
Oceania = "#9B59B6"
)
dark_theme <- theme_minimal(base_size = 13) +
theme(
plot.background = element_rect(fill = "#0F1923", color = NA),
panel.background = element_rect(fill = "#0F1923", color = NA),
panel.grid.major = element_line(color = "#1E2D3D"),
panel.grid.minor = element_blank(),
text = element_text(color = "white"),
axis.text = element_text(color = "#90A4AE"),
legend.position = "bottom",
legend.text = element_text(size = 11),
plot.caption = element_text(color = "#546E7A", size = 8, hjust = 1)
)
ggplot(gapminder_2007,
aes(x = gdpPercap, y = lifeExp, size = pop, color = continent)) +
geom_point(alpha = 0.75, stroke = 0.2) +
geom_text(
data = \(d) d[d$country %in% big_countries, ],
aes(label = country),
size = 3.2, vjust = -1.1, show.legend = FALSE
) +
scale_x_log10(
breaks = c(500, 1000, 2000, 5000, 10000, 50000),
labels = label_dollar(scale_cut = cut_short_scale())
) +
scale_size(range = c(2, 24), guide = "none") +
scale_color_manual(
values = continent_colors,
guide = guide_legend(override.aes = list(size = 5))
) +
labs(
title = "2007",
x = "GDP per capita (log scale)",
y = "Life expectancy at birth",
color = NULL,
caption = "Source: Gapminder | Bubble size = population"
) +
dark_themegganimate extends ggplot2 with transition_*
functions. transition_time(year) moves through the
year variable. shadow_mark() leaves a faint
ghost trail so you can see where countries have been.
p <- ggplot(gapminder,
aes(x = gdpPercap, y = lifeExp, size = pop, color = continent)) +
geom_point(alpha = 0.75, stroke = 0.2) +
geom_text(
data = \(d) d[d$country %in% big_countries, ],
aes(label = country),
size = 3.2, vjust = -1.1, show.legend = FALSE
) +
scale_x_log10(
breaks = c(500, 1000, 2000, 5000, 10000, 50000),
labels = label_dollar(scale_cut = cut_short_scale())
) +
scale_size(range = c(2, 24), guide = "none") +
scale_color_manual(
values = continent_colors,
guide = guide_legend(override.aes = list(size = 5))
) +
labs(
title = "{as.integer(frame_time)}", # <-- the magic: year as title
x = "GDP per capita (log scale)",
y = "Life expectancy at birth",
color = NULL,
caption = "Source: Gapminder | Bubble size = population"
) +
dark_theme +
theme(plot.title = element_text(size = 54, face = "bold", hjust = 0.5)) +
transition_time(year) + # animate over year
ease_aes("linear") + # constant speed between frames
shadow_mark(alpha = 0.06, size = 0.4) # ghost trail
animate(p,
nframes = 120,
fps = 12,
width = 900,
height = 650,
renderer = gifski_renderer("gapminder.gif"))Note: rendering takes ~1–2 minutes. The GIF will appear in your working directory (
getwd()).
Rendered with R 4.6.1 · ggplot2 · gganimate · gapminder