• 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

Repair Isn’t the Problem — The System Is

  • Show All Code
  • Hide All Code

  • View Source

Even easy-to-fix products fail in Repair Cafes — not due to skill, but due to missing parts, unclear diagnostics, and cost barriers

TidyTuesday
Data Visualization
R Programming
2026
Analysis of 178,000+ repair records from the global Repair Monitor dataset, examining why products rated as easy to fix still fail at Repair Cafes worldwide. A two-panel visualization contrasts repairability scores against actual repair outcomes by product category, revealing that system constraints — primarily missing spare parts — explain failure far more than volunteer skill or product difficulty. Built with ggplot2, patchwork, and binom in R.
Author

Steven Ponce

Published

April 4, 2026

Figure 1: A two-panel data visualization titled “Repair Isn’t the Problem — The System Is.” The left panel is a scatter plot showing product categories by median repairability score (x-axis, 1–10) versus percentage successfully repaired (y-axis, 0–100%). A shaded amber region marks the “system constraint” zone — categories rated easy to fix (score ≥6) but repaired less than 50% of the time. Two rust-colored points, Computer Equipment/Phones and Display and Sound Equipment, fall in this zone near the 50% threshold. Slate-colored points for Textiles and Tools Non-Electric sit above 85% success despite similar repairability scores. The right panel is a horizontal bar chart showing the share of recorded failure reasons. “Spare parts unavailable” dominates at 65% (rust bar), followed by “Failure unidentified” at 26% and “Insufficient time” at 9% (gray bars). Together, the panels show that repair failure is driven by systemic constraints — primarily missing parts — rather than by volunteer skill or product difficulty. Data source: Repair Monitor (repaircafes.org).

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, patchwork, binom    
    )
})

### |- figure size ----
camcorder::gg_record(
  dir    = here::here("temp_plots"),
  device = "png",
  width  = 12,
  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 = 14)
repairs <- tt$repairs |> clean_names()
repairs_text <- tt$repairs_text|> clean_names()
rm(tt)
```

3. Examine the Data

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

glimpse(repairs)
glimpse(repairs_text)
```

4. Tidy Data

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

### |- Panel A: potential vs. outcome ----

# Binary outcome: repaired = "yes" only; everything else = not repaired
panel_a_data <- repairs |>
  filter(!is.na(repairability)) |>
  mutate(fixed = repaired == "yes") |>
  group_by(category) |>
  summarise(
    n              = n(),
    median_repair  = median(repairability, na.rm = TRUE),
    n_fixed        = sum(fixed, na.rm = TRUE),
    .groups        = "drop"
  ) |>
  # Wilson CI on success rate — more reliable than raw proportion at small n
  mutate(
    ci             = map2(n_fixed, n, ~ binom.wilson(.x, .y)),
    pct_fixed      = map_dbl(ci, ~ .x$mean),
    pct_lower      = map_dbl(ci, ~ .x$lower),
    pct_upper      = map_dbl(ci, ~ .x$upper)
  ) |>
  filter(n >= 200) |>
  # Quadrant flag: high repairability (≥ 6) but low fix rate (< 50%)
  mutate(
    quadrant = case_when(
      median_repair >= 6 & pct_fixed < 0.50 ~ "system_failure",
      median_repair >= 6 & pct_fixed >= 0.50 ~ "system_works",
      median_repair < 6 & pct_fixed < 0.50 ~ "expected",
      median_repair < 6 & pct_fixed >= 0.50 ~ "skilled_volunteers",
      TRUE ~ "other"
    ),
    category_clean = str_to_title(
      str_wrap(str_replace_all(category, "_", " "), width = 22)
    )
  )

# Top "system failure" categories to label
label_cats <- panel_a_data |>
  filter(quadrant == "system_failure") |>
  slice_max(n, n = 6)

# Also label the top "system works" anchor point
label_works <- panel_a_data |>
  filter(quadrant == "system_works") |>
  slice_max(pct_fixed, n = 2)

panel_a_labels <- bind_rows(label_cats, label_works)


