R/gizmo-histogram.R

Defines functions suppress_hist_plot check_histogram normalise_histogram gizmo_histogram

Documented in gizmo_histogram

# Constructor -------------------------------------------------------------

#' Guide gizmo: histogram
#'
#' This guide displays a histogram of the aesthetic. If the aesthetic is
#' `colour` or `fill`, the shape will reflect this.
#'
#' @param hist One of the following:
#'   * `NULL` for computing histograms on the data values (default).
#'   * an atomic `<vector>` to feed to the `hist.fun` function.
#'   * A named `<list>` with `breaks` and `counts` numeric items, where the
#'   `breaks` item is exactly one element longer than the `counts` item. A
#'   typical way to construct such list is using the [`hist()`][graphics::hist]
#'   function. Please note that `<list>` input is expected in scale-transformed
#'   space, not original data space.
#' @param hist.args A `<list>` with additional arguments to the `hist.fun`
#'   argument. Only applies when `hist` is not provided as a `<list>` already.
#'   Please note that these arguments are only used for binning and counting:
#'   graphical arguments are ignored.
#' @param hist.fun A `<function>` to use for computing histograms when the
#'   `hist` argument is not provided as a list already.
#' @param just A `<numeric[1]>` between 0 and 1. Use 0 for bottom- or
#'   left-aligned histograms, use 1 for top- or right-aligned histograms and 0.5
#'   for centred histograms.
#' @param metric A `<character[1]>` either `"counts"` or `"density"` stating
#'   which field of the `<histogram>` class to display. The `"density"` metric
#'   might be more appropriate to display when the histogram breaks have
#'   non-constant intervals.
#' @inheritParams gizmo_density
#'
#' @details
#' Non-finite values such as `NA` and `NaN` are ignored while infinite values
#' such as `-Inf` and `Inf` are [squished][scales::oob_squish] to the limits.
#'
#' @return A `<GizmoHistogram>` object.
#' @family gizmos
#' @export
#'
#' @examples
#' # A standard plot
#' p <- ggplot(mpg, aes(displ, hwy, colour = cty)) +
#'   geom_point() +
#'   scale_colour_viridis_c()
#'
#' # Histogram from plot data
#' p + guides(colour = gizmo_histogram())
#'
#' # Using bins instead of gradient
#' p + guides(colour = gizmo_histogram("bins"))
#'
#' # Providing custom values to compute histogram
#' p + guides(colour = gizmo_histogram(hist = runif(1000, min = 5, max = 35)))
#'
#' # Providing precomputed histogram
#' p + guides(colour = gizmo_histogram(hist = hist(mpg$cty, breaks = 10)))
#'
#' # Alternatively, parameters may be passed through hist.args
#' p + guides(colour = gizmo_histogram(hist.arg = list(breaks = 10)))
gizmo_histogram <- function(
  key = waiver(),
  hist = NULL, hist.args = list(), hist.fun = graphics::hist,
  just = 1, oob = oob_keep, metric = "counts", alpha = NA,
  # standard arguments
  theme = NULL, position = waiver(), direction = NULL
) {
  hist <- suppress_hist_plot(enquo(hist))
  hist.args$plot <- hist.args$plot %||% FALSE

  check_number_decimal(just, min = 0, max = 1, allow_infinite = FALSE)
  check_argmatch(metric, c("counts", "density"))

  new_guide(
    key = key,
    hist      = hist,
    hist_args = hist.args,
    hist_fun  = hist.fun,
    just = just, oob  = oob, metric = metric, alpha = alpha,
    theme = theme, position = position, direction = direction,
    name = "histogram",
    super = GizmoHistogram
  )
}

# Class -------------------------------------------------------------------

#' @export
#' @rdname legendry_extensions
#' @format NULL
#' @usage NULL
GizmoHistogram <- ggproto(
  "GizmoHistogram", GizmoDensity,

  params = new_params(
    hist = NULL, hist_args = list(), hist_fun = graphics::hist,
    just = 0.5, nbin = 15, oob = oob_keep, metric = "counts",
    alpha = NA, key = "sequence"
  ),

  extract_decor = function(scale, hist, hist_args, hist_fun, ...) {
    if (is.null(hist)) {
      return(NULL) # extract data later
    }
    if (is.atomic(hist)) {
      hist <- filter_finite(scale$transform(hist))
      hist <- inject(hist_fun(hist, !!!hist_args))
    }
    check_histogram(hist)
    hist
  },

  extract_params = function(scale, params, ...) {
    params <- GizmoDensity$extract_params(scale, params, ...)
    if (is.null(params$hist) && inherits(params$key, "key_bins")) {
      breaks <- sort(union(params$key$min, params$key$max))
      params$hist_args$breaks <- params$hist_args$breaks %||% breaks
    }
    params
  },

  get_layer_key = function(params, layers, data = NULL, ...) {
    hist <- params$decor %||% params$hist
    if (length(hist) == 0) {
      values <- filter_finite(vec_c(!!!lapply(data, .subset2, params$aesthetic)))
      hist   <- inject(params$hist_fun(values, !!!params$hist_args))
      check_histogram(hist, params$metric)
    }
    params$decor  <- normalise_histogram(hist, params$metric)
    params$limits <- range(params$limits, params$decor$x)
    params
  }
)

# Helpers -----------------------------------------------------------------

normalise_histogram <- function(hist, metric = "counts") {
  x <- hist$breaks
  y <- hist[[metric]]

  xlim <- range(filter_finite(x), na.rm = TRUE)
  x    <- oob_squish_infinite(x, xlim)

  ylim <- range(filter_finite(y), na.rm = TRUE)
  y <- oob_squish_infinite(y, ylim)

  list(
    x = rep(x, each = 2),
    y = rescale_max(c(0, rep(y, each = 2), 0), to = c(0, 0.9), from = ylim)
  )
}

check_histogram <- function(x, metric = "counts", arg = caller_arg(x), call = caller_env()) {
  if (is_missing(x)) {
    cli::cli_abort("{.arg {arg}} cannot be missing.", call = call)
  }
  if (inherits(x, "histogram")) {
    # We'll trust this class
    return(x)
  }
  check_list_names(x, c("breaks", metric), arg = arg, call = call)

  if (length(x$breaks) != (length(x[[metric]]) + 1L)) {
    cli::cli_abort(c(paste0(
      "In the {.arg {arg}} argument, the {.field breaks} element should be ",
      "exactly 1 longer than the {.field {metric}} element."
    ),
    i = "{.code {arg}$breaks} has length {length(x$breaks)}.",
    i = "{.code {arg}${metric}} has length {length(x[[metric]])}."
    ), call = call)
  }
  check_length(x$breaks, min = 2, arg = as_cli("{arg}$breaks"), call = call)
  check_bare_numeric(x$breaks,    arg = as_cli("{arg}$breaks"), call = call)
  check_bare_numeric(x$counts,    arg = as_cli("{arg}${metric}"), call = call)
  invisible()
}

suppress_hist_plot <- function(hist) {
  if (!quo_is_call(hist)) {
    return(eval_tidy(hist))
  }
  expr <- quo_get_expr(hist)
  if (expr[[1]] != quote(hist)) {
    return(eval_tidy(hist))
  }
  expr <- call_match(expr, graphics::hist)
  expr <- call_modify(expr, plot = FALSE)
  hist <- quo_set_expr(hist, expr)
  eval_tidy(hist)
}

Try the legendry package in your browser

Any scripts or data that you put into this service are public.

legendry documentation built on April 4, 2025, 2:12 a.m.