Recreating Hans Rosling’s Bubble Chart in R

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

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 formatting

data(gapminder)

The data

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

Code
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.

Code
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.

Code
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

Code
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

Code
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.

Code
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,
        units    = "px",
        res      = 96,
        renderer = gifski_renderer("gapminder.gif"))

Note: rendering takes ~1–2 minutes. The GIF will appear in your working directory (getwd()).


The result

Code
# knitr::include_graphics("gapminder.gif")

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

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

Change the speed

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

Filter to one continent

Code
gapminder_asia <- gapminder[gapminder$continent == "Asia", ]
# then replace gapminder with gapminder_asia in the ggplot() call

Save as MP4 instead of GIF

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