### |- Panel B: failure reasons ----
panel_b_data <- repairs_text |>
  filter(!is.na(failure_reasons), failure_reasons != "") |>
  # failure_reasons is a comma-delimited list
  separate_rows(failure_reasons, sep = ",") |>
  mutate(
    failure_reasons = str_squish(str_to_lower(failure_reasons))
  ) |>
  filter(failure_reasons != "", failure_reasons != "na") |>
  # Group semantically similar reasons
  mutate(
    reason_group = case_when(
      str_detect(failure_reasons, "spare part|parts not|no parts|onderdeel") ~
        "Spare parts unavailable",
      str_detect(failure_reasons, "unidentified|unknown|not found|unclear") ~
        "Failure unidentified",
      str_detect(failure_reasons, "cost|expensive|not worth|economisch") ~
        "Not cost-effective",
      str_detect(failure_reasons, "time|too long|no time") ~
        "Insufficient time",
      str_detect(failure_reasons, "skill|expertise|knowledge|competence") ~
        "Skill / expertise gap",
      str_detect(failure_reasons, "owner|customer|taken away|meegenomen") ~
        "Owner decision",
      str_detect(failure_reasons, "safety|gevaar|dangerous") ~
        "Safety concern",
      str_detect(failure_reasons, "irreparable|beyond repair|too damaged") ~
        "Irreparable damage",
      TRUE ~ "Other"
    )
  ) |>
  filter(reason_group != "Other") |>
  count(reason_group, sort = TRUE) |>
  mutate(
    pct = n / sum(n),
    reason_group = fct_reorder(reason_group, pct),
    bar_fill = if_else(
      reason_group == fct_reorder(reason_group, pct) |>
        levels() |>
        last(),
      "accent", "neutral"
    )
  )
```

5. Visualization Parameters

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

### |-  plot aesthetics ----
colors <- get_theme_colors(
    palette = c(
        "system_failure"     = "#C26A3D",   
        "system_works"       = "#3B4252",   
        "expected"           = "#9BA8B5",   
        "skilled_volunteers" = "#5F7A8A",   
        "ref_line"           = "#4A4A4A",   
        "bar_accent"         = "#C26A3D",   
        "bar_neutral"        = "#B0B7BF"    
    )
)

### |- titles and caption ----
title_text    <- str_glue("Repair Isn't the Problem — The System Is")

subtitle_text <- str_glue(
    'Even **"easy-to-fix"** products fail in Repair Cafes — not due to **skill**,',
    " but due to **missing parts**, **unclear diagnostics**, and **cost barriers**"
)

caption_text <- create_social_caption(
    tt_year     = 2026,
    tt_week     = 14,
    source_text = "Repair Monitor (repaircafes.org)  ·  Bubble size = repair attempts"
)

panel_a_title <- "Potential vs. Outcome"
panel_a_sub   <- "High repairability scores don't guarantee a fix.\nAmber points identify categories where the system falls short."

panel_b_title <- 'Why **"Repairable"** Items Still Fail'
panel_b_sub   <- "When volunteers can't fix it, the cause is rarely skill.\nAmber bars reflect systemic constraints beyond the cafe."

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

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

weekly_theme <- extend_weekly_theme(
  base_theme,
  theme(
    # Panel
    panel.grid.major.y = element_line(color = "gray92", linewidth = 0.3),
    panel.grid.major.x = element_line(color = "gray92", linewidth = 0.3),
    panel.grid.minor = element_blank(),
    panel.spacing = unit(1.2, "lines"),

    # Axes
    axis.title = element_text(
      size = 9, color = "gray30",
      family = fonts$text
    ),
    axis.text = element_text(
      size = 8, color = "gray40",
      family = fonts$text
    ),
    axis.ticks = element_blank(),

    # Legend
    legend.position = "none",

    # Strip (if used)
    strip.text = element_text(
      size = 9, face = "bold",
      family = fonts$title
    )
  )
)

theme_set(weekly_theme)
```

6. Plot

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

