• 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

The Planet Still Breathes

  • Show All Code
  • Hide All Code

  • View Source

Decade-averaged CO₂ seasonal cycle at Mauna Loa, 1960s–2020s. Each curve shows how atmospheric CO₂ rises and falls across the year — driven by Northern Hemisphere plant growth and decay. +101 ppm higher baseline since the 1960s (~320 ppm to ~421 ppm)

30DayChartChallenge
Data Visualization
R Programming
2026
This chart isolates the seasonal ‘breath’ of atmospheric CO₂ from the Mauna Loa Observatory record — the annual rise and fall driven by Northern Hemisphere plant growth and decay. By plotting decade-averaged seasonal anomalies (deviation from the long-run trend) for four decades from the 1960s to the 2020s, the chart reveals a striking pattern: the shape of Earth’s annual CO₂ cycle has remained remarkably stable across 60 years, even as the absolute baseline has risen by +101 ppm. Built with R and ggplot2 using NOAA GML monthly mean data.
Author

Steven Ponce

Published

April 23, 2026

Figure 1: Line chart titled “The Planet Still Breathes” showing the decade-averaged seasonal CO₂ cycle at Mauna Loa Observatory across four decades (1960s–2020s). The x-axis spans January through December; the y-axis shows seasonal anomaly in parts per million (ppm), ranging from −4 to +4 ppm. Four smoothed curves — three in fading gray (1960s, 1980s, 2000s) and one in deep burgundy (2020s) — trace an identical breathing pattern: rising from ~0 ppm in January to a peak of ~+3.3 ppm in May, then falling to a trough of ~−3.6 ppm in September before recovering through December. The near-perfect overlap of all four curves illustrates that the seasonal amplitude (~6–8 ppm) has remained stable across 60 years. A subtitle annotation notes that the absolute CO₂ baseline has risen +101 ppm since the 1960s (~320 ppm to ~421 ppm) — a change invisible in the waveform itself.

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

### |- 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

Show code
```{r}
#| label: read
#| include: true
#| eval: true
#| warning: false

energy_raw <- read_csv(here("data/30DayChartChallenge/2026/owid-energy-data.csv"))
### |- read and parse NOAA MLO monthly CO2 ----
# Comment lines start with #; no formal header row
# Missing values coded as -99.99

co2_raw <- read_csv(
  here::here("data/30DayChartChallenge/2026/co2_mm_mlo.csv"),
  comment    = "#",
  col_names  = c("year", "month", "decimal_date", "average",
                 "deseasonalized", "ndays", "sdev", "unc"),
  col_types  = cols(.default = col_double())
) |>
  filter(!is.na(year))    # drop the header row that slips past comment="#"
```

3. Examine the Data

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

