Skip to content

Commit 40e8b60

Browse files
authored
Handle NA gracefully in geom_sf (#3546)
1 parent 528a374 commit 40e8b60

File tree

3 files changed

+52
-35
lines changed

3 files changed

+52
-35
lines changed

NEWS.md

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

3+
* `geom_sf()` now removes rows that can't be plotted due to `NA` aesthetics
4+
(#3546, @thomasp85)
5+
36
* A new scale type has been added, that allows binning of aesthetics at the
47
scale level. It has versions for both position and non-position aesthetics and
58
comes with two new guides (`guide_bins` and `guide_coloursteps`) (@thomasp85, #3096)
@@ -82,8 +85,6 @@
8285

8386
* `stat_density2d()` can now take an `adjust` parameter to scale the default bandwidth. (#2860, @haleyjeppson)
8487

85-
* `geom_sf()` now removes rows that contain missing `shape`/`size`/`colour` (#3483, @yutannihilation)
86-
8788
* Fix a bug when `show.legend` is a named logical vector (#3461, @yutannihilation).
8889

8990
* Increase the default `nbin` of `guide_colourbar()` to place the ticks more precisely (#3508, @yutannihilation).

R/geom-sf.R

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -86,28 +86,27 @@ NULL
8686
#' @usage NULL
8787
#' @format NULL
8888
GeomSf <- ggproto("GeomSf", Geom,
89-
required_aes = "geometry",
90-
default_aes = aes(
91-
shape = NULL,
92-
colour = NULL,
93-
fill = NULL,
94-
size = NULL,
95-
linetype = 1,
96-
alpha = NA,
97-
stroke = 0.5
98-
),
89+
required_aes = "geometry",
90+
default_aes = aes(
91+
shape = NULL,
92+
colour = NULL,
93+
fill = NULL,
94+
size = NULL,
95+
linetype = 1,
96+
alpha = NA,
97+
stroke = 0.5
98+
),
9999

100-
non_missing_aes = c("size", "shape", "colour"),
101-
102100
draw_panel = function(data, panel_params, coord, legend = NULL,
103-
lineend = "butt", linejoin = "round", linemitre = 10) {
101+
lineend = "butt", linejoin = "round", linemitre = 10,
102+
na.rm = TRUE) {
104103
if (!inherits(coord, "CoordSf")) {
105104
stop("geom_sf() must be used with coord_sf()", call. = FALSE)
106105
}
107106

108107
# Need to refactor this to generate one grob per geometry type
109108
coord <- coord$transform(data, panel_params)
110-
sf_grob(coord, lineend = lineend, linejoin = linejoin, linemitre = linemitre)
109+
sf_grob(coord, lineend = lineend, linejoin = linejoin, linemitre = linemitre, na.rm = na.rm)
111110
},
112111

113112
draw_key = function(data, params, size) {
@@ -132,17 +131,36 @@ default_aesthetics <- function(type) {
132131
}
133132
}
134133

135-
sf_grob <- function(x, lineend = "butt", linejoin = "round", linemitre = 10) {
136-
# Need to extract geometry out of corresponding list column
137-
geometry <- x$geometry
138-
type <- sf_types[sf::st_geometry_type(geometry)]
139-
is_point <- type %in% "point"
140-
type_ind <- match(type, c("point", "line", "other"))
134+
sf_grob <- function(x, lineend = "butt", linejoin = "round", linemitre = 10, na.rm = TRUE) {
135+
type <- sf_types[sf::st_geometry_type(x$geometry)]
136+
is_point <- type == "point"
137+
is_line <- type == "line"
138+
is_other <- type == "other"
139+
is_collection <- type == "collection"
140+
type_ind <- match(type, c("point", "line", "other", "collection"))
141+
remove <- rep_len(FALSE, nrow(x))
142+
remove[is_point] <- detect_missing(x, c(GeomPoint$required_aes, GeomPoint$non_missing_aes))[is_point]
143+
remove[is_line] <- detect_missing(x, c(GeomPath$required_aes, GeomPath$non_missing_aes))[is_line]
144+
remove[is_other] <- detect_missing(x, c(GeomPolygon$required_aes, GeomPolygon$non_missing_aes))[is_other]
145+
if (any(remove)) {
146+
if (!na.rm) {
147+
warning_wrap(
148+
"Removed ", sum(remove), " rows containing missing values (geom_sf)."
149+
)
150+
}
151+
x <- x[!remove, , drop = FALSE]
152+
type_ind <- type_ind[!remove]
153+
is_collection <- is_collection[!remove]
154+
}
141155
defaults <- list(
142156
GeomPoint$default_aes,
143157
GeomLine$default_aes,
144158
modify_list(GeomPolygon$default_aes, list(fill = "grey90", colour = "grey35"))
145159
)
160+
defaults[[4]] <- modify_list(
161+
defaults[[3]],
162+
rename(GeomPoint$default_aes, c(size = "point_size", fill = "point_fill"))
163+
)
146164
default_names <- unique(unlist(lapply(defaults, names)))
147165
defaults <- lapply(setNames(default_names, default_names), function(n) {
148166
unlist(lapply(defaults, function(def) def[[n]] %||% NA))
@@ -153,16 +171,17 @@ sf_grob <- function(x, lineend = "butt", linejoin = "round", linemitre = 10) {
153171
fill <- x$fill %||% defaults$fill[type_ind]
154172
fill <- alpha(fill, alpha)
155173
size <- x$size %||% defaults$size[type_ind]
174+
point_size <- ifelse(is_collection, x$size %||% defaults$point_size[type_ind], size)
156175
stroke <- (x$stroke %||% defaults$stroke[1]) * .stroke / 2
157-
fontsize <- size * .pt + stroke
176+
fontsize <- point_size * .pt + stroke
158177
lwd <- ifelse(is_point, stroke, size * .pt)
159178
pch <- x$shape %||% defaults$shape[type_ind]
160179
lty <- x$linetype %||% defaults$linetype[type_ind]
161180
gp <- gpar(
162181
col = col, fill = fill, fontsize = fontsize, lwd = lwd, lty = lty,
163182
lineend = lineend, linejoin = linejoin, linemitre = linemitre
164183
)
165-
sf::st_as_grob(geometry, pch = pch, gp = gp)
184+
sf::st_as_grob(x$geometry, pch = pch, gp = gp)
166185
}
167186

168187
#' @export
@@ -280,7 +299,7 @@ geom_sf_text <- function(mapping = aes(), data = NULL,
280299

281300
sf_types <- c(GEOMETRY = "other", POINT = "point", LINESTRING = "line",
282301
POLYGON = "other", MULTIPOINT = "point", MULTILINESTRING = "line",
283-
MULTIPOLYGON = "other", GEOMETRYCOLLECTION = "other",
302+
MULTIPOLYGON = "other", GEOMETRYCOLLECTION = "collection",
284303
CIRCULARSTRING = "line", COMPOUNDCURVE = "other", CURVEPOLYGON = "other",
285304
MULTICURVE = "other", MULTISURFACE = "other", CURVE = "other",
286305
SURFACE = "other", POLYHEDRALSURFACE = "other", TIN = "other",

R/utilities.r

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,20 +97,13 @@ remove_missing <- function(df, na.rm = FALSE, vars = names(df), name = "",
9797
finite = FALSE) {
9898
stopifnot(is.logical(na.rm))
9999

100-
vars <- intersect(vars, names(df))
101-
if (name != "") name <- paste(" (", name, ")", sep = "")
102-
103-
if (finite) {
104-
missing <- !cases(df[, vars, drop = FALSE], is_finite)
105-
str <- "non-finite"
106-
} else {
107-
missing <- !cases(df[, vars, drop = FALSE], is_complete)
108-
str <- "missing"
109-
}
100+
missing <- detect_missing(df, vars, finite)
110101

111102
if (any(missing)) {
112103
df <- df[!missing, ]
113104
if (!na.rm) {
105+
if (name != "") name <- paste(" (", name, ")", sep = "")
106+
str <- if (finite) "non-finite" else "missing"
114107
warning_wrap(
115108
"Removed ", sum(missing), " rows containing ", str, " values", name, "."
116109
)
@@ -119,6 +112,10 @@ remove_missing <- function(df, na.rm = FALSE, vars = names(df), name = "",
119112

120113
df
121114
}
115+
detect_missing <- function(df, vars, finite = FALSE) {
116+
vars <- intersect(vars, names(df))
117+
!cases(df[, vars, drop = FALSE], if (finite) is_finite else is_complete)
118+
}
122119

123120
# Returns a logical vector of same length as nrow(x). If all data on a row
124121
# is finite (not NA, NaN, Inf, or -Inf) return TRUE; otherwise FALSE.

0 commit comments

Comments
 (0)