Creating Kaplan-Meier Survival Plots with tflmetaR and gridify

Introduction

This vignette demonstrates how to create publication-quality Kaplan-Meier survival plots using the survminer package for visualization, gridify for professional annotation layouts, and tflmetaR for centralized metadata management.

The workflow covers:


Loading Required Packages

We begin by loading all necessary packages for survival analysis, data manipulation, and figure annotation.

library(tflmetaR)
library(gridify)
library(haven)
library(dplyr)
library(survival)
library(survminer)

Loading and Preparing ADTTE Data

The Analysis Dataset for Time-to-Event (ADTTE) follows CDISC ADaM standards. Here we use a synthetic oncology dataset generated by the CDISC Dataset Generator, containing 3 treatment arms and 500 subjects.

# get addtte data ----
# synthetic adtte for oncology therapeutic area from CDISC

adtte <- read_xpt(system.file("extdata", "adam_adtte.xpt", package = "tflmetaR"))

Data Transformation

We filter the data for Progression-Free Survival (PFS) analysis and prepare it for survival modeling.

tte <- adtte |>
  rename(avalc = AVALU) |>
  rename_with(tolower) |>
  filter(paramcd == "PFS") |>
  arrange(desc(trtp)) |>
  relocate(usubjid, trtp, cnsr, evntdesc, avalc, aval, trta) |>
  select(usubjid, trtp, cnsr, evntdesc, avalc, aval, trta)

tte$trtp <- factor(tte$trtp)

Fitting the Survival Model

We create a survival object and fit a Kaplan-Meier model stratified by treatment group (trtp).

# run survival modal ----
surv_object <- Surv(time = tte$aval, event = tte$cnsr)
fit1 <- survfit(surv_object ~ trtp, data = tte)

Creating the Survival Plot

The survminer package provides ggsurvplot() for creating publication-ready Kaplan-Meier curves with integrated risk tables, p-values, and median survival lines.

# survial plot and table ----
ggsurv <- ggsurvplot(fit1,
  data = tte, risk.table = TRUE, size = 1, # line size
  pval = TRUE,
  pval.size = 4,
  pval.method = TRUE,
  pval.method.size = 3,
  fontsize = 3,
  surv.median.line = "hv",
  tables.theme = theme_survminer() +
    theme(
      axis.text.x = element_text(size = 9, color = "gray30"),
      axis.text.y = element_text(size = 9, color = "gray30"),
      plot.title = element_text(size = 9),
      element_text(size = 9, color = "red")
    )
)

Customizing the Risk Table

We modify the risk table labels to display treatment group names in a more readable format.

# risk table -----
t <- ggsurv$table +
  scale_y_discrete(label = c("Treatment 3", "Treatment 2", "Treatment 1"))

p1 <- ggsurvplot(fit1, data = tte, risk.table = TRUE) ## can not add to the gridify directly

Combining Plot and Risk Table

Using ggpubr::ggarrange(), we combine the survival plot and risk table into a single figure with proper alignment and proportions.

# ggrance the survival plot  and risk table ----
p1 <- ggarrange(
  ggsurv$plot + labs(x = "", y = "Survival Probability") +
    scale_color_discrete() +
    theme(
      axis.title.x = element_text(vjust = 0, size = 9),
      axis.title.y = element_text(vjust = -3, size = 9), # y axis label
      axis.text.y = element_text(size = 9, color = "gray30"), # tick values
      axis.text.x = element_text(size = 9, color = "gray30"),
      legend.title = element_blank()
    ),
  t + labs(y = "") +
    theme(
      axis.title.x = element_text(vjust = 0, size = 9),
      axis.text.x = element_text(size = 9, color = "gray30")
    ),
  heights = c(2, 1.0),
  ncol = 1, nrow = 2, align = "v"
)

Annotating with gridify

The gridify package provides a framework for adding professional annotations to figures, including headers, titles, footnotes, and page numbers. The pharma_layout_base() function creates a layout compliant with pharmaceutical industry standards.

Basic Gridify Example

This example demonstrates manual annotation using gridify::set_cell() to populate each annotation field.

