Create Faceted Bars in ggplot2 to Visualize Global Inequalities in Aviation CO₂ Emissions

When long text strings are naively mapped to the x or y axis, it can greatly squeeze the available plotting space. Out of the many solutions, I’ll introduce in this article an elegant and advanced approach: facet the plot based on the variable which contains long strings, and display the long strings in the horizontal orientation as subplot titles.

I’ll illustrate this technique by visualizing the amount of air travel-released carbon dioxide by developed and developing countries. The inspiration comes from the graphic by Datawrapper by Lisa Charlotte Rost.


Packages and data cleanup

The data used in this visualization is sourced from Our World in Data, and can be downloaded here.

library(ggplot2)library(dplyr)library(tidyr)library(stringr)
d <- read.csv("/Users/boyuan/Desktop/R/gallery/DATASETS/bar_CO2_air_travel.csv")
d.tidy <- d %>% pivot_longer( -countries, names_to = "aspect", values_to = "percent")

Convert the countries and aspect variables into factor variables, such that the levels are displayed in order of their appearance in the dataset (or in reverse order). Check this complete guide to learn systematically how to reorder graphical elements in ggplot2.

d.tidy.ordered <- d.tidy %>%  # use 'unique' to extract unique levels;   # there should be NO duplicates for the 'levels' argument.   mutate(    countries = factor(countries, levels = unique(d.tidy$countries)),    aspect = factor(aspect, levels = unique(d.tidy$aspect) %>% rev())) 
head(d.tidy.ordered, n = 3)

Output:

# A tibble: 3 × 3
countries aspect percent
<fct> <fct> <int>
1 High income countries share_of_CO2_emissions_from_air_travel 62
2 High income countries share_of_world_population 16
3 Upper-middle income countries share_of_CO2_emissions_from_air_travel 28

Visualization

Create a bar plot with a flipped x and y axes. Note how the plot has been squeezed by the long labels of the x-axis (left) and legend.

p1 <- d.tidy.ordered %>%   ggplot(aes(x = countries, y = percent, fill = aspect)) +  geom_col(position = "dodge") +  # flip, and expand bars to fill up entire plot  coord_flip(expand = 0)p1

To solve the squeezing issue, here we facet the plot into subplots arranged in a single column. We’ll use the subplot titles in place of the x-axis labels to free up more plotting space. We’ll remove the x-axis labels at a later step. (An alternative solution is to wrap long labels as in this example)

p2 <- p1 +   facet_wrap(~countries, ncol = 1, scales = "free_y") p2

Annotate the bars with the share percentage in place of the y-axis labels (bottom). The texts are dodged with width of 0.9, which is the default dodge amount of geom_col() and geom_bar(). A vector of colors in length of 8 (as the number of bars) is used to specify the text color in each bar.

