---
title: "Some Countries Are Exiting Fossil Fuels Fast. Others Are Standing Still."
subtitle: "Annualized change in fossil fuel share of electricity generation, 2010–2023 (percentage points per year). Leftward arrows signal progress; rightward arrows signal backsliding."
description: "Arrow chart exploring how quickly 25 countries are reducing their reliance on fossil fuel electricity. Using annualized rates of change from 2010–2023, this visualization reveals a wide gap between fast movers like Denmark (−4.2 pp/year) and countries that have stalled or reversed course. Built as part of the SWD May 2026 Human + AI challenge using R/ggplot2 and Claude."
date: "2026-05-01"
author:
- name: "Steven Ponce"
url: "https://stevenponce.netlify.app"
citation:
url: "https://stevenponce.netlify.app/data_visualizations/SWD%20Challenge/2026/swd_2026_05.html"
categories: ["SWDchallenge", "2026"]
tags: [
"arrow-chart",
"energy",
"fossil-fuels",
"energy-transition",
"climate",
"OWID",
"ggplot2",
"human-AI-collaboration",
"annotation",
"ranking"
]
image: "thumbnails/swd_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
---
### Challenge
Explore how human + AI can work together to create more effective data communication. This month’s challenge invites you to experiment, think critically, and share what you learn.
Additional information can be found [HERE](hhttps://community.storytellingwithdata.com/challenges/may-2026-human-ai)
### Visualization
{#fig-1}
### [**Steps to Create this Graphic**]{.mark}
#### [1. Load Packages & Setup]{.smallcaps}
```{r}
#| label: load
if (!require("pacman")) install.packages("pacman")
pacman::p_load(
tidyverse, ggtext, showtext, janitor,
scales, glue
)
### |- figure size ----
camcorder::gg_record(
dir = here::here("temp_plots"),
device = "png",
width = 10,
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
raw_data <- read_csv(
here::here("data/SWDchallenge/2026/owid-energy-data.csv"),
show_col_types = FALSE
)
```
#### [3. Examine the Data]{.smallcaps}
```{r}
#| label: examine
#| include: true
#| eval: true
#| results: 'hide'
#| warning: false
glimpse(raw_data)
```
#### [4. Tidy Data]{.smallcaps}
```{r}
#| label: tidy
#| output: false
### |- define year window ----
year_start <- 2010
year_end <- 2023
### |- countries to include ----
# G20 + notable peers; filtered to those with complete data in both endpoints
# Excludes aggregates (EU, World) and small island states
selected_countries <- c(
"Australia", "Brazil", "Canada", "Chile", "China",
"Colombia", "Denmark", "France", "Germany", "India",
"Indonesia", "Italy", "Japan", "Mexico", "Netherlands",
"Poland", "Portugal", "South Africa", "South Korea",
"Spain", "Sweden", "Türkiye", "United Kingdom",
"United States", "Vietnam"
)
### |- extract endpoint values ----
df_endpoints <- raw_data |>
filter(
country %in% selected_countries,
year %in% c(year_start, year_end),
!is.na(fossil_share_elec)
) |>
select(country, year, fossil_share_elec) |>
pivot_wider(
names_from = year,
values_from = fossil_share_elec,
names_prefix = "fossil_"
) |>
# drop any country missing either endpoint
filter(!is.na(fossil_2010), !is.na(fossil_2023))
### |- compute annualized exit rate ----
n_years <- year_end - year_start
df_rates <- df_endpoints |>
mutate(
# total pp change (negative = exit; positive = backslide)
pp_change = fossil_2023 - fossil_2010,
# annualized rate in pp/year
annual_rate = pp_change / n_years,
# tier based on annual decline rate — 3 tiers for visual clarity
tier = case_when(
annual_rate <= -1.5 ~ "Fast",
annual_rate < 0 ~ "Slow",
TRUE ~ "Stalled / Backsliding"
),
tier = factor(tier, levels = c(
"Fast", "Slow", "Stalled / Backsliding"
))
) |>
arrange(annual_rate) |>
mutate(country = fct_inorder(country))
### |- label data with outward positioning ----
df_label <- df_rates |>
filter(
row_number() <= 3 |
row_number() > (n() - 3) |
annual_rate > 0
) |>
distinct(country, .keep_all = TRUE) |>
mutate(
label_x = case_when(
annual_rate < 0 ~ annual_rate - 0.10,
annual_rate > 0 ~ annual_rate + 0.10,
TRUE ~ annual_rate
),
label_hjust = case_when(
annual_rate < 0 ~ 1,
annual_rate > 0 ~ 0,
TRUE ~ 0.5
)
)
### |- tier separator y-positions ----
df_tier_breaks <- df_rates |>
mutate(y_int = as.integer(country)) |>
group_by(tier) |>
summarise(y_min = min(y_int), .groups = "drop") |>
filter(tier != first(levels(df_rates$tier))) |>
mutate(separator = y_min - 0.5)
```
#### [5. Visualization Parameters]{.smallcaps}
```{r}
#| label: params
### |- plot aesthetics ----
colors <- get_theme_colors(
palette = list(
"background" = "#F5F3EE",
"text" = "#2C2C2C",
"subtext" = "#5C5C5C",
"grid" = "#E8E5DF",
"fast" = "#1A6B3C",
"slow" = "#A8C5A0",
"stalled" = "#8B3030"
)
)
bg_col <- colors$palette$background
text_col <- colors$palette$text
sub_col <- colors$palette$subtext
grid_col <- colors$palette$grid
# tier color lookup — 3 tiers
tier_colors <- c(
"Fast" = colors$palette$fast,
"Slow" = colors$palette$slow,
"Stalled / Backsliding" = colors$palette$stalled
)
### |- titles and caption ----
title_text <- "Some Countries Are Exiting Fossil Fuels Fast. Others Are Standing Still."
subtitle_text <- glue(
"Annualized change in fossil fuel share of electricity generation, ",
"{year_start}–{year_end} (percentage points per year).<br>",
"Leftward arrows signal progress; rightward arrows signal backsliding."
)
caption_text <- create_swd_caption(
year = 2026,
month = "May",
source_text = "Our World in Data | Energy Mix (Ritchie, Rosado & Roser, 2024)"
)
### |- fonts ----
setup_fonts()
fonts <- get_font_families()
### |- base theme ----
base_theme <- create_base_theme(colors)
weekly_theme <- extend_weekly_theme(
base_theme,
theme(
plot.background = element_rect(fill = bg_col, color = NA),
panel.background = element_rect(fill = bg_col, color = NA),
# grid: vertical only
panel.grid.major.x = element_line(color = grid_col, linewidth = 0.3),
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank(),
# axes
axis.title.x = element_text(
color = sub_col, size = 8.5, family = fonts$text,
margin = margin(t = 6)
),
axis.title.y = element_blank(),
axis.text.x = element_text(color = sub_col, size = 8, family = fonts$text),
axis.text.y = element_text(
color = text_col, size = 8.5, family = fonts$text, hjust = 1
),
axis.ticks = element_blank(),
# no legend
legend.position = "none",
# titles
plot.title = element_text(
family = fonts$title, face = "bold",
size = 17, color = text_col,
margin = margin(b = 6)
),
plot.subtitle = element_markdown(
family = fonts$text, size = 9, color = sub_col,
lineheight = 1.45, margin = margin(b = 14)
),
plot.caption = element_markdown(
family = fonts$text, size = 7.5, color = sub_col,
hjust = 0, margin = margin(t = 12)
),
plot.margin = margin(20, 24, 14, 60)
)
)
theme_set(weekly_theme)
### |- tier bracket annotations ----
df_tier_labels <- df_rates |>
mutate(y_int = as.integer(country)) |>
group_by(tier) |>
summarise(
y_top = max(y_int) + 0.35,
y_bot = min(y_int) - 0.35,
y_mid = (max(y_int) + min(y_int)) / 2,
.groups = "drop"
) |>
mutate(
label_color = tier_colors[as.character(tier)],
label_text = case_when(
tier == "Stalled / Backsliding" ~ "Stalled /\nBacksliding",
TRUE ~ as.character(tier)
),
y_label = case_when(
tier == "Stalled / Backsliding" ~ y_mid - 0.10,
tier == "Fast" ~ y_mid + 0.10,
TRUE ~ y_mid
),
label_color = case_when(
tier == "Slow" ~ "#7A9478",
TRUE ~ label_color
)
)
```
#### [6. Plot]{.smallcaps}
```{r}
#| label: plot
#| output: false
### |- main plot ----
p <- ggplot(df_rates, aes(y = country, color = tier)) +
# Geoms
geom_hline(
data = df_tier_breaks,
aes(yintercept = separator),
color = grid_col,
linewidth = 0.45,
linetype = "solid",
inherit.aes = FALSE
) +
geom_vline(
xintercept = 0,
color = sub_col,
linewidth = 0.8,
linetype = "solid"
) +
geom_segment(
aes(
x = 0,
xend = annual_rate,
yend = country,
alpha = tier
),
linewidth = 0.7,
lineend = "butt",
arrow = arrow(
length = unit(0.18, "cm"),
type = "closed"
)
) +
geom_text(
data = df_label,
aes(
x = label_x,
y = country,
label = sprintf("%+.1f", annual_rate),
hjust = label_hjust
),
vjust = 0.5,
size = 2.8,
family = fonts$text,
color = text_col,
fontface = "plain",
inherit.aes = FALSE
) +
# Annotate
annotate(
"segment",
x = -4.45,
xend = -4.45,
y = df_tier_labels$y_bot,
yend = df_tier_labels$y_top,
color = df_tier_labels$label_color,
linewidth = 0.4,
alpha = 0.75
) +
annotate(
"text",
x = -4.36,
y = df_tier_labels$y_label,
label = df_tier_labels$label_text,
color = df_tier_labels$label_color,
size = 2.25,
hjust = 0,
vjust = 0.5,
fontface = "bold",
family = fonts$text
) +
# Scales
scale_color_manual(values = tier_colors) +
scale_alpha_manual(
values = c(
"Fast" = 1,
"Slow" = 0.65,
"Stalled / Backsliding" = 1
),
guide = "none"
) +
scale_x_continuous(
name = "Change in fossil fuel share (pp per year)",
labels = label_number(suffix = " pp", style_positive = "plus"),
breaks = c(-4, -3, -2, -1, 0, 1),
limits = c(-4.75, 1.20),
expand = expansion(mult = c(0, 0))
) +
guides(color = "none") +
coord_cartesian(clip = "off") +
# Labs
labs(
title = title_text,
subtitle = subtitle_text,
caption = caption_text,
y = NULL
)
```
#### [7. Save]{.smallcaps}
```{r}
#| label: save
### |- plot image ----
save_plot(
p,
type = 'swd',
year = 2026,
month = 05,
width = 10,
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 [`swd_2026_05.qmd`](https://github.com/poncest/personal-website/tree/master/data_visualizations/SWD%20Challenge/2026/swd_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
**SWD Challenge:**
- Storytelling with Data: [human + AI](https://community.storytellingwithdata.com/challenges/apr-2026-visualize-a-timeline)
**Data Sources:**
- All data are synthetic and illustrative. Timelines were constructed to reflect
plausible oncology drug development and launch timing ranges. No specific product,
company, or trial is depicted.
**Background References:**
- U.S. Food & Drug Administration. (2024). *Novel Drug Approvals for 2024*.
<https://www.fda.gov/patients/drug-development-process/step-3-clinical-research>
- Deloitte. (2024). *Measuring the return from pharmaceutical innovation*.
<https://www.deloitte.com/global/en/Industries/life-sciences/research/measuring-return-pharmaceutical-innovation.html>
**Book Reference:**
- Knaflic, C. N. (2019). *storytelling with data: let's practice!* Wiley.
:::
#### [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)
:::