glimpse(co2_raw)
range(co2_raw$year)
sum(co2_raw$average < 0)          # count missing (-99.99 sentinel)
```

4. Tidy Data

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

### |- clean and derive seasonal anomaly ----
co2_clean <- co2_raw |>
  filter(average > 0, deseasonalized > 0) |>
  mutate(
    seasonal_anomaly = average - deseasonalized,
    decade = floor(year / 10) * 10
  ) |>
  filter(decade >= 1960)

### |- decade-averaged seasonal waveform ----
# Keep 4 key decades: 1960s (baseline), 1980s, 2000s, 2020s (now)
key_decades <- c(1960, 1980, 2000, 2020)

co2_waveform_raw <- co2_clean |>
  filter(decade %in% key_decades) |>
  group_by(decade, month) |>
  summarise(
    mean_anomaly = mean(seasonal_anomaly, na.rm = TRUE),
    .groups = "drop"
  )

# Spline-interpolate each decade curve to 120 points (10 per month interval)
# This converts the angular 12-point polygon into a smooth breathing waveform
co2_waveform <- co2_waveform_raw |>
  group_by(decade) |>
  group_modify(~ {
    spl <- splinefun(.x$month, .x$mean_anomaly, method = "periodic")
    month_seq <- seq(1, 12, length.out = 120)
    tibble(month = month_seq, mean_anomaly = spl(month_seq))
  }) |>
  ungroup() |>
  mutate(
    decade_fct = factor(decade, levels = key_decades),
    month_abb  = month.abb[round(month)]
  )

### |- annotation data: decade mean CO2 for annotation text ----
decade_means <- co2_clean |>
  group_by(decade) |>
  summarise(mean_co2 = mean(average, na.rm = TRUE), .groups = "drop")

# values for annotation:
co2_1960s <- decade_means |>
  filter(decade == 1960) |> pull(mean_co2) |> round(0)

co2_2020s <- decade_means |> 
  filter(decade == 2020) |> pull(mean_co2) |> round(0)

### |- x-axis month labels ----
month_labels <- month.abb   

### |- direct end-line labels — 1960s and 2020s only ----
label_data <- co2_waveform |>
  group_by(decade) |>
  slice_max(month, n = 1) |>
  ungroup() |>
  filter(decade %in% c(1960, 2020)) |>
  mutate(label = glue("{decade}s"))

### |- zero reference line data ----
zero_line <- tibble(month = 1:12, y = 0)

### |- split data: context (faded) vs hero (2020s) ----
co2_context <- co2_waveform |> filter(decade != 2020)
co2_hero    <- co2_waveform |> filter(decade == 2020)
```

5. Visualization Parameters

Show code
```{r}
#| label: params
#| include: true
#| warning: false

### |- plot aesthetics ----
colors <- get_theme_colors(
  palette = list(
    "1960" = "#C2C2C2",    
    "1980" = "#9E9E9E",   
    "2000" = "#707070",   
    "2020" = "#6B0F1A",   
    "bg"   = "#F5F2EE",   
    "text" = "#2C2C2C",   
    "grid" = "#DDD9D4"    
  )
)

# extract as plain scalars 
decade_colors <- c(
  "1960" = colors$palette[["1960"]],
  "1980" = colors$palette[["1980"]],
  "2000" = colors$palette[["2000"]],
  "2020" = colors$palette[["2020"]]
)

# line widths
decade_linewidths <- c(
  "1960" = 0.7,
  "1980" = 0.7,
  "2000" = 0.7,
  "2020" = 1.5
)

col_bg   <- colors$palette[["bg"]]
col_text <- colors$palette[["text"]]
col_grid <- colors$palette[["grid"]]
col_2020 <- colors$palette[["2020"]]   

### |- titles and caption ----
title_text    <- "The Planet Still Breathes"

subtitle_text <- str_glue(
  "Decade-averaged CO₂ seasonal cycle at Mauna Loa, 1960s–2020s.<br>",
  "Each curve shows how atmospheric CO₂ rises and falls across the year — ",
  "driven by Northern Hemisphere plant growth and decay.<br>",
  "**+{co2_2020s - co2_1960s} ppm higher baseline since the 1960s** ",
  "(~{co2_1960s} ppm to ~{co2_2020s} ppm)"
)

caption_text  <- create_dcc_caption(
  dcc_year    = 2026,
  dcc_day     = 23,
  source_text = "NOAA Global Monitoring Laboratory · Mauna Loa CO₂ monthly means"
)

### |- x-axis month labels ----
month_labels <- month.abb   # "Jan" … "Dec"

### |- direct end-line labels — 1960s and 2020s only ----
label_data <- co2_waveform |>
  group_by(decade) |>
  slice_max(month, n = 1) |>
  ungroup() |>
  filter(decade %in% c(1960, 2020)) |>
  mutate(label = glue("{decade}s"))

### |- fonts ----
setup_fonts()
fonts <- get_font_families()

### |- base and weekly theme ----
base_theme <- create_base_theme(colors)

weekly_theme <- extend_weekly_theme(
  base_theme,
  theme(
    # background
    plot.background = element_rect(fill = col_bg, color = NA),
    panel.background = element_rect(fill = col_bg, color = NA),

    # grid — horizontal reference lines only; vertical absent
    panel.grid.major.y = element_line(color = col_grid, linewidth = 0.3),
    panel.grid.major.x = element_blank(),
    panel.grid.minor = element_blank(),

    # axes
    axis.ticks = element_blank(),
    axis.text.x = element_text(
      family = fonts$text, size = 9, color = col_text, margin = margin(t = 4)
    ),
    axis.text.y = element_text(
      family = fonts$text, size = 9, color = col_text
    ),
    axis.title.x = element_blank(),
    axis.title.y = element_text(
      family = fonts$text, size = 9.5, color = col_text,
      margin = margin(r = 8), angle = 90
    ),

    # text hierarchy
    plot.title = element_text(
      family = fonts$title, size = 28, face = "bold",
      color = col_text, margin = margin(b = 6)
    ),
    plot.subtitle = element_markdown(
      family = fonts$text, size = 10, color = col_text,
      lineheight = 1.45, margin = margin(b = 20)
    ),
    plot.caption = element_markdown(
      family = fonts$text, size = 7.5, color = "#888888",
      hjust = 0, margin = margin(t = 16)
    ),

    # margins
    plot.margin = margin(t = 20, r = 60, b = 12, l = 20)
  )
)

theme_set(weekly_theme)
```

