Skip to content

Commit 80d5406

Browse files
authored
Checks for bandwidth in stat_density_2d() (#6375)
* throw informative error * do not attempt to contour with empty data * add test * fallback for 0 IQR data * isolate bandwidth logic * accept snapshot
1 parent 0668f5c commit 80d5406

File tree

3 files changed

+43
-4
lines changed

3 files changed

+43
-4
lines changed

R/stat-density-2d.R

+29-4
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ StatDensity2d <- ggproto("StatDensity2d", Stat,
141141
check_installed("MASS", reason = "for calculating 2D density.")
142142
# first run the regular layer calculation to infer densities
143143
data <- ggproto_parent(Stat, self)$compute_layer(data, params, layout)
144+
if (empty(data)) {
145+
return(data_frame0())
146+
}
144147

145148
# if we're not contouring we're done
146149
if (!isTRUE(params$contour %||% TRUE)) return(data)
@@ -178,10 +181,8 @@ StatDensity2d <- ggproto("StatDensity2d", Stat,
178181

179182
compute_group = function(data, scales, na.rm = FALSE, h = NULL, adjust = c(1, 1),
180183
n = 100, ...) {
181-
if (is.null(h)) {
182-
h <- c(MASS::bandwidth.nrd(data$x), MASS::bandwidth.nrd(data$y))
183-
h <- h * adjust
184-
}
184+
185+
h <- precompute_2d_bw(data$x, data$y, h = h, adjust = adjust)
185186

186187
# calculate density
187188
dens <- MASS::kde2d(
@@ -214,3 +215,27 @@ StatDensity2dFilled <- ggproto("StatDensity2dFilled", StatDensity2d,
214215
contour_type = "bands"
215216
)
216217

218+
precompute_2d_bw <- function(x, y, h = NULL, adjust = 1) {
219+
220+
if (is.null(h)) {
221+
# Note: MASS::bandwidth.nrd is equivalent to stats::bw.nrd * 4
222+
h <- c(MASS::bandwidth.nrd(x), MASS::bandwidth.nrd(y))
223+
# Handle case when when IQR == 0 and thus regular nrd bandwidth fails
224+
if (h[1] == 0 && length(x) > 1) h[1] <- bw.nrd0(x) * 4
225+
if (h[2] == 0 && length(y) > 1) h[2] <- bw.nrd0(y) * 4
226+
h <- h * adjust
227+
}
228+
229+
check_numeric(h)
230+
check_length(h, 2L)
231+
232+
if (any(is.na(h) | h <= 0)) {
233+
cli::cli_abort(c(
234+
"The bandwidth argument {.arg h} must contain numbers larger than 0.",
235+
i = "Please set the {.arg h} argument to stricly positive numbers manually."
236+
))
237+
}
238+
239+
h
240+
}
241+

tests/testthat/_snaps/stat-density2d.md

+7
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,10 @@
55
Caused by error in `compute_layer()`:
66
! `contour_var` must be one of "density", "ndensity", or "count", not "abcd".
77

8+
# stat_density_2d handles faulty bandwidth
9+
10+
Computation failed in `stat_density2d()`.
11+
Caused by error in `precompute_2d_bw()`:
12+
! The bandwidth argument `h` must contain numbers larger than 0.
13+
i Please set the `h` argument to stricly positive numbers manually.
14+

tests/testthat/test-stat-density2d.R

+7
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,10 @@ test_that("stat_density2d can produce contour and raster data", {
9595
# error on incorrect contouring variable
9696
expect_snapshot_error(ggplot_build(p + stat_density_2d(contour_var = "abcd")))
9797
})
98+
99+
test_that("stat_density_2d handles faulty bandwidth", {
100+
p <- ggplot(faithful, aes(eruptions, waiting)) +
101+
stat_density_2d(h = c(0, NA))
102+
expect_snapshot_warning(b <- ggplot_build(p))
103+
expect_s3_class(layer_grob(b)[[1]], "zeroGrob")
104+
})

0 commit comments

Comments
 (0)