---
title: "Polishing plot output: labels, watermarks, and session options"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Polishing plot output: labels, watermarks, and session options}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.align='center'
)
```

```{r setup, echo=FALSE, include = FALSE}
library(dplyr)
library(xpose)
library(xpose.xtras)
```

## 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:

- `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.

## 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:

```{r default_labs_option}
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
```

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:

```{r default_labs_overwrite}
apply_default_labs(p, caption = "Run @run, @nobs observations", overwrite = TRUE)
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:

```{r default_labs_xpdb}
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)
```

`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:

```{r default_labs_direct}
dv_vs_ipred(xpdb_draft, quiet = TRUE) %>%
  apply_default_labs(caption = "Final for submission", xpdb = xpdb_draft, overwrite = TRUE)
```

## 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.

```{r watermark_basic}
p %>%
  add_watermark()
```

`label`, `colour`, `alpha`, `size`, `angle` and `fontface` are all adjustable:

```{r watermark_custom}
p %>%
  add_watermark(label = "PRELIMINARY", colour = "firebrick", alpha = 0.15, angle = 20)
```

The watermark is added as an ordinary `ggplot2` layer, so it survives faceting and pagination: one watermark per panel, automatically:

```{r watermark_facets}
dv_vs_ipred(xpdb_ex_pk, quiet = TRUE, facets = "SEX") %>%
  add_watermark(label = "DRAFT")
```

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:

```{r watermark_angle_unity_default}
pred_plot <- dv_vs_pred(xpdb_ex_pk, quiet = TRUE)

pred_plot %>%
  add_watermark(alpha = 0.6)
```

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:

```{r watermark_angle_unity_adjusted}
pred_plot %>%
  add_watermark(alpha = 0.6, angle = -45)
```

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:

```{r watermark_option}
options(xpose.xtras.default_watermark = list(label = "CONFIDENTIAL", colour = "firebrick"))
p %>%
  add_watermark()
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`.

```{r plot_default}
default_plots <- plot(xpdb_x, quiet = TRUE)
names(default_plots)
default_plots$dv_vs_ipred
```

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:

```{r plot_custom}
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
)
names(custom_plots)
```

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:

```{r plot_force}
flaky_plots <- plot(
  xpdb_x,
  plots = list(
    xpose::dv_vs_ipred,
    ~ stop("simulated failure"),
    xpose::eta_distrib
  ),
  force = TRUE,
  quiet = FALSE
)
names(flaky_plots)
```

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:

```{r plot_default_plots}
xpdb_custom <- set_default_plots(xpdb_x, list(~ xpose::dv_vs_ipred(.x), ~ xpose::eta_distrib(.x)))
names(plot(xpdb_custom, quiet = TRUE))
```

## 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.

```{r ggsave_xp_basic}
out_dir <- tempdir()

saved_path <- ggsave_xp(p, filename = "dv_vs_ipred.png", path = out_dir, width = 6, height = 4)
basename(saved_path)
```

That's a real file on disk. Here's what actually got written:

```{r ggsave_xp_basic_preview, echo = FALSE, out.width = "80%"}
# knitr/pkgdown can only reliably embed images under knitr's own fig.path,
# not an arbitrary absolute path like the tempdir() used above to
# demonstrate an arbitrary save location
preview_path <- knitr::fig_path("png")
dir.create(dirname(preview_path), recursive = TRUE, showWarnings = FALSE)
file.copy(saved_path, preview_path, overwrite = TRUE)
knitr::include_graphics(preview_path)
```

`filename`/`path` can also use `@keyword` placeholders, expanded the same way `xpose::xpose_save()` does it:

```{r ggsave_xp_keywords}
run_path <- ggsave_xp(dv_vs_ipred(xpdb_ex_pk, quiet = TRUE), filename = "@run_@plotfun.png", path = out_dir)
basename(run_path)
```

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:

```{r ggsave_xp_custom_backend}
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."
)

# the .png was written by ggplot2::ggsave() as usual; the .txt is new
readLines(sub("\\.png$", ".txt", annotated_path))
```

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:

```{r auto_apply_demo}
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)
```

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:

```{r auto_apply_off}
options(xpose.xtras.auto_apply = FALSE)

dv_vs_ipred(xpdb_ex_pk, quiet = TRUE)
```

```{r auto_apply_reset, include = FALSE}
options(
  xpose.xtras.auto_apply = NULL,
  xpose.xtras.default_labs = NULL,
  xpose.xtras.default_watermark = NULL
)
```

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:

```{r set_xtras_options}
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")
basename(saved_together)
```

```{r tied_together_preview, echo = FALSE, out.width = "80%"}
preview_path <- knitr::fig_path("png")
dir.create(dirname(preview_path), recursive = TRUE, showWarnings = FALSE)
file.copy(saved_together, preview_path, overwrite = TRUE)
knitr::include_graphics(preview_path)
```

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:

```{r set_xtras_options_theme}
set_xtras_options(gg_theme = theme_bw)

xpdb_ex_pk %>%
  as_xpdb_x() %>%
  dv_vs_ipred(quiet = TRUE)
```

`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:

```{r get_xtras_option}
xpdb_labelled <- xpdb_x %>%
  set_default_labs(caption = "Model-specific caption")

get_xtras_option("default_labs", xpdb_labelled)
```

```{r set_xtras_options_reset, include = FALSE}
set_xtras_options(
  default_labs = NULL, default_watermark = NULL,
  save_dir = NULL, save_width = NULL, save_height = NULL,
  gg_theme = NULL
)
```

The full list of recognized options, what each one does, and which ones have an `xpdb`-level counterpart, is documented on `?set_xtras_options`.
