Skip to content

Commit 375a24b

Browse files
authored
Reign in overly ambitious arrow() in geom_path() (#5078)
* Repair arrows in `!constant` branch * Add arrow repair tests * Add NEWS bullet * Move NEWS bullet
1 parent b1874b2 commit 375a24b

File tree

3 files changed

+58
-0
lines changed

3 files changed

+58
-0
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+
* When `geom_path()` has aesthetics varying within groups, the `arrow()` is
4+
applied to groups instead of individual segments (@teunbrand, #4935).
35
* The default width of `geom_bar()` is now based on panel-wise resolution of
46
the data, rather than global resolution (@teunbrand, #4336).
57
* To apply dodging more consistently in violin plots, `stat_ydensity()` now

R/geom-path.R

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ GeomPath <- ggproto("GeomPath", Geom,
188188
end <- c(group_diff, TRUE)
189189

190190
if (!constant) {
191+
192+
arrow <- repair_segment_arrow(arrow, munched$group)
193+
191194
segmentsGrob(
192195
munched$x[!end], munched$y[!end], munched$x[!start], munched$y[!start],
193196
default.units = "native", arrow = arrow,
@@ -363,3 +366,40 @@ stairstep <- function(data, direction = "hv") {
363366

364367
data_frame0(x = x, y = y, data_attr)
365368
}
369+
370+
repair_segment_arrow <- function(arrow, group) {
371+
# Early exit if there is no arrow
372+
if (is.null(arrow)) {
373+
return(arrow)
374+
}
375+
376+
# Get group parameters
377+
rle <- vec_group_rle(group) # handles NAs better than base::rle()
378+
n_groups <- length(rle)
379+
rle_len <- field(rle, "length") - 1 # segments have 1 member less than lines
380+
rle_end <- cumsum(rle_len)
381+
rle_start <- rle_end - rle_len + 1
382+
383+
# Recycle ends and lengths
384+
ends <- rep(rep(arrow$ends, length.out = n_groups), rle_len)
385+
len <- rep(rep(arrow$length, length.out = n_groups), rle_len)
386+
387+
# Repair ends
388+
# Convert 'both' ends to first/last in multi-member groups
389+
is_both <- which(ends == 3)
390+
ends[setdiff(intersect(rle_start, is_both), rle_end)] <- 1L
391+
ends[setdiff(intersect(rle_end, is_both), rle_start)] <- 2L
392+
arrow$ends <- ends
393+
394+
# Repair lengths
395+
zero <- unit(0, "mm")
396+
# Set length of first segment to zero when ends is 'last'
397+
len[intersect(setdiff(rle_start, rle_end), which(ends == 2))] <- zero
398+
# Set length of last segment to zero when ends is 'first'
399+
len[intersect(setdiff(rle_end, rle_start), which(ends == 1))] <- zero
400+
# Set length of middle pieces to zero
401+
len[setdiff(seq_along(len), c(rle_start, rle_end))] <- zero
402+
arrow$length <- len
403+
404+
return(arrow)
405+
}

tests/testthat/test-geom-path.R

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ test_that("geom_path() throws meaningful error on bad combination of varying aes
1010
expect_snapshot_error(ggplotGrob(p))
1111
})
1212

13+
test_that("repair_segment_arrow() repairs sensibly", {
14+
group <- c(1,1,1,1,2,2)
15+
16+
ans <- repair_segment_arrow(arrow(ends = "last"), group)
17+
expect_equal(ans$ends, rep(2L, 4))
18+
expect_equal(as.numeric(ans$length), c(0, 0, 0.25, 0.25))
19+
20+
ans <- repair_segment_arrow(arrow(ends = "first"), group)
21+
expect_equal(ans$ends, rep(1L, 4))
22+
expect_equal(as.numeric(ans$length), c(0.25, 0, 0, 0.25))
23+
24+
ans <- repair_segment_arrow(arrow(ends = "both"), group)
25+
expect_equal(ans$ends, c(1L, 3L, 2L, 3L))
26+
expect_equal(as.numeric(ans$length), c(0.25, 0, 0.25, 0.25))
27+
})
28+
1329
# Tests on stairstep() ------------------------------------------------------------
1430

1531
test_that("stairstep() does not error with too few observations", {

0 commit comments

Comments
 (0)