diff --git a/NEWS.md b/NEWS.md
index f189be98..f99ee16f 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -136,6 +136,15 @@ Beyond convenience, these improvements to categorical variable handling also
provide the scaffolding to eliminate some niggling inconsistencies; for example,
related to plot layering. See "Bug fixes" below.
+#### Axis aesthetics
+
+- Point-like glyphs (`"p"`, `"l"`, `"errorbar"`, and friends) gain smart padding
+ behaviour for categorical axes. Most notably, we now use more generous axis
+ padding when there are few unique categories (levels), so that the end tick
+ marks aren't drawn flush against the plot frame. Users can also override with
+ the new `x/ypad` arguments (see below) to match their own aesthetic
+ preferences. (#732 @grantmcdermott)
+
#### Other new features
- New top-level `tinyplot()`/`plt()` arguments:
diff --git a/R/assertions.R b/R/assertions.R
index 2b8ed343..1cd78172 100644
--- a/R/assertions.R
+++ b/R/assertions.R
@@ -346,7 +346,8 @@ known_type_hints = c(
"legend_border_fg", # legend swatch border is always par("fg")
"legend_fills_from_col", # legend swatch fill comes from `col`
"legend_fills_from_seq_palette", # ... or from the colour's sequential ramp
- "legend_reversed" # list the key bottom-up, not top-down
+ "legend_reversed", # list the key bottom-up, not top-down
+ "pads_cat_axis" # buffer the ends of a categorical axis
)
## Validate a type's declared hints.
diff --git a/R/facet.R b/R/facet.R
index 3c1397d4..2dd89dc7 100644
--- a/R/facet.R
+++ b/R/facet.R
@@ -26,6 +26,7 @@ draw_facet_window = function(
axes, flip, frame.plot, oxaxis, oyaxis,
xlabs, xlim, null_xlim, xaxt, xaxs, xaxb, xaxl, xaxr = NULL, xpad = NULL,
ylabs, ylim, null_ylim, yaxt, yaxs, yaxb, yaxl, yaxr = NULL, ypad = NULL,
+ xpad_user = NULL, ypad_user = NULL,
rev_x = FALSE, rev_y = FALSE,
xlim_partial = NULL, ylim_partial = NULL,
facet_labs = NULL,
@@ -525,8 +526,13 @@ draw_facet_window = function(
# 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 panel that derives its own categorical range asks for the same
+ # buffer lim_args() gives an unfaceted one, so the two agree.
+ .catpad = isTRUE(type_hints[["pads_cat_axis"]])
+ .xp = xpad_user %||% (if (.catpad && length(.fxlabs)) cat_pad(xext))
+ .yp = ypad_user %||% (if (.catpad && length(.fylabs)) cat_pad(yext))
+ if (!.padded_x) xext = expand_lim(xext, .xp %||% 0.04)
+ if (!.padded_y) yext = expand_lim(yext, .yp %||% 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)
diff --git a/R/flip.R b/R/flip.R
index 3b3b9ef7..98ed8db1 100644
--- a/R/flip.R
+++ b/R/flip.R
@@ -33,6 +33,7 @@ flip_datapoints = function(settings) {
swap_elements(settings, "xlabs", "ylabs")
swap_elements(settings, "xlim", "ylim")
swap_elements(settings, "xpad", "ypad")
+ swap_elements(settings, "xpad_user", "ypad_user")
swap_elements(settings, "null_xlim", "null_ylim")
swap_elements(settings, "rev_x", "rev_y")
swap_elements(settings, "xmax", "ymax")
diff --git a/R/lim.R b/R/lim.R
index c3437e49..0658f68f 100644
--- a/R/lim.R
+++ b/R/lim.R
@@ -7,7 +7,8 @@ lim_args = function(settings) {
c(
"xaxb", "xlabs", "xlim", "null_xlim", "rev_x",
"yaxb", "ylabs", "ylim", "null_ylim", "rev_y",
- "datapoints", "type", "type_hints", "xpad", "ypad", "log"
+ "datapoints", "type", "type_hints", "xpad", "ypad", "xaxs", "yaxs",
+ "log"
)
)
@@ -51,6 +52,18 @@ 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))
+ # A categorical axis asks for its buffer in category widths; xpad speaks in
+ # fractions of the range. Convert and let the existing machinery apply it.
+ # An explicit x/ylim or x/yaxs = "i" is the user's call; leave those to base.
+ if (isTRUE(type_hints[["pads_cat_axis"]])) {
+ if (is.null(xpad) && !is.null(xlabs) && null_xlim && !identical(xaxs, "i")) {
+ xpad = cat_pad(xlim)
+ }
+ if (is.null(ypad) && !is.null(ylabs) && null_ylim && !identical(yaxs, "i")) {
+ ypad = cat_pad(ylim)
+ }
+ }
+
if (!is.null(xpad)) {
xlim = expand_lim(widen_degenerate(xlim), xpad,
log = grepl("x", log, fixed = TRUE))
@@ -103,6 +116,21 @@ widen_degenerate = function(lim) {
lim + c(-1, 1) * (if (lim[1L] == 0) 1 else 0.4 * abs(lim[1L]))
}
+
+# The categorical buffer as the fraction of the range that `xpad` wants, one
+# category being one unit. Takes `lim` rather than a category count because
+# dodging widens the span past n-1, and the gutter should clear what is drawn.
+# Past a span of reach/0.04 (eight categories at the default) base's own 4%
+# already reaches further, so NULL hands those plots back to it untouched. A
+# quarter of a category, not the half box-like types take: a box is most of a
+# category wide and needs the room; a point is not.
+cat_pad = function(lim, reach = 0.25) {
+ span = abs(diff(lim))
+ if (!is.finite(span) || span == 0 || span > reach / 0.04) return(NULL)
+ reach / span
+}
+
+
# 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)
diff --git a/R/sanitize_axes.R b/R/sanitize_axes.R
index 581d44e6..88c2855e 100644
--- a/R/sanitize_axes.R
+++ b/R/sanitize_axes.R
@@ -43,10 +43,18 @@ sanitize_axes = function(settings) {
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")
+ ## Keep the user's pad distinct from the one lim_args() may synthesize for a
+ ## categorical axis: that one is a fraction of the whole plot's range, so a
+ ## free panel spanning fewer categories has to work its own out instead.
+ ## Captured here rather than in lim_args() because that never runs on the
+ ## `add = TRUE` path; flip_datapoints() swaps the pair alongside x/ypad.
+ xpad_user = xpad
+ ypad_user = ypad
env2env(
environment(),
settings,
- c("axes", "xaxt", "yaxt", "frame.plot", "xaxr", "yaxr", "xpad", "ypad")
+ c("axes", "xaxt", "yaxt", "frame.plot", "xaxr", "yaxr", "xpad", "ypad",
+ "xpad_user", "ypad_user")
)
}
diff --git a/R/tinyplot.R b/R/tinyplot.R
index 021feb43..24921f51 100644
--- a/R/tinyplot.R
+++ b/R/tinyplot.R
@@ -276,10 +276,23 @@
#' 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}}).
+#' data range, should be added to each end of the axes. If `NULL` (the
+#' default), follows an automatic padding heuristic that tries to optimize for
+#' plot aesthetic depending on the axis type:
+#'
+#' - numeric axis: inherited from `x/yaxs`. Under the default `x/yaxs = "r"`
+#' style, this means `0.04`, i.e. 4% padding on each end (see
+#' \code{\link[graphics]{par}}).
+#'
+#' - categorical axis: behaviour depends on the glyph-type and number of
+#' unique categories, and therefore tick marks. For example, point-like
+#' glyphs with numerous (8+) categories inherit the same padding logic as a
+#' numeric axis (i.e., usually 4%). For cases with fewer unique categories,
+#' the padding will instead correspond to 25% of the gap between interior
+#' tick marks, so that the end categories aren't drawn flush against the
+#' plot frame. Note that providing an explicit `x/ylim` or setting
+#' `x/yaxs = "i"` skips this heuristic in favour of the standard numeric
+#' behaviour.
#' @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
@@ -1656,8 +1669,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, xpad = xpad,
- ylabs = ylabs, ylim = ylim, null_ylim = null_ylim, yaxt = yaxt, yaxs = yaxs, yaxb = yaxb, yaxl = yaxl, yaxr = yaxr, ypad = ypad,
+ xlabs = xlabs, xlim = xlim, null_xlim = null_xlim, xaxt = xaxt, xaxs = xaxs, xaxb = xaxb, xaxl = xaxl, xaxr = xaxr, xpad = xpad, xpad_user = xpad_user,
+ ylabs = ylabs, ylim = ylim, null_ylim = null_ylim, yaxt = yaxt, yaxs = yaxs, yaxb = yaxb, yaxl = yaxl, yaxr = yaxr, ypad = ypad, ypad_user = ypad_user,
rev_x = rev_x, rev_y = rev_y,
xlim_partial = xlim_partial, ylim_partial = ylim_partial,
facet_labs = facet_labs,
@@ -1693,8 +1706,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, xpad = xpad,
- ylabs = ylabs, ylim = ylim, null_ylim = null_ylim, yaxt = yaxt, yaxs = yaxs, yaxb = yaxb, yaxl = yaxl, yaxr = yaxr, ypad = ypad,
+ xlabs = xlabs, xlim = xlim, null_xlim = null_xlim, xaxt = xaxt, xaxs = xaxs, xaxb = xaxb, xaxl = xaxl, xaxr = xaxr, xpad = xpad, xpad_user = xpad_user,
+ ylabs = ylabs, ylim = ylim, null_ylim = null_ylim, yaxt = yaxt, yaxs = yaxs, yaxb = yaxb, yaxl = yaxl, yaxr = yaxr, ypad = ypad, ypad_user = ypad_user,
rev_x = rev_x, rev_y = rev_y,
xlim_partial = xlim_partial, ylim_partial = ylim_partial,
facet_labs = facet_labs,
diff --git a/R/tpar.R b/R/tpar.R
index 0c542bf6..b6efa10a 100644
--- a/R/tpar.R
+++ b/R/tpar.R
@@ -89,7 +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.
+#' * `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 an automatic heuristic applies: a numeric axis inherits from `x/yaxs` (usually 4% padding on each end), whereas a categorical axis with point-like glyphs and few unique categories instead gets 25% of the gap between interior tick marks. Sets the default for the `xpad` and `ypad` arguments of [`tinyplot()`], which take precedence and where the heuristic is described in full.
#' * `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
diff --git a/R/type_lines.R b/R/type_lines.R
index 889892d0..cc4872ba 100644
--- a/R/type_lines.R
+++ b/R/type_lines.R
@@ -109,6 +109,8 @@ data_lines = function(dodge = 0, fixed.dodge = FALSE, xlevels = NULL, xord = NUL
xlabs = seq_along(xlvls)
names(xlabs) = xlvls
datapoints[["x"]] = as.integer(datapoints[["x"]])
+ # More generous padding if categorical x-axis; better plot aesthetic
+ settings[["type_hints"]][["pads_cat_axis"]] = TRUE
} else {
xlabs = NULL
}
@@ -117,6 +119,8 @@ data_lines = function(dodge = 0, fixed.dodge = FALSE, xlevels = NULL, xord = NUL
ylabs = seq_along(ylvls)
names(ylabs) = ylvls
datapoints[["y"]] = as.integer(datapoints[["y"]])
+ # More generous padding if categorical y-axis; better plot aesthetic
+ settings[["type_hints"]][["pads_cat_axis"]] = TRUE
} else {
ylabs = NULL
}
diff --git a/R/type_pointrange.R b/R/type_pointrange.R
index 2e64df95..14c14731 100644
--- a/R/type_pointrange.R
+++ b/R/type_pointrange.R
@@ -73,6 +73,8 @@ data_pointrange = function(dodge, fixed.dodge, xlevels = NULL, xord = "asis", or
xlabs = seq_along(xlvls)
names(xlabs) = xlvls
datapoints$x = as.integer(datapoints$x)
+ # More generous padding for better plot aesthetic
+ settings[["type_hints"]][["pads_cat_axis"]] = TRUE
}
datapoints$xmin = datapoints$x
datapoints$xmax = datapoints$x
diff --git a/R/type_points.R b/R/type_points.R
index 31c2fc55..e2589730 100644
--- a/R/type_points.R
+++ b/R/type_points.R
@@ -93,6 +93,8 @@ data_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE, xleve
xlabs = seq_along(xlvls)
names(xlabs) = xlvls
datapoints$x = as.integer(datapoints$x)
+ # More generous padding if categorical x-axis; better plot aesthetic
+ settings[["type_hints"]][["pads_cat_axis"]] = TRUE
} else {
xlabs = NULL
}
@@ -101,6 +103,8 @@ data_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE, xleve
ylabs = seq_along(ylvls)
names(ylabs) = ylvls
datapoints$y = as.integer(datapoints$y)
+ # More generous padding if categorical y-axis; better plot aesthetic
+ settings[["type_hints"]][["pads_cat_axis"]] = TRUE
} else {
ylabs = NULL
}
diff --git a/R/type_text.R b/R/type_text.R
index ad10a015..a8f8e22a 100644
--- a/R/type_text.R
+++ b/R/type_text.R
@@ -173,6 +173,8 @@ data_text = function(labels = NULL, labeller = NULL, clim = c(0.5, 2.5)) {
xlabs = seq_along(xlvls)
names(xlabs) = xlvls
datapoints$x = as.integer(datapoints$x)
+ # More generous padding if categorical x-axis; better plot aesthetic
+ settings[["type_hints"]][["pads_cat_axis"]] = TRUE
} else {
xlabs = NULL
}
@@ -181,6 +183,8 @@ data_text = function(labels = NULL, labeller = NULL, clim = c(0.5, 2.5)) {
ylabs = seq_along(ylvls)
names(ylabs) = ylvls
datapoints$y = as.integer(datapoints$y)
+ # More generous padding if categorical y-axis; better plot aesthetic
+ settings[["type_hints"]][["pads_cat_axis"]] = TRUE
} else {
ylabs = NULL
}
diff --git a/inst/tinytest/_tinysnapshot/dodge_errorbar_add_lines.svg b/inst/tinytest/_tinysnapshot/dodge_errorbar_add_lines.svg
index 5789fc1f..227b0b4d 100644
--- a/inst/tinytest/_tinysnapshot/dodge_errorbar_add_lines.svg
+++ b/inst/tinytest/_tinysnapshot/dodge_errorbar_add_lines.svg
@@ -47,15 +47,15 @@
estimate
-
-
-
-
-
-(Intercept)
-wt
-cyl
-hp
+
+
+
+
+
+(Intercept)
+wt
+cyl
+hp
@@ -75,57 +75,57 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/dodge_errorbar_add_ribbon.svg b/inst/tinytest/_tinysnapshot/dodge_errorbar_add_ribbon.svg
index ea2b7a94..62ca90fe 100644
--- a/inst/tinytest/_tinysnapshot/dodge_errorbar_add_ribbon.svg
+++ b/inst/tinytest/_tinysnapshot/dodge_errorbar_add_ribbon.svg
@@ -47,15 +47,15 @@
estimate
-
-
-
-
-
-(Intercept)
-wt
-cyl
-hp
+
+
+
+
+
+(Intercept)
+wt
+cyl
+hp
@@ -75,60 +75,60 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/dodge_pointrange.svg b/inst/tinytest/_tinysnapshot/dodge_pointrange.svg
index 77188c89..fd80eec0 100644
--- a/inst/tinytest/_tinysnapshot/dodge_pointrange.svg
+++ b/inst/tinytest/_tinysnapshot/dodge_pointrange.svg
@@ -47,15 +47,15 @@
estimate
-
-
-
-
-
-(Intercept)
-wt
-cyl
-hp
+
+
+
+
+
+(Intercept)
+wt
+cyl
+hp
@@ -75,36 +75,36 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/dodge_pointrange_false.svg b/inst/tinytest/_tinysnapshot/dodge_pointrange_false.svg
index 01018522..6c2b2471 100644
--- a/inst/tinytest/_tinysnapshot/dodge_pointrange_false.svg
+++ b/inst/tinytest/_tinysnapshot/dodge_pointrange_false.svg
@@ -47,15 +47,15 @@
estimate
-
-
-
-
-
-(Intercept)
-wt
-cyl
-hp
+
+
+
+
+
+(Intercept)
+wt
+cyl
+hp
@@ -75,36 +75,36 @@
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/dodge_pointrange_flip.svg b/inst/tinytest/_tinysnapshot/dodge_pointrange_flip.svg
index ddcf84e0..a3224678 100644
--- a/inst/tinytest/_tinysnapshot/dodge_pointrange_flip.svg
+++ b/inst/tinytest/_tinysnapshot/dodge_pointrange_flip.svg
@@ -58,15 +58,15 @@
20
30
40
-
-
-
-
-
-(Intercept)
-wt
-cyl
-hp
+
+
+
+
+
+(Intercept)
+wt
+cyl
+hp
@@ -80,31 +80,31 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/dodge_pointrange_true.svg b/inst/tinytest/_tinysnapshot/dodge_pointrange_true.svg
index 233d4742..2522a2f5 100644
--- a/inst/tinytest/_tinysnapshot/dodge_pointrange_true.svg
+++ b/inst/tinytest/_tinysnapshot/dodge_pointrange_true.svg
@@ -47,15 +47,15 @@
estimate
-
-
-
-
-
-(Intercept)
-wt
-cyl
-hp
+
+
+
+
+
+(Intercept)
+wt
+cyl
+hp
@@ -75,36 +75,36 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/facet_drop_levels_xaxb.svg b/inst/tinytest/_tinysnapshot/facet_drop_levels_xaxb.svg
index 7517ae18..8cad8b4d 100644
--- a/inst/tinytest/_tinysnapshot/facet_drop_levels_xaxb.svg
+++ b/inst/tinytest/_tinysnapshot/facet_drop_levels_xaxb.svg
@@ -38,10 +38,10 @@
-
-
+
+
-2
+2
4
@@ -64,11 +64,11 @@
-
+
-
+
2
-4
+4
@@ -83,36 +83,36 @@
-
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/facet_free_categorical_yaxis.svg b/inst/tinytest/_tinysnapshot/facet_free_categorical_yaxis.svg
index e088df26..1db86588 100644
--- a/inst/tinytest/_tinysnapshot/facet_free_categorical_yaxis.svg
+++ b/inst/tinytest/_tinysnapshot/facet_free_categorical_yaxis.svg
@@ -48,13 +48,13 @@
230
240
250
-
-
+
+
-
-Fellowship
+
+Fellowship
Two Towers
-Return
+Return
extended
@@ -78,30 +78,30 @@
190
195
200
-
-
+
+
-
-Fellowship
+
+Fellowship
Two Towers
-Return
+Return
theatrical
-
-
-
+
+
+
-
+
-
-
-
+
+
+
-
+
diff --git a/inst/tinytest/_tinysnapshot/flip_pointrange.svg b/inst/tinytest/_tinysnapshot/flip_pointrange.svg
index 057a9334..ed60ffe3 100644
--- a/inst/tinytest/_tinysnapshot/flip_pointrange.svg
+++ b/inst/tinytest/_tinysnapshot/flip_pointrange.svg
@@ -33,12 +33,12 @@
10
20
30
-(Intercept)
-wt
-gear4
-gear5
-wt:gear4
-wt:gear5
+(Intercept)
+wt
+gear4
+gear5
+wt:gear4
+wt:gear5
@@ -51,24 +51,24 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/lines_xord_rev.svg b/inst/tinytest/_tinysnapshot/lines_xord_rev.svg
index abee4dc7..8c6915ee 100644
--- a/inst/tinytest/_tinysnapshot/lines_xord_rev.svg
+++ b/inst/tinytest/_tinysnapshot/lines_xord_rev.svg
@@ -28,13 +28,13 @@
name
runtime
-
-
+
+
-
-Two Towers
+
+Two Towers
Return
-Fellowship
+Fellowship
@@ -54,10 +54,10 @@
-
-
-
-
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/matrix_basic.svg b/inst/tinytest/_tinysnapshot/matrix_basic.svg
index e8389c55..960aeaca 100644
--- a/inst/tinytest/_tinysnapshot/matrix_basic.svg
+++ b/inst/tinytest/_tinysnapshot/matrix_basic.svg
@@ -44,17 +44,17 @@
VADeaths
-
-
-
+
+
+
-
-
-50-54
-55-59
+
+
+50-54
+55-59
60-64
-65-69
-70-74
+65-69
+70-74
@@ -78,26 +78,26 @@
-
-
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
+
+
diff --git a/inst/tinytest/_tinysnapshot/matrix_facet.svg b/inst/tinytest/_tinysnapshot/matrix_facet.svg
index 1103ac61..a7ca53fe 100644
--- a/inst/tinytest/_tinysnapshot/matrix_facet.svg
+++ b/inst/tinytest/_tinysnapshot/matrix_facet.svg
@@ -55,15 +55,15 @@
-
-
-
+
+
+
-
-
-50-54
+
+
+50-54
60-64
-70-74
+70-74
@@ -91,15 +91,15 @@
-
-
-
+
+
+
-
-
-50-54
+
+
+50-54
60-64
-70-74
+70-74
@@ -127,15 +127,15 @@
-
-
-
+
+
+
-
-
-50-54
+
+
+50-54
60-64
-70-74
+70-74
@@ -163,15 +163,15 @@
-
-
-
+
+
+
-
-
-50-54
+
+
+50-54
60-64
-70-74
+70-74
@@ -192,36 +192,36 @@
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
diff --git a/inst/tinytest/_tinysnapshot/matrix_type_b.svg b/inst/tinytest/_tinysnapshot/matrix_type_b.svg
index d9d39d30..ea07816c 100644
--- a/inst/tinytest/_tinysnapshot/matrix_type_b.svg
+++ b/inst/tinytest/_tinysnapshot/matrix_type_b.svg
@@ -48,17 +48,17 @@
VADeaths
-
-
-
+
+
+
-
-
-50-54
-55-59
+
+
+50-54
+55-59
60-64
-65-69
-70-74
+65-69
+70-74
@@ -82,42 +82,42 @@
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
diff --git a/inst/tinytest/_tinysnapshot/pointrange_errorbar.svg b/inst/tinytest/_tinysnapshot/pointrange_errorbar.svg
index 0b22f9f7..a67f947f 100644
--- a/inst/tinytest/_tinysnapshot/pointrange_errorbar.svg
+++ b/inst/tinytest/_tinysnapshot/pointrange_errorbar.svg
@@ -28,15 +28,15 @@
x
y
-
-
-
-
-
-(Intercept)
-hp
-factor(cyl)6
-factor(cyl)8
+
+
+
+
+
+(Intercept)
+hp
+factor(cyl)6
+factor(cyl)8
@@ -56,22 +56,22 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/pointrange_with_layers.svg b/inst/tinytest/_tinysnapshot/pointrange_with_layers.svg
index fc8092ca..1bac3268 100644
--- a/inst/tinytest/_tinysnapshot/pointrange_with_layers.svg
+++ b/inst/tinytest/_tinysnapshot/pointrange_with_layers.svg
@@ -28,15 +28,15 @@
x
y
-
-
-
-
-
-(Intercept)
-hp
-factor(cyl)6
-factor(cyl)8
+
+
+
+
+
+(Intercept)
+hp
+factor(cyl)6
+factor(cyl)8
@@ -56,28 +56,28 @@
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/pointrange_with_layers_flipped.svg b/inst/tinytest/_tinysnapshot/pointrange_with_layers_flipped.svg
index 8fa10b1c..68d5fa67 100644
--- a/inst/tinytest/_tinysnapshot/pointrange_with_layers_flipped.svg
+++ b/inst/tinytest/_tinysnapshot/pointrange_with_layers_flipped.svg
@@ -39,15 +39,15 @@
10
20
30
-
-
-
-
-
-(Intercept)
-hp
-factor(cyl)6
-factor(cyl)8
+
+
+
+
+
+(Intercept)
+hp
+factor(cyl)6
+factor(cyl)8
@@ -56,16 +56,16 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/pointrange_xlevels_null.svg b/inst/tinytest/_tinysnapshot/pointrange_xlevels_null.svg
index 722f223f..8e6e7b7c 100644
--- a/inst/tinytest/_tinysnapshot/pointrange_xlevels_null.svg
+++ b/inst/tinytest/_tinysnapshot/pointrange_xlevels_null.svg
@@ -28,15 +28,15 @@
x
y
-
-
-
-
-
-(Intercept)
-factor(cyl)6
-factor(cyl)8
-hp
+
+
+
+
+
+(Intercept)
+factor(cyl)6
+factor(cyl)8
+hp
@@ -56,14 +56,14 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/points_xord_desc.svg b/inst/tinytest/_tinysnapshot/points_xord_desc.svg
index 0f16a85b..47fea24a 100644
--- a/inst/tinytest/_tinysnapshot/points_xord_desc.svg
+++ b/inst/tinytest/_tinysnapshot/points_xord_desc.svg
@@ -28,13 +28,13 @@
factor(cyl)
mpg
-
-
+
+
-
-4
+
+4
6
-8
+8
@@ -56,36 +56,36 @@
-
+
-
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
diff --git a/inst/tinytest/_tinysnapshot/points_xord_rev.svg b/inst/tinytest/_tinysnapshot/points_xord_rev.svg
index ae47da1e..adc741b0 100644
--- a/inst/tinytest/_tinysnapshot/points_xord_rev.svg
+++ b/inst/tinytest/_tinysnapshot/points_xord_rev.svg
@@ -28,13 +28,13 @@
factor(cyl)
mpg
-
-
+
+
-
-8
+
+8
6
-4
+4
@@ -56,36 +56,36 @@
-
+
-
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
diff --git a/inst/tinytest/_tinysnapshot/readme_pointrange.svg b/inst/tinytest/_tinysnapshot/readme_pointrange.svg
index c4c688a8..408d39b4 100644
--- a/inst/tinytest/_tinysnapshot/readme_pointrange.svg
+++ b/inst/tinytest/_tinysnapshot/readme_pointrange.svg
@@ -29,17 +29,17 @@
Effect on Temperature
term
estimate
-
-
-
+
+
+
-
-
-factor(Month)5
-factor(Month)6
+
+
+factor(Month)5
+factor(Month)6
factor(Month)7
-factor(Month)8
-factor(Month)9
+factor(Month)8
+factor(Month)9
@@ -59,16 +59,16 @@
-
-
+
+
-
-
-
-
+
+
+
+
-
-
+
+
diff --git a/inst/tinytest/_tinysnapshot/text_categorical_x.svg b/inst/tinytest/_tinysnapshot/text_categorical_x.svg
index 89516c40..8d3bb7c5 100644
--- a/inst/tinytest/_tinysnapshot/text_categorical_x.svg
+++ b/inst/tinytest/_tinysnapshot/text_categorical_x.svg
@@ -28,11 +28,11 @@
LETTERS[1:2]
1:2
-
-
-
-A
-B
+
+
+
+A
+B
@@ -54,8 +54,8 @@
-1
-2
+1
+2
diff --git a/inst/tinytest/_tinysnapshot/tinyplot_add_layer_category_alignment.svg b/inst/tinytest/_tinysnapshot/tinyplot_add_layer_category_alignment.svg
index 2030f903..c0a8f801 100644
--- a/inst/tinytest/_tinysnapshot/tinyplot_add_layer_category_alignment.svg
+++ b/inst/tinytest/_tinysnapshot/tinyplot_add_layer_category_alignment.svg
@@ -28,13 +28,13 @@
g
y
-
-
+
+
-
-a
+
+a
b
-c
+c
@@ -60,12 +60,12 @@
-
+
-
-
+
+
-
+
diff --git a/inst/tinytest/_tinysnapshot/type_lines_categorical_lines.svg b/inst/tinytest/_tinysnapshot/type_lines_categorical_lines.svg
index 01cfe2ec..b89886f4 100644
--- a/inst/tinytest/_tinysnapshot/type_lines_categorical_lines.svg
+++ b/inst/tinytest/_tinysnapshot/type_lines_categorical_lines.svg
@@ -28,13 +28,13 @@
name
runtime
-
-
+
+
-
-Fellowship
+
+Fellowship
Return
-Two Towers
+Two Towers
@@ -54,8 +54,8 @@
-
-
+
+
diff --git a/inst/tinytest/_tinysnapshot/type_lines_categorical_points.svg b/inst/tinytest/_tinysnapshot/type_lines_categorical_points.svg
index 01cfe2ec..b89886f4 100644
--- a/inst/tinytest/_tinysnapshot/type_lines_categorical_points.svg
+++ b/inst/tinytest/_tinysnapshot/type_lines_categorical_points.svg
@@ -28,13 +28,13 @@
name
runtime
-
-
+
+
-
-Fellowship
+
+Fellowship
Return
-Two Towers
+Two Towers
@@ -54,8 +54,8 @@
-
-
+
+
diff --git a/inst/tinytest/_tinysnapshot/type_lines_categorical_y.svg b/inst/tinytest/_tinysnapshot/type_lines_categorical_y.svg
index 6157d6e0..07b21455 100644
--- a/inst/tinytest/_tinysnapshot/type_lines_categorical_y.svg
+++ b/inst/tinytest/_tinysnapshot/type_lines_categorical_y.svg
@@ -39,13 +39,13 @@
190
195
200
-
-
+
+
-
-Fellowship
+
+Fellowship
Return
-Two Towers
+Two Towers
@@ -54,10 +54,10 @@
-
-
-
-
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/type_lines_explicit_levels.svg b/inst/tinytest/_tinysnapshot/type_lines_explicit_levels.svg
index af59da9d..deff7b2b 100644
--- a/inst/tinytest/_tinysnapshot/type_lines_explicit_levels.svg
+++ b/inst/tinytest/_tinysnapshot/type_lines_explicit_levels.svg
@@ -28,13 +28,13 @@
name
runtime
-
-
+
+
-
-Fellowship
+
+Fellowship
Two Towers
-Return
+Return
@@ -54,11 +54,11 @@
-
-
-
+
+
+
-
+
diff --git a/inst/tinytest/_tinysnapshot/type_lines_flip_labels.svg b/inst/tinytest/_tinysnapshot/type_lines_flip_labels.svg
index 6157d6e0..07b21455 100644
--- a/inst/tinytest/_tinysnapshot/type_lines_flip_labels.svg
+++ b/inst/tinytest/_tinysnapshot/type_lines_flip_labels.svg
@@ -39,13 +39,13 @@
190
195
200
-
-
+
+
-
-Fellowship
+
+Fellowship
Return
-Two Towers
+Two Towers
@@ -54,10 +54,10 @@
-
-
-
-
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/type_lines_layer_h_p.svg b/inst/tinytest/_tinysnapshot/type_lines_layer_h_p.svg
index 68d0f831..67d529b8 100644
--- a/inst/tinytest/_tinysnapshot/type_lines_layer_h_p.svg
+++ b/inst/tinytest/_tinysnapshot/type_lines_layer_h_p.svg
@@ -28,13 +28,13 @@
name
runtime
-
-
+
+
-
-Fellowship
+
+Fellowship
Return
-Two Towers
+Two Towers
@@ -54,11 +54,11 @@
-
-
+
+
-
-
+
+
diff --git a/inst/tinytest/_tinysnapshot/type_lines_xlevels_asis.svg b/inst/tinytest/_tinysnapshot/type_lines_xlevels_asis.svg
index af59da9d..deff7b2b 100644
--- a/inst/tinytest/_tinysnapshot/type_lines_xlevels_asis.svg
+++ b/inst/tinytest/_tinysnapshot/type_lines_xlevels_asis.svg
@@ -28,13 +28,13 @@
name
runtime
-
-
+
+
-
-Fellowship
+
+Fellowship
Two Towers
-Return
+Return
@@ -54,11 +54,11 @@
-
-
-
+
+
+
-
+
diff --git a/inst/tinytest/_tinysnapshot/type_points_xlevels_idx.svg b/inst/tinytest/_tinysnapshot/type_points_xlevels_idx.svg
index 2715e07c..9446aa21 100644
--- a/inst/tinytest/_tinysnapshot/type_points_xlevels_idx.svg
+++ b/inst/tinytest/_tinysnapshot/type_points_xlevels_idx.svg
@@ -28,13 +28,13 @@
name
runtime
-
-
+
+
-
-Two Towers
+
+Two Towers
Return
-Fellowship
+Fellowship
@@ -54,8 +54,8 @@
-
-
+
+
diff --git a/inst/tinytest/test-axis-pad.R b/inst/tinytest/test-axis-pad.R
index 8d6d1f3b..0a0bd3d6 100644
--- a/inst/tinytest/test-axis-pad.R
+++ b/inst/tinytest/test-axis-pad.R
@@ -77,4 +77,78 @@ tpar(xpad = NULL)
expect_error(tpar(ypad = -1), pattern = "ypad")
tpar(ypad = NULL)
+
+# With no explicit pad, a categorical axis of a point-like type gets a quarter
+# of the tick gap instead of the usual 4%, so its end categories aren't drawn
+# flush against the frame.
+cats = function(n) factor(letters[1:n], levels = letters[1:n])
+cat_usr = function(n = 3, ...) {
+ tinyplot(cats(n), seq_len(n), ...)
+ par("usr")
+}
+
+# Constant in tick gaps however many categories there are -- until 4% of the
+# span reaches further on its own, which is where it hands back to base.
+expect_equal(cat_usr(2, type = "p")[1:2], c(0.75, 2.25))
+expect_equal(cat_usr(7, type = "p")[1:2], c(0.75, 7.25))
+expect_equal(cat_usr(8, type = "p")[1:2], c(0.72, 8.28))
+
+# Each type that declares the hint does so in its own data function.
+expect_equal(cat_usr(3, type = "l")[1:2], c(0.75, 3.25))
+rng = function(...) {
+ d = data.frame(g = cats(3), y = c(1, 5, 10))
+ tinyplot(y ~ g, ymin = y - 1, ymax = y + 1, data = d, ...)
+ par("usr")
+}
+expect_equal(rng(type = "errorbar")[1:2], c(0.75, 3.25)) # "pointrange" same
+
+# An explicit pad still wins. So do explicit limits or xaxs = "i", which hand
+# the axis back to base's usual treatment. The buffer follows its variable
+# under a flip.
+expect_equal(cat_usr(3, type = "p", xpad = 0)[1:2], c(1, 3))
+expect_equal(cat_usr(3, type = "p", xaxs = "i")[1:2], c(1, 3))
+expect_equal(cat_usr(3, type = "p", xlim = c(1, 3))[1:2], c(0.92, 3.08))
+expect_equal(cat_usr(3, type = "p", flip = TRUE)[3:4], c(0.75, 3.25))
+
+# Dodging widens the drawn extent past n-1; the gutter clears what is actually
+# drawn, so cat_pad() has to read the limits rather than the category count.
+dodged = function(dodge) {
+ d = data.frame(g = rep(cats(3), 4), y = rnorm(12), m = rep(c("m1", "m2"), 6))
+ tinyplot(y ~ g | m, ymin = y - 1, ymax = y + 1, data = d,
+ type = type_pointrange(dodge = dodge))
+ par("usr")[1:2]
+}
+set.seed(1)
+expect_equal(diff(dodged(0.3)) - diff(dodged(0)), 0.9, tolerance = 1e-8)
+
+# Free facets derive a range per panel, so the buffer has to be worked out per
+# panel too -- a dropped panel spanning fewer categories than the plot cannot
+# just inherit. `.fusr` is where a free panel's extent lives; par("usr")
+# reports the outer region once the plot is finished.
+panel_reach = function(cats_per_panel, n_global, ...) {
+ g = rep(letters[seq_len(n_global)], length.out = sum(cats_per_panel))
+ d = data.frame(
+ g = factor(g, levels = letters[seq_len(n_global)]),
+ y = seq_along(g),
+ f = rep(seq_along(cats_per_panel), cats_per_panel)
+ )
+ tinyplot(y ~ g, facet = ~f, data = d, type = "p",
+ facet.args = list(free = TRUE, drop.levels = TRUE), ...)
+ fu = tinyplot:::get_environment_variable(".fusr")
+ vapply(seq_along(cats_per_panel),
+ function(i) fu[[i]][2] - cats_per_panel[i], numeric(1))
+}
+
+# Nine categories globally puts the plot past the breakeven, so lim_args()
+# computes nothing and the facet path has to reach for the buffer itself.
+expect_equal(panel_reach(c(3, 3, 3), 9)[1], 0.25)
+
+# Three globally puts it under, so lim_args() does compute a pad -- but that is
+# a fraction of the *global* span, and a narrower panel needs its own. Reusing
+# it gave a two-category panel 0.125 instead of 0.25. (#732)
+expect_equal(panel_reach(c(3, 2), 3), c(0.25, 0.25))
+
+# An explicit pad is the user's business and still applies verbatim throughout.
+expect_equal(panel_reach(c(3, 2), 3, xpad = 0), c(0, 0))
+
dev.off()
diff --git a/man/facet.Rd b/man/facet.Rd
index 7f747fe9..21f05c5c 100644
--- a/man/facet.Rd
+++ b/man/facet.Rd
@@ -47,6 +47,8 @@ draw_facet_window(
yaxl,
yaxr = NULL,
ypad = NULL,
+ xpad_user = NULL,
+ ypad_user = NULL,
rev_x = FALSE,
rev_y = FALSE,
xlim_partial = NULL,
diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd
index f7b5e3dd..be5fb20e 100644
--- a/man/tinyplot.Rd
+++ b/man/tinyplot.Rd
@@ -440,10 +440,23 @@ used for the x-axis and y-axis, respectively. See
\code{\link[graphics]{par}} for the possible values.}
\item{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 \code{NULL},
-in which case behaviour depends on the value of \code{x/yaxs}. In most cases,
-this will translate to a value of \code{0.04}, i.e. 4\% padding on each end (see
-\code{\link[graphics]{par}}).}
+data range, should be added to each end of the axes. If \code{NULL} (the
+default), follows an automatic padding heuristic that tries to optimize for
+plot aesthetic depending on the axis type:
+\itemize{
+\item numeric axis: inherited from \code{x/yaxs}. Under the default \code{x/yaxs = "r"}
+style, this means \code{0.04}, i.e. 4\% padding on each end (see
+\code{\link[graphics]{par}}).
+\item categorical axis: behaviour depends on the glyph-type and number of
+unique categories, and therefore tick marks. For example, point-like
+glyphs with numerous (8+) categories inherit the same padding logic as a
+numeric axis (i.e., usually 4\%). For cases with fewer unique categories,
+the padding will instead correspond to 25\% of the gap between interior
+tick marks, so that the end categories aren't drawn flush against the
+plot frame. Note that providing an explicit \code{x/ylim} or setting
+\code{x/yaxs = "i"} skips this heuristic in favour of the standard numeric
+behaviour.
+}}
\item{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
diff --git a/man/tpar.Rd b/man/tpar.Rd
index de32dc8f..1bfb5de4 100644
--- a/man/tpar.Rd
+++ b/man/tpar.Rd
@@ -100,7 +100,7 @@ you should rather use \code{par()} instead.
\item \code{palette.sequential}: Palette for sequential colors. See the \code{palette} argument in \code{?tinyplot}.
\item \code{record}: (experimental) Logical indicating whether \code{tinyplot()} should record plots and return them as replayable \code{\link{recordedtinyplot}} objects. Defaults to \code{NULL}, which is equivalent to \code{FALSE}. Setting to \code{TRUE} allows for assignment and later recall, e.g. \verb{myplot = tinyplot(...); myplot}. Sets the default for the \code{record} argument of \code{\link[=tinyplot]{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 \code{tinyplot()} automatically enables it for any device that it opens itself via \code{file}, and further emits a warning if the current device is not recording.
\item \code{ribbon.alpha}: Numeric factor in the range \verb{[0,1]} for modifying the opacity alpha of "ribbon" and "area" type plots. Default value is \code{0.2}.
-\item \code{xpad}, \code{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 \code{NULL}, in which case behaviour depends on the value of \code{x/yaxs}. In most cases, this will translate to a value of \code{0.04}, i.e. 4\% padding on each end (see \code{\link[graphics]{par}}). Sets the default for the \code{xpad} and \code{ypad} arguments of \code{\link[=tinyplot]{tinyplot()}}, which take precedence.
+\item \code{xpad}, \code{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 \code{NULL}, in which case an automatic heuristic applies: a numeric axis inherits from \code{x/yaxs} (usually 4\% padding on each end), whereas a categorical axis with point-like glyphs and few unique categories instead gets 25\% of the gap between interior tick marks. Sets the default for the \code{xpad} and \code{ypad} arguments of \code{\link[=tinyplot]{tinyplot()}}, which take precedence and where the heuristic is described in full.
\item \code{xaxr}, \code{yaxr}: Numeric giving the rotation of the x- and y-axis tick labels, in degrees counter-clockwise; \code{NULL} (the default) leaves them unrotated. Unlike \code{las}, which is limited to the four right angles, any angle is permitted. Setting one overrides \code{las} for that axis alone, leaving the other axis under \code{las} as usual, and \code{0} (or any multiple of 360) counts as no rotation at all. Sets the default for the \code{xaxr} and \code{yaxr} arguments of \code{\link[=tinyplot]{tinyplot()}}, which take precedence. Two caveats follow from tinyplot drawing rotated labels itself rather than deferring to base \code{axis()}. First, margins are only resized to fit them under a theme with \code{dynmar = TRUE} (see \code{tinytheme}); under the default theme the margins are left alone, so a long rotated label will be clipped unless you widen \code{mar} yourself. Second, rotated labels do not inherit the thinning that \code{axis()} applies via \code{gap.axis}, so they start to overlap once the spacing between ticks falls below \verb{line height / sin(srt)}.
}
}
diff --git a/vignettes/types.qmd b/vignettes/types.qmd
index e417d8e3..667702e1 100644
--- a/vignettes/types.qmd
+++ b/vignettes/types.qmd
@@ -406,6 +406,7 @@ need only define the list of hints that you actually want:
| `legend_fills_from_col` | `type_hexbin()`, `type_spineplot()` | Fill the legend key with the group colour (`col`) rather than `bg`. |
| `legend_fills_from_seq_palette` | `type_ridge()` | Fill the legend key with a lighter step of the group colour's sequential ramp. |
| `legend_border_fg` | `type_spineplot()` | Draw the legend key border in the foreground colour rather than the group colour. |
+| `pads_cat_axis` | `type_points()`, `type_lines()`, `type_pointrange()`, `type_errorbar()`, `type_text()` | Buffer the ends of a categorical axis, so that the outermost categories aren't drawn flush against the plot frame. Declare it for types whose glyphs sit *on* their tick with no width of their own; see `xpad` in `?tinyplot` or `?tpar` for the details. |
| `legend_reversed` | `type_area(stack = TRUE)` | List the legend key bottom-up rather than top-down. For types whose groups read from the bottom of the plot upwards---stacking puts the first `by` level in the bottom band---so that the key reads in the same direction as the geometry rather than backwards against it. |
To see `type_hints` in action, consult the