---
title: "Same Income Group, Very Different Power Systems"
subtitle: "Carbon intensity varies widely within World Bank income groups. Each tick is a country; dots and bands show the median and middle 50% (2020–2023 average)."
description: "A barcode strip plot comparing the carbon intensity of electricity across World Bank income groups using 2020–2023 country averages. Each tick is a country; the IQR band and median dot summarize each group's distribution. Sorted by median carbon intensity, the chart reveals that within-group variation often rivals differences between income groups — with high-income countries spanning a surprisingly wide range."
date: "2026-04-05"
author:
- name: "Steven Ponce"
url: "https://stevenponce.netlify.app"
citation:
url: "https://stevenponce.netlify.app/data_visualizations/30DayChartChallenge/2026/30dcc_2026_05.html"
categories: ["30DayChartChallenge", "2026"]
tags: [
"30DayChartChallenge",
"Comparisons",
"Experimental",
"Barcode Chart",
"Strip Plot",
"Carbon Intensity",
"Electricity",
"Energy",
"Climate",
"Income Groups",
"World Bank",
"Distribution",
"Our World in Data",
"ggplot2",
"ggrepel"
]
image: "thumbnails/30dcc_2026_05.png"
format:
html:
toc: true
toc-depth: 5
code-link: true
code-fold: true
code-tools: true
code-summary: "Show code"
editor_options:
chunk_output_type: inline
execute:
freeze: true
cache: true
error: false
message: false
warning: false
eval: true
---
{#fig-1}
### [**Steps to Create this Graphic**]{.mark}
#### [1. Load Packages & Setup]{.smallcaps}
```{r}
#| label: load
#| warning: false
#| message: false
#| results: "hide"
## 1. LOAD PACKAGES & SETUP ----
suppressPackageStartupMessages({
pacman::p_load(
tidyverse, ggtext, showtext,
janitor, scales, glue, ggrepel
)
})
### |- figure size ----
camcorder::gg_record(
dir = here::here("temp_plots"),
device = "png",
width = 6,
height = 8,
units = "in",
dpi = 320
)
# Source utility functions
suppressMessages(source(here::here("R/utils/fonts.R")))
source(here::here("R/utils/social_icons.R"))
source(here::here("R/utils/image_utils.R"))
source(here::here("R/themes/base_theme.R"))
```
#### [2. Read in the Data]{.smallcaps}
```{r}
#| label: read
#| include: true
#| eval: true
#| warning: false
### |- OWID energy data ----
# Download: https://raw.githubusercontent.com/owid/energy-data/master/owid-energy-data.csv
energy_raw <- read_csv(
here::here("data/30DayChartChallenge/2026/owid-energy-data.csv")
)
```
#### [3. Examine the Data]{.smallcaps}
```{r}
#| label: examine
#| include: true
#| eval: true
#| results: 'hide'
#| warning: false
glimpse(energy_raw)
```
#### [4. Tidy Data]{.smallcaps}
```{r}
#| label: tidy
#| warning: false
### |- World Bank FY2024 income classification ----
# Sourced from: https://databank.worldbank.org/data/download/site-content/CLASS.xlsx
# Hardcoded from the same tribble used in Day 03
wb_income <- tribble(
~country, ~income_group,
"Afghanistan", "Low income",
"Albania", "Upper middle income",
"Algeria", "Lower middle income",
"Angola", "Lower middle income",
"Argentina", "Upper middle income",
"Armenia", "Upper middle income",
"Australia", "High income",
"Austria", "High income",
"Azerbaijan", "Upper middle income",
"Bangladesh", "Lower middle income",
"Belarus", "Upper middle income",
"Belgium", "High income",
"Benin", "Low income",
"Bolivia", "Lower middle income",
"Bosnia and Herzegovina", "Upper middle income",
"Brazil", "Upper middle income",
"Bulgaria", "Upper middle income",
"Burkina Faso", "Low income",
"Burundi", "Low income",
"Cambodia", "Lower middle income",
"Cameroon", "Lower middle income",
"Canada", "High income",
"Chad", "Low income",
"Chile", "High income",
"China", "Upper middle income",
"Colombia", "Upper middle income",
"Congo", "Lower middle income",
"Costa Rica", "Upper middle income",
"Croatia", "High income",
"Czech Republic", "High income",
"Democratic Republic of Congo", "Low income",
"Denmark", "High income",
"Dominican Republic", "Upper middle income",
"Ecuador", "Upper middle income",
"Egypt", "Lower middle income",
"El Salvador", "Lower middle income",
"Estonia", "High income",
"Ethiopia", "Low income",
"Finland", "High income",
"France", "High income",
"Georgia", "Upper middle income",
"Germany", "High income",
"Ghana", "Lower middle income",
"Greece", "High income",
"Guatemala", "Upper middle income",
"Honduras", "Lower middle income",
"Hungary", "High income",
"India", "Lower middle income",
"Indonesia", "Upper middle income",
"Iran", "Lower middle income",
"Iraq", "Upper middle income",
"Ireland", "High income",
"Israel", "High income",
"Italy", "High income",
"Ivory Coast", "Lower middle income",
"Japan", "High income",
"Jordan", "Upper middle income",
"Kazakhstan", "Upper middle income",
"Kenya", "Lower middle income",
"Kosovo", "Upper middle income",
"Kuwait", "High income",
"Kyrgyzstan", "Lower middle income",
"Laos", "Lower middle income",
"Latvia", "High income",
"Lebanon", "Lower middle income",
"Libya", "Upper middle income",
"Lithuania", "High income",
"Luxembourg", "High income",
"Madagascar", "Low income",
"Malawi", "Low income",
"Malaysia", "Upper middle income",
"Mali", "Low income",
"Mexico", "Upper middle income",
"Moldova", "Lower middle income",
"Mongolia", "Lower middle income",
"Morocco", "Lower middle income",
"Mozambique", "Low income",
"Myanmar", "Lower middle income",
"Nepal", "Lower middle income",
"Netherlands", "High income",
"New Zealand", "High income",
"Nicaragua", "Lower middle income",
"Niger", "Low income",
"Nigeria", "Lower middle income",
"North Macedonia", "Upper middle income",
"Norway", "High income",
"Pakistan", "Lower middle income",
"Panama", "Upper middle income",
"Paraguay", "Upper middle income",
"Peru", "Upper middle income",
"Philippines", "Lower middle income",
"Poland", "High income",
"Portugal", "High income",
"Romania", "Upper middle income",
"Russia", "Upper middle income",
"Rwanda", "Low income",
"Saudi Arabia", "High income",
"Senegal", "Lower middle income",
"Serbia", "Upper middle income",
"Slovakia", "High income",
"Slovenia", "High income",
"South Africa", "Upper middle income",
"South Korea", "High income",
"Spain", "High income",
"Sri Lanka", "Lower middle income",
"Sudan", "Low income",
"Sweden", "High income",
"Switzerland", "High income",
"Syria", "Low income",
"Taiwan", "High income",
"Tajikistan", "Low income",
"Tanzania", "Lower middle income",
"Thailand", "Upper middle income",
"Togo", "Low income",
"Trinidad and Tobago", "High income",
"Tunisia", "Lower middle income",
"Turkey", "Upper middle income",
"Turkmenistan", "Upper middle income",
"Uganda", "Low income",
"Ukraine", "Lower middle income",
"United Arab Emirates", "High income",
"United Kingdom", "High income",
"United States", "High income",
"Uruguay", "High income",
"Uzbekistan", "Lower middle income",
"Venezuela", "Upper middle income",
"Vietnam", "Lower middle income",
"Yemen", "Low income",
"Zambia", "Low income",
"Zimbabwe", "Lower middle income"
)
### |- compute 2020–2023 average carbon intensity per country ----
carbon_avg <- energy_raw |>
filter(year %in% 2020:2023) |>
filter(!is.na(carbon_intensity_elec)) |>
# exclude aggregates / regions (no iso_code or all-caps codes like OWID_*)
filter(!is.na(iso_code), !str_detect(iso_code, "^OWID")) |>
group_by(country) |>
summarise(
carbon_intensity = mean(carbon_intensity_elec, na.rm = TRUE),
n_years = n(),
.groups = "drop"
) |>
# require at least 3 of 4 years of data for reliability
filter(n_years >= 3)
### |- join income classification ----
plot_data <- carbon_avg |>
inner_join(wb_income, by = "country") |>
mutate(
income_group = factor(income_group, levels = c(
"Low income", "Lower middle income",
"Upper middle income", "High income"
))
)
### |- compute group-level summary stats ----
group_stats <- plot_data |>
group_by(income_group) |>
summarise(
median_ci = median(carbon_intensity),
q25 = quantile(carbon_intensity, 0.25),
q75 = quantile(carbon_intensity, 0.75),
.groups = "drop"
)
### |- sort income groups by median carbon intensity (highest → lowest) ----
group_order <- group_stats |>
arrange(desc(median_ci)) |>
pull(income_group)
plot_data <- plot_data |>
mutate(income_group = factor(income_group, levels = group_order)) |>
mutate(
income_group = fct_recode(
income_group,
"Upper middle\nincome" = "Upper middle income",
"Lower middle\nincome" = "Lower middle income"
)
)
group_stats <- group_stats |>
mutate(income_group = factor(income_group, levels = group_order)) |>
mutate(
income_group = fct_recode(
income_group,
"Upper middle\nincome" = "Upper middle income",
"Lower middle\nincome" = "Lower middle income"
)
)
### |- identify labels: extremes + analytically chosen surprises ----
# Per group: highest, lowest, and country farthest from the global median
global_median <- median(plot_data$carbon_intensity)
label_data <- plot_data |>
group_by(income_group) |>
mutate(
deviation_from_global = abs(carbon_intensity - global_median),
rank_in_group = rank(carbon_intensity)
) |>
mutate(
is_extreme = rank_in_group == max(rank_in_group) | rank_in_group == min(rank_in_group),
is_surprise = deviation_from_global == max(deviation_from_global) & !is_extreme
) |>
ungroup() |>
filter(is_extreme | is_surprise)
```
#### [5. Visualization Parameters]{.smallcaps}
```{r}
#| label: params
#| include: true
#| warning: false
### |- plot aesthetics ----
colors <- get_theme_colors(
palette = list(
"Low income" = "#8B1A2A",
"Lower middle income" = "#C0434E",
"Upper middle income" = "#B08090",
"High income" = "#2E1122",
"iqr_band" = "#D4C7BB",
"median_dot_fill" = "#FFFFFF",
"median_dot_stroke" = "#3D0C1E",
"median_label" = "#3D0C1E",
"label_text" = "gray20",
"global_ref" = "#B29A8B"
)
)
### |- titles and caption ----
title_text <- "Same Income Group, Very Different Power Systems"
# "Each tick is a country" — plain weight; methodological note, not the story
subtitle_text <- str_glue(
"Carbon intensity varies widely within World Bank income groups.<br><br>",
"Each tick is a country; dots and bands show the median and middle 50% (2020–2023 average).<br>",
"<span style='color:gray45;font-style:italic;'>Within-group variation often rivals differences between groups.</span>"
)
caption_text <- create_dcc_caption(
dcc_year = 2026,
dcc_day = 05,
source_text = "Our World in Data — Energy Dataset (Ember / Energy Institute)"
)
### |- fonts ----
setup_fonts()
fonts <- get_font_families()
### |- plot theme ----
base_theme <- create_base_theme(colors)
weekly_theme <- extend_weekly_theme(
base_theme,
theme(
# strip / facet labels
strip.text = element_blank(),
# axes
axis.title.x = element_text(
size = 9, color = "gray40", margin = margin(t = 5, b = 5), family = fonts$text
),
axis.title.y = element_blank(),
axis.text.x = element_text(size = 8, color = "gray40", family = fonts$text),
axis.text.y = element_text(
size = 9.5, color = "gray25", face = "plain", family = fonts$text, hjust = 1
),
axis.ticks = element_blank(),
axis.line = element_blank(),
# grid — only faint vertical reference lines
panel.grid.major.x = element_line(color = "gray93", linewidth = 0.3),
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank(),
# plot margins
plot.margin = margin(t = 20, r = 25, b = 10, l = 15),
# title / subtitle / caption
plot.title = element_text(
size = 18, face = "bold", family = fonts$title,
color = "gray10", margin = margin(b = 6)
),
plot.subtitle = element_markdown(
size = 9, family = fonts$text, color = "gray35",
lineheight = 1.1, margin = margin(b = 18)
),
plot.caption = element_markdown(
size = 7, family = fonts$caption, color = "gray55",
hjust = 0, margin = margin(t = 5)
)
)
)
theme_set(weekly_theme)
```
#### [6. Plot]{.smallcaps}
```{r}
#| label: plot
#| warning: false
### |- main plot ----
p <- ggplot() +
geom_vline(
xintercept = global_median,
color = colors$palette$global_ref,
linewidth = 0.45,
linetype = "22",
alpha = 0.9
) +
annotate(
"text",
x = global_median,
y = 4.65,
label = "Global median",
size = 2.4,
color = "gray45",
family = fonts$text,
vjust = 0,
hjust = -0.1
) +
geom_segment(
data = group_stats,
aes(
x = q25,
xend = q75,
y = income_group,
yend = income_group
),
linewidth = 3.6,
color = colors$palette$iqr_band,
lineend = "round",
alpha = 0.75
) +
geom_segment(
data = plot_data,
aes(
x = carbon_intensity,
xend = carbon_intensity,
y = as.numeric(income_group) - 0.36,
yend = as.numeric(income_group) + 0.36,
color = income_group
),
linewidth = 0.45,
alpha = 0.65
) +
geom_point(
data = group_stats,
aes(x = median_ci, y = income_group),
size = 4.5,
shape = 21,
fill = colors$palette$median_dot_fill,
color = colors$palette$median_dot_stroke,
stroke = 1.6
) +
geom_text(
data = group_stats,
aes(
x = median_ci,
y = income_group,
label = glue("{round(median_ci)} g")
),
vjust = -1.4,
size = 2.8,
color = colors$palette$median_label,
family = fonts$text,
fontface = "bold"
) +
geom_text_repel(
data = label_data,
aes(
x = carbon_intensity,
y = income_group,
label = country
),
size = 2.6,
color = colors$palette$label_text,
family = fonts$text,
nudge_y = 0.42,
direction = "x",
segment.size = 0.25,
segment.color = "gray55",
segment.curvature = 0,
min.segment.length = 0.1,
box.padding = 0.15,
point.padding = 0.1,
force = 0.8,
max.overlaps = 20,
seed = 42
) +
scale_color_manual(
values = c(
"Low income" = colors$palette$`Low income`,
"Lower middle\nincome" = colors$palette$`Lower middle income`,
"Upper middle\nincome" = colors$palette$`Upper middle income`,
"High income" = colors$palette$`High income`
),
guide = "none"
) +
scale_x_continuous(
labels = label_number(suffix = " g"),
expand = expansion(mult = c(0.02, 0.05))
) +
scale_y_discrete(
expand = expansion(add = c(0.8, 0.8))
) +
coord_cartesian(xlim = c(0, 1400)) +
labs(
title = title_text,
subtitle = subtitle_text,
caption = caption_text,
x = "Carbon intensity of electricity (gCO₂ per kWh, 2020–2023 average)"
)
```
#### [7. Save]{.smallcaps}
```{r}
#| label: save
#| warning: false
### |- plot image ----
save_plot(
p,
type = "30daychartchallenge",
year = 2026,
day = 05,
width = 6,
height = 8
)
```
#### [8. Session Info]{.smallcaps}
::: {.callout-tip collapse="true"}
##### Expand for Session Info
```{r, echo = FALSE}
#| eval: true
#| warning: false
sessionInfo()
```
:::
#### [9. GitHub Repository]{.smallcaps}
::: {.callout-tip collapse="true"}
##### Expand for GitHub Repo
The complete code for this analysis is available in [`30dcc_2026_05.qmd`](https://github.com/poncest/personal-website/blob/master/data_visualizations/TidyTuesday/2026/30dcc_2026_05.qmd).
For the full repository, [click here](https://github.com/poncest/personal-website/).
:::
#### [10. References]{.smallcaps}
::: {.callout-tip collapse="true"}
##### Expand for References
1. Data Sources:
- Ritchie, H., Rosado, P., & Roser, M. (2024). *Our World in Data — Energy Dataset*.
Based on data from Ember and the Energy Institute Statistical Review of World Energy.
Retrieved April 2026 from
https://raw.githubusercontent.com/owid/energy-data/master/owid-energy-data.csv
- Ember. (2024). *Yearly Electricity Data*. Retrieved from https://ember-energy.org
- Energy Institute. (2024). *Statistical Review of World Energy*.
Retrieved from https://www.energyinst.org/statistical-review
:::
#### [11. Custom Functions Documentation]{.smallcaps}
::: {.callout-note collapse="true"}
##### 📦 Custom Helper Functions
This analysis uses custom functions from my personal module library for efficiency and consistency across projects.
**Functions Used:**
- **`fonts.R`**: `setup_fonts()`, `get_font_families()` - Font management with showtext
- **`social_icons.R`**: `create_social_caption()` - Generates formatted social media captions
- **`image_utils.R`**: `save_plot()` - Consistent plot saving with naming conventions
- **`base_theme.R`**: `create_base_theme()`, `extend_weekly_theme()`, `get_theme_colors()` - Custom ggplot2 themes
**Why custom functions?**\
These utilities standardize theming, fonts, and output across all my data visualizations. The core analysis (data tidying and visualization logic) uses only standard tidyverse packages.
**Source Code:**\
View all custom functions → [GitHub: R/utils](https://github.com/poncest/personal-website/tree/master/R)
:::