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: 9 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion R/assertions.R
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 8 additions & 2 deletions R/facet.R
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions R/flip.R
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
30 changes: 29 additions & 1 deletion R/lim.R
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
)

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 9 additions & 1 deletion R/sanitize_axes.R
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
}
29 changes: 21 additions & 8 deletions R/tinyplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion R/tpar.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions R/type_lines.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
2 changes: 2 additions & 0 deletions R/type_pointrange.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions R/type_points.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
4 changes: 4 additions & 0 deletions R/type_text.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
Loading