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
10 changes: 10 additions & 0 deletions .github/workflows/R-CMD-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ jobs:
steps:
- uses: actions/checkout@v6

# r-ci's run.sh (hotfix a07614a, 2026-08-19) seds /etc/apt/apt-mirrors.txt
# unconditionally to drop the flaky azure mirror. That file only exists on
# GitHub-hosted runner images, not in containers, so the sed exits 2 and
# kills the step under `bash -e`. (r2u4ci escapes it via the /etc/r2u_ci
# stamp, which makes Bootstrap() skip BootstrapLinux entirely; drd has no
# such stamp.) An empty file is inert here -- containers never used the
# azure mirror. Drop once upstream guards the sed with `test -f`.
- name: Work around r-ci apt-mirrors.txt assumption
run: touch /etc/apt/apt-mirrors.txt

- name: Setup
uses: eddelbuettel/github-actions/r-ci@master

Expand Down
493 changes: 266 additions & 227 deletions NEWS.md

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions R/align_layer.R
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ align_layer = function(settings) {
if (setequal(names(xlabs_layer), names(xlabs_orig))) {
# If mappings already agree and no dodge, no realignment needed
if (identical(xlabs_layer, xlabs_orig) && is.null(settings$dodge)) return(invisible())
orig_order = xlabs_orig[names(xlabs_layer)[settings$datapoints[["x"]]]]
x_layer = settings$datapoints[["x"]]
if (is.null(settings$dodge)) {
x_new = x_layer[orig_order]
# Per-row lookup, not a permutation: the position each row's category
# occupies in the original layer. Indexing `x_layer` by it instead
# only coincided with the right answer when the layer's rows happened
# to arrive in ascending order. (#679)
x_new = unname(xlabs_orig[names(xlabs_layer)[x_layer]])
} else {
names(x_layer) = names(xlabs_layer)[round(x_layer)]
x_new = x_layer + (xlabs_orig[names(round(x_layer))] - round(x_layer))
Expand Down
7 changes: 6 additions & 1 deletion R/facet.R
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,12 @@ draw_facet_window = function(
if (.free_axes) {
.ayf = args_y
.ayf[[1L]] = yfree
if (isTRUE(flip) && type %in% c("barplot", "pointrange", "errorbar", "ribbon", "boxplot", "p", "violin") && !is.null(ylabs)) {
# Same signal as the fixed-scale branch above: named `ylabs` means the
# type put categories on the y-axis. Listing eligible types by name
# instead not only dropped the labels for unlisted types, it left the
# `labels` inherited from `args_y` without a matching `at`, which
# axis() rejects outright. (#679)
if (!is.null(ylabs)) {
.ayf = modifyList(.ayf, list(at = ylabs, labels = names(ylabs)))
} else if (!is.null(yat)) {
.ayf = modifyList(.ayf, list(at = yat))
Expand Down
41 changes: 41 additions & 0 deletions R/sanitize_xlevels.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## Reorder the levels of a categorical variable, per a type's `xlevels` (or
## `ylevels`) argument. Shared by every type that exposes such an argument;
## the accepted inputs are:
##
## - NULL: keep the existing factor levels (the default everywhere
## except type_errorbar()/type_pointrange())
## - "asis": take the categories in the order they appear in the data,
## i.e. skip the alphabetical sorting that factor() applies
## when coercing a character variable (cf. read.table's
## `as.is` argument)
## - character: the levels in the desired order
## - numeric: indexes into the existing levels, e.g. 3:1
##
## Only affects factors (character variables have already been coerced by
## sanitize_datapoints() when this runs inside a type_data() function); any
## other class is returned untouched, so the argument is inert for numeric
## variables. A length-1 "asis" is always read as the keyword: in the
## degenerate case of a category literally named "asis", set the factor
## levels beforehand instead.
##
## Site-specific follow-ups -- re-syncing `by` when it aliases the releveled
## variable (spineplot, ridge), or converting the factor to integer positions
## (points, lines, pointrange) -- remain at the call sites.
sanitize_xlevels = function(x, xlevels, arg = "xlevels") {
if (is.null(xlevels) || !is.factor(x)) {
return(x)
}
if (identical(xlevels, "asis")) {
return(factor(x, levels = unique(x)))
}
if (is.numeric(xlevels)) {
xlevels = levels(x)[xlevels]
}
if (anyNA(xlevels) || !all(xlevels %in% levels(x))) {
warning(sprintf(
"not all '%s' correspond to levels of '%s'",
arg, substr(arg, 1, 1)
))
}
factor(x, levels = xlevels)
}
11 changes: 5 additions & 6 deletions R/type_barplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
#' group of `x` in case of using a two-sided formula `y ~ x` (default: mean).
#' @param xlevels a character or numeric vector specifying the ordering of the
#' levels of the `x` variable (if character) or the corresponding indexes
#' (if numeric) for the plot.
#' (if numeric) for the plot. The special keyword `"asis"` takes the
#' categories in the order that they appear in the data. Note that this
#' argument only affects categorical (i.e., factor or character) `x`
#' variables.
#' @param xaxlabels a character vector with the axis labels for the `x` variable,
#' defaulting to the levels of `x`.
#' @param offset optional specification for shifting bar baselines, accepting
Expand Down Expand Up @@ -171,11 +174,7 @@ data_barplot = function(width = 5/6, beside = FALSE, center = FALSE, offset = NU
if (is.null(FUN)) FUN = function(x, ...) mean(x, ..., na.rm = TRUE)
}
if (!is.factor(datapoints$x)) datapoints$x = factor(datapoints$x)
if (!is.null(xlevels)) {
xlevels = if(is.numeric(xlevels)) levels(datapoints$x)[xlevels] else xlevels
if (anyNA(xlevels) || !all(xlevels %in% levels(datapoints$x))) warning("not all 'xlevels' correspond to levels of 'x'")
datapoints$x = factor(datapoints$x, levels = xlevels)
}
datapoints$x = sanitize_xlevels(datapoints$x, xlevels)
if (!is.null(xaxlabels)) levels(datapoints$x) = xaxlabels
datapoints = aggregate(datapoints[, "y", drop = FALSE], datapoints[, c("x", "by", "facet")], FUN = FUN, drop = FALSE)
datapoints$y[is.na(datapoints$y)] = 0 #FIXME: always?#
Expand Down
15 changes: 13 additions & 2 deletions R/type_errorbar.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
#'
#' @inheritParams dodge_positions
#' @inheritParams graphics::arrows
#' @param xlevels a character or numeric vector specifying the order in which
#' the levels of the `x` variable should be plotted (as level names if
#' character, or level indexes if numeric, e.g. `3:1`). Note that this
#' argument only affects categorical (i.e., factor or character) `x`
#' variables; it is ignored for numeric `x`. Unlike most other plot types,
#' here it defaults to the special keyword `"asis"`, which takes the
#' categories in the order that they appear in the data: these types are
#' typically used for coefficient plots, where the row order of the data
#' (e.g., the terms of a model) is usually intentional. Set
#' `xlevels = NULL` to follow the factor levels instead, matching the other
#' plot types.
#' @examples
#' tinytheme("basic")
#'
Expand Down Expand Up @@ -86,10 +97,10 @@
#' tinytheme() # reset theme
#'
#' @export
type_errorbar = function(length = 0.05, dodge = 0, fixed.dodge = FALSE) {
type_errorbar = function(length = 0.05, dodge = 0, fixed.dodge = FALSE, xlevels = "asis") {
out = list(
draw = draw_errorbar(length = length),
data = data_pointrange(dodge = dodge, fixed.dodge = fixed.dodge),
data = data_pointrange(dodge = dodge, fixed.dodge = fixed.dodge, xlevels = xlevels),
name = "p"
)
class(out) = "tinyplot_type"
Expand Down
59 changes: 44 additions & 15 deletions R/type_lines.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,26 @@
#'
#' @inheritParams graphics::plot.default
#' @inheritParams dodge_positions
#'
#' @inheritParams type_points
#'
#' @section Categorical axes:
#'
#' Like the other plot types, `type_lines()` places categorical (factor or
#' character) data according to the factor levels. Character variables are
#' coerced with [factor()] and so end up in alphabetical order. To order the
#' categories by their appearance in the data instead, use
#' `xlevels = "asis"`, or set the levels explicitly, e.g.
#' `factor(x, levels = unique(x))`.
#'
#' Note that the lines themselves are always drawn in the order that the rows
#' arrive in, exactly as base [lines()] does. Categories whose level order
#' differs from their row order will therefore produce a zig-zag, just as an
#' unsorted numeric x-variable would.
#'
#' @examples
#' # "l" type convenience character string
#' tinyplot(circumference ~ age | Tree, data = Orange, type = "l")
#'
#'
#' # Use `type_lines()` to pass extra arguments for customization
#' tinyplot(circumference ~ age | Tree, data = Orange, type = type_lines(type = "s"))
#'
Expand All @@ -33,42 +48,56 @@
#' )
#'
#' @export
type_lines = function(type = "l", dodge = 0, fixed.dodge = FALSE) {
type_lines = function(type = "l", dodge = 0, fixed.dodge = FALSE, xlevels = NULL) {
out = list(
draw = draw_lines(type = type),
data = data_lines(dodge = dodge, fixed.dodge = fixed.dodge),
data = data_lines(dodge = dodge, fixed.dodge = fixed.dodge, xlevels = xlevels),
name = type
)
class(out) = "tinyplot_type"
return(out)
}


data_lines = function(dodge = 0, fixed.dodge = FALSE) {
data_lines = function(dodge = 0, fixed.dodge = FALSE, xlevels = NULL) {
fun = function(settings, ...) {
env2env(settings, environment(), c("datapoints", "xlabs"))
env2env(settings, environment(), "datapoints")

if (is.character(datapoints$x)) {
datapoints$x = as.factor(datapoints$x)
}
if (is.factor(datapoints$x)) {
# honour pre-ordered factors; otherwise fall back to first-appearance order
xlvls = if (is.ordered(datapoints$x)) levels(datapoints$x) else unique(datapoints$x)
datapoints$x = factor(datapoints$x, levels = xlvls)
# Categorical axes follow the factor levels, exactly as in data_points().
# (Character vectors have already been coerced by sanitize_datapoints().)
# Ordering by the levels rather than by first appearance means an explicit
# `factor(x, levels = ...)` is honoured, and that layering a line type onto
# a point type (or vice versa) lands on the same categories. #679
datapoints[["x"]] = sanitize_xlevels(datapoints[["x"]], xlevels)
if (is.factor(datapoints[["x"]])) {
xlvls = levels(datapoints[["x"]])
xlabs = seq_along(xlvls)
names(xlabs) = xlvls
datapoints$x = as.integer(datapoints$x)
datapoints[["x"]] = as.integer(datapoints[["x"]])
} else {
xlabs = NULL
}
if (is.factor(datapoints[["y"]])) {
ylvls = levels(datapoints[["y"]])
ylabs = seq_along(ylvls)
names(ylabs) = ylvls
datapoints[["y"]] = as.integer(datapoints[["y"]])
} else {
ylabs = NULL
}

# dodge
if (dodge != 0) {
datapoints = dodge_positions(datapoints, dodge, fixed.dodge)
}

x = datapoints$x
x = datapoints[["x"]]
y = datapoints[["y"]]
env2env(environment(), settings, c(
"x",
"y",
"xlabs",
"ylabs",
"datapoints"
))
}
Expand Down
14 changes: 8 additions & 6 deletions R/type_pointrange.R
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#' @rdname type_errorbar
#' @export
type_pointrange = function(dodge = 0, fixed.dodge = FALSE) {
type_pointrange = function(dodge = 0, fixed.dodge = FALSE, xlevels = "asis") {
out = list(
draw = draw_pointrange(),
data = data_pointrange(dodge = dodge, fixed.dodge = fixed.dodge),
data = data_pointrange(dodge = dodge, fixed.dodge = fixed.dodge, xlevels = xlevels),
name = "p"
)
class(out) = "tinyplot_type"
Expand Down Expand Up @@ -47,17 +47,19 @@ draw_pointrange = function() {
}


data_pointrange = function(dodge, fixed.dodge) {
data_pointrange = function(dodge, fixed.dodge, xlevels = "asis") {
fun = function(settings, ...) {
env2env(settings, environment(), c("datapoints", "xlabs", "cex", "lty", "lwd"))

if (is.character(datapoints$x)) {
datapoints$x = as.factor(datapoints$x)
}
## default xlevels = "asis" preserves the row order of the data (i.e., no
## new sorting by factor), since these types are typically used for
## coefficient plots where that order is intentional
datapoints$x = sanitize_xlevels(datapoints$x, xlevels)
if (is.factor(datapoints$x)) {
## original data (i.e., no new sorting by factor)
xlvls = unique(datapoints$x)
datapoints$x = factor(datapoints$x, levels = xlvls)
xlvls = levels(datapoints$x)
xlabs = seq_along(xlvls)
names(xlabs) = xlvls
datapoints$x = as.integer(datapoints$x)
Expand Down
16 changes: 13 additions & 3 deletions R/type_points.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
#' @description Type function for plotting points, i.e. a scatter plot.
#' @param clim Numeric giving the lower and upper limits of the character
#' expansion (`cex`) normalization for bubble charts.
#' @param xlevels a character or numeric vector specifying the order in which
#' the levels of the `x` variable should be plotted (as level names if
#' character, or level indexes if numeric, e.g. `3:1`). The special keyword
#' `"asis"` takes the categories in the order that they appear in the data,
#' i.e. skipping the alphabetical sort that is otherwise applied when
#' coercing a character variable to a factor. Note that this argument only
#' affects categorical (i.e., factor or character) `x` variables; it is
#' ignored for numeric `x`. The default `NULL` keeps the existing factor
#' levels (alphabetical for character variables).
#' @inheritParams dodge_positions
#'
#' @examples
Expand Down Expand Up @@ -32,24 +41,25 @@
#' pch = 21, fill = 0.3)
#'
#' @export
type_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE) {
type_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE, xlevels = NULL) {
out = list(
data = data_points(clim = clim, dodge = dodge, fixed.dodge = fixed.dodge),
data = data_points(clim = clim, dodge = dodge, fixed.dodge = fixed.dodge, xlevels = xlevels),
draw = draw_points(),
name = "p"
)
class(out) = "tinyplot_type"
return(out)
}

data_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE) {
data_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE, xlevels = NULL) {
fun = function(settings, ...) {
env2env(settings, environment(), "datapoints")

# Store clim for bubble() function
settings$clim = clim

# catch for factors (we should still be able to "force" plot these with points)
datapoints$x = sanitize_xlevels(datapoints$x, xlevels)
if (is.factor(datapoints$x)) {
xlvls = levels(datapoints$x)
xlabs = seq_along(xlvls)
Expand Down
5 changes: 3 additions & 2 deletions R/type_ridge.R
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
#' (rather than the raw original variable). Only one of `breaks` or
#' `probs` must be specified.
#' @param ylevels a character or numeric vector specifying in which order
#' the levels of the y-variable should be plotted.
#' the levels of the y-variable should be plotted. The special keyword
#' `"asis"` takes the categories in the order that they appear in the data.
#' @inheritParams stats::density
#' @param bw the smoothing \code{\link[stats:bw.nrd]{bandwidth}} to be used,
#' see \code{\link[stats]{density}} for details and options.
Expand Down Expand Up @@ -286,7 +287,7 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512,
## reorder levels of y-variable if requested
if (!is.null(ylevels)) {
if (!is.factor(datapoints$y)) datapoints$y = factor(datapoints$y)
datapoints$y = factor(datapoints$y, levels = if(is.numeric(ylevels)) levels(datapoints$y)[ylevels] else ylevels)
datapoints$y = sanitize_xlevels(datapoints$y, ylevels, arg = "ylevels")
if (y_by) datapoints$by = datapoints$y
}

Expand Down
12 changes: 5 additions & 7 deletions R/type_spineplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
#' to `type_spineplot()` if `y` is a factor variable.
#' @param xlevels,ylevels a character or numeric vector specifying the ordering of the
#' levels of the `x` and `y` variables (if character) or the corresponding indexes
#' (if numeric) for the plot.
#' (if numeric) for the plot. The special keyword `"asis"` takes the
#' categories in the order that they appear in the data. Note that these
#' arguments only affect categorical (i.e., factor or character) variables.
#' @inheritParams graphics::spineplot
#' @param lighten logical. For grouped spineplots where the `y` variable is
#' itself the grouping variable (i.e. `y == by`), should the fills use a
Expand Down Expand Up @@ -157,15 +159,11 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels

x.categorical = is.factor(datapoints$x)
if (!is.null(xlevels) && x.categorical) {
xlevels = if(is.numeric(xlevels)) levels(datapoints$x)[xlevels] else xlevels
if (anyNA(xlevels) || !all(xlevels %in% levels(datapoints$x))) warning("not all 'xlevels' correspond to levels of 'x'")
datapoints$x = factor(datapoints$x, levels = xlevels)
datapoints$x = sanitize_xlevels(datapoints$x, xlevels)
if (x_by) datapoints$by = datapoints$x
}
if (!is.null(ylevels)) {
ylevels = if(is.numeric(ylevels)) levels(datapoints$y)[ylevels] else ylevels
if (anyNA(ylevels) || !all(ylevels %in% levels(datapoints$y))) warning("not all 'ylevels' correspond to levels of 'y'")
datapoints$y = factor(datapoints$y, levels = ylevels)
datapoints$y = sanitize_xlevels(datapoints$y, ylevels, arg = "ylevels")
if (y_by) datapoints$by = datapoints$y
}

Expand Down
4 changes: 2 additions & 2 deletions altdoc/pkgdown.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
altdoc: 0.7.3
pandoc: '0'
pandoc: 3.8.3
pkgdown: 2.1.3
pkgdown_sha: ~
last_built: 2026-08-17T18:20:36+0000
last_built: 2026-08-18T05:02:34+0000
urls:
reference: https://grantmcdermott.com/tinyplot/man
article: https://grantmcdermott.com/tinyplot/vignettes
Loading