Skip to content

Commit 8d6a98f

Browse files
committed
Warn when position_jitterdodge() groups are inflated beyond fill
When additional discrete aesthetics are mapped in a layer using position_jitterdodge(), the implicit group can become finer than the fill-based dodge used by paired layers, causing visual misalignment. Add a targeted warning when dodge groups exceed fill levels, document the grouping interaction, and cover warning/no-warning cases in tests.
1 parent 6870419 commit 8d6a98f

5 files changed

Lines changed: 182 additions & 0 deletions

File tree

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# ggplot2 (development version)
22

3+
4+
* `position_jitterdodge()` now warns when dodge groups appear inflated by
5+
additional discrete aesthetics beyond `fill`, with guidance to set
6+
`aes(group = <fill variable>)` (@Jesssullivan, #6824).
37
* `make_constructor()` no longer captures `rlang::list2()` at build time.
48
* The `arrow` and `arrow.fill` arguments are now available in
59
`geom_linerange()` and `geom_pointrange()` layers (@teunbrand, #6481).

R/position-jitterdodge.R

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,56 @@
1212
#' the default `position_dodge()` width.
1313
#' @inheritParams position_jitter
1414
#' @inheritParams position_dodge
15+
#'
16+
#' @section Interaction with grouping:
17+
#' When no explicit `group` aesthetic is set, ggplot2 computes groups from the
18+
#' interaction of all discrete aesthetics in the layer (see [aes_group_order]).
19+
#' If your point layer maps additional discrete aesthetics beyond the `fill`
20+
#' used for dodging (e.g., `colour`, `shape`, or `linetype`), the points will
21+
#' be split into more groups than the dodged boxplots, causing misalignment.
22+
#'
23+
#' To fix this, explicitly set `group` to the same variable used for dodging
24+
#' (typically the `fill` variable):
25+
#'
26+
#' \preformatted{geom_point(aes(colour = status, group = fill_var),
27+
#' position = position_jitterdodge())}
28+
#'
1529
#' @export
1630
#' @examples
1731
#' set.seed(596)
1832
#' dsub <- diamonds[sample(nrow(diamonds), 1000), ]
1933
#' ggplot(dsub, aes(x = cut, y = carat, fill = clarity)) +
2034
#' geom_boxplot(outlier.size = 0) +
2135
#' geom_point(pch = 21, position = position_jitterdodge())
36+
#'
37+
#' # When mapping additional discrete aesthetics (e.g. colour), points
38+
#' # can misalign with boxes because the implicit groups are inflated.
39+
#' # Fix by setting group to the fill variable:
40+
#' \donttest{
41+
#' set.seed(596)
42+
#' df <- data.frame(
43+
#' x = rep(c("A", "B"), each = 20),
44+
#' y = rnorm(40),
45+
#' fill_var = rep(c("g1", "g2"), 20),
46+
#' colour_var = sample(c(TRUE, FALSE), 40, replace = TRUE)
47+
#' )
48+
#'
49+
#' # Misaligned: colour creates extra implicit groups
50+
#' ggplot(df, aes(x, y, fill = fill_var)) +
51+
#' geom_boxplot(outlier.shape = NA) +
52+
#' geom_point(
53+
#' aes(colour = colour_var),
54+
#' position = position_jitterdodge()
55+
#' )
56+
#'
57+
#' # Fixed: explicit group aligns points with boxes
58+
#' ggplot(df, aes(x, y, fill = fill_var)) +
59+
#' geom_boxplot(outlier.shape = NA) +
60+
#' geom_point(
61+
#' aes(colour = colour_var, group = fill_var),
62+
#' position = position_jitterdodge()
63+
#' )
64+
#' }
2265
position_jitterdodge <- function(jitter.width = NULL, jitter.height = 0,
2366
dodge.width = 0.75, reverse = FALSE,
2467
preserve = "total",
@@ -55,6 +98,28 @@ PositionJitterdodge <- ggproto("PositionJitterdodge", Position,
5598
setup_params = function(self, data) {
5699
flipped_aes <- has_flipped_aes(data)
57100
data <- flip_data(data, flipped_aes)
101+
102+
# Warn when additional discrete aesthetics inflate groups beyond fill
103+
if ("fill" %in% names(data) && is_discrete(data[["fill"]])) {
104+
groups_per_pos <- vec_unique(data[c("group", "PANEL", "x")])
105+
n_groups <- max(tabulate(vec_group_id(groups_per_pos[c("PANEL", "x")])))
106+
fills_per_pos <- vec_unique(data[c("fill", "PANEL", "x")])
107+
n_fills <- max(tabulate(vec_group_id(fills_per_pos[c("PANEL", "x")])))
108+
if (n_groups > n_fills) {
109+
cli::cli_warn(c(
110+
"Dodge groups are larger than the number of {.field fill} values.",
111+
"i" = paste(
112+
"This can happen when additional discrete aesthetics (e.g.,",
113+
"{.field colour}) inflate the implicit grouping."
114+
),
115+
"i" = paste(
116+
"Set {.code aes(group = <fill variable>)} to align points",
117+
"with the dodged layer."
118+
)
119+
))
120+
}
121+
}
122+
58123
width <- self$jitter.width %||% (resolution(data$x, zero = FALSE, TRUE) * 0.4)
59124

60125
if (identical(self$preserve, "total")) {

man/position_jitterdodge.Rd

Lines changed: 44 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# position_jitterdodge warns when groups exceed fill levels
2+
3+
Dodge groups are larger than the number of fill values.
4+
i This can happen when additional discrete aesthetics (e.g., colour) inflate the implicit grouping.
5+
i Set `aes(group = <fill variable>)` to align points with the dodged layer.
6+

tests/testthat/test-position-jitterdodge.R

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,66 @@ test_that("position_jitterdodge can preserve total or single width", {
3030
))
3131
expect_equal(get_layer_data(p)$x, new_mapped_discrete(c(0.75, 1.75, 2.25)))
3232
})
33+
34+
test_that("position_jitterdodge warns when groups exceed fill levels", {
35+
# colour must cross with fill within each x to inflate groups
36+
df <- data_frame(
37+
x = rep("A", 8),
38+
y = 1:8,
39+
fill = rep(c("f1", "f2"), each = 4),
40+
colour = rep(c("c1", "c2"), 4)
41+
)
42+
43+
p <- ggplot(df, aes(x, y, fill = fill, colour = colour)) +
44+
geom_point(position = position_jitterdodge(
45+
jitter.width = 0, jitter.height = 0
46+
))
47+
48+
expect_snapshot_warning(ggplot_build(p))
49+
})
50+
51+
test_that("position_jitterdodge does not warn with explicit group matching fill", {
52+
df <- data_frame(
53+
x = rep("A", 8),
54+
y = 1:8,
55+
fill = rep(c("f1", "f2"), each = 4),
56+
colour = rep(c("c1", "c2"), 4)
57+
)
58+
59+
p <- ggplot(df, aes(x, y, fill = fill, colour = colour, group = fill)) +
60+
geom_point(position = position_jitterdodge(
61+
jitter.width = 0, jitter.height = 0
62+
))
63+
64+
expect_silent(ggplot_build(p))
65+
})
66+
67+
test_that("position_jitterdodge does not warn with fill only", {
68+
df <- data_frame(
69+
x = rep(c("A", "B"), each = 10),
70+
y = 1:20,
71+
fill = rep(c("f1", "f2"), 10)
72+
)
73+
74+
p <- ggplot(df, aes(x, y, fill = fill)) +
75+
geom_point(position = position_jitterdodge(
76+
jitter.width = 0, jitter.height = 0
77+
))
78+
79+
expect_silent(ggplot_build(p))
80+
})
81+
82+
test_that("position_jitterdodge does not warn without fill", {
83+
df <- data_frame(
84+
x = rep(c("A", "B"), each = 5),
85+
y = 1:10,
86+
colour = rep(c("c1", "c2"), 5)
87+
)
88+
89+
p <- ggplot(df, aes(x, y, colour = colour)) +
90+
geom_point(position = position_jitterdodge(
91+
jitter.width = 0, jitter.height = 0
92+
))
93+
94+
expect_silent(ggplot_build(p))
95+
})

0 commit comments

Comments
 (0)