{"spec_id":"survival-kaplan-meier","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' survival-kaplan-meier: Kaplan-Meier Survival Plot\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 92/100 | Created: 2026-09-09\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(survival)\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\"\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# --- Data: clinical trial follow-up (2 treatment arms, right-censored) -----\nn_per_arm  <- 110\nfollow_up  <- 36  # months of administrative follow-up\narm        <- factor(rep(c(\"Standard care\", \"New therapy\"), each = n_per_arm),\n                      levels = c(\"Standard care\", \"New therapy\"))\nevent_rate <- ifelse(arm == \"Standard care\", 1 / 17, 1 / 25)\nevent_time   <- rexp(2 * n_per_arm, rate = event_rate)\ndropout_time <- runif(2 * n_per_arm, 18, follow_up)\npatients <- tibble::tibble(\n  time  = pmin(event_time, dropout_time, follow_up),\n  event = as.integer(event_time <= pmin(dropout_time, follow_up)),\n  arm   = arm\n)\n\nfit <- survfit(Surv(time, event) ~ arm, data = patients)\nfit_summary <- summary(fit, censored = TRUE)\n\nkm_curve <- tibble::tibble(\n  time     = fit_summary$time,\n  surv     = fit_summary$surv,\n  lower    = fit_summary$lower,\n  upper    = fit_summary$upper,\n  n_censor = fit_summary$n.censor,\n  arm      = factor(sub(\"^arm=\", \"\", as.character(fit_summary$strata)), levels = levels(arm))\n)\n\n# Prepend a t=0, survival=1 anchor per arm so the step curve starts at the top\nkm_start <- tibble::tibble(\n  time = 0, surv = 1, lower = 1, upper = 1, n_censor = 0,\n  arm = factor(levels(arm), levels = levels(arm))\n)\nkm_curve <- bind_rows(km_start, km_curve) %>% arrange(arm, time)\n\n# Stairstep confidence band: one rectangle per interval, held constant until\n# the next event/censoring time (geom_ribbon interpolates linearly, which\n# misrepresents a step function; geom_rect renders the true stairs instead)\nkm_band <- km_curve %>%\n  group_by(arm) %>%\n  mutate(time_end = lead(time, default = follow_up)) %>%\n  ungroup()\n\ncensor_marks <- km_curve %>% filter(n_censor > 0)\n\nlog_rank   <- survdiff(Surv(time, event) ~ arm, data = patients)\np_value    <- 1 - pchisq(log_rank$chisq, length(log_rank$n) - 1)\np_label    <- if (p_value < 0.001) \"p < 0.001\" else sprintf(\"p = %.3f\", p_value)\n\ntitle_text <- \"survival-kaplan-meier · r · ggplot2 · anyplot.ai\"\n\n# --- Plot --------------------------------------------------------------------\np <- ggplot(km_curve, aes(x = time, y = surv, color = arm, fill = arm)) +\n  geom_rect(\n    data = km_band,\n    aes(xmin = time, xmax = time_end, ymin = lower, ymax = upper, fill = arm),\n    inherit.aes = FALSE, alpha = 0.15, color = NA\n  ) +\n  geom_step(linewidth = 1.0) +\n  geom_point(\n    data = censor_marks, aes(x = time, y = surv, color = arm),\n    shape = 3, size = 2.5, stroke = 1, show.legend = FALSE\n  ) +\n  scale_color_manual(values = IMPRINT_PALETTE) +\n  scale_fill_manual(values = IMPRINT_PALETTE) +\n  scale_y_continuous(labels = scales::percent_format(accuracy = 1), limits = c(0, 1)) +\n  labs(\n    title    = title_text,\n    subtitle = sprintf(\"Log-rank test: %s\", p_label),\n    x        = \"Time Since Enrollment (months)\",\n    y        = \"Survival Probability\",\n    color    = \"Treatment Arm\",\n    fill     = \"Treatment Arm\"\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.y = element_line(color = INK, linewidth = 0.3),\n    panel.grid.minor   = element_blank(),\n    panel.grid.major.x = element_blank(),\n    axis.title         = element_text(color = INK, size = 10),\n    axis.text          = element_text(color = INK_SOFT, size = 8),\n    axis.line          = element_line(color = INK_SOFT),\n    plot.title         = element_text(color = INK, size = 12),\n    plot.subtitle      = element_text(color = INK_SOFT, size = 9),\n    legend.position        = \"inside\",\n    legend.position.inside = c(0.82, 0.82),\n    legend.background  = element_rect(fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.2),\n    legend.text        = element_text(color = INK_SOFT, size = 8),\n    legend.title       = element_text(color = INK, size = 9),\n    plot.margin        = margin(10, 14, 10, 10)\n  )\n\n# --- Save -------------------------------------------------------------------\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"}