6. Plot

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

### |- main plot ----
p <- ggplot() +

  # Geoms
  geom_hline(yintercept = 0, color = col_grid, linewidth = 0.6) +
  geom_line(
    data = co2_context,
    aes(x = month, y = mean_anomaly, color = decade_fct, group = decade_fct),
    linewidth = 0.7, lineend = "round", linejoin = "round"
  ) +
  geom_line(
    data = co2_hero,
    aes(x = month, y = mean_anomaly, group = decade_fct),
    color = col_2020,
    linewidth = 1.6, lineend = "round", linejoin = "round"
  ) +
  geom_text(
    data = label_data,
    aes(x = month + 0.15, y = mean_anomaly, label = label, color = decade_fct),
    hjust = 0,
    size = 3.2,
    family = fonts$text,
    fontface = "bold",
    inherit.aes = FALSE
  ) +

  # Annotate
  annotate(
    geom = "richtext",
    x = 6.2,
    y = 3.9,
    label = glue(
      "<b>Same seasonal breath (~6–8 ppm)</b><br>",
      "Amplitude stable across all 4 decades"
    ),
    hjust = 0,
    vjust = 1,
    size = 3.3,
    family = fonts$text,
    color = col_text,
    fill = NA,
    label.color = NA,
    lineheight = 1.4
  ) +

  # Scales
  scale_x_continuous(
    breaks = 1:12,
    labels = month_labels,
    expand = expansion(mult = c(0.02, 0.12))
  ) +
  scale_y_continuous(
    breaks = seq(-4, 4, by = 2),
    labels = function(x) glue("{x} ppm"),
    limits = c(-4.2, 4.2)
  ) +
  scale_color_manual(values = decade_colors) +
  guides(color = "none") +
  coord_cartesian(clip = "off") +

  # Labs
  labs(
    title    = title_text,
    subtitle = subtitle_text,
    caption  = caption_text,
    y        = "Seasonal anomaly (CO₂ ppm, deviation from trend)",
    color    = NULL
  )
