Skip to content

Commit c6a9a14

Browse files
authored
geom_rect() can derive corners from x/width or y/height (#5862)
* required aesthetics to non-missing aes * <GeomBar> doesn't inherit `optional_aes` * fill missing aes * throw error when unresolveable * protect against partial matching * add tests * add news bullet * adjust docs * fallback for corners wired through `params` * Revert "<GeomBar> doesn't inherit `optional_aes`" This reverts commit 65030f5. * adapt `check_required_aesthetics()` to deal with multiple or-logic * undo aesthetic field voodoo * fill data from params more reliably * throw error from `resolve_rect()` * document
1 parent fb33e26 commit c6a9a14

File tree

8 files changed

+178
-45
lines changed

8 files changed

+178
-45
lines changed

NEWS.md

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

3+
* `geom_rect()` can now derive the required corners positions from `x`/`width`
4+
or `y`/`height` parameterisation (@teunbrand, #5861).
35
* All position scales now use the same definition of `x` and `y` aesthetics.
46
This lets uncommon aesthetics like `xintercept` expand scales as usual.
57
(#3342, #4966, @teunbrand)

R/geom-rect.R

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,39 @@ GeomRect <- ggproto("GeomRect", Geom,
3131
default_aes = aes(colour = NA, fill = "grey35", linewidth = 0.5, linetype = 1,
3232
alpha = NA),
3333

34-
required_aes = c("xmin", "xmax", "ymin", "ymax"),
34+
required_aes = c("x|width|xmin|xmax", "y|height|ymin|ymax"),
35+
36+
setup_data = function(self, data, params) {
37+
if (all(c("xmin", "xmax", "ymin", "ymax") %in% names(data))) {
38+
return(data)
39+
}
40+
41+
# Fill in missing aesthetics from parameters
42+
required <- strsplit(self$required_aes, "|", fixed = TRUE)
43+
missing <- setdiff(unlist(required), names(data))
44+
default <- params[intersect(missing, names(params))]
45+
data[names(default)] <- default
46+
47+
if (is.null(data$xmin) || is.null(data$xmax)) {
48+
x <- resolve_rect(
49+
data[["xmin"]], data[["xmax"]],
50+
data[["x"]], data[["width"]],
51+
fun = snake_class(self), type = "x"
52+
)
53+
i <- lengths(x) > 1
54+
data[c("xmin", "xmax")[i]] <- x[i]
55+
}
56+
if (is.null(data$ymin) || is.null(data$ymax)) {
57+
y <- resolve_rect(
58+
data[["ymin"]], data[["ymax"]],
59+
data[["y"]], data[["height"]],
60+
fun = snake_class(self), type = "y"
61+
)
62+
i <- lengths(y) > 1
63+
data[c("ymin", "ymax")[i]] <- y[i]
64+
}
65+
data
66+
},
3567

3668
draw_panel = function(self, data, panel_params, coord, lineend = "butt", linejoin = "mitre") {
3769
data <- check_linewidth(data, snake_class(self))
@@ -73,3 +105,41 @@ GeomRect <- ggproto("GeomRect", Geom,
73105

74106
rename_size = TRUE
75107
)
108+
109+
resolve_rect <- function(min = NULL, max = NULL, center = NULL, length = NULL,
110+
fun, type) {
111+
absent <- c(is.null(min), is.null(max), is.null(center), is.null(length))
112+
if (sum(absent) > 2) {
113+
missing <- switch(
114+
type,
115+
x = c("xmin", "xmax", "x", "width"),
116+
y = c("ymin", "ymax", "y", "height")
117+
)
118+
cli::cli_abort(c(
119+
"{.fn {fun}} requires two of the following aesthetics: \\
120+
{.or {.field {missing}}}.",
121+
i = "Currently, {.field {missing[!absent]}} is present."
122+
))
123+
}
124+
125+
if (absent[1] && absent[2]) {
126+
min <- center - 0.5 * length
127+
max <- center + 0.5 * length
128+
return(list(min = min, max = max))
129+
}
130+
if (absent[1]) {
131+
if (is.null(center)) {
132+
min <- max - length
133+
} else {
134+
min <- max - 2 * (max - center)
135+
}
136+
}
137+
if (absent[2]) {
138+
if (is.null(center)) {
139+
max <- min + length
140+
} else {
141+
max <- min + 2 * (center - min)
142+
}
143+
}
144+
list(min = min, max = max)
145+
}

R/geom-tile.R

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
#' Rectangles
22
#'
33
#' `geom_rect()` and `geom_tile()` do the same thing, but are
4-
#' parameterised differently: `geom_rect()` uses the locations of the four
5-
#' corners (`xmin`, `xmax`, `ymin` and `ymax`), while
6-
#' `geom_tile()` uses the center of the tile and its size (`x`,
7-
#' `y`, `width`, `height`). `geom_raster()` is a high
8-
#' performance special case for when all the tiles are the same size, and no
9-
#' pattern fills are applied.
4+
#' parameterised differently: `geom_tile()` uses the center of the tile and its
5+
#' size (`x`, `y`, `width`, `height`), while `geom_rect()` can use those or the
6+
#' locations of the corners (`xmin`, `xmax`, `ymin` and `ymax`).
7+
#' `geom_raster()` is a high performance special case for when all the tiles
8+
#' are the same size, and no pattern fills are applied.
109
#'
11-
#' @eval rd_aesthetics("geom", "tile", "Note that `geom_raster()` ignores `colour`.")
10+
#' @eval rd_aesthetics(
11+
#' "geom", "rect",
12+
#' "`geom_tile()` understands only the `x`/`width` and `y`/`height` combinations.
13+
#' Note that `geom_raster()` ignores `colour`."
14+
#' )
1215
#' @inheritParams layer
1316
#' @inheritParams geom_point
1417
#' @inheritParams geom_segment
1518
#' @export
1619
#'
1720
#' @details
18-
#' `geom_rect()` and `geom_tile()`'s respond differently to scale
19-
#' transformations due to their parameterisation. In `geom_rect()`, the scale
20-
#' transformation is applied to the corners of the rectangles. In `geom_tile()`,
21-
#' the transformation is applied only to the centres and its size is determined
22-
#' after transformation.
21+
#' Please note that the `width` and `height` aesthetics are not true position
22+
#' aesthetics and therefore are not subject to scale transformation. It is
23+
#' only after transformation that these aesthetics are applied.
2324
#'
2425
#' @examples
2526
#' # The most common use for rectangles is to draw a surface. You always want

R/utilities-help.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ rd_aesthetics <- function(type, name, extra_note = NULL) {
2323

2424
rd_aesthetics_item <- function(x) {
2525
req <- x$required_aes
26-
req <- sub("|", "} \\emph{or} \\code{", req, fixed = TRUE)
26+
req <- gsub("|", "} \\emph{or} \\code{", req, fixed = TRUE)
2727
req_aes <- unlist(strsplit(x$required_aes, "|", fixed = TRUE))
2828
optional_aes <- setdiff(x$aesthetics(), req_aes)
2929
all <- union(req, sort(optional_aes))

R/utilities.R

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,53 @@ scales::alpha
2424
# @param name of object for error message
2525
# @keyword internal
2626
check_required_aesthetics <- function(required, present, name, call = caller_env()) {
27-
if (is.null(required)) return()
27+
if (is.null(required)) {
28+
return()
29+
}
2830

2931
required <- strsplit(required, "|", fixed = TRUE)
30-
if (any(lengths(required) > 1)) {
31-
required <- lapply(required, rep_len, 2)
32-
required <- list(
33-
vapply(required, `[`, character(1), 1),
34-
vapply(required, `[`, character(1), 2)
32+
n <- lengths(required)
33+
34+
is_present <- vapply(
35+
required,
36+
function(req) any(req %in% present),
37+
logical(1)
38+
)
39+
if (all(is_present)) {
40+
return()
41+
}
42+
43+
# Deal with paired (bidirectional) aesthetics
44+
pairs <- character()
45+
missing_pairs <- n == 2
46+
if (any(missing_pairs)) {
47+
pairs <- lapply(required[missing_pairs], rep_len, 2)
48+
pairs <- list(
49+
vapply(pairs, `[`, character(1), 1),
50+
vapply(pairs, `[`, character(1), 2)
3551
)
36-
} else {
37-
required <- list(unlist(required))
52+
pairs <- lapply(pairs, setdiff, present)
53+
pairs <- vapply(pairs, function(x) {
54+
as_cli("{.and {.field {x}}}")
55+
}, character(1))
56+
pairs <- as_cli("{.or {pairs}}")
3857
}
39-
missing_aes <- lapply(required, setdiff, present)
40-
if (any(lengths(missing_aes) == 0)) return()
41-
message <- "{.fn {name}} requires the following missing aesthetics: {.field {missing_aes[[1]]}}"
42-
if (length(missing_aes) > 1) {
43-
message <- paste0(message, " {.strong or} {.field {missing_aes[[2]]}}")
58+
59+
other <- character()
60+
missing_other <- !is_present & n != 2
61+
if (any(missing_other)) {
62+
other <- lapply(required[missing_other], setdiff, present)
63+
other <- vapply(other, function(x) {
64+
as_cli("{.or {.field {x}}}")
65+
}, character(1))
4466
}
45-
cli::cli_abort(paste0(message, "."), call = call)
67+
68+
missing <- c(other, pairs)
69+
70+
cli::cli_abort(
71+
"{.fn {name}} requires the following missing aesthetics: {.and {missing}}.",
72+
call = call
73+
)
4674
}
4775

4876
# Concatenate a named list for output

man/geom_tile.Rd

Lines changed: 12 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/utilities.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
---
1414

15-
`test()` requires the following missing aesthetics: x and fill or y and fill.
15+
`test()` requires the following missing aesthetics: fill and x or y.
1616

1717
# remove_missing checks input
1818

tests/testthat/test-geom-rect.R

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
test_that("geom_rect can derive corners", {
2+
3+
corners <- c("xmin", "xmax", "ymin", "ymax")
4+
full <- data.frame(
5+
xmin = c(1, 2), xmax = c(3, 6),
6+
ymin = c(1, 2), ymax = c(3, 6),
7+
width = c(2, 4), height = c(2, 4),
8+
x = c(2, 4), y = c(2, 4)
9+
)
10+
11+
test <- full[, c("xmin", "ymin", "width", "height")]
12+
test <- GeomRect$setup_data(test, NULL)
13+
expect_equal(full[, corners], test[, corners])
14+
15+
test <- full[, c("xmin", "ymin", "x", "y")]
16+
test <- GeomRect$setup_data(test, NULL)
17+
expect_equal(full[, corners], test[, corners])
18+
19+
test <- full[, c("x", "y", "width", "height")]
20+
test <- GeomRect$setup_data(test, NULL)
21+
expect_equal(full[, corners], test[, corners])
22+
23+
test <- full[, c("xmax", "ymax", "width", "height")]
24+
test <- GeomRect$setup_data(test, NULL)
25+
expect_equal(full[, corners], test[, corners])
26+
27+
test <- full[, c("xmax", "ymax", "x", "y")]
28+
test <- GeomRect$setup_data(test, NULL)
29+
expect_equal(full[, corners], test[, corners])
30+
31+
test <- full[, c("x", "y")]
32+
expect_error(
33+
GeomRect$setup_data(test, NULL),
34+
"requires two of the following aesthetics"
35+
)
36+
})

0 commit comments

Comments
 (0)