R/mappings.R

Defines functions ith_mappings_use ith_mappings uses_aesthetics uses_extra_mappings uses_mappings get_mappings.layer_to_check get_mappings.ggplot get_mappings aes_c identical_aes

Documented in get_mappings identical_aes ith_mappings ith_mappings_use uses_aesthetics uses_extra_mappings uses_mappings

#' Are aesthetic mapping specifications "identical"?
#'
#' The ggplot2 package uses quosures to record aesthetic mappings. These record
#' both the mapping described as well as the environment in which the mapping
#' was described. As a result, it is difficult to compare mappings created by
#' students in one environment to mappings created on the fly by graders in
#' another environment. \code{identical_aes} facilitates comparison by ignoring
#' the environments associated with an aesthetic mapping specification. If the
#' two specifications contain identical expressions, e.g. \code{x = displ},
#' etc., \code{identical_aes} returns \code{TRUE}.
#'
#' @param a1 The output of \code{\link[ggplot2]{aes}}, perhaps extracted from a ggplot object.
#' @param a2 The output of \code{\link[ggplot2]{aes}}, perhaps extracted from a ggplot object.
#'
#' @return \code{TRUE} or \code{FALSE}
#'
#' @family functions for checking mappings
#'
#' @export
identical_aes <- function(a1, a2) {
  # strip environments associated with the aesthetics before comparing
  a1 <- lapply(a1, deparse)
  a2 <- lapply(a2, deparse)
  identical(a1, a2)
}

aes_c <- function(a1, a2) {
  # override the a1 aesthetics with the a2 aesthetics
  # NOTE: this is used internally for `get_mappings.layer_to_check` when
  # retrieving the aesthetics for a particular layer by overriding global aesthetics.
  aesthetics <- names(a2)
  a1[aesthetics] <- a2
  a1
}

#' Get aesthetic mappings from a layer or plot
#'
#' \code{get_mappings} returns the mappings used by a ggplot object or a single
#' layer extracted from the object with \code{\link{get_geom_layer}} or \code{\link{get_stat_layer}}.
#'
#' When passed a ggplot object (i.e. a plot), \code{get_mappings} will return
#' only the mappings that have been set globally with
#' \code{\link[ggplot2]{ggplot}}. When passed a single layer from a plot, the
#' behavior of \code{get_mappings} will depend on the value of
#' \code{local_only}. If \code{local_only = TRUE}, \code{get_mappings} will
#' return only the mappings defined locally in a layer. When \code{local_only =
#' FALSE}, \code{get_mappings} will return the combination of global and local
#' methods that will be used to plot a layer.
#'
#' @param p A ggplot object or a layer extracted from a ggplot object with
#'   \code{\link{get_geom_layer}} or \code{\link{get_stat_layer}}.
#' @param local_only \code{TRUE} or \code{FALSE}. Should \code{get_mappings}
#'   return only the mappings defined locally in a layer. This has no effect
#'   when \code{p} is a ggplot object.
#'
#' @return A list with class uneval, as returned by \code{\link[ggplot2]{aes}}
#'   Components of the list are either quosures or constants.
#'
#' @family functions for checking mappings
#'
#' @export
#'
#' @examples
#' require(ggplot2)
#' p <- ggplot(data = mpg, mapping = aes(x = displ, y = hwy)) +
#'   geom_point(mapping = aes(color = class))
#' get_mappings(p)
#' get_mappings(get_geom_layer(p, i = 1), local_only = FALSE)
get_mappings <- function(p, local_only = FALSE) {
  UseMethod("get_mappings")
}

#' @export
get_mappings.ggplot <- function(p, local_only = FALSE) {
  global_map <- p$mapping

  if (local_only) {
    return(global_map)
  }

  layer_maps <- purrr::map(p$layers, "mapping")

  if (length(layer_maps) < 1) {
    return(global_map)
  }

  layer_names <- purrr::reduce(purrr::map(layer_maps, names), intersect)

  if (length(layer_names) < 1) {
    return(global_map)
  }

  layer_names <- purrr::set_names(layer_names)

  layer_maps_ubiquitous <- purrr::map(layer_names, function(name) {
    # If the aesthetic has the same value across all layers, return its
    # value in the first layer; otherwise return NULL
    if (all_identical(purrr::map(layer_maps, name))) {
      layer_maps[[1]][[name]]
    }
  })

  aes_map <- c(global_map, purrr::compact(layer_maps_ubiquitous))
  class(aes_map) <- "uneval"
  aes_map
}

#' @export
get_mappings.layer_to_check <- function(p, local_only = FALSE) {
  local_mappings <- p$layer$mapping
  if (local_only) {
    return(local_mappings)
  } else {
    return(aes_c(p$global_mapping, local_mappings))
  }
}

