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
9 changes: 7 additions & 2 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,17 @@ related to plot layering. See "Bug fixes" below.
#### Other new features

- New top-level `tinyplot()`/`plt()` arguments:
- `xpad` and `ypad` enable control over how much padding (as a fraction of the
data range) is added to each end of the axes. Following base R conventions,
the default for most plots is `0.04`, i.e. 4% padding on each side. Also
settable globally via `tpar(xpad = <xpad>, ypad = <ypad>)`.
(#729 @grantmcdermott)
- `xaxr` and `yaxr` enable rotating of the x- and y-axis tick labels by
arbitrary angles, closing a long-standing feature request (#346). Note that
setting one overrides `las` for that axis. Best combined with a dynamic
theme, since the plot margins are resized to fit the rotated labels. Also
settable globally via `tpar("x/yaxr")` and thus as part of a `tinytheme`
too. (#717 @grantmcdermott)
settable globally via `tpar(xaxr = <xaxr>, yaxr = <yaxr>)` and thus as part
of a `tinytheme` too. (#717 @grantmcdermott)
- (Experimental) `record` enables recording plots as replayable objects,
closing another long-standing feature request (#121). Specifically, setting
`record = TRUE` returns a `"recordedtinyplot"` object (see
Expand Down
34 changes: 27 additions & 7 deletions R/facet.R
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ draw_facet_window = function(
facet_blank = FALSE,
# axes args
axes, flip, frame.plot, oxaxis, oyaxis,
xlabs, xlim, null_xlim, xaxt, xaxs, xaxb, xaxl, xaxr = NULL,
ylabs, ylim, null_ylim, yaxt, yaxs, yaxb, yaxl, yaxr = NULL,
xlabs, xlim, null_xlim, xaxt, xaxs, xaxb, xaxl, xaxr = NULL, xpad = NULL,
ylabs, ylim, null_ylim, yaxt, yaxs, yaxb, yaxl, yaxr = NULL, ypad = NULL,
rev_x = FALSE, rev_y = FALSE,
xlim_partial = NULL, ylim_partial = NULL,
facet_labs = NULL,
Expand Down Expand Up @@ -395,6 +395,10 @@ draw_facet_window = function(
if (log == "x") log_flip = "y"
if (log == "y") log_flip = "x"
}
# Explicit user pad -> calculated limits are already final, so avoid
# automatic par(x/yaxs = "r") expansion. "i" takes limits as fixed.
if (!is.null(ypad)) pdots[["xaxs"]] = "i"
if (!is.null(xpad)) pdots[["yaxs"]] = "i"
do.call(
"plot.window",
c(list(xlim = ylim, ylim = xlim, asp = asp, log = log_flip), pdots)
Expand All @@ -403,6 +407,11 @@ draw_facet_window = function(
yside = 1
} else {
## ... standard plot window for all other cases

# Explicit user pad -> calculated limits are already final, so avoid
# automatic par(x/yaxs = "r") expansion. "i" takes limits as fixed.
if (!is.null(xpad)) pdots[["xaxs"]] = "i"
if (!is.null(ypad)) pdots[["yaxs"]] = "i"
do.call(
"plot.window",
c(list(xlim = xlim, ylim = ylim, asp = asp, log = log), pdots)
Expand Down Expand Up @@ -485,13 +494,18 @@ draw_facet_window = function(
# each panel spans only the categories it uses.
.xall_cat = length(.fxlabs) > 0 && is.null(facet_labs[["x"]])
.yall_cat = length(.fylabs) > 0 && is.null(facet_labs[["y"]])
if (null_xlim || !is.null(xlim_partial)) {
# A panel that derives its own range still has to expand it; one that
# inherits the fixed limits does not, because lim_args() expanded those
# already. Re-expanding an inherited range applies the padding twice.
.derived_x = null_xlim || !is.null(xlim_partial)
.derived_y = null_ylim || !is.null(ylim_partial)
if (.derived_x) {
xlim = facet_free_lim(
if (.xall_cat) xcat else xfree, xall, xlim_partial, "xlim"
) + .pad
if (length(.fxlabs)) xlim = range(c(xlim, .fxlabs))
}
if (null_ylim || !is.null(ylim_partial)) {
if (.derived_y) {
ylim = facet_free_lim(
if (.yall_cat) ycat else yfree, yall, ylim_partial, "ylim"
)
Expand All @@ -504,9 +518,15 @@ draw_facet_window = function(
# 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)
# expand_lim() returns an ascending pair, so reverse afterwards
xext = sort(xlim)
yext = sort(ylim)
# Free panels expand at a flat rate regardless of xaxs/yaxs; the
# barplot_facet_free snapshot will catch you if you change that.
.padded_x = !is.null(xpad) && !.derived_x # lim_args() got to it first
.padded_y = !is.null(ypad) && !.derived_y
if (!.padded_x) xext = expand_lim(xext, xpad %||% 0.04)
if (!.padded_y) yext = expand_lim(yext, ypad %||% 0.04)
# A facet with a single distinct x (or y) value yields a zero-width
# extent, which par(usr=) rejects. Mirror base plot.window() and pad
# a degenerate range symmetrically so the facet still draws. (#668)
Expand Down
1 change: 1 addition & 0 deletions R/flip.R
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ flip_datapoints = function(settings) {
swap_elements(settings, "xlab", "ylab")
swap_elements(settings, "xlabs", "ylabs")
swap_elements(settings, "xlim", "ylim")
swap_elements(settings, "xpad", "ypad")
swap_elements(settings, "null_xlim", "null_ylim")
swap_elements(settings, "rev_x", "rev_y")
swap_elements(settings, "xmax", "ymax")
Expand Down
39 changes: 37 additions & 2 deletions R/lim.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ lim_args = function(settings) {
c(
"xaxb", "xlabs", "xlim", "null_xlim", "rev_x",
"yaxb", "ylabs", "ylim", "null_ylim", "rev_y",
"datapoints", "type", "type_hints"
"datapoints", "type", "type_hints", "xpad", "ypad", "log"
)
)

Expand Down Expand Up @@ -51,6 +51,15 @@ lim_args = function(settings) {
if (null_xlim && !is.null(xaxb) && !prop_lim) xlim = range(c(xlim, xaxb))
if (null_ylim && !is.null(yaxb) && !prop_lim) ylim = range(c(ylim, yaxb))

if (!is.null(xpad)) {
xlim = expand_lim(widen_degenerate(xlim), xpad,
log = grepl("x", log, fixed = TRUE))
}
if (!is.null(ypad)) {
ylim = expand_lim(widen_degenerate(ylim), ypad,
log = grepl("y", log, fixed = TRUE))
}

# reverse axis direction last, once the range is otherwise finalized
if (isTRUE(rev_x)) xlim = rev(xlim)
if (isTRUE(rev_y)) ylim = rev(ylim)
Expand All @@ -59,7 +68,7 @@ lim_args = function(settings) {
env2env(
environment(),
settings,
c("xlim", "ylim", "xlabs", "ylabs", "xaxb", "yaxb")
c("xlim", "ylim", "xpad", "ypad", "xlabs", "ylabs", "xaxb", "yaxb")
)
}

Expand All @@ -68,6 +77,32 @@ lim_args = function(settings) {
# x/ylim helpers ----
#

# Widen a data range by `pad` at each end, as a fraction of the range.
#
# A logged axis is expanded in log space, which is where base applies it too --
# expanding the raw values would put the padding in the wrong place entirely
# once the range spans decades.
expand_lim = function(lim, pad, log = FALSE) {
if (length(lim) != 2L || !all(is.finite(lim))) return(lim)
if (!is.finite(pad) || pad == 0) return(lim)
logged = isTRUE(log) && all(lim > 0)
if (logged) lim = log10(lim)
out = lim + c(-1, 1) * pad * diff(lim)
if (logged) out = 10^out
out
}

# Widen a zero-width range the way base R does before any style expansion is
# applied: out to 0.4 of the value either side, or to +/-1 when the value is
# zero. Kept separate from expand_lim() because the three places tinyplot
# expands a range do not currently agree on this rule (base's here, 0.04 of the
# value in free facets, half a unit in the dynmar predictor). Reconciling them
# is a behaviour change and deliberately not part of this one.
widen_degenerate = function(lim) {
if (length(lim) != 2L || !all(is.finite(lim)) || diff(lim) != 0) return(lim)
lim + c(-1, 1) * (if (lim[1L] == 0) 1 else 0.4 * abs(lim[1L]))
}

# 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)
Expand Down
18 changes: 15 additions & 3 deletions R/sanitize_axes.R
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
sanitize_axes = function(settings) {
env2env(settings, environment(),
c("axes", "xaxt", "yaxt", "frame.plot", "xaxr", "yaxr"))
c("axes", "xaxt", "yaxt", "frame.plot", "xaxr", "yaxr", "xpad", "ypad"))
## handle defaults of axes, xaxt, yaxt, frame.plot
## - convert axes to character if necessary
## - set defaults of xaxt/yaxt (if these are NULL) based on axes
Expand Down Expand Up @@ -35,6 +35,18 @@ sanitize_axes = function(settings) {
if (!is.null(xaxr) && (!is.finite(xaxr) || xaxr %% 360 == 0)) xaxr = NULL
if (!is.null(yaxr) && (!is.finite(yaxr) || yaxr %% 360 == 0)) yaxr = NULL

env2env(environment(), settings,
c("axes", "xaxt", "yaxt", "frame.plot", "xaxr", "yaxr"))
## axis padding: an explicit x/ypad wins over the theme's tpar setting. This
## has to resolve here, before flip_datapoints() swaps the pair -- resolving
## it later would leave a tpar default attached to the axis rather than to
## the variable, so the two paths would disagree under `flip`.
if (is.null(xpad)) xpad = get_tpar("xpad")
if (is.null(ypad)) ypad = get_tpar("ypad")
assert_numeric(xpad, len = 1, lower = 0, null.ok = TRUE, name = "xpad")
assert_numeric(ypad, len = 1, lower = 0, null.ok = TRUE, name = "ypad")

env2env(
environment(),
settings,
c("axes", "xaxt", "yaxt", "frame.plot", "xaxr", "yaxr", "xpad", "ypad")
)
}
8 changes: 6 additions & 2 deletions R/tinyAxis.R
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,17 @@ axis_tick_labels = function(labelset, lim, axb = NULL, axl = NULL, log = FALSE,
## A log axis can't represent a zero or negative limit, so those fall back to a
## linear measurement: plot.window() raises its own, clearer complaint moments
## later, and log10() here would only put "NaNs produced" in front of it.
axis_usr = function(lim, log = FALSE, axb = NULL) {
axis_usr = function(lim, log = FALSE, axb = NULL, pad = NULL) {
log = isTRUE(log) && all(is.finite(lim)) && all(lim > 0)
if (log) lim = log10(lim)
# A single distinct value gives a zero-width range that extendrange() can't
# pad and axisTicks() can't tick, so widen it the way plot.window() does.
# An explicit `at` (xaxb/yaxb) supplies its own ticks, hence the guard.
usr = if (diff(lim) == 0 && is.null(axb)) {
# A non-NULL `pad` means lim_args() has already expanded these limits, so
# there is nothing left to add.
usr = if (!is.null(pad)) {
lim
} else if (diff(lim) == 0 && is.null(axb)) {
lim + c(-0.5, 0.5)
} else {
extendrange(lim, f = 0.04)
Expand Down
33 changes: 24 additions & 9 deletions R/tinyplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,11 @@
#' @param xaxs,yaxs character specifying the style of the interval calculation
#' used for the x-axis and y-axis, respectively. See
#' \code{\link[graphics]{par}} for the possible values.
#' @param xpad,ypad numeric specifying how much padding, as a fraction of the
#' data range, should be added to each end of the axes. Defaults to `NULL`,
#' in which case behaviour depends on the value of `x/yaxs`. In most cases,
#' this will translate to a value of `0.04`, i.e. 4% padding on each end (see
#' \code{\link[graphics]{par}}).
#' @param xaxb,yaxb numeric vector (or character vector, if appropriate) giving
#' the break points at which the axis tick-marks are to be drawn. Break points
#' outside the range of the data will be ignored if the associated axis
Expand Down Expand Up @@ -788,7 +793,9 @@ tinyplot.default = function(
xaxt = NULL,
yaxt = NULL,
xaxs = NULL,
xpad = NULL,
yaxs = NULL,
ypad = NULL,
xaxb = NULL,
yaxb = NULL,
xaxl = NULL,
Expand Down Expand Up @@ -982,11 +989,13 @@ tinyplot.default = function(
xaxl = xaxl,
xaxr = xaxr,
xaxs = xaxs,
xpad = xpad,
yaxt = yaxt,
yaxb = yaxb,
yaxl = yaxl,
yaxr = yaxr,
yaxs = yaxs,
ypad = ypad,
frame.plot = frame.plot,
xlim = xlim,
ylim = ylim,
Expand Down Expand Up @@ -1392,7 +1401,7 @@ tinyplot.default = function(
max(0, fin / par("csi") - pad)
}
if (!is.null(xaxr)) {
.u = axis_usr(xlim, log = .xlog, axb = xaxb)
.u = axis_usr(xlim, log = .xlog, axb = xaxb, pad = xpad)
.at = if (!is.null(xlabs)) as.numeric(xlabs) else
axisTicks(usr = .u[["usr"]], log = .u[["log"]])
# axisTicks() reports tick locations in data units, but the inset is a
Expand All @@ -1404,7 +1413,7 @@ tinyplot.default = function(
.dyn = .add_lean(.dyn, .ovh, .flank(.xside))
}
if (!is.null(yaxr)) {
.u = axis_usr(ylim, log = .ylog, axb = yaxb)
.u = axis_usr(ylim, log = .ylog, axb = yaxb, pad = ypad)
.at = if (!is.null(ylabs)) as.numeric(ylabs) else
axisTicks(usr = .u[["usr"]], log = .u[["log"]])
# axisTicks() reports tick locations in data units, but the inset is a
Expand Down Expand Up @@ -1533,10 +1542,14 @@ tinyplot.default = function(
}
par(mar = dynmar_computed + .whtsbp)
if (!is.null(xlim) && !is.null(ylim)) {
plot.window(xlim = xlim, ylim = ylim)
plot.window(xlim = xlim, ylim = ylim,
xaxs = if (is.null(xpad)) par("xaxs") else "i",
yaxs = if (is.null(ypad)) par("yaxs") else "i")
}
} else if (direct_labels_flag && !is.null(xlim) && !is.null(ylim)) {
plot.window(xlim = xlim, ylim = ylim)
plot.window(xlim = xlim, ylim = ylim,
xaxs = if (is.null(xpad)) par("xaxs") else "i",
yaxs = if (is.null(ypad)) par("yaxs") else "i")
}

# Expand right margin for direct labels based on actual label overshoot
Expand All @@ -1559,7 +1572,9 @@ tinyplot.default = function(
cur_mar[4] = cur_mar[4] + overshoot_lines
par(mar = cur_mar)
}
plot.window(xlim = xlim, ylim = ylim)
plot.window(xlim = xlim, ylim = ylim,
xaxs = if (is.null(xpad)) par("xaxs") else "i",
yaxs = if (is.null(ypad)) par("yaxs") else "i")
}
}

Expand Down Expand Up @@ -1641,8 +1656,8 @@ tinyplot.default = function(
# axes args
axes = axes, flip = flip, frame.plot = frame.plot,
oxaxis = oxaxis, oyaxis = oyaxis,
xlabs = xlabs, xlim = xlim, null_xlim = null_xlim, xaxt = xaxt, xaxs = xaxs, xaxb = xaxb, xaxl = xaxl, xaxr = xaxr,
ylabs = ylabs, ylim = ylim, null_ylim = null_ylim, yaxt = yaxt, yaxs = yaxs, yaxb = yaxb, yaxl = yaxl, yaxr = yaxr,
xlabs = xlabs, xlim = xlim, null_xlim = null_xlim, xaxt = xaxt, xaxs = xaxs, xaxb = xaxb, xaxl = xaxl, xaxr = xaxr, xpad = xpad,
ylabs = ylabs, ylim = ylim, null_ylim = null_ylim, yaxt = yaxt, yaxs = yaxs, yaxb = yaxb, yaxl = yaxl, yaxr = yaxr, ypad = ypad,
rev_x = rev_x, rev_y = rev_y,
xlim_partial = xlim_partial, ylim_partial = ylim_partial,
facet_labs = facet_labs,
Expand Down Expand Up @@ -1678,8 +1693,8 @@ tinyplot.default = function(
nfacets = nfacets, nfacet_cols = nfacet_cols, nfacet_rows = nfacet_rows,
axes = axes, flip = flip, frame.plot = frame.plot,
oxaxis = oxaxis, oyaxis = oyaxis,
xlabs = xlabs, xlim = xlim, null_xlim = null_xlim, xaxt = xaxt, xaxs = xaxs, xaxb = xaxb, xaxl = xaxl, xaxr = xaxr,
ylabs = ylabs, ylim = ylim, null_ylim = null_ylim, yaxt = yaxt, yaxs = yaxs, yaxb = yaxb, yaxl = yaxl, yaxr = yaxr,
xlabs = xlabs, xlim = xlim, null_xlim = null_xlim, xaxt = xaxt, xaxs = xaxs, xaxb = xaxb, xaxl = xaxl, xaxr = xaxr, xpad = xpad,
ylabs = ylabs, ylim = ylim, null_ylim = null_ylim, yaxt = yaxt, yaxs = yaxs, yaxb = yaxb, yaxl = yaxl, yaxr = yaxr, ypad = ypad,
rev_x = rev_x, rev_y = rev_y,
xlim_partial = xlim_partial, ylim_partial = ylim_partial,
facet_labs = facet_labs,
Expand Down
7 changes: 6 additions & 1 deletion R/tpar.R
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
#' * `palette.sequential`: Palette for sequential colors. See the `palette` argument in `?tinyplot`.
#' * `record`: (experimental) Logical indicating whether `tinyplot()` should record plots and return them as replayable \code{\link{recordedtinyplot}} objects. Defaults to `NULL`, which is equivalent to `FALSE`. Setting to `TRUE` allows for assignment and later recall, e.g. `myplot = tinyplot(...); myplot`. Sets the default for the `record` argument of [`tinyplot()`], which takes precedence. Note that recording requires a device with an enabled display list (see \code{\link[grDevices]{dev.control}}). Most interactive devices enable this behaviour by default, whereas file-based devices do not. However `tinyplot()` automatically enables it for any device that it opens itself via `file`, and further emits a warning if the current device is not recording.
#' * `ribbon.alpha`: Numeric factor in the range `[0,1]` for modifying the opacity alpha of "ribbon" and "area" type plots. Default value is `0.2`.
#' * `xpad`, `ypad`: Numeric specifying how much padding, as a fraction of the data range, should be added to each end of the x- and y-axis, respectively. Both default to `NULL`, in which case behaviour depends on the value of `x/yaxs`. In most cases, this will translate to a value of `0.04`, i.e. 4% padding on each end (see \code{\link[graphics]{par}}). Sets the default for the `xpad` and `ypad` arguments of [`tinyplot()`], which take precedence.
#' * `xaxr`, `yaxr`: Numeric giving the rotation of the x- and y-axis tick labels, in degrees counter-clockwise; `NULL` (the default) leaves them unrotated. Unlike `las`, which is limited to the four right angles, any angle is permitted. Setting one overrides `las` for that axis alone, leaving the other axis under `las` as usual, and `0` (or any multiple of 360) counts as no rotation at all. Sets the default for the `xaxr` and `yaxr` arguments of [`tinyplot()`], which take precedence. Two caveats follow from tinyplot drawing rotated labels itself rather than deferring to base `axis()`. First, margins are only resized to fit them under a theme with `dynmar = TRUE` (see `tinytheme`); under the default theme the margins are left alone, so a long rotated label will be clipped unless you widen `mar` yourself. Second, rotated labels do not inherit the thinning that `axis()` applies via `gap.axis`, so they start to overlap once the spacing between ticks falls below `line height / sin(srt)`.
#'
#' @importFrom graphics par
Expand Down Expand Up @@ -320,8 +321,10 @@ known_tpar = c(
"tinytheme",
"xaxr",
"xaxt",
"xpad",
"yaxr",
"yaxt"
"yaxt",
"ypad"
)


Expand All @@ -342,6 +345,8 @@ assert_tpar = function(.tpar) {
assert_numeric(.tpar[["cex.yaxs"]], len = 1, lower = 0, null.ok = TRUE, name = "cex.yaxs")
assert_numeric(.tpar[["xaxr"]], len = 1, null.ok = TRUE, name = "xaxr")
assert_numeric(.tpar[["yaxr"]], len = 1, null.ok = TRUE, name = "yaxr")
assert_numeric(.tpar[["xpad"]], len = 1, lower = 0, null.ok = TRUE, name = "xpad")
assert_numeric(.tpar[["ypad"]], len = 1, lower = 0, null.ok = TRUE, name = "ypad")
assert_flag(.tpar[["cairo"]], name = "cairo")
assert_flag(.tpar[["dynmar"]], null.ok = FALSE, name = "dynmar")
assert_choice(.tpar[["ljust"]], choice = c("left", "center", "l", "c"), null.ok = TRUE, name = "ljust")
Expand Down
Loading