Publication-ready figures for cancer bioinformatics, in the style the lab submits with. Kaplan-Meier curves with hazard ratios, ROC and time-dependent ROC curves, risk-score distributions, dimensionality-reduction projections, boxplots and correlation plots, lasso coefficient paths, enrichment plots, and immune-infiltration radar charts.
Every function returns a ggplot object, so figures stay editable with the
usual ggplot2 grammar.
library(gfplot)
p <- plot_KMCurve(clin, labels)
gfplot_save(p, "figure1.png", width = 7, height = 5)
Two problems come up on every manuscript. Figures drift apart, because each one is tuned by hand until nothing matches. And the code that produced them lives in one-off scripts, so the next paper starts from scratch.
gfplot fixes the second problem by making those figures functions, and the
first by being opinionated about presentation:
ggplot2 theme to
cowplot::theme_cowplot(), so every figure in the session starts from the
same look.get_color() returns colour-blind-friendly palettes
in the nature, jco, lancet, jama, and jama_classic styles.This is a deliberate choice, not a side effect. Attaching gfplot is a
statement that this is the intended house style, and the package applies it
consistently. See Figure style to change any part of it.
Prebuilt binaries for Linux, macOS, and Windows come from r-universe, so no compiler is needed:
install.packages(
"gfplot",
repos = c(gflab = "https://gflab.r-universe.dev", CRAN = "https://cloud.r-project.org")
)
Installing from source through GitHub also works:
# install.packages("remotes")
remotes::install_github("gflab/gfplot")
R 4.1 or later. The historical path gaofeng21cn/gfplot redirects here, so
older scripts keep working.
Survival curves with the log-rank p-value, the hazard ratio from a univariable Cox model, and a number-at-risk table.

library(gfplot)
library(survival)
data(lung)
keep <- complete.cases(lung[, c("time", "status", "sex")])
plot_KMCurve(
Surv(lung$time[keep], lung$status[keep] == 2),
factor(lung$sex[keep], labels = c("Male", "Female")),
xlab = "Follow up (days)",
title = "Overall survival"
)
One curve per marker, with the area under the curve and its confidence
interval in the legend. force05 = TRUE flips markers that rank the data
backwards so every curve rises.

plot_ROC(
cbind(Signature = signature_score, Stage = stage_score),
labels,
title = "Discrimination"
)
Patients ordered by score, coloured by outcome, which is the standard companion panel to a survival curve.