```

7. Save

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

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

8. Session Info

TipExpand for Session Info
R version 4.5.3 (2026-03-11 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 26100)

Matrix products: default
  LAPACK version 3.12.1

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      glue_1.8.0      scales_1.4.0    janitor_2.2.1  
 [5] showtext_0.9-8  showtextdb_3.0  sysfonts_0.8.9  ggtext_0.1.2   
 [9] lubridate_1.9.5 forcats_1.0.1   stringr_1.6.0   dplyr_1.2.1    
[13] purrr_1.2.2     readr_2.2.0     tidyr_1.3.2     tibble_3.3.1   
[17] ggplot2_4.0.2   tidyverse_2.0.0

loaded via a namespace (and not attached):
 [1] gtable_0.3.6       xfun_0.57          htmlwidgets_1.6.4  tzdb_0.5.0        
 [5] vctrs_0.7.3        tools_4.5.3        generics_0.1.4     curl_7.0.0        
 [9] parallel_4.5.3     gifski_1.32.0-2    pacman_0.5.1       pkgconfig_2.0.3   
[13] RColorBrewer_1.1-3 S7_0.2.1           lifecycle_1.0.5    compiler_4.5.3    
[17] farver_2.1.2       textshaping_1.0.5  codetools_0.2-20   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.9.1       commonmark_2.0.0  
[29] tidyselect_1.2.1   digest_0.6.39      stringi_1.8.7      rsvg_2.7.0        
[33] rprojroot_2.1.1    fastmap_1.2.0      grid_4.5.3         cli_3.6.6         
[37] magrittr_2.0.5     withr_3.0.2        bit64_4.6.0-1      timechange_0.4.0  
[41] rmarkdown_2.31     bit_4.6.0          otel_0.2.0         ragg_1.5.2        
[45] hms_1.1.4          evaluate_1.0.5     knitr_1.51         markdown_2.0      
[49] rlang_1.2.0        gridtext_0.1.6     Rcpp_1.1.1         xml2_1.5.2        
[53] svglite_2.2.2      rstudioapi_0.18.0  vroom_1.7.1        jsonlite_2.0.0    
[57] 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_23.qmd.

For the full repository, click here.

10. References

TipExpand for References
  1. Data Sources:
    • Thoning, K.W., Crotwell, A.M., and Mund, J.W. (2025). Atmospheric Carbon Dioxide Dry Air Mole Fractions from continuous measurements at Mauna Loa, Hawaii. National Oceanic and Atmospheric Administration (NOAA), Global Monitoring Laboratory (GML), Boulder, Colorado, USA. https://doi.org/10.15138/yaf1-bk21 Retrieved from: https://gml.noaa.gov/aftp/ccg/co2/trends/co2_mm_mlo.csv

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 = {The {Planet} {Still} {Breathes}},
  date = {2026-04-23},
  url = {https://stevenponce.netlify.app/data_visualizations/30DayChartChallenge/2026/30dcc_2026_23.html},
  langid = {en}
}
For attribution, please cite this work as:
Ponce, Steven. 2026. “The Planet Still Breathes.” April 23, 2026. https://stevenponce.netlify.app/data_visualizations/30DayChartChallenge/2026/30dcc_2026_23.html.
Source Code
---
title: "The Planet Still Breathes"
subtitle: "Decade-averaged CO₂ seasonal cycle at Mauna Loa, 1960s–2020s. Each curve shows how atmospheric CO₂ rises and falls across the year — driven by Northern Hemisphere plant growth and decay. +101 ppm higher baseline since the 1960s (~320 ppm to ~421 ppm)"
description: "This chart isolates the seasonal 'breath' of atmospheric CO₂ from the Mauna Loa Observatory record — the annual rise and fall driven by Northern Hemisphere plant growth and decay. By plotting decade-averaged seasonal anomalies (deviation from the long-run trend) for four decades from the 1960s to the 2020s, the chart reveals a striking pattern: the shape of Earth's annual CO₂ cycle has remained remarkably stable across 60 years, even as the absolute baseline has risen by +101 ppm. Built with R and ggplot2 using NOAA GML monthly mean data."
date: "2026-04-23" 
author:
  - name: "Steven Ponce"
    url: "https://stevenponce.netlify.app"
citation:
  url: "https://stevenponce.netlify.app/data_visualizations/30DayChartChallenge/2026/30dcc_2026_23.html"
categories: ["30DayChartChallenge", "Data Visualization", "R Programming", "2026"]
tags: [
  "30DayChartChallenge",
  "Timeseries",
  "Seasons",
  "Keeling Curve",
  "CO2",
  "Climate Change",
  "Seasonal Cycle",
  "Line Chart",
  "NOAA",
  "Mauna Loa",
  "ggplot2",
  "spline interpolation"
]
image: "thumbnails/30dcc_2026_23.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
---

![Line chart titled "The Planet Still Breathes" showing the decade-averaged seasonal CO₂ cycle at Mauna Loa Observatory across four decades (1960s–2020s). The x-axis spans January through December; the y-axis shows seasonal anomaly in parts per million (ppm), ranging from −4 to +4 ppm. Four smoothed curves — three in fading gray (1960s, 1980s, 2000s) and one in deep burgundy (2020s) — trace an identical breathing pattern: rising from ~0 ppm in January to a peak of ~+3.3 ppm in May, then falling to a trough of ~−3.6 ppm in September before recovering through December. The near-perfect overlap of all four curves illustrates that the seasonal amplitude (~6–8 ppm) has remained stable across 60 years. A subtitle annotation notes that the absolute CO₂ baseline has risen +101 ppm since the 1960s (~320 ppm to ~421 ppm) — a change invisible in the waveform itself.](30dcc_2026_23.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
  )
})

### |- 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

energy_raw <- read_csv(here("data/30DayChartChallenge/2026/owid-energy-data.csv"))
### |- read and parse NOAA MLO monthly CO2 ----
# Comment lines start with #; no formal header row
# Missing values coded as -99.99

co2_raw <- read_csv(
  here::here("data/30DayChartChallenge/2026/co2_mm_mlo.csv"),
  comment    = "#",
  col_names  = c("year", "month", "decimal_date", "average",
                 "deseasonalized", "ndays", "sdev", "unc"),
  col_types  = cols(.default = col_double())
) |>
  filter(!is.na(year))    # drop the header row that slips past comment="#"
```

