• 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

From what workers are owed to what humans are

  • Show All Code
  • Hide All Code

  • View Source

Across two encyclicals by two popes named Leo, 135 years apart, the vocabulary shifts from labor, wages, and property to dignity, personhood, and artificial intelligence.

TidyTuesday
Data Visualization
R Programming
2026
A diverging bar chart comparing the distinctive vocabulary of two papal encyclicals 135 years apart — Rerum Novarum (1891) and Magnifica Humanitas (2026) — showing the language of concern shift from labor and property to dignity and artificial intelligence. Distinctiveness is measured with weighted log-odds. Built in R with ggplot2 and tidytext.
Author

Steven Ponce

Published

June 22, 2026

Figure 1: Diverging bar chart titled “From what workers are owed to what humans are,” comparing the distinctive vocabulary of two papal encyclicals. Words extending left are characteristic of Rerum Novarum (1891, Leo XIII); words extending right are characteristic of Magnifica Humanitas (2026, Leo XIV); bar length is each word’s weighted log-odds distinctiveness. The 1891 encyclical’s signature words are labor, law, property, private, associations, classes, wages, employer, and workmen. The 2026 encyclical’s are dignity, person, digital, AI, technology, data, intelligence, artificial, algorithms, and machines. A note observes that both encyclicals lean heavily on a single word — human — and that what differs between them is the threat to it. Source: Vatican.va (Holy See).

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({
if (!require("pacman")) install.packages("pacman")
pacman::p_load(
    tidyverse, ggtext, showtext, janitor, ggrepel,      
    scales, glue, skimr, tidytext
    )
})

