diff --git a/NEWS.md b/NEWS.md
index d31d6a82..56a92978 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -8,20 +8,29 @@ where the formatting is also better._
### Breaking change
-- Internal settings and parameters are now stored in an environment called
- `settings`, which can be accessed and modified by type-specific functions.
- This may require changes to users' custom type functions that previously
- accessed settings as passed arguments. This change was necessary to improve
- the modularity and maintainability of the codebase, and also to add downstream
- flexibility. (#473 @vincentarelbundock and @grantmcdermott)
+- "Breaking" change for internal development and custom types only:
+ The plot settings and parameters from individual `tinyplot` calls are now
+ stored in a dedicated (temporary) internal environment called `settings`,
+ which can be accessed and modified by type-specific functions. This change
+ will enable various internal enhancements, from improving the modularity and
+ maintainability of the `tinyplot` codebase, to reducing memory overhead and
+ performance (since we require fewer object copies). Looking ahead, we also
+ expect that it will make it easier to support new features and integration
+ with downstream packages. Most `tinyplot` users should be unaffected by these
+ internal changes. However, users who have defined their own custom types will
+ need to make some adjustments to match the new `settings` logic; details are
+ provided in the updated `Types` vignette. (#473 @vincentarelbundock and @grantmcdermott)
### New features
- `type_text()` gains a `family` argument for controlling the font family,
separate to the main plot text elements. (#494 @grantmcdermott)
-- `type_ribbon()` gains a `dodge` argument, supporting similar functionality to
- `type_errorbar()` and `type_pointrange()` for dodging overlapping groups.
- (#522 @grantmcdermott)
+- Expanded `dodge` argument capabilities and consistency for overlapping groups:
+ - Logical `dodge = TRUE` gives automatic width spacing based on the number
+ of groups. (#525 @grantmcdermott)
+ - We now enforce that numeric `dodge` values must be in the range `[0,1)`.
+ (#526 @grantmcdermott)
+ - `dodge` argument now supported in `type_ribbon()` (#522 @grantmcdermott)
### Bug fixes
@@ -45,8 +54,8 @@ where the formatting is also better._
(e.g., `tinyplot.foo`) would fail. (#515 @grantmcdermott)
- Added layers, particularly from `tinyplot_add()`, should now respect the
x-axis order of the original plot layer. This should ensure that we don't end
- up with misaligned layers. For example, when adding a ribbon on top of an
- errorbar plot. (#517, #520, #523 @grantmcdermott)
+ up with misaligned layers. For example, when ribbon is added on top of an
+ errorbar plot. (#517, #520, #523, #526 @grantmcdermott)
### Documentation
diff --git a/R/align_layer.R b/R/align_layer.R
index 4468312d..51b4d72a 100644
--- a/R/align_layer.R
+++ b/R/align_layer.R
@@ -1,7 +1,6 @@
# Ensure added layers respect the x-axis order of the original plot layer
# (e.g., when adding lines or ribbons on top of errorbars)
align_layer = function(settings) {
-
# Retrieve xlabs from current and original layers
xlabs_layer = settings[["xlabs"]]
xlabs_orig = get("xlabs", envir = get(".tinyplot_env", envir = parent.env(environment())))
@@ -22,15 +21,23 @@ align_layer = function(settings) {
if (setequal(names(xlabs_layer), names(xlabs_orig))) {
orig_order = xlabs_orig[names(xlabs_layer)[settings$datapoints[["x"]]]]
x_layer = settings$datapoints[["x"]]
- settings$datapoints[["x"]] = orig_order
+ if (is.null(settings$dodge)) {
+ x_new = x_layer[orig_order]
+ } else {
+ names(x_layer) = names(xlabs_layer)[round(x_layer)]
+ x_new = x_layer + (xlabs_orig[names(round(x_layer))] - round(x_layer))
+ }
+ settings$datapoints[["x"]] = x_new
# Adjust ancillary variables
- for (v in c("rowid", "xmin", "xmax")) {
- if (identical(settings$datapoints[[v]], x_layer)) {
- settings$datapoints[[v]] = orig_order
+ for (v in c("xmin", "xmax")) {
+ if (identical(settings$datapoints[[v]], unname(x_layer))) {
+ settings$datapoints[[v]] = x_new
}
}
settings$datapoints = settings$datapoints[order(settings$datapoints[["x"]]), ]
+ settings$datapoints[["rowid"]] = seq_len(nrow(settings$datapoints))
}
}
}
}
+
diff --git a/R/dodge.R b/R/dodge.R
index ed8e9bd9..51ab2735 100644
--- a/R/dodge.R
+++ b/R/dodge.R
@@ -5,13 +5,22 @@
#'
#' @param datapoints Data frame containing plot data with at least `x` and `by`
#' columns.
-#' @param dodge Numeric value specifying the dodge amount. If 0, no dodging is
-#' performed.
-#' @param fixed.pos Logical. If `TRUE`, dodge positions are fixed based on the
-#' number of groups in `by`. If `FALSE`, dodge positions are calculated
-#' separately for each unique x value.
+#' @param dodge Numeric value in the range `[0,1)`, or logical. If numeric,
+#' values are scaled relative to x-axis break spacing (e.g., `dodge = 0.1`
+#' places outermost groups one-tenth of the way to adjacent breaks;
+#' `dodge = 0.5` places them midway between breaks; etc.). Values < 0.5 are
+#' recommended. If `TRUE`, 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
+#' `FALSE` or 0, no dodging is performed. Default is 0.
+#' @param fixed.pos Logical indicating whether dodged groups should retain a
+#' fixed relative position based on their group value. Relevant for `x`
+#' categories that only have a subset of the total number of groups. Defaults
+#' to `FALSE`, in which case dodging is based on the number of unique groups
+#' present in that `x` category alone. See Examples.
#' @param cols Character vector of column names to dodge. If `NULL` (default),
#' automatically detects and dodges `x`, `xmin`, and `xmax` if they exist.
+#' @param settings Environment containing plot settings. If `NULL` (default),
+#' retrieved from the calling environment.
#'
#' @return Modified `datapoints` data frame with dodged positions.
#'
@@ -29,11 +38,37 @@ dodge_positions = function(
datapoints,
dodge,
fixed.pos = TRUE,
- cols = NULL
+ cols = NULL,
+ settings = NULL
) {
+ if (is.null(settings)) {
+ settings = get("settings", envir = parent.frame())
+ }
+
+ if (is.logical(dodge)) {
+ if (isTRUE(dodge)) {
+ n = nlevels(datapoints$by)
+ dodge = if (n == 1) 0 else if (n <= 5) (n - 1) * 0.1 else 0.45
+ } else {
+ dodge = 0
+ }
+ }
+
+ assert_numeric(dodge, len = 1, lower = 0, upper = 1)
+ if (dodge >= 1) {
+ stop("`dodge` must be in the range [0,1).", call. = FALSE)
+ }
+ assert_logical(fixed.pos)
+
if (dodge == 0) {
return(datapoints)
+ } else if (dodge > 0.5) {
+ warning(
+ "Argument `dodge = ", dodge, "` exceeds 0.5. ",
+ "Large dodge values may position outer groups closer to neighboring axis breaks."
+ )
}
+ settings$dodge = dodge
# Auto-detect columns to dodge if not specified
if (is.null(cols)) {
@@ -64,3 +99,4 @@ dodge_positions = function(
datapoints
}
+
diff --git a/R/tinyplot.R b/R/tinyplot.R
index eb977f06..8d5c1904 100644
--- a/R/tinyplot.R
+++ b/R/tinyplot.R
@@ -795,6 +795,7 @@ tinyplot.default = function(
# misc
flip = flip,
+ dodge = NULL,
by = by,
dots = dots,
type_info = list() # pass type-specific info from type_data to type_draw
diff --git a/R/type_errorbar.R b/R/type_errorbar.R
index 8eac8c2d..d325acb9 100644
--- a/R/type_errorbar.R
+++ b/R/type_errorbar.R
@@ -2,16 +2,7 @@
#'
#' @description Type function(s) for producing error bar and pointrange plots.
#'
-#' @param dodge Numeric value (>= 0) for dodging of overlapping `by` groups.
-#' Dodging is scaled relative to the x-axis tick locations (i.e., unique
-#' levels of `x`). For example, `dodge = 0.5` would place the outermost dodged
-#' groups exactly midway the between axis ticks. Default value is 0 (no
-#' dodging).
-#' @param fixed.pos Logical indicating whether dodged groups should retain a
-#' fixed relative position based on their group value. Relevant for `x`
-#' categories that only have a subset of the total number of groups. Defaults
-#' to `FALSE`, in which case dodging is based on the number of unique groups
-#' present in that `x` category alone. See Examples.
+#' @inheritParams dodge_positions
#' @inheritParams graphics::arrows
#' @examples
#' mod = lm(mpg ~ wt * factor(am), mtcars)
diff --git a/R/type_pointrange.R b/R/type_pointrange.R
index 42615a18..9cefe803 100644
--- a/R/type_pointrange.R
+++ b/R/type_pointrange.R
@@ -1,9 +1,6 @@
#' @rdname type_errorbar
#' @export
type_pointrange = function(dodge = 0, fixed.pos = FALSE) {
- assert_numeric(dodge, len = 1, lower = 0)
- assert_logical(fixed.pos)
-
out = list(
draw = draw_pointrange(),
data = data_pointrange(dodge = dodge, fixed.pos = fixed.pos),
diff --git a/R/type_ribbon.R b/R/type_ribbon.R
index 6c19892e..8851e2af 100644
--- a/R/type_ribbon.R
+++ b/R/type_ribbon.R
@@ -79,9 +79,6 @@
#'
#' @export
type_ribbon = function(alpha = NULL, dodge = 0, fixed.pos = FALSE) {
- assert_numeric(dodge, len = 1, lower = 0)
- assert_logical(fixed.pos)
-
out = list(
draw = draw_ribbon(),
data = data_ribbon(ribbon.alpha = alpha, dodge = dodge, fixed.pos = fixed.pos),
diff --git a/inst/tinytest/_tinysnapshot/dodge_errorbar_add_ribbon.svg b/inst/tinytest/_tinysnapshot/dodge_errorbar_add_ribbon.svg
new file mode 100644
index 00000000..d72f004a
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/dodge_errorbar_add_ribbon.svg
@@ -0,0 +1,131 @@
+
+
diff --git a/inst/tinytest/_tinysnapshot/dodge_pointrange.svg b/inst/tinytest/_tinysnapshot/dodge_pointrange.svg
new file mode 100644
index 00000000..fd930a5c
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/dodge_pointrange.svg
@@ -0,0 +1,107 @@
+
+
diff --git a/inst/tinytest/_tinysnapshot/pointrange_with_layers_grouped.svg b/inst/tinytest/_tinysnapshot/dodge_pointrange_false.svg
similarity index 72%
rename from inst/tinytest/_tinysnapshot/pointrange_with_layers_grouped.svg
rename to inst/tinytest/_tinysnapshot/dodge_pointrange_false.svg
index 0b41a534..c177c2d4 100644
--- a/inst/tinytest/_tinysnapshot/pointrange_with_layers_grouped.svg
+++ b/inst/tinytest/_tinysnapshot/dodge_pointrange_false.svg
@@ -26,13 +26,13 @@
-
-
-
-model
-Model A
-Model B
-Model C
+
+
+
+model
+Model A
+Model B
+Model C
@@ -72,27 +72,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
diff --git a/inst/tinytest/_tinysnapshot/pointrange_dodge_01.svg b/inst/tinytest/_tinysnapshot/dodge_pointrange_flip.svg
similarity index 56%
rename from inst/tinytest/_tinysnapshot/pointrange_dodge_01.svg
rename to inst/tinytest/_tinysnapshot/dodge_pointrange_flip.svg
index 45f46709..9f4b7efa 100644
--- a/inst/tinytest/_tinysnapshot/pointrange_dodge_01.svg
+++ b/inst/tinytest/_tinysnapshot/dodge_pointrange_flip.svg
@@ -26,13 +26,13 @@
-
-
-
-model
-Model A
-Model B
-Model C
+
+
+
+model
+Model A
+Model B
+Model C
@@ -55,14 +55,14 @@
203040
-
-
-
-
+
+
+
+
-(Intercept)
-wt
-cyl
+(Intercept)
+wt
+cylhp
@@ -72,24 +72,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/inst/tinytest/_tinysnapshot/dodge_pointrange_true.svg b/inst/tinytest/_tinysnapshot/dodge_pointrange_true.svg
new file mode 100644
index 00000000..52a11a5f
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/dodge_pointrange_true.svg
@@ -0,0 +1,107 @@
+
+
diff --git a/inst/tinytest/test-dodge.R b/inst/tinytest/test-dodge.R
new file mode 100644
index 00000000..433f97a3
--- /dev/null
+++ b/inst/tinytest/test-dodge.R
@@ -0,0 +1,108 @@
+source("helpers.R")
+using("tinysnapshot")
+
+# Test data
+models = list(
+ "Model A" = lm(mpg ~ wt + cyl, data = mtcars),
+ "Model B" = lm(mpg ~ wt + hp + cyl, data = mtcars),
+ "Model C" = lm(mpg ~ wt, data = mtcars)
+)
+results = lapply(names(models), function(m) {
+ data.frame(
+ model = m,
+ term = names(coef(models[[m]])),
+ estimate = coef(models[[m]]),
+ setNames(data.frame(confint(models[[m]])), c("conf.low", "conf.high"))
+ )
+})
+results = do.call(rbind, results)
+
+
+#
+## Basic examples
+
+# Pointrange dodge with numeric value
+fun = function() {
+ tinyplot(estimate ~ term | model,
+ ymin = conf.low, ymax = conf.high,
+ data = results,
+ type = "pointrange",
+ dodge = 0.1,
+ theme = "basic")
+ # tinyplot_add(type = "pointrange")
+}
+expect_snapshot_plot(fun, label = "dodge_pointrange")
+
+# As above, but flipped
+fun = function() {
+ tinyplot(estimate ~ term | model,
+ ymin = conf.low, ymax = conf.high,
+ data = results,
+ type = "pointrange",
+ dodge = 0.1,
+ flip = TRUE,
+ theme = "basic")
+}
+expect_snapshot_plot(fun, label = "dodge_pointrange_flip")
+
+# Pointrange dodge with logical TRUE (automatic calculation)
+fun = function() {
+ tinyplot(estimate ~ term | model,
+ ymin = conf.low, ymax = conf.high,
+ data = results,
+ type = "pointrange",
+ dodge = TRUE,
+ theme = "basic")
+}
+expect_snapshot_plot(fun, label = "dodge_pointrange_true")
+
+# Pointrange dodge with logical FALSE (no dodging)
+fun = function() {
+ tinyplot(estimate ~ term | model,
+ ymin = conf.low, ymax = conf.high,
+ data = results,
+ type = "pointrange",
+ dodge = FALSE,
+ theme = "basic")
+}
+expect_snapshot_plot(fun, label = "dodge_pointrange_false")
+
+
+#
+## Dodge + layering
+
+fun = function() {
+ tinyplot(estimate ~ term | model,
+ ymin = conf.low, ymax = conf.high,
+ data = results,
+ type = "errorbar",
+ dodge = 0.1,
+ theme = "basic")
+ tinyplot_add(type = "ribbon")
+}
+expect_snapshot_plot(fun, label = "dodge_errorbar_add_ribbon")
+
+
+#
+## Warning and errors
+
+# Warning when dodge > 0.5
+expect_warning(
+ tinyplot(estimate ~ term | model,
+ ymin = conf.low, ymax = conf.high,
+ data = results,
+ type = type_pointrange(dodge = 0.6),
+ theme = "basic"),
+ pattern = "exceeds 0.5"
+)
+
+# Error when dodge >= 1
+expect_error(
+ tinyplot(estimate ~ term | model,
+ ymin = conf.low, ymax = conf.high,
+ data = results,
+ type = type_pointrange(dodge = 1)),
+ pattern = "must be in the range"
+)
+
+
diff --git a/inst/tinytest/test-type_pointrange.R b/inst/tinytest/test-type_pointrange.R
index d2f14477..ae87a228 100644
--- a/inst/tinytest/test-type_pointrange.R
+++ b/inst/tinytest/test-type_pointrange.R
@@ -71,38 +71,3 @@ fun = function() {
tinyplot_add(type = "hline", lty = 2)
}
expect_snapshot_plot(fun, label = "pointrange_with_layers_flipped")
-
-# Issue #406: dodge pointrange and errorbar
-models = list(
- "Model A" = lm(mpg ~ wt + cyl, data = mtcars),
- "Model B" = lm(mpg ~ wt + hp + cyl, data = mtcars),
- "Model C" = lm(mpg ~ wt, data = mtcars)
-)
-results = lapply(names(models), function(m) {
- data.frame(
- model = m,
- term = names(coef(models[[m]])),
- estimate = coef(models[[m]]),
- setNames(data.frame(confint(models[[m]])), c("conf.low", "conf.high"))
- )
-})
-results = do.call(rbind, results)
-fun = function() {
- tinyplot(estimate ~ term | model,
- ymin = conf.low, ymax = conf.high,
- flip = TRUE, data = results,
- type = type_pointrange(dodge = 0.2))
-}
-expect_snapshot_plot(fun, label = "pointrange_dodge_01")
-
-# issue #519 layer on top of grouped plots
-# (don't care about dodge yet; revist when #493 resolved)
-
-fun = function() {
- tinyplot(estimate ~ term | model,
- ymin = conf.low, ymax = conf.high,
- data = results,
- type = type_pointrange())
- tinyplot_add(type = 'l')
-}
-expect_snapshot_plot(fun, label = "pointrange_with_layers_grouped")
\ No newline at end of file
diff --git a/man/dodge_positions.Rd b/man/dodge_positions.Rd
index 525f0f9b..ff3df05f 100644
--- a/man/dodge_positions.Rd
+++ b/man/dodge_positions.Rd
@@ -4,21 +4,37 @@
\alias{dodge_positions}
\title{Dodge positions for grouped data}
\usage{
-dodge_positions(datapoints, dodge, fixed.pos = TRUE, cols = NULL)
+dodge_positions(
+ datapoints,
+ dodge,
+ fixed.pos = TRUE,
+ cols = NULL,
+ settings = NULL
+)
}
\arguments{
\item{datapoints}{Data frame containing plot data with at least \code{x} and \code{by}
columns.}
-\item{dodge}{Numeric value specifying the dodge amount. If 0, no dodging is
-performed.}
+\item{dodge}{Numeric value in the range \verb{[0,1)}, or logical. If numeric,
+values are scaled relative to x-axis break spacing (e.g., \code{dodge = 0.1}
+places 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. If \code{TRUE}, 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 is 0.}
-\item{fixed.pos}{Logical. If \code{TRUE}, dodge positions are fixed based on the
-number of groups in \code{by}. If \code{FALSE}, dodge positions are calculated
-separately for each unique x value.}
+\item{fixed.pos}{Logical indicating whether dodged groups should retain a
+fixed relative position based on their group value. Relevant for \code{x}
+categories that only have a subset of the total number of groups. Defaults
+to \code{FALSE}, in which case dodging is based on the number of unique groups
+present in that \code{x} category alone. See Examples.}
\item{cols}{Character vector of column names to dodge. If \code{NULL} (default),
automatically detects and dodges \code{x}, \code{xmin}, and \code{xmax} if they exist.}
+
+\item{settings}{Environment containing plot settings. If \code{NULL} (default),
+retrieved from the calling environment.}
}
\value{
Modified \code{datapoints} data frame with dodged positions.
diff --git a/man/type_errorbar.Rd b/man/type_errorbar.Rd
index d5b6ac59..fe3d8932 100644
--- a/man/type_errorbar.Rd
+++ b/man/type_errorbar.Rd
@@ -12,11 +12,13 @@ type_pointrange(dodge = 0, fixed.pos = FALSE)
\arguments{
\item{length}{length of the edges of the arrow head (in inches).}
-\item{dodge}{Numeric value (>= 0) for dodging of overlapping \code{by} groups.
-Dodging is scaled relative to the x-axis tick locations (i.e., unique
-levels of \code{x}). For example, \code{dodge = 0.5} would place the outermost dodged
-groups exactly midway the between axis ticks. Default value is 0 (no
-dodging).}
+\item{dodge}{Numeric value in the range \verb{[0,1)}, or logical. If numeric,
+values are scaled relative to x-axis break spacing (e.g., \code{dodge = 0.1}
+places 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. If \code{TRUE}, 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 is 0.}
\item{fixed.pos}{Logical indicating whether dodged groups should retain a
fixed relative position based on their group value. Relevant for \code{x}
diff --git a/man/type_ribbon.Rd b/man/type_ribbon.Rd
index 417e62fc..b2918dbb 100644
--- a/man/type_ribbon.Rd
+++ b/man/type_ribbon.Rd
@@ -15,11 +15,13 @@ If no \code{alpha} value is provided, then will default to \code{tpar("ribbon.al
(i.e., probably \code{0.2} unless this has been overridden by the user in their global
settings.)}
-\item{dodge}{Numeric value (>= 0) for dodging of overlapping \code{by} groups.
-Dodging is scaled relative to the x-axis tick locations (i.e., unique
-levels of \code{x}). For example, \code{dodge = 0.5} would place the outermost dodged
-groups exactly midway the between axis ticks. Default value is 0 (no
-dodging).}
+\item{dodge}{Numeric value in the range \verb{[0,1)}, or logical. If numeric,
+values are scaled relative to x-axis break spacing (e.g., \code{dodge = 0.1}
+places 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. If \code{TRUE}, 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 is 0.}
\item{fixed.pos}{Logical indicating whether dodged groups should retain a
fixed relative position based on their group value. Relevant for \code{x}