diff --git a/NEWS.md b/NEWS.md index 3340810611..6de17815e9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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) diff --git a/R/position-collide.r b/R/position-collide.r index 551688644c..1ba4d52060 100644 --- a/R/position-collide.r +++ b/R/position-collide.r @@ -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")]))) diff --git a/R/position-stack.r b/R/position-stack.r index b1c49658d2..b80cc9eb59 100644 --- a/R/position-stack.r +++ b/R/position-stack.r @@ -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. @@ -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 } @@ -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) } ) diff --git a/man/position_stack.Rd b/man/position_stack.Rd index 66f3579052..9b55c7dc92 100644 --- a/man/position_stack.Rd +++ b/man/position_stack.Rd @@ -13,6 +13,20 @@ position_stack() \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. +} \examples{ # Stacking is the default behaviour for most area plots: ggplot(mtcars, aes(factor(cyl), fill = factor(vs))) + geom_bar() @@ -47,6 +61,20 @@ ggplot(data.set, aes(Time, Value)) + # 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)) + } \seealso{ See \code{\link{geom_bar}}, and \code{\link{geom_area}} for diff --git a/tests/testthat/test-position-stack.R b/tests/testthat/test-position-stack.R new file mode 100644 index 0000000000..3fb315c46b --- /dev/null +++ b/tests/testthat/test-position-stack.R @@ -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)) +})