Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

S3method(print,recordedtinyplot)
S3method(str,recordedtinyplot)
S3method(tinyplot,array)
S3method(tinyplot,data.frame)
S3method(tinyplot,default)
S3method(tinyplot,density)
Expand Down
18 changes: 18 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,17 @@ related to plot layering. See "Bug fixes" below.
the new `x/ypad` arguments (see below) to match their own aesthetic
preferences. (#732 @grantmcdermott)

#### New `tinyplot.*` methods

- `tinyplot.array()`: for (unclassed) `array` objects with up to four
dimensions. This extends the `tinyplot.matrix()` conventions by mapping the
third dimension to facets, and a fourth to a facet grid. The names of the
dimnames (if any) are used for the axis, legend, and facet titles. Arrays
with a length-2 dimension (e.g., `gait`) are plotted as x/y pairs, i.e. one
slice against the other. See the `tinyplot.array`-specific `xy` argument.
Passing `facet = FALSE` draws everything in a single panel instead.
(#746 @grantmcdermott)

#### Other new features

- New top-level `tinyplot()`/`plt()` arguments:
Expand Down Expand Up @@ -243,6 +254,13 @@ related to plot layering. See "Bug fixes" below.

### Bug fixes

- Degenerate array inputs, i.e. 1-row or 1-column matrices, are now dropped
to plain vectors, so that e.g. `tinyplot(1:10, array(1:10, c(1, 10)))` works
just like `plot()`. (#548, #746 @tony-aw @zeileis @grantmcdermott)
- Two-sided facet formulas (`facet = rows ~ cols`) now work with the default
(non-formula) method, e.g. `tinyplot(x, y, facet = a ~ b, data = dat)`.
Previously this errored because `data` was not forwarded. (#746
@grantmcdermott)
- Free facets (`facet.args = list(free = TRUE)`) now respect `asp`.
(#744 @grantmcdermott)
- Multi-line x-axis tick labels (e.g. `"Hello\nWorld"`) are now spaced
Expand Down
21 changes: 15 additions & 6 deletions R/facet.R
Original file line number Diff line number Diff line change
Expand Up @@ -1430,19 +1430,28 @@ get_facet_fml = function(formula, data = NULL) {
xfacet = interaction(xfacet, sep = ":")
if (no_yfacet) {
ret = xfacet
attr(ret, "facet_vars") = list(x = xfacet_vars, y = NULL)
} else {
# yfacet = interaction(yfacet, sep = ":")
## NOTE: We "swap" the formula LHS and RHS since mfrow plots rowwise
ret = interaction(xfacet, yfacet, sep = "~")
attr(ret, "facet_grid") = TRUE
attr(ret, "facet_nrow") = length(unique(yfacet))
ret = facet_grid_factor(xfacet, yfacet, xfacet_vars, yfacet_vars)
}
attr(ret, "facet_vars") = list(x = xfacet_vars, y = yfacet_vars)

return(ret)
}


## Combine the column (x) and row (y) facet factors into a single grid facet,
## with the attributes that facet_layout() and facet_titles() expect. NOTE: the
## columns come first (i.e. we "swap" a formula's LHS and RHS), since mfrow
## plots rowwise.
facet_grid_factor = function(xfacet, yfacet, xvars, yvars) {
out = interaction(xfacet, yfacet, sep = "~")
attr(out, "facet_grid") = TRUE
attr(out, "facet_nrow") = length(unique(yfacet))
attr(out, "facet_vars") = list(x = xvars, y = yvars)
out
}


## Are a facet's interior tick labels visually anchored?
##
## draw_facet_axis() keys the "outer facets only" rule off framing, on the basis
Expand Down
14 changes: 13 additions & 1 deletion R/sanitize_datapoints.R
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ sanitize_datapoints = function(settings) {
)
)

## drop degenerate dimensions from array inputs, e.g. a 1-row or 1-column
## matrix is just a vector (#548)
drop_dims = function(z) {
if (length(dim(z)) > 1L && sum(dim(z) > 1L) <= 1L) drop(z) else z
}
x = drop_dims(x); xmin = drop_dims(xmin); xmax = drop_dims(xmax)
y = drop_dims(y); ymin = drop_dims(ymin); ymax = drop_dims(ymax)

## coerce character and logical variables to factors
## (aside: we won't risk converting x and y logicals to factors b/c it can
## mess up types that rely on predict underneath the hood, e.g type_lm)
Expand Down Expand Up @@ -53,5 +61,9 @@ sanitize_datapoints = function(settings) {
}

# potentially modified variables
env2env(environment(), settings, c("x", "y", "xaxt", "datapoints"))
env2env(
environment(),
settings,
c("x", "xmin", "xmax", "y", "ymin", "ymax", "xaxt", "datapoints")
)
}
6 changes: 2 additions & 4 deletions R/sanitize_facet.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ sanitize_facet = function(settings) {
settings,
environment(),
c(
"facet", "by", "null_facet", "facet_attr", "facet_by",
"facet", "data", "by", "null_facet", "facet_attr", "facet_by",
"by_dep", "facet_dep", "legend_args"
)
)
Expand All @@ -26,10 +26,8 @@ sanitize_facet = function(settings) {
# facet titles inherit the "by" variable name (same as the legend title)
facet_vars = list(x = facet_var_list(by, legend_args[["title"]] %||% by_dep))
} else if (inherits(facet, "formula")) {
# (grid layout is read off the "facet_grid" attribute downstream)
facet = get_facet_fml(facet, data = data)
if (isTRUE(attr(facet, "facet_grid"))) {
facet.args[["nrow"]] = attr(facet, "facet_nrow")
}
facet_vars = attr(facet, "facet_vars")
} else {
# recorded by tinyplot.formula(), else fall back to the deparsed input of
Expand Down
11 changes: 4 additions & 7 deletions R/tinyplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,7 @@ tinyplot.default = function(

# unevaluated expressions with side effects
draw = substitute(draw),
data = data, # for facet formulas passed to the default method
facet = facet,
facet.args = facet.args,
palette = substitute(palette),
Expand Down Expand Up @@ -2209,25 +2210,21 @@ tinyplot.formula = function(
} else {
if (xtype %in% c("none", "empty")) {
facet = yfacet
fvars = list(x = yfacet_vars)
attr(facet, "facet_vars") = list(x = yfacet_vars)
if (xtype == "empty") {
if (is.null(facet.args)) facet.args = list()
if (is.null(facet.args[["nrow"]])) facet.args[["nrow"]] = length(unique(yfacet))
}
} else if (ytype %in% c("none", "empty")) {
facet = xfacet
fvars = list(x = xfacet_vars)
attr(facet, "facet_vars") = list(x = xfacet_vars)
if (ytype == "empty") {
if (is.null(facet.args)) facet.args = list()
if (is.null(facet.args[["nrow"]])) facet.args[["nrow"]] = 1L
}
} else {
facet = interaction(xfacet, yfacet, sep = "~")
attr(facet, "facet_grid") = TRUE
attr(facet, "facet_nrow") = length(unique(yfacet))
fvars = list(x = xfacet_vars, y = yfacet_vars)
facet = facet_grid_factor(xfacet, yfacet, xfacet_vars, yfacet_vars)
}
attr(facet, "facet_vars") = fvars
}
} else if (!is.null(facet) && !inherits(facet, "formula") &&
is.null(attr(facet, "facet_vars")) && !identical(facet, "by")) {
Expand Down
208 changes: 208 additions & 0 deletions R/tinyplot.array.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#' tinyplot Method for Plotting Arrays
#'
#' @description Convenience interface for visualizing
#' \code{\link[base]{array}} objects with tinyplot. Extends the
#' \code{\link{tinyplot.matrix}} conventions to arrays with up to four
#' dimensions, by mapping the higher dimensions to facets.
#'
#' @details The first two dimensions of the array are treated exactly like the
#' rows and columns of a matrix; see \code{\link{tinyplot.matrix}}. By default,
#' that means the values are plotted against the (first dimension) row index,
#' with a separate `by` group for each element of the second dimension.
#' Higher dimensions are then mapped to facets:
#'
#' - 1D arrays are treated as a single-column matrix, i.e. a simple index
#' plot. If a `y` variable is also supplied, e.g. `tinyplot(tapply(...), y)`,
#' then the array is instead treated as a plain `x` vector.
#' - 2D arrays are matrices, and hence dispatch to
#' \code{\link{tinyplot.matrix}}.
#' - 3D arrays are faceted by the third dimension (i.e., a "facet wrap").
#' - 4D arrays are faceted by the third and fourth dimensions, as the rows
#' and columns of a "facet grid", respectively.
#' - Arrays with more than four dimensions are not supported. Subset or
#' reshape them first.
#'
#' Dimension names are used to label the groups, facets, and axis ticks, while
#' the names of the dimnames (if any) are used as the axis, legend, and facet
#' titles. To assign the dimensions to different roles, rearrange them first
#' with \code{\link[base]{aperm}}.
#'
#' **x/y pairs.** Arrays often hold a stack of matrices, one per variable,
#' e.g. the hip and knee angles of the `gait` data (`Time` x `Subject` x
#' `Variable`). If an array of three or more dimensions has exactly one
#' length-2 dimension besides the first, then its two slices are plotted
#' against each other, as x and y. Each of the remaining dimensions then
#' shifts down a role:
#'
#' - The first dimension becomes `by`, and orders the points along each path.
#' It is kept numeric where possible (its dimnames, e.g. times, or else its
#' index), so that each path is drawn as a line with a colour gradient. If
#' the dimnames are not numeric, then the groups are discrete and drawn as
#' points instead.
#' - The second dimension becomes a facet wrap (e.g. one panel per subject).
#' - The third dimension (if any) makes this a facet grid, as its columns.
#'
#' Control this via the `xy` argument. Tile and heatmap types are exempt,
#' since they need the array values as their fill.
#'
#' Note that contingency tables created by \code{\link[base]{table}} (e.g.,
#' `HairEyeColor` or `Titanic`) are of class `"table"` and hence do not
#' dispatch to this method.
#'
#' @inheritParams tinyplot.matrix
#' @param x an object of class `"array"`.
#' @param facet must be `NULL` (the default) or `FALSE` for arrays with three
#' or more dimensions, since the facets are then determined by the array
#' dimensions. `FALSE` draws everything in a single panel instead, by folding
#' the facet dimensions into the `by` groups. For x/y pairs, these groups
#' then replace the first dimension as `by`, so that each path is drawn
#' separately. Not supported for tile and heatmap types.
#' @param xy which dimension (if any) holds x/y pairs; see Details. The default
#' `NULL` auto-detects it, `FALSE` opts out, and a dimension index or name
#' selects it explicitly. The selected dimension must have length 2, and its
#' dimnames (if any) are used as the axis titles.
#' @param ... further arguments passed to `tinyplot`.
#'
#' @inherit tinyplot return
#'
#' @seealso \code{\link{tinyplot.matrix}}, \code{\link[graphics]{matplot}}
#'
#' @examples
#' # 3D array: facet wrap by the third dimension
#' sims = array(
#' cumsum(rnorm(20 * 3 * 4)), dim = c(20, 3, 4),
#' dimnames = list(NULL, paste("Series", 1:3), paste("Run", 1:4))
#' )
#' tinyplot(sims, type = "l")
#'
#' # 4D array: facet grid by the third and fourth dimensions
#' sims4 = array(
#' rnorm(20 * 3 * 2 * 2), dim = c(20, 3, 2, 2),
#' dimnames = list(
#' time = NULL, series = paste0("s", 1:3),
#' model = c("A", "B"), scenario = c("low", "high")
#' )
#' )
#' tinyplot(sims4, type = "b", facet.args = list(prefix = TRUE))
#'
#' # tile/heatmap types lay out each 2D slice as a grid
#' tinyplot(sims4[1:5, , , ], type = "heatmap", theme = "heatmap")
#'
#' @examplesIf getRversion() >= "4.5.0"
#' # x/y pairs: a length-2 dimension is plotted as x vs y
#' # (e.g., hip vs knee angle through the gait cycle, coloured by time and
#' # faceted by boy)
#' gait9 = gait[, 1:9, ] # take a subset for demonstration
#' tinyplot(gait9)
#'
#' # all boys in a single panel instead
#' tinyplot(gait9, facet = FALSE)
#'
#' # ... or in the background of each boy's own panel
#' tinyplot(
#' gait9,
#' draw = tinyplot(gait9, col = "lightgray", facet = FALSE, add = TRUE)
#' )
#'
#' # opt out of x/y pairs, to plot the angles against time instead
#' tinyplot(gait9, type = "l", xy = FALSE, legend = FALSE)
#'
#' @export
tinyplot.array = function(x, type = NULL, legend = NULL, facet = NULL, xlab = NULL, ylab = NULL, xy = NULL, ...) {
dep = deparse1(substitute(x))
nd = length(dim(x))
if (nd > 4L) {
stop(
"Arrays with more than 4 dimensions are not supported. ",
"Please subset or reshape your array first.",
call. = FALSE
)
}
if (nd == 1L) {
## A 1D array (e.g. from tapply()) passed alongside a `y` variable is just
## an x vector, so hand the call on to the default method. We check the
## unmatched call, since a positional `y` would otherwise be matched to
## one of this method's formals (`type`, `legend`, etc.).
cl = sys.call()
nms = names(cl) %||% character(length(cl))
if ("y" %in% nms || (length(cl) > 2L && !nzchar(nms[3L]))) {
cl[[1L]] = tinyplot.default
return(eval(cl, parent.frame()))
}
## Otherwise, it is treated as a single-column matrix
dn = dimnames(x)
dim(x) = c(length(x), 1L)
if (!is.null(dn)) dimnames(x) = c(dn, list(NULL))
}

## Split off a length-2 dimension holding x/y pairs (if any), leaving two
## arrays of one dimension less: the x values, and the matching y values
y = NULL
k = array_xy_dim(x, xy = xy, type = type)
if (!is.null(k)) {
si = slice.index(x, k)
xlvls = dimnames(x)[[k]]
if (is.null(xlvls)) {
## e.g. "arr[, , 1]" and "arr[, , 2]"
xlvls = vapply(1:2, function(i) {
idx = character(nd)
idx[k] = i
paste0(dep, "[", paste(idx, collapse = ", "), "]")
}, character(1))
}
if (is.null(xlab)) xlab = xlvls[1]
if (is.null(ylab)) ylab = xlvls[2]
y = array(x[si == 2L], dim(x)[-k], dimnames(x)[-k])
x = array(x[si == 1L], dim(x)[-k], dimnames(x)[-k])
}

## (facet = FALSE is fine for any array: it folds the facets into `by`)
if (is.null(k) && nd <= 2L) {
if (!isFALSE(facet)) assert_choice(facet, "by", null.ok = TRUE)
} else if (!is.null(facet) && !isFALSE(facet)) {
stop(
"`facet` must be NULL or FALSE for arrays with 3 or more dimensions (or ",
"x/y pairs), since the facets are determined by the array dimensions.",
call. = FALSE
)
}
array_plot(
x, y = y, type = type, legend = legend, facet = facet,
xlab = xlab, ylab = ylab, dep = dep, ...
)
}


## Which dimension of an array (if any) holds x/y pairs. Auto-detected as the
## only length-2 dimension besides the first (which orders the paths), unless
## the user sets `xy` explicitly: a dimension index or name, or FALSE to opt
## out. Tile and heatmap types are skipped, since they need the array values
## as their fill, leaving nothing to split into x and y.
array_xy_dim = function(x, xy = NULL, type = NULL) {
if (isFALSE(xy)) return(NULL)
dims = dim(x)
grid_type = is_grid_type(type)
if (is.null(xy)) {
k = setdiff(which(dims == 2L), 1L)
if (length(dims) < 3L || length(k) != 1L || grid_type) return(NULL)
return(k)
}
k = if (is.character(xy)) match(xy, names(dimnames(x))) else xy
if (length(k) != 1L || is.na(k) || !k %in% seq_along(dims)) {
stop(
"`xy` must be FALSE, or a single dimension index or name.",
call. = FALSE
)
}
if (dims[k] != 2L) {
stop("The `xy` dimension must have length 2.", call. = FALSE)
}
if (grid_type) {
stop(
"`xy` is not supported for \"tile\" or \"heatmap\" types, since they ",
"need the array values as their fill.",
call. = FALSE
)
}
k
}
Loading
Loading