diff --git a/NEWS.md b/NEWS.md index 53e3b94c..201df9d8 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 diff --git a/R/dodge.R b/R/dodge.R index 3a4242b8..9d061cb7 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 { diff --git a/R/type_summary.R b/R/type_summary.R index 059609f9..797b7889 100644 --- a/R/type_summary.R +++ b/R/type_summary.R @@ -4,11 +4,13 @@ #' @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}}. +#' @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,21 +38,26 @@ #' #' @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 = 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[["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 + } + datapoints = dodge_positions(datapoints, dodge, fixed.dodge) + } env2env(environment(), settings, "datapoints") } return(funky) diff --git a/inst/tinytest/_tinysnapshot/summary_dodge.svg b/inst/tinytest/_tinysnapshot/summary_dodge.svg new file mode 100644 index 00000000..94658a90 --- /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/_tinysnapshot/tinyplot_add_numeric_x_violin.svg b/inst/tinytest/_tinysnapshot/tinyplot_add_numeric_x_violin.svg index 6fda13b3..d958bae1 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/inst/tinytest/test-type_summary.R b/inst/tinytest/test-type_summary.R new file mode 100644 index 00000000..6912c5e5 --- /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 diff --git a/man/type_summary.Rd b/man/type_summary.Rd index 70e17e74..54ce4764 100644 --- a/man/type_summary.Rd +++ b/man/type_summary.Rd @@ -4,20 +4,42 @@ \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"}.} } \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