Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# ggplot2 2.1.0.9000

* `position_stack()` and `position_fill()` now sorts the stacking order so it
matches the order of the grouping. Use level reordering to alter the stacking
order. The default legend and stacking order is now also in line. The default
look of plots might change because of this (#1552, #1593).

* `position_stack()` now accepts negative values which will create stacks
extending below the x-axis (#1691)

* Restore functionality for use of `..density..` in
`geom_hexbin()` (@mikebirdgeneau, #1688)

Expand Down
6 changes: 3 additions & 3 deletions R/position-collide.r
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ collide <- function(data, width = NULL, name, strategy, check.width = TRUE) {
width <- widths[1]
}

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

# Check for overlap
intervals <- as.numeric(t(unique(data[c("xmin", "xmax")])))
Expand Down
52 changes: 48 additions & 4 deletions R/position-stack.r
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
#' \code{position_fill} additionally standardises each stack to have unit
#' height.
#'
#' @details \code{position_fill} and \code{position_stack} automatically stacks
#' values so their order follows the decreasing sort order of the fill
#' aesthetic. This makes sure that the stack order is aligned with the order in
#' the legend, as long as the scale order has not been changed using the
#' \code{breaks} argument. This also means that in order to change stacking
#' order while preserving parity with the legend order it is necessary to
#' reorder the factor levels of the fill aesthetic (see examples)
#'
#' Stacking of positive and negative values are performed separately so that
#' positive values stack upwards from the x-axis and negative values stack
#' downward. Do note that parity with legend order cannot be ensured when
#' positive and negative values are mixed.
#'
#' @family position adjustments
#' @seealso See \code{\link{geom_bar}}, and \code{\link{geom_area}} for
#' more examples.
Expand Down Expand Up @@ -41,6 +54,20 @@
#'
#' # But realise that this makes it *much* harder to compare individual
#' # trends
#'
#' # Stacking order can be changed using ordered factors
#' data.set$Type <- factor(data.set$Type, levels = c('c', 'b', 'd', 'a'))
#' ggplot(data.set, aes(Time, Value)) + geom_area(aes(fill = Type))
#'
#' # while changing the scale order won't affect the stacking
#' ggplot(data.set, aes(Time, Value)) + geom_area(aes(fill = Type)) +
#' scale_fill_discrete(breaks = c('a', 'b', 'c', 'd'))
#'
#' # Negative values can be stacked as well
#' neg <- data.set$Type %in% c('a', 'd')
#' data.set$Value[neg] <- data.set$Value[neg] * -1
#' ggplot(data.set, aes(Time, Value)) + geom_area(aes(fill = Type))
#'
position_stack <- function() {
PositionStack
}
Expand All @@ -61,14 +88,31 @@ PositionStack <- ggproto("PositionStack", Position,
"Maybe you want position = 'identity'?")
return(data)
}

if (!is.null(data$ymin) && !all(data$ymin == 0))
warning("Stacking not well defined when ymin != 0", call. = FALSE)
if (!is.null(data$ymax) && !is.null(data$ymin)) {
switch_index <- data$ymax < data$ymin
data$ymin[switch_index] <- data$ymax[switch_index]
data$ymax[switch_index] <- 0
}
if (!is.null(data$ymin) && !all((data$ymin == 0 & data$ymax >= 0) | data$ymax == 0 & data$ymin <= 0))
warning("Stacking not well defined when ymin and ymax is on opposite sides of 0", call. = FALSE)

data
},

compute_panel = function(data, params, scales) {
collide(data, NULL, "position_stack", pos_stack)
negative <- if (!is.null(data$ymin)) data$ymin < 0 else rep(FALSE, nrow(data))
neg <- data[which(negative), ]
pos <- data[which(!negative), ]
if (any(negative)) {
# Negate group so sorting order is consistent across the x-axis.
# Undo negation afterwards so it doesn't mess up the rest
neg$group <- -neg$group
neg <- collide(neg, NULL, "position_stack", pos_stack)
neg$group <- -neg$group
}
if (any(!negative)) {
pos <- collide(pos, NULL, "position_stack", pos_stack)
}
rbind(pos, neg)
}
)
28 changes: 28 additions & 0 deletions man/position_stack.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions tests/testthat/test-position-stack.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
context("position-stack")

test_that("ymin and ymax is sorted", {
df <- data.frame(
x = rep(1:2, each = 5),
group = rep(1:4, length.out = 10),
ymin = 0,
ymax = sample.int(10) * sample(c(-1, 1), 10, TRUE)
)
sorted <- PositionStack$setup_data(df)
expect_true(all(sorted$ymax[df$ymax < 0] == 0))
expect_true(all(sorted$ymin[df$ymax < 0] == df$ymax[df$ymax < 0]))
})

test_that("data is sorted prior to stacking", {
df <- data.frame(
x = rep(c(1:10), 3),
var = rep(c("a", "b", "c"), 10),
y = round(runif(30, 1, 5))
)
p <- ggplot(df, aes(x = x, y = y, fill = var)) +
geom_area(position = "stack")
dat <- layer_data(p)
expect_true(all(dat$group == 3:1))
})

test_that("negative and positive values are handled separately", {
df <- data.frame(
x = c(1,1,1,2,2),
g = c(1,2,3,1,2),
y = c(1,-1,1,2,-3)
)
p <- ggplot(df, aes(x, y, fill= factor(g))) +
geom_bar(stat = "identity")
dat <- layer_data(p)
expect_equal(dat$ymin, c(0,1,0,-1,-3))
expect_equal(dat$ymax, c(1,2,2,0,0))
})