From f892c6ac470217f6cffcee82c3e03ce291f87819 Mon Sep 17 00:00:00 2001 From: Achim Zeileis Date: Wed, 25 Jun 2025 00:48:56 +0200 Subject: [PATCH 1/5] support numeric xlevels specification (as intended), improve documentation and examples, and add warning about incorrect specification --- R/type_barplot.R | 15 ++++++++++++--- man/type_barplot.Rd | 9 +++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/R/type_barplot.R b/R/type_barplot.R index 944fc4ab..0ef1f3e9 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -19,8 +19,9 @@ #' or the mid-way in the third category, respectively. #' @param FUN a function to compute the summary statistic for `y` within each #' group of `x` in case of using a two-sided formula `y ~ x` (default: mean). -#' @param xlevels a character or numeric vector specifying in which order the -#' levels of the `x` variable should be plotted. +#' @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) should be plotted. #' @param xaxlabels a character vector with the axis labels for the `x` variable, #' defaulting to the levels of `x`. #' @param drop.zeros logical. Should bars with zero height be dropped? If set @@ -34,6 +35,10 @@ #' tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE) #' tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE, fill = 0.2) #' +#' # Reorder x variable categories either by their character levels or numeric indexes +#' tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = c("8", "6", "4")) +#' tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = 3:1) +#' #' # Note: Above we used automatic argument passing for `beside`. But this #' # wouldn't work for `width`, since it would conflict with the top-level #' # `tinyplot(..., width = )` argument. It's safer to pass these args @@ -88,7 +93,11 @@ data_barplot = function(width = 5/6, beside = FALSE, center = FALSE, FUN = NULL, 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)) datapoints$x = factor(datapoints$x, levels = if(is.numeric(xlevels)) levels(x)[xlevels] else xlevels) + if (!is.null(xlevels)) { + xlevels = if(is.numeric(xlevels)) levels(datapoints$x)[xlevels] else xlevels + if (any(is.na(xlevels)) || !all(xlevels %in% levels(datapoints$x))) warning("not all 'xlevels' correspond to levels of 'x'") + datapoints$x = factor(datapoints$x, levels = 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?# diff --git a/man/type_barplot.Rd b/man/type_barplot.Rd index 35972f0b..d74d9fb4 100644 --- a/man/type_barplot.Rd +++ b/man/type_barplot.Rd @@ -32,8 +32,9 @@ or the mid-way in the third category, respectively.} \item{FUN}{a function to compute the summary statistic for \code{y} within each group of \code{x} in case of using a two-sided formula \code{y ~ x} (default: mean).} -\item{xlevels}{a character or numeric vector specifying in which order the -levels of the \code{x} variable should be plotted.} +\item{xlevels}{a character or numeric vector specifying the ordering of the +levels of the \code{x} variable (if character) or the corresponding indexes +(if numeric) should be plotted.} \item{xaxlabels}{a character vector with the axis labels for the \code{x} variable, defaulting to the levels of \code{x}.} @@ -56,6 +57,10 @@ tinyplot(~ cyl | vs, data = mtcars, type = "barplot") tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE) tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE, fill = 0.2) +# Reorder x variable categories either by their character levels or numeric indexes +tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = c("8", "6", "4")) +tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = 3:1) + # Note: Above we used automatic argument passing for `beside`. But this # wouldn't work for `width`, since it would conflict with the top-level # `tinyplot(..., width = )` argument. It's safer to pass these args From d5c04af06a8e273a739e29847e717f84f05158b3 Mon Sep 17 00:00:00 2001 From: Achim Zeileis Date: Wed, 25 Jun 2025 00:52:29 +0200 Subject: [PATCH 2/5] document xlevels fix in type_barplot in NEWS --- NEWS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 3ee126e7..d2f39c39 100644 --- a/NEWS.md +++ b/NEWS.md @@ -16,7 +16,9 @@ where the formatting is also better._ - Safer handling of pre-plot hooks. Resolves an issue affecting how `tinyplot` behaves inside loops, particularly for themed plots where only the final plot was being drawn in Quarto/RMarkdown contexts. Special thanks to @hadley and @cderv - for helping us to debug. (@vincentarelbundock #425) + for helping us to debug. (#425 @vincentarelbundock) +- The `xlevels` argument of `type_barplot()` could not handle numeric indexes correctly + (#431 @zeileis) ## 0.4.1 From 01fa7f1c20c335ec07f3965e0eaafaabc777c37e Mon Sep 17 00:00:00 2001 From: Achim Zeileis Date: Wed, 25 Jun 2025 00:52:54 +0200 Subject: [PATCH 3/5] NEWS improvement --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index d2f39c39..96dc45d0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -17,7 +17,7 @@ where the formatting is also better._ behaves inside loops, particularly for themed plots where only the final plot was being drawn in Quarto/RMarkdown contexts. Special thanks to @hadley and @cderv for helping us to debug. (#425 @vincentarelbundock) -- The `xlevels` argument of `type_barplot()` could not handle numeric indexes correctly +- The `xlevels` argument of `type_barplot()` could not handle numeric indexes correctly. (#431 @zeileis) ## 0.4.1 From 865d1afef44acdb271614a4d2bc67c232b1a2d10 Mon Sep 17 00:00:00 2001 From: Achim Zeileis Date: Wed, 25 Jun 2025 01:11:14 +0200 Subject: [PATCH 4/5] Add (in addition to ) in for spine plots with categorical variable. --- NEWS.md | 2 ++ R/type_barplot.R | 2 +- R/type_spineplot.R | 30 +++++++++++++++++++++++------- man/type_barplot.Rd | 2 +- man/type_spineplot.Rd | 12 ++++++++++-- 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/NEWS.md b/NEWS.md index 96dc45d0..7e1cf144 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,6 +10,8 @@ where the formatting is also better._ - `type_text()` gains `xpd` and `srt` arguments for controlling text clipping rotation, respectively. (#428 @grantmcdermott) +- Add `xlevels` (in addition to `ylevels`) in `type_spineplot()` for spine plots + with categorical `x` variable. (#431 @zeileis) ### Bug fixes diff --git a/R/type_barplot.R b/R/type_barplot.R index 0ef1f3e9..64c3ac47 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -21,7 +21,7 @@ #' 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) should be plotted. +#' (if numeric) for the plot. #' @param xaxlabels a character vector with the axis labels for the `x` variable, #' defaulting to the levels of `x`. #' @param drop.zeros logical. Should bars with zero height be dropped? If set diff --git a/R/type_spineplot.R b/R/type_spineplot.R index 08cb8b37..13dcd537 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -4,6 +4,9 @@ #' are modified versions of histograms or mosaic plots, and particularly #' useful for visualizing factor variables. Note that [`tinyplot`] defaults #' 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. #' @inheritParams graphics::spineplot #' @examples #' # "spineplot" type convenience string @@ -47,7 +50,13 @@ #' type = type_spineplot(weights = ttnc$Freq), #' palette = "Dark 2", facet.args = list(nrow = 1), axes = "t" #' ) -#' +#' +#' # Reorder x and y variable categories either by their character levels or numeric indexes +#' tinyplot( +#' Survived ~ Sex, facet = ~ Class, data = ttnc, +#' type = type_spineplot(weights = ttnc$Freq, xlevels = c("Female", "Male"), ylevels = 2:1) +#' ) +#' #' # Note: It's possible to use "by" on its own (without faceting), but the #' # overlaid result isn't great. We will likely overhaul this behaviour in a #' # future version of tinyplot... @@ -56,10 +65,10 @@ #' ) #' #' @export -type_spineplot = function(breaks = NULL, tol.ylab = 0.05, off = NULL, ylevels = NULL, col = NULL, xaxlabels = NULL, yaxlabels = NULL, weights = NULL) { +type_spineplot = function(breaks = NULL, tol.ylab = 0.05, off = NULL, xlevels = NULL, ylevels = NULL, col = NULL, xaxlabels = NULL, yaxlabels = NULL, weights = NULL) { col = col out = list( - data = data_spineplot(off = off, breaks = breaks, ylevels = ylevels, xaxlabels = xaxlabels, yaxlabels = yaxlabels, weights = weights), + data = data_spineplot(off = off, breaks = breaks, xlevels = xlevels, ylevels = ylevels, xaxlabels = xaxlabels, yaxlabels = yaxlabels, weights = weights), draw = draw_spineplot(tol.ylab = tol.ylab, off = off, col = col, xaxlabels = xaxlabels, yaxlabels = yaxlabels), name = "spineplot" ) @@ -68,7 +77,7 @@ type_spineplot = function(breaks = NULL, tol.ylab = 0.05, off = NULL, ylevels = } #' @importFrom grDevices nclass.Sturges -data_spineplot = function(off = NULL, breaks = NULL, ylevels = ylevels, xaxlabels = NULL, yaxlabels = NULL, weights = NULL) { +data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels = ylevels, xaxlabels = NULL, yaxlabels = NULL, weights = NULL) { fun = function( datapoints, by = NULL, col = NULL, bg = NULL, palette = NULL, @@ -114,7 +123,6 @@ data_spineplot = function(off = NULL, breaks = NULL, ylevels = ylevels, xaxlabel ## process y variable if (!is.factor(datapoints$y)) datapoints$y = factor(datapoints$y) - # if (!is.null(ylevels)) datapoints$y = factor(datapoints$y, levels = if(is.numeric(ylevels)) levels(datapoints$y)[ylevels] else ylevels) if (is.null(ylim)) ylim = c(0, 1) ## adjust facet margins @@ -125,12 +133,20 @@ data_spineplot = function(off = NULL, breaks = NULL, ylevels = ylevels, xaxlabel x_by = identical(datapoints$x, datapoints$by) y_by = identical(datapoints$y, datapoints$by) + x.categorical = is.factor(datapoints$x) + if (!is.null(xlevels) && x.categorical) { + xlevels = if(is.numeric(xlevels)) levels(datapoints$x)[xlevels] else xlevels + if (any(is.na(xlevels)) || !all(xlevels %in% levels(datapoints$x))) warning("not all 'xlevels' correspond to levels of 'x'") + datapoints$x = factor(datapoints$x, levels = xlevels) + if (x_by) datapoints$by = datapoints$x + } if (!is.null(ylevels)) { - datapoints$y = factor(datapoints$y, levels = if(is.numeric(ylevels)) levels(datapoints$y)[ylevels] else ylevels) + ylevels = if(is.numeric(ylevels)) levels(datapoints$y)[ylevels] else ylevels + if (any(is.na(ylevels)) || !all(ylevels %in% levels(datapoints$y))) warning("not all 'ylevels' correspond to levels of 'y'") + datapoints$y = factor(datapoints$y, levels = ylevels) if (y_by) datapoints$by = datapoints$y } - x.categorical = is.factor(datapoints$x) x = datapoints$x y = datapoints$y diff --git a/man/type_barplot.Rd b/man/type_barplot.Rd index d74d9fb4..35266a78 100644 --- a/man/type_barplot.Rd +++ b/man/type_barplot.Rd @@ -34,7 +34,7 @@ group of \code{x} in case of using a two-sided formula \code{y ~ x} (default: me \item{xlevels}{a character or numeric vector specifying the ordering of the levels of the \code{x} variable (if character) or the corresponding indexes -(if numeric) should be plotted.} +(if numeric) for the plot.} \item{xaxlabels}{a character vector with the axis labels for the \code{x} variable, defaulting to the levels of \code{x}.} diff --git a/man/type_spineplot.Rd b/man/type_spineplot.Rd index c9a27743..ce0a3903 100644 --- a/man/type_spineplot.Rd +++ b/man/type_spineplot.Rd @@ -8,6 +8,7 @@ type_spineplot( breaks = NULL, tol.ylab = 0.05, off = NULL, + xlevels = NULL, ylevels = NULL, col = NULL, xaxlabels = NULL, @@ -27,8 +28,9 @@ type_spineplot( \item{off}{vertical offset between the bars (in per cent). It is fixed to \code{0} for spinograms and defaults to \code{2} for spine plots.} -\item{ylevels}{a character or numeric vector specifying in which order - the levels of the dependent variable should be plotted.} +\item{xlevels, ylevels}{a character or numeric vector specifying the ordering of the +levels of the \code{x} and \code{y} variables (if character) or the corresponding indexes +(if numeric) for the plot.} \item{col}{a vector of fill colors of the same length as \code{levels(y)}. The default is to call \code{\link{gray.colors}}.} @@ -92,6 +94,12 @@ tinyplot( palette = "Dark 2", facet.args = list(nrow = 1), axes = "t" ) +# Reorder x and y variable categories either by their character levels or numeric indexes +tinyplot( + Survived ~ Sex, facet = ~ Class, data = ttnc, + type = type_spineplot(weights = ttnc$Freq, xlevels = c("Female", "Male"), ylevels = 2:1) +) + # Note: It's possible to use "by" on its own (without faceting), but the # overlaid result isn't great. We will likely overhaul this behaviour in a # future version of tinyplot... From 61371253a15a2d9677e3de198a9233e25a98cc96 Mon Sep 17 00:00:00 2001 From: Achim Zeileis Date: Wed, 25 Jun 2025 01:16:40 +0200 Subject: [PATCH 5/5] add tinysnapshot for xlevels fix (issue 430) --- .../barplot_xlevels_issue430.svg | 63 +++++++++++++++++++ inst/tinytest/test-type_barplot.R | 7 ++- 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 inst/tinytest/_tinysnapshot/barplot_xlevels_issue430.svg diff --git a/inst/tinytest/_tinysnapshot/barplot_xlevels_issue430.svg b/inst/tinytest/_tinysnapshot/barplot_xlevels_issue430.svg new file mode 100644 index 00000000..26076ab1 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/barplot_xlevels_issue430.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + +cyl +Count +8 +6 +4 + + + + + + + + + +0 +2 +4 +6 +8 +10 +12 +14 + + + + + + + + + + + + + diff --git a/inst/tinytest/test-type_barplot.R b/inst/tinytest/test-type_barplot.R index f75580af..9fce4274 100644 --- a/inst/tinytest/test-type_barplot.R +++ b/inst/tinytest/test-type_barplot.R @@ -39,4 +39,9 @@ f = function() { tinyplot(Freq ~ Sex | Survived, facet = ~ Class, data = as.data.frame(Titanic), type = "barplot", flip = TRUE, fill = 0.6, beside = TRUE) } -expect_snapshot_plot(f, label = "barplot_flip_fancy") \ No newline at end of file +expect_snapshot_plot(f, label = "barplot_flip_fancy") + +f = function() { + tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = 3:1) +} +expect_snapshot_plot(f, label = "barplot_xlevels_issue430")