When visualizing competing risk data in R using the tidycmprsk and ggsurvfit packages, aligning the cumulative incidence plot with its accompanying risk table is essential for publication-ready figures. A common hurdle researchers encounter is attempting to modify the x-axis scale or interval breaks using scale_x_continuous(), only to find that the risk table fails to match the new plot breaks.

In this guide, we will explore why this misalignment happens and provide the exact order of operations needed to keep your cumulative incidence curves and risk tables perfectly synchronized.

The Root Cause: Order of Evaluation in ggsurvfit

In ggsurvfit (and functions like ggcuminc()), the risk table is not a passive element created at render time. When you call add_risktable(), it computes the risk set numbers and statistics for specific time points based on the plot's current x-axis breaks.

If you call scale_x_continuous() or scale_ggsurvfit() after add_risktable(), the risk table has already calculated its values based on default intervals, leaving your table out of sync with the newly defined axis scale. To fix this, you must define the scale breaks before adding the risk table, or explicitly declare the evaluation time points inside add_risktable().

Solution 1: Define the X-Axis Scale Before add_risktable()

The cleanest approach is to set your axis breaks and limits using scale_x_continuous() or scale_ggsurvfit() right after ggcuminc(), and prior to chaining add_risktable().

library(survival)
library(ggsurvfit)
library(tidycmprsk)

# Prepare sample dataset
df <- data.frame(ovarian)
df$fustat2 <- df$fustat
df[df$futime < 300, ]$fustat2 <- 2
df$futime <- df$futime / 12

df$rx <- factor(df$rx, c(1, 2), labels = c("Rx1", "Rx2"))
df$fustat2f <- factor(df$fustat2, 0:2, labels = c("censor", "event", "ce event"))

# Build the cumulative incidence model
fit <- cuminc(Surv(futime, fustat2f) ~ rx, data = df)

# Plot with synced breaks
cPlot <- fit %>%
  ggcuminc(outcome = "event") +
  add_censor_mark(size = 3) +
  # 1. Apply the scale BEFORE adding the risktable
  scale_x_continuous(
    limits = c(0, 110),
    breaks = seq(0, 110, by = 12),
    expand = c(0.02, 0)
  ) +
  # 2. add_risktable will now automatically inherit seq(0, 110, by = 12)
  add_risktable(
    combine_groups = FALSE,
    risktable_stats = c("n.risk", "cum.event", "{round((estimate) * 100)}%"),
    stats_label = c("At Risk", "Cumul. Events", "Cumul. Inc."),
    theme = theme(
      axis.line = element_line(colour = "black", linetype = "solid"),
      panel.background = element_blank(),
      axis.ticks.y = element_line(linewidth = 1)
    )
  )

cPlot

Solution 2: Explicitly Set Break Times in add_risktable()

If you prefer fine-grained control, or if you need the risk table to display a subset of time points independent of the primary x-axis tick marks, you can pass a vector directly into the times argument of add_risktable().

# Define desired break intervals
time_breaks <- seq(0, 108, by = 12)

fit %>%
  ggcuminc(outcome = "event") +
  add_censor_mark(size = 3) +
  scale_x_continuous(breaks = time_breaks, limits = c(0, 110)) +
  add_risktable(
    times = time_breaks,  # Explicitly specify time intervals
    combine_groups = FALSE,
    risktable_stats = c("n.risk", "cum.event", "{round((estimate) * 100)}%"),
    stats_label = c("At Risk", "Cumul. Events", "Cumul. Inc.")
  )

Tips for Custom Styling and Themes

When customizing survival and cumulative incidence plots for publication, keep the following best practices in mind:

  • Use scale_ggsurvfit(): The built-in scale_ggsurvfit() wrapper handles expand settings natively so curves start cleanly at the origin without unnecessary blank margins.
  • Coordinate Limits: Avoid using coord_cartesian(xlim = ...) when working with risk tables, as this clips the visual space without recalculating table statistics. Stick to setting limits within scale_x_continuous().
  • Risk Table Theme: You can pass custom ggplot2::theme() options directly into the theme argument of add_risktable() to match fonts, grid lines, and axis tick styling seamlessly.

Summary

The secret to syncing x-axis breaks between ggcuminc() and add_risktable() comes down to sequence. Always adjust your scales using scale_x_continuous() before calling add_risktable(), or use the times parameter to explicitly dictate the calculation points.