Nothing
# ============================================================================ #
# brma.regplot.R
# ============================================================================ #
#
# This file contains the regression plot (bubble plot) functions for brma
# objects with moderators.
#
# Bubble plots display observed effect sizes against a moderator variable,
# with point sizes proportional to study precision (inverse variance).
# For continuous moderators, a regression line with CI/PI bands is shown.
# For categorical moderators, grouped points with optional jitter are shown.
#
# ============================================================================ #
### regplot functions ----
#' @export
regplot <- function(x, ...) UseMethod("regplot")
#' @title Regression Plot (Bubble Plot) for brma Object
#'
#' @description \code{regplot.brma} creates a regression plot (also known as
#' bubble plot) for a fitted brma object with moderators. The plot displays
#' observed effect sizes against a moderator variable, with point sizes
#' proportional to study precision.
#'
#' @param x a fitted brma object with moderators
#' @param mod index or name of the moderator variable to plot on the x-axis.
#' If not specified and only one moderator is present, that moderator is used.
#' If multiple moderators are present, this argument is required.
#' @param pred logical; whether to show the prediction line. Defaults to \code{TRUE}.
#' @param ci logical; whether to show credible interval bands. Defaults to \code{TRUE}.
#' @param pi logical; whether to show prediction interval bands. Defaults to \code{FALSE}.
#' @param si logical; whether to show sampling interval bands. Defaults to \code{FALSE}.
#' The sampling interval shows the expected range of observed effect sizes,
#' incorporating both heterogeneity (tau) and a representative level of
#' sampling error (median SE across studies by default). When the model includes
#' publication bias adjustments and \code{sampling_bias = TRUE}, the
#' sampling interval incorporates the expected distortion from the
#' selection process.
#' @param level numeric; credible/prediction interval level in percent.
#' Defaults to \code{95}.
#' @param at numeric vector; for continuous moderators, values at which to
#' evaluate the prediction. If not specified, uses a sequence across the
#' observed range.
#' @param digits integer; number of decimal places for labels. Defaults to \code{2}.
#' @param transf function; transformation to apply to the y-axis (effect sizes).
#' Defaults to \code{NULL} (no transformation).
#' @param atransf reserved for axis-label transformations. Currently not
#' implemented and must be `NULL`.
#' @param targs reserved for additional transformation arguments. Currently not
#' implemented and must be `NULL`.
#' @param refline numeric; position of horizontal reference line.
#' Defaults to \code{NULL} (no reference line).
#' @param psize numeric vector or \code{NULL}; point sizes for each study.
#' If scalar, it is recycled to all studies. If \code{NULL} (default), sizes
#' are computed based on inverse sampling variance.
#' @param plim numeric vector of length 2; range for point sizes.
#' Defaults to \code{c(0.5, 3)}.
#' @param by character; name of a moderator variable to use for separate
#' lines/colors. Defaults to \code{NULL}. If omitted and the selected
#' moderator enters exactly one two-way interaction, the other variable in the
#' interaction is used automatically. Continuous \code{by} moderators are shown
#' at mean - SD, mean, and mean + SD.
#' @param legend logical; whether to show legend when \code{by} is specified.
#' Defaults to \code{TRUE}.
#' @param xlab character; x-axis label. Defaults to the moderator name.
#' @param ylab character; y-axis label. Defaults to "Observed Effect Size".
#' @param xlim numeric vector of length 2; x-axis limits. Defaults to data range.
#' @param ylim numeric vector of length 2; y-axis limits. Defaults to data range.
#' @param sampling_bias whether publication bias should be incorporated into
#' plotted predictions and sampling intervals. Defaults to \code{TRUE}. For
#' PET/PEESE models, this includes the expected regression bias in predictions.
#' For selection models, sampling-bias adjustment applies to sampling intervals
#' when \code{si = TRUE}; the mean prediction is unchanged.
#' @param sei single positive numeric value used as the reference standard
#' error for sampling-bias and sampling-interval calculations. Defaults to the
#' median observed standard error.
#' @param max_samples maximum number of posterior samples used for prediction
#' summaries and interval bands. Defaults to \code{10000}. Use \code{Inf} to
#' use all posterior samples.
#' @param plot_type character; whether to use base R graphics (\code{"base"})
#' or ggplot2 (\code{"ggplot"}). Defaults to \code{"base"}.
#' @param as_data logical; if \code{TRUE}, returns plot data instead of
#' creating plot. Defaults to \code{FALSE}.
#' @param ... additional graphical arguments:
#' \describe{
#' \item{main}{character string for plot title}
#' \item{pch}{point symbol (default: 21)}
#' \item{col}{point border color (default: "black")}
#' \item{bg}{point fill color (default: "#A6A6A6")}
#' \item{lcol}{line color (default: "black")}
#' \item{lwd}{line width (default: 2)}
#' \item{shade}{CI/PI/SI band shading (default: TRUE)}
#' \item{col.ci}{CI band color (default: "gray70")}
#' \item{col.pi}{PI band color (default: "gray85")}
#' \item{col.si}{SI band color (default: "gray92")}
#' \item{alpha.ci}{CI band transparency (default: 0.4)}
#' \item{alpha.pi}{PI band transparency (default: 0.2)}
#' \item{alpha.si}{SI band transparency (default: 0.15)}
#' \item{jitter}{jitter amount for categorical moderators (default: 0.2)}
#' \item{box.width}{box width for categorical interval summaries (default: 0.5)}
#' }
#'
#' @details
#' The regression plot (bubble plot) is a standard visualization for
#' meta-regression results. It displays:
#' \itemize{
#' \item Observed effect sizes (y-axis) against moderator values (x-axis)
#' \item Point sizes proportional to study precision (inverse variance)
#' \item Prediction line showing the estimated regression relationship
#' \item Confidence bands showing uncertainty in the mean prediction
#' \item Optional prediction bands showing expected range of true effects
#' \item Optional sampling interval bands showing expected range of observed outcomes
#' }
#'
#' For continuous moderators, predictions are computed across the observed
#' range of the moderator. For categorical moderators (factors), predictions
#' are computed at each factor level with optional jittering of points.
#'
#' The \code{by} argument allows displaying separate regression lines for
#' different levels of a second moderator, useful for visualizing interactions.
#'
#' @return \code{regplot.brma} returns \code{NULL} invisibly if
#' \code{plot_type = "base"} or a ggplot object if \code{plot_type = "ggplot"}.
#' If \code{as_data = TRUE}, returns a list with plot data components.
#'
#' @examples \dontrun{
#' if (requireNamespace("metadat", quietly = TRUE) &&
#' requireNamespace("metafor", quietly = TRUE)) {
#' data(dat.bcg, package = "metadat")
#' dat <- metafor::escalc(
#' measure = "RR",
#' ai = tpos,
#' bi = tneg,
#' ci = cpos,
#' di = cneg,
#' data = dat.bcg
#' )
#'
#' fit <- brma(
#' yi = yi,
#' vi = vi,
#' mods = ~ ablat + year,
#' data = dat,
#' measure = "RR"
#' )
#' regplot(fit, mod = "ablat")
#' regplot(fit, mod = "year", pi = TRUE, si = TRUE)
#' regplot(fit, mod = "ablat", plot_type = "ggplot")
#'
#' fit_cat <- brma(yi = yi, vi = vi, mods = ~ alloc, data = dat, measure = "RR")
#' regplot(fit_cat)
#' }
#' }
#'
#' @seealso [funnel.brma()], [predict.brma()]
#' @aliases regplot
#' @export
#' @exportS3Method metafor::regplot
#' @rdname regplot
regplot.brma <- function(x, mod = NULL, pred = TRUE, ci = TRUE, pi = FALSE, si = FALSE,
level = 95, at = NULL, digits = 2,
transf = NULL, atransf = NULL, targs = NULL,
refline = NULL, psize = NULL, plim = c(0.5, 3),
by = NULL, legend = TRUE,
xlab = NULL, ylab = NULL, xlim = NULL, ylim = NULL,
sampling_bias = TRUE, sei = NULL, max_samples = 10000,
plot_type = "base", as_data = FALSE, ...) {
# input validation
BayesTools::check_bool(pred, "pred")
BayesTools::check_bool(ci, "ci")
BayesTools::check_bool(pi, "pi")
BayesTools::check_bool(si, "si")
BayesTools::check_real(level, "level", lower = 50, upper = 99.9)
.check_plot_numeric(at, "at", allow_null = TRUE)
BayesTools::check_int(digits, "digits", lower = 0)
.check_plot_function(transf, "transf")
.check_plot_numeric(refline, "refline", check_length = 1, allow_null = TRUE)
.check_plot_numeric(plim, "plim", check_length = 2, lower = 0, allow_null = FALSE)
if (plim[1] >= plim[2]) {
stop("'plim' must be an increasing numeric vector of length 2.", call. = FALSE)
}
BayesTools::check_bool(legend, "legend")
.check_plot_label(xlab, "xlab")
.check_plot_label(ylab, "ylab")
.check_plot_limits(xlim, "xlim")
.check_plot_limits(ylim, "ylim")
BayesTools::check_bool(sampling_bias, "sampling_bias")
max_samples <- .normalize_max_samples(max_samples, "max_samples")
if (!is.null(sei)) {
BayesTools::check_real(sei, "sei", lower = 0)
if (sei <= 0) {
stop("'sei' must be positive.", call. = FALSE)
}
}
BayesTools::check_char(plot_type, "plot_type", allow_values = c("base", "ggplot"))
BayesTools::check_bool(as_data, "as_data")
# not yet implemented arguments
if (!is.null(atransf))
stop("The 'atransf' argument is not yet implemented.", call. = FALSE)
if (!is.null(targs))
stop("The 'targs' argument is not yet implemented.", call. = FALSE)
# check that model has moderators
if (!.is_mods(x)) {
stop("regplot requires a model with moderators. ",
"Use funnel() for intercept-only models.", call. = FALSE)
}
# set up graphical arguments with defaults
dots <- .set_dots_regplot(...)
# identify the moderator to plot
mod_info <- .regplot_get_moderator(x, mod)
mod_name <- mod_info$name
mod_type <- mod_info$type
mod_data <- mod_info$data
if (mod_type == "categorical" && !is.null(at)) {
stop("'at' is only available for continuous moderators.", call. = FALSE)
}
K <- length(.outcome_data_yi(x))
if (!is.null(psize)) {
.check_plot_numeric(psize, "psize", lower = 0, allow_null = FALSE)
if (!length(psize) %in% c(1L, K)) {
stop("'psize' must be either a scalar or have one value per study.", call. = FALSE)
}
}
# identify grouping variable (explicit or auto-detected interaction)
by_info <- .regplot_get_by(x, by, mod_name)
# generate plot data
regplot_data <- .regplot_data(
x = x,
mod_name = mod_name,
mod_type = mod_type,
mod_data = mod_data,
by_info = by_info,
pred = pred,
ci = ci,
pi = pi,
si = si,
level = level,
at = at,
digits = digits,
psize = psize,
plim = plim,
transf = transf,
xlim = xlim,
ylim = ylim,
xlab = xlab,
ylab = ylab,
refline = refline,
sampling_bias = sampling_bias,
max_samples = max_samples,
reference_sei = sei,
dots = dots
)
# allow data return for programmatic access
if (isTRUE(as_data)) {
return(regplot_data)
}
# create plot
if (plot_type == "ggplot") {
return(.regplot_plot_ggplot(regplot_data, dots, legend = legend))
} else {
.regplot_plot_base(regplot_data, dots, legend = legend)
return(invisible(NULL))
}
}
# ---------------------------------------------------------------------------- #
# .regplot_get_moderator
# ---------------------------------------------------------------------------- #
#
# Identify and validate the moderator variable for plotting.
#
# @param x brma object
# @param mod index or name of moderator (NULL for auto-detect)
#
# @return list with name, type, and data for the moderator
#
# ---------------------------------------------------------------------------- #
.regplot_get_moderator <- function(x, mod) {
mods_data <- x[["data"]][["mods"]]
design <- .fitted_formula_design(x, "mu", required = TRUE)
if (is.null(mods_data)) {
stop("No moderator data found in the model.", call. = FALSE)
}
mod_names <- design[["predictors"]]
if (is.null(mod)) {
# auto-detect: use single moderator or error if multiple
if (length(mod_names) == 1) {
mod_name <- mod_names[1]
} else {
stop("Multiple moderators present. Please specify 'mod' argument. ",
"Available moderators: ", paste(mod_names, collapse = ", "),
call. = FALSE)
}
} else if (is.numeric(mod)) {
# index-based selection
if (mod < 1 || mod > length(mod_names)) {
stop("Invalid moderator index. Must be between 1 and ",
length(mod_names), ".", call. = FALSE)
}
mod_name <- mod_names[mod]
} else if (is.character(mod)) {
# name-based selection
if (!mod %in% mod_names) {
stop("Moderator '", mod, "' not found. ",
"Available moderators: ", paste(mod_names, collapse = ", "),
call. = FALSE)
}
mod_name <- mod
} else {
stop("'mod' must be NULL, a character name, or a numeric index.",
call. = FALSE)
}
# extract moderator data and determine type
mod_values <- mods_data[[mod_name]]
predictor_type <- design[["predictor_types"]][[mod_name]]
if (identical(predictor_type, "factor") ||
is.factor(mod_values) || is.character(mod_values)) {
mod_type <- "categorical"
mod_values <- factor(
mod_values,
levels = if (!is.null(design[["xlevels"]][[mod_name]])) {
design[["xlevels"]][[mod_name]]
} else {
levels(factor(mod_values))
}
)
} else {
mod_type <- "continuous"
}
return(list(
name = mod_name,
type = mod_type,
data = mod_values
))
}
# ---------------------------------------------------------------------------- #
# .regplot_get_by
# ---------------------------------------------------------------------------- #
#
# Identify and validate a grouping moderator for interaction displays.
#
# ---------------------------------------------------------------------------- #
.regplot_get_by <- function(x, by, mod_name) {
mods_data <- x[["data"]][["mods"]]
design <- .fitted_formula_design(x, "mu", required = TRUE)
if (is.null(by)) {
by <- .regplot_detect_interaction_by(x, mod_name)
}
if (is.null(by)) {
return(NULL)
}
mod_names <- design[["predictors"]]
if (!is.character(by) || length(by) != 1L) {
stop("'by' must be NULL or a single moderator name.", call. = FALSE)
}
if (by == mod_name) {
stop("'by' must be different from 'mod'.", call. = FALSE)
}
if (!by %in% mod_names) {
stop("Moderator '", by, "' not found. ",
"Available moderators: ", paste(mod_names, collapse = ", "),
call. = FALSE)
}
by_values <- mods_data[[by]]
predictor_type <- design[["predictor_types"]][[by]]
if (identical(predictor_type, "factor") ||
is.factor(by_values) || is.character(by_values)) {
by_type <- "categorical"
by_values <- factor(
by_values,
levels = if (!is.null(design[["xlevels"]][[by]])) {
design[["xlevels"]][[by]]
} else {
levels(factor(by_values))
}
)
} else {
by_type <- "continuous"
}
return(list(
name = by,
type = by_type,
data = by_values
))
}
# ---------------------------------------------------------------------------- #
# .regplot_detect_interaction_by
# ---------------------------------------------------------------------------- #
#
# Auto-select the second variable from a unique two-way interaction containing
# the plotted moderator.
#
# ---------------------------------------------------------------------------- #
.regplot_detect_interaction_by <- function(x, mod_name) {
mods_data <- x[["data"]][["mods"]]
if (is.null(mods_data)) {
return(NULL)
}
term_labels <- .fitted_formula_terms(
object = x,
parameter = "mu",
include_intercept = FALSE,
display = TRUE
)
term_labels <- term_labels[grepl(":", term_labels, fixed = TRUE)]
if (length(term_labels) == 0L) {
return(NULL)
}
candidates <- character()
for (term in term_labels) {
variables <- strsplit(term, ":", fixed = TRUE)[[1]]
variables <- trimws(variables)
if (length(variables) == 2L && mod_name %in% variables) {
candidates <- c(candidates, setdiff(variables, mod_name))
}
}
candidates <- unique(candidates[candidates %in% names(mods_data)])
if (length(candidates) == 0L) {
return(NULL)
}
if (length(candidates) > 1L) {
stop("The selected moderator enters multiple interactions. ",
"Please specify 'by'. Candidate moderators: ",
paste(candidates, collapse = ", "), ".", call. = FALSE)
}
return(candidates)
}
# ---------------------------------------------------------------------------- #
# .regplot_band_data_continuous
# ---------------------------------------------------------------------------- #
#
# Build continuous interval-band data with one row per polygon vertex while
# preserving the underlying interval bounds for programmatic access.
#
# @param xpred numeric vector of prediction x values
# @param lower numeric vector of lower interval bounds
# @param upper numeric vector of upper interval bounds
#
# @return data.frame with consistent polygon and interval metadata
#
# ---------------------------------------------------------------------------- #
.regplot_band_data_continuous <- function(xpred, lower, upper, group = "All",
group_id = 1L) {
band_x <- c(xpred, rev(xpred))
band_lower <- c(lower, rev(lower))
band_upper <- c(upper, rev(upper))
data.frame(
x = unname(band_x),
y = unname(c(lower, rev(upper))),
lower = unname(band_lower),
upper = unname(band_upper),
xpred = unname(band_x),
group = rep(group, length(band_x)),
group_id = rep(group_id, length(band_x))
)
}
# ---------------------------------------------------------------------------- #
# .regplot_prediction_grid
# ---------------------------------------------------------------------------- #
#
# Build moderator values for predictions. The selected moderator forms the
# x-axis; an optional `by` moderator expands the grid into interaction lines.
#
# ---------------------------------------------------------------------------- #
.regplot_prediction_grid <- function(x, mod_name, mod_type, mod_data,
by_info, at, digits) {
mods_data <- x[["data"]][["mods"]]
design <- .fitted_formula_design(x, "mu", required = TRUE)
if (mod_type == "continuous") {
if (is.null(at)) {
x_range <- range(mod_data)
at_pred <- seq(x_range[1], x_range[2], length.out = 101)
} else {
at_pred <- at
}
x_values <- at_pred
x_numeric <- at_pred
x_levels <- NULL
} else {
x_levels <- levels(mod_data)
at_pred <- x_levels
x_values <- factor(at_pred, levels = x_levels)
x_numeric <- seq_along(at_pred)
}
if (is.null(by_info)) {
by_values <- list(
values = NULL,
labels = "All",
levels = NULL
)
} else {
by_values <- .regplot_by_values(by_info, digits)
}
grid <- expand.grid(
x_id = seq_along(at_pred),
group_id = seq_along(by_values[["labels"]]),
KEEP.OUT.ATTRS = FALSE
)
grid[["group"]] <- by_values[["labels"]][grid[["group_id"]]]
grid[["x"]] <- x_numeric[grid[["x_id"]]]
if (mod_type == "categorical") {
grid[["level"]] <- at_pred[grid[["x_id"]]]
}
n_pred <- nrow(grid)
newdata_list <- list()
for (nm in design[["predictors"]]) {
if (nm == mod_name) {
if (mod_type == "continuous") {
newdata_list[[nm]] <- x_values[grid[["x_id"]]]
} else {
newdata_list[[nm]] <- factor(at_pred[grid[["x_id"]]], levels = x_levels)
}
} else if (!is.null(by_info) && nm == by_info[["name"]]) {
if (by_info[["type"]] == "continuous") {
newdata_list[[nm]] <- by_values[["values"]][grid[["group_id"]]]
} else {
newdata_list[[nm]] <- factor(
by_values[["values"]][grid[["group_id"]]],
levels = by_values[["levels"]]
)
}
} else {
other_vals <- mods_data[[nm]]
if (is.factor(other_vals) || is.character(other_vals)) {
other_levels <- if (!is.null(design[["xlevels"]][[nm]])) {
design[["xlevels"]][[nm]]
} else {
levels(factor(other_vals))
}
newdata_list[[nm]] <- factor(
rep(other_levels[1], n_pred),
levels = other_levels
)
} else {
newdata_list[[nm]] <- rep(mean(other_vals), n_pred)
}
}
}
return(list(
newdata = as.data.frame(newdata_list),
grid = grid,
at_pred = at_pred,
groups = by_values[["labels"]],
x_levels = x_levels
))
}
# ---------------------------------------------------------------------------- #
# .regplot_by_values
# ---------------------------------------------------------------------------- #
#
# Values used for the grouping moderator.
#
# ---------------------------------------------------------------------------- #
.regplot_by_values <- function(by_info, digits) {
by_data <- by_info[["data"]]
if (by_info[["type"]] == "continuous") {
by_mean <- mean(by_data)
by_sd <- stats::sd(by_data)
values <- c(by_mean - by_sd, by_mean, by_mean + by_sd)
labels <- c("Mean - 1 SD", "Mean", "Mean + 1 SD")
if (!is.finite(by_sd) || by_sd == 0) {
values <- by_mean
labels <- format(round(by_mean, digits = digits), trim = TRUE)
}
labels <- paste0(by_info[["name"]], ": ", labels)
return(list(values = values, labels = labels, levels = NULL))
}
by_factor <- factor(by_data)
return(list(
values = levels(by_factor),
labels = levels(by_factor),
levels = levels(by_factor)
))
}
# ---------------------------------------------------------------------------- #
# .regplot_observed_groups
# ---------------------------------------------------------------------------- #
#
# Assign observed studies to plotting groups.
#
# ---------------------------------------------------------------------------- #
.regplot_observed_groups <- function(by_info, groups, n) {
if (is.null(by_info)) {
return(list(
group = rep("All", n),
group_id = rep(1L, n)
))
}
by_data <- by_info[["data"]]
if (by_info[["type"]] == "categorical") {
group <- as.character(factor(by_data, levels = groups))
} else {
by_mean <- mean(by_data)
by_sd <- stats::sd(by_data)
if (!is.finite(by_sd) || by_sd == 0 || length(groups) == 1L) {
group <- rep(groups[1], length(by_data))
} else {
by_values <- c(by_mean - by_sd, by_mean, by_mean + by_sd)
breaks <- c(-Inf, mean(by_values[1:2]), mean(by_values[2:3]), Inf)
group <- as.character(cut(by_data, breaks = breaks, labels = groups))
}
}
return(list(
group = group,
group_id = match(group, groups)
))
}
# ---------------------------------------------------------------------------- #
# .regplot_add_dummy_outcome
# ---------------------------------------------------------------------------- #
#
# Add outcome columns for prediction paths that use sampling bias inputs.
#
# ---------------------------------------------------------------------------- #
.regplot_add_dummy_outcome <- function(x, newdata, n_pred, sei) {
outcome_type <- .outcome_type(x)
if (outcome_type == "norm") {
newdata[["yi"]] <- rep(0, n_pred)
newdata[["sei"]] <- rep(sei, n_pred)
} else if (outcome_type == "bin") {
newdata[["ai"]] <- rep(0, n_pred)
newdata[["ci"]] <- rep(0, n_pred)
newdata[["n1i"]] <- rep(0, n_pred)
newdata[["n2i"]] <- rep(0, n_pred)
} else if (outcome_type == "pois") {
newdata[["x1i"]] <- rep(0, n_pred)
newdata[["x2i"]] <- rep(0, n_pred)
newdata[["t1i"]] <- rep(0, n_pred)
newdata[["t2i"]] <- rep(0, n_pred)
}
return(newdata)
}
# ---------------------------------------------------------------------------- #
# .regplot_band_data_categorical
# ---------------------------------------------------------------------------- #
#
# Categorical intervals are rendered as box-style summaries.
#
# ---------------------------------------------------------------------------- #
.regplot_band_data_categorical <- function(grid, middle, lower, upper) {
out <- data.frame(
x = grid[["x"]],
y = middle,
middle = middle,
lower = lower,
upper = upper,
group = grid[["group"]],
group_id = grid[["group_id"]],
level = grid[["level"]],
stringsAsFactors = FALSE
)
return(out)
}
# ---------------------------------------------------------------------------- #
# .regplot_data
# ---------------------------------------------------------------------------- #
#
# Generate data for regression plot.
#
# @param x brma object
# @param mod_name name of moderator variable
# @param mod_type "continuous" or "categorical"
# @param mod_data moderator data values
# @param by_info list with grouping variable info (or NULL)
# @param pred logical; show prediction line
# @param ci logical; show CI bands
# @param pi logical; show PI bands
# @param si logical; show SI bands
# @param level confidence level (0-100)
# @param at values at which to evaluate predictions
# @param psize point sizes (or NULL for auto)
# @param plim point size range
# @param transf transformation function
# @param xlim x-axis limits
# @param ylim y-axis limits
# @param xlab x-axis label
# @param ylab y-axis label
# @param refline reference line position
# @param sampling_bias logical; incorporate bias into predictions
# @param max_samples maximum posterior samples for plot summaries
# @param reference_sei numeric; reference standard error for sampling paths
# @param dots graphical parameters
#
# @return list with plot data components
#
# ---------------------------------------------------------------------------- #
.regplot_data <- function(x, mod_name, mod_type, mod_data, by_info,
pred, ci, pi, si, level, at, digits, psize, plim,
transf, xlim, ylim, xlab, ylab, refline,
sampling_bias, max_samples, reference_sei, dots) {
yi <- .outcome_data_yi(x)
sei_obs <- .outcome_data_sei(x)
vi <- sei_obs^2
K <- length(yi)
se_rep <- if (is.null(reference_sei)) stats::median(sei_obs) else reference_sei
alpha <- (100 - level) / 100
probs <- c(alpha / 2, 1 - alpha / 2)
if (is.null(psize)) {
weights <- 1 / vi
weights_n <- (weights - min(weights)) / (max(weights) - min(weights) + 1e-10)
psize <- plim[1] + weights_n * (plim[2] - plim[1])
} else if (length(psize) == 1) {
psize <- rep(psize, K)
}
yi_plot <- yi
if (!is.null(transf)) {
yi_plot <- transf(yi)
}
prediction_grid <- .regplot_prediction_grid(
x = x,
mod_name = mod_name,
mod_type = mod_type,
mod_data = mod_data,
by_info = by_info,
at = at,
digits = digits
)
grid <- prediction_grid[["grid"]]
groups <- prediction_grid[["groups"]]
n_pred <- nrow(grid)
prediction_se <- if (sampling_bias) se_rep else 0
newdata <- .regplot_add_dummy_outcome(
x = x,
newdata = prediction_grid[["newdata"]],
n_pred = n_pred,
sei = prediction_se
)
posterior_samples <- .get_posterior_samples(x[["fit"]])
selected_rows <- .thin_sample_rows(nrow(posterior_samples), max_samples)
if (!is.null(selected_rows)) {
posterior_samples <- posterior_samples[selected_rows, , drop = FALSE]
}
pred_samples <- predict.brma(
object = x,
newdata = newdata,
type = "terms",
bias_adjusted = !sampling_bias,
quiet = TRUE,
.posterior_samples = posterior_samples
)
pred_samples <- as.matrix(pred_samples)
if (nrow(pred_samples) != nrow(posterior_samples)) {
stop("Posterior sample count mismatch in regplot().", call. = FALSE)
}
pred_mean <- colMeans(pred_samples)
pred_lower <- apply(pred_samples, 2, stats::quantile, probs = probs[1], names = FALSE)
pred_upper <- apply(pred_samples, 2, stats::quantile, probs = probs[2], names = FALSE)
if (pi || si) {
is_scale <- .is_scale(x)
is_multilevel <- .is_multilevel(x)
scale_data <- NULL
scale_formula <- NULL
if (is_scale) {
new_data_prepared <- .prepare_newdata(object = x, newdata = newdata, type = "estimate")
scale_data <- new_data_prepared[["scale"]]
scale_formula <- .create_fit_formula_list(data = new_data_prepared, "scale")
}
tau_result <- .evaluate.brma.tau(
fit = x[["fit"]],
scale_data = scale_data,
scale_formula = scale_formula,
scale_priors = x[["priors"]][["scale"]],
is_scale = is_scale,
is_multilevel = is_multilevel,
K = n_pred,
posterior_samples = posterior_samples
)
tau_total <- sqrt(tau_result[["tau_within"]]^2 + tau_result[["tau_between"]]^2)
}
if (pi) {
pi_bounds <- .regplot_mixture_interval_quantiles(
mean_samples = pred_samples,
sd_samples = tau_total,
probs = probs
)
pi_lower <- pi_bounds[["lower"]]
pi_upper <- pi_bounds[["upper"]]
} else {
pi_lower <- NULL
pi_upper <- NULL
}
if (si) {
sd_si <- sqrt(tau_total^2 + se_rep^2)
if (sampling_bias && .is_weightfunction(x)) {
si_bounds <- .regplot_selection_mixture_interval_quantiles(
x = x,
mean_samples = pred_samples,
sd_samples = sd_si,
se = se_rep,
probs = probs,
posterior_samples = posterior_samples
)
} else {
si_bounds <- .regplot_mixture_interval_quantiles(
mean_samples = pred_samples,
sd_samples = sd_si,
probs = probs
)
}
si_lower <- si_bounds[["lower"]]
si_upper <- si_bounds[["upper"]]
} else {
si_lower <- NULL
si_upper <- NULL
}
if (!is.null(transf)) {
pred_mean <- transf(pred_mean)
ci_lo <- transf(pred_lower)
ci_hi <- transf(pred_upper)
pred_lower <- pmin(ci_lo, ci_hi)
pred_upper <- pmax(ci_lo, ci_hi)
if (pi) {
pi_lo <- transf(pi_lower)
pi_hi <- transf(pi_upper)
pi_lower <- pmin(pi_lo, pi_hi)
pi_upper <- pmax(pi_lo, pi_hi)
}
if (si) {
si_lo <- transf(si_lower)
si_hi <- transf(si_upper)
si_lower <- pmin(si_lo, si_hi)
si_upper <- pmax(si_lo, si_hi)
}
}
if (is.null(xlim)) {
if (mod_type == "continuous") {
xlim <- range(pretty(range(mod_data)))
} else {
xlim <- c(0.5, length(levels(mod_data)) + 0.5)
}
}
if (is.null(ylim)) {
all_y <- c(yi_plot, pred_mean)
if (ci) all_y <- c(all_y, pred_lower, pred_upper)
if (pi) all_y <- c(all_y, pi_lower, pi_upper)
if (si) all_y <- c(all_y, si_lower, si_upper)
ylim <- range(pretty(range(all_y, na.rm = TRUE)))
}
if (is.null(xlab)) xlab <- mod_name
if (is.null(ylab)) ylab <- "Observed Effect Size"
observed_groups <- .regplot_observed_groups(
by_info = by_info,
groups = groups,
n = K
)
df_points <- data.frame(
x = if (mod_type == "continuous") mod_data else as.numeric(mod_data),
y = yi_plot,
size = psize,
group = observed_groups[["group"]],
group_id = observed_groups[["group_id"]],
stringsAsFactors = FALSE
)
if (mod_type == "categorical") {
df_points$level <- mod_data
}
df_pred <- NULL
if (pred) {
df_pred <- data.frame(
x = grid[["x"]],
y = pred_mean,
group = grid[["group"]],
group_id = grid[["group_id"]],
stringsAsFactors = FALSE
)
if (mod_type == "categorical") {
df_pred[["level"]] <- grid[["level"]]
}
}
df_ci <- NULL
if (ci) {
if (mod_type == "continuous") {
df_ci <- do.call(rbind, lapply(seq_along(groups), function(i) {
rows <- grid[["group_id"]] == i
.regplot_band_data_continuous(
xpred = grid[["x"]][rows],
lower = pred_lower[rows],
upper = pred_upper[rows],
group = groups[i],
group_id = i
)
}))
rownames(df_ci) <- NULL
} else {
df_ci <- .regplot_band_data_categorical(grid, pred_mean, pred_lower, pred_upper)
}
}
df_pi <- NULL
if (pi) {
if (mod_type == "continuous") {
df_pi <- do.call(rbind, lapply(seq_along(groups), function(i) {
rows <- grid[["group_id"]] == i
.regplot_band_data_continuous(
xpred = grid[["x"]][rows],
lower = pi_lower[rows],
upper = pi_upper[rows],
group = groups[i],
group_id = i
)
}))
rownames(df_pi) <- NULL
} else {
df_pi <- .regplot_band_data_categorical(grid, pred_mean, pi_lower, pi_upper)
}
}
df_si <- NULL
if (si) {
if (mod_type == "continuous") {
df_si <- do.call(rbind, lapply(seq_along(groups), function(i) {
rows <- grid[["group_id"]] == i
.regplot_band_data_continuous(
xpred = grid[["x"]][rows],
lower = si_lower[rows],
upper = si_upper[rows],
group = groups[i],
group_id = i
)
}))
rownames(df_si) <- NULL
} else {
df_si <- .regplot_band_data_categorical(grid, pred_mean, si_lower, si_upper)
}
}
df_refline <- NULL
if (!is.null(refline)) {
df_refline <- data.frame(y = refline)
}
return(list(
points = df_points,
pred = df_pred,
ci = df_ci,
pi = df_pi,
si = df_si,
refline = df_refline,
xlim = xlim,
ylim = ylim,
xlab = xlab,
ylab = ylab,
mod_type = mod_type,
mod_name = mod_name,
by_name = if (!is.null(by_info)) by_info[["name"]] else NULL,
by_type = if (!is.null(by_info)) by_info[["type"]] else NULL,
groups = groups,
levels = if (mod_type == "categorical") levels(mod_data) else NULL,
sei = se_rep
))
}
# ---------------------------------------------------------------------------- #
# .regplot_mixture_interval_quantiles
# ---------------------------------------------------------------------------- #
#
# Deterministic quantiles of posterior mixtures of normal distributions.
#
# ---------------------------------------------------------------------------- #
.has_native_regplot_mixture <- function() {
return(is.loaded("RoBMA_regplot_normal_mixture_interval", PACKAGE = "RoBMA"))
}
.has_native_regplot_selection_mixture <- function() {
return(is.loaded("RoBMA_regplot_selnorm_mixture_interval", PACKAGE = "RoBMA"))
}
.regplot_mixture_interval_quantiles <- function(mean_samples, sd_samples, probs) {
mean_samples <- as.matrix(mean_samples)
sd_samples <- as.matrix(sd_samples)
if (.has_native_regplot_mixture()) {
return(.Call(
"RoBMA_regplot_normal_mixture_interval",
.native_numeric_matrix(mean_samples),
.native_numeric_matrix(sd_samples),
.native_numeric_vector(probs),
PACKAGE = "RoBMA"
))
}
return(.regplot_mixture_interval_quantiles_r(
mean_samples = mean_samples,
sd_samples = sd_samples,
probs = probs
))
}
.regplot_mixture_interval_quantiles_r <- function(mean_samples, sd_samples, probs) {
lower <- numeric(ncol(mean_samples))
upper <- numeric(ncol(mean_samples))
for (i in seq_len(ncol(mean_samples))) {
cdf_fun <- function(q) {
.regplot_normal_mixture_cdf(
q = q,
mean = mean_samples[, i],
sd = sd_samples[, i]
)
}
lower[i] <- .regplot_mixture_quantile(probs[1], mean_samples[, i], sd_samples[, i], cdf_fun)
upper[i] <- .regplot_mixture_quantile(probs[2], mean_samples[, i], sd_samples[, i], cdf_fun)
}
return(list(lower = lower, upper = upper))
}
# ---------------------------------------------------------------------------- #
# .regplot_selection_mixture_interval_quantiles
# ---------------------------------------------------------------------------- #
#
# Deterministic quantiles of observed-effect mixtures with selection branches.
#
# ---------------------------------------------------------------------------- #
.regplot_selection_mixture_interval_quantiles <- function(x, mean_samples,
sd_samples, se,
probs,
posterior_samples = NULL) {
mean_samples <- as.matrix(mean_samples)
sd_samples <- as.matrix(sd_samples)
setup <- .regplot_selection_setup(
x = x,
posterior_samples = posterior_samples
)
effect_direction <- .effect_direction(x)
if (!is.null(setup[["selection"]]) &&
se > 0 &&
.has_native_regplot_selection_mixture()) {
return(.regplot_selnorm_mixture_interval_quantiles(
mean_samples = mean_samples,
sd_samples = sd_samples,
se = se,
probs = probs,
selection_context = setup[["selection"]]
))
}
return(.regplot_selection_mixture_interval_quantiles_r(
mean_samples = mean_samples,
sd_samples = sd_samples,
se = se,
probs = probs,
setup = setup,
effect_direction = effect_direction
))
}
.regplot_selnorm_mixture_interval_quantiles <- function(mean_samples,
sd_samples, se,
probs,
selection_context) {
.selection_require_step_evaluable(
selection_context,
".regplot_selection_mixture_interval_quantiles()"
)
return(.Call(
"RoBMA_regplot_selnorm_mixture_interval",
.native_numeric_matrix(mean_samples),
.native_numeric_matrix(sd_samples),
.native_numeric_vector(se),
.native_numeric_vector(probs),
.native_numeric_matrix(selection_context[["omega"]]),
.native_numeric_vector(selection_context[["alpha"]]),
.native_integer_vector(selection_context[["phack_kind"]]),
.native_integer_vector(selection_context[["kernel_mode"]]),
.native_numeric_vector(selection_context[["z_lower"]]),
.native_numeric_vector(selection_context[["z_upper"]]),
.native_integer_vector(selection_context[["sign"]]),
.native_integer_vector(selection_context[["phack_q"]]),
.native_numeric_vector(selection_context[["phack_z_source"]]),
.native_numeric_vector(selection_context[["phack_z_dest"]]),
.native_numeric_vector(selection_context[["segments"]][["bounds"]]),
.native_integer_vector(selection_context[["segments"]][["step_bin"]]),
.native_integer_vector(selection_context[["segments"]][["phack_region"]]),
.selection_telescope_probabilities(selection_context),
PACKAGE = "RoBMA"
))
}
.regplot_selection_mixture_interval_quantiles_r <- function(mean_samples,
sd_samples, se,
probs, setup,
effect_direction) {
lower <- numeric(ncol(mean_samples))
upper <- numeric(ncol(mean_samples))
for (i in seq_len(ncol(mean_samples))) {
cdf_fun <- function(q) {
.regplot_selection_mixture_cdf(
q = q,
mean = mean_samples[, i],
sd = sd_samples[, i],
se = se,
setup = setup,
effect_direction = effect_direction
)
}
lower[i] <- .regplot_mixture_quantile(probs[1], mean_samples[, i], sd_samples[, i], cdf_fun)
upper[i] <- .regplot_mixture_quantile(probs[2], mean_samples[, i], sd_samples[, i], cdf_fun)
}
return(list(lower = lower, upper = upper))
}
# ---------------------------------------------------------------------------- #
# .regplot_mixture_quantile
# ---------------------------------------------------------------------------- #
#
# Invert a posterior-mixture CDF.
#
# ---------------------------------------------------------------------------- #
.regplot_mixture_quantile <- function(p, mean, sd, cdf_fun) {
eps_sd <- sqrt(.Machine$double.eps)
if (all(sd < eps_sd)) {
return(unname(stats::quantile(mean, probs = p, names = FALSE, type = 8)))
}
spread <- pmax(sd, eps_sd)
lower <- min(mean - 10 * spread, na.rm = TRUE)
upper <- max(mean + 10 * spread, na.rm = TRUE)
if (!is.finite(lower) || !is.finite(upper)) {
return(NA_real_)
}
if (lower >= upper) {
lower <- lower - 1
upper <- upper + 1
}
obj_fun <- function(q) cdf_fun(q) - p
lower_value <- obj_fun(lower)
upper_value <- obj_fun(upper)
step <- max(spread, na.rm = TRUE)
if (!is.finite(step) || step <= 0) {
step <- max(1, abs(mean), na.rm = TRUE)
}
for (i in seq_len(25)) {
if (lower_value <= 0 && upper_value >= 0) {
break
}
if (lower_value > 0) {
lower <- lower - step
lower_value <- obj_fun(lower)
}
if (upper_value < 0) {
upper <- upper + step
upper_value <- obj_fun(upper)
}
step <- step * 2
}
if (lower_value > 0 || upper_value < 0) {
return(.regplot_grid_quantile(p, lower, upper, cdf_fun))
}
out <- tryCatch(
stats::uniroot(obj_fun, interval = c(lower, upper), tol = 1e-6)[["root"]],
error = function(e) NA_real_
)
if (is.na(out)) {
out <- .regplot_grid_quantile(p, lower, upper, cdf_fun)
}
return(out)
}
# ---------------------------------------------------------------------------- #
# .regplot_normal_mixture_cdf
# ---------------------------------------------------------------------------- #
.regplot_normal_mixture_cdf <- function(q, mean, sd) {
eps_sd <- sqrt(.Machine$double.eps)
cdf_values <- rep(NA_real_, length(mean))
zero_sd <- sd < eps_sd
if (any(zero_sd)) {
cdf_values[zero_sd] <- as.numeric(q >= mean[zero_sd])
}
if (any(!zero_sd)) {
cdf_values[!zero_sd] <- stats::pnorm(
q,
mean = mean[!zero_sd],
sd = sd[!zero_sd]
)
}
cdf_values <- pmin(pmax(cdf_values, 0), 1)
return(base::mean(cdf_values))
}
# ---------------------------------------------------------------------------- #
# .regplot_selection_mixture_cdf
# ---------------------------------------------------------------------------- #
.regplot_selection_mixture_cdf <- function(q, mean, sd, se, setup,
effect_direction) {
eps_sd <- sqrt(.Machine$double.eps)
cdf_values <- rep(NA_real_, length(mean))
zero_sd <- sd < eps_sd
if (any(zero_sd)) {
cdf_values[zero_sd] <- as.numeric(q >= mean[zero_sd])
}
normal_rows <- !setup[["is_weightfunction"]] & !zero_sd
if (any(normal_rows)) {
cdf_values[normal_rows] <- stats::pnorm(
q,
mean = mean[normal_rows],
sd = sd[normal_rows]
)
}
selected_rows <- setup[["is_weightfunction"]] & !zero_sd
if (any(selected_rows)) {
setup[["mu"]] <- mean
rows <- which(selected_rows)
cdf_values[rows] <- .funnel_selected_cdf(
q = q,
rows = rows,
se = se,
total_sd = sd,
setup = setup,
effect_direction = effect_direction
)
}
cdf_values <- pmin(pmax(cdf_values, 0), 1)
return(base::mean(cdf_values))
}
# ---------------------------------------------------------------------------- #
# .regplot_selection_setup
# ---------------------------------------------------------------------------- #
.regplot_selection_setup <- function(x, posterior_samples = NULL) {
if (is.null(posterior_samples)) {
posterior_samples <- .get_posterior_samples(x[["fit"]])
}
S <- nrow(posterior_samples)
bias_indicator <- .extract_bias_indicator(x, posterior_samples = posterior_samples)
selection <- .selection_context(
object = x,
posterior_samples = posterior_samples
)
use_normal <- if (is.null(selection)) {
rep(TRUE, S)
} else {
selection[["use_normal"]]
}
return(list(
bias_indicator = bias_indicator,
is_weightfunction = !use_normal,
selection = selection
))
}
# ---------------------------------------------------------------------------- #
# .regplot_grid_quantile
# ---------------------------------------------------------------------------- #
.regplot_grid_quantile <- function(p, lower, upper, cdf_fun) {
grid <- seq(lower, upper, length.out = 1000)
cdf <- vapply(grid, cdf_fun, numeric(1))
index <- which(cdf >= p)[1]
if (is.na(index)) {
return(grid[length(grid)])
}
return(grid[index])
}
# ---------------------------------------------------------------------------- #
# Plotting helpers
# ---------------------------------------------------------------------------- #
.regplot_has_shade <- function(shade) {
return(isTRUE(shade))
}
.regplot_palette <- function(groups, default_col) {
n <- length(groups)
if (n == 1L) {
return(stats::setNames(default_col[1], groups))
}
if (length(default_col) >= n && length(unique(default_col)) > 1L) {
cols <- default_col[seq_len(n)]
} else {
cols <- grDevices::hcl.colors(n, palette = "Dark 3")
}
return(stats::setNames(cols, groups))
}
.regplot_dodge_x <- function(x, group_id, n_groups, dodge_width) {
if (n_groups <= 1L) {
return(x)
}
offset <- (group_id - (n_groups + 1) / 2) * dodge_width / n_groups
return(x + offset)
}
.regplot_jitter_values <- function(n, amount) {
if (amount <= 0) {
return(rep(0, n))
}
old_seed <- if (exists(".Random.seed", envir = .GlobalEnv)) {
get(".Random.seed", envir = .GlobalEnv)
} else {
NULL
}
on.exit({
if (!is.null(old_seed)) {
assign(".Random.seed", old_seed, envir = .GlobalEnv)
} else if (exists(".Random.seed", envir = .GlobalEnv)) {
rm(".Random.seed", envir = .GlobalEnv)
}
})
set.seed(42)
return(stats::runif(n, -amount, amount))
}
.regplot_band_edges <- function(df_band) {
out <- do.call(rbind, lapply(split(df_band, df_band[["group"]]), function(df_group) {
n <- nrow(df_group) / 2
data.frame(
x = rep(df_group[["x"]][seq_len(n)], 2),
y = c(df_group[["lower"]][seq_len(n)], df_group[["upper"]][seq_len(n)]),
edge = rep(c("lower", "upper"), each = n),
group = rep(df_group[["group"]][1], 2 * n),
group_id = rep(df_group[["group_id"]][1], 2 * n),
stringsAsFactors = FALSE
)
}))
rownames(out) <- NULL
return(out)
}
.regplot_draw_continuous_band_base <- function(df_band, fill_col, alpha,
border_col, shade, lwd) {
if (is.null(df_band)) {
return(invisible(NULL))
}
for (group in unique(df_band[["group"]])) {
df_group <- df_band[df_band[["group"]] == group, , drop = FALSE]
n <- nrow(df_group) / 2
if (.regplot_has_shade(shade)) {
graphics::polygon(
df_group[["x"]],
df_group[["y"]],
col = grDevices::adjustcolor(fill_col[group], alpha.f = alpha),
border = NA
)
}
graphics::lines(
df_group[["x"]][seq_len(n)],
df_group[["lower"]][seq_len(n)],
col = border_col[group],
lwd = max(1, lwd / 2)
)
graphics::lines(
df_group[["x"]][seq_len(n)],
df_group[["upper"]][seq_len(n)],
col = border_col[group],
lwd = max(1, lwd / 2)
)
}
return(invisible(NULL))
}
.regplot_draw_categorical_band_base <- function(df_band, fill_col, alpha,
border_col, shade, width,
n_groups, dodge_width, lwd) {
if (is.null(df_band)) {
return(invisible(NULL))
}
x <- .regplot_dodge_x(df_band[["x"]], df_band[["group_id"]], n_groups, dodge_width)
w <- width / max(1, n_groups)
for (i in seq_len(nrow(df_band))) {
group <- df_band[["group"]][i]
graphics::rect(
xleft = x[i] - w / 2,
ybottom = df_band[["lower"]][i],
xright = x[i] + w / 2,
ytop = df_band[["upper"]][i],
col = if (.regplot_has_shade(shade)) grDevices::adjustcolor(fill_col[group], alpha.f = alpha) else NA,
border = border_col[group],
lwd = max(1, lwd / 2)
)
graphics::segments(
x0 = x[i] - w / 2,
y0 = df_band[["middle"]][i],
x1 = x[i] + w / 2,
y1 = df_band[["middle"]][i],
col = border_col[group],
lwd = max(1, lwd / 2)
)
}
return(invisible(NULL))
}
.regplot_add_continuous_band_ggplot <- function(out, df_band, fill_col, alpha,
border_col, shade, has_by) {
if (is.null(df_band)) {
return(out)
}
if (.regplot_has_shade(shade)) {
if (has_by) {
out <- out +
ggplot2::geom_polygon(
data = df_band,
mapping = ggplot2::aes(
x = .data[["x"]],
y = .data[["y"]],
group = .data[["group"]],
fill = .data[["group"]]
),
alpha = alpha
)
} else {
out <- out +
ggplot2::geom_polygon(
data = df_band,
mapping = ggplot2::aes(
x = .data[["x"]],
y = .data[["y"]],
group = .data[["group"]]
),
fill = fill_col[1],
alpha = alpha
)
}
}
df_edges <- .regplot_band_edges(df_band)
if (has_by) {
out <- out +
ggplot2::geom_line(
data = df_edges,
mapping = ggplot2::aes(
x = .data[["x"]],
y = .data[["y"]],
group = interaction(.data[["group"]], .data[["edge"]]),
colour = .data[["group"]]
),
linewidth = 0.35
)
} else {
out <- out +
ggplot2::geom_line(
data = df_edges,
mapping = ggplot2::aes(
x = .data[["x"]],
y = .data[["y"]],
group = .data[["edge"]]
),
colour = border_col[1],
linewidth = 0.35
)
}
return(out)
}
.regplot_add_categorical_band_ggplot <- function(out, df_band, fill_col, alpha,
border_col, shade, has_by,
width, n_groups, dodge_width) {
if (is.null(df_band)) {
return(out)
}
df_band[["x_plot"]] <- .regplot_dodge_x(
df_band[["x"]],
df_band[["group_id"]],
n_groups,
dodge_width
)
width <- width / max(1, n_groups)
if (has_by && .regplot_has_shade(shade)) {
out <- out +
ggplot2::geom_crossbar(
data = df_band,
mapping = ggplot2::aes(
x = .data[["x_plot"]],
y = .data[["middle"]],
ymin = .data[["lower"]],
ymax = .data[["upper"]],
colour = .data[["group"]],
fill = .data[["group"]]
),
width = width,
alpha = alpha,
linewidth = 0.35
)
} else if (has_by) {
out <- out +
ggplot2::geom_crossbar(
data = df_band,
mapping = ggplot2::aes(
x = .data[["x_plot"]],
y = .data[["middle"]],
ymin = .data[["lower"]],
ymax = .data[["upper"]],
colour = .data[["group"]]
),
width = width,
fill = NA,
alpha = 1,
linewidth = 0.35
)
} else {
out <- out +
ggplot2::geom_crossbar(
data = df_band,
mapping = ggplot2::aes(
x = .data[["x_plot"]],
y = .data[["middle"]],
ymin = .data[["lower"]],
ymax = .data[["upper"]]
),
width = width,
colour = border_col[1],
fill = if (.regplot_has_shade(shade)) fill_col[1] else NA,
alpha = if (.regplot_has_shade(shade)) alpha else 1,
linewidth = 0.35
)
}
return(out)
}
# ---------------------------------------------------------------------------- #
# .regplot_plot_base
# ---------------------------------------------------------------------------- #
#
# Create regression plot using base R graphics.
#
# @param data list of plot data from .regplot_data
# @param dots list of graphical parameters
# @param legend logical; show legend
#
# @return NULL invisibly
#
# ---------------------------------------------------------------------------- #
.regplot_plot_base <- function(data, dots, legend = TRUE) {
pch <- dots[["pch"]]
col <- dots[["col"]]
bg <- dots[["bg"]]
lcol <- dots[["lcol"]]
lwd <- dots[["lwd"]]
col_ci <- dots[["col.ci"]]
col_pi <- dots[["col.pi"]]
col_si <- dots[["col.si"]]
alpha_ci <- dots[["alpha.ci"]]
alpha_pi <- dots[["alpha.pi"]]
alpha_si <- dots[["alpha.si"]]
shade <- dots[["shade"]]
main <- dots[["main"]]
jitter_am <- dots[["jitter"]]
box_width <- dots[["box.width"]]
dodge_w <- dots[["dodge.width"]]
las <- dots[["las"]]
df_points <- data$points
df_pred <- data$pred
df_ci <- data$ci
df_pi <- data$pi
df_si <- data$si
df_refline <- data$refline
mod_type <- data$mod_type
groups <- data$groups
n_groups <- length(groups)
has_by <- n_groups > 1L
line_cols <- .regplot_palette(groups, lcol)
point_cols <- if (has_by) line_cols else stats::setNames(rep(col, n_groups), groups)
point_bgs <- if (has_by) {
stats::setNames(grDevices::adjustcolor(line_cols, alpha.f = 0.45), groups)
} else {
stats::setNames(rep(bg, n_groups), groups)
}
if (has_by) {
ci_cols <- pi_cols <- si_cols <- line_cols
} else {
ci_cols <- stats::setNames(rep(col_ci, n_groups), groups)
pi_cols <- stats::setNames(rep(col_pi, n_groups), groups)
si_cols <- stats::setNames(rep(col_si, n_groups), groups)
}
if (mod_type == "categorical") {
graphics::plot(
NA, NA,
xlim = data$xlim,
ylim = data$ylim,
xlab = data$xlab,
ylab = data$ylab,
main = main,
type = "n",
xaxt = "n",
las = las
)
graphics::axis(
1,
at = seq_along(data$levels),
labels = data$levels,
las = las
)
} else {
graphics::plot(
NA, NA,
xlim = data$xlim,
ylim = data$ylim,
xlab = data$xlab,
ylab = data$ylab,
main = main,
type = "n",
las = las
)
}
if (!is.null(df_refline)) {
graphics::abline(h = df_refline$y, lty = "dashed", col = "gray50")
}
if (!is.null(df_si)) {
if (mod_type == "continuous") {
.regplot_draw_continuous_band_base(df_si, si_cols, alpha_si, si_cols, shade, lwd)
} else {
.regplot_draw_categorical_band_base(
df_si, si_cols, alpha_si, si_cols, shade,
box_width * 1.4, n_groups, dodge_w, lwd
)
}
}
if (!is.null(df_pi)) {
if (mod_type == "continuous") {
.regplot_draw_continuous_band_base(df_pi, pi_cols, alpha_pi, pi_cols, shade, lwd)
} else {
.regplot_draw_categorical_band_base(
df_pi, pi_cols, alpha_pi, pi_cols, shade,
box_width * 1.15, n_groups, dodge_w, lwd
)
}
}
if (!is.null(df_ci)) {
if (mod_type == "continuous") {
.regplot_draw_continuous_band_base(df_ci, ci_cols, alpha_ci, ci_cols, shade, lwd)
} else {
.regplot_draw_categorical_band_base(
df_ci, ci_cols, alpha_ci, ci_cols, shade,
box_width, n_groups, dodge_w, lwd
)
}
}
if (!is.null(df_pred)) {
if (mod_type == "continuous") {
for (group in groups) {
df_group <- df_pred[df_pred[["group"]] == group, , drop = FALSE]
graphics::lines(df_group$x, df_group$y, col = line_cols[group], lwd = lwd)
}
} else {
x_pred <- .regplot_dodge_x(df_pred$x, df_pred$group_id, n_groups, dodge_w)
graphics::points(
x_pred,
df_pred$y,
pch = 18,
col = line_cols[df_pred$group],
cex = 1.5
)
}
}
if (mod_type == "categorical") {
x_points <- .regplot_dodge_x(df_points$x, df_points$group_id, n_groups, dodge_w)
x_points <- x_points + .regplot_jitter_values(
n = nrow(df_points),
amount = jitter_am / max(1, n_groups)
)
graphics::points(
x_points,
df_points$y,
pch = pch,
col = point_cols[df_points$group],
bg = point_bgs[df_points$group],
cex = df_points$size
)
} else {
graphics::points(
df_points$x,
df_points$y,
pch = pch,
col = point_cols[df_points$group],
bg = point_bgs[df_points$group],
cex = df_points$size
)
}
if (has_by && legend) {
graphics::legend(
"topright",
legend = groups,
col = line_cols,
pt.bg = point_bgs,
pch = 21,
lty = 1,
lwd = lwd,
bty = "n"
)
}
return(invisible(NULL))
}
# ---------------------------------------------------------------------------- #
# .regplot_plot_ggplot
# ---------------------------------------------------------------------------- #
#
# Create regression plot using ggplot2.
#
# @param data list of plot data from .regplot_data
# @param dots list of graphical parameters
# @param legend logical; show legend
#
# @return ggplot object
#
# ---------------------------------------------------------------------------- #
.regplot_plot_ggplot <- function(data, dots, legend = TRUE) {
pch <- dots[["pch"]]
col <- dots[["col"]]
bg <- dots[["bg"]]
lcol <- dots[["lcol"]]
lwd <- dots[["lwd"]]
col_ci <- dots[["col.ci"]]
col_pi <- dots[["col.pi"]]
col_si <- dots[["col.si"]]
alpha_ci <- dots[["alpha.ci"]]
alpha_pi <- dots[["alpha.pi"]]
alpha_si <- dots[["alpha.si"]]
shade <- dots[["shade"]]
main <- dots[["main"]]
jitter_am <- dots[["jitter"]]
box_width <- dots[["box.width"]]
dodge_w <- dots[["dodge.width"]]
df_points <- data$points
df_pred <- data$pred
df_ci <- data$ci
df_pi <- data$pi
df_si <- data$si
df_refline <- data$refline
mod_type <- data$mod_type
groups <- data$groups
n_groups <- length(groups)
has_by <- n_groups > 1L
line_cols <- .regplot_palette(groups, lcol)
if (has_by) {
ci_cols <- pi_cols <- si_cols <- line_cols
} else {
ci_cols <- stats::setNames(rep(col_ci, n_groups), groups)
pi_cols <- stats::setNames(rep(col_pi, n_groups), groups)
si_cols <- stats::setNames(rep(col_si, n_groups), groups)
}
out <- ggplot2::ggplot()
if (!is.null(df_refline)) {
out <- out +
ggplot2::geom_hline(
yintercept = df_refline$y,
linetype = "dashed",
colour = "gray50"
)
}
if (!is.null(df_si)) {
if (mod_type == "continuous") {
out <- .regplot_add_continuous_band_ggplot(out, df_si, si_cols, alpha_si, si_cols, shade, has_by)
} else {
out <- .regplot_add_categorical_band_ggplot(
out, df_si, si_cols, alpha_si, si_cols, shade, has_by,
box_width * 1.4, n_groups, dodge_w
)
}
}
if (!is.null(df_pi)) {
if (mod_type == "continuous") {
out <- .regplot_add_continuous_band_ggplot(out, df_pi, pi_cols, alpha_pi, pi_cols, shade, has_by)
} else {
out <- .regplot_add_categorical_band_ggplot(
out, df_pi, pi_cols, alpha_pi, pi_cols, shade, has_by,
box_width * 1.15, n_groups, dodge_w
)
}
}
if (!is.null(df_ci)) {
if (mod_type == "continuous") {
out <- .regplot_add_continuous_band_ggplot(out, df_ci, ci_cols, alpha_ci, ci_cols, shade, has_by)
} else {
out <- .regplot_add_categorical_band_ggplot(
out, df_ci, ci_cols, alpha_ci, ci_cols, shade, has_by,
box_width, n_groups, dodge_w
)
}
}
if (!is.null(df_pred)) {
if (mod_type == "continuous") {
if (has_by) {
out <- out +
ggplot2::geom_line(
data = df_pred,
mapping = ggplot2::aes(
x = .data[["x"]],
y = .data[["y"]],
colour = .data[["group"]],
group = .data[["group"]]
),
linewidth = lwd / 2
)
} else {
out <- out +
ggplot2::geom_line(
data = df_pred,
mapping = ggplot2::aes(x = .data[["x"]], y = .data[["y"]]),
colour = lcol[1],
linewidth = lwd / 2
)
}
} else {
df_pred[["x_plot"]] <- .regplot_dodge_x(df_pred$x, df_pred$group_id, n_groups, dodge_w)
if (has_by) {
out <- out +
ggplot2::geom_point(
data = df_pred,
mapping = ggplot2::aes(
x = .data[["x_plot"]],
y = .data[["y"]],
colour = .data[["group"]]
),
shape = 18,
size = 4
)
} else {
out <- out +
ggplot2::geom_point(
data = df_pred,
mapping = ggplot2::aes(x = .data[["x_plot"]], y = .data[["y"]]),
shape = 18,
colour = lcol[1],
size = 4
)
}
}
}
if (mod_type == "categorical") {
df_points[["x_plot"]] <- .regplot_dodge_x(df_points$x, df_points$group_id, n_groups, dodge_w)
df_points[["x_plot"]] <- df_points[["x_plot"]] + .regplot_jitter_values(
n = nrow(df_points),
amount = jitter_am / max(1, n_groups)
)
} else {
df_points[["x_plot"]] <- df_points[["x"]]
}
if (has_by) {
out <- out +
ggplot2::geom_point(
data = df_points,
mapping = ggplot2::aes(
x = .data[["x_plot"]],
y = .data[["y"]],
size = .data[["size"]],
colour = .data[["group"]],
fill = .data[["group"]]
),
shape = pch
)
} else {
out <- out +
ggplot2::geom_point(
data = df_points,
mapping = ggplot2::aes(
x = .data[["x_plot"]],
y = .data[["y"]],
size = .data[["size"]]
),
shape = pch,
colour = col,
fill = bg
)
}
out <- out +
ggplot2::scale_size_identity()
if (mod_type == "categorical") {
out <- out +
ggplot2::scale_x_continuous(
breaks = seq_along(data$levels),
labels = data$levels,
limits = data$xlim,
name = data$xlab
)
} else {
out <- out +
ggplot2::scale_x_continuous(
limits = data$xlim,
name = data$xlab
)
}
out <- out +
ggplot2::scale_y_continuous(
limits = data$ylim,
name = data$ylab
)
if (has_by) {
out <- out +
ggplot2::scale_colour_manual(values = line_cols, name = data$by_name) +
ggplot2::scale_fill_manual(values = line_cols, name = data$by_name)
}
if (!is.null(main)) {
out <- out + ggplot2::ggtitle(main)
}
out <- out + ggplot2::guides(size = "none")
if (has_by && !legend) {
out <- out + ggplot2::guides(colour = "none", fill = "none")
}
return(out)
}
# ---------------------------------------------------------------------------- #
# .set_dots_regplot
# ---------------------------------------------------------------------------- #
#
# Process dots arguments for regression plot with sensible defaults.
#
# @param ... additional graphical arguments
#
# @return list of graphical parameters with defaults applied
#
# ---------------------------------------------------------------------------- #
.set_dots_regplot <- function(...) {
dots <- list(...)
# point styling
dots <- .plot_point_style_defaults(dots)
# line styling
if (is.null(dots[["lcol"]])) dots[["lcol"]] <- "black"
if (is.null(dots[["lwd"]])) dots[["lwd"]] <- 2
.check_plot_positive_scalar(dots[["lwd"]], "lwd")
# band styling
if (is.null(dots[["shade"]])) dots[["shade"]] <- TRUE
if (is.null(dots[["col.ci"]])) dots[["col.ci"]] <- "gray70"
if (is.null(dots[["col.pi"]])) dots[["col.pi"]] <- "gray85"
if (is.null(dots[["col.si"]])) dots[["col.si"]] <- "gray92"
if (is.null(dots[["alpha.ci"]])) dots[["alpha.ci"]] <- 0.4
if (is.null(dots[["alpha.pi"]])) dots[["alpha.pi"]] <- 0.2
if (is.null(dots[["alpha.si"]])) dots[["alpha.si"]] <- 0.15
BayesTools::check_bool(dots[["shade"]], "shade")
.check_plot_numeric(dots[["alpha.ci"]], "alpha.ci", check_length = 1, lower = 0, upper = 1, allow_null = FALSE)
.check_plot_numeric(dots[["alpha.pi"]], "alpha.pi", check_length = 1, lower = 0, upper = 1, allow_null = FALSE)
.check_plot_numeric(dots[["alpha.si"]], "alpha.si", check_length = 1, lower = 0, upper = 1, allow_null = FALSE)
# categorical moderator jitter
if (is.null(dots[["jitter"]])) dots[["jitter"]] <- 0.2
if (is.null(dots[["box.width"]])) dots[["box.width"]] <- 0.5
if (is.null(dots[["dodge.width"]])) dots[["dodge.width"]] <- 0.75
.check_plot_numeric(dots[["jitter"]], "jitter", check_length = 1, lower = 0, allow_null = FALSE)
.check_plot_positive_scalar(dots[["box.width"]], "box.width")
.check_plot_numeric(dots[["dodge.width"]], "dodge.width", check_length = 1, lower = 0, allow_null = FALSE)
# title (NULL = no title by default)
if (is.null(dots[["main"]])) dots[["main"]] <- NULL
if (is.null(dots[["las"]])) dots[["las"]] <- 1
.check_plot_label(dots[["main"]], "main")
BayesTools::check_int(dots[["las"]], "las", lower = 0)
if (!dots[["las"]] %in% 0:3) {
stop("'las' must be one of 0, 1, 2, or 3.", call. = FALSE)
}
.check_plot_positive_scalar(dots[["cex"]], "cex")
.check_plot_positive_scalar(dots[["size"]], "size")
return(dots)
}
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.