### |- Panel A: scatter — repairability vs. fix rate ----
p_a <- panel_a_data |>
  ggplot(aes(x = median_repair, y = pct_fixed)) +

  # Geoms
  annotate(
    "rect",
    xmin = 6, xmax = Inf, ymin = -Inf, ymax = 0.50,
    fill = "#C26A3D", alpha = 0.04
  ) +
  geom_vline(
    xintercept = 6, color = colors$palette["ref_line"],
    linetype = "dashed", linewidth = 0.4, alpha = 0.6
  ) +
  geom_hline(
    yintercept = 0.50, color = colors$palette["ref_line"],
    linetype = "dashed", linewidth = 0.4, alpha = 0.6
  ) +
  geom_linerange(
    aes(
      ymin = pct_lower, ymax = pct_upper,
      color = quadrant
    ),
    linewidth = 0.5, alpha = 0.4
  ) +
  geom_point(
    aes(size = n, color = quadrant, fill = quadrant),
    shape = 21, stroke = 0.5, alpha = 0.75
  ) +
  geom_text_repel(
    data = panel_a_labels,
    aes(label = category_clean, color = quadrant),
    size = 2.2,
    family = fonts$text,
    lineheight = 0.9,
    max.overlaps = 20,
    box.padding = 0.4,
    point.padding = 0.3,
    segment.color = "gray75",
    segment.size = 0.25,
    seed = 123
  ) +

  # Annotate
  annotate("text",
    x = 1.1, y = 0.97,
    label = "Expert-driven\nsuccess",
    hjust = 0, vjust = 1, size = 2.2, color = "gray65",
    fontface = "italic", lineheight = 0.9, family = fonts$text
  ) +
  annotate("text",
    x = 6.1, y = 0.97,
    label = "System works",
    hjust = 0, vjust = 1, size = 2.2, color = "gray65",
    fontface = "italic", family = fonts$text
  ) +
  annotate("text",
    x = 1.1, y = 0.03,
    label = "Expected\ndifficulty",
    hjust = 0, vjust = 0, size = 2.2, color = "gray65",
    fontface = "italic", lineheight = 0.9, family = fonts$text
  ) +
  annotate("text",
    x = 7.5, y = 0.30,
    label = "System\nconstraint",
    hjust = 0.5, vjust = 0.5, size = 2.3, color = "#C26A3D",
    fontface = "bold.italic", lineheight = 0.9, family = fonts$text
  ) +
  annotate("text",
    x = 6.08, y = 0.53,
    label = "≥6 = repairable",
    hjust = 0, vjust = 0, size = 2.0, color = "gray45",
    fontface = "italic", family = fonts$text
  ) +
  annotate("text",
    x = 1.1, y = 0.52,
    label = "50% success threshold",
    hjust = 0, vjust = 0, size = 2.0, color = "gray45",
    fontface = "italic", family = fonts$text
  ) +
  annotate("text",
    x = 9.5, y = 0.44,
    label = "← Amber zone:\nsee breakdown →",
    hjust = 0.5, vjust = 1, size = 2.1, color = "#C26A3D",
    fontface = "italic", lineheight = 0.95, family = fonts$text
  ) +

  # Scales
  scale_color_manual(values = c(
    "system_failure"     = unname(colors$palette["system_failure"]),
    "system_works"       = unname(colors$palette["system_works"]),
    "expected"           = unname(colors$palette["expected"]),
    "skilled_volunteers" = unname(colors$palette["skilled_volunteers"])
  )) +
  scale_fill_manual(values = c(
    "system_failure"     = unname(colors$palette["system_failure"]),
    "system_works"       = unname(colors$palette["system_works"]),
    "expected"           = unname(colors$palette["expected"]),
    "skilled_volunteers" = unname(colors$palette["skilled_volunteers"])
  )) +
  scale_size_continuous(range = c(2, 9)) +
  scale_x_continuous(
    name   = "Median Repairability Score  (≥6 = generally considered repairable)",
    limits = c(1, 10),
    breaks = 1:10
  ) +
  scale_y_continuous(
    name   = "% Successfully Repaired",
    labels = percent_format(accuracy = 1),
    limits = c(0, 1)
  ) +

  # Labs
  labs(
    title    = panel_a_title,
    subtitle = panel_a_sub
  ) +

  # Theme
  theme(
    plot.title = element_text(
      size = 11, face = "bold",
      family = fonts$title, color = "gray15"
    ),
    plot.subtitle = element_text(
      size = 8, color = "gray40",
      family = fonts$text, lineheight = 1.3,
      margin = margin(b = 8)
    )
  )

