Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# ggplot2 (development version)


* `position_jitterdodge()` now warns when dodge groups appear inflated by
additional discrete aesthetics beyond `fill`, with guidance to set
`aes(group = <fill variable>)` (@Jesssullivan, #6824).
* `make_constructor()` no longer captures `rlang::list2()` at build time.
* The `arrow` and `arrow.fill` arguments are now available in
`geom_linerange()` and `geom_pointrange()` layers (@teunbrand, #6481).
Expand Down
65 changes: 65 additions & 0 deletions R/position-jitterdodge.R
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,56 @@
#' the default `position_dodge()` width.
#' @inheritParams position_jitter
#' @inheritParams position_dodge
#'
#' @section Interaction with grouping:
#' When no explicit `group` aesthetic is set, ggplot2 computes groups from the
#' interaction of all discrete aesthetics in the layer (see [aes_group_order]).
#' If your point layer maps additional discrete aesthetics beyond the `fill`
#' used for dodging (e.g., `colour`, `shape`, or `linetype`), the points will
#' be split into more groups than the dodged boxplots, causing misalignment.
#'
#' To fix this, explicitly set `group` to the same variable used for dodging
#' (typically the `fill` variable):
#'
#' \preformatted{geom_point(aes(colour = status, group = fill_var),
#' position = position_jitterdodge())}
#'
#' @export
#' @examples
#' set.seed(596)
#' dsub <- diamonds[sample(nrow(diamonds), 1000), ]
#' ggplot(dsub, aes(x = cut, y = carat, fill = clarity)) +
#' geom_boxplot(outlier.size = 0) +
#' geom_point(pch = 21, position = position_jitterdodge())
#'
#' # When mapping additional discrete aesthetics (e.g. colour), points
#' # can misalign with boxes because the implicit groups are inflated.
#' # Fix by setting group to the fill variable:
#' \donttest{
#' set.seed(596)
#' df <- data.frame(
#' x = rep(c("A", "B"), each = 20),
#' y = rnorm(40),
#' fill_var = rep(c("g1", "g2"), 20),
#' colour_var = sample(c(TRUE, FALSE), 40, replace = TRUE)
#' )
#'
#' # Misaligned: colour creates extra implicit groups
#' ggplot(df, aes(x, y, fill = fill_var)) +
#' geom_boxplot(outlier.shape = NA) +
#' geom_point(
#' aes(colour = colour_var),
#' position = position_jitterdodge()
#' )
#'
#' # Fixed: explicit group aligns points with boxes
#' ggplot(df, aes(x, y, fill = fill_var)) +
#' geom_boxplot(outlier.shape = NA) +
#' geom_point(
#' aes(colour = colour_var, group = fill_var),
#' position = position_jitterdodge()
#' )
#' }
position_jitterdodge <- function(jitter.width = NULL, jitter.height = 0,
dodge.width = 0.75, reverse = FALSE,
preserve = "total",
Expand Down Expand Up @@ -55,6 +98,28 @@ PositionJitterdodge <- ggproto("PositionJitterdodge", Position,
setup_params = function(self, data) {
flipped_aes <- has_flipped_aes(data)
data <- flip_data(data, flipped_aes)

# Warn when additional discrete aesthetics inflate groups beyond fill
if ("fill" %in% names(data) && is_discrete(data[["fill"]])) {
groups_per_pos <- vec_unique(data[c("group", "PANEL", "x")])
n_groups <- max(tabulate(vec_group_id(groups_per_pos[c("PANEL", "x")])))
fills_per_pos <- vec_unique(data[c("fill", "PANEL", "x")])
n_fills <- max(tabulate(vec_group_id(fills_per_pos[c("PANEL", "x")])))
if (n_groups > n_fills) {
cli::cli_warn(c(
"Dodge groups are larger than the number of {.field fill} values.",
"i" = paste(
"This can happen when additional discrete aesthetics (e.g.,",
"{.field colour}) inflate the implicit grouping."
),
"i" = paste(
"Set {.code aes(group = <fill variable>)} to align points",
"with the dodged layer."
)
))
}
}

width <- self$jitter.width %||% (resolution(data$x, zero = FALSE, TRUE) * 0.4)

if (identical(self$preserve, "total")) {
Expand Down
44 changes: 44 additions & 0 deletions man/position_jitterdodge.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions tests/testthat/_snaps/position-jitterdodge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# position_jitterdodge warns when groups exceed fill levels

Dodge groups are larger than the number of fill values.
i This can happen when additional discrete aesthetics (e.g., colour) inflate the implicit grouping.
i Set `aes(group = <fill variable>)` to align points with the dodged layer.

63 changes: 63 additions & 0 deletions tests/testthat/test-position-jitterdodge.R
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,66 @@ test_that("position_jitterdodge can preserve total or single width", {
))
expect_equal(get_layer_data(p)$x, new_mapped_discrete(c(0.75, 1.75, 2.25)))
})

test_that("position_jitterdodge warns when groups exceed fill levels", {
# colour must cross with fill within each x to inflate groups
df <- data_frame(
x = rep("A", 8),
y = 1:8,
fill = rep(c("f1", "f2"), each = 4),
colour = rep(c("c1", "c2"), 4)
)

p <- ggplot(df, aes(x, y, fill = fill, colour = colour)) +
geom_point(position = position_jitterdodge(
jitter.width = 0, jitter.height = 0
))

expect_snapshot_warning(ggplot_build(p))
})

test_that("position_jitterdodge does not warn with explicit group matching fill", {
df <- data_frame(
x = rep("A", 8),
y = 1:8,
fill = rep(c("f1", "f2"), each = 4),
colour = rep(c("c1", "c2"), 4)
)

p <- ggplot(df, aes(x, y, fill = fill, colour = colour, group = fill)) +
geom_point(position = position_jitterdodge(
jitter.width = 0, jitter.height = 0
))

expect_silent(ggplot_build(p))
})

test_that("position_jitterdodge does not warn with fill only", {
df <- data_frame(
x = rep(c("A", "B"), each = 10),
y = 1:20,
fill = rep(c("f1", "f2"), 10)
)

p <- ggplot(df, aes(x, y, fill = fill)) +
geom_point(position = position_jitterdodge(
jitter.width = 0, jitter.height = 0
))

expect_silent(ggplot_build(p))
})

test_that("position_jitterdodge does not warn without fill", {
df <- data_frame(
x = rep(c("A", "B"), each = 5),
y = 1:10,
colour = rep(c("c1", "c2"), 5)
)

p <- ggplot(df, aes(x, y, colour = colour)) +
geom_point(position = position_jitterdodge(
jitter.width = 0, jitter.height = 0
))

expect_silent(ggplot_build(p))
})
Loading