### |- figure size ----          
camcorder::gg_record(
  dir    = here::here("temp_plots"),
    device = "png",
    width  = 9,
    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

tt <- tidytuesdayR::tt_load(2026, week = 25)
encyclicals <- tt$encyclicals |> clean_names()
# papal_encyclicals <- tt$papal_encyclicals |> clean_names()
# scripture_references <- tt$scripture_references |> clean_names()

rm(tt)
```

3. Examine the Data

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

glimpse(encyclicals)
# glimpse(papal_encyclicals)
# glimpse(scripture_references)
```

4. Tidy Data

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

### |- tokenize ----
tokens <- encyclicals |>
    select(encyclical, paragraph, text) |>
    unnest_tokens(word, text) |>
    filter(str_detect(word, "^[a-z]+$")) |>  
    anti_join(stop_words, by = "word")

### |- weighted log-odds (signed distinctiveness) ----
word_counts <- tokens |>
    count(encyclical, word, name = "n") |>
    filter(n >= 3) |>                          
    pivot_wider(names_from = encyclical, values_from = n, values_fill = 0) |>
    rename(mh = `Magnifica Humanitas`, rn = `Rerum Novarum`)

n_mh  <- sum(word_counts$mh)
n_rn  <- sum(word_counts$rn)
vocab <- nrow(word_counts)

keyness <- word_counts |>
    mutate(
        a_mh = mh + 1,                           # + uninformative prior
        a_rn = rn + 1,
        log_odds = log(a_mh / (n_mh + vocab - a_mh)) -
            log(a_rn / (n_rn + vocab - a_rn)),
        se = sqrt(1 / a_mh + 1 / a_rn),
        z  = log_odds / se                       # weighted log-odds (signed)
    )

### |- word selection: thesis vocabulary, log-odds ranked ----
# Pure log-odds buries the AI signal under high-count CST register
# (social, economic, justice, power...). So we force-include each side's
# vocabulary of CONCERN and suppress the institutional register -- same logic
# as the shared-spine exclusion, just extended. Every shown word is still
# verified distinctive (correct sign of z); sizing stays log-odds.

# words common to both / structural -- never shown ("human" returns as anchor)
shared_spine <- c(
    "human", "god", "church", "society", "life",
    "poor", "common", "world", "people", "time"
)

# modern-CST institutional register -- distinctive to MH but off-thesis
register_terms <- c(
    "social", "economic", "responsibility", "development",
    "doctrine", "political", "forms", "justice", "power",
    "humanity"
)

excluded <- c(shared_spine, register_terms)

# force-included concern vocabularies (verified by sign of z below)
rn_focus <- c(
    "labor", "property", "rights", "classes", "rich", "wages",
    "employer", "workmen", "associations", "private", "law"
)

mh_focus <- c(
    "dignity", "person", "ai", "digital", "artificial",
    "intelligence", "technology", "technological", "data",
    "algorithms", "machines"
)

n_each <- 11

# force-include only words that genuinely lean the intended way, then
# top up to n_each from the remaining distinctive (non-register) words.
pick_side <- function(focus, sign_dir) {
    forced <- keyness |>
        filter(word %in% focus, sign(z) == sign_dir)
    fill <- keyness |>
        filter(sign(z) == sign_dir, !word %in% excluded, !word %in% forced$word) |>
        slice_max(abs(z), n = n_each, with_ties = FALSE)
    bind_rows(forced, fill) |>
        distinct(word, .keep_all = TRUE) |>
        slice_head(n = n_each)
}

plot_words <- bind_rows(
    pick_side(mh_focus, 1L),
    pick_side(rn_focus, -1L)
) |>
    mutate(side = if_else(z > 0, "Magnifica Humanitas", "Rerum Novarum")) |>
    arrange(z) |>
    mutate(word = fct_inorder(word))

### |- shared anchor ----
anchor <- keyness |>
    filter(word == "human") |>
    mutate(
        mh_per1k = mh / n_mh * 1000,
        rn_per1k = rn / n_rn * 1000
    )
```

5. Visualization Parameters

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

### |- plot aesthetics ----
clrs <- get_theme_colors(
    palette = list(
        mh  = "#722F37",
        rn  = "#7A7068",
        ink = "#2C2825"
    )
)
col_mh <- clrs$palette$mh
col_rn <- clrs$palette$rn
col_ink <- clrs$palette$ink

### |- titles and caption ----
title_text <- str_glue("From what workers are owed to what humans are")

subtitle_text <- str_glue(
    "Across two encyclicals by two popes named Leo, 135 years apart, the ",
    "vocabulary shifts from **labor, wages, and property** to ",
    "**dignity, personhood, and artificial intelligence**."
)

method_note <- str_glue(
    "Each encyclical's distinctive vocabulary of concern (shared and ",
    "general-register terms removed);<br>bar length = weighted log-odds ",
    "(Monroe et al.). Words appear at least 3 times."
)

caption_text <- str_glue(
    "{create_social_caption(tt_year = 2026, tt_week = 25, ",
    "source_text = 'Vatican.va (Holy See)')}<br>{method_note}"
)

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

### |- plot theme ----
base_theme <- create_base_theme(clrs)

weekly_theme <- extend_weekly_theme(
    base_theme,
    theme(
        plot.title = element_markdown(
            face = "bold", family = fonts$title_1, size = rel(1.85),
            color = col_ink, margin = margin(b = 6)
        ),
        plot.subtitle = element_textbox_simple(
            family = fonts$subtitle, size = rel(0.8), color = col_ink,
            width = unit(1, "npc"), lineheight = 1.25, margin = margin(b = 30)
        ),
        plot.caption = element_markdown(
            family = fonts$caption, size = rel(0.55), color = col_rn,
            hjust = 0, lineheight = 1.3, margin = margin(t = 20, b = 10)
        ),
        axis.title.x = element_blank(),
        axis.text.x = element_blank(),
        axis.text.y = element_blank(),
        axis.title.y = element_blank(),
        panel.grid = element_blank(),
        axis.ticks = element_blank()
    )
)

theme_set(weekly_theme)

### |- annotation positions ----
x_max <- max(abs(plot_words$z)) * 1.05
y_header <- n_each * 2 + 1.4
y_anchor <- -1.3  
```

6. Plot

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

### |- plot ----
p <- plot_words |>
    ggplot(aes(x = z, y = word, fill = side)) +
    
    # Geoms
    geom_col(width = 0.72) +
    geom_text(
        data = \(d) filter(d, z > 0),
        aes(label = word), hjust = -0.08,
        family = fonts$text, size = 3.5, color = col_ink
    ) +
    geom_text(
        data  = \(d) filter(d, z < 0),
        aes(label = word), hjust = 1.08,
        family = fonts$text, size = 3.5, color = col_ink
    ) +
    geom_vline(xintercept = 0, color = col_ink, linewidth = 0.4) +
    # Annotations
    annotate(
        "richtext",
        x = -x_max, y = y_header, hjust = 0, vjust = 0,
        label.color = NA, fill = NA, family = fonts$text, size = 3.4,
        color = col_rn,
        label = "**Rerum Novarum** \u00b7 1891 \u00b7 Leo XIII<br>
             <span style='font-size:8pt'>The Industrial Revolution</span>"
    ) +
    annotate(
        "richtext",
        x = x_max, y = y_header, hjust = 1, vjust = 0,
        label.color = NA, fill = NA, family = fonts$text, size = 3.4,
        color = col_mh,
        label = "**Magnifica Humanitas** \u00b7 2026 \u00b7 Leo XIV<br>
             <span style='font-size:8pt'>The AI Revolution</span>"
    ) +
    annotate(
        "richtext",
        x = 0, y = y_anchor, hjust = 0.5, vjust = 0.5,
        label.color = NA, fill = NA, family = fonts$text, size = 3.1,
        color = col_rn, lineheight = 1.3,
        label = "**The constant is the human** -- both encyclicals lean on that one word.<br>
             What changes is the *threat* to it."
    ) +
    # Scales
    scale_fill_manual(
        values = c("Magnifica Humanitas" = col_mh, "Rerum Novarum" = col_rn),
        guide  = "none"
    ) +
    scale_x_continuous(
        limits = c(-x_max, x_max),
        expand = expansion(mult = c(0.22, 0.22))
    ) +
    scale_y_discrete(expand = expansion(add = c(3, 2.2))) +
    coord_cartesian(clip = "off") +
    # Labs
    labs(
        title    = title_text,
        subtitle = subtitle_text,
        caption  = caption_text,
        x        = NULL,
        y        = NULL
    )
```

7. Save

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

### |-  plot image ----  
save_plot(
  plot = p, 
  type = "tidytuesday", 
  year = 2026, 
  week = 25, 
  width  = 9,
  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      tidytext_0.4.3  skimr_2.2.2     glue_1.8.0     
 [5] scales_1.4.0    ggrepel_0.9.8   janitor_2.2.1   showtext_0.9-8 
 [9] showtextdb_3.0  sysfonts_0.8.9  ggtext_0.1.2    lubridate_1.9.5
[13] forcats_1.0.1   stringr_1.6.0   dplyr_1.2.1     purrr_1.2.2    
[17] readr_2.2.0     tidyr_1.3.2     tibble_3.3.1    ggplot2_4.0.3  
[21] tidyverse_2.0.0 pacman_0.5.1   

loaded via a namespace (and not attached):
 [1] tidyselect_1.2.1   farver_2.1.2       S7_0.2.1           fastmap_1.2.0     
 [5] gh_1.5.0           janeaustenr_1.0.0  digest_0.6.39      timechange_0.4.0  
 [9] lifecycle_1.0.5    rsvg_2.7.0         tokenizers_0.3.0   magrittr_2.0.5    
[13] compiler_4.5.3     rlang_1.2.0        tools_4.5.3        yaml_2.3.12       
[17] knitr_1.51         labeling_0.4.3     htmlwidgets_1.6.4  bit_4.6.0         
[21] curl_7.0.0         xml2_1.5.2         camcorder_0.1.0    repr_1.1.7        
[25] RColorBrewer_1.1-3 tidytuesdayR_1.3.2 withr_3.0.2        grid_4.5.3        
[29] gitcreds_0.1.2     cli_3.6.6          rmarkdown_2.31     crayon_1.5.3      
[33] ragg_1.5.2         generics_0.1.4     otel_0.2.0         rstudioapi_0.18.0 
[37] tzdb_0.5.0         commonmark_2.0.0   parallel_4.5.3     base64enc_0.1-6   
[41] vctrs_0.7.3        Matrix_1.7-4       jsonlite_2.0.0     litedown_0.9      
[45] hms_1.1.4          bit64_4.6.0-1      systemfonts_1.3.2  magick_2.9.1      
[49] gifski_1.32.0-2    codetools_0.2-20   stringi_1.8.7      gtable_0.3.6      
[53] pillar_1.11.1      rappdirs_0.3.4     htmltools_0.5.9    R6_2.6.1          
[57] httr2_1.2.2        textshaping_1.0.5  rprojroot_2.1.1    vroom_1.7.1       
[61] evaluate_1.0.5     lattice_0.22-9     markdown_2.0       SnowballC_0.7.1   
[65] gridtext_0.1.6     snakecase_0.11.1   Rcpp_1.1.1         svglite_2.2.2     
[69] xfun_0.57          pkgconfig_2.0.3   

9. GitHub Repository

TipExpand for GitHub Repo

The complete code for this analysis is available in tt_2026_25.qmd.

For the full repository, click here.

10. References

TipExpand for References
  1. Data Source:
    • TidyTuesday 2026 Week 245cals: Industrial Revolution vs. AI Revolution](https://github.com/rfordatascience/tidytuesday/blob/main/data/2026/2026-06-23/readme.md)

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 = {From What Workers Are Owed to What Humans Are},
  date = {2026-06-22},
  url = {https://stevenponce.netlify.app/data_visualizations/TidyTuesday/2026/tt_2026_25.html},
  langid = {en}
}
For attribution, please cite this work as:
Ponce, Steven. 2026. “From What Workers Are Owed to What Humans Are.” June 22. https://stevenponce.netlify.app/data_visualizations/TidyTuesday/2026/tt_2026_25.html.
Source Code
---
title: "From what workers are owed to what humans are"
subtitle: "Across two encyclicals by two popes named Leo, 135 years apart, the vocabulary shifts from labor, wages, and property to dignity, personhood, and artificial intelligence."
description: "A diverging bar chart comparing the distinctive vocabulary of two papal encyclicals 135 years apart — Rerum Novarum (1891) and Magnifica Humanitas (2026) — showing the language of concern shift from labor and property to dignity and artificial intelligence. Distinctiveness is measured with weighted log-odds. Built in R with ggplot2 and tidytext."
date: "2026-06-22"
author:
  - name: "Steven Ponce"
    url: "https://stevenponce.netlify.app"
citation:
  url: "https://stevenponce.netlify.app/data_visualizations/TidyTuesday/2026/tt_2026_25.html"
categories: ["TidyTuesday", "Data Visualization", "R Programming", "2026"]
tags: [
  "TidyTuesday",
  "Text Analysis",
  "Diverging Bar Chart",
  "Keyness",
  "Log-Odds",
  "Catholic Social Teaching",
  "Encyclicals",
  "Natural Language Processing",
  "tidytext",
  "ggplot2",
  "Annotation",
  "2026"
]
image: "thumbnails/tt_2026_25.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
---

![Diverging bar chart titled "From what workers are owed to what humans are," comparing the distinctive vocabulary of two papal encyclicals. Words extending left are characteristic of Rerum Novarum (1891, Leo XIII); words extending right are characteristic of Magnifica Humanitas (2026, Leo XIV); bar length is each word's weighted log-odds distinctiveness. The 1891 encyclical's signature words are labor, law, property, private, associations, classes, wages, employer, and workmen. The 2026 encyclical's are dignity, person, digital, AI, technology, data, intelligence, artificial, algorithms, and machines. A note observes that both encyclicals lean heavily on a single word — human — and that what differs between them is the threat to it. Source: Vatican.va (Holy See).](tt_2026_25.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({
if (!require("pacman")) install.packages("pacman")
pacman::p_load(
    tidyverse, ggtext, showtext, janitor, ggrepel,      
    scales, glue, skimr, tidytext
    )
})

### |- figure size ----          
camcorder::gg_record(
  dir    = here::here("temp_plots"),
    device = "png",
    width  = 9,
    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

tt <- tidytuesdayR::tt_load(2026, week = 25)
encyclicals <- tt$encyclicals |> clean_names()
# papal_encyclicals <- tt$papal_encyclicals |> clean_names()
# scripture_references <- tt$scripture_references |> clean_names()

rm(tt)
```

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

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

glimpse(encyclicals)
# glimpse(papal_encyclicals)
# glimpse(scripture_references)
```

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

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

### |- tokenize ----
tokens <- encyclicals |>
    select(encyclical, paragraph, text) |>
    unnest_tokens(word, text) |>
    filter(str_detect(word, "^[a-z]+$")) |>  
    anti_join(stop_words, by = "word")

### |- weighted log-odds (signed distinctiveness) ----
word_counts <- tokens |>
    count(encyclical, word, name = "n") |>
    filter(n >= 3) |>                          
    pivot_wider(names_from = encyclical, values_from = n, values_fill = 0) |>
    rename(mh = `Magnifica Humanitas`, rn = `Rerum Novarum`)

n_mh  <- sum(word_counts$mh)
n_rn  <- sum(word_counts$rn)
vocab <- nrow(word_counts)

keyness <- word_counts |>
    mutate(
        a_mh = mh + 1,                           # + uninformative prior
        a_rn = rn + 1,
        log_odds = log(a_mh / (n_mh + vocab - a_mh)) -
            log(a_rn / (n_rn + vocab - a_rn)),
        se = sqrt(1 / a_mh + 1 / a_rn),
        z  = log_odds / se                       # weighted log-odds (signed)
    )

### |- word selection: thesis vocabulary, log-odds ranked ----
# Pure log-odds buries the AI signal under high-count CST register
# (social, economic, justice, power...). So we force-include each side's
# vocabulary of CONCERN and suppress the institutional register -- same logic
# as the shared-spine exclusion, just extended. Every shown word is still
# verified distinctive (correct sign of z); sizing stays log-odds.

# words common to both / structural -- never shown ("human" returns as anchor)
shared_spine <- c(
    "human", "god", "church", "society", "life",
    "poor", "common", "world", "people", "time"
)

# modern-CST institutional register -- distinctive to MH but off-thesis
register_terms <- c(
    "social", "economic", "responsibility", "development",
    "doctrine", "political", "forms", "justice", "power",
    "humanity"
)

excluded <- c(shared_spine, register_terms)

# force-included concern vocabularies (verified by sign of z below)
rn_focus <- c(
    "labor", "property", "rights", "classes", "rich", "wages",
    "employer", "workmen", "associations", "private", "law"
)

mh_focus <- c(
    "dignity", "person", "ai", "digital", "artificial",
    "intelligence", "technology", "technological", "data",
    "algorithms", "machines"
)

n_each <- 11

# force-include only words that genuinely lean the intended way, then
# top up to n_each from the remaining distinctive (non-register) words.
pick_side <- function(focus, sign_dir) {
    forced <- keyness |>
        filter(word %in% focus, sign(z) == sign_dir)
    fill <- keyness |>
        filter(sign(z) == sign_dir, !word %in% excluded, !word %in% forced$word) |>
        slice_max(abs(z), n = n_each, with_ties = FALSE)
    bind_rows(forced, fill) |>
        distinct(word, .keep_all = TRUE) |>
        slice_head(n = n_each)
}

plot_words <- bind_rows(
    pick_side(mh_focus, 1L),
    pick_side(rn_focus, -1L)
) |>
    mutate(side = if_else(z > 0, "Magnifica Humanitas", "Rerum Novarum")) |>
    arrange(z) |>
    mutate(word = fct_inorder(word))

### |- shared anchor ----
anchor <- keyness |>
    filter(word == "human") |>
    mutate(
        mh_per1k = mh / n_mh * 1000,
        rn_per1k = rn / n_rn * 1000
    )
```

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

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

### |- plot aesthetics ----
clrs <- get_theme_colors(
    palette = list(
        mh  = "#722F37",
        rn  = "#7A7068",
        ink = "#2C2825"
    )
)
col_mh <- clrs$palette$mh
col_rn <- clrs$palette$rn
col_ink <- clrs$palette$ink

### |- titles and caption ----
title_text <- str_glue("From what workers are owed to what humans are")

subtitle_text <- str_glue(
    "Across two encyclicals by two popes named Leo, 135 years apart, the ",
    "vocabulary shifts from **labor, wages, and property** to ",
    "**dignity, personhood, and artificial intelligence**."
)

method_note <- str_glue(
    "Each encyclical's distinctive vocabulary of concern (shared and ",
    "general-register terms removed);<br>bar length = weighted log-odds ",
    "(Monroe et al.). Words appear at least 3 times."
)

caption_text <- str_glue(
    "{create_social_caption(tt_year = 2026, tt_week = 25, ",
    "source_text = 'Vatican.va (Holy See)')}<br>{method_note}"
)

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

### |- plot theme ----
base_theme <- create_base_theme(clrs)

weekly_theme <- extend_weekly_theme(
    base_theme,
    theme(
        plot.title = element_markdown(
            face = "bold", family = fonts$title_1, size = rel(1.85),
            color = col_ink, margin = margin(b = 6)
        ),
        plot.subtitle = element_textbox_simple(
            family = fonts$subtitle, size = rel(0.8), color = col_ink,
            width = unit(1, "npc"), lineheight = 1.25, margin = margin(b = 30)
        ),
        plot.caption = element_markdown(
            family = fonts$caption, size = rel(0.55), color = col_rn,
            hjust = 0, lineheight = 1.3, margin = margin(t = 20, b = 10)
        ),
        axis.title.x = element_blank(),
        axis.text.x = element_blank(),
        axis.text.y = element_blank(),
        axis.title.y = element_blank(),
        panel.grid = element_blank(),
        axis.ticks = element_blank()
    )
)

theme_set(weekly_theme)

### |- annotation positions ----
x_max <- max(abs(plot_words$z)) * 1.05
y_header <- n_each * 2 + 1.4
y_anchor <- -1.3  
```

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

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

### |- plot ----
p <- plot_words |>
    ggplot(aes(x = z, y = word, fill = side)) +
    
    # Geoms
    geom_col(width = 0.72) +
    geom_text(
        data = \(d) filter(d, z > 0),
        aes(label = word), hjust = -0.08,
        family = fonts$text, size = 3.5, color = col_ink
    ) +
    geom_text(
        data  = \(d) filter(d, z < 0),
        aes(label = word), hjust = 1.08,
        family = fonts$text, size = 3.5, color = col_ink
    ) +
    geom_vline(xintercept = 0, color = col_ink, linewidth = 0.4) +
    # Annotations
    annotate(
        "richtext",
        x = -x_max, y = y_header, hjust = 0, vjust = 0,
        label.color = NA, fill = NA, family = fonts$text, size = 3.4,
        color = col_rn,
        label = "**Rerum Novarum** \u00b7 1891 \u00b7 Leo XIII<br>
             <span style='font-size:8pt'>The Industrial Revolution</span>"
    ) +
    annotate(
        "richtext",
        x = x_max, y = y_header, hjust = 1, vjust = 0,
        label.color = NA, fill = NA, family = fonts$text, size = 3.4,
        color = col_mh,
        label = "**Magnifica Humanitas** \u00b7 2026 \u00b7 Leo XIV<br>
             <span style='font-size:8pt'>The AI Revolution</span>"
    ) +
    annotate(
        "richtext",
        x = 0, y = y_anchor, hjust = 0.5, vjust = 0.5,
        label.color = NA, fill = NA, family = fonts$text, size = 3.1,
        color = col_rn, lineheight = 1.3,
        label = "**The constant is the human** -- both encyclicals lean on that one word.<br>
             What changes is the *threat* to it."
    ) +
    # Scales
    scale_fill_manual(
        values = c("Magnifica Humanitas" = col_mh, "Rerum Novarum" = col_rn),
        guide  = "none"
    ) +
    scale_x_continuous(
        limits = c(-x_max, x_max),
        expand = expansion(mult = c(0.22, 0.22))
    ) +
    scale_y_discrete(expand = expansion(add = c(3, 2.2))) +
    coord_cartesian(clip = "off") +
    # Labs
    labs(
        title    = title_text,
        subtitle = subtitle_text,
        caption  = caption_text,
        x        = NULL,
        y        = NULL
    )
```

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

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

### |-  plot image ----  
save_plot(
  plot = p, 
  type = "tidytuesday", 
  year = 2026, 
  week = 25, 
  width  = 9,
  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 [`tt_2026_25.qmd`](https://github.com/poncest/personal-website/blob/master/data_visualizations/TidyTuesday/2026/tt_2026_25.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 Source:**
    -   TidyTuesday 2026 Week 245cals: Industrial Revolution vs. AI Revolution](https://github.com/rfordatascience/tidytuesday/blob/main/data/2026/2026-06-23/readme.md)

:::


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