### |- Panel B: failure reasons bar chart ----
p_b <- panel_b_data |>
  ggplot(aes(x = pct, y = reason_group, fill = bar_fill)) +
  # Geoms
  geom_col(width = 0.7) +
  geom_text(
    aes(label = percent(pct, accuracy = 1)),
    hjust = -0.15,
    size = 2.8,
    color = "gray30",
    family = fonts$text
  ) +
  # Scales
  scale_fill_manual(
    values = c(
      "accent"  = unname(colors$palette["bar_accent"]),
      "neutral" = unname(colors$palette["bar_neutral"])
    )
  ) +
  scale_x_continuous(
    labels = percent_format(accuracy = 1),
    expand = expansion(mult = c(0, 0.15))
  ) +
  # Labs
  labs(
    x        = "Share of recorded failure reasons",
    y        = NULL,
    title    = panel_b_title,
    subtitle = panel_b_sub
  ) +
  # Theme
  theme(
    panel.grid.major.x = element_line(color = "gray92", linewidth = 0.3),
    panel.grid.major.y = element_blank(),
    plot.title = element_markdown(
      size = 11, face = "bold",
      family = fonts$title, color = "gray15"
    ),
    plot.subtitle = element_text(
      size = 8, color = "gray40",
      family = fonts$text, lineheight = 1.3,
      margin = margin(b = 8)
    ),
    axis.text.y = element_text(
      size = 8.5, color = "gray25",
      family = fonts$text
    )
  )

### |- Combined layout ----
combined_plots <- p_a + p_b +
  plot_layout(widths = c(1.4, 1)) +
  plot_annotation(
    title = title_text,
    subtitle = subtitle_text,
    caption = caption_text,
    theme = theme(
      plot.title = element_text(
        size         = 20,
        face         = "bold",
        family       = fonts$title,
        color        = "gray10",
        margin       = margin(b = 6)
      ),
      plot.subtitle = element_markdown(
        size         = 12,
        family       = fonts$text,
        color        = "gray35",
        lineheight   = 1.35,
        margin       = margin(b = 14)
      ),
      plot.caption = element_markdown(
        size         = 7,
        family       = fonts$text,
        color        = "gray55",
        hjust        = 0.5,
        margin       = margin(t = 12)
      ),
      plot.background = element_rect(fill = "gray98", color = NA),
      plot.margin = margin(20, 20, 12, 20)
    )
  )
```

7. Save

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

### |-  plot image ----  
save_plot_patchwork(
  plot = combined_plots, 
  type = "tidytuesday", 
  year = 2026, 
  week = 14, 
  width  = 12,
  height = 8
  )
```

8. Session Info

TipExpand for Session Info
R version 4.3.1 (2023-06-16 ucrt)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 11 x64 (build 26100)

Matrix products: default


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      binom_1.1-1.1   patchwork_1.3.2 skimr_2.2.2    
 [5] glue_1.8.0      scales_1.4.0    ggrepel_0.9.8   janitor_2.2.1  
 [9] showtext_0.9-7  showtextdb_3.0  sysfonts_0.8.9  ggtext_0.1.2   
[13] lubridate_1.9.5 forcats_1.0.1   stringr_1.6.0   dplyr_1.2.0    
[17] purrr_1.2.1     readr_2.2.0     tidyr_1.3.2     tibble_3.2.1   
[21] ggplot2_4.0.2   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.0           fastmap_1.2.0     
 [5] gh_1.4.1           digest_0.6.39      timechange_0.4.0   lifecycle_1.0.5   
 [9] rsvg_2.6.2         magrittr_2.0.3     compiler_4.3.1     rlang_1.1.7       
[13] tools_4.3.1        yaml_2.3.12        knitr_1.51         labeling_0.4.3    
[17] htmlwidgets_1.6.4  bit_4.6.0          curl_7.0.0         xml2_1.5.2        
[21] camcorder_0.1.0    repr_1.1.7         RColorBrewer_1.1-3 tidytuesdayR_1.2.1
[25] withr_3.0.2        grid_4.3.1         gitcreds_0.1.2     cli_3.6.5         
[29] rmarkdown_2.30     crayon_1.5.3       generics_0.1.4     otel_0.2.0        
[33] rstudioapi_0.18.0  tzdb_0.5.0         commonmark_2.0.0   parallel_4.3.1    
[37] ggplotify_0.1.3    base64enc_0.1-6    vctrs_0.7.1        yulab.utils_0.2.4 
[41] jsonlite_2.0.0     litedown_0.9       gridGraphics_0.5-1 hms_1.1.4         
[45] bit64_4.6.0-1      systemfonts_1.3.2  magick_2.8.6       gifski_1.32.0-2   
[49] codetools_0.2-19   stringi_1.8.7      gtable_0.3.6       pillar_1.11.1     
[53] rappdirs_0.3.4     htmltools_0.5.9    R6_2.6.1           httr2_1.2.2       
[57] rprojroot_2.1.1    vroom_1.7.0        evaluate_1.0.5     markdown_2.0      
[61] gridtext_0.1.6     snakecase_0.11.1   Rcpp_1.1.1         svglite_2.1.3     
[65] xfun_0.56          fs_1.6.7           pkgconfig_2.0.3   

