Skip to content

Commit e67bf20

Browse files
authored
Contours for non-axis aligned grids (#5911)
* attempt contour calculation in unrotated space * add test * add news bullet * fix mistake in calculation * protect against huge amounts of data
1 parent f220ded commit e67bf20

File tree

3 files changed

+83
-0
lines changed

3 files changed

+83
-0
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# ggplot2 (development version)
22

3+
* `geom_contour()` should be able to recognise a rotated grid of points
4+
(@teunbrand, #4320)
35
* `geom_boxplot()` gains additional arguments to style the colour, linetype and
46
linewidths of the box, whiskers, median line and staples (@teunbrand, #5126)
57
* (internal) Using `after_scale()` in the `Geom*$default_aes()` field is now

R/stat-contour.R

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ StatContour <- ggproto("StatContour", Stat,
104104

105105
compute_group = function(data, scales, z.range, bins = NULL, binwidth = NULL,
106106
breaks = NULL, na.rm = FALSE) {
107+
# Undo data rotation
108+
rotation <- estimate_contour_angle(data$x, data$y)
109+
data[c("x", "y")] <- rotate_xy(data$x, data$y, -rotation)
107110

108111
breaks <- contour_breaks(z.range, bins, binwidth, breaks)
109112

@@ -113,6 +116,8 @@ StatContour <- ggproto("StatContour", Stat,
113116
path_df$level <- as.numeric(path_df$level)
114117
path_df$nlevel <- rescale_max(path_df$level)
115118

119+
# Re-apply data rotation
120+
path_df[c("x", "y")] <- rotate_xy(path_df$x, path_df$y, rotation)
116121
path_df
117122
}
118123
)
@@ -138,6 +143,11 @@ StatContourFilled <- ggproto("StatContourFilled", Stat,
138143
},
139144

140145
compute_group = function(data, scales, z.range, bins = NULL, binwidth = NULL, breaks = NULL, na.rm = FALSE) {
146+
147+
# Undo data rotation
148+
rotation <- estimate_contour_angle(data$x, data$y)
149+
data[c("x", "y")] <- rotate_xy(data$x, data$y, -rotation)
150+
141151
breaks <- contour_breaks(z.range, bins, binwidth, breaks)
142152

143153
isobands <- withr::with_options(list(OutDec = "."), xyz_to_isobands(data, breaks))
@@ -149,6 +159,8 @@ StatContourFilled <- ggproto("StatContourFilled", Stat,
149159
path_df$level_high <- breaks[as.numeric(path_df$level) + 1]
150160
path_df$level_mid <- 0.5*(path_df$level_low + path_df$level_high)
151161
path_df$nlevel <- rescale_max(path_df$level_high)
162+
# Re-apply data rotation
163+
path_df[c("x", "y")] <- rotate_xy(path_df$x, path_df$y, rotation)
152164

153165
path_df
154166
}
@@ -356,3 +368,49 @@ contour_deduplicate <- function(data, check = c("x", "y", "group", "PANEL")) {
356368
}
357369
data
358370
}
371+
372+
estimate_contour_angle <- function(x, y) {
373+
374+
# Compute most frequent angle among first 20 points
375+
all_angles <- atan2(diff(head(y, 20L)), diff(head(x, 20L)))
376+
freq <- tabulate(match(all_angles, unique(all_angles)))
377+
i <- which.max(freq)
378+
379+
# If this angle represents less than half of the angles, we probably
380+
# have unordered data, in which case the approach above is invalid
381+
if ((freq[i] / sum(freq)) < 0.5) {
382+
# In such case, try approach with convex hull
383+
hull <- grDevices::chull(x, y)
384+
hull <- c(hull, hull[1])
385+
# Find largest edge along hull
386+
dx <- diff(x[hull])
387+
dy <- diff(y[hull])
388+
i <- which.max(sqrt(dx^2 + dy^2))
389+
# Take angle of largest edge
390+
angle <- atan2(dy[i], dx[i])
391+
} else {
392+
angle <- all_angles[i]
393+
}
394+
395+
# No need to rotate contour data when angle is straight
396+
straight <- abs(angle - c(-1, -0.5, 0, 0.5, 1) * pi) < sqrt(.Machine$double.eps)
397+
if (any(straight)) {
398+
return(0)
399+
}
400+
angle
401+
}
402+
403+
rotate_xy <- function(x, y, angle) {
404+
# Skip rotation if angle was straight
405+
if (angle == 0) {
406+
return(list(x = x, y = y))
407+
}
408+
cos <- cos(angle)
409+
sin <- sin(angle)
410+
# Using zapsmall to make `unique0` later recognise values that may have
411+
# rounding errors.
412+
list(
413+
x = zapsmall(cos * x - sin * y, digits = 13),
414+
y = zapsmall(sin * x + cos * y, digits = 13)
415+
)
416+
}

tests/testthat/test-stat-contour.R

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,26 @@ test_that("stat_contour() removes duplicated coordinates", {
9999
expect_equal(new, df[1:4,], ignore_attr = TRUE)
100100
})
101101

102+
test_that("stat_contour() can infer rotations", {
103+
df <- data_frame0(
104+
x = c(0, 1, 2, 1),
105+
y = c(1, 2, 1, 0),
106+
z = c(1, 1, 2, 2)
107+
)
108+
109+
ld <- layer_data(
110+
ggplot(df, aes(x, y, z = z)) + geom_contour(breaks = 1.5)
111+
)
112+
expect_equal(ld$x, c(1.5, 0.5))
113+
expect_equal(ld$y, c(1.5, 0.5))
114+
115+
# Also for unordered data
116+
df <- df[c(1, 4, 2, 3), ]
117+
118+
ld <- layer_data(
119+
ggplot(df, aes(x, y, z = z)) + geom_contour(breaks = 1.5)
120+
)
121+
122+
expect_equal(ld$x, c(0.5, 1.5))
123+
expect_equal(ld$y, c(0.5, 1.5))
124+
})

0 commit comments

Comments
 (0)