Skip to content

Commit

Permalink
Support reverse stacking
Browse files Browse the repository at this point in the history
Fixes #1837

I also changed the default order to stack from inside-out, rather than bottom-to-top.
  • Loading branch information
hadley committed Oct 10, 2016
1 parent 5b70f2a commit f24770f
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 23 deletions.
2 changes: 1 addition & 1 deletion NEWS.md
Expand Up @@ -10,7 +10,7 @@ The main plot title is now left-aligned to better work better with a subtitle. T

### Stacking

`position_stack()` and `position_fill()` now sort the stacking order to match the order of grouping. This allows you to control the order through grouping, and ensures that the default legend matches the plot (#1552, #1593).
`position_stack()` and `position_fill()` now sort the stacking order to match the order of grouping. This allows you to control the order through grouping, and ensures that the default legend matches the plot (#1552, #1593). If you want the opposite order (useful if you have horizontal bars and horizontal legend), you can request reverse stacking by using `position = position_stack(reverse = TRUE)` (#1837).

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

Expand Down
13 changes: 9 additions & 4 deletions R/position-collide.r
@@ -1,6 +1,6 @@
# Detect and prevent collisions.
# Powers dodging, stacking and filling.
collide <- function(data, width = NULL, name, strategy, ..., check.width = TRUE) {
collide <- function(data, width = NULL, name, strategy, ..., check.width = TRUE, reverse = FALSE) {
# Determine width
if (!is.null(width)) {
# Width set manually
Expand All @@ -26,9 +26,14 @@ collide <- function(data, width = NULL, name, strategy, ..., check.width = TRUE)
width <- widths[1]
}

# 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), ]
# Reorder by x position, then on group. The default stacking order reverses
# the group in order to match the legend order.
if (reverse) {
data <- data[order(data$xmin, data$group), ]
} else {
data <- data[order(data$xmin, -data$group), ]
}


# Check for overlap
intervals <- as.numeric(t(unique(data[c("xmin", "xmax")])))
Expand Down
27 changes: 14 additions & 13 deletions R/position-stack.r
Expand Up @@ -21,6 +21,8 @@
#' (like points or lines), not a dimension (like bars or areas). Set to
#' \code{0} to align with the bottom, \code{0.5} for the middle,
#' and \code{1} (the default) for the top.
#' @param reverse If \code{TRUE}, will reverse the default stacking order.
#' This is useful if you're rotating both the plot and legend.
#' @seealso See \code{\link{geom_bar}}, and \code{\link{geom_area}} for
#' more examples.
#' @export
Expand Down Expand Up @@ -96,21 +98,21 @@
#' "b", -1, "y"
#' )
#' ggplot(data = df, aes(x, y, group = grp)) +
#' geom_col(aes(fill = grp)) +
#' geom_col(aes(fill = grp), position = position_stack(reverse = TRUE)) +
#' geom_hline(yintercept = 0)
#'
#' ggplot(data = df, aes(x, y, group = grp)) +
#' geom_col(aes(fill = grp)) +
#' geom_hline(yintercept = 0) +
#' geom_text(aes(label = grp), position = position_stack(vjust = 0.5))
position_stack <- function(vjust = 1) {
ggproto(NULL, PositionStack, vjust = vjust)
position_stack <- function(vjust = 1, reverse = FALSE) {
ggproto(NULL, PositionStack, vjust = vjust, reverse = reverse)
}

#' @export
#' @rdname position_stack
position_fill <- function(vjust = 1) {
ggproto(NULL, PositionFill, vjust = vjust)
position_fill <- function(vjust = 1, reverse = FALSE) {
ggproto(NULL, PositionFill, vjust = vjust, reverse = reverse)
}

#' @rdname ggplot2-ggproto
Expand All @@ -121,12 +123,14 @@ PositionStack <- ggproto("PositionStack", Position,
type = NULL,
vjust = 1,
fill = FALSE,
reverse = FALSE,

setup_params = function(self, data) {
list(
var = self$var %||% stack_var(data),
fill = self$fill,
vjust = self$vjust
vjust = self$vjust,
reverse = self$reverse
)
},

Expand Down Expand Up @@ -157,20 +161,17 @@ PositionStack <- ggproto("PositionStack", Position,
pos <- data[!negative, , drop = FALSE]

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,
vjust = params$vjust,
fill = params$fill
fill = params$fill,
reverse = params$reverse
)
neg$group <- -neg$group
}

if (any(!negative)) {
pos <- collide(pos, NULL, "position_stack", pos_stack,
vjust = params$vjust,
fill = params$fill
fill = params$fill,
reverse = params$reverse
)
}

Expand Down
6 changes: 3 additions & 3 deletions man/position_stack.Rd

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

15 changes: 13 additions & 2 deletions tests/testthat/test-position-stack.R
Expand Up @@ -28,6 +28,17 @@ test_that("negative and positive values are handled separately", {
expect_equal(dat$ymax[dat$x == 2], c(0, 2))
})

test_that("can request reverse stacking", {
df <- data.frame(
y = c(-2, 2, -1, 1),
g = c("a", "a", "b", "b")
)
p <- ggplot(df, aes(1, y, fill = g)) +
geom_col(position = position_stack(reverse = TRUE))
dat <- layer_data(p)
expect_equal(dat$ymin, c(-2, -3, 0, 2))
})

test_that("data with no extent is stacked correctly", {
df = data.frame(
x = c(1, 1),
Expand All @@ -38,6 +49,6 @@ test_that("data with no extent is stacked correctly", {
p0 <- base + geom_text(aes(label = y), position = position_stack(vjust = 0))
p1 <- base + geom_text(aes(label = y), position = position_stack(vjust = 1))

expect_equal(layer_data(p0)$y, c(-40, -115))
expect_equal(layer_data(p1)$y, c(0, -40))
expect_equal(layer_data(p0)$y, c(-75, -115))
expect_equal(layer_data(p1)$y, c(0, -75))
})

0 comments on commit f24770f

Please sign in to comment.