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 encodePrinted, 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 figureGitHub 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 supplementThe 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.