What is this?

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.


Packages

# 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 formatting

The data

The gapminder package ships with a clean, ready-to-use dataset. No downloading required.

head(gapminder, 12)
## # 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)

Building the chart step by step

Step 1 — the basic scatter

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.

Step 2 — improve the axes

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.

Step 3 — label the big ones

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()

Step 4 — dark theme + colors

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_theme

Step 5 — animate it

gganimate 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()).


The result


What the chart shows

  • Africa (red) clusters bottom-left in 1952 — low income, low life expectancy — and moves right and up over 55 years, but more slowly than other continents.
  • Asia (blue) shows the most dramatic movement. South Korea, Singapore, and China all make huge leaps.
  • Europe (green) starts already in the upper-right and pushes further right.
  • The gap between rich and poor narrows dramatically — the world Rosling called “two humps” in 1952 becomes one.

Customising it

Change which countries get labels

big_countries <- c("China", "India", "United States", "Germany", "Japan")

Change the speed

animate(p, nframes = 60, fps = 6, ...)   # slower
animate(p, nframes = 240, fps = 24, ...) # smoother

Filter to one continent

gapminder_asia <- gapminder[gapminder$continent == "Asia", ]

# then replace gapminder with gapminder_asia in the ggplot() call

Save as MP4 instead of GIF

# install.packages("av")
animate(p, renderer = av_renderer("gapminder.mp4"), fps = 24, width = 1280, height = 720)

Further reading


Rendered with R 4.6.1 · ggplot2 · gganimate · gapminder