Skip to content

Commit 984503f

Browse files
committed
Fix random seed for geoscatter
1 parent a7cdab8 commit 984503f

File tree

3 files changed

+91
-47
lines changed

3 files changed

+91
-47
lines changed

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
* Updated to use new inset shape definition style from {ggmapinset}.
44
There is no longer a need to override the `configure_inset()` function
55
which was brittle and depended on order of package attachment to work.
6+
* `stat_geoscatter()` and `geom_geoscatter()` now use a fixed seed by
7+
default so that the position of scattered points is reproducible.
8+
The old behaviour can be restored by setting `seed = NA`.
69

710
# ggautomap 0.3.3
811

R/geoscatter.R

Lines changed: 83 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@
1111
#' The \code{location} aesthetic is required.
1212
#' \code{geom_geoscatter()} understands the same aesthetics as [ggplot2::geom_point()].
1313
#'
14-
#' @param sample_type sampling type (see the \code{type} argument of [sf::st_sample()]).
15-
#' \code{"random"} will place points randomly inside the boundaries, whereas
16-
#' \code{"regular"} and \code{"hexagonal"} will evenly space points, leaving
17-
#' a small margin close to the boundaries.
1814
#' @inheritParams ggmapinset::geom_sf_inset
1915
#' @inheritParams stat_geoscatter
2016
#'
@@ -27,7 +23,7 @@
2723
#' cartographer::nc_type_example_2 |>
2824
#' ggplot(aes(location = county)) +
2925
#' geom_boundaries(feature_type = "sf.nc") +
30-
#' geom_geoscatter(aes(colour = type), size = 0.5) +
26+
#' geom_geoscatter(aes(colour = type), size = 0.5, seed = 123) +
3127
#' coord_automap(feature_type = "sf.nc")
3228
geom_geoscatter <- function(
3329
mapping = aes(),
@@ -37,33 +33,28 @@ geom_geoscatter <- function(
3733
...,
3834
feature_type = NA,
3935
sample_type = "random",
36+
seed = 12345,
4037
inset = waiver(),
4138
map_base = "clip",
4239
map_inset = "auto",
4340
na.rm = TRUE,
4441
show.legend = "point",
4542
inherit.aes = TRUE
4643
) {
47-
sample_type <- rlang::arg_match0(
48-
sample_type,
49-
c("random", "regular", "hexagonal")
50-
)
51-
52-
params <- rlang::list2(
53-
feature_type = feature_type,
54-
sample_type = sample_type,
55-
na.rm = na.rm,
56-
...
57-
)
58-
5944
build_sf_inset_layers(
6045
data = data,
6146
mapping = mapping,
6247
stat = stat,
6348
position = position,
6449
show.legend = show.legend,
6550
inherit.aes = inherit.aes,
66-
params = params,
51+
params = rlang::list2(
52+
feature_type = feature_type,
53+
sample_type = sample_type,
54+
na.rm = na.rm,
55+
seed = seed,
56+
...
57+
),
6758
inset = inset,
6859
map_base = map_base,
6960
map_inset = map_inset
@@ -82,6 +73,8 @@ geom_geoscatter <- function(
8273
#' \code{"random"} will place points randomly inside the boundaries, whereas
8374
#' \code{"regular"} and \code{"hexagonal"} will evenly space points, leaving
8475
#' a small margin close to the boundaries.
76+
#' @param seed random seed, used when `sample_type` is `"random"`.
77+
#' When `NA`, the global seed, if any, is used instead of a fixed seed.
8578
#' @param mapping,data,stat,geom,position,na.rm,show.legend,inherit.aes,... See [ggplot2::geom_sf()].
8679
#' @inheritParams cartographer::resolve_feature_type
8780
#'
@@ -94,14 +87,10 @@ stat_geoscatter <- function(
9487
...,
9588
feature_type = NA,
9689
sample_type = "random",
90+
seed = 12345,
9791
show.legend = NA,
9892
inherit.aes = TRUE
9993
) {
100-
sample_type <- rlang::arg_match0(
101-
sample_type,
102-
c("random", "regular", "hexagonal")
103-
)
104-
10594
ggplot2::layer_sf(
10695
data = data,
10796
mapping = mapping,
@@ -113,6 +102,7 @@ stat_geoscatter <- function(
113102
params = rlang::list2(
114103
feature_type = feature_type,
115104
sample_type = sample_type,
105+
seed = seed,
116106
...
117107
)
118108
)
@@ -124,40 +114,87 @@ stat_geoscatter <- function(
124114
#'
125115
#' @importFrom rlang .data
126116
#' @export
127-
StatGeoscatter <- ggplot2::ggproto("StatGeoscatter", ggmapinset::StatSfInset,
117+
StatGeoscatter <- ggproto(
118+
"StatGeoscatter",
119+
ggmapinset::StatSfInset,
128120
required_aes = c("location"),
129121

130-
compute_panel = function(data, scales, coord, feature_type = NA, sample_type = "random") {
122+
setup_params = function(data, params) {
123+
params$sample_type <- rlang::arg_match0(
124+
params$sample_type,
125+
c("random", "regular", "hexagonal")
126+
)
127+
128+
params
129+
},
130+
131+
compute_panel = function(
132+
data,
133+
scales,
134+
coord,
135+
feature_type = NA,
136+
sample_type = "random",
137+
seed = NA
138+
) {
139+
if (sample_type == "random" && !is.na(seed)) {
140+
old_seed <- .GlobalEnv$.Random.seed
141+
on.exit(
142+
{
143+
if (!is.null(old_seed)) {
144+
.GlobalEnv$.Random.seed <- old_seed
145+
} else {
146+
rm(".Random.seed", envir = .GlobalEnv)
147+
}
148+
},
149+
add = TRUE
150+
)
151+
set.seed(seed)
152+
}
153+
131154
feature_type <- get_feature_type(feature_type, coord, data$location)
132-
data$location <- cartographer::resolve_feature_names(data$location, feature_type)
155+
data$location <- cartographer::resolve_feature_names(
156+
data$location,
157+
feature_type
158+
)
133159

134160
data$ggautomap__row <- seq_len(nrow(data))
135161

136-
coords <- dplyr::group_modify(dplyr::group_by(data, .data$location), function(dat, grp) {
137-
geom <- cartographer::map_sfc(grp$location[[1]], feature_type)
162+
coords <- dplyr::group_modify(
163+
dplyr::group_by(data, .data$location),
164+
function(dat, grp) {
165+
geom <- cartographer::map_sfc(grp$location[[1]], feature_type)
138166

139-
# work in a CRS that isn't distorted near the middle of the map
140-
crs_orig <- sf::st_crs(geom)
141-
crs_working <- crs_eqc_midpoint(feature_type)
142-
geom <- sf::st_transform(geom, crs = crs_working)
167+
# work in a CRS that isn't distorted near the middle of the map
168+
crs_orig <- sf::st_crs(geom)
169+
crs_working <- crs_eqc_midpoint(feature_type)
170+
geom <- sf::st_transform(geom, crs = crs_working)
143171

144-
# shrink the geom a little so we don't get points near boundaries
145-
bbox <- sf::st_bbox(geom)
146-
geom <- sf::st_buffer(geom, dist = -0.05 * min(bbox[[3]] - bbox[[1]], bbox[[4]] - bbox[[2]]))
172+
# shrink the geom a little so we don't get points near boundaries
173+
bbox <- sf::st_bbox(geom)
174+
geom <- sf::st_buffer(
175+
geom,
176+
dist = -0.05 * min(bbox[[3]] - bbox[[1]], bbox[[4]] - bbox[[2]])
177+
)
147178

148-
size <- nrow(dat)
149-
if (sample_type != "random") {
150-
size <- as.integer(size * 1.3)
151-
}
179+
size <- nrow(dat)
180+
if (sample_type != "random") {
181+
size <- as.integer(size * 1.3)
182+
}
152183

153-
# FIXME ... additional arguments passed to [sf::st_sample()].
154-
points <- sf::st_sample(geom, size = size, type = sample_type, exact = TRUE)
155-
points <- sf::st_transform(points, crs = crs_orig)
156-
# FIXME: it's still possible for non-random modes to generate too few points
184+
# FIXME ... additional arguments passed to [sf::st_sample()].
185+
points <- sf::st_sample(
186+
geom,
187+
size = size,
188+
type = sample_type,
189+
exact = TRUE
190+
)
191+
points <- sf::st_transform(points, crs = crs_orig)
192+
# FIXME: it's still possible for non-random modes to generate too few points
157193

158-
dat$geometry <- points
159-
dat
160-
})
194+
dat$geometry <- points
195+
dat
196+
}
197+
)
161198
coords <- dplyr::ungroup(coords)
162199
coords <- dplyr::arrange(coords, .data$ggautomap__row)
163200
coords <- coords[, setdiff(names(coords), "ggautomap__row")]

man/geoscatter.Rd

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)