{"spec_id":"line-retention-cohort","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' line-retention-cohort: User Retention Curve by Cohort\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 90/100 | Created: 2026-06-20\n\nlibrary(ggplot2)\nlibrary(scales)\nlibrary(ragg)\n\nset.seed(42)\n\n# Theme tokens\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nINK_MUTED   <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\nGRID_COLOR  <- scales::alpha(INK, 0.15)\n\n# Imprint palette positions 1-4 for 4 categorical cohorts\nCOHORT_COLORS <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\")\n\n# Data — 4 monthly signup cohorts tracked weekly for 12 weeks\ncohort_info <- data.frame(\n  cohort    = c(\"Jan 2025\", \"Feb 2025\", \"Mar 2025\", \"Apr 2025\"),\n  n_users   = c(1245L, 1387L, 1623L, 1891L),\n  floor_pct = c(10.5, 12.5, 15.0, 18.0),\n  decay     = c(0.26, 0.22, 0.18, 0.14),\n  stringsAsFactors = FALSE\n)\n\nweeks <- 0:12\n\ndf <- do.call(rbind, lapply(seq_len(nrow(cohort_info)), function(i) {\n  cp  <- cohort_info[i, ]\n  # Exponential decay toward a floor; week 0 = exactly 100%\n  ret   <- cp$floor_pct + (100 - cp$floor_pct) * exp(-cp$decay * weeks)\n  noise <- c(0, rnorm(length(weeks) - 1, 0, 0.7))\n  data.frame(\n    week      = weeks,\n    retention = pmax(0, pmin(100, ret + noise)),\n    cohort    = cp$cohort,\n    n_users   = cp$n_users\n  )\n}))\n\ndf$cohort <- factor(df$cohort, levels = cohort_info$cohort)\n\nlegend_labels <- setNames(\n  paste0(cohort_info$cohort, \" (n=\", format(cohort_info$n_users, big.mark = \",\"), \")\"),\n  cohort_info$cohort\n)\n\n# Thinner lines for older cohorts → visual emphasis on recent improvement\nlw_vals <- c(0.55, 0.70, 0.88, 1.10)\n\nplot_title <- \"line-retention-cohort · r · ggplot2 · anyplot.ai\"\n\np <- ggplot(df, aes(x = week, y = retention, color = cohort, linewidth = cohort)) +\n  # 20% retention benchmark reference\n  geom_hline(\n    yintercept = 20,\n    linetype   = \"dashed\",\n    color      = INK_MUTED,\n    linewidth  = 0.35\n  ) +\n  annotate(\n    \"text\",\n    x = 0.2, y = 22.5,\n    label    = \"20% retention target\",\n    color    = INK_MUTED,\n    size     = 2.5,\n    hjust    = 0,\n    fontface = \"italic\"\n  ) +\n  geom_line(lineend = \"round\") +\n  scale_color_manual(\n    values = COHORT_COLORS,\n    labels = legend_labels,\n    name   = \"Signup Cohort\"\n  ) +\n  scale_linewidth_manual(\n    values = lw_vals,\n    labels = legend_labels,\n    name   = \"Signup Cohort\"\n  ) +\n  scale_x_continuous(\n    breaks = seq(0, 12, by = 2),\n    labels = paste0(\"Week \", seq(0, 12, by = 2)),\n    expand = expansion(mult = c(0.02, 0.03))\n  ) +\n  scale_y_continuous(\n    limits = c(0, 100),\n    breaks = seq(0, 100, by = 20),\n    labels = function(x) paste0(x, \"%\"),\n    expand = expansion(mult = c(0.01, 0.03))\n  ) +\n  labs(\n    title = plot_title,\n    x     = \"Weeks Since Signup\",\n    y     = \"Retained Users (%)\"\n  ) +\n  theme_minimal(base_size = 8) +\n  theme(\n    plot.background   = element_rect(fill = PAGE_BG,     color = PAGE_BG),\n    panel.background  = element_rect(fill = PAGE_BG,     color = NA),\n    panel.grid.major  = element_line(color = GRID_COLOR, linewidth = 0.4),\n    panel.grid.minor  = element_blank(),\n    panel.border      = element_blank(),\n    axis.line         = element_line(color = INK_SOFT,   linewidth = 0.35),\n    axis.title        = element_text(color = INK,        size = 10),\n    axis.text         = element_text(color = INK_SOFT,   size = 8),\n    plot.title        = element_text(color = INK,        size = 12, face = \"bold\"),\n    legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.3),\n    legend.text       = element_text(color = INK_SOFT,   size = 8),\n    legend.title      = element_text(color = INK,        size = 10),\n    legend.position   = \"right\",\n    legend.key.width  = unit(1.5, \"cm\"),\n    plot.margin       = margin(15, 20, 10, 10)\n  )\n\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 8,\n  height   = 4.5,\n  units    = \"in\",\n  dpi      = 400\n)\n"}