#### [3. Examine the Data]{.smallcaps}

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

glimpse(co2_raw)
range(co2_raw$year)
sum(co2_raw$average < 0)          # count missing (-99.99 sentinel)
```

#### [4. Tidy Data]{.smallcaps}

```{r}
#| label: tidy
#| warning: false

### |- clean and derive seasonal anomaly ----
co2_clean <- co2_raw |>
  filter(average > 0, deseasonalized > 0) |>
  mutate(
    seasonal_anomaly = average - deseasonalized,
    decade = floor(year / 10) * 10
  ) |>
  filter(decade >= 1960)

### |- decade-averaged seasonal waveform ----
# Keep 4 key decades: 1960s (baseline), 1980s, 2000s, 2020s (now)
key_decades <- c(1960, 1980, 2000, 2020)

co2_waveform_raw <- co2_clean |>
  filter(decade %in% key_decades) |>
  group_by(decade, month) |>
  summarise(
    mean_anomaly = mean(seasonal_anomaly, na.rm = TRUE),
    .groups = "drop"
  )

# Spline-interpolate each decade curve to 120 points (10 per month interval)
# This converts the angular 12-point polygon into a smooth breathing waveform
co2_waveform <- co2_waveform_raw |>
  group_by(decade) |>
  group_modify(~ {
    spl <- splinefun(.x$month, .x$mean_anomaly, method = "periodic")
    month_seq <- seq(1, 12, length.out = 120)
    tibble(month = month_seq, mean_anomaly = spl(month_seq))
  }) |>
  ungroup() |>
  mutate(
    decade_fct = factor(decade, levels = key_decades),
    month_abb  = month.abb[round(month)]
  )

### |- annotation data: decade mean CO2 for annotation text ----
decade_means <- co2_clean |>
  group_by(decade) |>
  summarise(mean_co2 = mean(average, na.rm = TRUE), .groups = "drop")

