Skip to content

Commit e3fbccc

Browse files
authored
Make stat_function() work with empty input data. (#3984)
* make stat_function() work with empty input data. fixes #3983. * update news item, fix documentation, add unit tests * one more test
1 parent 675b405 commit e3fbccc

File tree

5 files changed

+68
-14
lines changed

5 files changed

+68
-14
lines changed

NEWS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88

99
* Default continuous color scales (i.e., the `options()` `ggplot2.continuous.colour` and `ggplot2.continuous.fill`, which inform the `type` argument of `scale_fill_continuous()` and `scale_colour_continuous()`) now accept a function, which allows more control over these default `continuous_scale()`s (@cpsievert, #3827)
1010

11-
* A newly added `geom_function()` is now the recommended geom to use in
12-
conjunction with `stat_function()`. In addition, `stat_function()` now
13-
works with transformed y axes, e.g. `scale_y_log10()` (@clauswilke, #3611, #3905).
11+
* A newly added `geom_function()` is now recommended to use in conjunction
12+
with/instead of `stat_function()`. In addition, `stat_function()` now
13+
works with transformed y axes, e.g. `scale_y_log10()`, and in plots
14+
containing no other data or layers (@clauswilke, #3611, #3905, #3983).
1415

1516
* A bug was fixed in `stat_contour()` when calculating breaks based on
1617
the `bins` argument (@clauswilke, #3879).

R/geom-function.R

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
#' geom_function(fun = dnorm, colour = "red")
1919
#'
2020
#' # To plot functions without data, specify range of x-axis
21-
#' base <- ggplot(data.frame(x = c(-5, 5)), aes(x))
21+
#' base <- ggplot() + xlim(-5, 5)
2222
#' base + geom_function(fun = dnorm)
2323
#' base + geom_function(fun = dnorm, args = list(mean = 2, sd = .5))
2424
#'
@@ -45,8 +45,11 @@ geom_function <- function(mapping = NULL, data = NULL, stat = "function",
4545
position = "identity", ..., na.rm = FALSE,
4646
show.legend = NA, inherit.aes = TRUE) {
4747
# Warn if supplied data is going to be overwritten
48-
if (!is.null(data) && identical(stat, "function")) {
49-
warn("`data` is not used by stat_function()")
48+
if (identical(stat, "function")) {
49+
if (!is.null(data)) {
50+
warn("`data` is not used by stat_function()")
51+
}
52+
data <- ensure_nonempty_data
5053
}
5154

5255
layer(

R/stat-function.r

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ stat_function <- function(mapping = NULL, data = NULL,
2929
if (!is.null(data)) {
3030
warn("`data` is not used by stat_function()")
3131
}
32+
data <- ensure_nonempty_data
3233

3334
layer(
3435
data = data,
@@ -57,15 +58,21 @@ StatFunction <- ggproto("StatFunction", Stat,
5758
default_aes = aes(y = after_scale(y)),
5859

5960
compute_group = function(data, scales, fun, xlim = NULL, n = 101, args = list()) {
60-
range <- xlim %||% scales$x$dimension()
61-
xseq <- seq(range[1], range[2], length.out = n)
62-
63-
if (scales$x$is_discrete()) {
61+
if (is.null(scales$x)) {
62+
range <- xlim %||% c(0, 1)
63+
xseq <- seq(range[1], range[2], length.out = n)
6464
x_trans <- xseq
6565
} else {
66-
# For continuous scales, need to back transform from transformed range
67-
# to original values
68-
x_trans <- scales$x$trans$inverse(xseq)
66+
range <- xlim %||% scales$x$dimension()
67+
xseq <- seq(range[1], range[2], length.out = n)
68+
69+
if (scales$x$is_discrete()) {
70+
x_trans <- xseq
71+
} else {
72+
# For continuous scales, need to back transform from transformed range
73+
# to original values
74+
x_trans <- scales$x$trans$inverse(xseq)
75+
}
6976
}
7077

7178
if (is.formula(fun)) fun <- as_function(fun)
@@ -82,3 +89,15 @@ StatFunction <- ggproto("StatFunction", Stat,
8289
))
8390
}
8491
)
92+
93+
# Convenience function used by `stat_function()` and
94+
# `geom_function()` to convert empty input data into
95+
# non-empty input data without touching any non-empty
96+
# input data that may have been provided.
97+
ensure_nonempty_data <- function(data) {
98+
if (empty(data)) {
99+
new_data_frame(list(group = 1), n = 1)
100+
} else {
101+
data
102+
}
103+
}

man/geom_function.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-stat-function.R

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,37 @@ test_that("uses scale limits, not data limits", {
2424
expect_false(any(is.na(ret_log$y)))
2525
})
2626

27+
test_that("works in plots without any data", {
28+
f <- function(x) 2*x
29+
30+
# default limits, 0 to 1
31+
base <- ggplot() + geom_function(fun = f, n = 6)
32+
ret <- layer_data(base)
33+
expect_identical(ret$x, seq(0, 1, length.out = 6))
34+
expect_identical(ret$y, 2*ret$x)
35+
36+
# manually set limits with xlim()
37+
base <- ggplot() + xlim(0, 2) + geom_function(fun = f, n = 6)
38+
ret <- layer_data(base)
39+
expect_identical(ret$x, seq(0, 2, length.out = 6))
40+
expect_identical(ret$y, 2*ret$x)
41+
42+
# manually set limits with xlim argument
43+
base <- ggplot() + geom_function(fun = f, n = 6, xlim = c(0, 2))
44+
ret <- layer_data(base)
45+
expect_identical(ret$x, seq(0, 2, length.out = 6))
46+
expect_identical(ret$y, 2*ret$x)
47+
48+
# mapping of color via aes() works
49+
base <- ggplot() +
50+
geom_function(aes(color = "fun"), fun = f, n = 6) +
51+
scale_color_manual(values = c(fun = "#D55E00"))
52+
ret <- layer_data(base)
53+
expect_identical(ret$x, seq(0, 1, length.out = 6))
54+
expect_identical(ret$y, 2*ret$x)
55+
expect_identical(ret$colour, rep("#D55E00", 6))
56+
})
57+
2758
test_that("works with discrete x", {
2859
dat <- data_frame(x = c("a", "b"))
2960

0 commit comments

Comments
 (0)