9. GitHub Repository

TipExpand for GitHub Repo

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

For the full repository, click here.

10. References

TipExpand for References
  1. Data Source:
    • TidyTuesday 2026 Week 14: Repair Cafes Worldwide

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 = {Repair {Isn’t} the {Problem} — {The} {System} {Is}},
  date = {2026-04-04},
  url = {https://stevenponce.netlify.app/data_visualizations/TidyTuesday/2026/tt_2026_14.html},
  langid = {en}
}
For attribution, please cite this work as:
Ponce, Steven. 2026. “Repair Isn’t the Problem — The System Is.” April 4, 2026. https://stevenponce.netlify.app/data_visualizations/TidyTuesday/2026/tt_2026_14.html.
Source Code
---
title: "Repair Isn't the Problem — The System Is"
subtitle: "Even easy-to-fix products fail in Repair Cafes — not due to skill, but due to missing parts, unclear diagnostics, and cost barriers"
description: "Analysis of 178,000+ repair records from the global Repair Monitor dataset, examining why products rated as easy to fix still fail at Repair Cafes worldwide. A two-panel visualization contrasts repairability scores against actual repair outcomes by product category, revealing that system constraints — primarily missing spare parts — explain failure far more than volunteer skill or product difficulty. Built with ggplot2, patchwork, and binom in R."
date: "2026-04-04"
author:
  - name: "Steven Ponce"
    url: "https://stevenponce.netlify.app"
citation:
  url: "https://stevenponce.netlify.app/data_visualizations/TidyTuesday/2026/tt_2026_14.html" 
categories: ["TidyTuesday", "Data Visualization", "R Programming", "2026"]
tags: [
  "Repair Cafe",
  "Circular Economy",
  "Right to Repair",
  "Consumer Products",
  "Repairability",
  "Scatter Plot",
  "Bar Chart",
  "Two-Panel Layout",
  "patchwork",
  "ggrepel",
  "binom",
  "Wilson Confidence Intervals",
  "Supply Chain",
  "Sustainability"
]
image: "thumbnails/tt_2026_14.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
---

![A two-panel data visualization titled "Repair Isn't the Problem — The System Is." The left panel is a scatter plot showing product categories by median repairability score (x-axis, 1–10) versus percentage successfully repaired (y-axis, 0–100%). A shaded amber region marks the "system constraint" zone — categories rated easy to fix (score ≥6) but repaired less than 50% of the time. Two rust-colored points, Computer Equipment/Phones and Display and Sound Equipment, fall in this zone near the 50% threshold. Slate-colored points for Textiles and Tools Non-Electric sit above 85% success despite similar repairability scores. The right panel is a horizontal bar chart showing the share of recorded failure reasons. "Spare parts unavailable" dominates at 65% (rust bar), followed by "Failure unidentified" at 26% and "Insufficient time" at 9% (gray bars). Together, the panels show that repair failure is driven by systemic constraints — primarily missing parts — rather than by volunteer skill or product difficulty. Data source: Repair Monitor (repaircafes.org).](tt_2026_14.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, patchwork, binom    
    )
})

### |- figure size ----
camcorder::gg_record(
  dir    = here::here("temp_plots"),
  device = "png",
  width  = 12,
  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 = 14)
repairs <- tt$repairs |> clean_names()
repairs_text <- tt$repairs_text|> clean_names()
rm(tt)
```

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

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

glimpse(repairs)
glimpse(repairs_text)
```

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

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

### |- Panel A: potential vs. outcome ----