p3 <- p2 +   geom_text(    aes(label = paste(percent, "%"), y = 3),    # 0.9 is the default bar dodge width    position = position_dodge(.9),    fontface = "bold",    color = c(rep(c("white", "black"), 3), "black", "black")) p3

Use theme_void to quickly remove all axial elements (titles, labels, ticks), and the strip borders. As it also removes the margin around the plot, we’ll add it back later in the theme customization. Now we see that the removal of the long text strings on the x-axis (left) has greatly improved the graphic appearance.

p4 <- p3 + theme_void()p4

Further polish up the theme. In particular, free up more space on the plot’s right side by relocating the legend to the top of the plot in a horizontal orientation.

p5 <- p4 +  theme(    legend.position = "top", # move legend to the top    legend.text = element_text(size = 13),        # make subplot titles more prominent     strip.text = element_text(      hjust = 0, face = "bold", size = 12, margin = margin(b = 5, t = 20)),    # slightly reduce the spacing between panels    panel.spacing = unit(-5, "pt"),        # add margins around the four sides of the plot    plot.margin = margin(rep(10, 4), unit = "pt"))p5

Change the bar colors, remove the underscores in the legend labels, and remove the legend title, all done in the scale_fill_manual() function.

p6 <- p5 + scale_fill_manual(  values = c("grey80", "firebrick3"),  # remove the underscores from the legend labels  labels = function(x){    str_replace_all(x, pattern = "_", replacement = " ")},  name = NULL)  # remove legend titlep6

Add a summarizing plot title. We use ggtext package to enhance the text strings. This involves two steps:

  • Use HTML to format the title
  • use element_markdown() in replace of element_text() in the theme syntax. Alternatively, use element_textbox_simple() to automatically wrap long text strings.

Check here (1, 2, 3) for more examples of title enrichment with ggtext.

# install.packages("ggtext")library(ggtext)
plot.title <- c( "High income countries</span> make up <span style = 'color: black;'>16% of the world population,</span> but create <span style = 'color: firebrick;'>*62% of CO2 emissions*</span> from air travel.\n")
p7 <- p6 + ggtitle(plot.title) + theme(plot.title = element_textbox_simple( face = "bold", color = "steelblue2", margin = margin(b = 10, unit = "pt"), size = 14))p7
library(ggplot2)library(dplyr)library(tidyr)library(stringr)
d <- read.csv("/Users/boyuan/Desktop/R/gallery/DATASETS/bar_CO2_air_travel.csv")
d.tidy <- d %>% pivot_longer( -countries, names_to = "aspect", values_to = "percent")
d.tidy.ordered <- d.tidy %>% # use 'unique' to extract unique levels; # there should be NO duplicates for the 'levels' argument. mutate(countries = factor(countries, levels = unique(d.tidy$countries)), aspect = factor(aspect, levels = unique(d.tidy$aspect) %>% rev()))
head(d.tidy.ordered, n = 3)

# Create a basic bar plot in a flipped orientation. p1 <- d.tidy.ordered %>% ggplot(aes(x = countries, y = percent, fill = aspect)) + geom_col(position = "dodge") + # flip, and expand bars to fill up entire plot coord_flip(expand = 0)p1

# Facet the plot into subplots arranged in a single column. p2 <- p1 + facet_wrap(~countries, ncol = 1, scales = "free_y") p2

# Annotate the bars with the share percentage in replace of the y (bottom) axis. p3 <- p2 + geom_text( aes(label = paste(percent, "%"), y = 3), # 0.9 is the default bar dodge width position = position_dodge(.9), fontface = "bold", color = c(rep(c("white", "black"), 3), "black", "black")) p3

# Quickly remove all axial elements (titles, labels, ticks), and the strip borders. p4 <- p3 + theme_void()p4

# Further improve the theme specification.p5 <- p4 + theme( legend.position = "top", # move legend to the top legend.text = element_text(size = 13), # make subplot titles more prominent strip.text = element_text( hjust = 0, face = "bold", size = 12, margin = margin(b = 5, t = 20)), # slightly reduce the spacing between panels panel.spacing = unit(-5, "pt"), # add margins around the four sides of the plot plot.margin = margin(10, 10, 10, 10))p5

# Change the bar colors, remove the underscores in the legend labels, and remove the legend titlep6 <- p5 + scale_fill_manual( values = c("grey80", "firebrick3"), # remove the underscores from the legend labels labels = function(x){ str_replace_all(x, pattern = "_", replacement = " ")}, name = NULL) # remove legend titlep6

# Add a summarizing plot title. # install.packages("ggtext")library(ggtext)
plot.title <- c( "High income countries</span> make up <span style = 'color: black;'>16% of the world population,</span> but create <span style = 'color: firebrick;'>*62% of CO2 emissions*</span> from air travel.\n")
p7 <- p6 + ggtitle(plot.title) + theme(plot.title = element_textbox_simple( face = "bold", color = "steelblue2", margin = margin(b = 10, unit = "pt"), size = 14))p7




Continue Exploring — 🚀 one level up!


Bars are among the most concise and telling elements in data visualization. When arranged in order, they can be a powerful tool for story-telling. The following ordered bar plot vividly highlights the wide range of animals’ sleeping time, and its relationship with body size.



Barplots are the building blocks of the population pyramid, a frequently used tool in demographics to illustrate the age and gender distribution of a population.

And check this awesome article to render the pyramids to animation!