Presentation quality charts built on 'ggplot2' that the package itself does not provide. The bar chart race interpolates values on a uniform time grid, ranks every frame on its own values and eases bars into new positions, so a reordering field stays readable. Frames are ordinary 'ggplot2' objects laid out on a fixed pixel grid, which keeps the axis and label column from drifting between them, and are encoded to GIF or MP4. Circular images, such as the bundled country flags, can be placed at the end of each bar. Causal diagrams are drawn as directed acyclic graphs whose nodes and arrows each carry a rationale and references, shown on hover and opened with clickable links on click, through 'ggiraph'; the same diagram is also available as a static 'ggplot2' object. Network plots for network meta-analysis are drawn from arm level data, with the baseline characteristics and outcomes of every arm shown side by side on click. Forest plots for 'metafor' and 'meta' fits carry each study's record and risk of bias traffic lights, and replay a cumulative meta-analysis as an animation; funnel plots shade where each study would be significant and carry the pooled estimate without it, trim and fill and the tests for small-study effects; league tables for 'netmeta' fits show the direct and indirect evidence behind every estimate. Kaplan-Meier plots read survival and the hazard ratio at any time under the pointer, beside a risk table and proportional hazards tests, and swimmer plots give each patient a lane with their responses, progression and death. Nomograms of regression models, from linear and generalized linear models to mixed, Cox, parametric survival, ordinal and multinomial models, have a handle per predictor and compute each prediction with its confidence interval in the page. Choropleth maps of the world or of any 'sf' map step or play through the years, with several measures side by side for the same year. Causal diagrams can also show which paths between an exposure and an outcome an adjustment set leaves open, by the backdoor criterion; Kaplan-Meier plots give the restricted mean survival time up to a horizon the reader can move; league tables and network plots show where each network estimate's evidence comes from; and swimmer plots can carry a waterfall of best change and each patient's course beside the lanes. Four explorers put a threshold or an assumption in the reader's hands: the cutoff of a diagnostic test, with what it means for 1,000 people at any prevalence; the strength of unmeasured confounding, with E-values; the choices of a multiverse of analyses; and the threshold that defines a responder. Plots after CINeMA judge the confidence in each estimate of a network meta-analysis in six domains, with every judgment's reason and source, and show what lies behind them: each study's contribution, the estimates against a movable range of little difference, direct against indirect evidence, and a league table and a network marked with the judgments.
Presentation quality charts built on ggplot2 that the package itself does not provide. There are fifteen so far:
All fifteen are drawn as ordinary ggplot objects. Nothing is hidden behind a
separate rendering engine, so a frame or a diagram can be inspected, modified
or saved on its own.