# Binary outcome: repaired = "yes" only; everything else = not repaired
panel_a_data <- repairs |>
  filter(!is.na(repairability)) |>
  mutate(fixed = repaired == "yes") |>
  group_by(category) |>
  summarise(
    n              = n(),
    median_repair  = median(repairability, na.rm = TRUE),
    n_fixed        = sum(fixed, na.rm = TRUE),
    .groups        = "drop"
  ) |>
  # Wilson CI on success rate — more reliable than raw proportion at small n
  mutate(
    ci             = map2(n_fixed, n, ~ binom.wilson(.x, .y)),
    pct_fixed      = map_dbl(ci, ~ .x$mean),
    pct_lower      = map_dbl(ci, ~ .x$lower),
    pct_upper      = map_dbl(ci, ~ .x$upper)
  ) |>
  filter(n >= 200) |>
  # Quadrant flag: high repairability (≥ 6) but low fix rate (< 50%)
  mutate(
    quadrant = case_when(
      median_repair >= 6 & pct_fixed < 0.50 ~ "system_failure",
      median_repair >= 6 & pct_fixed >= 0.50 ~ "system_works",
      median_repair < 6 & pct_fixed < 0.50 ~ "expected",
      median_repair < 6 & pct_fixed >= 0.50 ~ "skilled_volunteers",
      TRUE ~ "other"
    ),
    category_clean = str_to_title(
      str_wrap(str_replace_all(category, "_", " "), width = 22)
    )
  )

# Top "system failure" categories to label
label_cats <- panel_a_data |>
  filter(quadrant == "system_failure") |>
  slice_max(n, n = 6)

# Also label the top "system works" anchor point
label_works <- panel_a_data |>
  filter(quadrant == "system_works") |>
  slice_max(pct_fixed, n = 2)

panel_a_labels <- bind_rows(label_cats, label_works)


### |- Panel B: failure reasons ----
panel_b_data <- repairs_text |>
  filter(!is.na(failure_reasons), failure_reasons != "") |>
  # failure_reasons is a comma-delimited list
  separate_rows(failure_reasons, sep = ",") |>
  mutate(
    failure_reasons = str_squish(str_to_lower(failure_reasons))
  ) |>
  filter(failure_reasons != "", failure_reasons != "na") |>
  # Group semantically similar reasons
  mutate(
    reason_group = case_when(
      str_detect(failure_reasons, "spare part|parts not|no parts|onderdeel") ~
        "Spare parts unavailable",
      str_detect(failure_reasons, "unidentified|unknown|not found|unclear") ~
        "Failure unidentified",
      str_detect(failure_reasons, "cost|expensive|not worth|economisch") ~
        "Not cost-effective",
      str_detect(failure_reasons, "time|too long|no time") ~
        "Insufficient time",
      str_detect(failure_reasons, "skill|expertise|knowledge|competence") ~
        "Skill / expertise gap",
      str_detect(failure_reasons, "owner|customer|taken away|meegenomen") ~
        "Owner decision",
      str_detect(failure_reasons, "safety|gevaar|dangerous") ~
        "Safety concern",
      str_detect(failure_reasons, "irreparable|beyond repair|too damaged") ~
        "Irreparable damage",
      TRUE ~ "Other"
    )
  ) |>
  filter(reason_group != "Other") |>
  count(reason_group, sort = TRUE) |>
  mutate(
    pct = n / sum(n),
    reason_group = fct_reorder(reason_group, pct),
    bar_fill = if_else(
      reason_group == fct_reorder(reason_group, pct) |>
        levels() |>
        last(),
      "accent", "neutral"
    )
  )
