From 7c919943165896c800b5c967211ddfe91b1a44f2 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 30 Aug 2026 13:25:35 -0700 Subject: [PATCH 1/6] switch to aggregate --- R/type_summary.R | 13 +---- .../tinyplot_add_numeric_x_violin.svg | 57 ------------------- man/type_summary.Rd | 4 +- 3 files changed, 5 insertions(+), 69 deletions(-) diff --git a/R/type_summary.R b/R/type_summary.R index 059609f95..b6efe9c7d 100644 --- a/R/type_summary.R +++ b/R/type_summary.R @@ -4,8 +4,8 @@ #' @description #' Applies a summary function to `y` along unique values of `x`. For example, #' plot the mean `y` value for each `x` value. Internally, -#' `type_summary()` applies a thin wrapper around \code{\link[stats]{ave}} and -#' then passes the result to [`type_lines`] for drawing. +#' `type_summary()` applies a thin wrapper around \code{\link[stats]{aggregate}} +#' and then passes the result to [`type_lines`] for drawing. #' #' @param fun summarizing function. Should be compatible with #' \code{\link[stats]{ave}}. Defaults to \code{\link[base]{mean}}. @@ -43,14 +43,7 @@ type_summary = function(fun = mean, ...) { funky = function(settings, ...) { env2env(settings, environment(), c("datapoints", "by", "facet")) - datapoints = split(datapoints, list(datapoints$facet, datapoints$by), drop = TRUE) - datapoints = lapply(datapoints, function(dat) { - newy = ave(dat$y, dat$x, FUN = fun) - dat$y = newy - dat = dat[order(dat$x), ] - return(dat) - }) - datapoints = do.call(rbind, datapoints) + datapoints = aggregate(y ~ x + facet + by, data = datapoints, FUN = fun) env2env(environment(), settings, "datapoints") } return(funky) diff --git a/inst/tinytest/_tinysnapshot/tinyplot_add_numeric_x_violin.svg b/inst/tinytest/_tinysnapshot/tinyplot_add_numeric_x_violin.svg index 6fda13b30..d958bae1f 100644 --- a/inst/tinytest/_tinysnapshot/tinyplot_add_numeric_x_violin.svg +++ b/inst/tinytest/_tinysnapshot/tinyplot_add_numeric_x_violin.svg @@ -58,64 +58,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/man/type_summary.Rd b/man/type_summary.Rd index 70e17e743..d4d0ed709 100644 --- a/man/type_summary.Rd +++ b/man/type_summary.Rd @@ -16,8 +16,8 @@ ex: \code{type="p"}, \code{col="pink"}.} \description{ Applies a summary function to \code{y} along unique values of \code{x}. For example, plot the mean \code{y} value for each \code{x} value. Internally, -\code{type_summary()} applies a thin wrapper around \code{\link[stats]{ave}} and -then passes the result to \code{\link{type_lines}} for drawing. +\code{type_summary()} applies a thin wrapper around \code{\link[stats]{aggregate}} +and then passes the result to \code{\link{type_lines}} for drawing. } \examples{ # Plot the mean chick weight over time From 998200aa41a2227fae8fda210b2d8f8aaf604abf Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 30 Aug 2026 13:51:48 -0700 Subject: [PATCH 2/6] support dodge --- R/type_summary.R | 28 ++++++++++++++++++++++++++-- man/type_summary.Rd | 24 +++++++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/R/type_summary.R b/R/type_summary.R index b6efe9c7d..3e2d05e6f 100644 --- a/R/type_summary.R +++ b/R/type_summary.R @@ -9,6 +9,8 @@ #' #' @param fun summarizing function. Should be compatible with #' \code{\link[stats]{ave}}. Defaults to \code{\link[base]{mean}}. +#' @inheritParams dodge_positions +#' @inheritParams type_points #' @param ... Additional arguments are passed to the `lines()` function, #' ex: `type="p"`, `col="pink"`. #' @seealso [`ave`] which performs the summarizing (averaging) behind the @@ -36,14 +38,36 @@ #' #' @importFrom stats ave #' @export -type_summary = function(fun = mean, ...) { +type_summary = function(fun = mean, dodge = 0, fixed.dodge = FALSE, ...) { assert_function(fun) lines_args = list(...) data_summary = function(fun) { funky = function(settings, ...) { env2env(settings, environment(), c("datapoints", "by", "facet")) + datapoints[["rowid"]] = NULL + datapoints = aggregate(. ~ x + facet + by, data = datapoints, FUN = fun) + if (dodge != 0) { + if (is.factor(datapoints[["x"]])) { + xlvls = levels(datapoints[["x"]]) + xlabs = seq_along(xlvls) + names(xlabs) = xlvls + datapoints[["x"]] = as.integer(datapoints[["x"]]) + env2env(environment(), settings, "xlabs") + } 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"]]) + env2env(environment(), settings, "ylabs") + } else { + ylabs = NULL + } - datapoints = aggregate(y ~ x + facet + by, data = datapoints, FUN = fun) + datapoints = dodge_positions(datapoints, dodge, fixed.dodge) + } env2env(environment(), settings, "datapoints") } return(funky) diff --git a/man/type_summary.Rd b/man/type_summary.Rd index d4d0ed709..54ce47647 100644 --- a/man/type_summary.Rd +++ b/man/type_summary.Rd @@ -4,12 +4,34 @@ \alias{type_summary} \title{Plot summary values of \code{y} at unique values of \code{x}} \usage{ -type_summary(fun = mean, ...) +type_summary(fun = mean, dodge = 0, fixed.dodge = FALSE, ...) } \arguments{ \item{fun}{summarizing function. Should be compatible with \code{\link[stats]{ave}}. Defaults to \code{\link[base]{mean}}.} +\item{dodge}{Adjustment parameter for dodging overlapping points or ranges in +grouped plots along the x-axis (or y-axis for flipped plots). Either: +\itemize{ +\item numeric value in the range \verb{[0,1)}. Note that values are scaled +relative to the spacing of x-axis breaks, e.g. \code{dodge = 0.1} places the +outermost groups one-tenth of the way to adjacent breaks, \code{dodge = 0.5} +places them midway between breaks, etc. Values < 0.5 are recommended. +\item logical. If \code{TRUE}, the dodge width is calculated automatically based on +the number of groups (0.1 per group for 2-4 groups, 0.45 for 5+ groups). If +\code{FALSE} or 0, no dodging is performed. +} + +Default value is 0 (no dodging). While we do not check, it is \emph{strongly} +recommended that dodging only be used in cases where the x-axis comprises a +limited number of discrete breaks.} + +\item{fixed.dodge}{Logical. If \code{FALSE} (default), dodge positions are +calculated independently for each \code{x} value, based only on the groups +present at that position. If \code{TRUE}, dodge positions are based on all +groups, ensuring "fixed" spacing across x-axis breaks (i.e., even if some +groups are missing for a particular \code{x} value).} + \item{...}{Additional arguments are passed to the \code{lines()} function, ex: \code{type="p"}, \code{col="pink"}.} } From a1c0005d91038d2dc863ae2598d95076513b5bc5 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 30 Aug 2026 19:43:19 -0700 Subject: [PATCH 3/6] news --- NEWS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NEWS.md b/NEWS.md index 53e3b94c4..201df9d8b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -131,6 +131,11 @@ related to plot layering. See "Bug fixes" below. `"cat"` (console), in any combination; a destination the user has already labelled is left alone. Shared bandwidths are reported once and named as joint, individual bandwidths per group. (#287 @haomeng797-ship-it) +- `type_summary()` gains a `dodge` (and `fixed.dodge`) argument, thus enabling + dodging of grouped plots. This is mostly useful for adding summaries on top of + a base layer that is itself dodged. Separately, `type_summary()`'s internals + have been refactored to use `stats::aggregate` instead of `stats::ave`. + (#701 @grantmcdermott) - Custom plot types have more control over the surrounding plot machinery, via a new `type_hints` mechanism. A type can declare properties about itself---that it draws its own axes, needs a secondary right-hand axis, uses proportional From bf1516b5e9ea6ffefb7a89bbb38240cf3147ee50 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 30 Aug 2026 20:21:32 -0700 Subject: [PATCH 4/6] factor gotcha --- R/dodge.R | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/R/dodge.R b/R/dodge.R index 3a4242b82..9d061cb76 100644 --- a/R/dodge.R +++ b/R/dodge.R @@ -54,6 +54,15 @@ dodge_positions = function( if (is.logical(dodge)) { if (isTRUE(dodge)) { + if (!is.factor(datapoints[["by"]])) { + msg = paste0( + "`dodge = TRUE` only possible with discrete (categorical) `by`. ", + "Either specify numeric [0,1] dodge, or coerce ` by` to a factor.\n", + "Ignoring.\n" + ) + warning(msg) + return(datapoints) + } n = nlevels(datapoints$by) dodge = if (n == 1) 0 else if (n <= 5) (n - 1) * 0.1 else 0.45 } else { From 4bfd2b26685fbba127fe51d3d22bf51d0cb1e67f Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 30 Aug 2026 20:22:36 -0700 Subject: [PATCH 5/6] simplify (don't need y as factor) --- R/type_summary.R | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/R/type_summary.R b/R/type_summary.R index 3e2d05e6f..797b7889c 100644 --- a/R/type_summary.R +++ b/R/type_summary.R @@ -47,25 +47,15 @@ type_summary = function(fun = mean, dodge = 0, fixed.dodge = FALSE, ...) { datapoints[["rowid"]] = NULL datapoints = aggregate(. ~ x + facet + by, data = datapoints, FUN = fun) if (dodge != 0) { - if (is.factor(datapoints[["x"]])) { - xlvls = levels(datapoints[["x"]]) - xlabs = seq_along(xlvls) - names(xlabs) = xlvls - datapoints[["x"]] = as.integer(datapoints[["x"]]) - env2env(environment(), settings, "xlabs") - } 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"]]) - env2env(environment(), settings, "ylabs") - } else { - ylabs = NULL - } - + if (is.factor(datapoints[["x"]])) { + xlvls = levels(datapoints[["x"]]) + xlabs = seq_along(xlvls) + names(xlabs) = xlvls + datapoints[["x"]] = as.integer(datapoints[["x"]]) + env2env(environment(), settings, "xlabs") + } else { + xlabs = NULL + } datapoints = dodge_positions(datapoints, dodge, fixed.dodge) } env2env(environment(), settings, "datapoints") From e70b2cb7ac2250caac4c1e746ed0eddf72a1a9cb Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 30 Aug 2026 20:22:42 -0700 Subject: [PATCH 6/6] test --- inst/tinytest/_tinysnapshot/summary_dodge.svg | 84 +++++++++++++++++++ inst/tinytest/test-type_summary.R | 14 ++++ 2 files changed, 98 insertions(+) create mode 100644 inst/tinytest/_tinysnapshot/summary_dodge.svg create mode 100644 inst/tinytest/test-type_summary.R diff --git a/inst/tinytest/_tinysnapshot/summary_dodge.svg b/inst/tinytest/_tinysnapshot/summary_dodge.svg new file mode 100644 index 000000000..94658a908 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/summary_dodge.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + +supp +OJ +VC + + + + + + + +dodged summary +dose +len + + + + + + + +0.5 +1.0 +1.5 +2.0 + + + + + +10 +15 +20 +25 + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-type_summary.R b/inst/tinytest/test-type_summary.R new file mode 100644 index 000000000..6912c5e57 --- /dev/null +++ b/inst/tinytest/test-type_summary.R @@ -0,0 +1,14 @@ +source("helpers.R") +using("tinysnapshot") + +# +## dodging + +f = function() { + tinyplot( + len ~ dose | supp, data = ToothGrowth, + type = type_summary(type = "b", dodge = TRUE), + main = "dodged summary" + ) +} +expect_snapshot_plot(f, label = "summary_dodge") \ No newline at end of file