---
title: "Youth unemployment in London is rising faster than the overall trend"
subtitle: "Since late 2024, unemployment among 16–24 year-olds has risen faster than the overall 16+ total, widening the gap | 4-quarter moving average of estimated unemployed counts (thousands)"
description: "A redesign of the MakeoverMonday 2026 Week 12 chart on London unemployment. While the original showed only the overall unemployment rate, this makeover adds a youth (16–24) series to tell the article's core story: young Londoners are being hit hardest. Built in R with ggplot2, the chart uses a ribbon gap and policy event annotations to show how the youth-overall divergence has widened since the October 2024 Budget announcement."
date: "2026-03-23"
author:
- name: "Steven Ponce"
url: "https://stevenponce.netlify.app"
citation:
url: "https://stevenponce.netlify.app/data_visualizations/MakeoverMonday/2026/mm_2026_12.html"
categories: ["MakeoverMonday", "2026"]
tags: [
"makeover-monday",
"unemployment",
"labour-market",
"time-series",
"line-chart",
"ribbon-chart",
"gap-visualization",
"youth",
"london",
"uk",
"policy",
"ggplot2",
]
image: "thumbnails/mm_2026_12.png"
format:
html:
toc: true
toc-depth: 5
code-link: true
code-fold: true
code-tools: true
code-summary: "Show code"
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
editor:
markdown:
wrap: 72
---
```{r}
#| label: setup-links
#| include: false
# CENTRALIZED LINK MANAGEMENT
## Project-specific info
current_year <- 2026
current_week <- 12
project_file <- "mm_2026_12.qmd"
project_image <- "mm_2026_12.png"
## Data Sources
data_main <- "https://data.world/makeovermonday/2026w12-uk-unemployment-estimates"
data_secondary <- "https://data.world/makeovermonday/2026w12-uk-unemployment-estimates"
## Repository Links
repo_main <- "https://github.com/poncest/personal-website/"
repo_file <- paste0("https://github.com/poncest/personal-website/blob/master/data_visualizations/MakeoverMonday/", current_year, "/", project_file)
## External Resources/Images
chart_original <- "https://raw.githubusercontent.com/poncest/MakeoverMonday/refs/heads/master/2026/Week_12/original_chart.png"
## Organization/Platform Links
org_primary <- "https://data.london.gov.uk/blog/unemployment-in-key-charts-young-londoners-hit-hardest-by-labour-market-slowdown/"
org_secondary <- "https://data.london.gov.uk/blog/unemployment-in-key-charts-young-londoners-hit-hardest-by-labour-market-slowdown/"
# Helper function to create markdown links
create_link <- function(text, url) {
paste0("[", text, "](", url, ")")
}
# Helper function for citation-style links
create_citation_link <- function(text, url, title = NULL) {
if (is.null(title)) {
paste0("[", text, "](", url, ")")
} else {
paste0("[", text, "](", url, ' "', title, '")')
}
}
```
### Original
The original visualization comes from `r create_link("London Unemployment Estimates", data_secondary)`