plot_RiskScore(risk_score, event, legend.position = c(0.15, 0.85))
Every function returns a ggplot object.
| Area | Functions |
|---|---|
| Survival |
plot_KMCurve(), generate_time_event()
|
| Discrimination |
plot_ROC(), plot_TimeROC(), plot_MulROC()
|
| Prediction performance |
plot_pr_curve(), plot_calibration(), plot_decision_curve()
|
| Clinical utility | plot_decision_curve() |
| Time-to-event | plot_cumulative_incidence() |
| Effect estimates |
plot_forest(), plot_lasso()
|
| Sample-level figures |
plot_RiskScore(), plot_Boxplot(), plot_barplot(), plot_cor(), plot_violin()
|
| Projections |
plot_embedding(), plot_PCA(), plot_UMAP(), plot_tsne()
|
| Matrix pattern |
plot_heatmap(), plot_confusion()
|
| Genomic and omics |
plot_volcano(), plot_GO()
|
| Trial response | plot_waterfall() |
| Enrichment and immune |
viewGSEA(), ggGSEA(), plot_immune()
|
| Style and output |
get_color(), gfplot_save(), gfplot_font_setup()
|
gfplot_families() lists every figure the package can draw, with its
category and an example call.
Notable arguments:
| Function | Argument | Purpose |
|---|---|---|
plot_KMCurve() |
risk.table |
Draw the number-at-risk table |
plot_KMCurve() |
limit |
Censor follow-up at a time point |
plot_KMCurve() |
annot |
Add your own annotation lines |
plot_ROC() |
force05 |
Flip markers that rank backwards |
plot_ROC() |
percent.style |
Label axes as percentages |
plot_UMAP(), plot_PCA()
|
palette |
Choose the group colours |
| every plot | font |
Override the typeface for one figure |
plot_forest() draws hazard ratios, odds ratios, or mean differences with
their confidence intervals and the numbers alongside. It reads a clinstats
regression table directly, so a result goes from model to figure without
reshaping:
library(clinstats)
table <- cox_table(
clin_crc,
time = "rfs.delay", event = "rfs.event",
factors = c("sex", "age", "tnm.stage"), multivariable = "none"
)
plot_forest(
table,
term = "term", estimate = "hr_univariable",
lower = "univ_ci_lower", upper = "univ_ci_upper", p = "univ_p",
log_scale = TRUE, title = "Overall survival (univariable)"
)
Ratios use a log axis, where a halving and a doubling are the same distance.
Pass log_scale = FALSE for an effect measured as a difference, which may
legitimately cross zero.
A figure can also be described as a value and drawn later, which is what a pipeline or an agent needs when the numbers and the chart are decided in different places:
spec <- gfplot_spec(
"forest",
data = table, term = "term",
estimate = "hr_univariable",
lower = "univ_ci_lower", upper = "univ_ci_upper",
p = "univ_p", log_scale = TRUE
)
gfplot_save(gfplot_render(spec), "figure2.png", width = 7, height = 4)
gfplot_families() lists every figure the package can draw and how to call
it.
plot_ROC() answers whether a model can rank patients. Three companion
figures answer what follows, which is usually the harder question:
# Precision against recall, for a rare positive class
plot_pr_curve(scores, outcome)
# Is the model honest about the probability it predicts?
plot_calibration(probability, outcome, bins = 10)
# Does acting on it do more good than harm?
plot_decision_curve(probability, outcome)
plot_calibration() bins the predictions into quantiles and draws binomial
intervals, so each point rests on a similar number of observations.
plot_decision_curve() draws the net benefit against treating everyone and
treating no one.
When a patient can experience one of several mutually exclusive events, a
Kaplan-Meier estimate of a single event is biased upward: patients who had a
competing event are still counted as being at risk of the event of interest.
plot_cumulative_incidence() uses the cumulative incidence function instead.
# status: 0 censored, 1 event of interest, 2 competing event
plot_cumulative_incidence(time, status, group = arm)
plot_volcano(effect, p_value, label) # differential analysis
plot_waterfall(response) # ranked patient response
plot_heatmap(matrix, diverging = TRUE) # correlation or signature matrix
plot_confusion(predicted, actual, positive = 1)# classification counts
plot_violin(value, group) # distributions with observations
plot_embedding(data, groups, method = "tsne") # PCA, t-SNE, or UMAP
gfplot_save() writes a file through a graphics device that can render the
figure font, chosen from the file extension. .png, .tiff, .jpg, and
.pdf are supported.
gfplot_save(p, "figure1.png", width = 7, height = 5, dpi = 300)
gfplot_save(p, "figure1.pdf", width = 7, height = 5)
Use a journal size preset to produce the figure at the width it will be printed, rather than scaling it afterwards:
gfplot_save(p, "figure2.png", size = "single") # 85 mm, one column
gfplot_save(p, "figure2.png", size = "onehalf") # 114 mm, 1.5 columns
gfplot_save(p, "figure2.png", size = "double") # 170 mm, full width
gfplot_save(p, "figure2.png", size = "slide") # 16:9 presentation
An explicit width or height overrides the preset for that dimension.
Add provenance = TRUE to write a .provenance.json beside the figure
recording what produced it:
gfplot_save(p, "figure2.png", size = "double", provenance = TRUE)
{
"schema": "gfplot.provenance.v1",
"figure": { "kind": "spec", "family": "forest" },
"output": { "format": "png", "width_in": 6.69, "sha256": "…" },
"environment": { "gfplot": "0.6.0", "r": "R version 4.6.0" }
}
That record is what lets a figure in a manuscript be traced back to the code and versions that drew it.
Figures are drawn when they are printed, so a plot object is only turned into
a file by gfplot_save(), print(), or ggsave().
Figures use Arial, which is what journals ask for. Most output needs no
preparation: the raster devices in ragg and the
quartz device on macOS resolve system fonts directly.
The base pdf() device keeps its own font table and is the one route that
needs help. gfplot_font_setup() reports the state of every route and
registers Arial with pdf() through
extrafont when it is installed:
gfplot_font_setup()
#> Figure font "Arial" can be rendered by:
#> • ragg raster devices (PNG, TIFF, JPEG): yes
#> • quartz (macOS PDF and screen): yes
#> • base pdf(): no
On macOS, gfplot_save(p, "figure.pdf") uses quartz and needs none of
this; the file embeds the real font. Every plotting function also takes
font, so a single figure can use a different family.
The house style is defined once, in three functions, so every figure in a manuscript matches without per-figure tuning.
| Function | What it sets |
|---|---|
gfplot_theme() |
Type, grid, panel border, and legend placement |
gfplot_colors() |
Colours by role — "model_curve", "comparator_curve", "reference_line"
|
gfplot_legend() |
Legend wrapping and when to hide it |
Colours are addressed by role rather than by hex value, so a figure states what a colour is for:
gfplot_colors("primary")
#> primary
#> "#0B4F6C"
gfplot_colors(c("model_curve", "comparator_curve"))
#> model_curve comparator_curve
#> "#0B4F6C" "#2A9D8F"
Risk-group labels are matched by meaning, not spelling, so "Low Risk", "low-risk", and "low risk" all take the model-curve colour.
Retune the whole suite for a session by overriding a role; no figure code changes:
options(gfplot.palette = list(primary = "#123456"))
Each part can also be changed for a single figure:
# A larger base size for a figure that will be printed small
plot_KMCurve(clin, labels) + gfplot_theme(base_size = 14)
# One figure in a different typeface
plot_ROC(scores, labels, font = "Helvetica")
# One figure with a journal palette instead of the house ramp
plot_UMAP(embedding, groups, palette = "lancet")
# Keep an existing theme instead of adopting the baseline
options(gfplot.set_theme = FALSE)
library(gfplot)
Functions that build on specialised packages check for them at call time and
report what to install, so library(gfplot) always works.
| Function | Back end | Install |
|---|---|---|
plot_KMCurve() |
survminer | install.packages("survminer") |
plot_TimeROC() |
survivalROC | install.packages("survivalROC") |
plot_UMAP(), plot_embedding(method = "umap")
|
umap | install.packages("umap") |
plot_tsne() |
Rtsne | install.packages("Rtsne") |
plot_lasso() |
glmnet | install.packages("glmnet") |
plot_immune() |
ggradar | remotes::install_github("ricardo-bion/ggradar") |
viewGSEA(), ggGSEA()
|
DOSE, fgsea | BiocManager::install(c("DOSE", "fgsea")) |
plot_barplot(), plot_violin() significance |
ggpubr, ggsignif | install.packages(c("ggpubr", "ggsignif")) |
gfplot_save() raster output |
ragg | install.packages("ragg") |
gfplot_save(provenance = TRUE) hashing |
digest | install.packages("digest") |
plot_forest() from a clinstats table |
clinstats | remotes::install_github("gflab/clinstats") |
citation("gfplot")
See CITATION.cff for machine-readable metadata. Both v0.1.0 and v0.3.0
are tagged, so a specific state can be pinned.
gfplot was published first at gaofeng21cn/gfplot and later at
gflab/gfplot. The repository was deleted in August 2026. Other public
research code still installed it — LidocaineQ/PIANOS calls
devtools::install_github("gflab/gfplot") in its README and notebooks — so
those install commands became dead links.
The package was restored in September 2026 from a verified git bundle of the
full history. The recovered commit 89a60bdb carries the original 0.1.0
sources unchanged and is tagged v0.1.0; v0.2.0 and v0.3.0 add the
modernisation on top. The repository moved to the gflab organisation, which
is why the personal path redirects here and both install commands resolve to
the same commit.
Every exported function keeps its 0.1.0 name and arguments, so existing
calls continue to work without edits. If you are reading this because an
install failed in that window, the package is installable again and no change
to your code is required.
Bug reports and feature requests are welcome through GitHub Issues. Reference documentation for every function is on the package site.
Apache License 2.0. See LICENSE.md. Copyright 2026 FengGao Lab contributors.
Versions before 0.4.1 were released under the MIT licence; those tags keep
the terms they were published with.