A model diagnostic is rarely just a diagnostic once it leaves an R session. It becomes a figure in a report, and figures in reports want consistent titles, an obvious “this is a draft” stamp while a model is still in flux, and a predictable place to land on disk. Doing all of that by hand, plot-by-plot and model-by-model, gets old fast.
xpose.xtras adds a small, composable toolkit for this
last mile:
apply_default_labs() / set_default_labs()
fill in (or overwrite) plot titles, subtitles, captions and tags.add_watermark() / set_default_watermark()
stamp a plot with draft/preliminary/confidential-style text.ggsave_xp() saves a plot, applying the above first,
with a swappable save backend.plot.xpose_data() / set_default_plots()
generate a whole batch of diagnostic plots from one xpdb in
a single call.All four read from the same kind of layered defaults, resolved in the
same order every time: a session-wide R option, then a per-model
(xpdb) default, then whatever is passed directly to the
function call. set_xtras_options() and
get_xtras_option() round this out by giving the whole
xpose.xtras.* option family one place to be set and
inspected.
Once a default is actually configured, it applies itself.
print.xpose_plot() and ggsave_xp() both pick
up a configured default_labs/default_watermark
automatically, with nothing else to call. That behavior is controlled by
the xpose.xtras.auto_apply option, TRUE unless
changed; it’s a no-op until something is configured, so nothing changes
for existing code until a default is actually set. The functions below
are still the way to configure those defaults in the first place, apply
them explicitly on demand, or reach an xpdb-level default
(which, unlike the option, print() has no way to pick up on
its own; see the note under apply_default_labs()). The last
section, Automatic application,
covers the auto-apply behavior itself.
apply_default_labs() takes a
ggplot/xpose_plot object and fills in any of
title/subtitle/caption/tag
that aren’t already set, using (in increasing precedence) the
xpose.xtras.default_labs option, an xpdb-level
default, and anything passed directly.
The simplest use is a session-wide option, handy for something that should apply to every plot in a script or report, like a draft caption:
options(xpose.xtras.default_labs = list(caption = "DRAFT: do not distribute"))
p <- dv_vs_ipred(xpdb_ex_pk, quiet = TRUE)
p_labelled <- apply_default_labs(p)
p_labelled
#> `geom_smooth()` using formula = 'y ~ x'Because dv_vs_ipred() already sets its own caption, that
one wasn’t touched. apply_default_labs() only fills in
labels that are still empty. Passing overwrite = TRUE
replaces existing labels too, and the value can use the same
@keyword placeholders xpose::parse_title()
understands elsewhere in the package:
apply_default_labs(p, caption = "Run @run, @nobs observations", overwrite = TRUE)
#> `geom_smooth()` using formula = 'y ~ x'A single global caption is a blunt instrument once more than one
model is in play. set_default_labs() attaches label
defaults to a specific xpdb instead, and those take
precedence over the option. This is useful when different models in the
same report need different framing:
xpdb_draft <- xpdb_x %>%
set_default_labs(caption = "Model @run, interim, subject to change")
dv_vs_ipred(xpdb_draft, quiet = TRUE) %>%
apply_default_labs(xpdb = xpdb_draft, overwrite = TRUE)
#> `geom_smooth()` using formula = 'y ~ x'apply_default_labs() can’t reach back into the
xpdb a plot was built from on its own. xpose’s
plotting functions only carry a reduced summary forward onto the plot
object, not the full xpdb, so the xpdb
argument has to be supplied explicitly whenever the
xpdb-level tier (or @keyword resolution)
should be used. Whatever is passed directly to
apply_default_labs() still wins over both tiers:
dv_vs_ipred(xpdb_draft, quiet = TRUE) %>%
apply_default_labs(caption = "Final for submission", xpdb = xpdb_draft, overwrite = TRUE)
#> `geom_smooth()` using formula = 'y ~ x'add_watermark() overlays large, semi-transparent,
rotated text across a plot, the kind of stamp that makes it obvious at a
glance that a figure isn’t final yet.
label, colour, alpha,
size, angle and fontface are all
adjustable:
p %>%
add_watermark(label = "PRELIMINARY", colour = "firebrick", alpha = 0.15, angle = 20)
#> `geom_smooth()` using formula = 'y ~ x'The watermark is added as an ordinary ggplot2 layer, so
it survives faceting and pagination: one watermark per panel,
automatically:
dv_vs_ipred(xpdb_ex_pk, quiet = TRUE, facets = "SEX") %>%
add_watermark(label = "DRAFT")
#> `geom_smooth()` using formula = 'y ~ x'Because the watermark is drawn at a fixed rotation, it can visually
merge into a plot that already has a strong diagonal of its own, like
the line of unity on a dv_vs_pred() plot. The default 30
degree angle sits close to that line and gets lost among the points
around it:
pred_plot <- dv_vs_pred(xpdb_ex_pk, quiet = TRUE)
pred_plot %>%
add_watermark(alpha = 0.6)
#> `geom_smooth()` using formula = 'y ~ x'Crossing the other way keeps the watermark clearly legible instead,
without competing with the plot’s own diagonal. This is exactly the kind
of case angle (and
alpha/size/colour) exist for:
Like the label functions, watermark settings resolve from the
xpose.xtras.default_watermark option, then an
xpdb-level default set with
set_default_watermark(), then whatever is passed directly.
A whole project can default to, say, a red “CONFIDENTIAL” stamp without
repeating those arguments at every call site:
options(xpose.xtras.default_watermark = list(label = "CONFIDENTIAL", colour = "firebrick"))
p %>%
add_watermark()
#> `geom_smooth()` using formula = 'y ~ x'plot()Building a standard set of diagnostics for a model usually means
calling several plotting functions one after another, then doing it all
again for the next model. plot.xpose_data() (an S3 method
for the base plot() generic) runs a whole list of
plot-generating calls in one go and returns them as a flat, named
list.
Called with no plots argument, it runs this package’s
built-in default battery: dv_vs_ipred,
dv_vs_pred, res_vs_idv,
res_vs_pred, eta_distrib,
eta_grid, eta_vs_cov_grid, and
ind_plots_sample.
default_plots <- plot(xpdb_x, quiet = TRUE)
#> Using data from $prob no.1
#> Filtering data by EVID == 0
#> Using data from $prob no.1
#> Filtering data by EVID == 0
#> Using data from $prob no.1
#> Filtering data by EVID == 0
#> Using data from $prob no.1
#> Filtering data by EVID == 0
#> Using data from $prob no.1
#> Removing duplicated rows based on: ID
#> Tidying data by ID, SEX, MED1, MED2, DOSE ... and 23 more variables
#> Using data from $prob no.1
#> Removing duplicated rows based on: ID
#> Using data from $prob no.1
#> Removing duplicated rows based on: ID
#> Using data from $prob no.1
#> Filtering data by EVID == 0
#> Tidying data by ID, SEX, MED1, MED2, DOSE ... and 23 more variables
names(default_plots)
#> [1] "dv_vs_ipred" "dv_vs_pred" "res_vs_idv" "res_vs_pred"
#> [5] "eta_distrib" "eta_grid" "eta_vs_cov_grid" "ind_plots_sample"
default_plots$dv_vs_ipred
#> `geom_smooth()` using formula = 'y ~ x'A custom plots list can mix bare functions and one-sided
formulas in the ~ fn(.x) idiom used elsewhere in this
package (see focus_function()). The formula form lets extra
arguments be baked straight into the call, which is also how the same
underlying plot function can appear more than once with different
options:
custom_plots <- plot(
xpdb_x,
plots = list(
xpose::dv_vs_ipred,
~ xpose::res_vs_idv(.x, res = "CWRES"),
~ xpose::res_vs_idv(.x, res = "IWRES")
),
quiet = TRUE
)
#> Using data from $prob no.1
#> Filtering data by EVID == 0
#> Using data from $prob no.1
#> Filtering data by EVID == 0
#> Using data from $prob no.1
#> Filtering data by EVID == 0
names(custom_plots)
#> [1] "plot_1" "xpose::res_vs_idv...2" "xpose::res_vs_idv...3"If a single entry itself returns a list of plots, that list is flattened into the overall output rather than kept nested, so the result is always a flat list regardless of what any one entry produces.
A failing plot, by default, aborts the whole call immediately
(showing the original error as its cause) without returning anything –
useful when a batch of plots feeds directly into a report and a silent
gap would be worse than stopping early. Passing
force = TRUE instead turns a failure into a warning and
skips just that entry, so the rest of plots still gets a
chance to run:
flaky_plots <- plot(
xpdb_x,
plots = list(
xpose::dv_vs_ipred,
~ stop("simulated failure"),
xpose::eta_distrib
),
force = TRUE,
quiet = FALSE
)
#> Using data from $prob no.1
#> Filtering data by EVID == 0
#> Warning: Failed to generate the "stop" plot (2 of 3); skipping.
#> Caused by error in `fn()`:
#> ! simulated failure
#> Using data from $prob no.1
#> Removing duplicated rows based on: ID
#> Tidying data by ID, SEX, MED1, MED2, DOSE ... and 23 more variables
#> ! 1 of 3 plot(s) failed and was skipped: "stop"
names(flaky_plots)
#> [1] "plot_1" "plot_3"Like default_labs/default_watermark, a
project-wide plots spec can be set once via the
xpose.xtras.default_plots session option (see
set_xtras_options()), or attached to a specific
xpdb with set_default_plots(), which takes
precedence over the option. Unlike those two, though, this default is
resolved as a whole rather than merged entry by entry – a plot spec’s
entries aren’t addressable by a stable key, since the same function can
legitimately appear more than once:
xpdb_custom <- set_default_plots(xpdb_x, list(~ xpose::dv_vs_ipred(.x), ~ xpose::eta_distrib(.x)))
names(plot(xpdb_custom, quiet = TRUE))
#> Using data from $prob no.1
#> Filtering data by EVID == 0
#> Using data from $prob no.1
#> Removing duplicated rows based on: ID
#> Tidying data by ID, SEX, MED1, MED2, DOSE ... and 23 more variables
#> [1] "xpose::dv_vs_ipred" "xpose::eta_distrib"ggsave_xp()ggsave_xp() is a drop-in,
ggplot2::ggsave()-compatible way to write a plot to disk:
same
plot/filename/path/width/height
arguments, but it applies
apply_default_labs()/add_watermark() first
(apply_labs/apply_watermark, more on those in
Automatic application), and
path/width/height fall back to
the
xpose.xtras.save_dir/save_width/save_height
options when not supplied.
out_dir <- tempdir()
saved_path <- ggsave_xp(p, filename = "dv_vs_ipred.png", path = out_dir, width = 6, height = 4)
#> `geom_smooth()` using formula = 'y ~ x'
basename(saved_path)
#> [1] "dv_vs_ipred.png"That’s a real file on disk. Here’s what actually got written:
#> [1] TRUE
filename/path can also use
@keyword placeholders, expanded the same way
xpose::xpose_save() does it:
run_path <- ggsave_xp(dv_vs_ipred(xpdb_ex_pk, quiet = TRUE), filename = "@run_@plotfun.png", path = out_dir)
#> `geom_smooth()` using formula = 'y ~ x'
basename(run_path)
#> [1] "run001_dv_vs_ipred.png"What actually does the saving is itself swappable, via
save_fun. The default is ggplot2::ggsave(),
but anything sharing its signature works, including third-party wrappers
like reportifyr::ggsave_with_metadata(). As a
self-contained illustration, so this vignette doesn’t need to depend on
a third-party package just to demonstrate the idea, here’s a
save_fun that writes a plain-text note alongside the
image:
save_with_note <- function(plot, filename, path = NULL, width, height, note = "", ...) {
saved <- ggplot2::ggsave(filename = filename, plot = plot, path = path, width = width, height = height, ...)
writeLines(note, sub("\\.[^.]+$", ".txt", saved))
saved
}
annotated_path <- ggsave_xp(
p, filename = "dv_vs_ipred_annotated.png", path = out_dir,
save_fun = save_with_note, note = "Generated for the Q3 interim analysis."
)
#> `geom_smooth()` using formula = 'y ~ x'
# the .png was written by ggplot2::ggsave() as usual; the .txt is new
readLines(sub("\\.png$", ".txt", annotated_path))
#> [1] "Generated for the Q3 interim analysis."Any function accepting
plot/filename/path/width/height
(plus whatever else it needs via ...) can be dropped in
this way. There’s nothing xpose.xtras-specific about
save_fun itself.
Everything above can be called explicitly, but it doesn’t have to be.
Once a default_labs/default_watermark is
actually configured, at the option level, as in the examples so far,
print.xpose_plot() and ggsave_xp() apply it on
their own:
options(
xpose.xtras.default_labs = list(caption = "DRAFT: do not distribute"),
xpose.xtras.default_watermark = list(label = "DRAFT")
)
dv_vs_ipred(xpdb_ex_pk, quiet = TRUE)
#> `geom_smooth()` using formula = 'y ~ x'No apply_default_labs(), no
add_watermark(), just an ordinary
dv_vs_ipred() call, printed the ordinary way. This is
exactly what happens when a plot auto-prints at the console or in a
knitted report. It’s controlled by the
xpose.xtras.auto_apply option, TRUE unless
changed. Note that it’s the configured default that’s applied
automatically, not an unconditional one: with
default_watermark left unset,
auto_apply = TRUE would not stamp a “DRAFT” on every plot
on its own. It’s purely “apply what was already configured, without
asking again.”
To turn this off everywhere at once:
options(xpose.xtras.auto_apply = FALSE)
dv_vs_ipred(xpdb_ex_pk, quiet = TRUE)
#> `geom_smooth()` using formula = 'y ~ x'Or, to opt out for one ggsave_xp() call without touching
the option, pass
apply_labs = FALSE/apply_watermark = FALSE
directly to that call.
Either way, this only ever reaches the session-wide option tier.
print() has no way to receive the xpdb a plot
was built from, so an xpdb-level default (set via
set_default_labs()/set_default_watermark())
still needs
apply_default_labs()/add_watermark(), or
ggsave_xp()’s xpdb argument, called
explicitly. Auto-apply covers the common “one default for the whole
session” case, not the per-model one.
Setting several of the options above one at a time is easy to get
wrong: a typo in an option name just silently does nothing, since
options() doesn’t validate names.
set_xtras_options() is a small, validated wrapper for the
whole xpose.xtras.* family (prefix added automatically),
good for pinning a project’s defaults once at the top of a script:
set_xtras_options(
default_labs = list(caption = "DRAFT"),
default_watermark = list(label = "DRAFT"),
save_dir = tempdir(),
save_width = 6,
save_height = 4
)
# labels and watermark are auto-applied, and save_dir/save_width/save_height
# supply filename's path/width/height: one call, nothing else configured
saved_together <- ggsave_xp(dv_vs_ipred(xpdb_ex_pk, quiet = TRUE), filename = "tied_together.png")
#> `geom_smooth()` using formula = 'y ~ x'
basename(saved_together)
#> [1] "tied_together.png"#> [1] TRUE
The same family also covers a session-wide default theme,
gg_theme/xp_theme, picked up automatically the
first time an xpose_data object is converted with
as_xpdb_x(). A project look can be set once instead of
calling xpose::update_themes() on every model:
set_xtras_options(gg_theme = theme_bw)
xpdb_ex_pk %>%
as_xpdb_x() %>%
dv_vs_ipred(quiet = TRUE)
#> `geom_smooth()` using formula = 'y ~ x'get_xtras_option() reports, for a given option and
(optionally) a specific xpdb, what’s currently set at each
tier and which one would win:
xpdb_labelled <- xpdb_x %>%
set_default_labs(caption = "Model-specific caption")
get_xtras_option("default_labs", xpdb_labelled)
#> $option
#> $option$caption
#> [1] "DRAFT"
#>
#>
#> $xpdb
#> $xpdb$caption
#> [1] "Model-specific caption"
#>
#>
#> $dominant
#> [1] "xpdb"The full list of recognized options, what each one does, and which
ones have an xpdb-level counterpart, is documented on
?set_xtras_options.