Nothing
# model -----------------------------------------------------------------------
#' Add a fitted-model curve/ribbon layer
#'
#' Adds the model layer: a fitted exposure-response curve with an
#' uncertainty ribbon, or possibly a spaghetti plot of simulated draws.
#'
#' @param object Partially constructed plot (has S3 class `er_plot`).
#' @param model A fitted exposure-response model. Must implement [er_predict()].
#' @param style Style used to draw the model curve/ribbon layer. Can
#' either be a string corresponding to one of the registered style labels
#' (e.g., `"ribbonline"`, the default), or a builder function used to
#' compute the relevant plot object (see "Styles" below).
#' @param keep_strata Logical; whether this layer should use stratification.
#' Defaults to `TRUE` when a stratification variable has been specified,
#' and `FALSE` otherwise.
#' @param conf_level Confidence level for the prediction ribbon. Defaults
#' to `0.95`.
#' @param predict_args A named list of additional arguments forwarded to
#' [er_predict()] when generating model-based predictions.
#' @param ... Additional named arguments forwarded to the `style` builder
#' function when the plot is built.
#'
#' @details
#' This layer uses [er_predict()] to compute model predictions on the response
#' scale. `model` may reference covariates beyond the exposure and strata
#' variables. erplots fills any additional covariates from the plot data with
#' a reference value (first factor level or numeric mean) when building the
#' prediction grid. erplots does not check that `model` was fit on the same
#' exposure/response as the plot; the caller must ensure compatibility.
#'
#' @section Styles:
#' The following pre-defined styles are available for this layer. Please
#' see the documentation for the corresponding builder function to see what
#' customisation options are available:
#'
#' | Label | Builder | Description |
#' | --- | --- | --- |
#' | `"ribbonline"` | [er_style_model_ribbonline()] | Fitted curve with an uncertainty ribbon (the default). |
#' | `"line"` | [er_style_model_line()] | Fitted curve only, no ribbon. |
#' | `"spaghetti"` | [er_style_model_spaghetti()] | Fitted curve plus a spaghetti plot of simulated draws, for models implementing [er_simulate()]. |
#'
#' See [er_style()] for details on how style builder functions are
#' defined for the exposure-response mini-grammar, should a custom style
#' be required.
#'
#' @returns The input `object`, with the model layer added.
#'
#' @examples
#' if (requireNamespace("erglm", quietly = TRUE)) {
#' library(erglm)
#' mod <- erglm_model(ae1 ~ aucss, erglm_data, family = binomial())
#' erglm_data |>
#' er_plot(aucss, ae1) |>
#' er_plot_add_model(mod) |>
#' plot()
#'
#' # a spaghetti plot instead of the default ribbon
#' erglm_data |>
#' er_plot(aucss, ae1) |>
#' er_plot_add_model(mod, style = er_style_model_spaghetti) |>
#' plot()
#'
#' # the same spaghetti plot, selected by its registered label instead
#' # (see `?er_style_labels`)
#' erglm_data |>
#' er_plot(aucss, ae1) |>
#' er_plot_add_model(mod, style = "spaghetti") |>
#' plot()
#'
#' # plug in a fully custom model-curve builder
#' build_model_dashed <- function(data, config, stratify, exposure, response, strata, theme, ...) {
#' ggplot2::geom_line(
#' data = config$predictions,
#' mapping = ggplot2::aes(x = .data[[exposure$name]], y = fit_resp),
#' linetype = "dashed"
#' )
#' }
#' erglm_data |>
#' er_plot(aucss, ae1) |>
#' er_plot_add_model(mod, style = build_model_dashed) |>
#' plot()
#'
#' # a model with a covariate beyond the exposure variable still works even when
#' # this layer isn't stratifying by it: `sex` is set to a reference value
#' # when building the prediction grid, which may not be what the user wants
#' mod_sex <- erglm_model(ae1 ~ aucss + sex, erglm_data, family = binomial())
#' erglm_data |>
#' er_plot(aucss, ae1) |>
#' er_plot_add_model(mod_sex) |>
#' plot()
#' }
#'
#' @seealso [er_plot()], [er_plot_add_summary()], [er_plot_add_quantiles()],
#' [er_plot_add_data()], [er_plot_add_groups()], [er_style()]
#'
#' @export
er_plot_add_model <- function(object, model, style = NULL,
keep_strata = NULL, conf_level = 0.95,
predict_args = list(), ...) {
dots <- rlang::list2(...)
.check_dots_named(dots)
.check_dots_named(predict_args, arg = "predict_args")
if (!inherits(object, "er_plot")) rlang::abort("`object` must be an er_plot object")
if (!is.null(style) && !is.function(style) && !is.character(style)) {
rlang::abort("`style` must be a function, a registered label string, or NULL")
}
if (is.character(style)) style <- .lookup_style_label("plot_model", style, arg = "style")
if (is.null(keep_strata)) keep_strata <- !is.null(object$strata$name)
style <- style %||% er_style_model_ribbonline
.check_style_layer(style, "plot_model", arg = "style")
object$layer$model <- .layer_model(
object = object,
model = model,
stratify = keep_strata,
conf_level = conf_level,
predict_args = predict_args,
style = style,
dots = dots
)
return(object)
}
# summary -----------------------------------------------------------------
#' Add a summary annotation layer
#'
#' Adds the summary layer: a text/label annotation placed in whichever
#' corner of the base panel is furthest from the observed data, computed
#' from the raw `(exposure, response)` coordinates of the data.
#'
#' @param object Partially constructed plot (has S3 class `er_plot`).
#' @param model A fitted exposure-response model, or `NULL` (the default).
#' Only needed for builder styles (e.g.
#' [er_style_summary_pvalue()]) that produce model-based summaries; a
#' purely descriptive builder (e.g. [er_style_summary_n()]) ignores it.
#' @param style Style used to draw the summary annotation layer. Can
#' either be a string corresponding to one of the registered style labels
#' (e.g., `"pvalue"`, the default), or a builder function used to
#' compute the relevant plot object (see "Styles" below).
#' @param keep_strata Logical; whether this layer should use stratification.
#' Defaults to `TRUE` when a stratification variable has been specified,
#' and `FALSE` otherwise.
#' @param conf_level Confidence level forwarded to [er_summary()] (used,
#' e.g., for the `conf_low`/`conf_high` columns of its `coefficients`
#' result -- see `?er_model_interface`). Defaults to `0.95`. Ignored
#' when `model` is `NULL`.
#' @param summary_args A named list of additional arguments forwarded to
#' [er_summary()] when generating summaries.
#' @param ... Additional named arguments forwarded to the `style` builder
#' function when the plot is built.
#'
#' @section Styles:
#' The following pre-defined styles are available for this layer. Please
#' see the documentation for the corresponding builder function to see what
#' customisation options are available:
#'
#' | Label | Builder | Description |
#' | --- | --- | --- |
#' | `"pvalue"` | [er_style_summary_pvalue()] | A formatted p-value from the model's [er_summary()] result (the default). |
#' | `"n"` | [er_style_summary_n()] | Observation counts; model-agnostic, works with `model = NULL`. |
#' | `"coefficients"` | [er_style_summary_coefficients()] | One line per model parameter, from [er_summary()]'s `coefficients` table. |
#' | `"gof"` | [er_style_summary_gof()] | A goodness-of-fit annotation (N/AIC/BIC/R-squared) from [er_summary()]'s `glance` table. |
#'
#' See [er_style()] for details on how style builder functions are
#' defined for the exposure-response mini-grammar, should a custom style
#' be required.
#'
#' @returns The input `object`, with the summary layer added.
#'
#' @examples
#' if (requireNamespace("erglm", quietly = TRUE)) {
#' library(erglm)
#' mod <- erglm_model(ae1 ~ aucss, erglm_data, family = binomial())
#' erglm_data |>
#' er_plot(aucss, ae1) |>
#' er_plot_add_model(mod) |>
#' er_plot_add_summary(model = mod) |>
#' plot()
#'
#' # a purely descriptive annotation, with no model at all
#' erglm_data |>
#' er_plot(aucss, ae1) |>
#' er_plot_add_summary(style = er_style_summary_n) |>
#' plot()
#' }
#'
#' @seealso [er_plot()], [er_plot_add_model()], [er_plot_add_quantiles()],
#' [er_plot_add_data()], [er_plot_add_groups()], [er_style()]
#'
#' @export
er_plot_add_summary <- function(object, model = NULL, style = NULL, keep_strata = NULL,
conf_level = 0.95, summary_args = list(), ...) {
dots <- rlang::list2(...)
.check_dots_named(dots)
.check_dots_named(summary_args, arg = "summary_args")
if (!inherits(object, "er_plot")) rlang::abort("`object` must be an er_plot object")
if (!is.null(style) && !is.function(style) && !is.character(style)) {
rlang::abort("`style` must be a function, a registered label string, or NULL")
}
if (is.character(style)) style <- .lookup_style_label("plot_summary", style, arg = "style")
if (is.null(keep_strata)) keep_strata <- !is.null(object$strata$name)
style <- style %||% er_style_summary_pvalue
.check_style_layer(style, "plot_summary")
object$layer$summary <- .layer_summary(
object = object,
model = model,
stratify = keep_strata,
conf_level = conf_level,
summary_args = summary_args,
style = style,
dots = dots
)
return(object)
}
# quantiles -------------------------------------------------------------------
#' Add a quantile-binned response summary layer
#'
#' Adds the quantile layer: exposure is cut into quantile bins (see
#' [cut_exposure_quantile()]) and, within each bin, the response is
#' summarised with a point estimate and confidence interval.
#'
#' @param object Partially constructed plot, an `er_plot` object.
#' @param style Style used to draw the quantile summary layer. Can
#' either be a string corresponding to one of the registered style labels
#' (e.g., `"errorbar"`, the default), or a builder function used to
#' compute the relevant plot object (see "Styles" below).
#' @param keep_strata Logical; whether this layer should use stratification.
#' Defaults to `TRUE` when a stratification variable has been specified,
#' and `FALSE` otherwise.
#' @param conf_level Confidence level for the interval. Defaults to `0.95`.
#' @param n_bins Number of exposure bins (not counting placebo). Defaults
#' to `4`.
#' @param ties,quantile_type,labeller Passed straight through to
#' [cut_exposure_quantile()] to control how the exposure variable is
#' split into bins -- see its documentation for what each controls.
#' @param ... Additional named arguments forwarded to the `style` builder
#' function when the plot is built.
#'
#' @returns The input `object`, with the quantile layer added.
#'
#' @details
#' The type of confidence interval shown depends on the `response_type`
#' set in [er_plot()]:
#' * `"binary"`: Clopper-Pearson interval (see [ci_clopper_pearson()])
#' * `"continuous"`: Student t-interval (see [ci_t()])
#' * `"count"`: exact Poisson interval (see [ci_poisson()])
#'
#' Note that count responses are not automatically detected as such: they
#' default to `"continuous"` and are summarised the same way as any other
#' continuous response unless `response_type = "count"` is declared
#' explicitly in [er_plot()].
#'
#' `n_bins`/`ties`/`quantile_type`/`labeller` are local to this layer --
#' they aren't shared with [er_plot_add_groups()], even when that layer
#' groups by the same exposure variable. [er_plot_build()] warns (doesn't
#' error) if the two disagree in that specific case; pass matching values
#' to both calls to avoid the warning, or ignore it if the difference is
#' intentional.
#'
#' @section Styles:
#' The following pre-defined styles are available for this layer. Please
#' see the documentation for the corresponding builder function to see what
#' customisation options are available:
#'
#' | Label | Builder | Description |
#' | --- | --- | --- |
#' | `"errorbar"` | [er_style_quantile_errorbar()] | Point + error bar per bin (the default). |
#' | `"errorbar_vlines"` | [er_style_quantile_errorbar_vlines()] | `"errorbar"` plus a labelled vline at every bin boundary. |
#' | `"pointrange"` | [er_style_quantile_pointrange()] | Point + range per bin, via [ggplot2::geom_pointrange()]. |
#' | `"pointrange_vlines"` | [er_style_quantile_pointrange_vlines()] | `"pointrange"` plus a labelled vline at every bin boundary. |
#'
#' See [er_style()] for details on how style builder functions are
#' defined for the exposure-response mini-grammar, should a custom style
#' be required.
#'
#'
#' @examples
#' if (requireNamespace("erglm", quietly = TRUE)) {
#' library(erglm)
#' mod <- erglm_model(ae1 ~ aucss, erglm_data, family = binomial())
#' erglm_data |>
#' er_plot(aucss, ae1) |>
#' er_plot_add_model(mod) |>
#' er_plot_add_quantiles() |>
#' plot()
#'
#' # continuous response: bin means/t-intervals instead of rates/
#' # Clopper-Pearson intervals, auto-detected from the response column
#' mod3 <- erglm_model(biomarker_change ~ aucss, erglm_data, family = gaussian())
#' erglm_data |>
#' er_plot(aucss, biomarker_change) |>
#' er_plot_add_model(mod3) |>
#' er_plot_add_quantiles() |>
#' plot()
#'
#' # count response: declare response_type = "count" explicitly for an
#' # exact Poisson interval instead of the t-interval approximation used
#' # by the auto-detected ("continuous") default
#' mod4 <- erglm_model(ae_count ~ aucss, erglm_data, family = poisson())
#' erglm_data |>
#' er_plot(aucss, ae_count, response_type = "count") |>
#' er_plot_add_model(mod4) |>
#' er_plot_add_quantiles() |>
#' plot()
#'
#' # a pointrange instead of the default errorbar
#' erglm_data |>
#' er_plot(aucss, ae1) |>
#' er_plot_add_model(mod) |>
#' er_plot_add_quantiles(style = er_style_quantile_pointrange) |>
#' plot()
#'
#' # the default errorbar, with dotted lines marking the quantile-bin
#' # boundaries
#' erglm_data |>
#' er_plot(aucss, ae1) |>
#' er_plot_add_model(mod) |>
#' er_plot_add_quantiles(style = er_style_quantile_errorbar_vlines) |>
#' plot()
#'
#' # plug in a fully custom builder; see `?er_style`
#' build_quantile_crossbar <- function(data, config, stratify, exposure,
#' response, strata, theme, ...) {
#' ggplot2::geom_crossbar(
#' data = config$summary,
#' mapping = ggplot2::aes(x = x_mid, y = y_mid, ymin = ci_lower, ymax = ci_upper),
#' inherit.aes = FALSE
#' )
#' }
#' erglm_data |>
#' er_plot(aucss, ae1) |>
#' er_plot_add_model(mod) |>
#' er_plot_add_quantiles(style = build_quantile_crossbar) |>
#' plot()
#' }
#'
#' @seealso [er_plot()], [er_plot_add_model()], [er_plot_add_summary()],
#' [er_plot_add_data()], [er_plot_add_groups()], [er_vpc()],
#' [er_style()]
#'
#' @export
er_plot_add_quantiles <- function(object, style = NULL, keep_strata = NULL,
conf_level = 0.95, n_bins = 4,
ties = "upward", quantile_type = 7,
labeller = NULL, ...) {
dots <- rlang::list2(...)
.check_dots_named(dots)
if (!inherits(object, "er_plot")) rlang::abort("`object` must be an er_plot object")
if (!is.null(style) && !is.function(style) && !is.character(style)) {
rlang::abort("`style` must be a function, a registered label string, or NULL")
}
if (is.character(style)) style <- .lookup_style_label("plot_quantile", style, arg = "style")
if (is.null(keep_strata)) keep_strata <- !is.null(object$strata$name)
style <- style %||% er_style_quantile_errorbar
.check_style_layer(style, "plot_quantile")
object$layer$quantile <- .layer_quantile(
object = object,
stratify = keep_strata,
n_bins = n_bins,
conf_level = conf_level,
style = style,
dots = dots,
ties = ties,
quantile_type = quantile_type,
labeller = labeller
)
return(object)
}
# data --------------------------------------------------------------------
#' Add a raw-data layer
#'
#' Adds the data layer: individual observations. By default, points are drawn
#' as an overlay showing the exposure and response values in the main panel of
#' the plot, but other possibilities are available.
#'
#' @param object Partially constructed plot (has S3 class `er_plot`).
#' @param style Style used to draw the data layer. Can
#' either be a string corresponding to one of the registered style labels
#' (e.g., `"overlay"`, the default), or a builder function used to
#' compute the relevant plot object (see "Styles" below).
#' @param keep_strata Logical; whether this layer should use stratification.
#' Defaults to `TRUE` when a stratification variable has been specified,
#' and `FALSE` otherwise.
#' @param panel Character string: `"upper"`, `"lower"`, or `"both"` (the
#' default). Only meaningful for [er_style_data_boxjitter()] on a
#' binary response; see "Details" for when `"both"` is required.
#' @param ... Additional named arguments forwarded to the `style` builder
#' function when the plot is built.
#'
#' @returns The input `object`, with the data layer added.
#'
#' @section Styles:
#' The following pre-defined styles are available for this layer. Please
#' see the documentation for the corresponding builder function to see what
#' customisation options are available:
#'
#' | Label | Builder | Description |
#' | --- | --- | --- |
#' | `"overlay"` | [er_style_data_overlay()] | Raw points (jittered for a binary response) drawn on the main panel (the default). |
#' | `"hex"` | [er_style_data_hex()] | 2D hexbin density of the raw points on the main panel. |
#' | `"boxjitter"` | [er_style_data_boxjitter()] | Boxplot + jittered points in a stacked panel, split by response (binary response only). |
#'
#' See [er_style()] for details on how style builder functions are
#' defined for the exposure-response mini-grammar, should a custom style
#' be required.
#'
#' @section Default builders:
#' The default builder for the data layer is `er_style_data_overlay()`,
#' which creates a plain scatter plot for
#' continuous/count responses, or a scatter with a small vertical jitter
#' for a binary response (whose y-values are exactly 0/1 and would
#' otherwise overplot into two solid lines). This works uniformly across
#' all three response types, with no response-type dispatch on which
#' builder to use. `er_style_data_boxjitter()` instead uses a
#' panel-based design, and is binary-response-only: responders (`response
#' == 1`) get a boxplot + jittered points in an upper panel and
#' non-responders (`response == 0`) get the same in a lower panel, so the
#' panel shows the exposure *distribution* conditional on response, not
#' just raw points. There is no built-in "panel"-layout builder for a
#' continuous/count response; `panel` must be `"both"` (the default) for these
#' response types regardless of builder, since there's no upper/lower
#' partition to select from.
#'
#' @section Structural families:
#' Every data-layer builder declares which of these two *structural*
#' families it belongs to via [er_style_tag()] -- `"overlay"` (a single call
#' merged into the main panel) or `"panel"` (one-or-more panels stacked
#' below the base plot) -- which `er_plot_add_data()` reads off `style`
#' to decide how to assemble the layer, rather than taking a separate
#' argument for it. This makes the pairing structural rather than
#' incidental: `er_style_data_overlay()` can never be routed into upper/lower
#' panels, and `er_style_data_boxjitter()` can never be merged into the main
#' panel. See [er_style_tag()] and [er_style()] for how to tag a custom
#' builder the same way. If `style` is tagged with a `layer` other than
#' `"plot_data"`, [er_plot_add_data()] errors informatively; an untagged
#' builder is never checked (only `layout` is a hard requirement).
#'
#' @section Effect of `keep_strata`:
#' `keep_strata`'s effect also depends on a builder's structural family:
#' for an "overlay"-layout builder it always means a shared colour
#' aesthetic, for any response type; for a "panel"-layout builder on a
#' continuous/count response it instead produces one panel per stratum
#' level rather than a shared colour aesthetic. `panel` must be `"both"`
#' for an "overlay"-layout builder (there's no upper/lower partition to
#' select from) and for a continuous/count response under a
#' "panel"-layout builder (same reason).
#'
#' @examples
#' if (requireNamespace("erglm", quietly = TRUE)) {
#' library(erglm)
#' mod2 <- erglm_model(ae2 ~ aucss + sex, erglm_data, family = binomial())
#' erglm_data |>
#' er_plot(aucss, ae2, stratify_by = sex) |>
#' er_plot_add_model(mod2) |>
#' er_plot_add_quantiles() |>
#' er_plot_add_data() |>
#' plot()
#'
#' # continuous response: overlay works the same way, with no
#' # response-type-specific styling needed
#' mod3 <- erglm_model(biomarker_change ~ aucss, erglm_data, family = gaussian())
#' erglm_data |>
#' er_plot(aucss, biomarker_change) |>
#' er_plot_add_model(mod3) |>
#' er_plot_add_data() |>
#' plot()
#'
#' # panel-based design, binary-response only: a boxplot + jittered
#' # points per panel (responders above, non-responders below), instead
#' # of an overlay in the main panel
#' erglm_data |>
#' er_plot(aucss, ae2, stratify_by = sex) |>
#' er_plot_add_model(mod2) |>
#' er_plot_add_data(style = er_style_data_boxjitter) |>
#' plot()
#'
#' # plug in a 2D density in the main panel instead of a scatter; tagging
#' # it "overlay" via `er_style_tag()` keeps it in the single main-panel
#' # layout -- see `?er_style`
#' build_data_density <- er_style_tag(
#' function(data, config, stratify, exposure, response, strata, theme, ...) {
#' ggplot2::geom_density_2d(
#' data = data,
#' mapping = ggplot2::aes(x = .data[[exposure$name]], y = .data[[response$name]])
#' )
#' },
#' layout = "overlay"
#' )
#' erglm_data |>
#' er_plot(aucss, biomarker_change) |>
#' er_plot_add_model(mod3) |>
#' er_plot_add_data(style = build_data_density) |>
#' plot()
#' }
#'
#' @seealso [er_plot()], [er_plot_add_model()], [er_plot_add_summary()],
#' [er_plot_add_quantiles()], [er_plot_add_groups()], [er_style()]
#'
#' @export
er_plot_add_data <- function(object, style = NULL, keep_strata = NULL, panel = "both", ...) {
dots <- rlang::list2(...)
.check_dots_named(dots)
if (!inherits(object, "er_plot")) rlang::abort("`object` must be an er_plot object")
if (!is.null(style) && !is.function(style) && !is.character(style)) {
rlang::abort("`style` must be a function, a registered label string, or NULL")
}
if (is.character(style)) style <- .lookup_style_label("plot_data", style, arg = "style")
style <- style %||% er_style_data_overlay
.check_style_layer(style, "plot_data")
layout <- .style_layout(style)
if (layout == "overlay" && panel != "both") {
rlang::abort(c(
"`panel` must be \"both\" for an \"overlay\"-layout `style`.",
"i" = "The \"upper\"/\"lower\" partition is specific to a \"panel\"-layout builder on a binary response."
))
}
if (layout == "panel" && object$response$type %in% c("continuous", "count") && panel != "both") {
rlang::abort(c(
paste0("`panel` must be \"both\" for a ", object$response$type, " response."),
"i" = "The \"upper\"/\"lower\" two-panel design is specific to binary responses.",
"i" = "A continuous/count response uses a single colour-encoded panel instead."
))
}
if (is.null(keep_strata)) keep_strata <- !is.null(object$strata$name)
# use `[` (not `$`) to clear the other slot -- `object$layer$x <- NULL`
# would remove "x" from the list entirely rather than setting it to
# NULL, dropping it from `layer_set`/`plot_set` in `print.er_plot()`
if (layout == "overlay") {
object$layer$overlay <- .layer_overlay(object = object, stratify = keep_strata, style = style, dots = dots)
object$layer["data"] <- list(NULL)
} else {
object$layer$data <- .layer_data(
object = object,
stratify = keep_strata,
panel = panel,
style = style,
dots = dots
)
object$layer["overlay"] <- list(NULL)
}
return(object)
}
# groups plot -----------------------------------------------------------------
#' Add a grouped exposure-distribution panel
#'
#' Adds a group layer: a boxplot/violin panel showing the exposure
#' distribution, split by one or more grouping variables (continuous
#' grouping variables are binned into quantiles first).
#'
#' @param object Partially constructed plot (has S3 class `er_plot`).
#' @param group_by Grouping variables to define groups for distribution
#' plots (a tidyselection of variables).
#' @param style Style used to draw the group layer. Can
#' either be a string corresponding to one of the registered style labels
#' (e.g., `"boxplot"`, the default), or a builder function used to
#' compute the relevant plot object (see "Styles" below).
#' @param keep_strata Logical; whether this layer should use stratification.
#' Defaults to `TRUE` when a stratification variable has been specified,
#' and `FALSE` otherwise.
#' @param n_bins Number of quantile bins used for continuous grouping
#' variables (`NULL`, the default, uses [cut_quantile()]'s own default).
#' Applied identically to every grouping variable added by this call.
#' @param ties,quantile_type,labeller Passed straight through to
#' [cut_quantile()]/[cut_exposure_quantile()] to control how a
#' continuous grouping variable is split into bins -- see their
#' documentation for what each controls. Applied identically to every
#' grouping variable added by this call.
#' @param ... Additional named arguments forwarded to the `style` builder
#' function when the plot is built.
#'
#' @returns The input `object`, with a group panel added.
#'
#' @details
#' Unlike the other four layers, the groups layer is **additive**: each call
#' adds another panel alongside any already added by a previous call,
#' rather than replacing it.
#'
#' [er_style_group_violin()] and [er_style_group_histogram()] are the
#' other built-in `style` options; any function matching the standard
#' `(data, config, stratify, exposure, response, strata, theme, ...)`
#' signature can be supplied instead. If `style` is tagged with a
#' `layer` (via [er_style_tag()]) other than `"plot_group"`, this errors
#' informatively; an untagged builder is never checked.
#'
#' `keep_strata = TRUE` errors if `group_by` is itself the plot's
#' stratification variable, since that would mean grouping and
#' stratifying by the same column at once; pass `keep_strata = FALSE`
#' for that grouping variable instead.
#'
#' `n_bins`/`ties`/`quantile_type`/`labeller` are local to this call --
#' different grouping variables (including across separate
#' `er_plot_add_groups()` calls) aren't required to agree, and generally
#' shouldn't: they're usually different variables with no reason to share
#' a binning scheme. The one exception is grouping by the plot's own
#' exposure variable, which risks silently disagreeing with
#' [er_plot_add_quantiles()]'s own exposure-binning; [er_plot_build()]
#' warns (doesn't error) if the two disagree in that specific case.
#'
#' @section Styles:
#' The following pre-defined styles are available for this layer. Please
#' see the documentation for the corresponding builder function to see what
#' customisation options are available:
#'
#' | Label | Builder | Description |
#' | --- | --- | --- |
#' | `"boxplot"` | [er_style_group_boxplot()] | Boxplot per group level, group levels on the y-axis (the default). |
#' | `"violin"` | [er_style_group_violin()] | Violin per group level, group levels on the y-axis. |
#' | `"histogram"` | [er_style_group_histogram()] | Histogram per group level, group levels on facet strips, y-axis freed for counts. |
#' | `"linerange"` | [er_style_group_linerange()] | Median dot with inner/outer-range lines per group level, group levels on the y-axis. |
#' | `"boxjitter"` | [er_style_group_boxjitter()] | `"boxplot"` with jittered raw exposure values overlaid. |
#' | `"violinjitter"` | [er_style_group_violinjitter()] | `"violin"` with jittered raw exposure values overlaid. |
#'
#' See [er_style()] for details on how style builder functions are
#' defined for the exposure-response mini-grammar, should a custom style
#' be required.
#'
#' @examples
#' if (requireNamespace("erglm", quietly = TRUE)) {
#' library(erglm)
#' mod <- erglm_model(ae1 ~ aucss, erglm_data, family = binomial())
#' erglm_data |>
#' er_plot(aucss, ae1) |>
#' er_plot_add_model(mod) |>
#' er_plot_add_groups(aucss) |>
#' plot()
#'
#' # additive: a second call adds a second panel rather than replacing the first
#' erglm_data |>
#' er_plot(aucss, ae1) |>
#' er_plot_add_model(mod) |>
#' er_plot_add_groups(aucss) |>
#' er_plot_add_groups(treatment) |>
#' plot()
#' }
#'
#' @seealso [er_plot()], [er_plot_add_model()], [er_plot_add_summary()],
#' [er_plot_add_quantiles()], [er_plot_add_data()], [er_style()]
#'
#' @export
er_plot_add_groups <- function(object, group_by, style = NULL, keep_strata = NULL, n_bins = NULL,
ties = "upward", quantile_type = 7, labeller = NULL, ...) {
dots <- rlang::list2(...)
.check_dots_named(dots)
if (!inherits(object, "er_plot")) rlang::abort("`object` must be an er_plot object")
if (!is.null(style) && !is.function(style) && !is.character(style)) {
rlang::abort("`style` must be a function, a registered label string, or NULL")
}
if (is.character(style)) style <- .lookup_style_label("plot_group", style, arg = "style")
if (is.null(keep_strata)) keep_strata <- !is.null(object$strata$name)
group_cols <- tidyselect::eval_select(rlang::enquo(group_by), object$data)
group_cols <- names(group_cols)
style <- style %||% er_style_group_boxplot
.check_style_layer(style, "plot_group")
new_group <- .layer_group(
object = object,
group_cols = group_cols,
stratify = keep_strata,
ties = ties,
quantile_type = quantile_type,
labeller = labeller,
n_bins = n_bins,
style = style,
dots = dots
)
# additive: merge into any existing group panels rather than replacing
# them (`modifyList()` so re-adding the same grouping variable still
# replaces just that one panel, in insertion order for new names)
if (is.null(object$layer$group)) {
object$layer$group <- new_group
} else {
object$layer$group$config <- utils::modifyList(
object$layer$group$config,
new_group$config
)
}
# kept only for `.polish_legends()`'s layer-level strata-legend dedup:
# TRUE if *any* group panel (across all `er_plot_add_groups()` calls)
# is stratified, since per-panel stratification is now read from each
# group's own `config[[g]]$stratify` (see `.build_group_plot()`)
object$layer$group$stratify <- any(
purrr::map_lgl(object$layer$group$config, \(cfg) cfg$stratify)
)
return(object)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.