```

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

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

### |-  plot aesthetics ----
colors <- get_theme_colors(
    palette = c(
        "system_failure"     = "#C26A3D",   
        "system_works"       = "#3B4252",   
        "expected"           = "#9BA8B5",   
        "skilled_volunteers" = "#5F7A8A",   
        "ref_line"           = "#4A4A4A",   
        "bar_accent"         = "#C26A3D",   
        "bar_neutral"        = "#B0B7BF"    
    )
)

### |- titles and caption ----
title_text    <- str_glue("Repair Isn't the Problem — The System Is")

subtitle_text <- str_glue(
    'Even **"easy-to-fix"** products fail in Repair Cafes — not due to **skill**,',
    " but due to **missing parts**, **unclear diagnostics**, and **cost barriers**"
)

caption_text <- create_social_caption(
    tt_year     = 2026,
    tt_week     = 14,
    source_text = "Repair Monitor (repaircafes.org)  ·  Bubble size = repair attempts"
)

panel_a_title <- "Potential vs. Outcome"
panel_a_sub   <- "High repairability scores don't guarantee a fix.\nAmber points identify categories where the system falls short."

panel_b_title <- 'Why **"Repairable"** Items Still Fail'
panel_b_sub   <- "When volunteers can't fix it, the cause is rarely skill.\nAmber bars reflect systemic constraints beyond the cafe."

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

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

weekly_theme <- extend_weekly_theme(
  base_theme,
  theme(
    # Panel
    panel.grid.major.y = element_line(color = "gray92", linewidth = 0.3),
    panel.grid.major.x = element_line(color = "gray92", linewidth = 0.3),
    panel.grid.minor = element_blank(),
    panel.spacing = unit(1.2, "lines"),

    # Axes
    axis.title = element_text(
      size = 9, color = "gray30",
      family = fonts$text
    ),
    axis.text = element_text(
      size = 8, color = "gray40",
      family = fonts$text
    ),
    axis.ticks = element_blank(),

    # Legend
    legend.position = "none",

    # Strip (if used)
    strip.text = element_text(
      size = 9, face = "bold",
      family = fonts$title
    )
  )
)

theme_set(weekly_theme)
```

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

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

### |- Panel A: scatter — repairability vs. fix rate ----
p_a <- panel_a_data |>
  ggplot(aes(x = median_repair, y = pct_fixed)) +

  # Geoms
  annotate(
    "rect",
    xmin = 6, xmax = Inf, ymin = -Inf, ymax = 0.50,
    fill = "#C26A3D", alpha = 0.04
  ) +
  geom_vline(
    xintercept = 6, color = colors$palette["ref_line"],
    linetype = "dashed", linewidth = 0.4, alpha = 0.6
  ) +
  geom_hline(
    yintercept = 0.50, color = colors$palette["ref_line"],
    linetype = "dashed", linewidth = 0.4, alpha = 0.6
  ) +
  geom_linerange(
    aes(
      ymin = pct_lower, ymax = pct_upper,
      color = quadrant
    ),
    linewidth = 0.5, alpha = 0.4
  ) +
  geom_point(
    aes(size = n, color = quadrant, fill = quadrant),
    shape = 21, stroke = 0.5, alpha = 0.75
  ) +
  geom_text_repel(
    data = panel_a_labels,
    aes(label = category_clean, color = quadrant),
    size = 2.2,
    family = fonts$text,
    lineheight = 0.9,
    max.overlaps = 20,
    box.padding = 0.4,
    point.padding = 0.3,
    segment.color = "gray75",
    segment.size = 0.25,
    seed = 123
  ) +

  # Annotate
  annotate("text",
    x = 1.1, y = 0.97,
    label = "Expert-driven\nsuccess",
    hjust = 0, vjust = 1, size = 2.2, color = "gray65",
    fontface = "italic", lineheight = 0.9, family = fonts$text
  ) +
  annotate("text",
    x = 6.1, y = 0.97,
    label = "System works",
    hjust = 0, vjust = 1, size = 2.2, color = "gray65",
    fontface = "italic", family = fonts$text
  ) +
  annotate("text",
    x = 1.1, y = 0.03,
    label = "Expected\ndifficulty",
    hjust = 0, vjust = 0, size = 2.2, color = "gray65",
    fontface = "italic", lineheight = 0.9, family = fonts$text
  ) +
  annotate("text",
    x = 7.5, y = 0.30,
    label = "System\nconstraint",
    hjust = 0.5, vjust = 0.5, size = 2.3, color = "#C26A3D",
    fontface = "bold.italic", lineheight = 0.9, family = fonts$text
  ) +
  annotate("text",
    x = 6.08, y = 0.53,
    label = "≥6 = repairable",
    hjust = 0, vjust = 0, size = 2.0, color = "gray45",
    fontface = "italic", family = fonts$text
  ) +
  annotate("text",
    x = 1.1, y = 0.52,
    label = "50% success threshold",
    hjust = 0, vjust = 0, size = 2.0, color = "gray45",
    fontface = "italic", family = fonts$text
  ) +
  annotate("text",
    x = 9.5, y = 0.44,
    label = "← Amber zone:\nsee breakdown →",
    hjust = 0.5, vjust = 1, size = 2.1, color = "#C26A3D",
    fontface = "italic", lineheight = 0.95, family = fonts$text
  ) +

  # Scales
  scale_color_manual(values = c(
    "system_failure"     = unname(colors$palette["system_failure"]),
    "system_works"       = unname(colors$palette["system_works"]),
    "expected"           = unname(colors$palette["expected"]),
    "skilled_volunteers" = unname(colors$palette["skilled_volunteers"])
  )) +
  scale_fill_manual(values = c(
    "system_failure"     = unname(colors$palette["system_failure"]),
    "system_works"       = unname(colors$palette["system_works"]),
    "expected"           = unname(colors$palette["expected"]),
    "skilled_volunteers" = unname(colors$palette["skilled_volunteers"])
  )) +
  scale_size_continuous(range = c(2, 9)) +
  scale_x_continuous(
    name   = "Median Repairability Score  (≥6 = generally considered repairable)",
    limits = c(1, 10),
    breaks = 1:10
  ) +
  scale_y_continuous(
    name   = "% Successfully Repaired",
    labels = percent_format(accuracy = 1),
    limits = c(0, 1)
  ) +

  # Labs
  labs(
    title    = panel_a_title,
    subtitle = panel_a_sub
  ) +

  # Theme
  theme(
    plot.title = element_text(
      size = 11, face = "bold",
      family = fonts$title, color = "gray15"
    ),
    plot.subtitle = element_text(
      size = 8, color = "gray40",
      family = fonts$text, lineheight = 1.3,
      margin = margin(b = 8)
    )
  )

