diff --git a/NEWS.md b/NEWS.md index 17be8be0..848520a2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -163,6 +163,12 @@ related to plot layering. See "Bug fixes" below. theme, since the plot margins are resized to fit the rotated labels. Also settable globally via `tpar(xaxr = , yaxr = )` and thus as part of a `tinytheme` too. (#717 @grantmcdermott) + - `las` does the same thing, but limited to the four right angles of base R's + `par(las=)` convention, having previously been settable only via `tpar()` or + a theme. Note that this top-level argument applies to a single plot only and + takes precedence over the active theme, so it can also be used to opt _out_ + of a theme's `las`, e.g. `tinyplot(..., theme = "clean", las = 0)`. + (#353 @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 diff --git a/R/sanitize_axes.R b/R/sanitize_axes.R index 88c2855e..435975e8 100644 --- a/R/sanitize_axes.R +++ b/R/sanitize_axes.R @@ -1,6 +1,7 @@ sanitize_axes = function(settings) { env2env(settings, environment(), - c("axes", "xaxt", "yaxt", "frame.plot", "xaxr", "yaxr", "xpad", "ypad")) + c("axes", "xaxt", "yaxt", "frame.plot", "xaxr", "yaxr", "xpad", "ypad", + "las")) ## 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 @@ -35,6 +36,16 @@ 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 + ## tick label orientation. Unlike x/yaxr, not resolved against get_tpar(): + ## that reads the global .tpar, which lacks the active theme's own las (queued + ## in a hook), so folding it in would overwrite a theme's las with 0. + ## one membership test, so NA/NaN/Inf and fractions all land on the same + ## message rather than tripping over a comparison that returns NA + assert_numeric(las, len = 1, null.ok = TRUE, name = "las") + if (!is.null(las) && !(las %in% 0:3)) { + stop("`las` must be one of 0, 1, 2, or 3", call. = FALSE) + } + ## 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 @@ -55,6 +66,6 @@ sanitize_axes = function(settings) { environment(), settings, c("axes", "xaxt", "yaxt", "frame.plot", "xaxr", "yaxr", "xpad", "ypad", - "xpad_user", "ypad_user") + "xpad_user", "ypad_user", "las") ) } diff --git a/R/tinyplot.R b/R/tinyplot.R index 24921f51..c500c711 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -303,6 +303,14 @@ #' documentation and examples. Note that this is a post-processing step that #' affects the _appearance_ of the tick labels only; use in conjunction with #' `x/yaxb` if you would like to adjust the position of the tick marks too. +#' @param las numeric in `0:3` giving the orientation of the axis tick labels, +#' following the base \code{\link[graphics]{par}} convention: `0` (parallel +#' to the axis), `1` (always horizontal), `2` (perpendicular to the axis), or +#' `3` (always vertical). `NULL` (the default) defers to the active theme, +#' then to `par("las")`. Passing it here overrides both, but +#' only for this plot; unlike `tpar(las=)` or `tinytheme(las=)` it does not +#' persist. For rotations other than the four right angles, see `xaxr`/`yaxr` +#' below. #' @param xaxr,yaxr numeric giving the rotation of the x- or y-axis tick labels, #' in degrees counter-clockwise; `NULL` (the default) leaves them unrotated. #' Setting one overrides `las` for that axis alone, leaving the other axis @@ -815,6 +823,7 @@ tinyplot.default = function( yaxl = NULL, xaxr = NULL, yaxr = NULL, + las = NULL, log = "", flip = FALSE, frame.plot = NULL, @@ -1009,6 +1018,7 @@ tinyplot.default = function( yaxr = yaxr, yaxs = yaxs, ypad = ypad, + las = las, frame.plot = frame.plot, xlim = xlim, ylim = ylim, @@ -1235,6 +1245,27 @@ tinyplot.default = function( env2env(settings, environment()) + # + ## per-call tick label orientation ----- + # + # A theme sets its own las at plot.new(), so ours is appended after its hook + # (last appended runs last). The direct par() covers add = TRUE, which fires + # no plot.new(). Undone on exit, so the override stays per-call. (#353) + # + if (!is.null(las)) { + .las_old = par("las") + .las_hook = function() par(las = las) + setHook("before.plot.new", .las_hook, action = "append") + on.exit({ + .hks = getHook("before.plot.new") + .keep = !vapply(.hks, identical, logical(1), .las_hook) + setHook("before.plot.new", .hks[.keep], action = "replace") + # par() would *open* a device if none is left (e.g. after `file=`) + if (!is.null(dev.list())) par(las = .las_old) + }, add = TRUE) + par(las = las) + } + # ## dynmar: compute margins up front ----- # @@ -1267,6 +1298,8 @@ tinyplot.default = function( .bp = environment(.h)[["base_par"]] if (is.list(.bp)) .tpars = modifyList(.tpars, .bp) } + # runs before plot.new(), so the las hook hasn't fired and par() is stale + if (!is.null(las)) .tpars[["las"]] = las if (!is.null(.tpars[["mar"]])) .theme_mar = .tpars[["mar"]] # Tick-label cex is per-side (cex.xaxs/cex.yaxs), each falling back to the diff --git a/inst/tinytest/_tinysnapshot/axis_las1.svg b/inst/tinytest/_tinysnapshot/axis_las1.svg new file mode 100644 index 00000000..a4822a07 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/axis_las1.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + +Index +1:10 + + + + + + +2 +4 +6 +8 +10 + + + + + + +2 +4 +6 +8 +10 + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/axis_las2_override.svg b/inst/tinytest/_tinysnapshot/axis_las2_override.svg new file mode 100644 index 00000000..f9e9aa6b --- /dev/null +++ b/inst/tinytest/_tinysnapshot/axis_las2_override.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + +species +bill_len + + + + +Adelie +Chinstrap +Gentoo + + + + + + + +35 +40 +45 +50 +55 +60 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-tinyAxis.R b/inst/tinytest/test-tinyAxis.R index 5d708b82..e428fad9 100644 --- a/inst/tinytest/test-tinyAxis.R +++ b/inst/tinytest/test-tinyAxis.R @@ -46,3 +46,19 @@ f = function() { plt(disp ~ mpg, data = mtcars, log = "y") } expect_silent(f()) + + +# +## las as a per-call argument (#353) +# + +# `las` was accepted by tpar()/tinytheme() but ignored as an argument. +f = function() tinyplot(1:10, las = 1) +expect_snapshot_plot(f, label = "axis_las1") + +# An explicit `las` must beat a theme's default, but dynmar still has to reserve +# room for the rotated x labels. +f = function() { + tinyplot(bill_len ~ species, data = penguins, las = 2, theme = "dynamic") +} +expect_snapshot_plot(f, label = "axis_las2_override") diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd index be5fb20e..55f0a96a 100644 --- a/man/tinyplot.Rd +++ b/man/tinyplot.Rd @@ -46,6 +46,7 @@ tinyplot(x, ...) yaxl = NULL, xaxr = NULL, yaxr = NULL, + las = NULL, log = "", flip = FALSE, frame.plot = NULL, @@ -480,6 +481,15 @@ default theme a long rotated label will be clipped unless you widen \code{mar} yourself. Defaults to the value of \code{tpar("xaxr")} / \code{tpar("yaxr")}, whose documentation covers this and the label-spacing caveat in more detail.} +\item{las}{numeric in \code{0:3} giving the orientation of the axis tick labels, +following the base \code{\link[graphics]{par}} convention: \code{0} (parallel +to the axis), \code{1} (always horizontal), \code{2} (perpendicular to the axis), or +\code{3} (always vertical). \code{NULL} (the default) defers to the active theme, +then to \code{par("las")}. Passing it here overrides both, but +only for this plot; unlike \code{tpar(las=)} or \code{tinytheme(las=)} it does not +persist. For rotations other than the four right angles, see \code{xaxr}/\code{yaxr} +below.} + \item{log}{a character string which contains \code{"x"} if the x axis is to be logarithmic, \code{"y"} if the y axis is to be logarithmic and \code{"xy"} or \code{"yx"} if both axes are to be logarithmic.}