Beeswarm plot on Age Profile of 2025 Papal Conclave
Delving into the demographics of the 2025 Papal Conclave electors, this visualization, crafted with the elegant aesthetics of {ggplot2} and the non-overlapping precision of {ggbeeswarm}, paired with image integration via {ggimage} and web scraping powered by {rvest}, offers a unique perspective on the ages and global representation of the cardinals poised to shape the Catholic Church’s future.
Geopolitics
Images
Web Scraping
{rvest}
{ggbeeswarm}
{ggimage}
Author
Aditya Dahiya
Published
April 26, 2025
Who Will Choose the Next Pope?
Figure 1: A visual snapshot of the College of Cardinals eligible to elect the next Pope in the 2025 conclave. This beeswarm plot beautifully arranges each cardinal by age, preventing overlap while clearly displaying the distribution. Each circle represents a cardinal, colored by their ecclesiastical order (bishop, priest, or deacon), and is accompanied by their portrait, name, and country. The clustering effect of the beeswarm layout, powered by the ggbeeswarm package and its position_beeswarm() function, reveals a predominantly older demographic within the electors, highlighting the age range from 45 to 79.
About the Data
The data for this visualization originates from the Wikipedia article Cardinal electors in the 2025 papal conclave, which provides a comprehensive and up-to-date list of the 135 cardinal electors eligible to participate in the upcoming conclave following the death of Pope Francis on April 21, 2025. This resource compiles information from official Vatican sources, including the Holy See Press Office and the Annuarium Statisticum Ecclesiae, and details each cardinal’s name, country, date of birth, ecclesiastical order (bishop, priest, or deacon), date of appointment (consistory), and the pope who appointed them. Notably, it also tracks changes in eligibility, such as Cardinal Antonio Cañizares Llovera’s decision not to attend due to health reasons, reducing the number of expected participants to 134. The dataset reflects the global composition of the College of Cardinals, with representation from 71 countries across six continents, and highlights that Pope Francis appointed 108 of the 135 electors, underscoring his significant influence on the Church’s future leadership. (Cardinal electors in the 2025 papal conclave)
How I made this graphic?
To craft this graphic illustrating the age profile of the 2025 Papal Conclave electors, I leveraged the power of several R packages within the tidyverse ecosystem. First, I used the rvest package, specifically the read_html() and html_table() functions, to scrape the crucial data on cardinal electors directly from the linked Wikipedia article. After cleaning and transforming this data using functions from dplyr and lubridate, the core visualization was built with ggplot2. To arrange the cardinal portraits and age points in a visually appealing and non-overlapping manner, I employed the ggbeeswarm package, utilizing its geom_beeswarm() function with a custom position_beeswarm() function set to the "center" method and a small cex value for fine-tuned spacing. Each cardinal’s image, sourced using a custom function involving Google’s Custom Search API and processed with the magick and ggimage packages, was then layered onto the plot using geom_image(). The ecclesiastical order of each cardinal was represented by color, managed through scale_colour_manual() with a custom palette from paletteer. Finally, textual elements, including names, countries, titles, and captions with linked social media handles, were added using standard ggplot2 geoms and the ggtext package for enhanced formatting, while custom fonts were incorporated via the showtext package.
Loading required libraries
Code
pacman::p_load( tidyverse, # All things tidy scales, # Nice Scales for ggplot2 fontawesome, # Icons display in ggplot2 ggtext, # Markdown text support for ggplot2 showtext, # Display fonts in ggplot2 colorspace, # Lighten and Darken colours magick, # Download images and edit them ggimage, # Display images in ggplot2 patchwork, # Composing Plots rvest, # Web-Scraping ggbeeswarm # Beeswarm Plots)# URL of the Wikipedia page# Read the HTML content of the pagepage <-read_html("https://en.wikipedia.org/wiki/Cardinal_electors_in_the_2025_papal_conclave")# Extract the first table (which contains the list of cardinals)table_df <- page |>html_table(fill =TRUE)table_df <- table_df[[1]]rm(page)
Visualization Parameters
Code
# Font for titlesfont_add_google("Saira",family ="title_font") # Font for the captionfont_add_google("Saira Extra Condensed",family ="caption_font") # Font for plot textfont_add_google("Saira Condensed",family ="body_font") showtext_auto()# cols4all::c4a_gui()mypal <- paletteer::paletteer_d("NineteenEightyR::sunset2")[c(5,3,1)]# A base Colourbg_col <-"white"seecolor::print_color(bg_col)# Colour for highlighted texttext_hil <-"grey30"seecolor::print_color(text_hil)# Colour for the texttext_col <-"grey30"seecolor::print_color(text_col)line_col <-"grey30"# Define Base Text Sizebts <-90# Caption stuff for the plotsysfonts::font_add(family ="Font Awesome 6 Brands",regular = here::here("docs", "Font Awesome 6 Brands-Regular-400.otf"))github <-""github_username <-"aditya-dahiya"xtwitter <-""xtwitter_username <-"@adityadahiyaias"social_caption_1 <- glue::glue("<span style='font-family:\"Font Awesome 6 Brands\";'>{github};</span> <span style='color: {text_hil}'>{github_username} </span>")social_caption_2 <- glue::glue("<span style='font-family:\"Font Awesome 6 Brands\";'>{xtwitter};</span> <span style='color: {text_hil}'>{xtwitter_username}</span>")plot_caption <-paste0("**Data:** Wikipedia", " | **Code:** ", social_caption_1, " | **Graphics:** ", social_caption_2 )rm(github, github_username, xtwitter, xtwitter_username, social_caption_1, social_caption_2)# Add text to plot-------------------------------------------------plot_title <-"Age Profile of the 2025 Papal Conclave Electors"|>str_wrap(12)str_view(plot_title)plot_subtitle <-"Cardinals eligible to vote in the 2025 conclave span from age 45 to 79, but skew older. Each circle is a cardinal, colored by ecclesiastical order, with names and countries below."|>str_wrap(25)str_view(plot_subtitle)
Get temporary files on images of each Cardinal
Code
# Get a custom google search engine and API key# Tutorial: https://developers.google.com/custom-search/v1/overview# Tutorial 2: https://programmablesearchengine.google.com/# From:https://developers.google.com/custom-search/v1/overview# google_api_key <- "LOAD YOUR GOOGLE API KEY HERE"# From: https://programmablesearchengine.google.com/controlpanel/all# my_cx <- "GET YOUR CUSTOM SEARCH ENGINE ID HERE"# Improved function to download and save food imagesdownload_cardinal_potrait <-function(i) { google_api_key <- google_api_key my_cx <- my_cx# Build the API request URL with additional filters url <-paste0("https://www.googleapis.com/customsearch/v1?q=",URLencode(paste0("Cardinal ", cardinals$name[i], " photo")),"&cx=", my_cx,"&searchType=image","&key=", google_api_key,# "&imgSize=large", # Restrict to medium-sized images# "&imgType=photo","&num=1"# Fetch only one result )# Make the request response <- httr::GET(url)# if (response$status_code != 200) {# warning("Failed to fetch data for Cardinal: ", # cardinals$name[i])# return(NULL)# }# Parse the response result <- httr::content(response, "parsed")# Extract the image URL image_url <- result$items[[1]]$link# Process the image magick::image_read(image_url) |>image_resize("x300") |># Crop the image into a circle # (Credits: https://github.com/doehm/cropcircles) cropcircles::circle_crop(border_colour ="black",border_size =0 ) |>image_read() |>image_background(color ="transparent") |>image_resize("x300") |># Save or display the resultimage_write( here::here("data_vizs", paste0("temp_cardinals_", i, ".png") ) )}# Iterate and download imagesfor (i in1:nrow(cardinals)) {download_cardinal_potrait(i)}problem_numbers <-c(5, 6, 7, 16, 32, 36, 73)
Exploratory Data Analysis and Wrangling
Code
# Ensure the table is a tibblecardinals <-as_tibble(table_df) |> janitor::clean_names() |>mutate(# Extract the date string from raw databorn =str_extract(born, "^\\d{1,2} \\w+ \\d{4}"),# Convert to Date formatborn =dmy(born), # Calculate age for each cardinal in yearsage =time_length(interval(start = born, end =today()), unit ="years") |>floor() ) |>mutate(date_consistory =str_extract( consistory, "^\\d{1,2} \\w+ \\d{4}" ) |>dmy(),pope_consistory =str_remove( consistory, "^\\d{1,2} \\w+ \\d{4}" ) |>str_trim(),pope_consistory =paste0("Pope ", pope_consistory),.keep ="unused" ) |>select(-ref) |>mutate(country =str_remove_all(country, "\\[.*?\\]"),order =fct(order, levels =c("CB", "CP", "CD")) )# A temporary tibble for nice y-axislabels_y <- cardinals |>distinct(age) |>arrange(age) |>mutate(labels_age =row_number())labels_col <-c("cardinal bishops", "cardinal priests", "cardinal deacons") |>str_to_title()plotdf1 <- cardinals |>mutate(x_var = ((rank -1) %%12) +1,y_var =12- ((rank -1) %/%12),image_var =paste0("data_vizs/temp_cardinals_", rank, ".png") ) |>left_join(labels_y)
# Saving a thumbnaillibrary(magick)# Saving a thumbnail for the webpageimage_read(here::here("data_vizs", "viz_papal_conclave_2025_2.png")) |>image_resize(geometry ="x400") |>image_write( here::here("data_vizs", "thumbnails", "viz_papal_conclave_2025_2.png" ) )
Session Info
Code
pacman::p_load( tidyverse, # All things tidy scales, # Nice Scales for ggplot2 fontawesome, # Icons display in ggplot2 ggtext, # Markdown text support for ggplot2 showtext, # Display fonts in ggplot2 colorspace, # Lighten and Darken colours magick, # Download images and edit them ggimage, # Display images in ggplot2 patchwork, # Composing Plots rvest, # Web-Scraping ggbeeswarm # Beeswarm Plots)sessioninfo::session_info()$packages |>as_tibble() |>select(package, version = loadedversion, date, source) |>arrange(package) |> janitor::clean_names(case ="title" ) |> gt::gt() |> gt::opt_interactive(use_search =TRUE ) |> gtExtras::gt_theme_espn()
Table 1: R Packages and their versions used in the creation of this page and graphics