# values for annotation:
co2_1960s <- decade_means |>
  filter(decade == 1960) |> pull(mean_co2) |> round(0)

co2_2020s <- decade_means |> 
  filter(decade == 2020) |> pull(mean_co2) |> round(0)

### |- x-axis month labels ----
month_labels <- month.abb   

### |- direct end-line labels — 1960s and 2020s only ----
label_data <- co2_waveform |>
  group_by(decade) |>
  slice_max(month, n = 1) |>
  ungroup() |>
  filter(decade %in% c(1960, 2020)) |>
  mutate(label = glue("{decade}s"))

### |- zero reference line data ----
zero_line <- tibble(month = 1:12, y = 0)

### |- split data: context (faded) vs hero (2020s) ----
co2_context <- co2_waveform |> filter(decade != 2020)
co2_hero    <- co2_waveform |> filter(decade == 2020)
```


#### [5. Visualization Parameters]{.smallcaps}

```{r}
#| label: params
#| include: true
#| warning: false

### |- plot aesthetics ----
colors <- get_theme_colors(
  palette = list(
    "1960" = "#C2C2C2",    
    "1980" = "#9E9E9E",   
    "2000" = "#707070",   
    "2020" = "#6B0F1A",   
    "bg"   = "#F5F2EE",   
    "text" = "#2C2C2C",   
    "grid" = "#DDD9D4"    
  )
)

# extract as plain scalars 
decade_colors <- c(
  "1960" = colors$palette[["1960"]],
  "1980" = colors$palette[["1980"]],
  "2000" = colors$palette[["2000"]],
  "2020" = colors$palette[["2020"]]
)

# line widths
decade_linewidths <- c(
  "1960" = 0.7,
  "1980" = 0.7,
  "2000" = 0.7,
  "2020" = 1.5
)

col_bg   <- colors$palette[["bg"]]
col_text <- colors$palette[["text"]]
col_grid <- colors$palette[["grid"]]
col_2020 <- colors$palette[["2020"]]   

### |- titles and caption ----
title_text    <- "The Planet Still Breathes"

subtitle_text <- str_glue(
  "Decade-averaged CO₂ seasonal cycle at Mauna Loa, 1960s–2020s.<br>",
  "Each curve shows how atmospheric CO₂ rises and falls across the year — ",
  "driven by Northern Hemisphere plant growth and decay.<br>",
  "**+{co2_2020s - co2_1960s} ppm higher baseline since the 1960s** ",
  "(~{co2_1960s} ppm to ~{co2_2020s} ppm)"
)

caption_text  <- create_dcc_caption(
  dcc_year    = 2026,
  dcc_day     = 23,
  source_text = "NOAA Global Monitoring Laboratory · Mauna Loa CO₂ monthly means"
)

### |- x-axis month labels ----
month_labels <- month.abb   # "Jan" … "Dec"

### |- direct end-line labels — 1960s and 2020s only ----
label_data <- co2_waveform |>
  group_by(decade) |>
  slice_max(month, n = 1) |>
  ungroup() |>
  filter(decade %in% c(1960, 2020)) |>
  mutate(label = glue("{decade}s"))

### |- fonts ----
setup_fonts()
fonts <- get_font_families()

### |- base and weekly theme ----
base_theme <- create_base_theme(colors)

