diff --git a/NEWS.md b/NEWS.md index b6321b90bb..749a360908 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # ggplot2 (development version) +* `geom_point()` can be dodged vertically by using + `position_dodge(..., orientation = "y")` (@teunbrand, #5809). * Fixed bug where `na.value` was incorrectly mapped to non-`NA` values (@teunbrand, #5756). * Fixed bug in `guide_custom()` that would throw error with `theme_void()` diff --git a/R/position-dodge.R b/R/position-dodge.R index ef24531207..17bd607324 100644 --- a/R/position-dodge.R +++ b/R/position-dodge.R @@ -13,6 +13,9 @@ #' geoms. See the examples. #' @param preserve Should dodging preserve the `"total"` width of all elements #' at a position, or the width of a `"single"` element? +#' @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"`. #' @family position adjustments #' @export #' @examples @@ -79,10 +82,11 @@ #' #' ggplot(mtcars, aes(factor(cyl), fill = factor(vs))) + #' geom_bar(position = position_dodge2(preserve = "total")) -position_dodge <- function(width = NULL, preserve = "total") { +position_dodge <- function(width = NULL, preserve = "total", orientation = "x") { ggproto(NULL, PositionDodge, width = width, - preserve = arg_match0(preserve, c("total", "single")) + preserve = arg_match0(preserve, c("total", "single")), + orientation = arg_match0(orientation, c("x", "y")) ) } @@ -93,8 +97,9 @@ position_dodge <- function(width = NULL, preserve = "total") { PositionDodge <- ggproto("PositionDodge", Position, width = NULL, preserve = "total", + orientation = "x", setup_params = function(self, data) { - flipped_aes <- has_flipped_aes(data) + flipped_aes <- has_flipped_aes(data, default = self$orientation == "y") data <- flip_data(data, flipped_aes) if (is.null(data$xmin) && is.null(data$xmax) && is.null(self$width)) { cli::cli_warn(c( diff --git a/R/utilities.R b/R/utilities.R index 9f9133a0b5..1a9181be69 100644 --- a/R/utilities.R +++ b/R/utilities.R @@ -476,6 +476,8 @@ switch_orientation <- function(aesthetics) { #' @param main_is_optional Is the main axis aesthetic optional and, if not #' given, set to `0` #' @param flip Logical. Is the layer flipped. +#' @param default The logical value to return if no orientation can be discerned +#' from the data. #' #' @return `has_flipped_aes()` returns `TRUE` if it detects a layer in the other #' orientation and `FALSE` otherwise. `flip_data()` will return the input @@ -492,7 +494,7 @@ switch_orientation <- function(aesthetics) { has_flipped_aes <- function(data, params = list(), main_is_orthogonal = NA, range_is_orthogonal = NA, group_has_equal = FALSE, ambiguous = FALSE, main_is_continuous = FALSE, - main_is_optional = FALSE) { + main_is_optional = FALSE, default = FALSE) { # Is orientation already encoded in data? if (!is.null(data$flipped_aes)) { not_na <- which(!is.na(data$flipped_aes)) @@ -561,8 +563,7 @@ has_flipped_aes <- function(data, params = list(), main_is_orthogonal = NA, } } - # default to no - FALSE + isTRUE(default) } #' @rdname bidirection #' @export diff --git a/man/bidirection.Rd b/man/bidirection.Rd index f58460091c..be6ffec336 100644 --- a/man/bidirection.Rd +++ b/man/bidirection.Rd @@ -15,7 +15,8 @@ has_flipped_aes( group_has_equal = FALSE, ambiguous = FALSE, main_is_continuous = FALSE, - main_is_optional = FALSE + main_is_optional = FALSE, + default = FALSE ) flip_data(data, flip = NULL) @@ -48,6 +49,9 @@ the continuous one correspond to the main orientation?} \item{main_is_optional}{Is the main axis aesthetic optional and, if not given, set to \code{0}} +\item{default}{The logical value to return if no orientation can be discerned +from the data.} + \item{flip}{Logical. Is the layer flipped.} } \value{ diff --git a/man/position_dodge.Rd b/man/position_dodge.Rd index b42353b35e..3efb462168 100644 --- a/man/position_dodge.Rd +++ b/man/position_dodge.Rd @@ -5,7 +5,7 @@ \alias{position_dodge2} \title{Dodge overlapping objects side-to-side} \usage{ -position_dodge(width = NULL, preserve = "total") +position_dodge(width = NULL, preserve = "total", orientation = "x") position_dodge2( width = NULL, @@ -22,6 +22,10 @@ geoms. See the examples.} \item{preserve}{Should dodging preserve the \code{"total"} width of all elements at a position, or the width of a \code{"single"} element?} +\item{orientation}{Fallback orientation when the layer or the data does not +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.} diff --git a/tests/testthat/test-position_dodge.R b/tests/testthat/test-position_dodge.R index 4d540176ad..aa036c7abe 100644 --- a/tests/testthat/test-position_dodge.R +++ b/tests/testthat/test-position_dodge.R @@ -9,3 +9,17 @@ test_that("can control whether to preserve total or individual width", { expect_equal(layer_data(p_total)$x, new_mapped_discrete(c(1, 1.75, 2.25))) expect_equal(layer_data(p_single)$x, new_mapped_discrete(c(0.75, 1.75, 2.25))) }) + +test_that("position_dodge() can dodge points vertically", { + + df <- data.frame(x = c(1, 2, 3, 4), y = c("a", "a", "b", "b")) + + horizontal <- ggplot(df, aes(y, x, group = seq_along(x))) + + geom_point(position = position_dodge(width = 1, orientation = "x")) + vertical <- ggplot(df, aes(x, y, group = seq_along(x))) + + geom_point(position = position_dodge(width = 1, orientation = "y")) + + expect_equal(layer_data(horizontal)$x, c(0.75, 1.25, 1.75, 2.25), ignore_attr = "class") + expect_equal(layer_data(vertical)$y, c(0.75, 1.25, 1.75, 2.25), ignore_attr = "class") + +})