Skip to content

Commit cf716a3

Browse files
committed
Reorder by group as well as x
Fixes #1691. Positive and negative y-values are now calculated separately to allow stacking below the x-axis Fix for geom_area geom_area doesn’t sort ymin and ymax so this needs to be handled here for correct results Add docs Adding documentation to position_stack Unit tests for the bug fixes/features Add bullets to news
1 parent b4fbeb8 commit cf716a3

File tree

5 files changed

+125
-7
lines changed

5 files changed

+125
-7
lines changed

NEWS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# ggplot2 2.1.0.9000
22

3+
* `position_stack()` and `position_fill()` now sorts the stacking order so it
4+
matches the order of the grouping. Use level reordering to alter the stacking
5+
order. The default legend and stacking order is now also in line. The default
6+
look of plots might change because of this (#1552, #1593).
7+
8+
* `position_stack()` now accepts negative values which will create stacks
9+
extending below the x-axis (#1691)
10+
311
* Restore functionality for use of `..density..` in
412
`geom_hexbin()` (@mikebirdgeneau, #1688)
513

R/position-collide.r

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ collide <- function(data, width = NULL, name, strategy, check.width = TRUE) {
2626
width <- widths[1]
2727
}
2828

29-
# Reorder by x position, relying on stable sort to preserve existing
30-
# ordering, which may be by group or order.
31-
data <- data[order(data$xmin), ]
29+
# Reorder by x position, then on group. Group is reversed so stacking order
30+
# follows the default legend order
31+
data <- data[order(data$xmin, -data$group), ]
3232

3333
# Check for overlap
3434
intervals <- as.numeric(t(unique(data[c("xmin", "xmax")])))

R/position-stack.r

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@
33
#' \code{position_fill} additionally standardises each stack to have unit
44
#' height.
55
#'
6+
#' @details \code{position_fill} and \code{position_stack} automatically stacks
7+
#' values so their order follows the decreasing sort order of the fill
8+
#' aesthetic. This makes sure that the stack order is aligned with the order in
9+
#' the legend, as long as the scale order has not been changed using the
10+
#' \code{breaks} argument. This also means that in order to change stacking
11+
#' order while preserving parity with the legend order it is necessary to
12+
#' reorder the factor levels of the fill aesthetic (see examples)
13+
#'
14+
#' Stacking of positive and negative values are performed separately so that
15+
#' positive values stack upwards from the x-axis and negative values stack
16+
#' downward. Do note that parity with legend order cannot be ensured when
17+
#' positive and negative values are mixed.
18+
#'
619
#' @family position adjustments
720
#' @seealso See \code{\link{geom_bar}}, and \code{\link{geom_area}} for
821
#' more examples.
@@ -41,6 +54,20 @@
4154
#'
4255
#' # But realise that this makes it *much* harder to compare individual
4356
#' # trends
57+
#'
58+
#' # Stacking order can be changed using ordered factors
59+
#' data.set$Type <- factor(data.set$Type, levels = c('c', 'b', 'd', 'a'))
60+
#' ggplot(data.set, aes(Time, Value)) + geom_area(aes(fill = Type))
61+
#'
62+
#' # while changing the scale order won't affect the stacking
63+
#' ggplot(data.set, aes(Time, Value)) + geom_area(aes(fill = Type)) +
64+
#' scale_fill_discrete(breaks = c('a', 'b', 'c', 'd'))
65+
#'
66+
#' # Negative values can be stacked as well
67+
#' neg <- data.set$Type %in% c('a', 'd')
68+
#' data.set$Value[neg] <- data.set$Value[neg] * -1
69+
#' ggplot(data.set, aes(Time, Value)) + geom_area(aes(fill = Type))
70+
#'
4471
position_stack <- function() {
4572
PositionStack
4673
}
@@ -61,14 +88,31 @@ PositionStack <- ggproto("PositionStack", Position,
6188
"Maybe you want position = 'identity'?")
6289
return(data)
6390
}
64-
65-
if (!is.null(data$ymin) && !all(data$ymin == 0))
66-
warning("Stacking not well defined when ymin != 0", call. = FALSE)
91+
if (!is.null(data$ymax) && !is.null(data$ymin)) {
92+
switch_index <- data$ymax < data$ymin
93+
data$ymin[switch_index] <- data$ymax[switch_index]
94+
data$ymax[switch_index] <- 0
95+
}
96+
if (!is.null(data$ymin) && !all((data$ymin == 0 & data$ymax >= 0) | data$ymax == 0 & data$ymin <= 0))
97+
warning("Stacking not well defined when ymin and ymax is on opposite sides of 0", call. = FALSE)
6798

6899
data
69100
},
70101

71102
compute_panel = function(data, params, scales) {
72-
collide(data, NULL, "position_stack", pos_stack)
103+
negative <- if (!is.null(data$ymin)) data$ymin < 0 else rep(FALSE, nrow(data))
104+
neg <- data[which(negative), ]
105+
pos <- data[which(!negative), ]
106+
if (any(negative)) {
107+
# Negate group so sorting order is consistent across the x-axis.
108+
# Undo negation afterwards so it doesn't mess up the rest
109+
neg$group <- -neg$group
110+
neg <- collide(neg, NULL, "position_stack", pos_stack)
111+
neg$group <- -neg$group
112+
}
113+
if (any(!negative)) {
114+
pos <- collide(pos, NULL, "position_stack", pos_stack)
115+
}
116+
rbind(pos, neg)
73117
}
74118
)

man/position_stack.Rd

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

tests/testthat/test-position-stack.R

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
context("position-stack")
2+
3+
test_that("ymin and ymax is sorted", {
4+
df <- data.frame(
5+
x = rep(1:2, each = 5),
6+
group = rep(1:4, length.out = 10),
7+
ymin = 0,
8+
ymax = sample.int(10) * sample(c(-1, 1), 10, TRUE)
9+
)
10+
sorted <- PositionStack$setup_data(df)
11+
expect_true(all(sorted$ymax[df$ymax < 0] == 0))
12+
expect_true(all(sorted$ymin[df$ymax < 0] == df$ymax[df$ymax < 0]))
13+
})
14+
15+
test_that("data is sorted prior to stacking", {
16+
df <- data.frame(
17+
x = rep(c(1:10), 3),
18+
var = rep(c("a", "b", "c"), 10),
19+
y = round(runif(30, 1, 5))
20+
)
21+
p <- ggplot(df, aes(x = x, y = y, fill = var)) +
22+
geom_area(position = "stack")
23+
dat <- layer_data(p)
24+
expect_true(all(dat$group == 3:1))
25+
})
26+
27+
test_that("negative and positive values are handled separately", {
28+
df <- data.frame(
29+
x = c(1,1,1,2,2),
30+
g = c(1,2,3,1,2),
31+
y = c(1,-1,1,2,-3)
32+
)
33+
p <- ggplot(df, aes(x, y, fill= factor(g))) +
34+
geom_bar(stat = "identity")
35+
dat <- layer_data(p)
36+
expect_equal(dat$ymin, c(0,1,0,-1,-3))
37+
expect_equal(dat$ymax, c(1,2,2,0,0))
38+
})

0 commit comments

Comments
 (0)