## --- use gridify to annotate the figure ---
fig <- gridify(
  p1,
  layout = pharma_layout_base(
    margin = grid::unit(c(1, 1, 1.23, 1), "inches"),
    global_gpar = grid::gpar(fontfamily = "Courier")
  )
) |>
  set_cell("header_left_1", "UCB") |>
  set_cell("header_left_2", "Drug X / Unspecified") |>
  set_cell("header_left_3", "STUDY001") |>
  set_cell("header_right_1", "CONFIDENTIAL") |>
  set_cell("header_right_2", "Final") |>
  set_cell("header_right_3", paste0("Data Cut-Off Date")) |>
  set_cell("output_num", "Figure F01") |>
  set_cell("title_1", "The Kaplan-Meier Curves of Progression-Free Survival Among Treatment Groups") |>
  set_cell("title_2", "Population: Safety Set") |>
  set_cell("title_3", "") |>
  set_cell(
    "note",
    paste0(
      "The synthetic oncology ADTTE was generated by CDISC Dataset Generator, ",
      "with 3 treatment arms and 500 subjects.\n",
      "Note: The Kaplan-Meier estimate of survival probability at a given time ",
      "is the product of these conditional probabilities up until that given time."
    ),
    mchar = 132
  ) |>
  set_cell("footer_left", "Program: f_surv_gridify.R, Source(s): ADTTE") |>
  set_cell("footer_right", paste0("Page ", 1, " of ", 1))
print(fig)


Integrating tflmetaR for Metadata Management

The tflmetaR package enables separation of metadata and code by storing titles, footnotes, and headers in external spreadsheets. This approach:

Loading Metadata Files

We read the titles and header information from an Excel spreadsheet.

## --- tflmetaR related code ----

# read headers, titles, and footnotes excel spreadsheet

file_name <- system.file("extdata", "st_titles.xls", package = "tflmetaR")
title_file <- tflmetaR::read_tfile(filename = file_name, sheetname = "Sheet1")
header_file <- tflmetaR::read_tfile(filename = file_name, sheetname = "Headr", validate = FALSE)

fig_number <- "Figure F01"

Extracting Annotations

Using tflmetaR accessor functions, we retrieve titles, footnotes, and header content for the specific figure.

ulheader <- tflmetaR::get_ulheader(header_file)[1, ]
urheader <- tflmetaR::get_urheader(header_file)[1, ]
titles <- tflmetaR::get_title(title_file, tnumber = fig_number)
footnotes <- tflmetaR::get_footnote(title_file, tnumber = fig_number, add_footr_tstamp = FALSE)
pgmname <- tflmetaR::get_pgmname(title_file, tnumber = fig_number)
source <- tflmetaR::get_source(title_file, tnumber = fig_number)

# convert footnote list to string
footnote_str <- paste(unlist(footnotes), collapse = "\n")

Creating the Final Annotated Figure

Now we combine gridify with tflmetaR-extracted metadata to create the final publication-ready figure. This approach ensures that any updates to titles or footnotes in the metadata spreadsheet are automatically reflected in the output.

## --- re-draw graph with pulled titles and footnotes ----
fig2 <- gridify(
  p1,
  layout = pharma_layout_base(
    margin = grid::unit(c(0.5, 1, 0.25, 1), "inches"),
    global_gpar = grid::gpar(fontfamily = "Courier")
  )
) |>
  set_cell("header_left_1", ulheader[[1]]) |>
  set_cell("header_left_2", ulheader[[2]]) |>
  set_cell("header_left_3", ulheader[[3]]) |>
  set_cell("header_right_1", urheader[[1]]) |>
  set_cell("header_right_2", urheader[[2]]) |>
  set_cell("header_right_3", urheader[[3]]) |>
  set_cell("output_num", titles[[1]]) |>
  set_cell("title_1", "") |>
  set_cell("title_2", titles[[2]]) |>
  set_cell("title_3", titles[[3]]) |>
  set_cell("note", footnote_str, mch = 120) |>
  set_cell("footer_left", paste0(
    "Program: ", pgmname, ",  ",
    "Source(s): ", source
  )) |>
  set_cell("footer_right", sprintf("Page %d of %d", 1, 1))

You can now preview the annotated figure or export the result to a PDF file using gridify::export_to(fig2, "f_surv_tflmetar.pdf").

print(fig2)


Summary

This vignette demonstrated a complete workflow for creating annotated Kaplan-Meier survival plots:

  1. Data preparation: Loading and transforming ADTTE data following CDISC standards
  2. Survival analysis: Fitting Kaplan-Meier models using the survival package
  3. Visualization: Creating publication-quality plots with survminer
  4. Annotation: Adding professional headers, titles, and footnotes using gridify
  5. Metadata integration: Leveraging tflmetaR to externalize annotations for better maintainability

By combining these tools, organizations can produce regulatory-compliant figures while maintaining a clear separation between analysis code and presentation metadata.