The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

Population pyramids with ggpyramid

A population pyramid shows a population by age group and sex as two bar charts set back to back, with the age groups stacked from youngest at the bottom to oldest at the top. ggpyramid builds it from ordinary ggplot2 layers, so scales, facets, themes and further layers work as they do in any other plot.

library(ggplot2)
library(ggpyramid)

The examples use europe_pop, the population of four European countries in 2004 and 2024 by five-year age group and sex, from Eurostat:

head(europe_pop)
#>   country year   age  sex population
#> 1  France 2004   0-4 Male    2003057
#> 2  France 2004   5-9 Male    1955935
#> 3  France 2004 10-14 Male    2029376
#> 4  France 2004 15-19 Male    2089909
#> 5  France 2004 20-24 Male    2005783
#> 6  France 2004 25-29 Male    1918838

A first pyramid

geom_pyramid() draws the bars. Map age to y, the side to fill, and the population to x. coord_pyramid() labels both halves of the value axis without a minus sign and gives them the same range.

france <- subset(europe_pop, country == "France" & year == 2024)

ggplot(france, aes(x = population, y = age, fill = sex)) +
  geom_pyramid() +
  scale_x_continuous(labels = scales::label_comma()) +
  coord_pyramid()

The first level of the side variable goes on the left, so Male is drawn on the left here because it is the first level of sex. left in coord_pyramid() chooses the other side for every layer of the plot at once. The legend keeps the order of the levels, and breaks in the fill scale reorders it to match:

ggplot(france, aes(x = population, y = age, fill = sex)) +
  geom_pyramid() +
  scale_x_continuous(labels = scales::label_comma()) +
  scale_fill_discrete(breaks = c("Female", "Male")) +
  coord_pyramid(left = "Female")

A single layer can take another side with position_pyramid(left = ...), which overrides the coordinate system. Without coord_pyramid(), as in a vertical pyramid, reorder the levels of the side variable instead.

geom_pyramid() sums the value it is given within each age group, and counts rows when no value is mapped, so the same code works for aggregated tables and for individual records:

# One row per person
ggplot(people, aes(y = age_group, fill = sex)) +
  geom_pyramid()

Placing the age axis

coord_pyramid() places the age axis on the "left" (the default), on the "right", on "both" sides, or in the "centre", where the labels sit in a gap between the two halves. theme_pyramid() removes the grid lines that run along the bars and moves the legend to the top.

p <- ggplot(france, aes(x = population, y = age, fill = sex)) +
  geom_pyramid(width = 1, colour = "white") +
  scale_x_continuous(
    labels = scales::label_number(scale = 1e-6, suffix = "M")
  ) +
  labs(x = NULL, y = NULL, fill = NULL) +
  theme_pyramid()

p + coord_pyramid(age_axis = "centre")

The width of the gap is a proportion of the panel, 0.12 by default. Widen it with gap when the age labels are long, or open a narrow gap with the age axis elsewhere. A gap draws the zero of the value axis on both of its edges, so keep it wide enough for two labels:

p + coord_pyramid(age_axis = "both", gap = 0.08)

Proportions and facets

denominator = "total" shows each bar as a share of the whole population in the panel, and denominator = "side" as a share of its own side. Shares make populations of different sizes comparable, and each facet panel gets its own denominator.

ggplot(europe_pop, aes(x = population, y = age, fill = sex)) +
  geom_pyramid(denominator = "total") +
  scale_x_continuous(labels = scales::label_percent()) +
  coord_pyramid() +
  facet_grid(year ~ country) +
  labs(x = "Share of the population", y = NULL, fill = NULL) +
  theme_pyramid()

Labels

geom_pyramid_text() writes the value of each bar at its end. It aggregates the data in the same way as the bars, and labels formats the numbers. Labels go "outside" the bars by default, or "inside" them.

ireland <- subset(europe_pop, country == "Ireland" & year == 2024)

ggplot(ireland, aes(x = population, y = age, fill = sex)) +
  geom_pyramid(denominator = "side") +
  geom_pyramid_text(
    denominator = "side",
    labels = scales::label_percent(accuracy = 0.1),
    size = 2.6
  ) +
  scale_x_continuous(
    labels = scales::label_percent(),
    expand = expansion(mult = 0.15)
  ) +
  coord_pyramid(age_axis = "centre") +
  labs(x = "Share of each sex", y = NULL, fill = NULL) +
  theme_pyramid()

Comparing two populations

geom_pyramid_outline() traces the edge of a pyramid as a stepped line. Over the bars of one year it shows how the population has changed since another. Here Italy in 2024 is drawn as bars and Italy in 2004 as the outline.

italy <- subset(europe_pop, country == "Italy")

ggplot(italy, aes(x = population, y = age, fill = sex)) +
  geom_pyramid(data = subset(italy, year == 2024), width = 1, alpha = 0.8) +
  geom_pyramid_outline(data = subset(italy, year == 2004), linewidth = 0.6) +
  scale_x_continuous(
    labels = scales::label_number(scale = 1e-6, suffix = "M")
  ) +
  coord_pyramid(age_axis = "centre") +
  labs(
    x = NULL,
    y = NULL,
    fill = "2024",
    caption = "The outline shows 2004."
  ) +
  theme_pyramid()

Subgroups within each side

The side of a bar comes from fill unless the side aesthetic is mapped. Mapping side separately frees fill for subgroups, which are stacked within each side:

recent <- subset(europe_pop, year == 2024 & country %in% c("Ireland", "Poland"))

ggplot(recent, aes(x = population, y = age, fill = country, side = sex)) +
  geom_pyramid(width = 1) +
  scale_x_continuous(
    labels = scales::label_number(scale = 1e-6, suffix = "M")
  ) +
  coord_pyramid(age_axis = "centre") +
  labs(x = NULL, y = NULL, fill = NULL) +
  theme_pyramid()

Vertical pyramids

Map age to x for a vertical pyramid. coord_pyramid() is for horizontal pyramids only, so use scale_y_pyramid() to label the value axis and theme_pyramid(flipped = TRUE) for the grid.

ggplot(france, aes(x = age, y = population, fill = sex)) +
  geom_pyramid() +
  scale_y_pyramid(labels = scales::label_number(scale = 1e-6, suffix = "M")) +
  labs(x = NULL, y = NULL, fill = NULL) +
  theme_pyramid(flipped = TRUE) +
  theme(axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1))

How the sides are drawn

position_pyramid() makes the values on the left side negative inside the plot, and stacks subgroups on each side away from the centre. Your data stays as it is. The signs matter when you set limits or draw reference lines on the left side, which take negative values:

geom_vline(xintercept = c(-1e6, 1e6), linetype = "dashed")
coord_pyramid(xlim = c(-3e6, 3e6))

Because the mirroring is done by the position, any layer can join a pyramid with position = "pyramid", and the plot stays correct with another coordinate system. Only the presentation that coord_pyramid() adds, such as the absolute labels and the centre gap, depends on the coordinate system.

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.