#' Does a plot or layer use one or more mappings?
#'
#' \code{uses_mappings} checks whether the student used one or more mappings in
#' their plot. By default, \code{uses_mappings} ignores whether or not the student
#' also supplied additional mappings. Use \code{uses_extra_mappings} to check if they did.
#' If \code{exact} is \code{TRUE}, then all of the mappings have to match exactly.
#'
#' @param p A ggplot object or a layer extracted from a ggplot object with
#'   \code{\link{get_geom_layer}} or \code{\link{get_stat_layer}}.
#' @param mappings One or more aesthetic mappings created with
#'   \code{\link[ggplot2]{aes}}.
#' @param local_only If \code{TRUE}, \code{uses_mappings} will check only the
#'   mappings defined locally in a layer for the presence of \code{mappings}. If
#'   \code{FALSE}, \code{uses_mappings} will check for \code{mappings} in the
#'   combination of global and local methods that will be used to plot a layer.
#' @param exact If \code{TRUE}, mappings need to be mapped exactly
#'
#' @return A logical value.
#'
#' @family functions for checking mappings
#'
#' @export
#'
#' @examples
#' require(ggplot2)
#' p <- ggplot(data = mpg, mapping = aes(x = displ, y = hwy)) +
#'   geom_point(mapping = aes(color = class))
#' uses_mappings(p, aes(x = displ))
#' uses_mappings(get_geom_layer(p, i = 1), aes(x = displ, color = class), local_only = FALSE)
#' uses_mappings(get_geom_layer(p, i = 1), aes(x = displ, color = class), local_only = TRUE)
#' uses_mappings(p, aes(x = displ, y = hwy), exact = TRUE)
uses_mappings <- function(p, mappings, local_only = FALSE, exact = FALSE) {
  aes_map <- get_mappings(p, local_only)
  mapping_names <- names(mappings)
  if (exact) {
    return(identical_aes(mappings, get_mappings(p, local_only)))
  } else {
    return(
      all(mapping_names %in% names(aes_map)) && identical_aes(mappings, aes_map[mapping_names])
    )
  }
}

#' Does the plot uses extra aesthetic mappings?
#'
#' \code{uses_extra_mappings} checks if a student's plot contains more than the
#' required aesthetic mappings. Note that we still return \code{TRUE} if
#' the student's plot differs from the required aesthetic mappings because they
#' are technically extra mappings from required set. We recommend you use
#' \code{uses_mapping} checks for checking required mappings before \code{uses_extra_mappings}.
#'
#' @param p A ggplot object or a layer extracted from a ggplot object with
#'   \code{\link{get_geom_layer}} or \code{\link{get_stat_layer}}.
#' @param mappings One or more aesthetic mappings created with
#'   \code{\link[ggplot2]{aes}}.
#' @param local_only If \code{TRUE}, \code{uses_extra_mappings} will check only the
#'   mappings defined locally in a layer for the presence of \code{mappings}. If
#'   \code{FALSE}, \code{uses_extra_mappings} will check for \code{mappings} in the
#'   combination of global and local methods that will be used to plot a layer.
#'
#' @return A logical value.
#' @export
#'
#' @examples
#' require(ggplot2)
#' p <- ggplot(data = diamonds, aes(x = cut, sample = price)) +
#'   geom_qq()
#' uses_extra_mappings(p, aes(sample = price))
uses_extra_mappings <- function(p, mappings, local_only = FALSE) {
  aes_map <- get_mappings(p, local_only)
  aes_names <- names(aes_map)
  mapping_names <- names(mappings)
  # the plot has any variables beyond target mappings
  any(!(aes_names %in% mapping_names))
}

#' Does a plot use one or more aesthetics?
#'
#' \code{uses_aesthetics} checks whether the student used one or more aesthetics.
#'
#' By default, \code{uses_aesthetics} requires that only one of the
#' aesthetics need to be used. Set \code{exact} to \code{TRUE} to check if all of
#' the variables have to be matched exactly.
#'
#' @param p A ggplot object or a layer extracted from a ggplot object with
#'   \code{\link{get_geom_layer}} or \code{\link{get_stat_layer}}..
#' @param aesthetics character vector of variables to check for, e.g. "x" or c("x")
#' @param exact If \code{TRUE}, variables need to be mapped exactly
#' @param local_only \code{TRUE} or \code{FALSE}. Should \code{uses_aesthetics} only
#'   return mappings defined locally in the layer?
#'
#' @return A logical value.
#' @export
#'
#' @examples
#' require(ggplot2)
#' p <- ggplot(data = mpg, mapping = aes(x = displ, y = hwy)) +
#'   geom_point(mapping = aes(color = class))
#' uses_aesthetics(p, "x")
#' uses_aesthetics(p, c("x", "y"))
#' uses_aesthetics(get_geom_layer(p, "point"), c("x", "y", "color"), local_only = TRUE)
#' uses_aesthetics(get_geom_layer(p, "point"), c("x", "y"), local_only = FALSE)
uses_aesthetics <- function(p, aesthetics, local_only = FALSE, exact = FALSE) {
  pmaps_names <- names(get_mappings(p, local_only = local_only))
  # NOTE: ggplot2 seems to switch aesthetic color to colour, so we standardize it to 'color'
  pmaps_names[which(pmaps_names == "colour")] <- "color"
  aesthetics[which(aesthetics == "colour")] <- "color"
  if (exact) {
    return(identical(aesthetics, pmaps_names))
  } else {
    return(any(aesthetics %in% pmaps_names))
  }
}

