--- title: "Population pyramids with ggpyramid" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Population pyramids with ggpyramid} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4.5, out.width = "100%", dpi = 96 ) ``` 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. ```{r setup} 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: ```{r} head(europe_pop) ``` ## 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. ```{r first} 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: ```{r left} 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: ```{r, eval = FALSE} # 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. ```{r centre} 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: ```{r both} 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. ```{r facets, fig.height = 6} 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. ```{r labels} 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. ```{r 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: ```{r subgroups} 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. ```{r vertical} 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: ```{r, eval = FALSE} 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.