# install.packages("remotes")
remotes::install_github("choxos/ggextreme")
Writing output requires an encoder: gifski or magick for GIF, av
or an ffmpeg binary for MP4. Images on the bars require magick.
ggrace() takes long data with one row per entity per time point, and three
bare column names for the value, the label and the time.
library(ggextreme)
race <- ggrace(
clefts_qci,
value = qci,
name = country,
time = year,
top_n = 15,
duration = 15,
title = "Quality of care for orofacial clefts",
caption = "Source: Sofi-Mahmudi et al. 2025, PLOS ONE 20(1): e0317267"
)
race # plays in the page, as an interactive widget
graph_save(race, "race.html") # the same, as a single web page
race_frame(race, 200) # one frame, as a ggplot
animate_race(race, "race.mp4") # draw every frame and encode
Printed, the race plays like the package's other interactive graphs: the card's round button plays and pauses it, the timeline seeks, and hovering over a bar shows its value and rank, while a click follows it through the race.
time may be numeric or a Date. Each entity and time pair must appear
once; a repeat is an error rather than a silent average. The encoder is
chosen from the file extension, and frames are drawn across cores by default.
Selected arguments:
| argument | effect |
|---|---|
top_n |
number of bars visible at once |
duration, fps, end_pause |
length in seconds, frame rate, hold on the final frame |
swap |
seconds a bar takes to move into a new rank |
group |
color bars by category and draw a legend |
palette, breaks |
bar colors; gridline positions |
label_value, label_time |
formatters for the bar numbers and the time label |
images |
pictures placed at the end of the bars |
timeline, play_button, card |
optional chrome around the plot |
width, res |
output size; the layout scales with width |
Passing a group column colors the bars by category rather than
individually and draws a legend above the axis. Each entity must belong to
exactly one category; a factor keeps the legend in the order of its levels.
ggrace(
clefts_qci, qci, country, year,
group = region,
legend_title = "Region",
top_n = 15
)
The card grows to make room for the legend, wrapping onto more rows when the
categories do not fit across it. legend = FALSE keeps the coloring and
drops the legend.
images takes image file paths named by entity. Pictures are cropped to a
circle and right aligned just inside the end of each bar; entities without an
image simply get none. A circular flag for every ISO 3166-1 country, plus
Kurdistan, is bundled, so country races need no extra files.
key <- unique(clefts_qci[c("country", "iso")])
flags <- setNames(race_flags(key$iso), key$country)
ggrace(clefts_qci, qci, country, year, top_n = 15, images = flags)
Any image works, not only flags. Pass paths to logos, portraits or crests in the same way.
Three choices govern how the animation reads. They are set out in full in
vignette("how-the-animation-works").
swap seconds.
Bars therefore rest in place and trade positions in one short move rather
than drifting for a whole time step.ggcausal() draws a DAG from two data frames: edges, with one row per
arrow, and nodes, with one row per variable. A rationale and
references column on either one explains why that node or arrow is in the
diagram. Hovering shows the rationale; clicking opens a panel with the full
text and clickable references, which also works on touch screens.
dag <- ggcausal(cleft_dag$edges, cleft_dag$nodes, legend_title = "Role")
dag # interactive widget
graph_save(dag, "dag.html") # a single file for a supplement
graph_save(dag, "dag.png") # a static figure
GitHub cannot run the widget, so the image above is static. The interactive version is on the package website.
Nodes are colored by role, with fixed colors for exposures, outcomes,
confounders, mediators, colliders, instruments and unobserved variables.
The layout is layered so that every arrow points the same way, and an arrow
that skips a layer bends around the boxes in between; x and y columns
place the boxes by hand instead. Any other column in either data frame
appears as a labeled field. The widget embeds a web copy of Lato and works in
R Markdown, Quarto, 'pkgdown' and 'shiny'.
With paths = TRUE, the diagram shows which paths between the exposure
and the outcome are open or blocked: click a variable to adjust for it, and
a panel under the diagram says whether the set is sufficient by the backdoor
criterion, why each path is open or blocked, and which minimal sets would
be.
ggcausal(cleft_dag$edges, cleft_dag$nodes, paths = TRUE, adjust = "ses")
ggnma() draws the network of a network meta-analysis from arm level data,
one row per study arm. Nodes are treatments and lines join treatments
compared directly in at least one study; node area follows the number of
participants, line width the number of studies, and a shaded polygon joins
the treatments of each multi-arm study. Hovering over a node, line or
polygon shows its arms side by side, in the manner of a trial's baseline
table, with the columns chosen in hover; clicking opens the full table
with every other column of the data as a row.
net <- ggnma(psoriasis_nma, study, treatment, n = n, group = class,
legend_title = "Class")
net # interactive widget
graph_save(net, "network.html") # a single file for a supplement
The interactive version
is on the package website. Nodes sit on a circle or wherever positions
places them. Rows of the arm tables are named from each column's label
attribute, text that is the same across a study, such as a
reference, is listed once per study, and DOIs and URLs are linked.
With contributions, a netmeta fit on the same network, the widget gains a
menu of comparisons; picking one widens each line by the share of that
network estimate flowing through it.
ggmeta() draws the forest plot of a fitted meta-analysis, a metafor
rma() fit or a meta object. Hovering over a study shows its effect, weight
and chosen columns, and clicking it opens every column of its record. Risk
of bias judgments, from RoB 2, RoB 1 or ROBINS-I, are drawn as traffic
lights beside each study.
ggmeta(fit,
columns = c("P2Y12 inhibitor" = "p2y12", Aspirin = "aspirin"),
rob = c(R = "rob.R", D = "rob.D", Mi = "rob.Mi", Me = "rob.Me",
S = "rob.S", Overall = "rob.overall"),
favors = c("Favors P2Y12 inhibitor", "Favors aspirin"))
cumulative = TRUE shows the pooled estimate after each study, and
animate_meta() replays it as a GIF or MP4, each trial fading in as the
pooled diamond eases to its new value:

ggleague() draws every estimate of a netmeta fit as a grid, network
estimates below the diagonal and direct estimates above it, following
netmeta::netleague(), with a P-score ranking beside it. Hovering over a
cell shows the network, direct and indirect estimates and the share that
comes from direct trials; clicking it opens the direct trials arm by arm.
ggleague(nma, psoriasis_nma, study, treatment, small_values = "undesirable")
contributions = TRUE adds where each network estimate comes from, by
netmeta::netcontrib(): hovering over an estimate outlines the direct
comparisons it draws on, with their shares.
Five plots follow CINeMA (Nikolakopoulou et al. 2020; Papakonstantinou et
al. 2020) in judging how far each estimate of a network meta-analysis can
be trusted. cinema_judge() applies its published rules to every
comparison in six domains, within-study bias, reporting bias, indirectness,
imprecision, heterogeneity and incoherence, with each reason in words and
whether a rule computed it or you gave it; judgments made elsewhere, such as
the CINeMA web application's report, can be given instead.
j <- cinema_judge(nma, rob = rob, indirectness = indirectness,
reporting = data.frame(judgment = "undetected"),
threshold = 1.25, small_values = "undesirable")
cinema_league(j) # six marks per estimate, never added into a score
cinema_contribution(j) # which studies each estimate rests on
cinema_clinical(j) # estimates against a movable range of little difference
cinema_incoherence(j) # direct, indirect and network estimates side by side
cinema_network(psoriasis_nma, study, treatment, n = n, rob = rob)
The study judgments in the example are illustrative, not published assessments.
ggfunnel() draws the funnel plot of a metafor or meta fit, shaded where a
study would be significant against no effect, so a gap where studies would
not be significant points to publication bias rather than heterogeneity.
Hovering over a study shows its effect, weight and risk of bias; clicking it
gives the pooled estimate without it. With trim_fill = TRUE the imputed
studies and the adjusted estimate are added behind a switch, and a collapsed
section under the plot gives Egger's and Begg's tests.
ggfunnel(fit, hover = c("alloc", "ablat"), trim_fill = TRUE)
ggkm() draws Kaplan-Meier curves by group from a Surv(time, status) ~ group formula. Hovering anywhere along the time axis shows each group's
survival with its confidence interval, the number at risk and the events so
far, and the hazard ratio against the reference group at that time, from the
smoothed Schoenfeld residuals or a time interaction model, while the matching
column of the risk table lights up. With ph_tests = TRUE, a collapsed
section under the plot gives the Cox hazard ratios, the log-rank test, the
Grambsch and Therneau test and the group by time and group by log time
interactions.
ggkm(Surv(years, status) ~ arm, data = colon, ph_tests = TRUE,
xlab = "Years since randomization")
rmst = 5 adds the restricted mean survival time up to five years, with
each arm's mean and its difference from the reference, and a slider that
moves the horizon while the prespecified one stays marked.
animate_km() draws the curves over follow-up as a GIF or MP4:

ggswimmer() gives every patient a lane: the time on treatment or on study,
with responses, progression, relapse and death marked along it and an arrow
for patients still ongoing. Hovering over a lane shows the patient's record
and fades the rest, clicking it lists their events in order, and buttons
under the plot reorder the lanes by duration, arm or best response.
ggswimmer(aml, id, futime / 30.44, events = events, group = arm,
ongoing = death == 0, ongoing_label = "Alive at last follow-up",
xlab = "Months since randomization")
waterfall adds each patient's best change from baseline beside their lane,
and trajectories their change over time under the lanes, with the response
and progression thresholds marked; hovering over a patient in any panel
lights them in all three.
ggresponder() draws, for two arms, the share of patients who improved by
at least each amount, with the prespecified threshold marked, beside the
difference in responders at every threshold, and a table of responders,
their difference, the number needed to treat and the mean difference. A
slider moves the threshold.
ggresponder(change ~ arm, pain, threshold = 2, higher_is_better = FALSE)
ggdiagnostic() shows what a cutoff on a continuous test means: the
marker's distributions, the ROC curve and the predictive values across
prevalence, above a grid of 1,000 people found, missed, falsely alarmed or
cleared, and a table of every measure with its interval. Drag the cutoff,
or set the prevalence of the population the test is for.
pima <- rbind(MASS::Pima.tr, MASS::Pima.te)
ggdiagnostic(type ~ glu, pima, cutoff = 126, prevalence = 0.1,
labels = c("No diabetes", "Diabetes"))
ggsensitivity() shades every pair of strengths an unmeasured confounder
could have by what would survive it, marks the E-values for the estimate
and its confidence limit, and compares measured covariates as benchmarks.
Click the surface to choose a confounder.
ggsensitivity(1.8, 1.4, 2.31, important = 1.25,
benchmarks = data.frame(label = c("Age", "Smoking"),
exposure = c(1.6, 2.3), outcome = c(1.9, 1.5)))
ggmultiverse() draws every analysis of one question, one row of the data
each, as a specification curve above a grid of the choices behind it, with
the median estimate for each choice. Drag across the curve, or click a
choice, to see what the analyses in view share.
ggmultiverse(specs, or, lo, hi,
decisions = c("outcome", "adjustment", "model", "missing", "sample"),
primary = outcome == "Primary definition" & adjustment == "Standard",
ylab = "Odds ratio")
ggnomogram() draws the nomogram of a fitted model and gives every
predictor a handle: drag it, click a category or use the arrow keys, and the
points, the total and the prediction with its 95% confidence interval follow.
It reads linear, generalized linear, mixed (lme4, nlme, glmmTMB), Cox,
parametric survival, ordinal and multinomial models, and models from rms and
mgcv. Splines, polynomials and interactions work, because the points come
from the model's design matrix, and every class is checked against its own
predict().
fit <- glm(low ~ splines::ns(age, 3) + lwt + race + smoke * ht,
family = binomial, data = bw)
ggnomogram(fit, outcome = "Risk of low birth weight")
ggchoropleth() colors every country by a measure, one map per measure side
by side, with a slider and a play button under them that step through the
years. All the maps show the same year: hovering over a country outlines it
on every map and lists its value and rank on each measure, and clicking it
opens its whole series. Countries match by ISO code or by name, including the
forms the WHO and the Global Burden of Disease study use, and any 'sf' map
of polygons can replace the bundled world map.
qci <- clefts_qci_world
first <- qci$qci[qci$year == 1990][match(qci$iso3, qci$iso3[qci$year == 1990])]
qci$change <- qci$qci - first
ggchoropleth(qci, iso3, year,
values = c("Quality of Care Index" = "qci",
"Change since 1990" = "change"),
title = "Quality of care for orofacial clefts")
animate_choropleth() plays the years as a GIF or MP4:

Every interactive graph follows the page it sits on. On a dark 'pkgdown' or
'bslib' page, a dark Quarto theme or a saved page viewed in dark mode, the
background, text, lines and neutral fills take dark counterparts, colors that
carry meaning keep their hue, and the hover cards and panels follow, even when
the page switches theme while it is open. graph_widget(x, theme = "dark")
fixes the theme, and graph_save(x, "plot.png", theme = "dark") writes a
dark static copy.
clefts_qci gives the Quality of Care Index for orofacial clefts in fifteen
countries from 1990 to 2019. The index is a composite of four secondary
indices derived from Global Burden of Disease estimates, summarized by
principal component analysis and rescaled from 0 to 100.
Sofi-Mahmudi A, Shamsoddin E, Khademioore S, Khazaei Y, Vahdati A, Tovani-Palone MR (2025). Global, regional, and national survey on burden and Quality of Care Index (QCI) of orofacial clefts: Global burden of disease systematic analysis 1990-2019. PLOS ONE 20(1): e0317267. https://doi.org/10.1371/journal.pone.0317267
clefts_qci_world holds the full country panel of the same analysis: 195
countries and territories, named as the Global Burden of Disease study names
them and with their ISO 3166-1 alpha-3 codes.
psoriasis_nma gives arm level baseline characteristics and PASI 75
response for five randomized trials in plaque psoriasis (CLEAR, ERASURE,
FEATURE, FIXTURE and JUNCTURE), as compiled by Phillippo (2019) and
distributed with the 'multinma' package. They were analyzed in Phillippo et
al. (2020), Journal of the Royal Statistical Society Series A 183(3):
1189-1210, https://doi.org/10.1111/rssa.12579.
cleft_dag is a small illustrative causal diagram for maternal smoking and
orofacial clefts. Its rationales were written for the package as a teaching
example, and every reference it cites was checked against PubMed.
MIT. The package bundles the Lato typeface, and a web subset of it for the
interactive graphs, under the SIL Open Font License (inst/fonts/OFL.txt),
and country flag artwork from the flag-icons project under the MIT License,
with two exceptions noted in inst/extdata/flags/SOURCE.txt. The world map
is simplified from Natural Earth's 1:50m countries, which are in the public
domain.