Skip to content

Commit cac3a95

Browse files
yutannihilationclauswilke
authored andcommitted
Implement geom_sf_label() and geom_sf_text() (#2761)
Closes #2742 * Add geom_sf_label() and geom_sf_text() * Add a missing parenthesis * Add tests for stat_sf_coordinates() * Fix a typo * Fix a typo in examples * Add visual tests for geom_sf_label() and geom_sf_text() * Match args for stat_sf_coordinates() and StatSfCoordinates$compute_group() * Document stat_sf_coordinates()'s na.rm * Add documents about stat_sf_coordinats() * Set the default of fun.geometry to NULL When sf package is not installed, test-function-args fails. * Set more fun.geometry to NULL * Fix mistakenly passed sf::point_on_surface * Stop cross-referencing sf functions * Ignore Z and M dimension * Fix the example of stat_sf_coordinates() * Remove Rplot001.png * Fix doc of stat_sf_coordinates() * Fix an example of stat_sf_coordinates() * Fix default fun.geometry and tests * Fix a typo in doc * Add reference images for vdiffr tests * Disable stat-sf-coordinates test * Fix "texts" to "text", and move seealso * Add () to functions in ggsf.Rd for consitency * Add a news bullet
1 parent 01155ba commit cac3a95

11 files changed

+597
-10
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ Collate:
212212
'stat-qq-line.R'
213213
'stat-qq.r'
214214
'stat-quantile.r'
215+
'stat-sf-coordinates.R'
215216
'stat-smooth-methods.r'
216217
'stat-smooth.r'
217218
'stat-sum.r'

NAMESPACE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ export(StatQq)
217217
export(StatQqLine)
218218
export(StatQuantile)
219219
export(StatSf)
220+
export(StatSfCoordinates)
220221
export(StatSmooth)
221222
export(StatSum)
222223
export(StatSummary)
@@ -335,6 +336,8 @@ export(geom_ribbon)
335336
export(geom_rug)
336337
export(geom_segment)
337338
export(geom_sf)
339+
export(geom_sf_label)
340+
export(geom_sf_text)
338341
export(geom_smooth)
339342
export(geom_spoke)
340343
export(geom_step)
@@ -527,6 +530,7 @@ export(stat_qq)
527530
export(stat_qq_line)
528531
export(stat_quantile)
529532
export(stat_sf)
533+
export(stat_sf_coordinates)
530534
export(stat_smooth)
531535
export(stat_spoke)
532536
export(stat_sum)

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
is now always internally converted to "colour", even when part of a longer
4242
aesthetic name (e.g., `point_color`) (@clauswilke, #2649).
4343

44+
* New `geom_sf_label()` and `geom_sf_text()` draw labels and text on sf objects.
45+
Under the hood, new `stat_sf_coordinates()` calculates the x and y from the
46+
coordinates of the geometries. You can customize the calculation method via
47+
`fun.geometry` argument (@yutannihilation, #2761).
48+
4449
# ggplot2 3.0.0
4550

4651
## Breaking changes

R/sf.R

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
#' Visualise sf objects
22
#'
33
#' This set of geom, stat, and coord are used to visualise simple feature (sf)
4-
#' objects. For simple plots, you will only need `geom_sf` as it
5-
#' uses `stat_sf` and adds `coord_sf` for you. `geom_sf` is
4+
#' objects. For simple plots, you will only need `geom_sf()` as it
5+
#' uses `stat_sf()` and adds `coord_sf()` for you. `geom_sf()` is
66
#' an unusual geom because it will draw different geometric objects depending
77
#' on what simple features are present in the data: you can get points, lines,
88
#' or polygons.
9+
#' For text and labels, you can use `geom_sf_text()` and `geom_sf_label()`.
910
#'
1011
#' @section Geometry aesthetic:
11-
#' `geom_sf` uses a unique aesthetic: `geometry`, giving an
12+
#' `geom_sf()` uses a unique aesthetic: `geometry`, giving an
1213
#' column of class `sfc` containing simple features data. There
1314
#' are three ways to supply the `geometry` aesthetic:
1415
#'
15-
#' - Do nothing: by default `geom_sf` assumes it is stored in
16+
#' - Do nothing: by default `geom_sf()` assumes it is stored in
1617
#' the `geometry` column.
1718
#' - Explicitly pass an `sf` object to the `data` argument.
1819
#' This will use the primary geometry column, no matter what it's called.
@@ -23,7 +24,7 @@
2324
#'
2425
#' @section CRS:
2526
#' `coord_sf()` ensures that all layers use a common CRS. You can
26-
#' either specify it using the `CRS` param, or `coord_sf` will
27+
#' either specify it using the `CRS` param, or `coord_sf()` will
2728
#' take it from the first layer that defines a CRS.
2829
#'
2930
#' @param show.legend logical. Should this layer be included in the legends?
@@ -32,6 +33,7 @@
3233
#'
3334
#' You can also set this to one of "polygon", "line", and "point" to
3435
#' override the default legend.
36+
#' @seealso [stat_sf_coordinates()]
3537
#' @examples
3638
#' if (requireNamespace("sf", quietly = TRUE)) {
3739
#' nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)
@@ -70,6 +72,11 @@
7072
#' "+proj=laea +y_0=0 +lon_0=155 +lat_0=-90 +ellps=WGS84 +no_defs"
7173
#' )
7274
#' ggplot() + geom_sf(data = world2)
75+
#'
76+
#' # To add labels, use geom_sf_label().
77+
#' ggplot(nc_3857[1:3, ]) +
78+
#' geom_sf(aes(fill = AREA)) +
79+
#' geom_sf_label(aes(label = NAME))
7380
#' }
7481
#' @name ggsf
7582
NULL
@@ -257,6 +264,114 @@ geom_sf <- function(mapping = aes(), data = NULL, stat = "sf",
257264
)
258265
}
259266

267+
#' @export
268+
#' @rdname ggsf
269+
#' @inheritParams geom_label
270+
#' @inheritParams stat_sf_coordinates
271+
geom_sf_label <- function(mapping = aes(), data = NULL,
272+
stat = "sf_coordinates", position = "identity",
273+
...,
274+
parse = FALSE,
275+
nudge_x = 0,
276+
nudge_y = 0,
277+
label.padding = unit(0.25, "lines"),
278+
label.r = unit(0.15, "lines"),
279+
label.size = 0.25,
280+
na.rm = FALSE,
281+
show.legend = NA,
282+
inherit.aes = TRUE,
283+
fun.geometry = NULL) {
284+
285+
# Automatically determin name of geometry column
286+
if (!is.null(data) && is_sf(data)) {
287+
geometry_col <- attr(data, "sf_column")
288+
} else {
289+
geometry_col <- "geometry"
290+
}
291+
if (is.null(mapping$geometry)) {
292+
mapping$geometry <- as.name(geometry_col)
293+
}
294+
295+
if (!missing(nudge_x) || !missing(nudge_y)) {
296+
if (!missing(position)) {
297+
stop("Specify either `position` or `nudge_x`/`nudge_y`", call. = FALSE)
298+
}
299+
300+
position <- position_nudge(nudge_x, nudge_y)
301+
}
302+
303+
layer(
304+
data = data,
305+
mapping = mapping,
306+
stat = stat,
307+
geom = GeomLabel,
308+
position = position,
309+
show.legend = show.legend,
310+
inherit.aes = inherit.aes,
311+
params = list(
312+
parse = parse,
313+
label.padding = label.padding,
314+
label.r = label.r,
315+
label.size = label.size,
316+
na.rm = na.rm,
317+
fun.geometry = fun.geometry,
318+
...
319+
)
320+
)
321+
}
322+
323+
#' @export
324+
#' @rdname ggsf
325+
#' @inheritParams geom_text
326+
#' @inheritParams stat_sf_coordinates
327+
geom_sf_text <- function(mapping = aes(), data = NULL,
328+
stat = "sf_coordinates", position = "identity",
329+
...,
330+
parse = FALSE,
331+
nudge_x = 0,
332+
nudge_y = 0,
333+
check_overlap = FALSE,
334+
na.rm = FALSE,
335+
show.legend = NA,
336+
inherit.aes = TRUE,
337+
fun.geometry = NULL) {
338+
# Automatically determin name of geometry column
339+
if (!is.null(data) && is_sf(data)) {
340+
geometry_col <- attr(data, "sf_column")
341+
} else {
342+
geometry_col <- "geometry"
343+
}
344+
if (is.null(mapping$geometry)) {
345+
mapping$geometry <- as.name(geometry_col)
346+
}
347+
348+
if (!missing(nudge_x) || !missing(nudge_y)) {
349+
if (!missing(position)) {
350+
stop("Specify either `position` or `nudge_x`/`nudge_y`", call. = FALSE)
351+
}
352+
353+
position <- position_nudge(nudge_x, nudge_y)
354+
}
355+
356+
layer(
357+
data = data,
358+
mapping = mapping,
359+
stat = stat,
360+
geom = GeomText,
361+
position = position,
362+
show.legend = show.legend,
363+
inherit.aes = inherit.aes,
364+
params = list(
365+
parse = parse,
366+
check_overlap = check_overlap,
367+
na.rm = na.rm,
368+
fun.geometry = fun.geometry,
369+
...
370+
)
371+
)
372+
}
373+
374+
260375
#' @export
261376
scale_type.sfc <- function(x) "identity"
262377

R/stat-sf-coordinates.R

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#' Extract coordinates from 'sf' objects
2+
#'
3+
#' `stat_sf_coordinates()` extracts the coordinates from 'sf' objects and
4+
#' summarises them to one pair of coordinates (x and y) per geometry. This is
5+
#' convenient when you draw an sf object as geoms like text and labels (so
6+
#' [geom_sf_text()] and [geom_sf_label()] relies on this).
7+
#'
8+
#' @rdname stat_sf_coordinates
9+
#' @details
10+
#' coordinates of an `sf` object can be retrieved by `sf::st_coordinates()`.
11+
#' But, we cannot simply use `sf::st_coordinates()` because, whereas text and
12+
#' labels require exactly one coordinate per geometry, it returns multiple ones
13+
#' for a polygon or a line. Thus, these two steps are needed:
14+
#'
15+
#' 1. Choose one point per geometry by some function like `sf::st_centroid()`
16+
#' or `sf::st_point_on_surface()`.
17+
#' 2. Retrieve coordinates from the points by `sf::st_coordinates()`.
18+
#'
19+
#' For the first step, you can use an arbitrary function via `fun.geometry`.
20+
#' By default, `function(x) sf::st_point_on_surface(sf::st_zm(x))` is used;
21+
#' `sf::st_point_on_surface()` seems more appropriate than `sf::st_centroid()`
22+
#' since lables and text usually are intended to be put within the polygon or
23+
#' the line. `sf::st_zm()` is needed to drop Z and M dimension beforehand,
24+
#' otherwise `sf::st_point_on_surface()` may fail when the geometries have M
25+
#' dimension.
26+
#'
27+
#' @section Computed variables:
28+
#' \describe{
29+
#' \item{x}{X dimension of the simple feature}
30+
#' \item{y}{Y dimension of the simple feature}
31+
#' }
32+
#'
33+
#' @examples
34+
#' if (requireNamespace("sf", quietly = TRUE)) {
35+
#' nc <- sf::st_read(system.file("shape/nc.shp", package="sf"))
36+
#'
37+
#' ggplot(nc) +
38+
#' stat_sf_coordinates()
39+
#'
40+
#' ggplot(nc) +
41+
#' geom_errorbarh(
42+
#' aes(geometry = geometry,
43+
#' xmin = stat(x) - 0.1,
44+
#' xmax = stat(x) + 0.1,
45+
#' y = stat(y),
46+
#' height = 0.04),
47+
#' stat = "sf_coordinates"
48+
#' )
49+
#' }
50+
#'
51+
#' @export
52+
#' @inheritParams stat_identity
53+
#' @inheritParams geom_point
54+
#' @param fun.geometry
55+
#' A function that takes a `sfc` object and returns a `sfc_POINT` with the
56+
#' same length as the input. If `NULL`, `function(x) sf::st_point_on_surface(sf::st_zm(x))`
57+
#' will be used. Note that the function may warn about the incorrectness of
58+
#' the result if the data is not projected, but you can ignore this except
59+
#' when you really care about the exact locations.
60+
stat_sf_coordinates <- function(mapping = aes(), data = NULL, geom = "point",
61+
position = "identity", na.rm = FALSE,
62+
show.legend = NA, inherit.aes = TRUE,
63+
fun.geometry = NULL,
64+
...) {
65+
# Automatically determin name of geometry column
66+
if (!is.null(data) && is_sf(data)) {
67+
geometry_col <- attr(data, "sf_column")
68+
} else {
69+
geometry_col <- "geometry"
70+
}
71+
if (is.null(mapping$geometry)) {
72+
mapping$geometry <- as.name(geometry_col)
73+
}
74+
75+
layer(
76+
stat = StatSfCoordinates,
77+
data = data,
78+
mapping = mapping,
79+
geom = geom,
80+
position = position,
81+
show.legend = show.legend,
82+
inherit.aes = inherit.aes,
83+
params = list(
84+
na.rm = na.rm,
85+
fun.geometry = fun.geometry,
86+
...
87+
)
88+
)
89+
}
90+
91+
#' @rdname stat_sf_coordinates
92+
#' @usage NULL
93+
#' @format NULL
94+
#' @export
95+
StatSfCoordinates <- ggproto(
96+
"StatSfCoordinates", Stat,
97+
compute_group = function(data, scales, fun.geometry = NULL) {
98+
if (is.null(fun.geometry)) {
99+
fun.geometry <- function(x) sf::st_point_on_surface(sf::st_zm(x))
100+
}
101+
102+
points_sfc <- fun.geometry(data$geometry)
103+
coordinates <- sf::st_coordinates(points_sfc)
104+
data$x <- coordinates[, "X"]
105+
data$y <- coordinates[, "Y"]
106+
107+
data
108+
},
109+
110+
default_aes = aes(x = stat(x), y = stat(y)),
111+
required_aes = c("geometry")
112+
)

0 commit comments

Comments
 (0)