The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

Polishing plot output: labels, watermarks, and session options

Introduction

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:

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.

Default labels

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'

options(xpose.xtras.default_labs = NULL)

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'

Watermarks

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.

p %>%
  add_watermark()
#> `geom_smooth()` using formula = 'y ~ x'

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:

pred_plot %>%
  add_watermark(alpha = 0.6, angle = -45)
#> `geom_smooth()` using formula = 'y ~ x'

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'

options(xpose.xtras.default_watermark = NULL)

Generating a batch of plots with 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"

Saving with 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.

Automatic application

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.

Tying it together with session options

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.

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.