Get started: from data to a publication-ready figure

survminer turns a survival fit into a figure you can put straight into a manuscript. This page is the three-minute tour: fit a curve, get a publication-ready figure with one argument, adjust the look, report the numbers a reviewer expects, and build on the plot when you need something bespoke.

library(survminer)
library(survival)

# Label the sex factor once, so every figure shows readable arm names
# (rather than "sex=1" / "sex=2").
lung$sex <- factor(lung$sex, labels = c("Male", "Female"))

Every example uses the lung data set (NCCTG lung cancer) from the survival package, so you can paste and run any chunk as-is.

Three lines to a survival curve

Fit a Kaplan-Meier curve with survival::survfit(), then draw it with ggsurvplot(). That is the whole minimal workflow:

fit <- survfit(Surv(time, status) ~ sex, data = lung)
ggsurvplot(fit, data = lung)

ggsurvplot() already gives you a coloured curve per group, a censoring legend, and clean axis labels. From here every element – confidence bands, a p-value, a number-at-risk table – is one more argument.

One argument to a publication-ready figure

Instead of setting a dozen arguments by hand, pass preset = "publication". It bundles the evidence a survival figure is expected to carry – confidence bands, the log-rank p-value, median-survival crosshairs, a number-at-risk table, a colourblind-friendly palette, and a percent y-axis:

ggsurvplot(fit, data = lung, preset = "publication")

Everything in that figure maps to an argument you could have set yourself:

Visible element Argument the preset set
Shaded confidence bands conf.int = TRUE
p-value, labelled “Log-rank” pval = TRUE, pval.method = TRUE
Dashed median-survival crosshairs surv.median.line = "hv"
Number-at-risk table below risk.table = TRUE
Colourblind-friendly colours palette = "jco"
Survival as a percentage surv.scale = "percent"

The preset is a starting point, not a straitjacket – the next section shows how your own arguments override it.

Your arguments always win

A preset only fills in the arguments you did not supply. Anything you pass explicitly wins, so you can take the publication bundle and change just one thing – here, the palette:

ggsurvplot(
  fit, data = lung,
  preset  = "publication",
  palette = c("#E7B800", "#2E9FDF")   # your palette overrides the preset's "jco"
)

The default is preset = "none", which changes nothing – existing code keeps its exact output.

Choosing a look

There are four presets. "publication" is the full evidence panel above; "classic" is a greyscale-safe textbook look (one line type per group); "minimal" is an airy, stripped-back curve; "presentation" uses bold, large type for slides. Compare them on the same fit:

To compare them like-for-like we override two preset defaults so every panel is the same height – turning the risk table and the median lines off (another illustration of “your arguments win”) – and look only at the styling:

looks <- c("classic", "minimal", "presentation", "publication")
plots <- lapply(looks, function(p)
  ggsurvplot(fit, data = lung, preset = p, title = p,
             risk.table = FALSE, surv.median.line = "none"))
arrange_ggsurvplots(plots, ncol = 2, nrow = 2)

Pick "publication" or "classic" for a paper, "presentation" for a talk, "minimal" when the curve is one panel of a larger figure.

Reusable themes

A preset bundles the look and the evidence layers (bands, p-value, table); a theme restyles only the look. The looks above are also available as standalone ggplot2 themes you can apply to any survminer plot (or any ggplot) through ggtheme, without a preset: theme_surv_classic(), theme_surv_minimal(), and theme_surv_bold().

ggsurvplot(
  fit, data = lung,
  conf.int = TRUE,
  palette  = "jco",
  ggtheme  = theme_surv_bold()
)

The numbers every KM paper reports

Two numbers belong in the caption of almost every Kaplan-Meier figure: the median survival per group, and the median follow-up (the typical observation time – estimated by the reverse Kaplan-Meier method, which swaps the event and censoring roles).

# Median survival per group (with 95% CI)
surv_median(fit)
#>       strata median lower upper
#> 1   sex=Male    270   212   310
#> 2 sex=Female    426   348   550

# Median follow-up per group (reverse Kaplan-Meier)
surv_median_followup(fit, data = lung)
#>       strata median lower upper
#> 1   sex=Male    840   806    NA
#> 2 sex=Female    529   376    NA

Both return tidy data frames, so you can drop them into a table or paste the values into a figure caption.

Building on the curve

ggsurvplot() returns a compound object (the curve in $plot, the risk table in $table, and more), which is ideal for a finished figure but is not a plain ggplot you can freely add layers to. When you want to compose your own annotations, ask for a bare ggplot with output = "ggplot":

The bare curve does not carry the compound object’s risk table or the preset styling – you add exactly the layers you want:

p <- ggsurvplot(fit, data = lung, palette = "jco", output = "ggplot")

# it is a genuine ggplot, so ordinary ggplot2 verbs work
p +
  ggplot2::geom_hline(yintercept = 0.5, linetype = "dashed", colour = "grey50") +
  ggplot2::annotate("text", x = 0, y = 0.53, hjust = 0, size = 3,
                    label = "median survival") +
  ggplot2::labs(title = "Add any layer you like")

Because it is a real ggplot, + geom_*, facet_wrap(), and ggsave() all behave normally – the bare curve is the surface to build RMST shading, a competing-risks overlay, or a parametric fit on (see the other articles).

Saving your figure

ggsave() works directly on the object ggsurvplot() returns – the curve and its risk table are saved together, correctly aligned:

fig <- ggsurvplot(fit, data = lung, preset = "publication")
ggplot2::ggsave("survival.png", fig, width = 7, height = 6.5, dpi = 300)

For a journal, a width of 7 inches at 300 dpi is a safe starting point.

Where to next

  • Absolute effect measures beyond the p-value – restricted mean survival time (RMST), milestone survival, and landmark analysis.
  • Interrogating a Cox model – forest plots (ggforest(), ggforest_models(), ggforest_subgroup()) and proportional-hazards diagnostics.
  • Competing risks – cumulative incidence done right, with Gray’s test and the naive-Kaplan-Meier contrast.
  • Customization recipes – compose on the objects survminer returns to solve one-off requests.