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.
Fit a Kaplan-Meier curve with survival::survfit(), then
draw it with ggsurvplot(). That is the whole minimal
workflow:
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.
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:
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.
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.
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.
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().
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 NABoth return tidy data frames, so you can drop them into a table or paste the values into a figure caption.
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).
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.
ggforest(), ggforest_models(),
ggforest_subgroup()) and proportional-hazards
diagnostics.