#' Return the aesthetic mappings used by the ith layer
#'
#' \code{ith_mappings} returns the mappings used by a ggplot object or a single
#' layer extracted from the object with \code{\link{get_geom_layer}} or \code{\link{get_stat_layer}}.
#'
#' Functions that use the \code{ith_} prefix are
#' designed to eliminate the need to call \code{get_layer} to check a specific
#' layer in a plot, e.g. \code{p %>% get_geom_layer(geom = "point") %>% get_mappings()}.
#'
#' @param p A ggplot object or a layer extracted from a ggplot object with
#'   \code{\link{get_geom_layer}} or \code{\link{get_stat_layer}}.
#' @param i A numerical index that corresponds to the first layer of a plot (1),
#'   the second layer (2), and so on. \code{ith_mappings_use} will check the
#'   aesthetics used by the ith layer.
#' @param local_only If \code{TRUE}, \code{ith_mappings_use} will check only the
#'   mappings defined locally in a layer for the presence of \code{mappings}. If
#'   \code{FALSE}, \code{ith_mappings_use} will check for \code{mappings} in the
#'   combination of global and local methods that will be used to plot a layer.
#'
#' @return A list with class uneval, as returned by \code{\link[ggplot2]{aes}}
#'   Components of the list are either quosures or constants.
#'
#' @family functions for checking mappings
#'
#' @export
#'
#' @examples
#' require(ggplot2)
#' p <- ggplot(data = mpg, mapping = aes(x = displ, y = hwy)) +
#'   geom_point(mapping = aes(color = class)) +
#'   geom_smooth()
#' ith_mappings(p, i = 1, local_only = FALSE)
#' ith_mappings(p, i = 1, local_only = TRUE)
#' ith_mappings(p, i = 2, local_only = FALSE)
ith_mappings <- function(p, i, local_only = FALSE) {
  stop_if_not_ggplot(p)
  get_mappings(get_layer(p, i = i), local_only)
}

#' Does the ith layer use one or more aesthetic mappings?
#'
#' \code{ith_mappings_use} checks whether the student uses the supplied mappings
#' in the ith layer of their plot.
#'
#' \code{ith_mappings_use} ignores whether or not the student supplied
#' additional mappings as well. Functions that use the \code{ith_} prefix are
#' designed to eliminate the need to call \code{get_layer} to check a specific
#' layer in a plot, e.g. \code{p %>% get_geom_layer(geom = "point") %>%
#' uses_mappings(aes(color = class))}.
#'
#' @param p A ggplot object or a layer extracted from a ggplot object with
#'   \code{\link{get_geom_layer}} or \code{\link{get_stat_layer}}.
#' @param mappings One or more aesthetic mappings created with
#'   \code{\link[ggplot2]{aes}}.
#' @param i A numerical index that corresponds to the first layer of a plot (1),
#'   the second layer (2), and so on. \code{ith_mappings_use} will check the
#'   aesthetics used by the ith layer.
#' @param local_only If \code{TRUE}, \code{ith_mappings_use} will check only the
#'   mappings defined locally in a layer for the presence of \code{mappings}. If
#'   \code{FALSE}, \code{ith_mappings_use} will check for \code{mappings} in the
#'   combination of global and local methods that will be used to plot a layer.
#' @param exact If \code{TRUE}, mappings need to be mapped exactly
#'
#' @return A logical value
#'
#' @family functions for checking mappings
#'
#' @export
#'
#' @examples
#' require(ggplot2)
#' p <- ggplot(data = mpg, mapping = aes(x = displ, y = hwy)) +
#'   geom_point(mapping = aes(color = class)) +
#'   geom_smooth()
#' ith_mappings_use(p, i = 1, aes(x = displ), local_only = FALSE)
#' ith_mappings_use(p, i = 1, aes(x = displ), local_only = TRUE)
#' ith_mappings_use(p, i = 2, aes(x = displ, y = hwy), local_only = FALSE)
ith_mappings_use <- function(p, mappings, i, local_only = FALSE, exact = FALSE) {
  layer <- get_layer(p, i = i)
  aes_map <- get_mappings(layer, local_only)
  if (exact) {
    return(identical_aes(mappings, aes_map))
  } else {
    return(
      all(names(mappings) %in% names(aes_map)) &&
        identical_aes(mappings, aes_map[names(mappings)])
    )
  }
}
rstudio-education/ggcheck documentation built on May 12, 2023, 11:33 a.m.