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
7 changes: 7 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,13 @@ Theme fixes:
column arrangements. (#562 @zeileis)
- `plt(..., facet = z ~ 1)` <-> `plt(..., facet = ~z, facet.args = list(ncol = 1))`
- `plt(..., facet = 1 ~ z)` <-> `plt(..., facet = ~z, facet.args = list(nrow = 1))`.
- `x/ylim` gain several "smart" override forms. (#644 @grantmcdermott)
- A single scalar (e.g. `ylim = 0`) ensures that value is covered by the
axis range, e.g. for forcing zero onto a coefficient plot.
- A length-2 vector with one `NA` (e.g. `ylim = c(0, NA)`) pins the non-`NA`
limit and lets the data determine the other.
- The string `"rev"` (or `"reverse"`) reverses the auto-computed axis range,
without needing to know the data extent in advance.
- Type-specific updates:
- `type_barplot()` gains an `offset` argument for shifting bar baselines away
from zero. (#611, #615 @grantmcdermott @zeileis)
Expand Down
24 changes: 22 additions & 2 deletions R/facet.R
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ draw_facet_window = function(
axes, flip, frame.plot, oxaxis, oyaxis,
xlabs, xlim, null_xlim, xaxt, xaxs, xaxb, xaxl,
ylabs, ylim, null_ylim, yaxt, yaxs, yaxb, yaxl,
rev_x = FALSE, rev_y = FALSE,
asp, log,
# other args (in approx. alphabetical + group ordering)
dots,
Expand Down Expand Up @@ -355,8 +356,23 @@ draw_facet_window = function(
yfree = if (!is.null(facet)) split(c(y, ymin, ymax), facet)[[ii]] else c(y, ymin, ymax)
if (null_xlim) xlim = range(xfree, na.rm = TRUE)
if (null_ylim) ylim = range(yfree, na.rm = TRUE)
xext = extendrange(xlim, f = 0.04)
yext = extendrange(ylim, f = 0.04)
# An axis is reversed either via the `rev_x`/`rev_y` flag (e.g. the
# "reverse" keyword) or when the user supplies descending fixed limits
# (e.g. xlim = c(10, 0)). The latter must be detected before extendrange()
# below, which always returns an ascending pair and would otherwise drop
# the descending order. (#644)
rev_xext = isTRUE(rev_x) || (!null_xlim && length(xlim) == 2L && xlim[2L] < xlim[1L])
rev_yext = isTRUE(rev_y) || (!null_ylim && length(ylim) == 2L && ylim[2L] < ylim[1L])
# extendrange() returns an ascending pair, so reverse afterwards
xext = extendrange(sort(xlim), f = 0.04)
yext = extendrange(sort(ylim), f = 0.04)
# base axTicks() misbehaves on a reversed usr (it collapses to a single
# tick), so precompute ticks from the ascending extent and pass them as
# an explicit `at` below; placement against the reversed usr is fine.
xat = if (rev_xext) axisTicks(usr = xext, log = par("xlog")) else NULL
yat = if (rev_yext) axisTicks(usr = yext, log = par("ylog")) else NULL
if (rev_xext) xext = rev(xext)
if (rev_yext) yext = rev(yext)
# We'll save this in a special .fusr env var (list) that we'll re-use
# when it comes to plotting the actual elements later
if (ii == 1) {
Expand All @@ -371,12 +387,16 @@ draw_facet_window = function(
# if plot frame is true then print axes per normal...
if (type %in% c("barplot", "pointrange", "errorbar", "ribbon", "boxplot", "p", "violin") && !is.null(xlabs)) {
tinyAxis(xfree, side = xside, at = xlabs, labels = names(xlabs), type = xaxt, labeller = xaxl)
} else if (!is.null(xat)) {
tinyAxis(xfree, side = xside, at = xat, type = xaxt, labeller = xaxl)
} else {
tinyAxis(xfree, side = xside, type = xaxt, labeller = xaxl)
}
if (.ymgp_shift > 0) par(mgp = par("mgp") - c(0, .ymgp_shift, 0))
if (isTRUE(flip) && type %in% c("barplot", "pointrange", "errorbar", "ribbon", "boxplot", "p", "violin") && !is.null(ylabs)) {
tinyAxis(yfree, side = yside, at = ylabs, labels = names(ylabs), type = yaxt, labeller = yaxl)
} else if (!is.null(yat)) {
tinyAxis(yfree, side = yside, at = yat, type = yaxt, labeller = yaxl)
} else {
tinyAxis(yfree, side = yside, type = yaxt, labeller = yaxl)
}
Expand Down
1 change: 1 addition & 0 deletions R/flip.R
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ flip_datapoints = function(settings) {
swap_elements(settings, "xlab", "ylab")
swap_elements(settings, "xlabs", "ylabs")
swap_elements(settings, "xlim", "ylim")
swap_elements(settings, "rev_x", "rev_y")
swap_elements(settings, "xmax", "ymax")
swap_elements(settings, "xmin", "ymin")

Expand Down
68 changes: 66 additions & 2 deletions R/lim.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ lim_args = function(settings) {
settings,
environment(),
c(
"xaxb", "xlabs", "xlim", "null_xlim",
"yaxb", "ylabs", "ylim", "null_ylim",
"xaxb", "xlabs", "xlim", "null_xlim", "rev_x",
"yaxb", "ylabs", "ylim", "null_ylim", "rev_y",
"datapoints", "type"
)
)
Expand All @@ -26,11 +26,21 @@ lim_args = function(settings) {
xlim = range(as.numeric(c(
datapoints[["x"]], datapoints[["xmin"]],
datapoints[["xmax"]])), finite = TRUE)
} else if (length(xlim) != 2L || anyNA(xlim)) {
xdrng = range(as.numeric(c(
datapoints[["x"]], datapoints[["xmin"]],
datapoints[["xmax"]])), finite = TRUE)
xlim = resolve_lim(xlim, xdrng, "xlim")
}
if (is.null(ylim)) {
ylim = range(as.numeric(c(
datapoints[["y"]], datapoints[["ymin"]],
datapoints[["ymax"]])), finite = TRUE)
} else if (length(ylim) != 2L || anyNA(ylim)) {
ydrng = range(as.numeric(c(
datapoints[["y"]], datapoints[["ymin"]],
datapoints[["ymax"]])), finite = TRUE)
ylim = resolve_lim(ylim, ydrng, "ylim")
}

if (identical(type, "boxplot")) {
Expand All @@ -40,10 +50,64 @@ lim_args = function(settings) {
if (null_xlim && !is.null(xaxb) && type != "spineplot") xlim = range(c(xlim, xaxb))
if (null_ylim && !is.null(yaxb) && type != "spineplot") ylim = range(c(ylim, yaxb))

# reverse axis direction last, once the range is otherwise finalized
if (isTRUE(rev_x)) xlim = rev(xlim)
if (isTRUE(rev_y)) ylim = rev(ylim)

# update settings
env2env(
environment(),
settings,
c("xlim", "ylim", "xlabs", "ylabs", "xaxb", "yaxb")
)
}


#
# x/ylim helpers ----
#

# Resolve a user-supplied x/ylim that may be a scalar or contains a single NA.
# `lim` : raw user value (already known to be non-NULL)
# `drng` : data range, 2-element numeric, i.e. range(..., finite = TRUE)
# Returns a 2-element numeric vector (raw; window padding applied downstream).
resolve_lim = function(lim, drng, arg = "xlim") {
if (!is.numeric(lim)) {
stop(sprintf("`%s` must be numeric (a scalar or length-2 vector).", arg), call. = FALSE)
}
n = length(lim)
if (n == 1L) {
if (is.na(lim)) stop(sprintf("`%s` cannot be a single `NA`.", arg), call. = FALSE)
# scalar: ensure the value is covered alongside the data
return(range(c(drng, lim)))
}
if (n == 2L) {
nas = is.na(lim)
if (all(nas)) {
stop(sprintf("`%s` cannot be `c(NA, NA)`; supply at least one finite limit.", arg), call. = FALSE)
}
if (!any(nas)) return(lim) # full override: unchanged (reversed axis OK)
# exactly one NA: fill that side from the data range, pin the other verbatim
if (nas[1L]) lim[1L] = drng[1L] else lim[2L] = drng[2L]
return(lim)
}
stop(sprintf("`%s` must be length 1 or 2, not length %d.", arg, n), call. = FALSE)
}

# Resolve an axis-reversal keyword passed to x/ylim, e.g. xlim = "reverse" (or
# the "rev" abbreviation). Returns a list with the (possibly NULL-ified) limit
# and a logical flag. When a keyword is detected we strip the limit back to NULL
# so that all the usual auto-range machinery (data range, breaks, free facets,
# type-specific limits) runs untouched; the flag is consumed later to rev() the
# finalized range.
sanitize_lim_rev = function(lim, arg = "xlim") {
if (is.character(lim)) {
rev_ok = length(lim) == 1L && !is.na(lim) &&
nchar(lim) >= 3L && pmatch(tolower(lim), "reverse", nomatch = 0L) == 1L
if (!rev_ok) {
stop(sprintf('If `%s` is a character string it must be "reverse" (or "rev").', arg), call. = FALSE)
}
return(list(lim = NULL, rev = TRUE))
}
list(lim = lim, rev = FALSE)
}
27 changes: 24 additions & 3 deletions R/tinyplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,19 @@
#' @param ann a logical value indicating whether the default annotation (title
#' and x and y axis labels) should appear on the plot.
#' @param xlim the x limits (x1, x2) of the plot. Note that x1 > x2 is allowed
#' and leads to a ‘reversed axis’. The default value, NULL, indicates that
#' the range of the `finite` values to be plotted should be used.
#' @param ylim the y limits of the plot.
#' and leads to a reversed axis (although see the `"rev(erse)"` keyword option
#' below). The default value, `NULL`, indicates that the range of the `finite`
#' values to be plotted should be used. Alongside the standard length-2 vector
#' (e.g., `xlim = c(0, 1)`), `tinyplot` supports three further convenience
#' forms:
#' - a single scalar (e.g. `xlim = 0`) ensures that the provided value is
#' covered by the axis range, irrespective of the data extent.
#' - a length-2 vector with one `NA` (e.g. `xlim = c(0, NA)`) pins the
#' non-`NA` limit and lets the data determine the other limit.
#' - the convenience string `"rev"` (or the longer `"reverse"`) reverses the
#' automatically-computed axis range. This is equivalent to passing a
#' descending vector, but without having to know the data extent in advance.
#' @param ylim the y limits of the plot. Accepts the same input forms as `xlim`.
#' @param axes logical or character. Should axes be drawn (`TRUE` or `FALSE`)?
#' Or alternatively what type of axes should be drawn: `"standard"` (with
#' axis, ticks, and labels; equivalent to `TRUE`), `"none"` (no axes;
Expand Down Expand Up @@ -788,6 +798,13 @@ tinyplot.default = function(

dots = list(...)

# resolve any axis-reversal keyword (e.g. xlim = "reverse") into a flag, and
# reset the limit to NULL so the normal auto-range path runs (see lim.R)
.revx = sanitize_lim_rev(xlim, "xlim")
xlim = .revx[["lim"]]; rev_x = .revx[["rev"]]
.revy = sanitize_lim_rev(ylim, "ylim")
ylim = .revy[["lim"]]; rev_y = .revy[["rev"]]

settings_list = list(
# save call to check user input later
call = match.call(),
Expand Down Expand Up @@ -849,6 +866,8 @@ tinyplot.default = function(
frame.plot = frame.plot,
xlim = xlim,
ylim = ylim,
rev_x = rev_x,
rev_y = rev_y,

# flags to check user input (useful later on)
null_by = is.null(by),
Expand Down Expand Up @@ -1357,6 +1376,7 @@ tinyplot.default = function(
oxaxis = oxaxis, oyaxis = oyaxis,
xlabs = xlabs, xlim = xlim, null_xlim = null_xlim, xaxt = xaxt, xaxs = xaxs, xaxb = xaxb, xaxl = xaxl,
ylabs = ylabs, ylim = ylim, null_ylim = null_ylim, yaxt = yaxt, yaxs = yaxs, yaxb = yaxb, yaxl = yaxl,
rev_x = rev_x, rev_y = rev_y,
asp = asp, log = log,
# other args (in approx. alphabetical + group ordering)
dots = dots,
Expand Down Expand Up @@ -1389,6 +1409,7 @@ tinyplot.default = function(
oxaxis = oxaxis, oyaxis = oyaxis,
xlabs = xlabs, xlim = xlim, null_xlim = null_xlim, xaxt = xaxt, xaxs = xaxs, xaxb = xaxb, xaxl = xaxl,
ylabs = ylabs, ylim = ylim, null_ylim = null_ylim, yaxt = yaxt, yaxs = yaxs, yaxb = yaxb, yaxl = yaxl,
rev_x = rev_x, rev_y = rev_y,
asp = asp, log = log,
dots = dots,
draw = draw,
Expand Down
2 changes: 2 additions & 0 deletions R/zzz.R
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
"oxaxis",
"oyaxis",
"pch",
"rev_x",
"rev_y",
"ribbon.alpha",
"split_data",
"tpars",
Expand Down
117 changes: 117 additions & 0 deletions inst/tinytest/_tinysnapshot/lim_partial_na.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading