Skip to content

Commit 7e5ff92

Browse files
authored
Make discrete scale determination stable (#3998)
1 parent e2c020b commit 7e5ff92

11 files changed

+56
-73
lines changed

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ S3method("$",ggproto)
44
S3method("$",ggproto_parent)
55
S3method("$<-",uneval)
66
S3method("+",gg)
7+
S3method("[",mapped_discrete)
78
S3method("[",uneval)
9+
S3method("[<-",mapped_discrete)
810
S3method("[<-",uneval)
911
S3method("[[",ggproto)
1012
S3method("[[<-",uneval)
1113
S3method(.DollarNames,ggproto)
1214
S3method(as.list,ggproto)
1315
S3method(autolayer,default)
1416
S3method(autoplot,default)
17+
S3method(c,mapped_discrete)
1518
S3method(drawDetails,zeroGrob)
1619
S3method(element_grob,element_blank)
1720
S3method(element_grob,element_line)

R/scale-discrete-.r

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,9 @@ ScaleDiscretePosition <- ggproto("ScaleDiscretePosition", ScaleDiscrete,
108108

109109
map = function(self, x, limits = self$get_limits()) {
110110
if (is.discrete(x)) {
111-
seq_along(limits)[match(as.character(x), limits)]
112-
} else {
113-
x
111+
x <- seq_along(limits)[match(as.character(x), limits)]
114112
}
113+
new_mapped_discrete(x)
115114
},
116115

117116
rescale = function(self, x, limits = self$get_limits(), range = self$dimension(limits = limits)) {
@@ -129,3 +128,29 @@ ScaleDiscretePosition <- ggproto("ScaleDiscretePosition", ScaleDiscrete,
129128
new
130129
}
131130
)
131+
132+
# TODO: This is a clear candidate for vctrs once we adopt it
133+
new_mapped_discrete <- function(x) {
134+
if (!is.numeric(x)) {
135+
abort("`mapped_discrete` objects can only be created from numeric vectors")
136+
}
137+
class(x) <- c("mapped_discrete", "numeric")
138+
x
139+
}
140+
is_mapped_discrete <- function(x) inherits(x, "mapped_discrete")
141+
#' @export
142+
c.mapped_discrete <- function(..., recursive = FALSE) {
143+
new_mapped_discrete(c(unlist(lapply(list(...), unclass))))
144+
}
145+
#' @export
146+
`[.mapped_discrete` <- function(x, ..., drop = TRUE) {
147+
new_mapped_discrete(NextMethod())
148+
}
149+
#' @export
150+
`[<-.mapped_discrete` <- function(x, ..., value) {
151+
if (length(value) == 0) {
152+
return(x)
153+
}
154+
value <- as.numeric(unclass(value))
155+
new_mapped_discrete(NextMethod())
156+
}

R/utilities.r

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -562,10 +562,10 @@ has_flipped_aes <- function(data, params = list(), main_is_orthogonal = NA,
562562
}
563563

564564
# Is there a single actual discrete position
565-
y_is_int <- is.integer(y)
566-
x_is_int <- is.integer(x)
567-
if (xor(y_is_int, x_is_int)) {
568-
return(y_is_int != main_is_continuous)
565+
y_is_discrete <- is_mapped_discrete(y)
566+
x_is_discrete <- is_mapped_discrete(x)
567+
if (xor(y_is_discrete, x_is_discrete)) {
568+
return(y_is_discrete != main_is_continuous)
569569
}
570570

571571
# Does each group have a single x or y value
@@ -586,51 +586,6 @@ has_flipped_aes <- function(data, params = list(), main_is_orthogonal = NA,
586586
}
587587
}
588588

589-
# give up early
590-
if (!has_x && !has_y) {
591-
return(FALSE)
592-
}
593-
594-
# Both true discrete. give up
595-
if (y_is_int && x_is_int) {
596-
return(FALSE)
597-
}
598-
# Is there a single discrete-like position
599-
y_is_int <- if (has_y) isTRUE(all.equal(y, round(y))) else FALSE
600-
x_is_int <- if (has_x) isTRUE(all.equal(x, round(x))) else FALSE
601-
if (xor(y_is_int, x_is_int)) {
602-
return(y_is_int != main_is_continuous)
603-
}
604-
605-
if (main_is_optional) {
606-
# Is one of the axes all 0
607-
if (all(x == 0)) {
608-
return(main_is_continuous)
609-
}
610-
if (all(y == 0)) {
611-
return(!main_is_continuous)
612-
}
613-
}
614-
615-
y_diff <- diff(sort(y))
616-
x_diff <- diff(sort(x))
617-
618-
# FIXME: If both are discrete like, give up. Probably, we can make a better
619-
# guess, but it's not possible with the current implementation as very little
620-
# information is available in Geom$setup_params().
621-
if (y_is_int && x_is_int) {
622-
return(FALSE)
623-
}
624-
625-
y_diff <- y_diff[y_diff != 0]
626-
x_diff <- x_diff[x_diff != 0]
627-
628-
# If none are discrete is either regularly spaced
629-
y_is_regular <- if (has_y && length(y_diff) != 0) all((y_diff / min(y_diff)) %% 1 < .Machine$double.eps) else FALSE
630-
x_is_regular <- if (has_x && length(x_diff) != 0) all((x_diff / min(x_diff)) %% 1 < .Machine$double.eps) else FALSE
631-
if (xor(y_is_regular, x_is_regular)) {
632-
return(y_is_regular != main_is_continuous)
633-
}
634589
# default to no
635590
FALSE
636591
}

tests/testthat/test-build.r

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ test_that("position aesthetics are coerced to correct type", {
2525
l2 <- ggplot(df, aes(x, z)) + geom_point() + scale_x_discrete()
2626
d2 <- layer_data(l2, 1)
2727

28-
expect_is(d2$x, "integer")
29-
expect_is(d2$y, "integer")
28+
expect_s3_class(d2$x, "mapped_discrete")
29+
expect_s3_class(d2$y, "mapped_discrete")
3030
})
3131

3232
test_that("non-position aesthetics are mapped", {

tests/testthat/test-coord-polar.r

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ test_that("Inf is squished to range", {
7676

7777
# 0.4 is the upper limit of radius hardcoded in r_rescale()
7878
expect_equal(d[[2]]$r, 0.4)
79-
expect_equal(d[[2]]$theta, 0)
79+
expect_equal(d[[2]]$theta, new_mapped_discrete(0))
8080
expect_equal(d[[3]]$r, 0)
81-
expect_equal(d[[3]]$theta, 0)
81+
expect_equal(d[[3]]$theta, new_mapped_discrete(0))
8282
})
8383

8484

tests/testthat/test-geom-freqpoly.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ test_that("can do frequency polygon with categorical x", {
66
p <- ggplot(df, aes(x)) + geom_freqpoly(stat = "count")
77
d <- layer_data(p)
88

9-
expect_is(d$x, "integer")
10-
expect_equal(d$x, 1:3)
9+
expect_s3_class(d$x, "mapped_discrete")
10+
expect_equal(d$x, new_mapped_discrete(1:3))
1111
expect_equal(d$y, 3:1)
1212
})

tests/testthat/test-geom-tile.R

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ test_that("accepts width and height params", {
44
df <- data_frame(x = c("a", "b"), y = c("a", "b"))
55

66
out1 <- layer_data(ggplot(df, aes(x, y)) + geom_tile())
7-
expect_equal(out1$xmin, c(0.5, 1.5))
8-
expect_equal(out1$xmax, c(1.5, 2.5))
7+
expect_equal(out1$xmin, new_mapped_discrete(c(0.5, 1.5)))
8+
expect_equal(out1$xmax, new_mapped_discrete(c(1.5, 2.5)))
99

1010
out2 <- layer_data(ggplot(df, aes(x, y)) + geom_tile(width = 0.5, height = 0.5))
11-
expect_equal(out2$xmin, c(0.75, 1.75))
12-
expect_equal(out2$xmax, c(1.25, 2.25))
11+
expect_equal(out2$xmin, new_mapped_discrete(c(0.75, 1.75)))
12+
expect_equal(out2$xmax, new_mapped_discrete(c(1.25, 2.25)))
1313
})
1414

1515
test_that("accepts width and height aesthetics", {

tests/testthat/test-position_dodge.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ test_that("can control whether to preserve total or individual width", {
88
p_single <- ggplot(df, aes(x, fill = y)) +
99
geom_bar(position = position_dodge(preserve = "single"), width = 1)
1010

11-
expect_equal(layer_data(p_total)$x, c(1, 1.75, 2.25))
12-
expect_equal(layer_data(p_single)$x, c(0.75, 1.75, 2.25))
11+
expect_equal(layer_data(p_total)$x, new_mapped_discrete(c(1, 1.75, 2.25)))
12+
expect_equal(layer_data(p_single)$x, new_mapped_discrete(c(0.75, 1.75, 2.25)))
1313
})

tests/testthat/test-scale-discrete.R

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,18 @@ test_that("NAs are translated/preserved for position scales", {
1515
p2a <- ggplot(df, aes(x2, y)) + geom_point()
1616
p3a <- ggplot(df, aes(x3, y)) + geom_point()
1717

18-
expect_equal(layer_data(p1a)$x, c(1, 2, 3))
19-
expect_equal(layer_data(p2a)$x, c(1, 2, 3))
20-
expect_equal(layer_data(p3a)$x, c(1, 2, 3))
18+
expect_equal(layer_data(p1a)$x, new_mapped_discrete(c(1, 2, 3)))
19+
expect_equal(layer_data(p2a)$x, new_mapped_discrete(c(1, 2, 3)))
20+
expect_equal(layer_data(p3a)$x, new_mapped_discrete(c(1, 2, 3)))
2121

2222
rm_na_x <- scale_x_discrete(na.translate = FALSE)
2323
p1b <- p1a + rm_na_x
2424
p2b <- p2a + rm_na_x
2525
p3b <- p3a + rm_na_x
2626

27-
expect_equal(layer_data(p1b)$x, c(1, 2, NA))
28-
expect_equal(layer_data(p2b)$x, c(1, 2, NA))
29-
expect_equal(layer_data(p3b)$x, c(1, 2, NA))
27+
expect_equal(layer_data(p1b)$x, new_mapped_discrete(c(1, 2, NA)))
28+
expect_equal(layer_data(p2b)$x, new_mapped_discrete(c(1, 2, NA)))
29+
expect_equal(layer_data(p3b)$x, new_mapped_discrete(c(1, 2, NA)))
3030
})
3131

3232
test_that("NAs are translated/preserved for non-position scales", {

tests/testthat/test-stat-bin.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,13 @@ test_that("stat_count preserves x order for continuous and discrete", {
175175
# x is factor where levels match numeric order
176176
mtcars$carb2 <- factor(mtcars$carb)
177177
b <- ggplot_build(ggplot(mtcars, aes(carb2)) + geom_bar())
178-
expect_identical(b$data[[1]]$x, 1:6)
178+
expect_identical(b$data[[1]]$x, new_mapped_discrete(1:6))
179179
expect_identical(b$data[[1]]$y, c(7,10,3,10,1,1))
180180

181181
# x is factor levels differ from numeric order
182182
mtcars$carb3 <- factor(mtcars$carb, levels = c(4,1,2,3,6,8))
183183
b <- ggplot_build(ggplot(mtcars, aes(carb3)) + geom_bar())
184-
expect_identical(b$data[[1]]$x, 1:6)
184+
expect_identical(b$data[[1]]$x, new_mapped_discrete(1:6))
185185
expect_identical(b$layout$panel_params[[1]]$x$get_labels(), c("4","1","2","3","6","8"))
186186
expect_identical(b$data[[1]]$y, c(10,7,10,3,1,1))
187187
})

0 commit comments

Comments
 (0)