Skip to content

Commit ed1b80d

Browse files
authored
Aesthetics for position adjustments (#6100)
* positions have `default_aes` field * positions communicate their aesthetics * positions populate defaults * simplify `PositionNudge$compute_layer()` * `position_nudge()` accepts nudge aesthetics * remove nudge specific stuff from `geom_text()`/`geom_label()` * document * `order` aesthetic for `position_dodge()` * document aesthetic * add news bullet * add test * fix small note * document
1 parent 5964f7a commit ed1b80d

26 files changed

+137
-134
lines changed

NEWS.md

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

3+
* Position adjustments can now have auxiliary aesthetics (@teunbrand).
4+
* `position_nudge()` gains `nudge_x` and `nudge_y` aesthetics (#3026, #5445).
5+
* `position_dodge()` gains `order` aesthetic (#3022, #3345)
36
* More stability for vctrs-based palettes (@teunbrand, #6117).
47
* Fixed regression in `guide_bins(reverse = TRUE)` (@teunbrand, #6183).
58
* New function family for setting parts of a theme. For example, you can now use

R/geom-label.R

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,16 @@
44
#' @param label.r Radius of rounded corners. Defaults to 0.15 lines.
55
#' @param label.size Size of label border, in mm.
66
geom_label <- function(mapping = NULL, data = NULL,
7-
stat = "identity", position = "identity",
7+
stat = "identity", position = "nudge",
88
...,
99
parse = FALSE,
10-
nudge_x = 0,
11-
nudge_y = 0,
1210
label.padding = unit(0.25, "lines"),
1311
label.r = unit(0.15, "lines"),
1412
label.size = 0.25,
1513
size.unit = "mm",
1614
na.rm = FALSE,
1715
show.legend = NA,
1816
inherit.aes = TRUE) {
19-
if (!missing(nudge_x) || !missing(nudge_y)) {
20-
if (!missing(position)) {
21-
cli::cli_abort(c(
22-
"Both {.arg position} and {.arg nudge_x}/{.arg nudge_y} are supplied.",
23-
"i" = "Choose one approach to alter the position."
24-
))
25-
}
26-
27-
position <- position_nudge(nudge_x, nudge_y)
28-
}
2917

3018
layer(
3119
data = data,

R/geom-sf.R

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -314,11 +314,9 @@ geom_sf <- function(mapping = aes(), data = NULL, stat = "sf",
314314
#' @inheritParams geom_label
315315
#' @inheritParams stat_sf_coordinates
316316
geom_sf_label <- function(mapping = aes(), data = NULL,
317-
stat = "sf_coordinates", position = "identity",
317+
stat = "sf_coordinates", position = "nudge",
318318
...,
319319
parse = FALSE,
320-
nudge_x = 0,
321-
nudge_y = 0,
322320
label.padding = unit(0.25, "lines"),
323321
label.r = unit(0.15, "lines"),
324322
label.size = 0.25,
@@ -327,17 +325,6 @@ geom_sf_label <- function(mapping = aes(), data = NULL,
327325
inherit.aes = TRUE,
328326
fun.geometry = NULL) {
329327

330-
if (!missing(nudge_x) || !missing(nudge_y)) {
331-
if (!missing(position)) {
332-
cli::cli_abort(c(
333-
"Both {.arg position} and {.arg nudge_x}/{.arg nudge_y} are supplied.",
334-
"i" = "Only use one approach to alter the position."
335-
))
336-
}
337-
338-
position <- position_nudge(nudge_x, nudge_y)
339-
}
340-
341328
layer_sf(
342329
data = data,
343330
mapping = mapping,
@@ -363,28 +350,15 @@ geom_sf_label <- function(mapping = aes(), data = NULL,
363350
#' @inheritParams geom_text
364351
#' @inheritParams stat_sf_coordinates
365352
geom_sf_text <- function(mapping = aes(), data = NULL,
366-
stat = "sf_coordinates", position = "identity",
353+
stat = "sf_coordinates", position = "nudge",
367354
...,
368355
parse = FALSE,
369-
nudge_x = 0,
370-
nudge_y = 0,
371356
check_overlap = FALSE,
372357
na.rm = FALSE,
373358
show.legend = NA,
374359
inherit.aes = TRUE,
375360
fun.geometry = NULL) {
376361

377-
if (!missing(nudge_x) || !missing(nudge_y)) {
378-
if (!missing(position)) {
379-
cli::cli_abort(c(
380-
"Both {.arg position} and {.arg nudge_x}/{.arg nudge_y} are supplied.",
381-
"i" = "Only use one approach to alter the position."
382-
))
383-
}
384-
385-
position <- position_nudge(nudge_x, nudge_y)
386-
}
387-
388362
layer_sf(
389363
data = data,
390364
mapping = mapping,

R/geom-text.R

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,6 @@
4141
#' @inheritParams geom_point
4242
#' @param parse If `TRUE`, the labels will be parsed into expressions and
4343
#' displayed as described in `?plotmath`.
44-
#' @param nudge_x,nudge_y Horizontal and vertical adjustment to nudge labels by.
45-
#' Useful for offsetting text from points, particularly on discrete scales.
46-
#' Cannot be jointly specified with `position`.
47-
#' @param position A position adjustment to use on the data for this layer.
48-
#' Cannot be jointy specified with `nudge_x` or `nudge_y`. This
49-
#' can be used in various ways, including to prevent overplotting and
50-
#' improving the display. The `position` argument accepts the following:
51-
#' * The result of calling a position function, such as `position_jitter()`.
52-
#' * A string naming the position adjustment. To give the position as a
53-
#' string, strip the function name of the `position_` prefix. For example,
54-
#' to use `position_jitter()`, give the position as `"jitter"`.
55-
#' * For more information and other ways to specify the position, see the
56-
#' [layer position][layer_positions] documentation.
5744
#' @param check_overlap If `TRUE`, text that overlaps previous text in the
5845
#' same layer will not be plotted. `check_overlap` happens at draw time and in
5946
#' the order of the data. Therefore data should be arranged by the label
@@ -166,28 +153,15 @@
166153
#' geom_text(aes(label = text), vjust = "inward", hjust = "inward")
167154
#' }
168155
geom_text <- function(mapping = NULL, data = NULL,
169-
stat = "identity", position = "identity",
156+
stat = "identity", position = "nudge",
170157
...,
171158
parse = FALSE,
172-
nudge_x = 0,
173-
nudge_y = 0,
174159
check_overlap = FALSE,
175160
size.unit = "mm",
176161
na.rm = FALSE,
177162
show.legend = NA,
178163
inherit.aes = TRUE)
179164
{
180-
if (!missing(nudge_x) || !missing(nudge_y)) {
181-
if (!missing(position)) {
182-
cli::cli_abort(c(
183-
"Both {.arg position} and {.arg nudge_x}/{.arg nudge_y} are supplied.",
184-
"i" = "Only use one approach to alter the position."
185-
))
186-
}
187-
188-
position <- position_nudge(nudge_x, nudge_y)
189-
}
190-
191165
layer(
192166
data = data,
193167
mapping = mapping,

R/layer.R

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,12 @@ layer <- function(geom = NULL, stat = NULL,
128128

129129
# Split up params between aesthetics, geom, and stat
130130
params <- rename_aes(params)
131-
aes_params <- params[intersect(names(params), geom$aesthetics())]
131+
aes_params <- params[intersect(names(params), union(geom$aesthetics(), position$aesthetics()))]
132132
geom_params <- params[intersect(names(params), geom$parameters(TRUE))]
133133
stat_params <- params[intersect(names(params), stat$parameters(TRUE))]
134134

135135
ignore <- c("key_glyph", "name")
136-
all <- c(geom$parameters(TRUE), stat$parameters(TRUE), geom$aesthetics(), ignore)
136+
all <- c(geom$parameters(TRUE), stat$parameters(TRUE), geom$aesthetics(), position$aesthetics(), ignore)
137137

138138
# Take care of plain patterns provided as aesthetic
139139
pattern <- vapply(aes_params, is_pattern, logical(1))
@@ -164,7 +164,7 @@ layer <- function(geom = NULL, stat = NULL,
164164

165165
extra_aes <- setdiff(
166166
mapped_aesthetics(mapping),
167-
c(geom$aesthetics(), stat$aesthetics())
167+
c(geom$aesthetics(), stat$aesthetics(), position$aesthetics())
168168
)
169169
# Take care of size->linewidth aes renaming
170170
if (geom$rename_size && "size" %in% extra_aes && !"linewidth" %in% mapped_aesthetics(mapping)) {
@@ -415,6 +415,7 @@ Layer <- ggproto("Layer", NULL,
415415
compute_position = function(self, data, layout) {
416416
if (empty(data)) return(data_frame0())
417417

418+
data <- self$position$use_defaults(data, self$aes_params)
418419
params <- self$position$setup_params(data)
419420
data <- self$position$setup_data(data, params)
420421

R/position-.R

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
Position <- ggproto("Position",
4747
required_aes = character(),
4848

49+
default_aes = aes(),
50+
4951
setup_params = function(self, data) {
5052
list()
5153
},
@@ -66,6 +68,36 @@ Position <- ggproto("Position",
6668

6769
compute_panel = function(self, data, params, scales) {
6870
cli::cli_abort("Not implemented.")
71+
},
72+
73+
aesthetics = function(self) {
74+
required_aes <- self$required_aes
75+
if (!is.null(required_aes)) {
76+
required_aes <- unlist(strsplit(self$required_aes, "|", fixed = TRUE))
77+
}
78+
c(union(required_aes, names(self$default_aes)))
79+
},
80+
81+
use_defaults = function(self, data, params = list()) {
82+
83+
aes <- self$aesthetics()
84+
defaults <- self$default_aes
85+
86+
params <- params[intersect(names(params), aes)]
87+
params <- params[setdiff(names(params), names(data))]
88+
defaults <- defaults[setdiff(names(defaults), c(names(params), names(data)))]
89+
90+
if ((length(params) + length(defaults)) < 1) {
91+
return(data)
92+
}
93+
94+
new <- compact(lapply(defaults, eval_tidy, data = data))
95+
new[names(params)] <- params
96+
check_aesthetics(new, nrow(data))
97+
98+
data[names(new)] <- new
99+
data
100+
69101
}
70102
)
71103

R/position-dodge.R

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
#' @param reverse If `TRUE`, will reverse the default stacking order.
2020
#' This is useful if you're rotating both the plot and legend.
2121
#' @family position adjustments
22+
#' @eval rd_aesthetics("position", "dodge")
23+
#'
2224
#' @export
2325
#' @examples
2426
#' ggplot(mtcars, aes(factor(cyl), fill = factor(vs))) +
@@ -104,7 +106,10 @@ PositionDodge <- ggproto("PositionDodge", Position,
104106
preserve = "total",
105107
orientation = "x",
106108
reverse = NULL,
109+
default_aes = aes(order = NULL),
110+
107111
setup_params = function(self, data) {
112+
108113
flipped_aes <- has_flipped_aes(data, default = self$orientation == "y")
109114
check_required_aesthetics(
110115
if (flipped_aes) "y|ymin" else "x|xmin",
@@ -139,9 +144,22 @@ PositionDodge <- ggproto("PositionDodge", Position,
139144

140145
setup_data = function(self, data, params) {
141146
data <- flip_data(data, params$flipped_aes)
147+
142148
if (!"x" %in% names(data) && all(c("xmin", "xmax") %in% names(data))) {
143149
data$x <- (data$xmin + data$xmax) / 2
144150
}
151+
152+
data$order <- xtfrm( # xtfrm makes anything 'sortable'
153+
data$order %||% ave(data$group, data$x, data$PANEL, FUN = match_sorted)
154+
)
155+
if (params$reverse) {
156+
data$order <- -data$order
157+
}
158+
if (is.null(params$n)) { # preserve = "total"
159+
data$order <- ave(data$order, data$x, data$PANEL, FUN = match_sorted)
160+
} else { # preserve = "single"
161+
data$order <- match_sorted(data$order)
162+
}
145163
flip_data(data, params$flipped_aes)
146164
},
147165

@@ -179,7 +197,7 @@ pos_dodge <- function(df, width, n = NULL) {
179197

180198
# Have a new group index from 1 to number of groups.
181199
# This might be needed if the group numbers in this set don't include all of 1:n
182-
groupidx <- match(df$group, unique0(df$group))
200+
groupidx <- df$order %||% match_sorted(df$group)
183201

184202
# Find the center for each group, then use that to calculate xmin and xmax
185203
df$x <- df$x + width * ((groupidx - 0.5) / n - 0.5)
@@ -188,3 +206,7 @@ pos_dodge <- function(df, width, n = NULL) {
188206

189207
df
190208
}
209+
210+
match_sorted <- function(x, y = x, ...) {
211+
vec_match(x, vec_sort(unique0(y), ...))
212+
}

R/position-nudge.R

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#' @family position adjustments
99
#' @param x,y Amount of vertical and horizontal distance to move.
1010
#' @export
11+
#' @eval rd_aesthetics("position", "nudge")
1112
#' @examples
1213
#' df <- data.frame(
1314
#' x = c(1,3,2,5),
@@ -26,7 +27,7 @@
2627
#' ggplot(df, aes(x, y)) +
2728
#' geom_point() +
2829
#' geom_text(aes(label = y), nudge_y = -0.1)
29-
position_nudge <- function(x = 0, y = 0) {
30+
position_nudge <- function(x = NULL, y = NULL) {
3031
ggproto(NULL, PositionNudge,
3132
x = x,
3233
y = y
@@ -38,25 +39,21 @@ position_nudge <- function(x = 0, y = 0) {
3839
#' @usage NULL
3940
#' @export
4041
PositionNudge <- ggproto("PositionNudge", Position,
41-
x = 0,
42-
y = 0,
42+
x = NULL,
43+
y = NULL,
44+
45+
default_aes = aes(nudge_x = 0, nudge_y = 0),
4346

4447
setup_params = function(self, data) {
45-
list(x = self$x, y = self$y)
48+
list(
49+
x = self$x %||% data$nudge_x,
50+
y = self$y %||% data$nudge_y
51+
)
4652
},
4753

4854
compute_layer = function(self, data, params, layout) {
49-
# transform only the dimensions for which non-zero nudging is requested
50-
if (any(params$x != 0)) {
51-
if (any(params$y != 0)) {
52-
transform_position(data, function(x) x + params$x, function(y) y + params$y)
53-
} else {
54-
transform_position(data, function(x) x + params$x, NULL)
55-
}
56-
} else if (any(params$y != 0)) {
57-
transform_position(data, NULL, function(y) y + params$y)
58-
} else {
59-
data # if both x and y are 0 we don't need to transform
60-
}
55+
trans_x <- if (any(params$x != 0)) function(x) x + params$x
56+
trans_y <- if (any(params$y != 0)) function(y) y + params$y
57+
transform_position(data, trans_x, trans_y)
6158
}
6259
)

R/utilities-help.R

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
rd_aesthetics <- function(type, name, extra_note = NULL) {
44
obj <- switch(type,
55
geom = validate_subclass(name, "Geom", env = globalenv()),
6-
stat = validate_subclass(name, "Stat", env = globalenv())
6+
stat = validate_subclass(name, "Stat", env = globalenv()),
7+
position = validate_subclass(name, "Position", env = globalenv())
78
)
89
aes <- rd_aesthetics_item(obj)
910

R/utilities.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,7 @@ compute_data_size <- function(data, size, default = 0.9,
940940
res <- vapply(res, resolution, FUN.VALUE = numeric(1), ...)
941941
res <- min(res, na.rm = TRUE)
942942
} else if (panels == "by") {
943-
res <- ave(data[[var]], data$PANEL, FUN = function(x) resolution(x, ...))
943+
res <- stats::ave(data[[var]], data$PANEL, FUN = function(x) resolution(x, ...))
944944
} else {
945945
res <- resolution(data[[var]], ...)
946946
}

man/geom_boxplot.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/geom_dotplot.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)