• Steven Ponce
  • About
  • Data Visualizations
  • Projects
  • Resume
  • Email

On this page

  • Steps to Create this Graphic
    • 1. Load Packages & Setup
    • 2. Read in the Data
    • 3. Examine the Data
    • 4. Tidy Data
    • 5. Visualization Parameters
    • 6. Plot
    • 7. Save
    • 8. Session Info
    • 9. GitHub Repository
    • 10. References
    • 11. Custom Functions Documentation

Same Income Group, Very Different Power Systems

  • Show All Code
  • Hide All Code

  • View Source

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).

30DayChartChallenge
Data Visualization
R Programming
2026
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.
Author

Steven Ponce

Published

April 5, 2026

Figure 1: Barcode strip plot showing the carbon intensity of electricity (gCO₂ per kWh, 2020–2023 average) for countries grouped by World Bank income classification. Each vertical tick represents one country. A taupe band marks the middle 50% (IQR) and a white-filled dot marks the group median. Groups are sorted from lowest to highest median: High income (250 g), Low income (256 g), Upper middle income (454 g), and Lower middle income (481 g). A dashed vertical line marks the global median. Notable outliers include Norway and Poland (High income), Ethiopia and Syria (Low income), Albania and Turkmenistan (Upper middle income), and Nepal and Uzbekistan (Lower middle income). Within-group variation is wide across all four groups, often rivaling differences between groups.

Steps to Create this Graphic

1. Load Packages & Setup

Show code
```{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

Show code
```{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

Show code
```{r}
#| label: examine
#| include: true
#| eval: true
#| results: 'hide'
#| warning: false

glimpse(energy_raw)
```

4. Tidy Data

Show code
```{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

Show code
```{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

Show code
```{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

Show code
```{r}
#| label: save
#| warning: false

### |-  plot image ----  
save_plot(
  p, 
  type = "30daychartchallenge", 
  year = 2026, 
  day = 05, 
  width = 6, 
  height = 8
  )
```

8. Session Info

TipExpand for Session Info
R version 4.3.1 (2023-06-16 ucrt)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 11 x64 (build 26100)

Matrix products: default


locale:
[1] LC_COLLATE=English_United States.utf8 
[2] LC_CTYPE=English_United States.utf8   
[3] LC_MONETARY=English_United States.utf8
[4] LC_NUMERIC=C                          
[5] LC_TIME=English_United States.utf8    

time zone: America/New_York
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] here_1.0.2      ggrepel_0.9.8   glue_1.8.0      scales_1.4.0   
 [5] janitor_2.2.1   showtext_0.9-7  showtextdb_3.0  sysfonts_0.8.9 
 [9] ggtext_0.1.2    lubridate_1.9.5 forcats_1.0.1   stringr_1.6.0  
[13] dplyr_1.2.0     purrr_1.2.1     readr_2.2.0     tidyr_1.3.2    
[17] tibble_3.2.1    ggplot2_4.0.2   tidyverse_2.0.0

loaded via a namespace (and not attached):
 [1] gtable_0.3.6       xfun_0.56          htmlwidgets_1.6.4  tzdb_0.5.0        
 [5] vctrs_0.7.1        tools_4.3.1        generics_0.1.4     curl_7.0.0        
 [9] parallel_4.3.1     gifski_1.32.0-2    pacman_0.5.1       pkgconfig_2.0.3   
[13] RColorBrewer_1.1-3 S7_0.2.0           lifecycle_1.0.5    compiler_4.3.1    
[17] farver_2.1.2       textshaping_1.0.4  codetools_0.2-19   snakecase_0.11.1  
[21] litedown_0.9       htmltools_0.5.9    yaml_2.3.12        pillar_1.11.1     
[25] crayon_1.5.3       camcorder_0.1.0    magick_2.8.6       commonmark_2.0.0  
[29] tidyselect_1.2.1   digest_0.6.39      stringi_1.8.7      labeling_0.4.3    
[33] rsvg_2.6.2         rprojroot_2.1.1    fastmap_1.2.0      grid_4.3.1        
[37] cli_3.6.5          magrittr_2.0.3     withr_3.0.2        bit64_4.6.0-1     
[41] timechange_0.4.0   rmarkdown_2.30     bit_4.6.0          otel_0.2.0        
[45] ragg_1.5.0         hms_1.1.4          evaluate_1.0.5     knitr_1.51        
[49] markdown_2.0       rlang_1.1.7        gridtext_0.1.6     Rcpp_1.1.1        
[53] xml2_1.5.2         svglite_2.1.3      rstudioapi_0.18.0  vroom_1.7.0       
[57] jsonlite_2.0.0     R6_2.6.1           systemfonts_1.3.2 

9. GitHub Repository

TipExpand for GitHub Repo

The complete code for this analysis is available in 30dcc_2026_05.qmd.

For the full repository, click here.

10. References

TipExpand 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

Note📦 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

Back to top

Citation

BibTeX citation:
@online{ponce2026,
  author = {Ponce, Steven},
  title = {Same {Income} {Group,} {Very} {Different} {Power} {Systems}},
  date = {2026-04-05},
  url = {https://stevenponce.netlify.app/data_visualizations/30DayChartChallenge/2026/30dcc_2026_05.html},
  langid = {en}
}
For attribution, please cite this work as:
Ponce, Steven. 2026. “Same Income Group, Very Different Power Systems.” April 5, 2026. https://stevenponce.netlify.app/data_visualizations/30DayChartChallenge/2026/30dcc_2026_05.html.
Source Code
---
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", "Data Visualization", "R Programming", "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"
    self-contained: true
    theme: 
      light: [flatly, assets/styling/custom_styles.scss]
      dark: [darkly, assets/styling/custom_styles_dark.scss]
editor_options: 
  chunk_output_type: inline
execute: 
  freeze: true                                                  
  cache: true                                                   
  error: false
  message: false
  warning: false
  eval: true
---

![Barcode strip plot showing the carbon intensity of electricity (gCO₂ per kWh, 2020–2023 average) for countries grouped by World Bank income classification. Each vertical tick represents one country. A taupe band marks the middle 50% (IQR) and a white-filled dot marks the group median. Groups are sorted from lowest to highest median: High income (250 g), Low income (256 g), Upper middle income (454 g), and Lower middle income (481 g). A dashed vertical line marks the global median. Notable outliers include Norway and Poland (High income), Ethiopia and Syria (Low income), Albania and Turkmenistan (Upper middle income), and Nepal and Uzbekistan (Lower middle income). Within-group variation is wide across all four groups, often rivaling differences between groups.](30dcc_2026_05.png){#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)
:::

© 2024 Steven Ponce

Source Issues