From c2229c1829233860886502b18473ea7f15186da6 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Fri, 31 May 2024 14:16:13 +0200 Subject: [PATCH 1/7] `reverse` plumbing for `position_dodge() --- R/position-dodge.R | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/R/position-dodge.R b/R/position-dodge.R index 17bd607324..4baf10ad27 100644 --- a/R/position-dodge.R +++ b/R/position-dodge.R @@ -82,11 +82,14 @@ #' #' ggplot(mtcars, aes(factor(cyl), fill = factor(vs))) + #' geom_bar(position = position_dodge2(preserve = "total")) -position_dodge <- function(width = NULL, preserve = "total", orientation = "x") { +position_dodge <- function(width = NULL, preserve = "total", orientation = "x", + reverse = TRUE) { + check_bool(reverse) ggproto(NULL, PositionDodge, width = width, preserve = arg_match0(preserve, c("total", "single")), - orientation = arg_match0(orientation, c("x", "y")) + orientation = arg_match0(orientation, c("x", "y")), + reverse = reverse ) } @@ -98,6 +101,7 @@ PositionDodge <- ggproto("PositionDodge", Position, width = NULL, preserve = "total", orientation = "x", + reverse = NULL, setup_params = function(self, data) { flipped_aes <- has_flipped_aes(data, default = self$orientation == "y") data <- flip_data(data, flipped_aes) @@ -119,7 +123,8 @@ PositionDodge <- ggproto("PositionDodge", Position, list( width = self$width, n = n, - flipped_aes = flipped_aes + flipped_aes = flipped_aes, + reverse = self$reverse %||% TRUE ) }, @@ -139,7 +144,8 @@ PositionDodge <- ggproto("PositionDodge", Position, name = "position_dodge", strategy = pos_dodge, n = params$n, - check.width = FALSE + check.width = FALSE, + reverse = params$reverse ) flip_data(collided, params$flipped_aes) } From e0976b34f35b6ad78fd3dc7a396723d3fcccb740 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Fri, 31 May 2024 14:16:27 +0200 Subject: [PATCH 2/7] `reverse` plumbing in `position_jitterdodge()` --- R/position-jitterdodge.R | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/R/position-jitterdodge.R b/R/position-jitterdodge.R index fba28a47fa..1f9b4a7192 100644 --- a/R/position-jitterdodge.R +++ b/R/position-jitterdodge.R @@ -11,6 +11,7 @@ #' @param dodge.width the amount to dodge in the x direction. Defaults to 0.75, #' the default `position_dodge()` width. #' @inheritParams position_jitter +#' @inheritParams position_dodge #' @export #' @examples #' set.seed(596) @@ -19,15 +20,18 @@ #' geom_boxplot(outlier.size = 0) + #' geom_point(pch = 21, position = position_jitterdodge()) position_jitterdodge <- function(jitter.width = NULL, jitter.height = 0, - dodge.width = 0.75, seed = NA) { + dodge.width = 0.75, reverse = TRUE, + seed = NA) { if (!is.null(seed) && is.na(seed)) { seed <- sample.int(.Machine$integer.max, 1L) } + check_bool(reverse) ggproto(NULL, PositionJitterdodge, jitter.width = jitter.width, jitter.height = jitter.height, dodge.width = dodge.width, + reverse = reverse, seed = seed ) } @@ -40,6 +44,7 @@ PositionJitterdodge <- ggproto("PositionJitterdodge", Position, jitter.width = NULL, jitter.height = NULL, dodge.width = NULL, + reverse = NULL, required_aes = c("x", "y"), @@ -64,14 +69,15 @@ PositionJitterdodge <- ggproto("PositionJitterdodge", Position, jitter.height = self$jitter.height, jitter.width = width / (ndodge + 2), seed = self$seed, - flipped_aes = flipped_aes + flipped_aes = flipped_aes, + reverse = self$reverse %||% TRUE ) }, compute_panel = function(data, params, scales) { data <- flip_data(data, params$flipped_aes) data <- collide(data, params$dodge.width, "position_jitterdodge", pos_dodge, - check.width = FALSE) + check.width = FALSE, reverse = params$reverse) trans_x <- if (params$jitter.width > 0) function(x) jitter(x, amount = params$jitter.width) trans_y <- if (params$jitter.height > 0) function(x) jitter(x, amount = params$jitter.height) From 6e61a363836fef9fdc9da30240142dfa44a596e9 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Fri, 31 May 2024 14:16:57 +0200 Subject: [PATCH 3/7] do not internally sort groups in `position_dodge()` --- R/position-dodge.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/position-dodge.R b/R/position-dodge.R index 4baf10ad27..7b28b2e6bd 100644 --- a/R/position-dodge.R +++ b/R/position-dodge.R @@ -170,7 +170,7 @@ pos_dodge <- function(df, width, n = NULL) { # Have a new group index from 1 to number of groups. # This might be needed if the group numbers in this set don't include all of 1:n - groupidx <- match(df$group, sort(unique0(df$group))) + groupidx <- match(df$group, unique0(df$group)) # Find the center for each group, then use that to calculate xmin and xmax df$x <- df$x + width * ((groupidx - 0.5) / n - .5) From ea5d9dab82421830f309ee4c7e1029abe3066791 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Fri, 31 May 2024 14:24:09 +0200 Subject: [PATCH 4/7] add test --- tests/testthat/test-position_dodge.R | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/testthat/test-position_dodge.R b/tests/testthat/test-position_dodge.R index 51d7b54cce..4aa66afb71 100644 --- a/tests/testthat/test-position_dodge.R +++ b/tests/testthat/test-position_dodge.R @@ -23,3 +23,17 @@ test_that("position_dodge() can dodge points vertically", { expect_equal(layer_data(vertical)$y, c(0.75, 1.25, 1.75, 2.25), ignore_attr = "class") }) + +test_that("position_dodge() can reverse the dodge order", { + + df <- data.frame(x = c(1, 2, 2, 3, 3), group = c("A", "A", "B", "B", "C")) + + # Use label as easy to track identifier + p <- ggplot(df, aes(x, y = 1, fill = group, label = group)) + + ld <- get_layer_data(p + geom_col(position = position_dodge(reverse = FALSE))) + expect_equal(ld$label[order(ld$x)], c("A", "B", "A", "C", "B")) + + ld <- get_layer_data(p + geom_col(position = position_dodge(reverse = TRUE))) + expect_equal(ld$label[order(ld$x)], c("A", "A", "B", "B", "C")) +}) From 93f88b02f5e5fc560ada339baa9a64afe8974aac Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Fri, 31 May 2024 14:24:53 +0200 Subject: [PATCH 5/7] add news bullet --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index de3e87cee3..ee368aea40 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # ggplot2 (development version) +* `position_dodge()` and `position_jitterdodge()` now have a `reverse` argument + (@teunbrand, #3610) * The `arrow.fill` parameter is now applied to more line-based functions: `geom_path()`, `geom_line()`, `geom_step()` `geom_function()`, line geometries in `geom_sf()` and `element_line()`. From 01243456b2ffa86d58e0c7a85e1942510e56ac72 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Fri, 31 May 2024 14:38:56 +0200 Subject: [PATCH 6/7] Align interpretation with `position_dodge2()` --- R/position-dodge.R | 6 +++--- R/position-jitterdodge.R | 14 ++++++++++---- tests/testthat/test-position_dodge.R | 4 ++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/R/position-dodge.R b/R/position-dodge.R index 7b28b2e6bd..84578c6b5d 100644 --- a/R/position-dodge.R +++ b/R/position-dodge.R @@ -83,7 +83,7 @@ #' ggplot(mtcars, aes(factor(cyl), fill = factor(vs))) + #' geom_bar(position = position_dodge2(preserve = "total")) position_dodge <- function(width = NULL, preserve = "total", orientation = "x", - reverse = TRUE) { + reverse = FALSE) { check_bool(reverse) ggproto(NULL, PositionDodge, width = width, @@ -124,7 +124,7 @@ PositionDodge <- ggproto("PositionDodge", Position, width = self$width, n = n, flipped_aes = flipped_aes, - reverse = self$reverse %||% TRUE + reverse = self$reverse %||% FALSE ) }, @@ -145,7 +145,7 @@ PositionDodge <- ggproto("PositionDodge", Position, strategy = pos_dodge, n = params$n, check.width = FALSE, - reverse = params$reverse + reverse = !params$reverse # for consistency with `position_dodge2()` ) flip_data(collided, params$flipped_aes) } diff --git a/R/position-jitterdodge.R b/R/position-jitterdodge.R index 1f9b4a7192..2641cdd443 100644 --- a/R/position-jitterdodge.R +++ b/R/position-jitterdodge.R @@ -20,7 +20,7 @@ #' geom_boxplot(outlier.size = 0) + #' geom_point(pch = 21, position = position_jitterdodge()) position_jitterdodge <- function(jitter.width = NULL, jitter.height = 0, - dodge.width = 0.75, reverse = TRUE, + dodge.width = 0.75, reverse = FALSE, seed = NA) { if (!is.null(seed) && is.na(seed)) { seed <- sample.int(.Machine$integer.max, 1L) @@ -70,14 +70,20 @@ PositionJitterdodge <- ggproto("PositionJitterdodge", Position, jitter.width = width / (ndodge + 2), seed = self$seed, flipped_aes = flipped_aes, - reverse = self$reverse %||% TRUE + reverse = self$reverse %||% FALSE ) }, compute_panel = function(data, params, scales) { data <- flip_data(data, params$flipped_aes) - data <- collide(data, params$dodge.width, "position_jitterdodge", pos_dodge, - check.width = FALSE, reverse = params$reverse) + data <- collide( + data, + params$dodge.width, + "position_jitterdodge", + strategy = pos_dodge, + check.width = FALSE, + reverse = !params$reverse # for consistency with `position_dodge2()` + ) trans_x <- if (params$jitter.width > 0) function(x) jitter(x, amount = params$jitter.width) trans_y <- if (params$jitter.height > 0) function(x) jitter(x, amount = params$jitter.height) diff --git a/tests/testthat/test-position_dodge.R b/tests/testthat/test-position_dodge.R index 4aa66afb71..1d7127a239 100644 --- a/tests/testthat/test-position_dodge.R +++ b/tests/testthat/test-position_dodge.R @@ -31,9 +31,9 @@ test_that("position_dodge() can reverse the dodge order", { # Use label as easy to track identifier p <- ggplot(df, aes(x, y = 1, fill = group, label = group)) - ld <- get_layer_data(p + geom_col(position = position_dodge(reverse = FALSE))) + ld <- get_layer_data(p + geom_col(position = position_dodge(reverse = TRUE))) expect_equal(ld$label[order(ld$x)], c("A", "B", "A", "C", "B")) - ld <- get_layer_data(p + geom_col(position = position_dodge(reverse = TRUE))) + ld <- get_layer_data(p + geom_col(position = position_dodge(reverse = FALSE))) expect_equal(ld$label[order(ld$x)], c("A", "A", "B", "B", "C")) }) From 05de68b07c4f848545cff13578aabb77dbe4c3e1 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Fri, 31 May 2024 15:06:05 +0200 Subject: [PATCH 7/7] document --- R/position-dodge.R | 2 ++ R/position-dodge2.R | 2 -- man/position_dodge.Rd | 13 +++++++++---- man/position_jitterdodge.Rd | 4 ++++ 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/R/position-dodge.R b/R/position-dodge.R index 84578c6b5d..4fcee78d70 100644 --- a/R/position-dodge.R +++ b/R/position-dodge.R @@ -16,6 +16,8 @@ #' @param orientation Fallback orientation when the layer or the data does not #' indicate an explicit orientation, like `geom_point()`. Can be `"x"` #' (default) or `"y"`. +#' @param reverse If `TRUE`, will reverse the default stacking order. +#' This is useful if you're rotating both the plot and legend. #' @family position adjustments #' @export #' @examples diff --git a/R/position-dodge2.R b/R/position-dodge2.R index a4c5fdc8ba..bf5f18c56d 100644 --- a/R/position-dodge2.R +++ b/R/position-dodge2.R @@ -2,8 +2,6 @@ #' @rdname position_dodge #' @param padding Padding between elements at the same position. Elements are #' shrunk by this proportion to allow space between them. Defaults to 0.1. -#' @param reverse If `TRUE`, will reverse the default stacking order. -#' This is useful if you're rotating both the plot and legend. position_dodge2 <- function(width = NULL, preserve = "total", padding = 0.1, reverse = FALSE) { ggproto(NULL, PositionDodge2, diff --git a/man/position_dodge.Rd b/man/position_dodge.Rd index 3efb462168..e4f9211110 100644 --- a/man/position_dodge.Rd +++ b/man/position_dodge.Rd @@ -5,7 +5,12 @@ \alias{position_dodge2} \title{Dodge overlapping objects side-to-side} \usage{ -position_dodge(width = NULL, preserve = "total", orientation = "x") +position_dodge( + width = NULL, + preserve = "total", + orientation = "x", + reverse = FALSE +) position_dodge2( width = NULL, @@ -26,11 +31,11 @@ at a position, or the width of a \code{"single"} element?} indicate an explicit orientation, like \code{geom_point()}. Can be \code{"x"} (default) or \code{"y"}.} -\item{padding}{Padding between elements at the same position. Elements are -shrunk by this proportion to allow space between them. Defaults to 0.1.} - \item{reverse}{If \code{TRUE}, will reverse the default stacking order. This is useful if you're rotating both the plot and legend.} + +\item{padding}{Padding between elements at the same position. Elements are +shrunk by this proportion to allow space between them. Defaults to 0.1.} } \description{ Dodging preserves the vertical position of an geom while adjusting the diff --git a/man/position_jitterdodge.Rd b/man/position_jitterdodge.Rd index d158162211..ca5bb8e30c 100644 --- a/man/position_jitterdodge.Rd +++ b/man/position_jitterdodge.Rd @@ -8,6 +8,7 @@ position_jitterdodge( jitter.width = NULL, jitter.height = 0, dodge.width = 0.75, + reverse = FALSE, seed = NA ) } @@ -20,6 +21,9 @@ resolution of the data.} \item{dodge.width}{the amount to dodge in the x direction. Defaults to 0.75, the default \code{position_dodge()} width.} +\item{reverse}{If \code{TRUE}, will reverse the default stacking order. +This is useful if you're rotating both the plot and legend.} + \item{seed}{A random seed to make the jitter reproducible. Useful if you need to apply the same jitter twice, e.g., for a point and a corresponding label.