Nothing
..required_plotting_packages <- function(extended = FALSE) {
plot_packages <- c("ggplot2", "labeling", "scales", "rlang")
if (extended) plot_packages <- c(plot_packages, "gtable", "ragg")
return(plot_packages)
}
#' Familiar ggplot2 theme
#'
#' This is the default theme used for plots created by familiar. The theme uses
#' `ggplot2::theme_light` as the base template.
#'
#' @param base_size Base font size in points. Size of other plot text elements
#' is based off this.
#' @param base_family Font family used for text elements.
#' @param base_line_size Base size for line elements, in points.
#' @param base_rect_size Base size for rectangular elements, in points.
#'
#' @return A complete plotting theme.
#' @export
theme_familiar <- function(
base_size = 10.0,
base_family = "",
base_line_size = 0.5,
base_rect_size = 0.5
) {
# The default familiar theme is based on ggplot2::theme_light.
ggtheme <- ggplot2::theme_light(
base_size = base_size,
base_family = base_family,
base_line_size = base_line_size,
base_rect_size = base_rect_size
)
# Set colour to black.
ggtheme$axis.text$colour <- "black"
ggtheme$axis.ticks$colour <- "black"
ggtheme$axis.line <- ggplot2::element_line(
colour = "black",
lineend = "square"
)
# Legend does not have a legend or border, and reserves less space.
ggtheme$legend.background <- ggplot2::element_blank()
ggtheme$legend.key <- ggplot2::element_blank()
ggtheme$legend.key.size <- grid::unit(base_size * 1.1, "pt")
ggtheme$legend.justification <- c("left", "center")
ggtheme$legend.margin <- ggplot2::margin(0.0, 0.0, 0.0, 0.0, "pt")
# Panel does not have a background, grid, or border.
ggtheme$panel.background <- ggplot2::element_blank()
ggtheme$panel.border <- ggplot2::element_blank()
ggtheme$panel.grid <- ggplot2::element_blank()
# Avoid removing some elements altogether by directly assigning NULL.
ggtheme["panel.grid.major"] <- list(NULL)
ggtheme["panel.grid.minor"] <- list(NULL)
# Minimal plot margins
ggtheme$plot.margin <- ggplot2::margin(1.0, 1.0, 1.0, 1.0, "pt")
# The plot does not have a background
ggtheme$plot.background <- ggplot2::element_blank()
# Make title a bit smaller, and bold.
ggtheme$plot.title <- ggplot2::element_text(
face = "bold",
size = ggplot2::rel(1.1),
hjust = 0.0,
vjust = 1.0,
margin = ggplot2::margin(b = base_size / 2.0)
)
# Make subtitle a bit smaller.
ggtheme$plot.subtitle <- ggplot2::element_text(
size = ggplot2::rel(0.8),
hjust = 0.0,
vjust = 1.0,
margin = ggplot2::margin(b = base_size / 2.0)
)
# Make caption a bit smaller.
ggtheme$plot.caption <- ggplot2::element_text(
size = ggplot2::rel(0.7),
hjust = 1.0,
vjust = 1.0,
margin = ggplot2::margin(b = base_size / 2.0)
)
# Make tag bold.
ggtheme$plot.tag <- ggplot2::element_text(
face = "bold",
hjust = 0.0,
vjust = 0.7
)
# Make strip text black.
ggtheme$strip.text <- ggplot2::element_text(
size = ggplot2::rel(0.8),
colour = "grey10",
margin = ggplot2::margin(
t = base_size / 4.0,
r = base_size / 4.0,
b = base_size / 4.0,
l = base_size / 4.0,
unit = "pt"
)
)
# Remove strip background
ggtheme$strip.background <- ggplot2::element_blank()
return(ggtheme)
}
.check_ggtheme <- function(ggtheme) {
# Check if the provided theme is a suitable theme.
if (inherits(ggtheme, "theme")) {
# Check if the theme is complete.
if (!attr(ggtheme, "complete")) {
..error(paste0(
"The plotting theme is not complete. The most likely cause is lack ",
"of a valid template, such as theme_familiar or ggplot2::theme_light. ",
"Note that ggplot2::theme is designed to tweak existing themes when creating a plot."
))
}
} else if (is.null(ggtheme)) {
ggtheme <- theme_familiar(base_size = 9.0)
} else {
# Get the specified theme.
ggtheme_fun <- switch(
ggtheme,
"default" = theme_familiar,
"theme_familiar" = theme_familiar,
"theme_gray" = ggplot2::theme_gray,
"theme_grey" = ggplot2::theme_grey,
"theme_bw" = ggplot2::theme_bw,
"theme_linedraw" = ggplot2::theme_linedraw,
"theme_light" = ggplot2::theme_light,
"theme_dark" = ggplot2::theme_dark,
"theme_minimal" = ggplot2::theme_minimal,
"theme_classic" = ggplot2::theme_classic
)
if (is.null(ggtheme_fun)) {
..error(paste0(
"The selected theme is not the default theme, or a standard ggplot2 theme. Found: ", ggtheme
))
}
# Generate theme
ggtheme <- ggtheme_fun(base_size = 9.0)
}
return(ggtheme)
}
#' Checks and sanitizes splitting variables for plotting.
#'
#' @param x data.table or data.frame containing the data used for splitting.
#' @param split_by (*optional*) Splitting variables. This refers to column names
#' on which datasets are split. A separate figure is created for each split.
#' See details for available variables.
#' @param color_by (*optional*) Variables used to determine fill colour of plot
#' objects. The variables cannot overlap with those provided to the `split_by`
#' argument, but may overlap with other arguments. See details for available
#' variables.
#' @param linetype_by (*optional*) Variables that are used to determine the
#' linetype of lines in a plot. The variables cannot overlap with those
#' provided to the `split_by` argument, but may overlap with other arguments.
#' Sett details for available variables.
#' @param facet_by (*optional*) Variables used to determine how and if facets of
#' each figure appear. In case the `facet_wrap_cols` argument is `NULL`, the
#' first variable is used to define columns, and the remaing variables are
#' used to define rows of facets. The variables cannot overlap with those
#' provided to the `split_by` argument, but may overlap with other arguments.
#' See details for available variables.
#' @param x_axis_by (*optional*) Variable plotted along the x-axis of a plot.
#' The variable cannot overlap with variables provided to the `split_by` and
#' `y_axis_by` arguments (if used), but may overlap with other arguments. Only
#' one variable is allowed for this argument. See details for available
#' variables.
#' @param y_axis_by (*optional*) Variable plotted along the y-axis of a plot.
#' The variable cannot overlap with variables provided to the `split_by` and
#' `x_axis_by` arguments (if used), but may overlap with other arguments. Only
#' one variable is allowed for this argument. See details for available
#' variables.
#' @param available Names of columns available for splitting.
#'
#' @details This internal function allows some flexibility regarding the exact
#' input. Allowed splitting variables should be defined by the available
#' argument.
#'
#' @return A sanitized list of splitting variables.
#' @md
#' @keywords internal
.check_plot_splitting_variables <- function(
x,
split_by = NULL,
color_by = NULL,
linetype_by = NULL,
facet_by = NULL,
x_axis_by = NULL,
y_axis_by = NULL,
available = NULL
) {
# Find unique variables
splitting_vars <- c(split_by, color_by, linetype_by, facet_by, x_axis_by, y_axis_by)
if (is.null(available) && length(splitting_vars) == 0L) {
return(list())
} else if (is.null(available) && length(splitting_vars) > 0L) {
..error(paste0(
"The current plot has no required splitting variables defined, but ",
paste_s(splitting_vars),
ifelse(length(splitting_vars) == 1L, " was assigned.", " were assigned.")
))
}
# Filter available down to those present in the data.
filter_available <- intersect(available, colnames(x))
# Filter available down to those that have more than one variable
filter_available <- filter_available[sapply(
filter_available,
function(ii, x) (data.table::uniqueN(x = x, by = ii) > 1L),
x = x
)]
if (is.null(filter_available)) {
return(list())
} else if (!all(filter_available %in% splitting_vars)) {
missing_vars <- filter_available[!filter_available %in% splitting_vars]
..error(paste0(
"The current plot requires ",
paste_s(filter_available),
ifelse(length(filter_available) > 1L, " as splitting variables", " as a splitting_variable"),
", but ",
paste_s(missing_vars),
ifelse(length(missing_vars) == 1L, " was not assigned.", " were not assigned.")
))
}
# Update available
available <- filter_available
# Generate output
output_list <- list()
# Update split_by
if (!is.null(split_by) && any(split_by %in% available)) {
output_list$split_by <- intersect(split_by, available)
}
# Update color_by
if (!is.null(color_by) && any(color_by %in% available)) {
output_list$color_by <- intersect(color_by, available)
}
# update linetype_by
if (!is.null(linetype_by) && any(linetype_by %in% available)) {
output_list$linetype_by <- intersect(linetype_by, available)
}
# update facet_by
if (!is.null(facet_by) && any(facet_by %in% available)) {
output_list$facet_by <- intersect(facet_by, available)
}
# update x_axis_by
if (!is.null(x_axis_by) && any(x_axis_by %in% available)) {
output_list$x_axis_by <- intersect(x_axis_by, available)
}
# update y_axis_by
if (!is.null(y_axis_by) && any(y_axis_by %in% available)) {
output_list$y_axis_by <- intersect(y_axis_by, available)
}
# Check split_by variable.
.check_value_not_shared(output_list$split_by, output_list$color_by, "split_by", "color_by")
.check_value_not_shared(output_list$split_by, output_list$linetype_by, "split_by", "linetype_by")
.check_value_not_shared(output_list$split_by, output_list$facet_by, "split_by", "facet_by")
.check_value_not_shared(output_list$split_by, output_list$x_axis_by, "split_by", "x_axis_by")
.check_value_not_shared(output_list$split_by, output_list$y_axis_by, "split_by", "y_axis_by")
# Check x_axis_by variable and y_axis_by variables.
.check_value_not_shared(output_list$x_axis_by, output_list$y_axis_by, "x_axis_by", "y_axis_by")
# Check length of x_axis_by and y_axis_by variables.
.check_argument_length(output_list$x_axis_by, "x_axis_by", min = 0L, max = 1L)
.check_argument_length(output_list$y_axis_by, "y_axis_by", min = 0L, max = 1L)
return(output_list)
}
.parse_plot_facet_by <- function(x, facet_by, facet_wrap_cols) {
if (is.null(facet_by)) {
return(list())
} else if (length(facet_by) == 1L) {
if (is.null(facet_wrap_cols)) {
return(list("facet_cols" = quos(!!ensym(facet_by))))
} else {
return(list("facet_by" = quos(!!ensym(facet_by))))
}
} else {
if (is.null(facet_wrap_cols)) {
facet_col <- facet_by[1L]
facet_rows <- facet_by[2L:length(facet_by)]
return(list(
"facet_cols" = quos(!!ensym(facet_col)),
"facet_rows" = quos(!!!parse_exprs(facet_rows))
))
} else {
return(list("facet_by" = quos(!!!parse_exprs(facet_by))))
}
}
}
.create_plot_subtitle <- function(
x,
split_by = NULL,
additional = NULL
) {
# Do not create a subtitle if there is no subtitle to be created.
subtitle <- NULL
# Generate subtitle from splitting variables and data.
if (!is.null(split_by)) {
subtitle <- c(
subtitle,
sapply(
split_by,
function(name, x) {
split_variable_name <- name
if (split_variable_name == "vimp_method") {
split_variable_name <- "VIMP method"
} else if (split_variable_name == "learner") {
split_variable_name <- "learner"
} else if (split_variable_name == "data_set") {
split_variable_name <- "data set"
} else if (split_variable_name == "evaluation_time") {
split_variable_name <- "time point"
} else if (split_variable_name == "sample_id") {
split_variable_name <- "sample"
} else if (split_variable_name %in% c("feature_name", "feature")) {
split_variable_name <- "feature"
}
# Remove all underscores.
split_variable_name <- gsub(
x = split_variable_name,
pattern = "_",
replacement = " ",
fixed = TRUE
)
# Parse to an elementary string.
split_variable_name <- paste0(split_variable_name, ": ", x[[name]][1L])
return(split_variable_name)
},
x = x
)
)
}
# Generate additional strings from additional.
if (!is.null(additional)) {
subtitle <- c(
subtitle,
mapply(
function(name, value) {
# Remove all underscores.
split_variable_name <- gsub(
x = name,
pattern = "_",
replacement = " ",
fixed = TRUE
)
# Parse to an elementary string.
split_variable_name <- paste0(split_variable_name, ": ", value)
},
name = names(additional),
value = additional
)
)
}
# Check if any subtitle was generated.
if (is.null(subtitle)) return(NULL)
# Combine into single string.
subtitle <- paste_s(subtitle)
return(subtitle)
}
.add_time_to_plot_subtitle <- function(value) {
return(list("time point" = value))
}
.create_plot_subtype <- function(
x,
subtype = NULL,
split_by = NULL,
additional = NULL
) {
# Generate additional terms for the subtype, based on splits.
if (!is.null(split_by)) {
subtype <- c(
subtype,
as.character(sapply(
split_by,
function(jj, x) (x[[jj]][1L]),
x = x
))
)
}
if (!is.null(additional)) {
subtype <- c(
subtype,
sapply(additional, function(jj) as.character(jj[1L]))
)
}
if (is.null(subtype)) return(NULL)
# Combine into a single string.
subtype <- paste0(subtype, collapse = "_")
return(subtype)
}
.create_plot_legend_title <- function(
user_label,
color_by = NULL,
linetype_by = NULL,
combine_legend = FALSE
) {
# Sent for inspection
.check_input_plot_args(
legend_label = user_label,
combine_legend = combine_legend
)
if (is.null(color_by) && is.null(linetype_by)) {
# No splitting variables are used
return(list(
"guide_color" = NULL,
"guide_linetype" = NULL
))
}
# Collect required list entries
req_entries <- character(0L)
if (!is.null(color_by)) {
req_entries <- c(req_entries, "guide_color")
}
if (!is.null(linetype_by)) {
req_entries <- c(req_entries, "guide_linetype")
}
# Waiver: no user input
if (is.waive(user_label)) {
if (combine_legend) {
legend_label <- gsub(
x = paste0(unique(c(color_by, linetype_by)), collapse = " & "),
pattern = "_",
replacement = " ",
fixed = TRUE
)
return(list(
"guide_color" = legend_label,
"guide_linetype" = legend_label
))
} else {
# Colour labels
if (!is.null(color_by)) {
color_guide_label <- gsub(
x = paste0(color_by, collapse = " & "),
pattern = "_",
replacement = " ",
fixed = TRUE
)
} else {
color_guide_label <- NULL
}
# Linetype labels
if (!is.null(linetype_by)) {
linetype_guide_label <- gsub(
x = paste0(linetype_by, collapse = " & "),
pattern = "_",
replacement = " ",
fixed = TRUE
)
} else {
linetype_guide_label <- NULL
}
return(list(
"guide_color" = color_guide_label,
"guide_linetype" = linetype_guide_label
))
}
} else if (is.null(user_label)) {
# NULL input
return(list(
"guide_color" = NULL,
"guide_linetype" = NULL
))
} else if (is.list(user_label)) {
# List input
# Check entries for existence
for (current_entry in req_entries) {
if (!current_entry %in% names(user_label)) {
..error(paste0(
"A legend name is missing for ", current_entry,
". Please set this name to a \"", current_entry,
"\" list element, e.g. list(\"", current_entry,
"\"=\"some name\", ...)."
))
}
}
# Select required entries
user_label <- user_label[names(user_label) %in% req_entries]
# Check that all entries are the same
if (combine_legend && length(req_entries) >= 2L) {
if (!all(sapply(
user_label[2L:length(user_label)],
identical,
user_label[[1L]]
))) {
..error(paste0(
"Not all provided legend names are identical, but identical legend ",
"names are required for combining the legend."
))
}
}
return(user_label)
} else if (length(req_entries) >= 2L && !combine_legend) {
# Single input where multiple is required
..error(paste0(
"Multiple legend names are required, but only one is provided. ",
"Please return a list with ",
paste0("\"", req_entries, "\"", collapse = ", "), " elements."
))
} else {
# Single input
return(list(
"guide_color" = user_label,
"guide_linetype" = user_label
))
}
}
.create_plot_guide_table <- function(
x,
color_by = NULL,
linetype_by = NULL,
discrete_palette = NULL,
combine_legend = TRUE
) {
.get_guide_tables <- function(x, color_by, linetype_by, discrete_palette) {
# Suppress NOTES due to non-standard evaluation in data.table
color_id <- linetype_id <- NULL
# Select unique variables
unique_vars <- unique(c(color_by, linetype_by))
# Check whether there are any unique splitting variables
if (is.null(unique_vars)) return(NULL)
# Generate a guide table
guide_table <- data.table::data.table(expand.grid(lapply(
rev(unique_vars),
function(ii, x) (levels(x[[ii]])),
x = x
)))
# Rename variables
data.table::setnames(x = guide_table, rev(unique_vars))
# Convert to factors
for (ii in unique_vars) {
guide_table[[ii]] <- factor(
x = guide_table[[ii]],
levels = levels(x[[ii]])
)
}
# Order columns according to unique_vars
data.table::setcolorder(
x = guide_table,
neworder = unique_vars
)
# Order data set by columns
data.table::setorderv(
x = guide_table,
cols = unique_vars
)
# Set breaks
breaks <- apply(guide_table, 1L, paste, collapse = ", ")
# Extend guide table
if (!is.null(color_by)) {
# Generate breaks
guide_table$color_breaks <- factor(
x = breaks,
levels = breaks
)
# Define colour groups
guide_table[, "color_id" := .GRP, by = color_by]
# Get the palette to use.
discr_palette <- .get_palette(
x = discrete_palette,
n = max(guide_table$color_id),
palette_type = "qualitative"
)
# Assign colour values
guide_table[, "color_values" := discr_palette[color_id]]
}
if (!is.null(linetype_by)) {
# Generate breaks
guide_table$linetype_breaks <- factor(
x = breaks,
levels = breaks
)
# Define linetype groups
guide_table[, "linetype_id" := .GRP, by = linetype_by]
# Get the palette to use
line_palette <- scales::linetype_pal()(max(guide_table$linetype_id))
# Assign linetypes
guide_table[, "linetype_values" := line_palette[linetype_id]]
}
return(guide_table)
}
if (is_empty(x)) return(list("data" = x))
# Extract guide tables
if (combine_legend) {
guide_list <- list(
"guide_color" = .get_guide_tables(
x = x,
color_by = color_by,
linetype_by = linetype_by,
discrete_palette = discrete_palette
),
"guide_linetype" = .get_guide_tables(
x = x,
color_by = color_by,
linetype_by = linetype_by,
discrete_palette = discrete_palette
)
)
} else {
guide_list <- list(
"guide_color" = .get_guide_tables(
x = x,
color_by = color_by,
linetype_by = NULL,
discrete_palette = discrete_palette
),
"guide_linetype" = .get_guide_tables(
x = x,
color_by = NULL,
linetype_by = linetype_by,
discrete_palette = discrete_palette
)
)
}
# Filter out lists corresponding to missing split variables
guide_list <- guide_list[!sapply(list(color_by, linetype_by), is.null)]
if (length(guide_list) == 0L) return(list("data" = x))
# Initialise return list
return_list <- list()
# Add break column of the remaining lists to x
for (guide_type in names(guide_list)) {
if (guide_type == "guide_color") {
# Add color_breaks to x
if (combine_legend) {
x <- merge(
x = x,
y = guide_list[[guide_type]][, mget(c(unique(c(color_by, linetype_by)), "color_breaks"))],
by = unique(c(color_by, linetype_by)),
all.x = TRUE,
all.y = FALSE
)
} else {
x <- merge(
x = x,
y = guide_list[[guide_type]][, mget(c(color_by, "color_breaks"))],
by = color_by,
all.x = TRUE,
all.y = FALSE
)
}
# Return guide_color
return_list[[guide_type]] <- guide_list[[guide_type]]
} else if (guide_type == "guide_linetype") {
# Add linetype_breaks to x
if (combine_legend) {
x <- merge(
x = x,
y = guide_list[[guide_type]][, mget(c(unique(c(color_by, linetype_by)), "linetype_breaks"))],
by = unique(c(color_by, linetype_by)),
all.x = TRUE,
all.y = FALSE
)
} else {
x <- merge(
x = x,
y = guide_list[[guide_type]][, mget(c(linetype_by, "linetype_breaks"))],
by = linetype_by,
all.x = TRUE,
all.y = FALSE
)
}
# Return guide_linetype
return_list[[guide_type]] <- guide_list[[guide_type]]
}
}
# Add updated data
return_list$data <- x
return(return_list)
}
.add_plot_cluster_name <- function(
x,
color_by = NULL,
facet_by = NULL,
singular_cluster_character = "\u2014"
) {
# Suppress NOTES due to non-standard evaluation in data.table
cluster_size <- cluster_id <- feature <- new_cluster_id <- cluster_name <- NULL
..integer_to_char <- function(x) {
# Initialise placeholders
x_remain <- x
new_string <- character(0L)
while (as.integer(ceiling(x_remain / 26L)) > 0L) {
# Determine the modulo.
mod <- x_remain %% 26L
# Find if mod is equal to 0, which would indicate Z.
mod <- ifelse(mod == 0L, 26L, mod)
# Add letter
new_string <- c(new_string, LETTERS[mod])
# Update the remain variable
x_remain <- (x_remain - mod) / 26L
}
return(paste(rev(new_string), collapse = ""))
}
# Identify splitting variables
splitting_vars <- unique(c(color_by, facet_by))
# Split x by splitting variables.
if (length(splitting_vars) > 0L) {
x <- split(x, by = splitting_vars)
} else {
x <- list(x)
}
# Iterate and add cluster names.
x <- lapply(
x,
function(y) {
# Check if x is empty.
if (is_empty(y)) return(NULL)
# This is for backward compatibility.
if (!all(c("cluster_id", "cluster_size") %in% colnames(y))) {
# Add cluster name
y[, "cluster_name" := singular_cluster_character]
return(y)
}
# Only determine cluster_name for those clusters that have cluster_size >
# 1. Also, the most important features should receive a higher replacement
# cluster_id.
y_short <- y[cluster_size > 1L, mget(c("feature", "cluster_id"))]
if (!is_empty(y_short)) {
# Remove unused levels for the name column. The levels of name are
# ordered according to importance.
y_short <- droplevels(y_short)
# Set placeholder cluster id
y_short[, "new_cluster_id" := NA_integer_]
new_id <- 1L
for (current_feature in levels(y_short$feature)) {
# Provide new cluster id in case none exists.
if (is.na(y_short[feature == current_feature, ]$new_cluster_id[1L])) {
# Find the old cluster id.
old_cluster_id <- y_short[feature == current_feature, ]$cluster_id[1L]
# Update all entries with the same old cluster id.
y_short[cluster_id == old_cluster_id, "new_cluster_id" := new_id]
# Increment new cluster id.
new_id <- new_id + 1L
}
}
# Determine cluster name based on id.
y_short[, "cluster_name" := ..integer_to_char(new_cluster_id), by = "feature"]
# Drop redundant columns
y_short[, ":="(
"cluster_id" = NULL,
"new_cluster_id" = NULL
)]
# Merge with y.
y <- merge(
x = y,
y = y_short,
by = "feature",
all = TRUE
)
# Mark singular clusters
y[is.na(cluster_name), "cluster_name" := singular_cluster_character]
} else {
# Mark singular clusters
y[, "cluster_name" := singular_cluster_character]
}
return(y)
}
)
x <- data.table::rbindlist(x, use.names = TRUE)
return(x)
}
.split_data_by_plot_facet <- function(x, plot_layout_table = NULL, ...) {
if (is_empty(x)) return(NULL)
if (is.null(plot_layout_table)) {
plot_layout_table <- do.call(
.get_plot_layout_table,
args = c(list("x" = x), list(...))
)
}
# Derive facet_by
facet_by <- setdiff(
colnames(plot_layout_table),
c("col_id", "row_id")
)
if (length(facet_by) > 0L) {
# Merge the plot_layout_table into x. This will keep things in order. All
# levels are kept.
x <- merge(
x = x,
y = plot_layout_table,
by = facet_by,
all = TRUE
)
} else {
x <- cbind(x, plot_layout_table)
}
# Split data by row, then column
split_data <- split(
x,
by = c("col_id", "row_id"),
sorted = TRUE
)
return(split_data)
}
.get_plot_layout_dims <- function(plot_layout_table = NULL, ...) {
# Create the plot_layout_table if it is not provided.
if (is.null(plot_layout_table)) {
plot_layout_table <- do.call(
.get_plot_layout_table,
args = list(...)
)
}
# Return (nrows, ncols)
return(c(
max(plot_layout_table$row_id),
max(plot_layout_table$col_id)
))
}
.get_plot_layout_table <- function(x, facet_by, facet_wrap_cols) {
if (is.null(facet_by)) {
# Simple 1x1 layout without facets.
plot_layout_table <- data.table::data.table(
"col_id" = 1L,
"row_id" = 1L
)
} else if (is.null(facet_wrap_cols)) {
# Generate a plot_layout_table and order it
plot_layout_table <- expand.grid(
lapply(
facet_by,
function(column, x) levels(x[[column]]),
x = x
),
KEEP.OUT.ATTRS = FALSE
)
plot_layout_table <- data.table::as.data.table(plot_layout_table)
data.table::setnames(plot_layout_table, facet_by)
data.table::setorderv(x = plot_layout_table, cols = facet_by)
# Find the number of columns
n_cols <- length(unique(x[[facet_by[1L]]]))
# Add column id to the plot_layout_table
plot_layout_table[, "col_id" := .GRP, by = get(facet_by[1L])]
if (length(facet_by) > 1L) {
# Find the number of rows
n_levels <- sapply(
facet_by[2L:length(facet_by)],
function(ii, x) {
if (is.factor(x[[ii]])) {
return(nlevels(x[[ii]]))
} else {
return(length(unique(x[[ii]])))
}
},
x = x
)
n_rows <- prod(n_levels)
# Add row id to the plot_layout_table
facet_row_cols <- facet_by[2L:length(facet_by)]
plot_layout_table[, "row_id" := .GRP, by = mget(facet_row_cols)]
} else {
# There is only one row
n_rows <- 1L
plot_layout_table[, "row_id" := 1L]
}
} else {
# Generate a plot_layout_table, and order
plot_layout_table <- unique(x[, (facet_by), with = FALSE], by = facet_by)
data.table::setorderv(x = plot_layout_table, cols = facet_by)
# Number of columns is provided using facet_wrap_cols.
len_table <- nrow(plot_layout_table)
n_cols <- facet_wrap_cols
n_rows <- ceiling(len_table / n_cols)
# Generate the column and row positions.
col_ids <- rep(seq_len(n_cols), times = n_rows)[seq_len(len_table)]
row_ids <- rep(seq_len(n_rows), each = n_cols)[seq_len(len_table)]
# Add column and row ids to the plot_layout_table.
plot_layout_table[, ":="(
"col_id" = col_ids,
"row_id" = row_ids
)]
}
return(plot_layout_table)
}
.combine_plot_elements <- function(
g_main,
g_new,
element_name,
spacer = NULL,
stack_direction = "vertical"
) {
for (current_element_name in element_name) {
# Determine matching elements in both g_main and g_new.
element_names_main <- g_main$layout$name
present_elements_main <- element_names_main[sapply(
element_names_main,
startswith_any,
prefix = current_element_name
)]
element_names_new <- g_new$layout$name
present_elements_new <- element_names_new[sapply(
element_names_new,
startswith_any,
prefix = current_element_name
)]
if (length(present_elements_main) != 1L || length(present_elements_new) != 1L) next
grob_index_main <- which(g_main$layout$name == present_elements_main)
grob_index_new <- which(g_new$layout$name == present_elements_new)
# Determine if there is anything to add from g_new.
if (any(sapply(c("zeroGrob", "nullGrob"), function(ii, x) (is(x, ii)), x = g_new$grobs[[grob_index_new]]))) next
grob_main <- g_main$grobs[[grob_index_main]]
grob_new <- g_new$grobs[[grob_index_new]]
# If there is an empty grob in g_main, simply copy the element from g_new
# into g_main.
if (any(sapply(c("zeroGrob", "nullGrob"), function(ii, x) (is(x, ii)), x = g_main$grobs[[grob_index_main]]))) {
g_main$grobs[[grob_index_main]] <- grob_new
} else {
g <- list(grob_main, grob_new)
# Find widths and heights
widths <- lapply(g, .gtable_get_grob_aspect_size, aspect = "width")
if (!is.null(spacer) && stack_direction == "horizontal") {
widths <- append(widths, 0L, after = 1L)
# Direct insertion of spacer with append results in spacer losing its
# simpleUnit class.
widths[[2L]] <- spacer
}
widths <- do.call(grid::unit.c, widths)
heights <- lapply(g, .gtable_get_grob_aspect_size, aspect = "height")
if (!is.null(spacer) && stack_direction == "vertical") {
heights <- append(heights, 0L, after = 1L)
# Direct insertion of spacer with append results in spacer losing its
# simpleUnit class.
heights[[2L]] <- spacer
}
heights <- do.call(grid::unit.c, heights)
if (stack_direction == "vertical") {
# Concatenate the widths.
widths <- max(widths)
} else {
# Concatenate the heights.
heights <- max(heights)
}
# Set up basic gtable.
g_combine <- gtable::gtable(
widths = widths,
heights = heights,
name = present_elements_main
)
# Insert grob from main.
g_combine <- gtable::gtable_add_grob(
g_combine,
grobs = g[[1L]],
t = 1L,
l = 1L
)
# Insert grob from new.
g_combine <- gtable::gtable_add_grob(
g_combine,
grobs = g[[2L]],
t = length(heights),
l = length(widths)
)
# Insert spacer.
if (!is.null(spacer) && stack_direction == "vertical") {
spacer_position <- c(2L, 2L, 1L, 1L)
names(spacer_position) <- c("t", "b", "l", "r")
g_combine <- .gtable_insert_spacer(
g_combine,
position = spacer_position,
height = spacer
)
} else if (!is.null(spacer) && stack_direction == "horizontal") {
spacer_position <- c(1L, 1L, 2L, 2L)
names(spacer_position) <- c("t", "b", "l", "r")
g_combine <- .gtable_insert_spacer(
g_combine,
position = spacer_position,
width = spacer
)
}
g_main$grobs[[grob_index_main]] <- g_combine
}
}
g_main <- .gtable_update_layout(g = g_main)
return(g_main)
}
.compose_figure <- function(
figure_list,
plot_layout_table,
x_text_shared = "overall",
x_label_shared = "overall",
y_text_shared = "overall",
y_label_shared = "overall",
facet_wrap_cols = NULL,
ggtheme = NULL
) {
# Suppress NOTES due to non-standard evaluation in data.table
col_id <- row_id <- is_present <- n_present <- NULL
type <- NULL
# Global layout --------------------------------------------------------------
# Add figure names to plot_layout_table.
plot_layout_table[, figure_name := paste0(row_id, ".", col_id)]
plot_layout_table[, is_present := figure_name %in% names(figure_list)]
# Drop columns with only missing information.
empty_cols <- plot_layout_table[
,
list("n_present" = sum(is_present)),
by = "col_id"
]
empty_cols <- empty_cols[n_present == 0L]$col_id
if (length(empty_cols) > 0L) {
plot_layout_table <- plot_layout_table[!col_id %in% empty_cols]
}
# Drop rows with only missing information.
empty_rows <- plot_layout_table[
,
list("n_present" = sum(is_present)),
by = "row_id"
]
empty_rows <- empty_rows[n_present == 0L]$row_id
if (length(empty_rows) > 0L) {
plot_layout_table <- plot_layout_table[!row_id %in% empty_rows]
}
# Check that any part of the plot is remaining
if (is_empty(plot_layout_table)) return(NULL)
# Create missing figure panels -----------------------------------------------
# Identify and add missing figures.
missing_figures <- plot_layout_table[is_present == FALSE]$figure_name
for (missing_figure in missing_figures) {
# Get col_id and row_id to identify template figures.
current_row_id <- plot_layout_table[figure_name == missing_figure]$row_id
current_col_id <- plot_layout_table[figure_name == missing_figure]$col_id
# Identify figures to use as templates.
template_figure_row_name <- head(plot_layout_table[row_id == current_row_id & is_present == TRUE]$figure_name, n = 1L)
template_figure_col_name <- head(plot_layout_table[col_id == current_col_id & is_present == TRUE]$figure_name, n = 1L)
# Add template.
figure_list[[missing_figure]] <- .create_placeholder_figure(
template_figure_row = figure_list[[template_figure_row_name]],
template_figure_col = figure_list[[template_figure_col_name]],
row_id = current_row_id,
col_id = current_col_id
)
}
# Check that no figures are missing now.
plot_layout_table[, is_present := figure_name %in% names(figure_list)]
if (!all(plot_layout_table$is_present)) {
..error_reached_unreachable_code("All panels of the figure should be present, but one or more are missing.")
}
# Shared elements between figure panels --------------------------------------
# Determine top and bottom rows, and left and right columns.
top_row_id <- min(plot_layout_table$row_id)
bottom_row_id <- max(plot_layout_table$row_id)
left_col_id <- min(plot_layout_table$col_id)
right_col_id <- max(plot_layout_table$col_id)
# Remove elements from figures.
for (figure_name in names(figure_list)) {
# Configure removal.
figure_list[[figure_name]] <- .set_figure_element_removal(
object = figure_list[[figure_name]],
top_row_id = top_row_id,
bottow_row_id = bottom_row_id,
left_col_id = left_col_id,
right_col_id = right_col_id,
x_text_shared = x_text_shared,
y_text_shared = y_text_shared,
x_label_shared = x_label_shared,
y_label_shared = y_label_shared
)
# Remove elements by replacing them with a zeroGrob. This maintains the
# size of the figure's gtable.
figure_list[[figure_name]] <- .remove_figure_elements(
object = figure_list[[figure_name]],
replace_by_zero_grob = TRUE
)
}
# Compose figure panels ------------------------------------------------------
# Form plot rows.
g <- NULL
spacer_width_x <- .get_plot_panel_spacing(ggtheme = ggtheme, axis = "x")
spacer_width_y <- .get_plot_panel_spacing(ggtheme = ggtheme, axis = "y")
unique_rows <- sort(unique(plot_layout_table$row_id))
unique_cols <- sort(unique(plot_layout_table$col_id))
for (current_row_id in unique_rows) {
# Merge columns within each row.
g_row <- NULL
for (current_col_id in unique_cols) {
if (is.null(g_row)) {
# Use
g_row <- figure_list[[paste0(current_row_id, ".", current_col_id)]]@gtable
} else {
# Insert column for spacer.
g_row <- gtable::gtable_add_cols(
g_row,
widths = spacer_width_x,
pos = ncol(g_row)
)
# Add spacer.
g_row <- .gtable_insert_spacer(
g = g_row,
position = c("t" = 1L, "b" = nrow(g_row), "l" = ncol(g_row), "r" = ncol(g_row)),
width = spacer_width_x
)
# Combine gtable by columns.
g_row <- cbind(g_row, figure_list[[paste0(current_row_id, ".", current_col_id)]]@gtable)
}
}
# Merge with existing rows.
if (is.null(g)) {
g <- g_row
} else {
# Insert row for spacer.
g_row <- gtable::gtable_add_rows(
g_row,
heights = spacer_width_y,
pos = nrow(g_row)
)
# Add spacer element in the new row.
g_row <- .gtable_insert_spacer(
g = g_row,
position = c("t" = nrow(g_row), "b" = nrow(g_row), "l" = 1L, "r" = ncol(g_row)),
height = spacer_width_y
)
# Combine gtable by rows.
g <- rbind(g, g_row)
}
}
# Insert global elements -----------------------------------------------------
# Insert global elements.
global_element_names <- c(
.all_gtable_title_names(),
.all_gtable_guide_names()
)
if (x_text_shared %in% c("overall", "TRUE")) {
global_element_names <- c(global_element_names, .all_gtable_axis_x_names())
}
if (x_label_shared %in% c("overall", "TRUE")) {
global_element_names <- c(global_element_names, .all_gtable_label_x_names())
}
if (y_text_shared %in% c("overall", "TRUE")) {
global_element_names <- c(global_element_names, .all_gtable_axis_y_names())
}
if (y_label_shared %in% c("overall", "TRUE")) {
global_element_names <- c(global_element_names, .all_gtable_label_x_names())
}
# Isolate global elements that need to be updated.
global_elements <- figure_list[[1L]]@global_elements
global_element_name_list <- lapply(
names(global_elements),
function(x, y) {
if (!startswith_any(x, y)) return(NULL)
return(list(
"name" = x,
"type" = y[sapply(y, function(y, x) {startsWith(x, y)}, x = x)]
))
},
y = global_element_names
)
global_element_name_list <- data.table::rbindlist(global_element_name_list)
# For title-like objects, insert in place, at the top.
if (any(global_element_name_list$type %in% .all_gtable_title_names("title"))) {
x <- global_element_name_list[type %in% .all_gtable_title_names("title")]
for (ii in seq_len(nrow(x))) {
element_positions <- g$layout[g$layout$name == x$name[ii], c("t", "l", "b", "r"), drop = FALSE]
position <- c("t" = 0L, "l" = 0L, "b" = 0L, "r" = 0L)
position["t"] <- position["b"] <- min(element_positions$t)
position["l"] <- min(element_positions$l)
position["r"] <- max(element_positions$r)
g <- .gtable_insert(
g = g,
g_new = global_elements[[x$name[ii]]],
where = c("at", position),
grob_name = x$name[ii]
)
}
}
# For caption-like objects, insert in place, at the bottom.
if (any(global_element_name_list$type %in% .all_gtable_title_names("caption"))) {
x <- global_element_name_list[type %in% .all_gtable_title_names("caption")]
for (ii in seq_len(nrow(x))) {
element_positions <- g$layout[g$layout$name == x$name[ii], c("t", "l", "b", "r"), drop = FALSE]
position <- c("t" = 0L, "l" = 0L, "b" = 0L, "r" = 0L)
position["t"] <- position["b"] <- max(element_positions$b)
position["l"] <- min(element_positions$l)
position["r"] <- max(element_positions$r)
g <- .gtable_insert(
g = g,
g_new = global_elements[[x$name[ii]]],
where = c("at", position),
grob_name = x$name[ii]
)
}
}
# For guide-like objects, insert at the corresponding position.
if (any(global_element_name_list$type %in% .all_gtable_guide_names())) {
x <- global_element_name_list[type %in% .all_gtable_guide_names()]
for (ii in seq_len(nrow(x))) {
element_positions <- g$layout[g$layout$name == x$name[ii], c("t", "l", "b", "r"), drop = FALSE]
position <- c("t" = 0L, "l" = 0L, "b" = 0L, "r" = 0L)
# Positioning
if (x$type[ii] %in% .all_gtable_guide_names("top")) {
position["t"] <- position["b"] <- min(element_positions$t)
position["l"] <- min(element_positions$l)
position["r"] <- max(element_positions$r)
spacer_position <- position
spacer_position[["t"]] <- spacer_position[["b"]] <- position[["b"]] + 1L
g <- .gtable_insert_spacer(
g = g,
position = spacer_position,
height = .get_plot_panel_spacing(ggtheme = ggtheme, axis = "y"),
make_space = TRUE
)
} else if (x$type[ii] %in% .all_gtable_guide_names("bottom")) {
position["t"] <- position["b"] <- max(element_positions$b)
position["l"] <- min(element_positions$l)
position["r"] <- max(element_positions$r)
spacer_position <- position
spacer_position[["t"]] <- spacer_position[["b"]] <- position[["t"]]
g <- .gtable_insert_spacer(
g = g,
position = spacer_position,
height = .get_plot_panel_spacing(ggtheme = ggtheme, axis = "y"),
make_space = TRUE
)
# Inserting the spacer moves the guide object down.
position[["t"]] <- position[["t"]] + 1L
position[["b"]] <- position[["b"]] + 1L
} else if (x$type[ii] %in% .all_gtable_guide_names("left")) {
position["l"] <- position["r"] <- min(element_positions$l)
position["t"] <- min(element_positions$t)
position["b"] <- max(element_positions$b)
spacer_position <- position
spacer_position[["l"]] <- spacer_position[["r"]] <- position[["r"]] + 1L
g <- .gtable_insert_spacer(
g = g,
position = spacer_position,
width = .get_plot_panel_spacing(ggtheme = ggtheme, axis = "x"),
make_space = TRUE
)
} else if (x$type[ii] %in% .all_gtable_guide_names("right")) {
position["l"] <- position["r"] <- max(element_positions$r)
position["t"] <- min(element_positions$t)
position["b"] <- max(element_positions$b)
spacer_position <- position
spacer_position[["l"]] <- spacer_position[["r"]] <- position[["l"]]
g <- .gtable_insert_spacer(
g = g,
position = spacer_position,
width = .get_plot_panel_spacing(ggtheme = ggtheme, axis = "x"),
make_space = TRUE
)
# Inserting the spacer moves the guide object right.
position[["l"]] <- position[["l"]] + 1L
position[["r"]] <- position[["r"]] + 1L
} else {
..error_reached_unreachable_code(paste0("unknown type: ", x$type[1L]))
}
g <- .gtable_insert(
g = g,
g_new = global_elements[[x$name[ii]]],
where = c("at", position),
grob_name = x$name[ii]
)
}
}
# Labels
if (any(global_element_name_list$type %in% .all_gtable_label_names())) {
x <- global_element_name_list[type %in% .all_gtable_label_names()]
for (ii in seq_len(nrow(x))) {
element_positions <- g$layout[g$layout$name == x$name[ii], c("t", "l", "b", "r"), drop = FALSE]
position <- c("t" = 0L, "l" = 0L, "b" = 0L, "r" = 0L)
# Positioning
if (x$type[ii] %in% .all_gtable_label_names("top")) {
position["t"] <- position["b"] <- min(element_positions$t)
position["l"] <- min(element_positions$l)
position["r"] <- max(element_positions$r)
} else if (x$type[ii] %in% .all_gtable_label_names("bottom")) {
position["t"] <- position["b"] <- max(element_positions$b)
position["l"] <- min(element_positions$l)
position["r"] <- max(element_positions$r)
} else if (x$type[ii] %in% .all_gtable_label_names("left")) {
position["l"] <- position["r"] <- min(element_positions$l)
position["t"] <- min(element_positions$t)
position["b"] <- max(element_positions$b)
} else if (x$type[ii] %in% .all_gtable_label_names("right")) {
position["l"] <- position["r"] <- max(element_positions$r)
position["t"] <- min(element_positions$t)
position["b"] <- max(element_positions$b)
} else {
..error_reached_unreachable_code(paste0("unknown type: ", x$type[1L]))
}
g <- .gtable_insert(
g = g,
g_new = global_elements[[x$name[ii]]],
where = c("at", position),
grob_name = x$name[ii]
)
}
}
# Axis elements
if (any(global_element_name_list$type %in% .all_gtable_axis_names())) {
x <- global_element_name_list[type %in% .all_gtable_axis_names()]
for (ii in seq_len(nrow(x))) {
element_positions <- g$layout[g$layout$name == x$name[ii], c("t", "l", "b", "r"), drop = FALSE]
position <- c("t" = 0L, "l" = 0L, "b" = 0L, "r" = 0L)
# Positioning
if (x$type[ii] %in% .all_gtable_axis_names("top")) {
position["t"] <- position["b"] <- min(element_positions$t)
position["l"] <- min(element_positions$l)
position["r"] <- max(element_positions$r)
} else if (x$type[ii] %in% .all_gtable_axis_names("bottom")) {
position["t"] <- position["b"] <- max(element_positions$b)
position["l"] <- min(element_positions$l)
position["r"] <- max(element_positions$r)
} else if (x$type[ii] %in% .all_gtable_axis_names("left")) {
position["l"] <- position["r"] <- min(element_positions$l)
position["t"] <- min(element_positions$t)
position["b"] <- max(element_positions$b)
} else if (x$type[ii] %in% .all_gtable_axis_names("right")) {
position["l"] <- position["r"] <- max(element_positions$r)
position["t"] <- min(element_positions$t)
position["b"] <- max(element_positions$b)
} else {
..error_reached_unreachable_code(paste0("unknown type: ", x$type[1L]))
}
g <- .gtable_insert(
g = g,
g_new = global_elements[[x$name[ii]]],
where = c("at", position),
grob_name = x$name[ii]
)
}
}
# Clean-up -------------------------------------------------------------------
# Drop all zeroGrob elements to prevent occlusion by empty objects.
matched_elements <- !sapply(g$grobs, is, "zeroGrob")
g$layout <- g$layout[matched_elements, , drop = FALSE]
g$grobs <- g$grobs[matched_elements]
g <- gtable::gtable_trim(g)
# Update heights and widths.
g <- .gtable_update_layout(g = g)
return(g)
}
..get_plot_theme_linewidth <- function(ggtheme = NULL) {
# Import default ggtheme in case none is provided.
ggtheme <- .check_ggtheme(ggtheme)
# Since ggplot 3.4.0, the width of a line is determined by linewidth instead
# of size.
linewidth <- ggtheme$line$linewidth
return(linewidth)
}
..get_plot_element_spacing <- function(ggtheme = NULL, axis, theme_element) {
# Obtain spacing from a ggtheme element
# Import default ggtheme in case none is provided.
ggtheme <- .check_ggtheme(ggtheme)
# Basic spacing settings
spacing <- ggtheme$spacing
spacing_rel <- 1.0
# Attempt to base the text size on the general axis.text attribute.
if (!is.null(ggtheme[[theme_element]])) {
if (inherits(ggtheme[[theme_element]], "rel")) {
# Find the relative text size of axis text.
spacing_rel <- as.numeric(ggtheme[[theme_element]])
} else {
# Set absolute text size.
spacing <- ggtheme[[theme_element]]
spacing_rel <- 1.0
}
}
# Attempt to refine the text size using the axis.text.y attribute in
# particular.
if (!is.null(ggtheme[[paste0(theme_element, ".", axis)]])) {
if (inherits(ggtheme[[paste0(theme_element, ".", axis)]], "rel")) {
# Set relative text size of axis text
spacing_rel <- as.numeric(ggtheme[[paste0(theme_element, ".", axis)]])
} else {
# Set absolute text size.
spacing <- ggtheme[[paste0(theme_element, ".", axis)]]
spacing_rel <- 1.0
}
}
spacing <- spacing * spacing_rel
# If no spacing is provided, produce 0.0 length spacing.
if (!grid::is.unit(spacing)) spacing <- grid::unit(0.0, "pt")
return(spacing)
}
.get_plot_panel_spacing <- function(ggtheme = NULL, axis) {
# Obtain spacing between panels. This determines distance between facets.
return(..get_plot_element_spacing(
ggtheme = ggtheme,
axis = axis,
theme_element = "panel.spacing"
))
}
.get_plot_legend_spacing <- function(ggtheme = NULL, axis) {
# Obtain spacing between legend and the main panel.
return(..get_plot_element_spacing(
ggtheme = ggtheme,
axis = axis,
theme_element = "legend.spacing"
))
}
.get_plot_geom_text_settings <- function(ggtheme = NULL) {
# Import formatting settings from the provided ggtheme.
# Import default ggtheme in case none is provided.
ggtheme <- .check_ggtheme(ggtheme)
# Find the text size for the table. This is based on text sizes in the
# ggtheme.
fontsize <- ggtheme$text$size
fontsize_rel <- 1.0
# Attempt to base the text size on the general axis.text attribute.
if (!is.null(ggtheme$axis.text$size)) {
if (inherits(ggtheme$axis.text$size, "rel")) {
# Find the relative text size of axis text.
fontsize_rel <- as.numeric(ggtheme$axis.text$size)
} else {
# Set absolute text size.
fontsize <- ggtheme$axis.text$size
fontsize_rel <- 1.0
}
}
# Attempt to refine the text size using the axis.text.y attribute in
# particular.
if (!is.null(ggtheme$axis.text.y$size)) {
if (inherits(ggtheme$axis.text.y$size, "rel")) {
# Set relative text size of axis text
fontsize_rel <- as.numeric(ggtheme$axis.text.y$size)
} else {
# Set absolute text size.
fontsize <- as.numeric(ggtheme$axis.text.y$size)
fontsize_rel <- 1.0
}
}
# Update the text size using the magical ggplot2 point size (ggplot2:::.pt).
geom_text_size <- fontsize * fontsize_rel / ggplot2::.pt
# Obtain lineheight
lineheight <- ggtheme$text$lineheight
if (!is.null(ggtheme$axis.text$lineheight)) lineheight <- ggtheme$axis.text$lineheight
if (!is.null(ggtheme$axis.text.y$lineheight)) lineheight <- ggtheme$axis.text.y$lineheight
# Obtain family
fontfamily <- ggtheme$text$family
if (!is.null(ggtheme$axis.text$family)) fontfamily <- ggtheme$axis.text$family
if (!is.null(ggtheme$axis.text.y$family)) fontfamily <- ggtheme$axis.text.y$family
if (!is.null(ggtheme$axis.text.x$family)) fontfamily <- ggtheme$axis.text.x$family
# Obtain face
fontface <- ggtheme$text$face
if (!is.null(ggtheme$axis.text$face)) fontface <- ggtheme$axis.text$face
if (!is.null(ggtheme$axis.text.y$face)) fontface <- ggtheme$axis.text.y$face
if (!is.null(ggtheme$axis.text.x$face)) fontface <- ggtheme$axis.text.x$face
# Obtain colour
colour <- ggtheme$text$colour
if (!is.null(ggtheme$axis.text$colour)) colour <- ggtheme$axis.text$colour
if (!is.null(ggtheme$axis.text.y$colour)) colour <- ggtheme$axis.text.y$colour
if (!is.null(ggtheme$axis.text.x$colour)) colour <- ggtheme$axis.text.x$colour
return(list(
"geom_text_size" = geom_text_size,
"fontsize" = fontsize,
"fontsize_rel" = fontsize_rel,
"colour" = colour,
"family" = fontfamily,
"face" = fontface,
"lineheight" = lineheight
))
}
.rename_plot_grobs <- function(g, extension = "main", use_generic = TRUE) {
if (is.null(g)) return(g)
element_names <- c(
.all_gtable_panel_names(),
.all_gtable_label_x_names(),
.all_gtable_label_y_names(),
.all_gtable_axis_x_names(),
.all_gtable_axis_y_names()
)
# Filter only elements that are present, and generate list of matching
# local and global elements.
renamable_elements <- g$layout$name
element_list <- lapply(
renamable_elements,
function(x, y) {
if (!startswith_any(x, prefix = y)) return(NULL)
y <- y[sapply(y, function(y, x) {startsWith(x, y)}, x = x)]
return(list("local" = x, "global" = y))
},
y = element_names
)
element_list <- data.table::rbindlist(element_list)
# If generic names are not used, replace global (generic) names by the local
# names.
if (!use_generic) {
element_list$global <- element_list$local
}
# Rename elements and add extension.
for (ii in seq_len(nrow(element_list))) {
g <- .gtable_rename_element(
g = g,
old = element_list$local[ii],
new = paste0(element_list$global[ii], "-", extension),
partial_match = FALSE,
allow_missing = FALSE
)
}
return(g)
}
.convert_to_grob <- function(plots_or_grobs) {
# Convert to list if the input is a single grob or
unlist_grobs <- FALSE
if (grid::is.grob(plots_or_grobs) || ggplot2::is_ggplot(plots_or_grobs)) {
plots_or_grobs <- list(plots_or_grobs)
# Set a flag so that we unlist the results after conversion.
unlist_grobs <- TRUE
}
# Initialise list of grobs
grobs <- list()
for (p in plots_or_grobs) {
if (inherits(p, "familiar_ggplot")) {
# Convert to grob
g <- suppressWarnings(tryCatch(
ggplot2::ggplotGrob(p),
error = identity
))
if (inherits(g, "error")) g <- NULL
# Make changes to g according to p$custom_grob.
if (!is.null(p$custom_grob)) {
if (!is.null(p$custom_grob$heights)) {
# Iterate over the elements that need to be updated.
for (ii in seq_len(length(p$custom_grob$heights$name))) {
# Extract name and height
name <- p$custom_grob$heights$name[ii]
height <- p$custom_grob$heights$height[ii]
# Find the intended grob.
grob_index <- which(g$layout$name == name)
# Update the height in the layout table.
g$heights[g$layout[grob_index, "t"]] <- height
# Update (or set) the height of the grob.
g$grobs[[grob_index]]$heights <- height
}
}
if (!is.null(p$custom_grob$widths)) {
# Iterate over the elements that need to be updated.
for (ii in seq_len(length(p$custom_grob$widths$name))) {
name <- p$custom_grob$widths$name[ii]
width <- p$custom_grob$widths$width[ii]
# Find the intended grob.
grob_index <- which(g$layout$name == name)
# Update the height in the layout table.
g$widths[g$layout[grob_index, "l"]] <- width
# Update (or set) the height of the grob.
g$grobs[[grob_index]]$widths <- width
}
}
}
} else if (inherits(p, "ggplot")) {
# Convert to grob
g <- suppressWarnings(tryCatch(
ggplot2::ggplotGrob(p),
error = identity
))
if (inherits(g, "error")) g <- NULL
} else if (inherits(p, "grob")) {
# Assign grob
g <- p
} else {
..warning(paste0(
"Could not convert an object of class ", class(p), " to a grob."
))
g <- NULL
}
# Set panel sizes.
if (!is.null(g)) {
# Make panels inherit heights and widths, if they don't have any. This is done
# to ensure that panels retain heights and widths, even if supporting elements
# such as the axis text and label elements are stripped on figure composition.
g <- .gtable_update_panel_aspects(g = g)
}
grobs <- c(grobs, list(g))
}
if (unlist_grobs) grobs <- grobs[[1L]]
return(grobs)
}
.draw_plot <- function(plot_or_grob) {
suppress_warnings(
..draw_plot(plot_or_grob),
regexp = c(
"containing missing values",
"containing non-finite values",
"no non-missing arguments to min",
"no non-missing arguments to max"
)
)
}
..draw_plot <- function(plot_or_grob) {
if (ggplot2::is_ggplot(plot_or_grob)) {
show(plot_or_grob)
} else if (grid::is.grob(plot_or_grob)) {
grid::grid.newpage()
grid::grid.draw(plot_or_grob)
} else {
..error("Plot could not be drawn.")
}
return(invisible(NULL))
}
.save_plot_to_file <- function(
plot_or_grob,
object,
dir_path,
type,
x,
subtype = NULL,
split_by = NULL,
additional = NULL,
filename = NULL,
device = "png",
...
) {
# ... are passed to ggplot2::ggsave
# Check if the plot object exists
if (is.null(plot_or_grob)) return(NULL)
# Check if directory exists
if (is.encapsulated_path(dir_path)) {
file_dir <- normalizePath(
file.path(dir_path, type),
mustWork = FALSE
)
} else {
file_dir <- normalizePath(dir_path, mustWork = FALSE)
}
if (!dir.exists(file_dir)) {
dir.create(file_dir, recursive = TRUE)
}
if (!is.null(filename)) {
# These are the file extensions supported by ggsave (v3.4.0).
file_extensions <- c(
"eps", "ps", "tex", "pdf", "svg", "emf", "wmf",
"png", "jpg", "jpeg", "bmp", "tiff"
)
# Test if a file extension is present.
device_present <- endswith_any(
filename,
suffix = paste0(".", file_extensions)
)
if (any(device_present)) {
# Update device indicated by the filename.
device <- head(file_extensions[device_present], n = 1L)
# Remove device from filename.
filename <- sub_last(
pattern = paste0(".", device),
replacement = "",
x = filename
)
}
# Extend the filename if multiple plots are created from the same data.
if (!is.null(split_by)) {
subtype <- paste0(
as.character(sapply(split_by, function(jj, x) (x[[jj]][1L]), x = x)),
collapse = "_"
)
filename <- paste0(filename, subtype, collapse = "_")
}
} else {
# Set subtype.
subtype <- .create_plot_subtype(
x = x,
subtype = subtype,
split_by = split_by,
additional = additional
)
# Combine type and subtype as the filename.
filename <- paste0(
type,
ifelse(is.null(subtype), "", paste0("_", subtype))
)
}
for (current_device in device) {
# Add in extension again.
filename <- paste0(filename, ".", current_device)
# There may be an issue with a cold RStudio where the plotting devices have
# not started.
tryCatch(
# Call ggsave to save the data
suppressMessages(
do.call(
ggplot2::ggsave,
args = c(
list(
"filename" = filename,
"plot" = plot_or_grob,
"device" = current_device,
"path" = file_dir
),
list(...)
)
)
),
error = function(err) {
logger_warning(
paste0(
"Could not create plot ",
filename,
". The OS may not allow long file names."
)
)
}
)
}
return(invisible(NULL))
}
.get_plot_results <- function(
dir_path = NULL,
plot_list = NULL,
export_collection = FALSE,
object = NULL
) {
# Do not return plot information.
if (!is.null(dir_path)) plot_list <- NULL
if (export_collection) {
return(list(
"collection" = object,
"plot_list" = plot_list
))
} else {
return(plot_list)
}
}
.format_plot_number <- function(
x,
digits = 3L,
common_base = NULL,
min_common_base = NULL,
max_common_base = NULL,
character_out = TRUE
) {
# Determine the common base.
if (is.null(common_base)) {
common_base <- ..format_get_common_base(x)
if (is.numeric(max_common_base)) {
common_base <- ifelse(common_base > max_common_base, max_common_base, common_base)
}
if (is.numeric(min_common_base)) {
common_base <- ifelse(common_base < min_common_base, min_common_base, common_base)
}
}
# Round numbers.
x <- as.integer(round(x / 10.0^(1.0 + common_base - digits))) * 10.0^(1.0 + common_base - digits)
# Format output.
if (character_out) {
return(format(x, digits = digits, trim = TRUE))
} else {
return(x)
}
}
..format_get_common_base <- function(x) {
# Find the base-10 integer of the data.
x_base <- floor(log10(abs(x)))
x_base <- x_base[is.finite(x_base)]
return(as.integer(max(x_base)))
}
.format_plot_number_nice_range <- function(input_range, x) {
# Shrink input range to first and last value
input_range <- c(
head(input_range, n = 1L),
tail(input_range, n = 1L)
)
# Find values in input_range that should be replaced.
replace_index <- is.na(input_range)
# Return input range if no updating is required.
if (!any(replace_index)) return(input_range)
# Find range of values in x.
value_range <- range(x)
# Replace missing elements of the input range.
input_range[replace_index] <- value_range[replace_index]
# Make the input range nice
nice_range <- range(labeling::extended(
dmin = input_range[1L],
dmax = input_range[2L],
m = 5L,
only.loose = TRUE
))
# Update the input_range with nice values
input_range[replace_index] <- nice_range[replace_index]
return(input_range)
}
.convert_dendrogram_to_table <- function(h, similarity_metric) {
# Suppress NOTES due to non-standard evaluation in data.table
x_1 <- y_1 <- x_2 <- NULL
# Convert to dendrogram
h <- stats::as.dendrogram(h)
# Determine the metric range.
metric_range <- get_similarity_range(
similarity_metric = similarity_metric,
as_distance = TRUE
)
# Convert dendogram to a list of connectors that can later be used for
# plotting. Note that we do not know where the origin should be located on the
# x-axis. We will correct for that later.
connectors <- .decompose_dendrogram(
h = h,
parent_height = max(metric_range)
)
# Combine into single data.table.
connectors <- data.table::rbindlist(connectors)
# Keep only nodes with finite parent height (y_1). Depending on the metric,
# this means that the connector with the origin may not be drawn.
connectors <- connectors[is.finite(y_1)]
# Return null if there are no connectors to be drawn.
if (is_empty(connectors)) return(NULL)
# Reposition the left-most leaf to 0.0.
min_leaf_pos <- min(c(connectors$x_1, connectors$x_2))
connectors[, ":="(
"x_1" = x_1 - min_leaf_pos,
"x_2" = x_2 - min_leaf_pos
)]
return(connectors)
}
.decompose_dendrogram <- function(
h,
parent_height = Inf,
parent_x = NA,
leafs_visited = 0L
) {
# Decompose dendogram. The function is designed to iterate through a
# dendogram, and obtain the connector between node (h) and its parent, as well
# as the connectors between the node and its children h[[1]] and h[[2]],
# unless it has no children.
dend_attr <- attributes(h)
if (is.na(parent_x)) {
parent_x <- ifelse(is.null(dend_attr$midpoint), 0.0, dend_attr$midpoint)
}
if (is.null(dend_attr$midpoint)) {
# This indicates that the node has no children.
# Connector from parent.
conn_parent_child <- data.table::data.table(
"x_1" = parent_x,
"y_1" = parent_height,
"x_2" = parent_x,
"y_2" = dend_attr$height,
"feature" = dend_attr$label
)
return(list(conn_parent_child))
}
# Connector from parent.
conn_parent_child <- data.table::data.table(
"x_1" = parent_x,
"y_1" = parent_height,
"x_2" = parent_x,
"y_2" = dend_attr$height,
"feature" = NA_character_
)
# Left child node x-axis location
if (!is.null(attributes(h[[1L]])$midpoint)) {
child_l_pos <- leafs_visited + attributes(h[[1L]])$midpoint
} else {
child_l_pos <- leafs_visited
}
# Connector to left leaf.
conn_child_l_leaf <- data.table::data.table(
"x_1" = parent_x,
"y_1" = dend_attr$height,
"x_2" = child_l_pos,
"y_2" = dend_attr$height,
"feature" = NA_character_
)
# Right child node x-axis location
if (
!is.null(attributes(h[[2L]])$midpoint) &&
!is.null(attributes(h[[1L]])$members)
) {
child_r_pos <- leafs_visited + attributes(h[[1L]])$members + attributes(h[[2L]])$midpoint
} else if (!is.null(attributes(h[[1L]])$members)) {
child_r_pos <- leafs_visited + attributes(h[[1L]])$members
} else {
child_r_pos <- leafs_visited
}
# Connector to right leaf.
conn_child_r_leaf <- data.table::data.table(
"x_1" = parent_x,
"y_1" = dend_attr$height,
"x_2" = child_r_pos,
"y_2" = dend_attr$height,
"feature" = NA_character_
)
# Add data.tables as list elements.
connector_list <- list(
conn_parent_child,
conn_child_l_leaf,
conn_child_r_leaf
)
# Left leaf
if (!is.null(h[[1L]])) {
left_leaf_connectors <- .decompose_dendrogram(
h = h[[1L]],
parent_height = dend_attr$height,
parent_x = child_l_pos,
leafs_visited = leafs_visited
)
# Append to list
connector_list <- append(connector_list, left_leaf_connectors)
}
# Right leaf
if (!is.null(h[[2L]])) {
right_leaf_connectors <- .decompose_dendrogram(
h = h[[2L]],
parent_height = dend_attr$height,
parent_x = child_r_pos,
leafs_visited = ifelse(
!is.null(attributes(h[[1L]])$members),
leafs_visited + attributes(h[[1L]])$members,
leafs_visited
)
)
# Append to list
connector_list <- append(connector_list, right_leaf_connectors)
}
return(connector_list)
}
..set_edge_points <- function(x, range, type) {
# Function used to determine edge points, such as used for ggplot2::geom_rect.
if (!is.numeric(x)) {
x <- as.numeric(x)
range <- c(0.5, length(x) + 0.5)
}
if (length(x) > 1L) {
# Make sure x is sorted ascendingly.
sort_index <- sort(x, index.return = TRUE)$ix
x <- x[sort_index]
# Compute difference between subsequent values.
diff_x <- diff(x)
# Compute edges.
xmax <- c(
head(x, n = length(x) - 1L) + diff_x / 2.0,
tail(x, n = 1L) + tail(diff_x, n = 1L) / 2.0
)
xmin <- c(
head(x, n = 1L) - head(diff_x, n = 1L) / 2.0,
tail(x, n = length(x) - 1L) - diff_x / 2.0
)
# Shuffle back to input order.
xmax[sort_index] <- xmax
xmin[sort_index] <- xmin
} else {
xmin <- range[1L]
xmax <- range[2L]
}
edge_points <- list(xmin, xmax)
if (type == "x") {
names(edge_points) <- c("xmin", "xmax")
} else if (type == "y") {
names(edge_points) <- c("ymin", "ymax")
} else {
..error_reached_unreachable_code(paste0(
"..set_edge_points: unknown type specified: ", type
))
}
return(edge_points)
}
..get_luminosity <- function(col) {
values <- as.vector(grDevices::col2rgb(col)) / 255.0
return ((min(values) + max(values)) / 2.0)
}
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.