weekly_theme <- extend_weekly_theme(
  base_theme,
  theme(
    # background
    plot.background = element_rect(fill = col_bg, color = NA),
    panel.background = element_rect(fill = col_bg, color = NA),

    # grid — horizontal reference lines only; vertical absent
    panel.grid.major.y = element_line(color = col_grid, linewidth = 0.3),
    panel.grid.major.x = element_blank(),
    panel.grid.minor = element_blank(),

    # axes
    axis.ticks = element_blank(),
    axis.text.x = element_text(
      family = fonts$text, size = 9, color = col_text, margin = margin(t = 4)
    ),
    axis.text.y = element_text(
      family = fonts$text, size = 9, color = col_text
    ),
    axis.title.x = element_blank(),
    axis.title.y = element_text(
      family = fonts$text, size = 9.5, color = col_text,
      margin = margin(r = 8), angle = 90
    ),

    # text hierarchy
    plot.title = element_text(
      family = fonts$title, size = 28, face = "bold",
      color = col_text, margin = margin(b = 6)
    ),
    plot.subtitle = element_markdown(
      family = fonts$text, size = 10, color = col_text,
      lineheight = 1.45, margin = margin(b = 20)
    ),
    plot.caption = element_markdown(
      family = fonts$text, size = 7.5, color = "#888888",
      hjust = 0, margin = margin(t = 16)
    ),

    # margins
    plot.margin = margin(t = 20, r = 60, b = 12, l = 20)
  )
)

theme_set(weekly_theme)

```

#### [6. Plot]{.smallcaps}

```{r}
#| label: plot
#| warning: false

### |- main plot ----
p <- ggplot() +

  # Geoms
  geom_hline(yintercept = 0, color = col_grid, linewidth = 0.6) +
  geom_line(
    data = co2_context,
    aes(x = month, y = mean_anomaly, color = decade_fct, group = decade_fct),
    linewidth = 0.7, lineend = "round", linejoin = "round"
  ) +
  geom_line(
    data = co2_hero,
    aes(x = month, y = mean_anomaly, group = decade_fct),
    color = col_2020,
    linewidth = 1.6, lineend = "round", linejoin = "round"
  ) +
  geom_text(
    data = label_data,
    aes(x = month + 0.15, y = mean_anomaly, label = label, color = decade_fct),
    hjust = 0,
    size = 3.2,
    family = fonts$text,
    fontface = "bold",
    inherit.aes = FALSE
  ) +

  # Annotate
  annotate(
    geom = "richtext",
    x = 6.2,
    y = 3.9,
    label = glue(
      "<b>Same seasonal breath (~6–8 ppm)</b><br>",
      "Amplitude stable across all 4 decades"
    ),
    hjust = 0,
    vjust = 1,
    size = 3.3,
    family = fonts$text,
    color = col_text,
    fill = NA,
    label.color = NA,
    lineheight = 1.4
  ) +

  # Scales
  scale_x_continuous(
    breaks = 1:12,
    labels = month_labels,
    expand = expansion(mult = c(0.02, 0.12))
  ) +
  scale_y_continuous(
    breaks = seq(-4, 4, by = 2),
    labels = function(x) glue("{x} ppm"),
    limits = c(-4.2, 4.2)
  ) +
  scale_color_manual(values = decade_colors) +
  guides(color = "none") +
  coord_cartesian(clip = "off") +

  # Labs
  labs(
    title    = title_text,
    subtitle = subtitle_text,
    caption  = caption_text,
    y        = "Seasonal anomaly (CO₂ ppm, deviation from trend)",
    color    = NULL
  )
```

#### [7. Save]{.smallcaps}

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

### |-  plot image ----  
save_plot(
  p,
  type = "30daychartchallenge",
  year = 2026,
  day = 23,
  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 [`30dcc_2026_23.qmd`](https://github.com/poncest/personal-website/blob/master/data_visualizations/TidyTuesday/2026/30dcc_2026_23.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:**
   - Thoning, K.W., Crotwell, A.M., and Mund, J.W. (2025).
     Atmospheric Carbon Dioxide Dry Air Mole Fractions from continuous
     measurements at Mauna Loa, Hawaii. National Oceanic and Atmospheric
     Administration (NOAA), Global Monitoring Laboratory (GML),
     Boulder, Colorado, USA.
     https://doi.org/10.15138/yaf1-bk21
     Retrieved from: https://gml.noaa.gov/aftp/ccg/co2/trends/co2_mm_mlo.csv
:::


#### [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