| Title: | Population Pyramids with 'ggplot2' |
|---|---|
| Description: | Build population pyramids, the back-to-back bar charts of a population by age group and sex, as layers of a 'ggplot2' plot. A position adjustment mirrors one side and stacks subgroups within each side, so bars, labels and lines from any layer land on the correct side while the plot can still be faceted, themed and extended as usual. A coordinate system labels both sides with absolute values, gives them one shared scale, and places the age axis on either side or in a gap at the centre. Further layers count individual records or sum aggregated values, show proportions, label the bars, and draw a second population as a stepped outline for comparison. |
| Authors: | Matan Hakim [aut, cre, cph] (ORCID: <https://orcid.org/0009-0002-9372-8975>) |
| Maintainer: | Matan Hakim <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-11-10 23:45:29 UTC |
| Source: | https://github.com/cran/ggpyramid |
coord_pyramid() is a Cartesian coordinate system for horizontal pyramids
drawn with position_pyramid(). It labels the value axis without the minus
sign on the left side, extends the value axis equally far on both sides of
zero, and places the age axis on the left, on the right, on both sides or in
the centre of the pyramid, optionally in a gap between the two sides.
coord_pyramid( age_axis = c("left", "right", "both", "centre", "none"), left = NULL, gap = NULL, symmetric = TRUE, xlim = NULL, ylim = NULL, expand = TRUE, clip = "on" )coord_pyramid( age_axis = c("left", "right", "both", "centre", "none"), left = NULL, gap = NULL, symmetric = TRUE, xlim = NULL, ylim = NULL, expand = TRUE, clip = "on" )
age_axis |
Where to draw the age axis: |
left |
The value of the side variable drawn on the left, such as
|
gap |
The width of an empty strip between the two sides, as a
proportion of the panel width. Defaults to |
symmetric |
If |
xlim, ylim
|
Limits for the x and y axes. |
expand |
If |
clip |
Should drawing be clipped to the extent of the plot panel? A
setting of |
A ggproto object of class CoordPyramid.
Only the labels lose their sign. The data and the scale keep the signed
values that position_pyramid() produces, so breaks, limits and reference
lines on the left side are still given as negative numbers. Any labels
given to ggplot2::scale_x_continuous() apply to the absolute values, for
example labels = scales::label_percent(). With a gap, a break at zero is
drawn on both inner edges, so a gap narrower than two zero labels makes them
overlap. The value axis must be linear, and a secondary value axis keeps
its signs.
"left" and "right" move the age scale to that side together with its
title, and "both" duplicates it on the right, as
scale_y_discrete(sec.axis = dup_axis()) would. "centre" draws the age
labels inside the panel, above the layers and in the middle of the gap,
styled by the axis.text.y.left theme element. If the labels are wider
than the gap, increase gap.
position_pyramid(), which mirrors the left side.
library(ggplot2) pop <- data.frame( age = factor(rep(c("0-19", "20-39", "40-59", "60+"), 2)), sex = factor(rep(c("Male", "Female"), each = 4), c("Male", "Female")), n = c(220, 200, 160, 90, 210, 205, 170, 120) ) p <- ggplot(pop, aes(x = n, y = age, fill = sex)) + geom_pyramid() p + coord_pyramid() # Women on the left p + coord_pyramid(left = "Female") # Age labels in a gap between the two sides p + coord_pyramid(age_axis = "centre") # Age labels on both sides, with a narrow gap p + coord_pyramid(age_axis = "both", gap = 0.08)library(ggplot2) pop <- data.frame( age = factor(rep(c("0-19", "20-39", "40-59", "60+"), 2)), sex = factor(rep(c("Male", "Female"), each = 4), c("Male", "Female")), n = c(220, 200, 160, 90, 210, 205, 170, 120) ) p <- ggplot(pop, aes(x = n, y = age, fill = sex)) + geom_pyramid() p + coord_pyramid() # Women on the left p + coord_pyramid(left = "Female") # Age labels in a gap between the two sides p + coord_pyramid(age_axis = "centre") # Age labels on both sides, with a narrow gap p + coord_pyramid(age_axis = "both", gap = 0.08)
The population on 1 January 2004 and 2024 of France, Ireland, Italy and Poland, by five-year age group and sex, from Eurostat. It is included for examples and has the shape most population data come in: one row per country, year, age group and sex.
europe_popeurope_pop
A data frame with 288 rows and 5 columns:
Country, a factor.
Year of the population count, 2004 or 2024.
Age group, a factor from "0-4" to "85+" in ascending
order.
Sex, a factor with levels "Male" and "Female".
Number of people.
Eurostat, "Population on 1 January by age group and sex"
(demo_pjangroup),
https://ec.europa.eu/eurostat/databrowser/view/demo_pjangroup,
retrieved 25 October 2026 and reused under the Creative Commons
Attribution 4.0 licence. Some 2024 figures are provisional.
library(ggplot2) 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)library(ggplot2) 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)
geom_pyramid() draws the bars of a population pyramid: one bar per age
group on each side, extending away from a shared age axis. It counts rows,
so it accepts individual records (aes(y = age, fill = sex)), and it sums a
value when one is mapped, so it also accepts aggregated data
(aes(x = n, y = age, fill = sex)). The bars are mirrored and stacked by
position_pyramid(), and every other part of the plot (scales, facets,
themes and further layers) comes from ggplot2 as usual.
geom_pyramid( mapping = NULL, data = NULL, stat = "pyramid", position = "pyramid", ..., denominator = NULL, just = 0.5, na.rm = FALSE, orientation = NA, show.legend = NA, inherit.aes = TRUE ) stat_pyramid( mapping = NULL, data = NULL, geom = "bar", position = "pyramid", ..., denominator = NULL, na.rm = FALSE, orientation = NA, show.legend = NA, inherit.aes = TRUE )geom_pyramid( mapping = NULL, data = NULL, stat = "pyramid", position = "pyramid", ..., denominator = NULL, just = 0.5, na.rm = FALSE, orientation = NA, show.legend = NA, inherit.aes = TRUE ) stat_pyramid( mapping = NULL, data = NULL, geom = "bar", position = "pyramid", ..., denominator = NULL, na.rm = FALSE, orientation = NA, show.legend = NA, inherit.aes = TRUE )
mapping |
Set of aesthetic mappings created by |
data |
The data to be displayed in this layer. There are three options: If A A |
stat |
The statistical transformation to use on the data for this
layer. The default, |
position |
A position adjustment. The default, |
... |
Other arguments passed on to |
denominator |
|
just |
Adjustment for column placement. Set to |
na.rm |
If |
orientation |
The orientation of the layer. The default ( |
show.legend |
logical. Should this layer be included in the legends?
|
inherit.aes |
If |
geom |
The geometric object used to display the data. |
A ggplot2 layer.
Age goes on y, or on x for a vertical pyramid. The value axis is
optional: when it is mapped, its values are summed within each age group,
and when it is not, rows are counted. weight multiplies either. The side
of each bar comes from side, or from fill when side is not mapped; see
position_pyramid().
These can be used with ggplot2::after_stat():
after_stat(count): the number of rows, or the sum of the value, in each
age group. This is what the bars show by default.
after_stat(prop): count as a proportion of all rows in the panel, or
of the rows on the same side with denominator = "side". Each facet
panel has its own denominator. The bars show prop whenever
denominator is set.
position_pyramid() to mirror other layers, and coord_pyramid()
for the axes of a pyramid.
library(ggplot2) pop <- data.frame( age = factor(rep(c("0-19", "20-39", "40-59", "60+"), 2)), sex = factor(rep(c("Male", "Female"), each = 4), c("Male", "Female")), n = c(220, 200, 160, 90, 210, 205, 170, 120) ) # Aggregated data: the value is summed ggplot(pop, aes(x = n, y = age, fill = sex)) + geom_pyramid() + coord_pyramid() # Individual records: rows are counted people <- pop[rep(seq_len(nrow(pop)), pop$n), c("age", "sex")] ggplot(people, aes(y = age, fill = sex)) + geom_pyramid() # Percentages of each side ggplot(pop, aes(x = n, y = age, fill = sex)) + geom_pyramid(denominator = "side") + scale_x_continuous(labels = scales::label_percent()) + coord_pyramid() + labs(x = "Share of each sex")library(ggplot2) pop <- data.frame( age = factor(rep(c("0-19", "20-39", "40-59", "60+"), 2)), sex = factor(rep(c("Male", "Female"), each = 4), c("Male", "Female")), n = c(220, 200, 160, 90, 210, 205, 170, 120) ) # Aggregated data: the value is summed ggplot(pop, aes(x = n, y = age, fill = sex)) + geom_pyramid() + coord_pyramid() # Individual records: rows are counted people <- pop[rep(seq_len(nrow(pop)), pop$n), c("age", "sex")] ggplot(people, aes(y = age, fill = sex)) + geom_pyramid() # Percentages of each side ggplot(pop, aes(x = n, y = age, fill = sex)) + geom_pyramid(denominator = "side") + scale_x_continuous(labels = scales::label_percent()) + coord_pyramid() + labs(x = "Share of each sex")
geom_pyramid_outline() traces the outer edge of a pyramid as a stepped
line, without filling the bars. Drawn over geom_pyramid(), it compares two
populations, such as two census years or a region and the whole country.
It aggregates the data in the same way as geom_pyramid(), so it accepts
individual records and aggregated data alike.
geom_pyramid_outline( mapping = NULL, data = NULL, stat = "pyramid", position = position_pyramid(stack = FALSE), ..., denominator = NULL, lineend = "butt", linejoin = "mitre", linemitre = 10, na.rm = FALSE, orientation = NA, show.legend = NA, inherit.aes = TRUE )geom_pyramid_outline( mapping = NULL, data = NULL, stat = "pyramid", position = position_pyramid(stack = FALSE), ..., denominator = NULL, lineend = "butt", linejoin = "mitre", linemitre = 10, na.rm = FALSE, orientation = NA, show.legend = NA, inherit.aes = TRUE )
mapping |
Set of aesthetic mappings created by |
data |
The data to be displayed in this layer. There are three options: If A A |
stat |
The statistical transformation to use on the data for this
layer. The default, |
position |
A position adjustment. The default mirrors the left side
without stacking, as |
... |
Other arguments passed on to |
denominator |
|
lineend |
Line end style (round, butt, square). |
linejoin |
Line join style (round, mitre, bevel). |
linemitre |
Line mitre limit (number greater than 1). |
na.rm |
If |
orientation |
The orientation of the layer. The default ( |
show.legend |
logical. Should this layer be included in the legends?
|
inherit.aes |
If |
Each side gets one line, and so does each subgroup that fill, colour,
linetype, linewidth or alpha sets apart. The line starts at zero below
the youngest age group, follows the value of each group across its whole
band, and returns to zero above the oldest. It drops to zero across an age
group with no data.
A ggplot2 layer.
The age and value axes are as in geom_pyramid(), and the side of each line
comes from side, fill or colour, as described in
position_pyramid(). The line is styled by colour, linewidth,
linetype and alpha.
geom_pyramid() for the bars.
library(ggplot2) france <- subset(europe_pop, country == "France") # 2024 as bars, 2004 as an outline ggplot(france, aes(x = population, y = age, fill = sex)) + geom_pyramid(data = subset(france, year == 2024)) + geom_pyramid_outline(data = subset(france, year == 2004)) + coord_pyramid(age_axis = "centre")library(ggplot2) france <- subset(europe_pop, country == "France") # 2024 as bars, 2004 as an outline ggplot(france, aes(x = population, y = age, fill = sex)) + geom_pyramid(data = subset(france, year == 2024)) + geom_pyramid_outline(data = subset(france, year == 2004)) + coord_pyramid(age_axis = "centre")
geom_pyramid_text() writes a label at the end of each bar of a pyramid.
It aggregates the data in the same way as geom_pyramid(), so the labels
show the counts, or the proportions when denominator is set, without
further mapping. Labels on the left side are justified towards the left,
so they sit outside (or inside) the bar on both sides.
geom_pyramid_text( mapping = NULL, data = NULL, stat = "pyramid", position = "pyramid", ..., denominator = NULL, place = c("outside", "inside"), padding = 1, labels = NULL, parse = FALSE, check_overlap = FALSE, size.unit = "mm", na.rm = FALSE, orientation = NA, show.legend = NA, inherit.aes = TRUE )geom_pyramid_text( mapping = NULL, data = NULL, stat = "pyramid", position = "pyramid", ..., denominator = NULL, place = c("outside", "inside"), padding = 1, labels = NULL, parse = FALSE, check_overlap = FALSE, size.unit = "mm", na.rm = FALSE, orientation = NA, show.legend = NA, inherit.aes = TRUE )
mapping |
Set of aesthetic mappings created by |
data |
The data to be displayed in this layer. There are three options: If A A |
stat |
The statistical transformation to use on the data for this
layer. The default, |
position |
A position adjustment. The default, |
... |
Other arguments passed on to |
denominator |
|
place |
Where to draw the labels: |
padding |
The distance between the label and the end of the bar, in millimetres. |
labels |
A function that formats the labels, such as
|
parse |
If |
check_overlap |
If |
size.unit |
How the |
na.rm |
If |
orientation |
The orientation of the layer. The default ( |
show.legend |
logical. Should this layer be included in the legends?
|
inherit.aes |
If |
A ggplot2 layer.
label defaults to the value the bars show, after_stat(count) or
after_stat(prop), and can be mapped to anything else. The horizontal and
vertical justification are set by place and the text angle, so a mapped
or fixed hjust or vjust is replaced. The other aesthetics are those of
ggplot2::geom_text().
The justification and padding follow the orientation of the data, so
ggplot2::coord_flip() puts them on the wrong axis. For a vertical pyramid,
map age to x instead.
geom_pyramid() for the bars.
library(ggplot2) pop <- data.frame( age = factor(rep(c("0-19", "20-39", "40-59", "60+"), 2)), sex = factor(rep(c("Male", "Female"), each = 4), c("Male", "Female")), n = c(220, 200, 160, 90, 210, 205, 170, 120) ) p <- ggplot(pop, aes(x = n, y = age, fill = sex)) + coord_pyramid() p + geom_pyramid() + geom_pyramid_text() + scale_x_continuous(expand = expansion(mult = 0.15)) # Percentages inside the bars p + geom_pyramid(denominator = "total") + geom_pyramid_text( denominator = "total", place = "inside", labels = scales::label_percent(accuracy = 1), colour = "white" )library(ggplot2) pop <- data.frame( age = factor(rep(c("0-19", "20-39", "40-59", "60+"), 2)), sex = factor(rep(c("Male", "Female"), each = 4), c("Male", "Female")), n = c(220, 200, 160, 90, 210, 205, 170, 120) ) p <- ggplot(pop, aes(x = n, y = age, fill = sex)) + coord_pyramid() p + geom_pyramid() + geom_pyramid_text() + scale_x_continuous(expand = expansion(mult = 0.15)) # Percentages inside the bars p + geom_pyramid(denominator = "total") + geom_pyramid_text( denominator = "total", place = "inside", labels = scales::label_percent(accuracy = 1), colour = "white" )
position_pyramid() draws the rows of one side in the negative direction,
so that the two sides of a pyramid extend away from a shared age axis, and
stacks subgroups within each side. It works with any layer, so bars, labels,
points and lines all land on the same side.
position_pyramid(left = NULL, stack = TRUE, vjust = 1, reverse = FALSE)position_pyramid(left = NULL, stack = TRUE, vjust = 1, reverse = FALSE)
left |
The value of the side variable drawn on the left in this layer.
Overrides the |
stack |
If |
vjust, reverse
|
Passed on to the stacking. See
|
A ggproto object of class PositionPyramid.
position_pyramid() understands the side aesthetic, which assigns each
row to one of the two sides. When side is not mapped, the side comes from
fill, and failing that from colour. Map side explicitly when fill
shows subgroups, for example aes(fill = region, side = sex). Rows with a
missing side are drawn on the right.
The data you supply stays positive. Inside the plot, values on the left side
are negative, so a manual reference line or limit on that side carries a
minus sign: geom_vline(xintercept = -100) or
scale_x_continuous(limits = c(-500, 500)). coord_pyramid() shows the
axis labels without the sign; with another coordinate system or in a
vertical pyramid, use scale_x_pyramid() or scale_y_pyramid().
The axis that does not hold age is the one that gets mirrored. Age is the
discrete axis when only one of them is discrete, and the y axis when both
are continuous, so a pyramid is horizontal unless age is mapped to x. A
mapped hjust (or vjust in a vertical pyramid) is mirrored as well, so
text justified away from the bar end on the right does the same on the
left. A fixed hjust set outside ggplot2::aes() is not mirrored.
geom_pyramid() for the bars of a pyramid, and coord_pyramid()
for its axes.
library(ggplot2) pop <- data.frame( age = factor(rep(c("0-19", "20-39", "40-59", "60+"), 2)), sex = factor(rep(c("Male", "Female"), each = 4), c("Male", "Female")), n = c(220, 200, 160, 90, 210, 205, 170, 120) ) # Any geom can be mirrored ggplot(pop, aes(x = n, y = age, fill = sex)) + geom_col(position = position_pyramid()) + geom_text(aes(label = n, hjust = -0.2), position = "pyramid") + scale_x_continuous(expand = expansion(mult = 0.15)) + coord_pyramid() # Subgroups stack within each side pop2 <- rbind(transform(pop, group = "A"), transform(pop, group = "B")) ggplot(pop2, aes(x = n, y = age, fill = group, side = sex)) + geom_col(position = "pyramid") + coord_pyramid(left = "Female")library(ggplot2) pop <- data.frame( age = factor(rep(c("0-19", "20-39", "40-59", "60+"), 2)), sex = factor(rep(c("Male", "Female"), each = 4), c("Male", "Female")), n = c(220, 200, 160, 90, 210, 205, 170, 120) ) # Any geom can be mirrored ggplot(pop, aes(x = n, y = age, fill = sex)) + geom_col(position = position_pyramid()) + geom_text(aes(label = n, hjust = -0.2), position = "pyramid") + scale_x_continuous(expand = expansion(mult = 0.15)) + coord_pyramid() # Subgroups stack within each side pop2 <- rbind(transform(pop, group = "A"), transform(pop, group = "B")) ggplot(pop2, aes(x = n, y = age, fill = group, side = sex)) + geom_col(position = "pyramid") + coord_pyramid(left = "Female")
scale_x_pyramid() and scale_y_pyramid() are continuous position scales
that label the value axis of a pyramid without the minus sign and extend it
equally far on both sides of zero. coord_pyramid() does the same for a
horizontal pyramid, along with placing the age axis; these scales serve a
vertical pyramid, or a plot that needs another coordinate system.
scale_x_pyramid(..., labels = waiver(), limits = NULL, symmetric = TRUE) scale_y_pyramid(..., labels = waiver(), limits = NULL, symmetric = TRUE)scale_x_pyramid(..., labels = waiver(), limits = NULL, symmetric = TRUE) scale_y_pyramid(..., labels = waiver(), limits = NULL, symmetric = TRUE)
... |
Other arguments passed on to |
labels |
Labels for the breaks, applied to their absolute values. One of:
|
limits |
One of:
|
symmetric |
If |
A ggplot2 scale.
coord_pyramid(), which labels and limits the value axis of a
horizontal pyramid.
library(ggplot2) france <- subset(europe_pop, country == "France" & year == 2024) # A vertical pyramid, with age on x ggplot(france, aes(x = age, y = population, fill = sex)) + geom_pyramid() + scale_y_pyramid(labels = scales::label_comma()) + theme(axis.text.x = element_text(angle = 90, vjust = 0.5))library(ggplot2) france <- subset(europe_pop, country == "France" & year == 2024) # A vertical pyramid, with age on x ggplot(france, aes(x = age, y = population, fill = sex)) + geom_pyramid() + scale_y_pyramid(labels = scales::label_comma()) + theme(axis.text.x = element_text(angle = 90, vjust = 0.5))
theme_pyramid() is ggplot2::theme_minimal() adjusted for pyramids. It
removes the grid lines that run along the bars, which the age axis already
marks, keeps the major grid lines across the value axis, and puts the legend
above the plot, where it reads as a heading for the two sides.
theme_pyramid(..., flipped = FALSE)theme_pyramid(..., flipped = FALSE)
... |
Arguments passed on to |
flipped |
If |
A ggplot2 theme.
library(ggplot2) france <- subset(europe_pop, country == "France" & year == 2024) ggplot(france, aes(x = population, y = age, fill = sex)) + geom_pyramid() + coord_pyramid() + theme_pyramid()library(ggplot2) france <- subset(europe_pop, country == "France" & year == 2024) ggplot(france, aes(x = population, y = age, fill = sex)) + geom_pyramid() + coord_pyramid() + theme_pyramid()