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 @@
+
+
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