### Makeover
{#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({
if (!require("pacman")) install.packages("pacman")
pacman::p_load(
tidyverse, ggtext, showtext, scales,
glue, janitor, readxl, zoo
)
})
### |- 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
#| include: true
#| eval: true
#| warning: false
#|
df_raw <- readxl::read_xlsx(
here::here("data/MakeoverMonday/2026/London Unemployment Estimates.xlsx")) |>
clean_names()
```
#### [3. Examine the Data]{.smallcaps}
```{r}
#| label: examine
#| include: true
#| eval: true
#| results: 'hide'
#| warning: false
glimpse(df_raw)
skimr::skim_without_charts(df_raw)
```
#### [4. Tidy Data]{.smallcaps}
```{r}
#| label: tidy
#| warning: false
### |- filter to counts only ----
df_counts <- df_raw |>
filter(unit == "Estimated Number of People")
### |- focal series: youth (16-24) and overall (16+) ----
df_main <- df_counts |>
mutate(date = as.Date(period_midpoint_date)) |>
filter(date >= as.Date("2019-01-01")) |>
mutate(
youth_k = x16_24 / 1000,
overall_k = all_aged_16_over / 1000
) |>
arrange(date) |>
select(date, period, youth_k, overall_k)
### |- 4-period rolling average ----
df_main <- df_main |>
mutate(
youth_smooth = rollmean(youth_k, k = 4, fill = NA, align = "right"),
overall_smooth = rollmean(overall_k, k = 4, fill = NA, align = "right")
) |>
filter(!is.na(youth_smooth), !is.na(overall_smooth)) |>
mutate(
gap_k = overall_smooth - youth_smooth,
gap_mid_k = youth_smooth + gap_k / 2
)
### |- latest values for labels ----
latest <- df_main |>
slice_max(date, n = 1)
latest_youth <- round(latest$youth_smooth, 1)
latest_overall <- round(latest$overall_smooth, 1)
latest_gap <- round(latest$gap_k, 1)
### |- x positions for labels / callouts ----
x_label_end <- latest$date + 35
x_gap_label <- latest$date - 60
### |- y positions for stacked end labels ----
y_overall_lab_top <- latest$overall_smooth + 7
y_overall_lab_val <- latest$overall_smooth - 3
y_youth_lab_top <- latest$youth_smooth + 7
y_youth_lab_val <- latest$youth_smooth - 3
### |- policy event windows ----
announce_start <- as.Date("2024-10-15")
announce_end <- as.Date("2024-11-15")
implement_start <- as.Date("2025-03-15")
implement_end <- as.Date("2025-05-15")
```
#### [5. Visualization Parameters]{.smallcaps}
```{r}
#| label: params
#| include: true
#| warning: false
### |- Colors ----
colors <- get_theme_colors(
palette = list(
youth = "#7B1E3A",
overall = "#2C3E50",
gap = "#F8E6E3",
zone = "#D5DBDB",
gap_txt = "#A93226"
)
)
### |- Titles and caption ----
title_text <- "Youth unemployment in London is rising faster than the overall trend"
subtitle_text <- paste0(
"Since late 2024, unemployment among 16–24 year-olds has risen faster than the overall 16+ total, widening the gap.<br>",
"<span style='font-size:9pt;'>4-quarter moving average of estimated unemployed counts (thousands)</span>"
)
caption_text <- create_mm_caption(
mm_year = 2026,
mm_week = 12,
source_text = "GLA Economics, ONS Quarterly Labour Force Survey<br>
**Note:** Counts shown are estimated unemployed people, not unemployment rates.<br>
Policy zones mark the Oct 2024 Budget announcement and Apr 2025 NIC & minimum wage implementation."
)
### |- fonts ----
setup_fonts()
fonts <- get_font_families()
### |- plot theme ----
base_theme <- create_base_theme(colors)
weekly_theme <- extend_weekly_theme(
base_theme,
theme(
axis.title = element_blank(),
axis.text.x = element_text(size = 9, color = "gray40"),
axis.text.y = element_text(size = 9, color = "gray40"),
axis.line.x = element_line(color = "gray82", linewidth = 0.4),
panel.grid.major.y = element_line(color = "gray90", linewidth = 0.35),
panel.grid.major.x = element_blank(),
panel.grid.minor = element_blank(),
legend.position = "none",
plot.margin = margin(t = 16, r = 55, b = 12, l = 16),
plot.title = element_text(
size = rel(1.3),
family = fonts$title,
face = "bold",
color = colors$title,
lineheight = 1.05,
hjust = 0,
margin = margin(t = 5, b = 10)
),
plot.subtitle = element_markdown(
size = rel(0.75),
family = fonts$subtitle,
face = "italic",
color = alpha(colors$subtitle, 0.92),
lineheight = 1.08,
margin = margin(t = 0, b = 20)
),
plot.caption = element_markdown(
size = rel(0.5),
family = fonts$subtitle,
color = colors$caption,
hjust = 0,
lineheight = 1.35,
margin = margin(t = 20, b = 5)
)
)
)
theme_set(weekly_theme)
```
#### [6. Plot]{.smallcaps}
```{r}
#| label: plot
#| warning: false
### |- main plot ----
p <- ggplot(df_main, aes(x = date)) +
# Annotate
annotate(
"rect",
xmin = announce_start, xmax = announce_end,
ymin = -Inf, ymax = Inf,
fill = colors$palette$zone,
alpha = 0.28
) +
annotate(
"rect",
xmin = implement_start, xmax = implement_end,
ymin = -Inf, ymax = Inf,
fill = colors$palette$zone,
alpha = 0.28
) +
# Geoms
geom_ribbon(
aes(
ymin = pmin(youth_smooth, overall_smooth),
ymax = pmax(youth_smooth, overall_smooth)
),
fill = colors$palette$gap,
alpha = 0.45
) +
geom_line(
aes(y = overall_smooth),
color = colors$palette$overall,
linewidth = 0.95,
lineend = "round"
) +
geom_line(
aes(y = youth_smooth),
color = colors$palette$youth,
linewidth = 1.25,
lineend = "round"
) +
geom_point(
data = latest,
aes(y = overall_smooth),
color = colors$palette$overall,
size = 3.6
) +
geom_point(
data = latest,
aes(y = youth_smooth),
color = colors$palette$youth,
size = 3.6
) +
# Annotate
annotate(
"text",
x = x_label_end,
y = y_overall_lab_top,
label = "16+ overall",
color = colors$palette$overall,
hjust = 0,
vjust = 0.5,
size = 2.8,
family = fonts$text
) +
annotate(
"text",
x = x_label_end,
y = y_overall_lab_val,
label = glue("{latest_overall}k"),
color = colors$palette$overall,
hjust = 0,
vjust = 0.5,
size = 3.8,
fontface = "bold",
family = fonts$text
) +
annotate(
"text",
x = x_label_end,
y = y_youth_lab_top,
label = "16–24 youth",
color = colors$palette$youth,
hjust = 0,
vjust = 0.5,
size = 2.8,
family = fonts$text
) +
annotate(
"text",
x = x_label_end,
y = y_youth_lab_val,
label = glue("{latest_youth}k"),
color = colors$palette$youth,
hjust = 0,
vjust = 0.5,
size = 3.8,
fontface = "bold",
family = fonts$text
) +
annotate(
"text",
x = announce_start,
y = Inf,
label = "Budget\n(Oct 2024)",
color = "gray48",
hjust = 0.5,
vjust = 1.35,
size = 2.7,
family = fonts$text
) +
annotate(
"text",
x = implement_start,
y = Inf,
label = "Implementation\n(Apr 2025)",
color = "gray48",
hjust = 0.5,
vjust = 3.7,
size = 2.7,
family = fonts$text
) +
annotate(
"curve",
x = x_gap_label + 12,
xend = latest$date - 5,
y = latest$gap_mid_k + 10,
yend = latest$gap_mid_k + 2,
curvature = -0.2,
linewidth = 0.35,
color = alpha(colors$palette$gap_txt, 0.8)
) +
annotate(
"text",
x = x_gap_label,
y = latest$gap_mid_k + 12,
label = glue("+{latest_gap}k gap"),
color = colors$palette$gap_txt,
hjust = 0,
vjust = 0.5,
size = 3.1,
fontface = "bold",
family = fonts$text
) +
annotate(
"text",
x = as.Date("2025-08-01"),
y = latest$gap_mid_k - 12,
label = "Largest gap in the recent period",
color = alpha(colors$palette$gap_txt, 0.9),
hjust = 0,
vjust = 0.5,
size = 2.7,
family = fonts$text
) +
# Scales
scale_x_date(
date_breaks = "1 year",
date_labels = "Q4 %Y",
expand = expansion(mult = c(0.01, 0.13))
) +
scale_y_continuous(
labels = label_number(suffix = "k", accuracy = 1),
breaks = c(100, 200, 300, 400),
expand = expansion(mult = c(0.06, 0.12))
) +
# Labs
labs(
title = title_text,
subtitle = subtitle_text,
caption = caption_text
)
```
#### [7. Save]{.smallcaps}
```{r}
#| label: save
#| warning: false
### |- plot image ----
save_plot(
plot = p,
type = "makeovermonday",
year = current_year,
week = current_week,
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 `r create_link(project_file, repo_file)`.
For the full repository, `r create_link("click here", repo_main)`.
:::
#### [10. References]{.smallcaps}
::: {.callout-tip collapse="true"}
##### Expand for References
**Primary Data (Makeover Monday):**
1. Makeover Monday `r current_year` Week `r current_week`: `r create_link("London Unemployment Estimates", data_main)`
2. Original Article: `r create_link("Unemployment in key charts: Young Londoners hit hardest by labour market slowdown", "https://data.london.gov.uk/blog/unemployment-in-key-charts-young-londoners-hit-hardest-by-labour-market-slowdown/")`
- Source: GLA Economics (Greater London Authority)
- Coverage: London unemployment trends by age group, with policy event context
**Source Data:**
3. Dataset: `r create_link("2026 Week 12 — London Unemployment Estimates", "https://data.world/makeovermonday/2026w12-uk-unemployment-estimates")`
- Source: ONS Quarterly Labour Force Survey via data.world/makeovermonday
- Data includes: Rolling 3-month period estimates of unemployed counts by age group (16–17, 18–24, 16–24, 25–34, 35–49, 50–64, 16–64, 65+) for London, Jun 2001–Jan 2026
- Scope: 294 rolling periods; counts are estimated numbers of unemployed people, not labor-force-denominated rates
4. ONS Regional Unemployment by Age: `r create_link("Dataset x02", "https://www.ons.gov.uk/employmentandlabourmarket/peoplenotinwork/unemployment/datasets/regionalunemploymentbyagex02")`
- Source: Office for National Statistics
- Coverage: Regional unemployment by age, used as original source context
**Note:** Counts shown are estimated unemployed people (thousands), not unemployment rates. The smoothed series uses a 4-period rolling average to match the original chart's approach. Policy zones mark the October 2024 Autumn Budget announcement (NIC increase from 13.8% to 15%) and the April 2025 implementation of NIC and minimum wage changes for 18–20 year-olds. No population denominators or external adjustment data were applied.
:::
#### [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)
:::