### |- Panel B: failure reasons bar chart ----
p_b <- panel_b_data |>
  ggplot(aes(x = pct, y = reason_group, fill = bar_fill)) +
  # Geoms
  geom_col(width = 0.7) +
  geom_text(
    aes(label = percent(pct, accuracy = 1)),
    hjust = -0.15,
    size = 2.8,
    color = "gray30",
    family = fonts$text
  ) +
  # Scales
  scale_fill_manual(
    values = c(
      "accent"  = unname(colors$palette["bar_accent"]),
      "neutral" = unname(colors$palette["bar_neutral"])
    )
  ) +
  scale_x_continuous(
    labels = percent_format(accuracy = 1),
    expand = expansion(mult = c(0, 0.15))
  ) +
  # Labs
  labs(
    x        = "Share of recorded failure reasons",
    y        = NULL,
    title    = panel_b_title,
    subtitle = panel_b_sub
  ) +
  # Theme
  theme(
    panel.grid.major.x = element_line(color = "gray92", linewidth = 0.3),
    panel.grid.major.y = element_blank(),
    plot.title = element_markdown(
      size = 11, face = "bold",
      family = fonts$title, color = "gray15"
    ),
    plot.subtitle = element_text(
      size = 8, color = "gray40",
      family = fonts$text, lineheight = 1.3,
      margin = margin(b = 8)
    ),
    axis.text.y = element_text(
      size = 8.5, color = "gray25",
      family = fonts$text
    )
  )

### |- Combined layout ----
combined_plots <- p_a + p_b +
  plot_layout(widths = c(1.4, 1)) +
  plot_annotation(
    title = title_text,
    subtitle = subtitle_text,
    caption = caption_text,
    theme = theme(
      plot.title = element_text(
        size         = 20,
        face         = "bold",
        family       = fonts$title,
        color        = "gray10",
        margin       = margin(b = 6)
      ),
      plot.subtitle = element_markdown(
        size         = 12,
        family       = fonts$text,
        color        = "gray35",
        lineheight   = 1.35,
        margin       = margin(b = 14)
      ),
      plot.caption = element_markdown(
        size         = 7,
        family       = fonts$text,
        color        = "gray55",
        hjust        = 0.5,
        margin       = margin(t = 12)
      ),
      plot.background = element_rect(fill = "gray98", color = NA),
      plot.margin = margin(20, 20, 12, 20)
    )
  )
```

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

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

### |-  plot image ----  
save_plot_patchwork(
  plot = combined_plots, 
  type = "tidytuesday", 
  year = 2026, 
  week = 14, 
  width  = 12,
  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_14.qmd`](https://github.com/poncest/personal-website/blob/master/data_visualizations/TidyTuesday/2026/tt_2026_14.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 14: [Repair Cafes Worldwide
](https://github.com/rfordatascience/tidytuesday/blob/main/data/2026/2026-04-07/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