Nothing
#' Fill column with a bar of a length proportional to a value
#'
#' Fills the background of a column cell using a bar proportional to the value
#' of the cell
#'
#' In Excel output ([condformat2excel()]/[condformat2excelsheet()]), this is
#' rendered using Excel's own native data bar conditional formatting, coloured
#' with `low` (`high`, `background` and `lockcells` don't apply to it, since
#' Excel data bars are solid-coloured and are a workbook-level conditional
#' formatting rule, not a per-cell style). This only happens when `expression`
#' is left at its default (or explicitly `.col`): Excel data bars always
#' reflect a cell's own displayed value against a range, so a custom
#' `expression` (e.g. sizing one column's bar by another column's values)
#' can't be replicated there and is skipped, with a warning; the cell's flat
#' `background`/`na.value` colours are still applied in that case.
#'
#' @family rule
#'
#' @param x A condformat object, typically created with [condformat()]
#' @param columns A character vector with column names to be coloured. Optionally
#' [tidyselect::language()] can be used.
#' @param expression an expression to be evaluated with the data.
#' It should evaluate to a numeric vector,
#' that will be used to determine the colour gradient level.
#' When `columns` selects more than one column, the expression is
#' evaluated once per column, with the `.col` pronoun bound to that
#' column's own values (and `limits`, if not given, computed from
#' that column alone). If omitted, it defaults to `.col`.
#' @param low Colour for the beginning of the bar
#' @param high Colour for the end of the bar
#' @param background Background colour for the cell
#' @param na.value Colour for missing values
#' @param limits range of limits that the gradient should cover
#' @param lockcells logical value determining if no further rules should be applied to the affected cells.
#'
#' @return The condformat_tbl object, with the added formatting information
#' @examples
#' data(iris)
#' cf <- condformat(iris[c(1:5, 70:75, 120:125), ]) |> rule_fill_bar("Sepal.Length")
#' \dontrun{
#' print(cf)
#' }
#' @export
rule_fill_bar <- function(x, columns, expression,
low = "darkgreen",
high = "white",
background = "white",
na.value = "gray",
limits = NA,
lockcells = FALSE) {
if (!inherits(x, "condformat_tbl")) {
x <- condformat(x)
}
columnsquo <- rlang::enquo(columns)
expr <- rlang::enquo(expression)
rule <- structure(list(columns = columnsquo,
expression = expr,
low = force(low),
high = force(high),
background = force(background),
na.value = force(na.value),
limits = force(limits),
lockcells = force(lockcells)),
class = c("condformat_rule", "rule_fill_bar"))
x <- add_rule_to_condformat(x, rule)
return(x)
}
#' @importFrom scales rescale
rule_to_cf_field.rule_fill_bar <- function(rule, xfiltered, xview, ...) {
columns <- tidyselect::eval_select(expr = rule[["columns"]], data = xview)
if (length(columns) == 0) {
return(NULL)
}
if (rlang::quo_is_missing(rule[["expression"]])) {
rule[["expression"]] <- rlang::sym(".col")
}
values_per_column <- eval_expression_per_column(rule[["expression"]], xfiltered, columns)
bar_width_percent <- matrix(NA,
nrow = nrow(xview), ncol = ncol(xview),
byrow = FALSE)
colnames(bar_width_percent) <- colnames(xview)
pbar_is_na <- matrix(NA,
nrow = nrow(xview), ncol = ncol(xview),
byrow = FALSE)
colnames(pbar_is_na) <- colnames(xview)
for (col_name in names(columns)) {
values_determining_color <- values_per_column[[col_name]]
limits <- resolve_limits(values_determining_color, rule[["limits"]])
values_rescaled <- scales::rescale(x = values_determining_color, from = limits)
stopifnot(identical(length(values_rescaled), nrow(xview)))
bar_width_percent[, col_name] <- values_rescaled
pbar_is_na[, col_name] <- is.na(values_rescaled)
}
col_low <- grDevices::col2rgb(rule[["low"]])
col_high <- grDevices::col2rgb(rule[["high"]])
col_background <- grDevices::col2rgb(rule[["background"]])
col_na_value <- grDevices::col2rgb(rule[["na.value"]])
cf_field <- structure(list(col_low = col_low,
col_high = col_high,
col_background = col_background,
col_na_value = col_na_value,
border = TRUE,
bar_width_percent = bar_width_percent,
pbar_is_na = pbar_is_na,
lock_cells = rule[["lockcells"]]),
class = c("cf_field_rule_bar_gradient",
"cf_field"))
return(cf_field)
}
# This is used by all CSS based rules
#' @export
cf_field_to_css.cf_field_rule_bar_gradient <- function(cf_field, xview, css_fields, unlocked) {
bar_width_percent <- cf_field[["bar_width_percent"]]
mask <- unlocked
# mask == TRUE if cell can be changed, false otherwise
# if the css value is NA, ignore it as well
# (so we don't override previous values)
mask <- mask & !is.na(bar_width_percent)
get_css_key <- function(css_fields, css_key, dims) {
if (css_key %in% names(css_fields)) {
prev_values <- css_fields[[css_key]]
} else {
prev_values <- matrix(NA, nrow = dims[1], ncol = dims[2])
}
return(prev_values)
}
border <- get_css_key(css_fields, "border", dim(xview))
border[mask] <- "1px solid black"
css_fields[["border"]] <- border
pbar_is_na <- cf_field[["pbar_is_na"]]
background_repeat <- get_css_key(css_fields, "background-repeat", dim(xview))
background_repeat[mask & !pbar_is_na] <- "no-repeat"
css_fields[["background-repeat"]] <- background_repeat
col_low <- cf_field[["col_low"]]
col_high <- cf_field[["col_high"]]
col_na <- cf_field[["col_na_value"]]
col_bg <- cf_field[["col_background"]]
background_color <- get_css_key(css_fields, "background-color", dim(xview))
# NA cells are excluded from `mask`, so this branch needs to be gated on
# `unlocked` alone. pbar_is_na holds raw NA (not FALSE) for columns the
# rule doesn't target, so clean that up first to avoid NA in the index.
na_cell <- pbar_is_na
na_cell[is.na(na_cell)] <- FALSE
background_color[unlocked & na_cell] <- sprintf("#%02X%02X%02X", col_na[1], col_na[2], col_na[3])
background_color[mask & !pbar_is_na] <- sprintf("#%02X%02X%02X", col_bg[1], col_bg[2], col_bg[3])
css_fields[["background-color"]] <- background_color
background_image <- get_css_key(css_fields, "background-image", dim(xview))
background_image[mask & !pbar_is_na] <- sprintf(
"linear-gradient(to right, rgba(%d, %d, %d, 1) 0%%, rgba(%d, %d, %d, 1) 100%%)",
col_low[1], col_low[2], col_low[3], col_high[1], col_high[2], col_high[3])
css_fields[["background-image"]] <- background_image
background_size <- get_css_key(css_fields, "background-size", dim(xview))
bg_size_val <- sprintf("%d%% 100%%", as.integer(round(100*cf_field[["bar_width_percent"]])))
background_size[mask & !pbar_is_na] <- bg_size_val[mask & !pbar_is_na]
css_fields[["background-size"]] <- background_size
if (identical(cf_field[["lock_cells"]], TRUE)) {
unlocked[mask] <- FALSE
unlocked[na_cell] <- FALSE
}
return(list(css_fields = css_fields, unlocked = unlocked))
}
#' @export
cf_field_to_gtable.cf_field_rule_bar_gradient <- function(
cf_field, xview, gridobj, unlocked, has_rownames, has_colnames) {
bar_width_percent <- cf_field[["bar_width_percent"]]
mask <- unlocked
# mask == TRUE if cell can be changed, false otherwise
# if the css value is NA, ignore it as well
# (so we don't override previous values)
mask <- mask & !is.na(bar_width_percent)
pbar_is_na <- cf_field[["pbar_is_na"]]
col_low <- cf_field[["col_low"]]
col_low <- sprintf("#%02X%02X%02X", col_low[1], col_low[2], col_low[3])
col_high <- cf_field[["col_high"]]
col_high <- sprintf("#%02X%02X%02X", col_high[1], col_high[2], col_high[3])
col_na <- cf_field[["col_na_value"]]
col_na <- sprintf("#%02X%02X%02X", col_na[1], col_na[2], col_na[3])
col_bg <- cf_field[["col_background"]]
col_bg <- sprintf("#%02X%02X%02X", col_bg[1], col_bg[2], col_bg[3])
row_col <- which(!is.na(pbar_is_na), arr.ind = TRUE)
for (tocolor in seq_len(nrow(row_col))) {
if (!unlocked[row_col[tocolor, 1], row_col[tocolor, 2]]) {
next
}
ind <- find_cell(gridobj,
as.integer(has_colnames) + row_col[tocolor, 1],
as.integer(has_rownames) + row_col[tocolor, 2],
name = "core-bg")
if (pbar_is_na[row_col[tocolor, 1], row_col[tocolor, 2]]) {
gridobj$grobs[ind][[1]][["gp"]] <- grid::gpar(fill = col_na)
} else {
rect <- gridobj$grobs[ind][[1]]
#grid::removeGrob(gridobj, rect$name)
grad_levels <- 100
fill_to <- round(100*bar_width_percent[row_col[tocolor, 1], row_col[tocolor, 2]])
# colorRampPalette(...)(0) errors (it can't request zero colours), so
# a 0%-width bar gets no gradient colours at all, just the background.
if (fill_to > 0) {
ramp_colors <- grDevices::colorRampPalette(c(col_low, col_high), space = "Lab")(fill_to)
} else {
ramp_colors <- character(0)
}
gp <- grid::gpar(col = NA,
fill = c(
ramp_colors,
rep(col_bg, grad_levels - fill_to)))
gridobj <- gtable::gtable_add_grob(gridobj,
grobs = grid::rectGrob(
x = grid::unit(seq(from = 0, by = 1/grad_levels, length.out = grad_levels), "npc"),
y = grid::unit(0.5, "npc"),
width = rep(grid::unit(1/grad_levels, "npc") -
grid::unit(2/grad_levels, "scaledpts"), grad_levels),
height = grid::unit(1, "npc") - grid::unit(2, "scaledpts"),
hjust = 0,
vjust = 0.5,
gp = gp),
t = row_col[tocolor, 1] + has_colnames,
b = row_col[tocolor, 1] + has_colnames,
l = row_col[tocolor, 2] + has_rownames,
r = row_col[tocolor, 2] + has_rownames,
z = 0)
}
}
if (identical(cf_field[["lock_cells"]], TRUE)) {
# `mask` already excludes NA cells; lock those separately, cleaning the
# raw NA that pbar_is_na holds for untargeted columns first.
na_cell <- pbar_is_na
na_cell[is.na(na_cell)] <- FALSE
unlocked[mask] <- FALSE
unlocked[na_cell] <- FALSE
}
return(list(gridobj = gridobj, unlocked = unlocked))
}
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.