Skip to content

Commit 781204d

Browse files
authored
Implement filled 2d density contours (#3864)
* initial implementation idea; still tons of stuff to do * geom_density2d_filled, doesn't work yet * fix geom_density2d_filled() * fix unit test * update usage examples * improve documentation * add news item, allow manual setting of breaks, add nlevel column to contour output * compute density scaled to samle size, just like in geom_density(). * Make `level` numeric for filled contours. Fixes #3875. * Document computed variables. * properly contour in a two-step process * cleanup, better docs * clean-up code, docs * unit tests * add a proper `stat_density_2d_filled()`. Closes #3846. * update news * fix unit tests * keep track of the type of contour needed rather than the stat that does the work.
1 parent 86c6ec1 commit 781204d

File tree

10 files changed

+495
-97
lines changed

10 files changed

+495
-97
lines changed

NAMESPACE

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,13 @@ export(GeomBlank)
165165
export(GeomBoxplot)
166166
export(GeomCol)
167167
export(GeomContour)
168+
export(GeomContourFilled)
168169
export(GeomCrossbar)
169170
export(GeomCurve)
170171
export(GeomCustomAnn)
171172
export(GeomDensity)
172173
export(GeomDensity2d)
174+
export(GeomDensity2dFilled)
173175
export(GeomDotplot)
174176
export(GeomErrorbar)
175177
export(GeomErrorbarh)
@@ -231,6 +233,7 @@ export(StatContourFilled)
231233
export(StatCount)
232234
export(StatDensity)
233235
export(StatDensity2d)
236+
export(StatDensity2dFilled)
234237
export(StatEcdf)
235238
export(StatEllipse)
236239
export(StatFunction)
@@ -341,7 +344,9 @@ export(geom_crossbar)
341344
export(geom_curve)
342345
export(geom_density)
343346
export(geom_density2d)
347+
export(geom_density2d_filled)
344348
export(geom_density_2d)
349+
export(geom_density_2d_filled)
345350
export(geom_dotplot)
346351
export(geom_errorbar)
347352
export(geom_errorbarh)
@@ -591,7 +596,9 @@ export(stat_contour_filled)
591596
export(stat_count)
592597
export(stat_density)
593598
export(stat_density2d)
599+
export(stat_density2d_filled)
594600
export(stat_density_2d)
601+
export(stat_density_2d_filled)
595602
export(stat_ecdf)
596603
export(stat_ellipse)
597604
export(stat_function)

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
* `annotation_raster()` adds support for native rasters. For large rasters,
44
native rasters render significantly faster than arrays (@kent37, #3388)
55

6+
* A newly added geom `geom_density_2d_filled()` and associated stat
7+
`stat_density_2d_filled()` can draw filled density contours
8+
(@clauswilke, #3846).
9+
610
* Support graphics devices that use the `file` argument instead of `fileneame`
711
in `ggsave()` (@bwiernik, #3810)
812

R/geom-contour.r

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
#' 2d contours of a 3d surface
1+
#' 2D contours of a 3D surface
22
#'
3-
#' ggplot2 can not draw true 3d surfaces, but you can use `geom_contour`
4-
#' and [geom_tile()] to visualise 3d surfaces in 2d. To be a valid
5-
#' surface, the data must contain only a single row for each unique combination
6-
#' of the variables mapped to the `x` and `y` aesthetics. Contouring
3+
#' ggplot2 can not draw true 3D surfaces, but you can use `geom_contour()`,
4+
#' `geom_contour_filled()`, and [geom_tile()] to visualise 3D surfaces in 2D.
5+
#' To specify a valid surface, the data must contain `x`, `y`, and `z` coordinates,
6+
#' and each unique combination of `x` and `y` can appear exactly once. Contouring
77
#' tends to work best when `x` and `y` form a (roughly) evenly
88
#' spaced grid. If your data is not evenly spaced, you may want to interpolate
9-
#' to a grid before visualising.
9+
#' to a grid before visualising, see [geom_density_2d()].
1010
#'
1111
#' @eval rd_aesthetics("geom", "contour")
12+
#' @eval rd_aesthetics("geom", "contour_filled")
1213
#' @inheritParams layer
1314
#' @inheritParams geom_point
1415
#' @inheritParams geom_path
@@ -20,7 +21,7 @@
2021
#' @seealso [geom_density_2d()]: 2d density contours
2122
#' @export
2223
#' @examples
23-
#' #' # Basic plot
24+
#' # Basic plot
2425
#' v <- ggplot(faithfuld, aes(waiting, eruptions, z = density))
2526
#' v + geom_contour()
2627
#'
@@ -33,7 +34,7 @@
3334
#' v + geom_contour_filled()
3435
#'
3536
#' # Setting bins creates evenly spaced contours in the range of the data
36-
#' v + geom_contour(bins = 2)
37+
#' v + geom_contour(bins = 5)
3738
#' v + geom_contour(bins = 10)
3839
#'
3940
#' # Setting binwidth does the same thing, parameterised by the distance
@@ -95,7 +96,7 @@ geom_contour_filled <- function(mapping = NULL, data = NULL,
9596
data = data,
9697
mapping = mapping,
9798
stat = stat,
98-
geom = GeomPolygon,
99+
geom = GeomContourFilled,
99100
position = position,
100101
show.legend = show.legend,
101102
inherit.aes = inherit.aes,
@@ -123,3 +124,11 @@ GeomContour <- ggproto("GeomContour", GeomPath,
123124
alpha = NA
124125
)
125126
)
127+
128+
#' @rdname ggplot2-ggproto
129+
#' @format NULL
130+
#' @usage NULL
131+
#' @export
132+
#' @include geom-polygon.r
133+
GeomContourFilled <- ggproto("GeomContourFilled", GeomPolygon)
134+

R/geom-density2d.r

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,41 @@
1-
#' Contours of a 2d density estimate
1+
#' Contours of a 2D density estimate
22
#'
33
#' Perform a 2D kernel density estimation using [MASS::kde2d()] and
44
#' display the results with contours. This can be useful for dealing with
5-
#' overplotting. This is a 2d version of [geom_density()].
5+
#' overplotting. This is a 2D version of [geom_density()]. `geom_density_2d()`
6+
#' draws contour lines, and `geom_density_2d_filled()` draws filled contour
7+
#' bands.
68
#'
79
#' @eval rd_aesthetics("geom", "density_2d")
8-
#' @seealso [geom_contour()] for information about how contours
9-
#' are drawn; [geom_bin2d()] for another way of dealing with
10+
#' @eval rd_aesthetics("geom", "density_2d_filled")
11+
#' @seealso [geom_contour()], [geom_contour_filled()] for information about
12+
#' how contours are drawn; [geom_bin2d()] for another way of dealing with
1013
#' overplotting.
1114
#' @param geom,stat Use to override the default connection between
1215
#' `geom_density_2d` and `stat_density_2d`.
1316
#' @inheritParams layer
1417
#' @inheritParams geom_point
1518
#' @inheritParams geom_path
19+
#' @param contour_var Character string identifying the variable to contour
20+
#' by. Can be one of `"density"`, `"ndensity"`, or `"count"`. See the section
21+
#' on computed variables for details.
1622
#' @export
1723
#' @examples
1824
#' m <- ggplot(faithful, aes(x = eruptions, y = waiting)) +
1925
#' geom_point() +
2026
#' xlim(0.5, 6) +
2127
#' ylim(40, 110)
28+
#'
29+
#' # contour lines
2230
#' m + geom_density_2d()
31+
#'
2332
#' \donttest{
24-
#' m + stat_density_2d(aes(fill = after_stat(level)), geom = "polygon")
33+
#' # contour bands
34+
#' m + geom_density_2d_filled(alpha = 0.5)
35+
#'
36+
#' # contour bands and contour lines
37+
#' m + geom_density_2d_filled(alpha = 0.5) +
38+
#' geom_density_2d(size = 0.25, colour = "black")
2539
#'
2640
#' set.seed(4393)
2741
#' dsmall <- diamonds[sample(nrow(diamonds), 1000), ]
@@ -30,23 +44,29 @@
3044
#' # set of contours for each value of that variable
3145
#' d + geom_density_2d(aes(colour = cut))
3246
#'
33-
#' # Similarly, if you apply faceting to the plot, contours will be
34-
#' # drawn for each facet, but the levels will calculated across all facets
35-
#' d + stat_density_2d(aes(fill = after_stat(level)), geom = "polygon") +
36-
#' facet_grid(. ~ cut) + scale_fill_viridis_c()
37-
#' # To override this behavior (for instace, to better visualize the density
38-
#' # within each facet), use after_stat(nlevel)
39-
#' d + stat_density_2d(aes(fill = after_stat(nlevel)), geom = "polygon") +
40-
#' facet_grid(. ~ cut) + scale_fill_viridis_c()
47+
#' # If you draw filled contours across multiple facets, the same bins are
48+
#' # used across all facets
49+
#' d + geom_density_2d_filled() + facet_wrap(vars(cut))
50+
#' # If you want to make sure the peak intensity is the same in each facet,
51+
#' # use `contour_var = "ndensity"`.
52+
#' d + geom_density_2d_filled(contour_var = "ndensity") + facet_wrap(vars(cut))
53+
#' # If you want to scale intensity by the number of observations in each group,
54+
#' # use `contour_var = "count"`.
55+
#' d + geom_density_2d_filled(contour_var = "count") + facet_wrap(vars(cut))
4156
#'
42-
#' # If we turn contouring off, we can use use geoms like tiles:
43-
#' d + stat_density_2d(geom = "raster", aes(fill = after_stat(density)), contour = FALSE)
57+
#' # If we turn contouring off, we can use other geoms, such as tiles:
58+
#' d + stat_density_2d(
59+
#' geom = "raster",
60+
#' aes(fill = after_stat(density)),
61+
#' contour = FALSE
62+
#' ) + scale_fill_viridis_c()
4463
#' # Or points:
4564
#' d + stat_density_2d(geom = "point", aes(size = after_stat(density)), n = 20, contour = FALSE)
4665
#' }
4766
geom_density_2d <- function(mapping = NULL, data = NULL,
48-
stat = "density2d", position = "identity",
67+
stat = "density_2d", position = "identity",
4968
...,
69+
contour_var = "density",
5070
lineend = "butt",
5171
linejoin = "round",
5272
linemitre = 10,
@@ -65,6 +85,8 @@ geom_density_2d <- function(mapping = NULL, data = NULL,
6585
lineend = lineend,
6686
linejoin = linejoin,
6787
linemitre = linemitre,
88+
contour = TRUE,
89+
contour_var = contour_var,
6890
na.rm = na.rm,
6991
...
7092
)
@@ -84,3 +106,42 @@ geom_density2d <- geom_density_2d
84106
GeomDensity2d <- ggproto("GeomDensity2d", GeomPath,
85107
default_aes = aes(colour = "#3366FF", size = 0.5, linetype = 1, alpha = NA)
86108
)
109+
110+
#' @export
111+
#' @rdname geom_density_2d
112+
geom_density_2d_filled <- function(mapping = NULL, data = NULL,
113+
stat = "density_2d_filled", position = "identity",
114+
...,
115+
contour_var = "density",
116+
na.rm = FALSE,
117+
show.legend = NA,
118+
inherit.aes = TRUE) {
119+
layer(
120+
data = data,
121+
mapping = mapping,
122+
stat = stat,
123+
geom = GeomDensity2dFilled,
124+
position = position,
125+
show.legend = show.legend,
126+
inherit.aes = inherit.aes,
127+
params = list(
128+
na.rm = na.rm,
129+
contour = TRUE,
130+
contour_var = contour_var,
131+
...
132+
)
133+
)
134+
}
135+
136+
#' @export
137+
#' @rdname geom_density_2d
138+
#' @usage NULL
139+
geom_density2d_filled <- geom_density_2d_filled
140+
141+
#' @rdname ggplot2-ggproto
142+
#' @format NULL
143+
#' @usage NULL
144+
#' @export
145+
#' @include geom-polygon.r
146+
GeomDensity2dFilled <- ggproto("GeomDensity2dFilled", GeomPolygon)
147+

R/stat-contour.r

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22
#' @inheritParams geom_contour
33
#' @export
44
#' @eval rd_aesthetics("stat", "contour")
5+
#' @eval rd_aesthetics("stat", "contour_filled")
56
#' @section Computed variables:
7+
#' The computed variables differ somewhat for contour lines (computed by
8+
#' `stat_contour()`) and contour bands (filled contours, computed by `stat_contour_filled()`).
9+
#' The variables `nlevel` and `piece` are available for both, whereas `level_low`, `level_high`,
10+
#' and `level_mid` are only available for bands. The variable `level` is a numeric or a factor
11+
#' depending on whether lines or bands are calculated.
612
#' \describe{
7-
#' \item{level}{height of contour}
8-
#' \item{nlevel}{height of contour, scaled to maximum of 1}
9-
#' \item{piece}{contour piece (an integer)}
13+
#' \item{`level`}{Height of contour. For contour lines, this is numeric vector that
14+
#' represents bin boundaries. For contour bands, this is an ordered factor that
15+
#' represents bin ranges.}
16+
#' \item{`level_low`, `level_high`, `level_mid`}{(contour bands only) Lower and upper
17+
#' bin boundaries for each band, as well the mid point between the boundaries.}
18+
#' \item{`nlevel`}{Height of contour, scaled to maximum of 1.}
19+
#' \item{`piece`}{Contour piece (an integer).}
1020
#' }
1121
#' @rdname geom_contour
1222
stat_contour <- function(mapping = NULL, data = NULL,
@@ -39,7 +49,7 @@ stat_contour <- function(mapping = NULL, data = NULL,
3949
#' @rdname geom_contour
4050
#' @export
4151
stat_contour_filled <- function(mapping = NULL, data = NULL,
42-
geom = "polygon", position = "identity",
52+
geom = "contour_filled", position = "identity",
4353
...,
4454
bins = NULL,
4555
binwidth = NULL,
@@ -74,11 +84,15 @@ StatContour <- ggproto("StatContour", Stat,
7484
required_aes = c("x", "y", "z"),
7585
default_aes = aes(order = after_stat(level)),
7686

77-
compute_group = function(data, scales, bins = NULL, binwidth = NULL,
87+
setup_params = function(data, params) {
88+
params$z.range <- range(data$z, na.rm = TRUE, finite = TRUE)
89+
params
90+
},
91+
92+
compute_group = function(data, scales, z.range, bins = NULL, binwidth = NULL,
7893
breaks = NULL, na.rm = FALSE) {
7994

80-
z_range <- range(data$z, na.rm = TRUE, finite = TRUE)
81-
breaks <- contour_breaks(z_range, bins, binwidth, breaks)
95+
breaks <- contour_breaks(z.range, bins, binwidth, breaks)
8296

8397
isolines <- xyz_to_isolines(data, breaks)
8498
path_df <- iso_to_path(isolines, data$group[1])
@@ -99,16 +113,23 @@ StatContourFilled <- ggproto("StatContourFilled", Stat,
99113
required_aes = c("x", "y", "z"),
100114
default_aes = aes(order = after_stat(level), fill = after_stat(level)),
101115

102-
compute_group = function(data, scales, bins = NULL, binwidth = NULL, breaks = NULL, na.rm = FALSE) {
116+
setup_params = function(data, params) {
117+
params$z.range <- range(data$z, na.rm = TRUE, finite = TRUE)
118+
params
119+
},
103120

104-
z_range <- range(data$z, na.rm = TRUE, finite = TRUE)
105-
breaks <- contour_breaks(z_range, bins, binwidth, breaks)
121+
compute_group = function(data, scales, z.range, bins = NULL, binwidth = NULL, breaks = NULL, na.rm = FALSE) {
122+
breaks <- contour_breaks(z.range, bins, binwidth, breaks)
106123

107124
isobands <- xyz_to_isobands(data, breaks)
108125
names(isobands) <- pretty_isoband_levels(names(isobands))
109126
path_df <- iso_to_polygon(isobands, data$group[1])
110127

111128
path_df$level <- ordered(path_df$level, levels = names(isobands))
129+
path_df$level_low <- breaks[as.numeric(path_df$level)]
130+
path_df$level_high <- breaks[as.numeric(path_df$level) + 1]
131+
path_df$level_mid <- 0.5*(path_df$level_low + path_df$level_high)
132+
path_df$nlevel <- rescale_max(path_df$level_high)
112133

113134
path_df
114135
}

0 commit comments

Comments
 (0)