From 38d4e104f0eb2c63e2f7a570b20947a9c7d9f368 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Fri, 6 Dec 2019 10:48:51 -0600 Subject: [PATCH 01/25] Implement default crs for non-sf objects in coord_sf(). --- R/coord-sf.R | 65 ++++++++++++++++++++++++++++++++++------- R/stat-sf-coordinates.R | 17 ++++++++++- 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index ed41ce4c94..6e63fddb89 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -4,6 +4,9 @@ #' @format NULL CoordSf <- ggproto("CoordSf", CoordCartesian, + # default crs to be used + default_crs = 4326, # default is WGS 84 + # Find the first CRS if not already supplied setup_params = function(self, data) { if (!is.null(self$crs)) { @@ -41,15 +44,20 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, }, transform = function(self, data, panel_params) { + # we need to transform all non-sf data into the correct coordinate system + source_crs <- self$default_crs + target_crs <- panel_params$crs + + # normalize geometry data, it should already be in the correct crs here data[[ geom_column(data) ]] <- sf_rescale01( data[[ geom_column(data) ]], panel_params$x_range, panel_params$y_range ) - # Assume x and y supplied directly already in common CRS + # transform and normalize regular position data data <- transform_position( - data, + sf_transform_xy(data, target_crs, source_crs), function(x) sf_rescale01_x(x, panel_params$x_range), function(x) sf_rescale01_x(x, panel_params$y_range) ) @@ -165,23 +173,33 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, ) }, - backtransform_range = function(panel_params) { - # this does not actually return backtransformed ranges in the general case, needs fixing - warning( - "range backtransformation not implemented in this coord; results may be wrong.", - call. = FALSE - ) - list(x = panel_params$x_range, y = panel_params$y_range) + backtransform_range = function(self, panel_params) { + target_crs <- self$default_crs + source_crs <- panel_params$crs + + x <- panel_params$x_range + y <- panel_params$y_range + data <- list(x = c(x, x), y = c(y, rev(y))) + data <- sf_transform_xy(data, target_crs, source_crs) + list(x = range(data$x), y = range(data$y)) }, range = function(panel_params) { list(x = panel_params$x_range, y = panel_params$y_range) }, - # CoordSf enforces a fixed aspect ratio -> axes cannot be changed freely under faceting is_free = function() FALSE, + # for regular geoms (such as geom_path, geom_polygon, etc.), CoordSf is non-linear + is_linear = function() FALSE, + + distance = function(self, x, y, panel_params) { + d <- self$backtransform_range(panel_params) + max_dist <- dist_euclidean(d$x, d$y) + dist_euclidean(x, y) / max_dist + }, + aspect = function(self, panel_params) { if (isTRUE(sf::st_is_longlat(panel_params$crs))) { # Contributed by @edzer @@ -375,6 +393,26 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, } ) +## helper functions to transform and normalize geometry and position data +# transform position data (columns x and y in a data frame) +sf_transform_xy <- function(data, target_crs, source_crs) { + if (identical(target_crs, source_crs) || + is.null(target_crs) || is.null(source_crs) || is.null(data) || + !all(c("x", "y") %in% names(data))) { + return(data) + } + + sf_data <- sf::st_sfc( + sf::st_multipoint(cbind(data$x, data$y)), + crs = source_crs + ) + sf_data_trans <- sf::st_transform(sf_data, target_crs)[[1]] + data$x <- sf_data_trans[, 1] + data$y <- sf_data_trans[, 2] + data +} + +# normalize geometry data (variable x is geometry column) sf_rescale01 <- function(x, x_range, y_range) { if (is.null(x)) { return(x) @@ -382,13 +420,17 @@ sf_rescale01 <- function(x, x_range, y_range) { sf::st_normalize(x, c(x_range[1], y_range[1], x_range[2], y_range[2])) } + +# normalize position data (variable x is x or y position) sf_rescale01_x <- function(x, range) { (x - range[1]) / diff(range) } + #' @param crs Use this to select a specific coordinate reference system (CRS). #' If not specified, will use the CRS defined in the first layer. +#' @param default_crs TODO: document #' @param datum CRS that provides datum to use when generating graticules #' @param label_axes Character vector or named list of character values #' specifying which graticule lines (meridians or parallels) should be labeled on @@ -417,7 +459,7 @@ sf_rescale01_x <- function(x, range) { #' @export #' @rdname ggsf coord_sf <- function(xlim = NULL, ylim = NULL, expand = TRUE, - crs = NULL, datum = sf::st_crs(4326), + crs = NULL, default_crs = 4326, datum = sf::st_crs(4326), label_graticule = waiver(), label_axes = waiver(), ndiscr = 100, default = FALSE, clip = "on") { @@ -457,6 +499,7 @@ coord_sf <- function(xlim = NULL, ylim = NULL, expand = TRUE, limits = list(x = xlim, y = ylim), datum = datum, crs = crs, + default_crs = default_crs, label_axes = label_axes, label_graticule = label_graticule, ndiscr = ndiscr, diff --git a/R/stat-sf-coordinates.R b/R/stat-sf-coordinates.R index 60cf470f07..07270f476a 100644 --- a/R/stat-sf-coordinates.R +++ b/R/stat-sf-coordinates.R @@ -84,12 +84,27 @@ stat_sf_coordinates <- function(mapping = aes(), data = NULL, geom = "point", #' @export StatSfCoordinates <- ggproto( "StatSfCoordinates", Stat, - compute_group = function(data, scales, fun.geometry = NULL) { + + default_crs = NULL, # if set to null, take default from coord + + compute_layer = function(self, data, params, layout) { + # extract default crs if not set manually + if (is.null(self$default_crs)) { + self$default_crs <- layout$coord$default_crs + } + ggproto_parent(Stat, self)$compute_layer(data, params, layout) + }, + + compute_group = function(self, data, scales, fun.geometry = NULL) { if (is.null(fun.geometry)) { fun.geometry <- function(x) sf::st_point_on_surface(sf::st_zm(x)) } points_sfc <- fun.geometry(data$geometry) + # transform to the coords default crs if possible + if (!(is.null(self$default_crs) || is.na(sf::st_crs(points_sfc)))) { + points_sfc <- sf::st_transform(points_sfc, self$default_crs) + } coordinates <- sf::st_coordinates(points_sfc) data$x <- coordinates[, "X"] data$y <- coordinates[, "Y"] From 64794d1506381f53c82c1299d873ad983323082c Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Fri, 6 Dec 2019 17:45:14 -0600 Subject: [PATCH 02/25] make limits work --- R/coord-sf.R | 39 ++++++++++++++++++++++++++++++++++++--- R/stat-sf.R | 37 +++++++++++++++++++++++++++++++------ 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index 6e63fddb89..9654862e3d 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -43,6 +43,14 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, }) }, + # Allow sf layer to record the bounding boxes of elements + record_bbox = function(self, xmin, xmax, ymin, ymax) { + self$bbox$xmin <- min(self$bbox$xmin, xmin) + self$bbox$xmax <- min(self$bbox$xmax, xmax) + self$bbox$ymin <- min(self$bbox$ymin, ymin) + self$bbox$ymax <- min(self$bbox$ymax, ymax) + }, + transform = function(self, data, panel_params) { # we need to transform all non-sf data into the correct coordinate system source_crs <- self$default_crs @@ -134,11 +142,35 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, }, setup_panel_params = function(self, scale_x, scale_y, params = list()) { - # Bounding box of the data + # expansion factors for scale limits expansion_x <- default_expansion(scale_x, expand = self$expand) - x_range <- expand_limits_scale(scale_x, expansion_x, coord_limits = self$limits$x) expansion_y <- default_expansion(scale_y, expand = self$expand) - y_range <- expand_limits_scale(scale_y, expansion_y, coord_limits = self$limits$y) + + # get scale limits and transform to common crs + scale_xlim <- scale_x$get_limits() + scale_ylim <- scale_y$get_limits() + + source_crs <- self$default_crs + target_crs <- params$crs + scales_bbox <- sf_transform_xy( + list(x = c(scale_xlim, scale_xlim), y = c(scale_ylim, rev(scale_ylim))), + target_crs, source_crs + ) + + # merge scale limits and coord limits + scales_xrange <- c(min(scales_bbox$x, self$bbox$xmin), max(scales_bbox$x, self$bbox$xmax)) + scales_yrange <- c(min(scales_bbox$y, self$bbox$ymin), max(scales_bbox$y, self$bbox$ymax)) + + # calculate final coord limits by putting everything together and applying expansion + coord_limits_x <- self$limits$x %||% c(NA_real_, NA_real_) + coord_limits_y <- self$limits$y %||% c(NA_real_, NA_real_) + + x_range <- expand_limits_continuous( + scales_xrange, expansion_x, coord_limits = coord_limits_x + ) + y_range <- expand_limits_continuous( + scales_yrange, expansion_y, coord_limits = coord_limits_y + ) bbox <- c( x_range[1], y_range[1], x_range[2], y_range[2] @@ -398,6 +430,7 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, sf_transform_xy <- function(data, target_crs, source_crs) { if (identical(target_crs, source_crs) || is.null(target_crs) || is.null(source_crs) || is.null(data) || + is.na(target_crs) || is.na(source_crs) || !all(c("x", "y") %in% names(data))) { return(data) } diff --git a/R/stat-sf.R b/R/stat-sf.R index e35b89bed0..630ab91559 100644 --- a/R/stat-sf.R +++ b/R/stat-sf.R @@ -3,12 +3,37 @@ #' @usage NULL #' @format NULL StatSf <- ggproto("StatSf", Stat, - compute_group = function(data, scales) { - bbox <- sf::st_bbox(data[[ geom_column(data) ]]) - data$xmin <- bbox[["xmin"]] - data$xmax <- bbox[["xmax"]] - data$ymin <- bbox[["ymin"]] - data$ymax <- bbox[["ymax"]] + compute_layer = function(self, data, params, layout) { + # add coord to the params, so it can be forwarded to compute_group() + params$coord <- layout$coord + ggproto_parent(Stat, self)$compute_layer(data, params, layout) + }, + + compute_group = function(data, scales, coord) { + geometry_data <- data[[ geom_column(data) ]] + geometry_crs <- sf::st_crs(geometry_data) + + bbox <- sf::st_bbox(geometry_data) + coord$record_bbox( + xmin = bbox[["xmin"]], xmax = bbox[["xmax"]], + ymin = bbox[["ymin"]], ymax = bbox[["ymax"]] + ) + + # register geometric center of each bbox, to give regular scales + # some indication of where shapes lie + bbox_trans <- sf_transform_xy( + list( + x = 0.5*(bbox[["xmin"]] + bbox[["xmax"]]), + y = 0.5*(bbox[["ymin"]] + bbox[["ymax"]]) + ), + coord$default_crs, + geometry_crs + ) + + data$xmin <- bbox_trans$x + data$xmax <- bbox_trans$x + data$ymin <- bbox_trans$y + data$ymax <- bbox_trans$y data }, From 0a885b8b353d9436b8c3c7522d89c2adbfd95b57 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Fri, 6 Dec 2019 23:38:37 -0600 Subject: [PATCH 03/25] cleanup code, write documentation --- DESCRIPTION | 2 +- R/coord-sf.R | 13 ++++++++++--- R/stat-sf-coordinates.R | 18 ++++++++---------- man/ggsf.Rd | 13 ++++++++++--- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 20143da6b5..966a963fd1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -253,6 +253,6 @@ Collate: 'zxx.r' 'zzz.r' VignetteBuilder: knitr -RoxygenNote: 7.0.1 +RoxygenNote: 7.0.2 Roxygen: list(markdown = TRUE) Encoding: UTF-8 diff --git a/R/coord-sf.R b/R/coord-sf.R index 9654862e3d..c80a22afcb 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -46,9 +46,9 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, # Allow sf layer to record the bounding boxes of elements record_bbox = function(self, xmin, xmax, ymin, ymax) { self$bbox$xmin <- min(self$bbox$xmin, xmin) - self$bbox$xmax <- min(self$bbox$xmax, xmax) + self$bbox$xmax <- max(self$bbox$xmax, xmax) self$bbox$ymin <- min(self$bbox$ymin, ymin) - self$bbox$ymax <- min(self$bbox$ymax, ymax) + self$bbox$ymax <- max(self$bbox$ymax, ymax) }, transform = function(self, data, panel_params) { @@ -463,7 +463,14 @@ sf_rescale01_x <- function(x, range) { #' @param crs Use this to select a specific coordinate reference system (CRS). #' If not specified, will use the CRS defined in the first layer. -#' @param default_crs TODO: document +#' @param default_crs The default CRS to be used for non-sf layers (which +#' don't carry any CRS information). If not specified, this defaults to +#' the World Geodetic System 1984 (WGS84), which means x and y positions +#' are interpreted as longitude and latitude, respectively. The default CRS +#' is also the reference system used to set limits via scales/`xlim()`/`ylim()`. +#' @param xlim,ylim Limits for the x and y axes. These limits are specified +#' in the units of the CRS set via the `crs` argument or, if `crs` is not +#' specified, the CRS of the first layer that has a CRS. #' @param datum CRS that provides datum to use when generating graticules #' @param label_axes Character vector or named list of character values #' specifying which graticule lines (meridians or parallels) should be labeled on diff --git a/R/stat-sf-coordinates.R b/R/stat-sf-coordinates.R index 07270f476a..18411f341a 100644 --- a/R/stat-sf-coordinates.R +++ b/R/stat-sf-coordinates.R @@ -85,25 +85,23 @@ stat_sf_coordinates <- function(mapping = aes(), data = NULL, geom = "point", StatSfCoordinates <- ggproto( "StatSfCoordinates", Stat, - default_crs = NULL, # if set to null, take default from coord - compute_layer = function(self, data, params, layout) { - # extract default crs if not set manually - if (is.null(self$default_crs)) { - self$default_crs <- layout$coord$default_crs - } + # add coord to the params, so it can be forwarded to compute_group() + params$coord <- layout$coord ggproto_parent(Stat, self)$compute_layer(data, params, layout) }, - compute_group = function(self, data, scales, fun.geometry = NULL) { + compute_group = function(self, data, scales, coord, fun.geometry = NULL) { if (is.null(fun.geometry)) { fun.geometry <- function(x) sf::st_point_on_surface(sf::st_zm(x)) } points_sfc <- fun.geometry(data$geometry) - # transform to the coords default crs if possible - if (!(is.null(self$default_crs) || is.na(sf::st_crs(points_sfc)))) { - points_sfc <- sf::st_transform(points_sfc, self$default_crs) + # transform to the coord's default crs if possible + default_crs <- coord$default_crs + if (!(is.null(default_crs) || is.na(default_crs) || + is.na(sf::st_crs(points_sfc)))) { + points_sfc <- sf::st_transform(points_sfc, default_crs) } coordinates <- sf::st_coordinates(points_sfc) data$x <- coordinates[, "X"] diff --git a/man/ggsf.Rd b/man/ggsf.Rd index da03f7f8d3..091dd0cd67 100644 --- a/man/ggsf.Rd +++ b/man/ggsf.Rd @@ -18,6 +18,7 @@ coord_sf( ylim = NULL, expand = TRUE, crs = NULL, + default_crs = 4326, datum = sf::st_crs(4326), label_graticule = waiver(), label_axes = waiver(), @@ -83,9 +84,9 @@ stat_sf( ) } \arguments{ -\item{xlim}{Limits for the x and y axes.} - -\item{ylim}{Limits for the x and y axes.} +\item{xlim, ylim}{Limits for the x and y axes. These limits are specified +in the units of the CRS set via the \code{crs} argument or, if \code{crs} is not +specified, the CRS of the first layer that has a CRS.} \item{expand}{If \code{TRUE}, the default, adds a small expansion factor to the limits to ensure that data and axes don't overlap. If \code{FALSE}, @@ -94,6 +95,12 @@ limits are taken exactly from the data or \code{xlim}/\code{ylim}.} \item{crs}{Use this to select a specific coordinate reference system (CRS). If not specified, will use the CRS defined in the first layer.} +\item{default_crs}{The default CRS to be used for non-sf layers (which +don't carry any CRS information). If not specified, this defaults to +the World Geodetic System 1984 (WGS84), which means x and y positions +are interpreted as longitude and latitude, respectively. The default CRS +is also the reference system used to set limits via scales/\code{xlim()}/\code{ylim()}.} + \item{datum}{CRS that provides datum to use when generating graticules} \item{label_graticule}{Character vector indicating which graticule lines should be labeled From 829b0811ec8debba776ae6c2c20d37f2e16fdaea Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Fri, 6 Dec 2019 23:39:59 -0600 Subject: [PATCH 04/25] more accurately specify CRS --- R/coord-sf.R | 5 +++-- man/ggsf.Rd | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index c80a22afcb..75018a2954 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -5,7 +5,7 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, # default crs to be used - default_crs = 4326, # default is WGS 84 + default_crs = sf::st_crs(4326), # default is WGS 84 # Find the first CRS if not already supplied setup_params = function(self, data) { @@ -499,7 +499,8 @@ sf_rescale01_x <- function(x, range) { #' @export #' @rdname ggsf coord_sf <- function(xlim = NULL, ylim = NULL, expand = TRUE, - crs = NULL, default_crs = 4326, datum = sf::st_crs(4326), + crs = NULL, default_crs = sf::st_crs(4326), + datum = sf::st_crs(4326), label_graticule = waiver(), label_axes = waiver(), ndiscr = 100, default = FALSE, clip = "on") { diff --git a/man/ggsf.Rd b/man/ggsf.Rd index 091dd0cd67..e9dcbdf845 100644 --- a/man/ggsf.Rd +++ b/man/ggsf.Rd @@ -18,7 +18,7 @@ coord_sf( ylim = NULL, expand = TRUE, crs = NULL, - default_crs = 4326, + default_crs = sf::st_crs(4326), datum = sf::st_crs(4326), label_graticule = waiver(), label_axes = waiver(), From b482d7b6546b3fb87b8dd9bc7851476f3a352f88 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Fri, 6 Dec 2019 23:51:19 -0600 Subject: [PATCH 05/25] handle missing or infinite values in sf_transform_xy(). --- R/coord-sf.R | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/R/coord-sf.R b/R/coord-sf.R index 75018a2954..0a380afb6b 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -435,6 +435,13 @@ sf_transform_xy <- function(data, target_crs, source_crs) { return(data) } + # we need to exclude any non-finite values from st_transform + # we replace them with 0 and afterwards with NA + finite_x <- is.finite(data$x) + finite_y <- is.finite(data$y) + data$x[!finite_x] <- 0 + data$y[!finite_y] <- 0 + sf_data <- sf::st_sfc( sf::st_multipoint(cbind(data$x, data$y)), crs = source_crs @@ -442,6 +449,9 @@ sf_transform_xy <- function(data, target_crs, source_crs) { sf_data_trans <- sf::st_transform(sf_data, target_crs)[[1]] data$x <- sf_data_trans[, 1] data$y <- sf_data_trans[, 2] + + data$x[!(finite_x & finite_y)] <- NA + data$y[!(finite_x & finite_y)] <- NA data } From fe3107751bb3d65e6bf78a1df84459b10f2772be Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Sat, 7 Dec 2019 00:23:07 -0600 Subject: [PATCH 06/25] fix package build --- R/coord-sf.R | 4 ++-- man/ggsf.Rd | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index 0a380afb6b..8eedd3ebd5 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -5,7 +5,7 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, # default crs to be used - default_crs = sf::st_crs(4326), # default is WGS 84 + default_crs = 4326, # default is WGS 84 # Find the first CRS if not already supplied setup_params = function(self, data) { @@ -477,7 +477,7 @@ sf_rescale01_x <- function(x, range) { #' don't carry any CRS information). If not specified, this defaults to #' the World Geodetic System 1984 (WGS84), which means x and y positions #' are interpreted as longitude and latitude, respectively. The default CRS -#' is also the reference system used to set limits via scales/`xlim()`/`ylim()`. +#' is also the reference system used to set limits via position scales. #' @param xlim,ylim Limits for the x and y axes. These limits are specified #' in the units of the CRS set via the `crs` argument or, if `crs` is not #' specified, the CRS of the first layer that has a CRS. diff --git a/man/ggsf.Rd b/man/ggsf.Rd index e9dcbdf845..06cf0601a4 100644 --- a/man/ggsf.Rd +++ b/man/ggsf.Rd @@ -99,7 +99,7 @@ If not specified, will use the CRS defined in the first layer.} don't carry any CRS information). If not specified, this defaults to the World Geodetic System 1984 (WGS84), which means x and y positions are interpreted as longitude and latitude, respectively. The default CRS -is also the reference system used to set limits via scales/\code{xlim()}/\code{ylim()}.} +is also the reference system used to set limits via position scales.} \item{datum}{CRS that provides datum to use when generating graticules} From 325a458af778148927cc0f51bf90d5715aa3368f Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Sun, 8 Dec 2019 16:40:41 -0600 Subject: [PATCH 07/25] properly reset bbox at beginning of plot generation --- R/coord-sf.R | 58 ++++++++++++++++++++++++++++------------- R/stat-sf-coordinates.R | 2 +- R/stat-sf.R | 2 +- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index 8eedd3ebd5..c8d7afe2f8 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -4,13 +4,32 @@ #' @format NULL CoordSf <- ggproto("CoordSf", CoordCartesian, - # default crs to be used - default_crs = 4326, # default is WGS 84 + # CoordSf needs to keep track of some parameters + # internally as the plot is built. These are stored + # here. + params = list(), + + get_default_crs = function(self) { + self$default_crs %||% self$params$default_crs + }, - # Find the first CRS if not already supplied setup_params = function(self, data) { + crs <- self$determine_crs(data) + + params <- list( + crs = crs, + default_crs = self$default_crs %||% crs + ) + self$params <- params + + params + }, + + # Helper function for setup_params(), + # finds the first CRS if not already supplied + determine_crs = function(self, data) { if (!is.null(self$crs)) { - return(list(crs = self$crs)) + return(self$crs) } for (layer_data in data) { @@ -23,10 +42,10 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, if (is.na(crs)) next - return(list(crs = crs)) + return(crs) } - list(crs = NULL) + NULL }, # Transform all layers to common CRS (if provided) @@ -45,15 +64,17 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, # Allow sf layer to record the bounding boxes of elements record_bbox = function(self, xmin, xmax, ymin, ymax) { - self$bbox$xmin <- min(self$bbox$xmin, xmin) - self$bbox$xmax <- max(self$bbox$xmax, xmax) - self$bbox$ymin <- min(self$bbox$ymin, ymin) - self$bbox$ymax <- max(self$bbox$ymax, ymax) + bbox <- self$params$bbox + bbox$xmin <- min(bbox$xmin, xmin) + bbox$xmax <- max(bbox$xmax, xmax) + bbox$ymin <- min(bbox$ymin, ymin) + bbox$ymax <- max(bbox$ymax, ymax) + self$params$bbox <- bbox }, transform = function(self, data, panel_params) { # we need to transform all non-sf data into the correct coordinate system - source_crs <- self$default_crs + source_crs <- self$get_default_crs() target_crs <- panel_params$crs # normalize geometry data, it should already be in the correct crs here @@ -150,16 +171,15 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, scale_xlim <- scale_x$get_limits() scale_ylim <- scale_y$get_limits() - source_crs <- self$default_crs - target_crs <- params$crs scales_bbox <- sf_transform_xy( list(x = c(scale_xlim, scale_xlim), y = c(scale_ylim, rev(scale_ylim))), - target_crs, source_crs + params$crs, params$default_crs ) # merge scale limits and coord limits - scales_xrange <- c(min(scales_bbox$x, self$bbox$xmin), max(scales_bbox$x, self$bbox$xmax)) - scales_yrange <- c(min(scales_bbox$y, self$bbox$ymin), max(scales_bbox$y, self$bbox$ymax)) + coord_bbox <- self$params$bbox + scales_xrange <- c(min(scales_bbox$x, coord_bbox$xmin), max(scales_bbox$x, coord_bbox$xmax)) + scales_yrange <- c(min(scales_bbox$y, coord_bbox$ymin), max(scales_bbox$y, coord_bbox$ymax)) # calculate final coord limits by putting everything together and applying expansion coord_limits_x <- self$limits$x %||% c(NA_real_, NA_real_) @@ -200,13 +220,14 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, y_range = y_range, graticule = graticule, crs = params$crs, + default_crs = params$default_crs, label_axes = self$label_axes, label_graticule = self$label_graticule ) }, backtransform_range = function(self, panel_params) { - target_crs <- self$default_crs + target_crs <- panel_params$default_crs source_crs <- panel_params$crs x <- panel_params$x_range @@ -477,7 +498,8 @@ sf_rescale01_x <- function(x, range) { #' don't carry any CRS information). If not specified, this defaults to #' the World Geodetic System 1984 (WGS84), which means x and y positions #' are interpreted as longitude and latitude, respectively. The default CRS -#' is also the reference system used to set limits via position scales. +#' is also the reference system used to set limits via position scales. If +#' set to `NULL`, uses the setting for `crs`. #' @param xlim,ylim Limits for the x and y axes. These limits are specified #' in the units of the CRS set via the `crs` argument or, if `crs` is not #' specified, the CRS of the first layer that has a CRS. diff --git a/R/stat-sf-coordinates.R b/R/stat-sf-coordinates.R index 18411f341a..e763663d4c 100644 --- a/R/stat-sf-coordinates.R +++ b/R/stat-sf-coordinates.R @@ -98,7 +98,7 @@ StatSfCoordinates <- ggproto( points_sfc <- fun.geometry(data$geometry) # transform to the coord's default crs if possible - default_crs <- coord$default_crs + default_crs <- coord$get_default_crs() if (!(is.null(default_crs) || is.na(default_crs) || is.na(sf::st_crs(points_sfc)))) { points_sfc <- sf::st_transform(points_sfc, default_crs) diff --git a/R/stat-sf.R b/R/stat-sf.R index 630ab91559..be34962bf5 100644 --- a/R/stat-sf.R +++ b/R/stat-sf.R @@ -26,7 +26,7 @@ StatSf <- ggproto("StatSf", Stat, x = 0.5*(bbox[["xmin"]] + bbox[["xmax"]]), y = 0.5*(bbox[["ymin"]] + bbox[["ymax"]]) ), - coord$default_crs, + coord$get_default_crs(), geometry_crs ) From 6110b02324a4bdfc5224e5de7c64ea2e5a2d3dc9 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Sun, 8 Dec 2019 16:46:47 -0600 Subject: [PATCH 08/25] cleanup --- R/coord-sf.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index c8d7afe2f8..60335c0171 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -74,7 +74,7 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, transform = function(self, data, panel_params) { # we need to transform all non-sf data into the correct coordinate system - source_crs <- self$get_default_crs() + source_crs <- panel_params$default_crs target_crs <- panel_params$crs # normalize geometry data, it should already be in the correct crs here From c583ceddde33bce461a0f9bee74cd3c5cd4f3b17 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Sun, 8 Dec 2019 18:44:16 -0600 Subject: [PATCH 09/25] check that the coord is of type CoordSf before calling its unique function --- R/stat-sf-coordinates.R | 11 ++++++---- R/stat-sf.R | 48 +++++++++++++++++++++++++---------------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/R/stat-sf-coordinates.R b/R/stat-sf-coordinates.R index e763663d4c..6c7744b1f6 100644 --- a/R/stat-sf-coordinates.R +++ b/R/stat-sf-coordinates.R @@ -97,11 +97,14 @@ StatSfCoordinates <- ggproto( } points_sfc <- fun.geometry(data$geometry) + # transform to the coord's default crs if possible - default_crs <- coord$get_default_crs() - if (!(is.null(default_crs) || is.na(default_crs) || - is.na(sf::st_crs(points_sfc)))) { - points_sfc <- sf::st_transform(points_sfc, default_crs) + if (inherits(coord, "CoordSf")) { + default_crs <- coord$get_default_crs() + if (!(is.null(default_crs) || is.na(default_crs) || + is.na(sf::st_crs(points_sfc)))) { + points_sfc <- sf::st_transform(points_sfc, default_crs) + } } coordinates <- sf::st_coordinates(points_sfc) data$x <- coordinates[, "X"] diff --git a/R/stat-sf.R b/R/stat-sf.R index be34962bf5..ba62a40c1f 100644 --- a/R/stat-sf.R +++ b/R/stat-sf.R @@ -14,26 +14,38 @@ StatSf <- ggproto("StatSf", Stat, geometry_crs <- sf::st_crs(geometry_data) bbox <- sf::st_bbox(geometry_data) - coord$record_bbox( - xmin = bbox[["xmin"]], xmax = bbox[["xmax"]], - ymin = bbox[["ymin"]], ymax = bbox[["ymax"]] - ) - # register geometric center of each bbox, to give regular scales - # some indication of where shapes lie - bbox_trans <- sf_transform_xy( - list( - x = 0.5*(bbox[["xmin"]] + bbox[["xmax"]]), - y = 0.5*(bbox[["ymin"]] + bbox[["ymax"]]) - ), - coord$get_default_crs(), - geometry_crs - ) + if (inherits(coord, "CoordSf")) { + # if the coord derives from CoordSf, then it + # needs to know about bounding boxes of geometry data + coord$record_bbox( + xmin = bbox[["xmin"]], xmax = bbox[["xmax"]], + ymin = bbox[["ymin"]], ymax = bbox[["ymax"]] + ) + + # register geometric center of each bbox, to give regular scales + # some indication of where shapes lie + bbox_trans <- sf_transform_xy( + list( + x = 0.5*(bbox[["xmin"]] + bbox[["xmax"]]), + y = 0.5*(bbox[["ymin"]] + bbox[["ymax"]]) + ), + coord$get_default_crs(), + geometry_crs + ) - data$xmin <- bbox_trans$x - data$xmax <- bbox_trans$x - data$ymin <- bbox_trans$y - data$ymax <- bbox_trans$y + data$xmin <- bbox_trans$x + data$xmax <- bbox_trans$x + data$ymin <- bbox_trans$y + data$ymax <- bbox_trans$y + } else { + # for all other coords, we record the full extent of the + # geometry object + data$xmin <- bbox[["xmin"]] + data$xmax <- bbox[["xmax"]] + data$ymin <- bbox[["ymin"]] + data$ymax <- bbox[["ymax"]] + } data }, From aece1e47473b5652d28964c0edaf661c0c7bc662 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Sat, 14 Dec 2019 18:42:53 -0600 Subject: [PATCH 10/25] scale limit improvements --- R/coord-sf.R | 49 ++++++++++++++++++++++++++++++------------------- R/stat-sf.R | 19 +++++++++++-------- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index 60335c0171..4573e25cb4 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -171,15 +171,32 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, scale_xlim <- scale_x$get_limits() scale_ylim <- scale_y$get_limits() + # we take the mid-point along each side of the scale range scales_bbox <- sf_transform_xy( - list(x = c(scale_xlim, scale_xlim), y = c(scale_ylim, rev(scale_ylim))), + list( + x = c(rep(mean(scale_xlim), 2), scale_xlim), + y = c(scale_ylim, rep(mean(scale_ylim), 2)) + ), params$crs, params$default_crs ) - # merge scale limits and coord limits - coord_bbox <- self$params$bbox - scales_xrange <- c(min(scales_bbox$x, coord_bbox$xmin), max(scales_bbox$x, coord_bbox$xmax)) - scales_yrange <- c(min(scales_bbox$y, coord_bbox$ymin), max(scales_bbox$y, coord_bbox$ymax)) + # merge coord bbox into scale limits if scale limits not explicitly set + if (is.null(scale_x$limits) && is.null(scale_y$limits)) { + coord_bbox <- self$params$bbox + scales_xrange <- range(scales_bbox$x, coord_bbox$xmin, coord_bbox$xmax, na.rm = TRUE) + scales_yrange <- range(scales_bbox$y, coord_bbox$ymin, coord_bbox$ymax, na.rm = TRUE) + } else if (any(!is.finite(scales_bbox$x) | !is.finite(scales_bbox$y))) { + warning( + "Projection of scale limits failed.\n", + "Consider working in projected coordinates by setting `default_crs = NULL` in `coord_sf()`." + ) + coord_bbox <- self$params$bbox + scales_xrange <- c(coord_bbox$xmin, coord_bbox$xmax) + scales_yrange <- c(coord_bbox$ymin, coord_bbox$ymax) + } else { + scales_xrange <- range(scales_bbox$x, na.rm = TRUE) + scales_yrange <- range(scales_bbox$y, na.rm = TRUE) + } # calculate final coord limits by putting everything together and applying expansion coord_limits_x <- self$limits$x %||% c(NA_real_, NA_real_) @@ -447,7 +464,7 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, ) ## helper functions to transform and normalize geometry and position data -# transform position data (columns x and y in a data frame) +# transform position data (columns x and y in a data frame or list) sf_transform_xy <- function(data, target_crs, source_crs) { if (identical(target_crs, source_crs) || is.null(target_crs) || is.null(source_crs) || is.null(data) || @@ -456,23 +473,17 @@ sf_transform_xy <- function(data, target_crs, source_crs) { return(data) } - # we need to exclude any non-finite values from st_transform - # we replace them with 0 and afterwards with NA - finite_x <- is.finite(data$x) - finite_y <- is.finite(data$y) - data$x[!finite_x] <- 0 - data$y[!finite_y] <- 0 - + # by turning the data into a geometry list column of individual points, + # we can make sure that the output length equals the input length, even + # if the transformation fails in some cases sf_data <- sf::st_sfc( - sf::st_multipoint(cbind(data$x, data$y)), + mapply(function(x, y) sf::st_point(c(x, y)), data$x, data$y, SIMPLIFY = FALSE), crs = source_crs ) - sf_data_trans <- sf::st_transform(sf_data, target_crs)[[1]] - data$x <- sf_data_trans[, 1] - data$y <- sf_data_trans[, 2] + sf_data_trans <- sf::st_transform(sf_data, target_crs) + data$x <- vapply(sf_data_trans, function(x) x[1], numeric(1)) + data$y <- vapply(sf_data_trans, function(x) x[2], numeric(1)) - data$x[!(finite_x & finite_y)] <- NA - data$y[!(finite_x & finite_y)] <- NA data } diff --git a/R/stat-sf.R b/R/stat-sf.R index ba62a40c1f..12555763f7 100644 --- a/R/stat-sf.R +++ b/R/stat-sf.R @@ -23,21 +23,24 @@ StatSf <- ggproto("StatSf", Stat, ymin = bbox[["ymin"]], ymax = bbox[["ymax"]] ) - # register geometric center of each bbox, to give regular scales - # some indication of where shapes lie + # to represent the location of the geometry in default coordinates, + # we take the mid-point along each side of the bounding box and + # backtransform bbox_trans <- sf_transform_xy( list( - x = 0.5*(bbox[["xmin"]] + bbox[["xmax"]]), - y = 0.5*(bbox[["ymin"]] + bbox[["ymax"]]) + x = c(rep(0.5*(bbox[["xmin"]] + bbox[["xmax"]]), 2), bbox[["xmin"]], bbox[["xmax"]]), + y = c(bbox[["ymin"]], bbox[["ymax"]], rep(0.5*(bbox[["ymin"]] + bbox[["ymax"]]), 2)) ), coord$get_default_crs(), geometry_crs ) - data$xmin <- bbox_trans$x - data$xmax <- bbox_trans$x - data$ymin <- bbox_trans$y - data$ymax <- bbox_trans$y + # record as xmin, xmax, ymin, ymax so regular scales + # have some indication of where shapes lie + data$xmin <- min(bbox_trans$x) + data$xmax <- max(bbox_trans$x) + data$ymin <- min(bbox_trans$y) + data$ymax <- max(bbox_trans$y) } else { # for all other coords, we record the full extent of the # geometry object From 63a52d77d369b571e933abaf3470e2e3198a1408 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Sat, 14 Dec 2019 19:10:40 -0600 Subject: [PATCH 11/25] Register bounding box even for stat_sf_coordinates. Gives better default limits. --- R/stat-sf-coordinates.R | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/R/stat-sf-coordinates.R b/R/stat-sf-coordinates.R index 6c7744b1f6..a675b5e9a9 100644 --- a/R/stat-sf-coordinates.R +++ b/R/stat-sf-coordinates.R @@ -98,8 +98,16 @@ StatSfCoordinates <- ggproto( points_sfc <- fun.geometry(data$geometry) - # transform to the coord's default crs if possible if (inherits(coord, "CoordSf")) { + # register bounding box if the coord derives from CoordSf + bbox <- sf::st_bbox(points_sfc) + + coord$record_bbox( + xmin = bbox[["xmin"]], xmax = bbox[["xmax"]], + ymin = bbox[["ymin"]], ymax = bbox[["ymax"]] + ) + + # transform to the coord's default crs if possible default_crs <- coord$get_default_crs() if (!(is.null(default_crs) || is.na(default_crs) || is.na(sf::st_crs(points_sfc)))) { From 6831ab28f916aa6e4260a056f0b62fd33234e859 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Sat, 1 Feb 2020 13:52:37 -0800 Subject: [PATCH 12/25] finalize handling of limits, improve documentation --- NAMESPACE | 1 + NEWS.md | 10 ++++ R/annotation-map.r | 68 +++++++++++++++------- R/coord-sf.R | 100 ++++++++++++++++++++++++--------- man/annotation_map.Rd | 68 +++++++++++++++------- man/ggsf.Rd | 38 +++++++++---- man/sf_transform_xy.Rd | 39 +++++++++++++ tests/testthat/test-coord_sf.R | 29 ++++++++++ 8 files changed, 274 insertions(+), 79 deletions(-) create mode 100644 man/sf_transform_xy.Rd diff --git a/NAMESPACE b/NAMESPACE index f5f7149913..7d6b00221a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -574,6 +574,7 @@ export(scale_y_sqrt) export(scale_y_time) export(sec_axis) export(set_last_plot) +export(sf_transform_xy) export(should_stop) export(stage) export(standardise_aes_names) diff --git a/NEWS.md b/NEWS.md index f0ff5e75d0..5032f29194 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,15 @@ # ggplot2 (development version) +* `coord_sf()` now has an argument `default_crs` that specifies the coordinate + reference system (crs) for non-sf layers and scale/coord limits. This argument + defaults to the World Geodetic System 1984 (WGS84), which means x and y positions + are interpreted as longitude and latitude. This is a potentially breaking change + for users who use projected coordinates in non-sf layers or in limits. Setting + `default_crs = NULL` recovers the old behavior. Further, authors of extension + packages implementing `stat_sf()`-like functionality are encouraged to look at the + source code of `stat_sf()`'s `compute_group()` function to see how to provide + scale-limit hints to `coord_sf()` (@clauswilke, #3659). + # ggplot2 3.3.0 * Fix a bug in `geom_raster()` that squeezed the image when it went outside diff --git a/R/annotation-map.r b/R/annotation-map.r index 73d303ec1d..8e05e2c874 100644 --- a/R/annotation-map.r +++ b/R/annotation-map.r @@ -1,34 +1,60 @@ #' @include geom-map.r NULL -#' Annotation: a maps +#' Annotation: a map #' -#' Display a fixed map on a plot. +#' Display a fixed map on a plot. This function predates the [`geom_sf()`] +#' framework and does not work with sf geometry columns as input. However, +#' it can be used in conjunction with `geom_sf()` layers and/or +#' [`coord_sf()`] (see examples). #' -#' @param map data frame representing a map. Most map objects can be -#' converted into the right format by using [fortify()] -#' @param ... other arguments used to modify aesthetics +#' @param map Data frame representing a map. See [`geom_map()`] for +#' details. +#' @param ... Other arguments used to modify visual parameters, such as +#' `colour` or `fill`. #' @export #' @examples -#' if (require("maps")) { -#' usamap <- map_data("state") +#' \dontrun{ +#' if (requireNamespace("maps", quietly = TRUE)) { +#' # location of cities in North Carolina +#' df <- data.frame( +#' name = c("Charlotte", "Raleigh", "Greensboro"), +#' lat = c(35.227, 35.772, 36.073), +#' long = c(-80.843, -78.639, -79.792) +#' ) #' -#' seal.sub <- subset(seals, long > -130 & lat < 45 & lat > 40) -#' ggplot(seal.sub, aes(x = long, y = lat)) + -#' annotation_map(usamap, fill = NA, colour = "grey50") + -#' geom_segment(aes(xend = long + delta_long, yend = lat + delta_lat)) -#' } +#' p <- ggplot(df, aes(x = long, y = lat)) + +#' annotation_map( +#' map_data("state"), +#' fill = "antiquewhite", colour = "darkgrey" +#' ) + +#' geom_point(color = "blue") + +#' geom_text( +#' aes(label = name), +#' hjust = 1.105, vjust = 1.05, color = "blue" +#' ) #' -#' if (require("maps")) { -#' seal2 <- transform(seal.sub, -#' latr = cut(lat, 2), -#' longr = cut(long, 2)) +#' # use without coord_sf() is possible but not recommended +#' p + xlim(-84, -76) + ylim(34, 37.2) #' -#' ggplot(seal2, aes(x = long, y = lat)) + -#' annotation_map(usamap, fill = NA, colour = "grey50") + -#' geom_segment(aes(xend = long + delta_long, yend = lat + delta_lat)) + -#' facet_grid(latr ~ longr, scales = "free", space = "free") -#' } +#' if (requireNamespace("sf", quietly = TRUE)) { +#' # use with coord_sf() for appropriate projection +#' p + +#' coord_sf( +#' crs = st_crs(3347), +#' xlim = c(-84, -76), +#' ylim = c(34, 37.2) +#' ) +#' +#' # you can mix annotation_map() and geom_sf() +#' nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE) +#' p + +#' geom_sf( +#' data = nc, inherit.aes = FALSE, +#' fill = NA, color = "black", size = 0.1 +#' ) + +#' coord_sf(crs = st_crs(3347)) +#' }}} annotation_map <- function(map, ...) { # Get map input into correct form if (!is.data.frame(map)) { diff --git a/R/coord-sf.R b/R/coord-sf.R index cef80c71d6..b8f7ed4781 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -167,11 +167,20 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, expansion_x <- default_expansion(scale_x, expand = self$expand) expansion_y <- default_expansion(scale_y, expand = self$expand) - # get scale limits and transform to common crs + # get scale limits and coord limits and merge together + # coord limits take precedence over scale limits scale_xlim <- scale_x$get_limits() scale_ylim <- scale_y$get_limits() + coord_xlim <- self$limits$x %||% c(NA_real_, NA_real_) + coord_ylim <- self$limits$y %||% c(NA_real_, NA_real_) - # we take the mid-point along each side of the scale range + scale_xlim <- ifelse(is.na(coord_xlim), scale_xlim, coord_xlim) + scale_ylim <- ifelse(is.na(coord_ylim), scale_ylim, coord_ylim) + + # now, transform limits to common crs + # we take the mid-point along each side of the scale range for + # better behavior when box is nonlinear or rotated in projected + # space scales_bbox <- sf_transform_xy( list( x = c(rep(mean(scale_xlim), 2), scale_xlim), @@ -181,12 +190,13 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, ) # merge coord bbox into scale limits if scale limits not explicitly set - if (is.null(scale_x$limits) && is.null(scale_y$limits)) { + if (is.null(self$limits$x) && is.null(self$limits$y) && + is.null(scale_x$limits) && is.null(scale_y$limits)) { coord_bbox <- self$params$bbox scales_xrange <- range(scales_bbox$x, coord_bbox$xmin, coord_bbox$xmax, na.rm = TRUE) scales_yrange <- range(scales_bbox$y, coord_bbox$ymin, coord_bbox$ymax, na.rm = TRUE) } else if (any(!is.finite(scales_bbox$x) | !is.finite(scales_bbox$y))) { - warn("Projection of scale limits failed.\nConsider working in projected coordinates by setting `default_crs = NULL` in `coord_sf()`.") + warn("Projection of x or y limits failed.\nConsider working in projected coordinates by setting `default_crs = NULL` in `coord_sf()`.") coord_bbox <- self$params$bbox scales_xrange <- c(coord_bbox$xmin, coord_bbox$xmax) scales_yrange <- c(coord_bbox$ymin, coord_bbox$ymax) @@ -195,16 +205,9 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, scales_yrange <- range(scales_bbox$y, na.rm = TRUE) } - # calculate final coord limits by putting everything together and applying expansion - coord_limits_x <- self$limits$x %||% c(NA_real_, NA_real_) - coord_limits_y <- self$limits$y %||% c(NA_real_, NA_real_) - - x_range <- expand_limits_continuous( - scales_xrange, expansion_x, coord_limits = coord_limits_x - ) - y_range <- expand_limits_continuous( - scales_yrange, expansion_y, coord_limits = coord_limits_y - ) + # apply coordinate expansion + x_range <- expand_limits_continuous(scales_xrange, expansion_x) + y_range <- expand_limits_continuous(scales_yrange, expansion_y) bbox <- c( x_range[1], y_range[1], x_range[2], y_range[2] @@ -460,8 +463,33 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, } ) -## helper functions to transform and normalize geometry and position data -# transform position data (columns x and y in a data frame or list) +#' Transform spatial position data +#' +#' Helper function that can transform spatial position data (pairs of x, y +#' values) among coordinate systems. +#' +#' @param data Data frame or list containing numerical columns `x` and `y`. +#' @param target_crs,source_crs Target and source coordinate reference systems. +#' If `NULL` or `NA`, the data is not transformed. +#' @return A copy of the input data with `x` and `y` replaced by transformed values. +#' @examples +#' if (requireNamespace("sf", quietly = TRUE)) { +#' # location of cities in NC by long (x) and lat (y) +#' data <- data.frame( +#' city = c("Charlotte", "Raleigh", "Greensboro"), +#' x = c(-80.843, -78.639, -79.792), +#' y = c(35.227, 35.772, 36.073) +#' ) +#' +#' # transform to projected coordinates +#' data_proj <- sf_transform_xy(data, 3347, 4326) +#' data_proj +#' +#' # transform back +#' sf_transform_xy(data_proj, 4326, 3347) +#' } +#' @keywords internal +#' @export sf_transform_xy <- function(data, target_crs, source_crs) { if (identical(target_crs, source_crs) || is.null(target_crs) || is.null(source_crs) || is.null(data) || @@ -484,6 +512,8 @@ sf_transform_xy <- function(data, target_crs, source_crs) { data } +## helper functions to normalize geometry and position data + # normalize geometry data (variable x is geometry column) sf_rescale01 <- function(x, x_range, y_range) { if (is.null(x)) { @@ -499,19 +529,35 @@ sf_rescale01_x <- function(x, range) { } - -#' @param crs Use this to select a specific coordinate reference system (CRS). -#' If not specified, will use the CRS defined in the first layer. +#' @param crs The coordinate reference system (CRS) into which all data should +#' be projected before plotting. If not specified, will use the CRS defined +#' in the first sf layer of the plot. #' @param default_crs The default CRS to be used for non-sf layers (which -#' don't carry any CRS information). If not specified, this defaults to -#' the World Geodetic System 1984 (WGS84), which means x and y positions -#' are interpreted as longitude and latitude, respectively. The default CRS -#' is also the reference system used to set limits via position scales. If -#' set to `NULL`, uses the setting for `crs`. +#' don't carry any CRS information) and scale limits. If not specified, this +#' defaults to the World Geodetic System 1984 (WGS84), which means x and y +#' positions are interpreted as longitude and latitude, respectively. If +#' set to `NULL`, uses the setting for `crs`, which means that then all +#' non-sf layers and scale limits are assumed to be specified in projected +#' coordinates. #' @param xlim,ylim Limits for the x and y axes. These limits are specified -#' in the units of the CRS set via the `crs` argument or, if `crs` is not -#' specified, the CRS of the first layer that has a CRS. -#' @param datum CRS that provides datum to use when generating graticules +#' in the units of the default CRS. To specify limits in projected coordinates, +#' set `default_crs = NULL`. How limit specifications translate into the exact +#' region shown on the plot can be confusing when non-linear or rotated coordinate +#' systems are used. First, limits along one direction (e.g., longitude) are +#' applied at the midpoint of the other direction (e.g., latitude). This principle +#' avoids excessively large limits for rotated coordinate systems but means +#' that sometimes limits need to be expanded a little further if extreme data +#' points are to be included in the final plot region. Second, specifying limits +#' along only one direction can affect the automatically generated limits along the +#' other direction. Therefore, it is best to always specify limits for both x and y. +#' Third, specifying limits via position scales or `xlim()`/`ylim()` is strongly +#' discouraged, as it can result in data points being dropped from the plot even +#' though they would be visible in the final plot region. Finally, specifying limits +#' that cross the international date boundary is not possible with WGS84 as the default +#' crs. All these issues can be avoided by working in projected coordinates, +#' via `default_crs = NULL`, but at the cost of having to provide less intuitive +#' numeric values for the limit parameters. +#' @param datum CRS that provides datum to use when generating graticules. #' @param label_axes Character vector or named list of character values #' specifying which graticule lines (meridians or parallels) should be labeled on #' which side of the plot. Meridians are indicated by `"E"` (for East) and diff --git a/man/annotation_map.Rd b/man/annotation_map.Rd index b093f0be03..468bd4c049 100644 --- a/man/annotation_map.Rd +++ b/man/annotation_map.Rd @@ -2,37 +2,63 @@ % Please edit documentation in R/annotation-map.r \name{annotation_map} \alias{annotation_map} -\title{Annotation: a maps} +\title{Annotation: a map} \usage{ annotation_map(map, ...) } \arguments{ -\item{map}{data frame representing a map. Most map objects can be -converted into the right format by using \code{\link[=fortify]{fortify()}}} +\item{map}{Data frame representing a map. See \code{\link[=geom_map]{geom_map()}} for +details.} -\item{...}{other arguments used to modify aesthetics} +\item{...}{Other arguments used to modify visual parameters, such as +\code{colour} or \code{fill}.} } \description{ -Display a fixed map on a plot. +Display a fixed map on a plot. This function predates the \code{\link[=geom_sf]{geom_sf()}} +framework and does not work with sf geometry columns as input. However, +it can be used in conjunction with \code{geom_sf()} layers and/or +\code{\link[=coord_sf]{coord_sf()}} (see examples). } \examples{ -if (require("maps")) { -usamap <- map_data("state") +\dontrun{ +if (requireNamespace("maps", quietly = TRUE)) { +# location of cities in North Carolina +df <- data.frame( + name = c("Charlotte", "Raleigh", "Greensboro"), + lat = c(35.227, 35.772, 36.073), + long = c(-80.843, -78.639, -79.792) +) -seal.sub <- subset(seals, long > -130 & lat < 45 & lat > 40) -ggplot(seal.sub, aes(x = long, y = lat)) + - annotation_map(usamap, fill = NA, colour = "grey50") + - geom_segment(aes(xend = long + delta_long, yend = lat + delta_lat)) -} +p <- ggplot(df, aes(x = long, y = lat)) + + annotation_map( + map_data("state"), + fill = "antiquewhite", colour = "darkgrey" + ) + + geom_point(color = "blue") + + geom_text( + aes(label = name), + hjust = 1.105, vjust = 1.05, color = "blue" + ) -if (require("maps")) { -seal2 <- transform(seal.sub, - latr = cut(lat, 2), - longr = cut(long, 2)) +# use without coord_sf() is possible but not recommended +p + xlim(-84, -76) + ylim(34, 37.2) -ggplot(seal2, aes(x = long, y = lat)) + - annotation_map(usamap, fill = NA, colour = "grey50") + - geom_segment(aes(xend = long + delta_long, yend = lat + delta_lat)) + - facet_grid(latr ~ longr, scales = "free", space = "free") -} +if (requireNamespace("sf", quietly = TRUE)) { +# use with coord_sf() for appropriate projection +p + + coord_sf( + crs = st_crs(3347), + xlim = c(-84, -76), + ylim = c(34, 37.2) + ) + +# you can mix annotation_map() and geom_sf() +nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE) +p + + geom_sf( + data = nc, inherit.aes = FALSE, + fill = NA, color = "black", size = 0.1 + ) + + coord_sf(crs = st_crs(3347)) +}}} } diff --git a/man/ggsf.Rd b/man/ggsf.Rd index 665ca32130..1a23428e8a 100644 --- a/man/ggsf.Rd +++ b/man/ggsf.Rd @@ -85,23 +85,41 @@ stat_sf( } \arguments{ \item{xlim, ylim}{Limits for the x and y axes. These limits are specified -in the units of the CRS set via the \code{crs} argument or, if \code{crs} is not -specified, the CRS of the first layer that has a CRS.} +in the units of the default CRS. To specify limits in projected coordinates, +set \code{default_crs = NULL}. How limit specifications translate into the exact +region shown on the plot can be confusing when non-linear or rotated coordinate +systems are used. First, limits along one direction (e.g., longitude) are +applied at the midpoint of the other direction (e.g., latitude). This principle +avoids excessively large limits for rotated coordinate systems but means +that sometimes limits need to be expanded a little further if extreme data +points are to be included in the final plot region. Second, specifying limits +along only one direction can affect the automatically generated limits along the +other direction. Therefore, it is best to always specify limits for both x and y. +Third, specifying limits via position scales or \code{xlim()}/\code{ylim()} is strongly +discouraged, as it can result in data points being dropped from the plot even +though they would be visible in the final plot region. Finally, specifying limits +that cross the international date boundary is not possible with WGS84 as the default +crs. All these issues can be avoided by working in projected coordinates, +via \code{default_crs = NULL}, but at the cost of having to provide less intuitive +numeric values for the limit parameters.} \item{expand}{If \code{TRUE}, the default, adds a small expansion factor to the limits to ensure that data and axes don't overlap. If \code{FALSE}, limits are taken exactly from the data or \code{xlim}/\code{ylim}.} -\item{crs}{Use this to select a specific coordinate reference system (CRS). -If not specified, will use the CRS defined in the first layer.} +\item{crs}{The coordinate reference system (CRS) into which all data should +be projected before plotting. If not specified, will use the CRS defined +in the first sf layer of the plot.} \item{default_crs}{The default CRS to be used for non-sf layers (which -don't carry any CRS information). If not specified, this defaults to -the World Geodetic System 1984 (WGS84), which means x and y positions -are interpreted as longitude and latitude, respectively. The default CRS -is also the reference system used to set limits via position scales.} - -\item{datum}{CRS that provides datum to use when generating graticules} +don't carry any CRS information) and scale limits. If not specified, this +defaults to the World Geodetic System 1984 (WGS84), which means x and y +positions are interpreted as longitude and latitude, respectively. If +set to \code{NULL}, uses the setting for \code{crs}, which means that then all +non-sf layers and scale limits are assumed to be specified in projected +coordinates.} + +\item{datum}{CRS that provides datum to use when generating graticules.} \item{label_graticule}{Character vector indicating which graticule lines should be labeled where. Meridians run north-south, and the letters \code{"N"} and \code{"S"} indicate that diff --git a/man/sf_transform_xy.Rd b/man/sf_transform_xy.Rd new file mode 100644 index 0000000000..3db14eb8cb --- /dev/null +++ b/man/sf_transform_xy.Rd @@ -0,0 +1,39 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/coord-sf.R +\name{sf_transform_xy} +\alias{sf_transform_xy} +\title{Transform spatial position data} +\usage{ +sf_transform_xy(data, target_crs, source_crs) +} +\arguments{ +\item{data}{Data frame or list containing numerical columns \code{x} and \code{y}.} + +\item{target_crs, source_crs}{Target and source coordinate reference systems. +If \code{NULL} or \code{NA}, the data is not transformed.} +} +\value{ +A copy of the input data with \code{x} and \code{y} replaced by transformed values. +} +\description{ +Helper function that can transform spatial position data (pairs of x, y +values) among coordinate systems. +} +\examples{ +if (requireNamespace("sf", quietly = TRUE)) { +# location of cities in NC by long (x) and lat (y) +data <- data.frame( + city = c("Charlotte", "Raleigh", "Greensboro"), + x = c(-80.843, -78.639, -79.792), + y = c(35.227, 35.772, 36.073) +) + +# transform to projected coordinates +data_proj <- sf_transform_xy(data, 3347, 4326) +data_proj + +# transform back +sf_transform_xy(data_proj, 4326, 3347) +} +} +\keyword{internal} diff --git a/tests/testthat/test-coord_sf.R b/tests/testthat/test-coord_sf.R index cf4d325b25..86e3689390 100644 --- a/tests/testthat/test-coord_sf.R +++ b/tests/testthat/test-coord_sf.R @@ -204,3 +204,32 @@ test_that("Inf is squished to range", { expect_equal(d[[2]]$x, 0) expect_equal(d[[2]]$y, 1) }) + +test_that("sf_transform_xy() works", { + skip_if_not_installed("sf") + + data <- list( + city = c("Charlotte", "Raleigh", "Greensboro"), + x = c(-80.843, -78.639, -79.792), + y = c(35.227, 35.772, 36.073) + ) + + # no transformation if one crs is missing + out <- sf_transform_xy(data, NULL, 4326) + expect_identical(data, out) + out <- sf_transform_xy(data, 4326, NULL) + expect_identical(data, out) + + # transform to projected coordinates + out <- sf_transform_xy(data, 3347, 4326) + expect_identical(data$city, out$city) # columns other than x, y are not changed + expect_true(all(abs(out$x - c(7275499, 7474260, 7357835)) < 10)) + expect_true(all(abs(out$y - c(-60169, 44384, 57438)) < 10)) + + # transform back + out2 <- sf_transform_xy(out, 4326, 3347) + expect_identical(data$city, out2$city) + expect_true(all(abs(out2$x - data$x) < .01)) + expect_true(all(abs(out2$y - data$y) < .01)) + +}) From b590d643f521f243a369f5cc64146606ebf7af0e Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Sat, 1 Feb 2020 23:34:30 -0600 Subject: [PATCH 13/25] unit tests for new coord_sf() features --- .../figs/coord-sf/default-crs-turned-off.svg | 90 +++++++++++++++++++ .../coord-sf/limits-specified-in-long-lat.svg | 85 ++++++++++++++++++ .../limits-specified-in-projected-coords.svg | 85 ++++++++++++++++++ .../coord-sf/non-sf-geoms-use-long-lat.svg | 90 +++++++++++++++++++ tests/testthat/test-coord_sf.R | 49 ++++++++++ 5 files changed, 399 insertions(+) create mode 100644 tests/figs/coord-sf/default-crs-turned-off.svg create mode 100644 tests/figs/coord-sf/limits-specified-in-long-lat.svg create mode 100644 tests/figs/coord-sf/limits-specified-in-projected-coords.svg create mode 100644 tests/figs/coord-sf/non-sf-geoms-use-long-lat.svg diff --git a/tests/figs/coord-sf/default-crs-turned-off.svg b/tests/figs/coord-sf/default-crs-turned-off.svg new file mode 100644 index 0000000000..a0923bdb7b --- /dev/null +++ b/tests/figs/coord-sf/default-crs-turned-off.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +35 +° +N +36 +° +N +37 +° +N +38 +° +N +39 +° +N +40 +° +N + + + + + + + + + + + +81 +° +W +80 +° +W +79 +° +W +78 +° +W +77 +° +W +x +y +default crs turned off + diff --git a/tests/figs/coord-sf/limits-specified-in-long-lat.svg b/tests/figs/coord-sf/limits-specified-in-long-lat.svg new file mode 100644 index 0000000000..e128364288 --- /dev/null +++ b/tests/figs/coord-sf/limits-specified-in-long-lat.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +37 +° +N +38 +° +N +39 +° +N +40 +° +N +41 +° +N + + + + + + + + + + +81 +° +W +80 +° +W +79 +° +W +78 +° +W +77 +° +W +x +y +limits specified in long-lat + diff --git a/tests/figs/coord-sf/limits-specified-in-projected-coords.svg b/tests/figs/coord-sf/limits-specified-in-projected-coords.svg new file mode 100644 index 0000000000..9a46fdabc0 --- /dev/null +++ b/tests/figs/coord-sf/limits-specified-in-projected-coords.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +37 +° +N +38 +° +N +39 +° +N +40 +° +N +41 +° +N + + + + + + + + + + +81 +° +W +80 +° +W +79 +° +W +78 +° +W +77 +° +W +x +y +limits specified in projected coords + diff --git a/tests/figs/coord-sf/non-sf-geoms-use-long-lat.svg b/tests/figs/coord-sf/non-sf-geoms-use-long-lat.svg new file mode 100644 index 0000000000..ce1f75cb76 --- /dev/null +++ b/tests/figs/coord-sf/non-sf-geoms-use-long-lat.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +35 +° +N +36 +° +N +37 +° +N +38 +° +N +39 +° +N +40 +° +N + + + + + + + + + + + +81 +° +W +80 +° +W +79 +° +W +78 +° +W +77 +° +W +x +y +non-sf geoms use long-lat + diff --git a/tests/testthat/test-coord_sf.R b/tests/testthat/test-coord_sf.R index 86e3689390..b2eac12d2f 100644 --- a/tests/testthat/test-coord_sf.R +++ b/tests/testthat/test-coord_sf.R @@ -205,6 +205,55 @@ test_that("Inf is squished to range", { expect_equal(d[[2]]$y, 1) }) +test_that("default crs works", { + skip_if_not_installed("sf") + + polygon <- sf::st_sfc( + sf::st_polygon(list(matrix(c(-80, -76, -76, -80, -80, 35, 35, 40, 40, 35), ncol = 2))), + crs = 4326 # basic long-lat crs + ) + polygon <- sf::st_transform(polygon, crs = 3347) + + points <- data_frame( + x = c(-80, -80, -76, -76), + y = c(35, 40, 35, 40) + ) + + p <- ggplot(polygon) + geom_sf(fill = NA) + + # projected sf objects can be mixed with regular geoms using non-projected data + expect_doppelganger( + "non-sf geoms use long-lat", + p + geom_point(data = points, aes(x, y)) + ) + + # default crs can be turned off + points_trans <- sf_transform_xy(points, 3347, 4326) + expect_doppelganger( + "default crs turned off", + p + geom_point(data = points_trans, aes(x, y)) + + coord_sf(default_crs = NULL) + ) + + # by default, coord limits are specified in long-lat + expect_doppelganger( + "limits specified in long-lat", + p + geom_point(data = points, aes(x, y)) + + coord_sf(xlim = c(-80.5, -76), ylim = c(36, 41)) + ) + + # when default crs is off, limits are specified in projected coords + lims <- sf_transform_xy( + list(x = c(-80.5, -76, -78.25, -78.25), y = c(38.5, 38.5, 36, 41)), + 3347, 4326 + ) + expect_doppelganger( + "limits specified in projected coords", + p + geom_point(data = points_trans, aes(x, y)) + + coord_sf(xlim = lims$x[1:2], ylim = lims$y[3:4], default_crs = NULL) + ) +}) + test_that("sf_transform_xy() works", { skip_if_not_installed("sf") From edcae201ef82c6159fd14dc3d255e5275ad86332 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Sun, 2 Feb 2020 23:14:53 -0600 Subject: [PATCH 14/25] alternative limit methods --- R/coord-sf.R | 71 ++++++++++++++++++++++++++++++++++++++++------------ man/ggsf.Rd | 22 ++++++++++------ 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index b8f7ed4781..c594197fef 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -9,6 +9,10 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, # here. params = list(), + # the method used to convert limits across nonlinear + # coordinate systems. + lims_method = "cross", + get_default_crs = function(self) { self$default_crs %||% self$params$default_crs }, @@ -178,14 +182,12 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, scale_ylim <- ifelse(is.na(coord_ylim), scale_ylim, coord_ylim) # now, transform limits to common crs - # we take the mid-point along each side of the scale range for - # better behavior when box is nonlinear or rotated in projected - # space - scales_bbox <- sf_transform_xy( - list( - x = c(rep(mean(scale_xlim), 2), scale_xlim), - y = c(scale_ylim, rep(mean(scale_ylim), 2)) - ), + # note: return value is not a true bounding box, but a + # list of x and y values whose max/mins are the bounding + # box + scales_bbox <- calc_limits_bbox( + self$lims_method, + scale_xlim, scale_ylim, params$crs, params$default_crs ) @@ -528,6 +530,36 @@ sf_rescale01_x <- function(x, range) { (x - range[1]) / diff(range) } +# different limits methods +calc_limits_bbox <- function(method, xlim, ylim, crs, default_crs) { + bbox <- switch( + method, + # For method "box", we take the limits and turn them into a + # box. We subdivide the box edges into multiple segments to + # better cover the respective area under non-linear transformation + box = list( + x = c( + rep(xlim[1], 20), seq(xlim[1], xlim[2], length.out = 20), + rep(xlim[2], 20), seq(xlim[2], xlim[1], length.out = 20) + ), + y = c( + seq(ylim[1], ylim[2], length.out = 20), rep(ylim[2], 20), + seq(ylim[2], ylim[1], length.out = 20), rep(ylim[1], 20) + ) + ), + # For method "cross", we take the mid-point along each side of + # the scale range for better behavior when box is nonlinear or + # rotated in projected space + # + # method "cross" is also the default + cross =, + list( + x = c(rep(mean(xlim), 20), seq(xlim[1], xlim[2], length.out = 20)), + y = c(seq(ylim[1], ylim[2], length.out = 20), rep(mean(ylim), 20)) + ) + ) + sf_transform_xy(bbox, crs, default_crs) +} #' @param crs The coordinate reference system (CRS) into which all data should #' be projected before plotting. If not specified, will use the CRS defined @@ -543,11 +575,8 @@ sf_rescale01_x <- function(x, range) { #' in the units of the default CRS. To specify limits in projected coordinates, #' set `default_crs = NULL`. How limit specifications translate into the exact #' region shown on the plot can be confusing when non-linear or rotated coordinate -#' systems are used. First, limits along one direction (e.g., longitude) are -#' applied at the midpoint of the other direction (e.g., latitude). This principle -#' avoids excessively large limits for rotated coordinate systems but means -#' that sometimes limits need to be expanded a little further if extreme data -#' points are to be included in the final plot region. Second, specifying limits +#' systems are used. First, different methods can be preferable under different +#' conditions. See parameter `lims_method` for details. Second, specifying limits #' along only one direction can affect the automatically generated limits along the #' other direction. Therefore, it is best to always specify limits for both x and y. #' Third, specifying limits via position scales or `xlim()`/`ylim()` is strongly @@ -557,6 +586,15 @@ sf_rescale01_x <- function(x, range) { #' crs. All these issues can be avoided by working in projected coordinates, #' via `default_crs = NULL`, but at the cost of having to provide less intuitive #' numeric values for the limit parameters. +#' @param lims_method Two methods are currently implemented, `"cross"` (the default) and +#' `"box"`. For method `"cross"`, limits along one direction (e.g., longitude) are +#' applied at the midpoint of the other direction (e.g., latitude). This method +#' avoids excessively large limits for rotated coordinate systems but means +#' that sometimes limits need to be expanded a little further if extreme data +#' points are to be included in the final plot region. By contrast, for method `"box"`, +#' a box is generated out of the limits along both directions, and then limits in +#' projected coordinates are chosen such that the entire box is visible. This method +#' can yield plot regions that are too large. #' @param datum CRS that provides datum to use when generating graticules. #' @param label_axes Character vector or named list of character values #' specifying which graticule lines (meridians or parallels) should be labeled on @@ -579,8 +617,8 @@ sf_rescale01_x <- function(x, range) { #' are not guaranteed to reside on only one particular side of the plot panel. #' #' This parameter can be used alone or in combination with `label_axes`. -#' @param ndiscr number of segments to use for discretising graticule lines; -#' try increasing this when graticules look unexpected +#' @param ndiscr Number of segments to use for discretising graticule lines; +#' try increasing this number when graticules look incorrect. #' @inheritParams coord_cartesian #' @export #' @rdname ggsf @@ -588,7 +626,7 @@ coord_sf <- function(xlim = NULL, ylim = NULL, expand = TRUE, crs = NULL, default_crs = sf::st_crs(4326), datum = sf::st_crs(4326), label_graticule = waiver(), - label_axes = waiver(), + label_axes = waiver(), lims_method = "cross", ndiscr = 100, default = FALSE, clip = "on") { if (is.waive(label_graticule) && is.waive(label_axes)) { @@ -618,6 +656,7 @@ coord_sf <- function(xlim = NULL, ylim = NULL, expand = TRUE, ggproto(NULL, CoordSf, limits = list(x = xlim, y = ylim), + lims_method = lims_method, datum = datum, crs = crs, default_crs = default_crs, diff --git a/man/ggsf.Rd b/man/ggsf.Rd index 1a23428e8a..6d30d1b199 100644 --- a/man/ggsf.Rd +++ b/man/ggsf.Rd @@ -22,6 +22,7 @@ coord_sf( datum = sf::st_crs(4326), label_graticule = waiver(), label_axes = waiver(), + lims_method = "cross", ndiscr = 100, default = FALSE, clip = "on" @@ -88,11 +89,8 @@ stat_sf( in the units of the default CRS. To specify limits in projected coordinates, set \code{default_crs = NULL}. How limit specifications translate into the exact region shown on the plot can be confusing when non-linear or rotated coordinate -systems are used. First, limits along one direction (e.g., longitude) are -applied at the midpoint of the other direction (e.g., latitude). This principle -avoids excessively large limits for rotated coordinate systems but means -that sometimes limits need to be expanded a little further if extreme data -points are to be included in the final plot region. Second, specifying limits +systems are used. First, different methods can be preferable under different +conditions. See parameter \code{lims_method} for details. Second, specifying limits along only one direction can affect the automatically generated limits along the other direction. Therefore, it is best to always specify limits for both x and y. Third, specifying limits via position scales or \code{xlim()}/\code{ylim()} is strongly @@ -144,8 +142,18 @@ specified with \code{list(bottom = "E", left = "N")}. This parameter can be used alone or in combination with \code{label_graticule}.} -\item{ndiscr}{number of segments to use for discretising graticule lines; -try increasing this when graticules look unexpected} +\item{lims_method}{Two methods are currently implemented, \code{"cross"} (the default) and +\code{"box"}. For method \code{"cross"}, limits along one direction (e.g., longitude) are +applied at the midpoint of the other direction (e.g., latitude). This method +avoids excessively large limits for rotated coordinate systems but means +that sometimes limits need to be expanded a little further if extreme data +points are to be included in the final plot region. By contrast, for method \code{"box"}, +a box is generated out of the limits along both directions, and then limits in +projected coordinates are chosen such that the entire box is visible. This method +can yield plot regions that are too large.} + +\item{ndiscr}{Number of segments to use for discretising graticule lines; +try increasing this number when graticules look incorrect.} \item{default}{Is this the default coordinate system? If \code{FALSE} (the default), then replacing this coordinate system with another one creates a message alerting From 0942ae447bbf1c6ede7d9e3e3c4b881190aab39b Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Sun, 2 Feb 2020 23:49:01 -0600 Subject: [PATCH 15/25] ensure point data is always numeric --- R/coord-sf.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index c594197fef..cc599ed0a7 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -504,7 +504,7 @@ sf_transform_xy <- function(data, target_crs, source_crs) { # we can make sure that the output length equals the input length, even # if the transformation fails in some cases sf_data <- sf::st_sfc( - mapply(function(x, y) sf::st_point(c(x, y)), data$x, data$y, SIMPLIFY = FALSE), + mapply(function(x, y) sf::st_point(as.numeric(c(x, y))), data$x, data$y, SIMPLIFY = FALSE), crs = source_crs ) sf_data_trans <- sf::st_transform(sf_data, target_crs) From c5dd56a1fe88569047e90be417addbd8b39a0f1e Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Thu, 6 Feb 2020 00:23:04 -0600 Subject: [PATCH 16/25] expand documentation --- R/geom-sf.R | 23 +++++++++++++++++++++++ man/ggsf.Rd | 25 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/R/geom-sf.R b/R/geom-sf.R index 56e156f1aa..ac8988fc09 100644 --- a/R/geom-sf.R +++ b/R/geom-sf.R @@ -27,6 +27,29 @@ #' either specify it using the `CRS` param, or `coord_sf()` will #' take it from the first layer that defines a CRS. #' +#' @section Combining sf layers and regular geoms: +#' Most regular geoms, such as [geom_point()], [geom_path()], +#' [geom_text()], [geom_polygon()] etc. will work fine with `coord_sf()`. However +#' when using these geoms, two problems arise. First, what CRS should be used +#' for the x and y coordinates used by these non-sf geoms? The CRS applied to +#' non-sf geoms is set by the `default_crs` parameter, and it defaults to +#' the World Geodetic System 1984 (WGS84). This means that x and y +#' positions are interpreted as longitude and latitude, respectively. You can +#' also specify any other valid CRS as the default CRS for non-sf geoms. Moreover, +#' if you set `default_crs = NULL`, then positions for non-sf geoms are +#' interpreted as projected coordinates. This setting allows you complete control +#' over where exactly items are placed on the plot canvas. +#' +#' The second problem that arises for non-sf geoms is how straight lines +#' should be interpreted in projected space. The approach `coord_sf()` takes is +#' to break straight lines into small pieces (i.e., segmentize them) and +#' then transform the pieces into projected coordinates. For the default setting +#' where x and y are interpreted as longitude and latitude, this approach means +#' that horizontal lines follow the parallels and vertical lines follow the +#' meridians. If you need a different approach to handling straight lines, then +#' you should manually segmentize and project coordinates and generate the plot +#' in projected coordinates. +#' #' @param show.legend logical. Should this layer be included in the legends? #' `NA`, the default, includes if any aesthetics are mapped. #' `FALSE` never includes, and `TRUE` always includes. diff --git a/man/ggsf.Rd b/man/ggsf.Rd index 6d30d1b199..1ece0d4adb 100644 --- a/man/ggsf.Rd +++ b/man/ggsf.Rd @@ -278,6 +278,31 @@ either specify it using the \code{CRS} param, or \code{coord_sf()} will take it from the first layer that defines a CRS. } +\section{Combining sf layers and regular geoms}{ + +Most regular geoms, such as \code{\link[=geom_point]{geom_point()}}, \code{\link[=geom_path]{geom_path()}}, +\code{\link[=geom_text]{geom_text()}}, \code{\link[=geom_polygon]{geom_polygon()}} etc. will work fine with \code{coord_sf()}. However +when using these geoms, two problems arise. First, what CRS should be used +for the x and y coordinates used by these non-sf geoms? The CRS applied to +non-sf geoms is set by the \code{default_crs} parameter, and it defaults to +the World Geodetic System 1984 (WGS84). This means that x and y +positions are interpreted as longitude and latitude, respectively. You can +also specify any other valid CRS as the default CRS for non-sf geoms. Moreover, +if you set \code{default_crs = NULL}, then positions for non-sf geoms are +interpreted as projected coordinates. This setting allows you complete control +over where exactly items are placed on the plot canvas. + +The second problem that arises for non-sf geoms is how straight lines +should be interpreted in projected space. The approach \code{coord_sf()} takes is +to break straight lines into small pieces (i.e., segmentize them) and +then transform the pieces into projected coordinates. For the default setting +where x and y are interpreted as longitude and latitude, this approach means +that horizontal lines follow the parallels and vertical lines follow the +meridians. If you need a different approach to handling straight lines, then +you should manually segmentize and project coordinates and generate the plot +in projected coordinates. +} + \examples{ if (requireNamespace("sf", quietly = TRUE)) { nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE) From 371af57f4ab6f3811479b6a8abba85294ff82321 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Thu, 6 Feb 2020 00:30:00 -0600 Subject: [PATCH 17/25] delete space --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index e9160a28d1..dadbd5b73c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -12,7 +12,7 @@ packages implementing `stat_sf()`-like functionality are encouraged to look at the source code of `stat_sf()`'s `compute_group()` function to see how to provide scale-limit hints to `coord_sf()` (@clauswilke, #3659). - + # ggplot2 3.3.0 * Fix a bug in `geom_raster()` that squeezed the image when it went outside From 98433331298cefd10de3c9b395bf56dbf51a96e2 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Thu, 6 Feb 2020 16:26:30 -0600 Subject: [PATCH 18/25] capitalize crs --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index dadbd5b73c..465e11a22f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,7 +4,7 @@ native rasters render significantly faster than arrays (@kent37, #3388) * `coord_sf()` now has an argument `default_crs` that specifies the coordinate - reference system (crs) for non-sf layers and scale/coord limits. This argument + reference system (CRS) for non-sf layers and scale/coord limits. This argument defaults to the World Geodetic System 1984 (WGS84), which means x and y positions are interpreted as longitude and latitude. This is a potentially breaking change for users who use projected coordinates in non-sf layers or in limits. Setting From 11fa427f8262248351f248f02c2aa403b7295ebe Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Mon, 10 Feb 2020 19:51:52 -0600 Subject: [PATCH 19/25] check against incorrect mapping --- R/coord-sf.R | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index cc599ed0a7..9c20894060 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -532,6 +532,10 @@ sf_rescale01_x <- function(x, range) { # different limits methods calc_limits_bbox <- function(method, xlim, ylim, crs, default_crs) { + if (any(!is.finite(c(xlim, ylim)))) { + abort("Scale limits cannot be mapped onto spatial coordinates. Are you mapping the right data columns to the `x` and/or `y` aesthetics?") + } + bbox <- switch( method, # For method "box", we take the limits and turn them into a @@ -626,7 +630,7 @@ coord_sf <- function(xlim = NULL, ylim = NULL, expand = TRUE, crs = NULL, default_crs = sf::st_crs(4326), datum = sf::st_crs(4326), label_graticule = waiver(), - label_axes = waiver(), lims_method = "cross", + label_axes = waiver(), lims_method = c("cross", "box"), ndiscr = 100, default = FALSE, clip = "on") { if (is.waive(label_graticule) && is.waive(label_axes)) { @@ -656,7 +660,7 @@ coord_sf <- function(xlim = NULL, ylim = NULL, expand = TRUE, ggproto(NULL, CoordSf, limits = list(x = xlim, y = ylim), - lims_method = lims_method, + lims_method = match.arg(lims_method), datum = datum, crs = crs, default_crs = default_crs, From 385e4b9221a6315a1065af5c2a491e7be63b6b2e Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Mon, 10 Feb 2020 20:08:43 -0600 Subject: [PATCH 20/25] update docs --- man/ggsf.Rd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/ggsf.Rd b/man/ggsf.Rd index 1ece0d4adb..bd93d53d08 100644 --- a/man/ggsf.Rd +++ b/man/ggsf.Rd @@ -22,7 +22,7 @@ coord_sf( datum = sf::st_crs(4326), label_graticule = waiver(), label_axes = waiver(), - lims_method = "cross", + lims_method = c("cross", "box"), ndiscr = 100, default = FALSE, clip = "on" From 45b6b38c7aaec19326247c78d1b5b12550ccb6a5 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Mon, 10 Feb 2020 22:05:48 -0600 Subject: [PATCH 21/25] more limits methods --- R/coord-sf.R | 45 ++++++++++++++++++++++++++++++--------------- man/ggsf.Rd | 23 +++++++++++++---------- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index 9c20894060..af1e33a3ea 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -198,10 +198,12 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, scales_xrange <- range(scales_bbox$x, coord_bbox$xmin, coord_bbox$xmax, na.rm = TRUE) scales_yrange <- range(scales_bbox$y, coord_bbox$ymin, coord_bbox$ymax, na.rm = TRUE) } else if (any(!is.finite(scales_bbox$x) | !is.finite(scales_bbox$y))) { - warn("Projection of x or y limits failed.\nConsider working in projected coordinates by setting `default_crs = NULL` in `coord_sf()`.") + if (self$lims_method != "geometry_bbox") { + warn("Projection of x or y limits failed.\nConsider working in projected coordinates by setting `default_crs = NULL` in `coord_sf()`.") + } coord_bbox <- self$params$bbox - scales_xrange <- c(coord_bbox$xmin, coord_bbox$xmax) - scales_yrange <- c(coord_bbox$ymin, coord_bbox$ymax) + scales_xrange <- c(coord_bbox$xmin, coord_bbox$xmax) %||% c(0, 0) + scales_yrange <- c(coord_bbox$ymin, coord_bbox$ymax) %||% c(0, 0) } else { scales_xrange <- range(scales_bbox$x, na.rm = TRUE) scales_yrange <- range(scales_bbox$y, na.rm = TRUE) @@ -551,11 +553,21 @@ calc_limits_bbox <- function(method, xlim, ylim, crs, default_crs) { seq(ylim[2], ylim[1], length.out = 20), rep(ylim[1], 20) ) ), - # For method "cross", we take the mid-point along each side of + # For method "geometry_bbox" we ignore all limits info provided here + geometry_bbox = list( + x = c(NA_real_, NA_real_), + y = c(NA_real_, NA_real_) + ), + # For method "projected" we simply return what we are given + projected = list( + x = xlim, + y = ylim + ), + # For method "cross" we take the mid-point along each side of # the scale range for better behavior when box is nonlinear or # rotated in projected space # - # method "cross" is also the default + # Method "cross" is also the default cross =, list( x = c(rep(mean(xlim), 20), seq(xlim[1], xlim[2], length.out = 20)), @@ -590,15 +602,18 @@ calc_limits_bbox <- function(method, xlim, ylim, crs, default_crs) { #' crs. All these issues can be avoided by working in projected coordinates, #' via `default_crs = NULL`, but at the cost of having to provide less intuitive #' numeric values for the limit parameters. -#' @param lims_method Two methods are currently implemented, `"cross"` (the default) and -#' `"box"`. For method `"cross"`, limits along one direction (e.g., longitude) are -#' applied at the midpoint of the other direction (e.g., latitude). This method -#' avoids excessively large limits for rotated coordinate systems but means -#' that sometimes limits need to be expanded a little further if extreme data -#' points are to be included in the final plot region. By contrast, for method `"box"`, -#' a box is generated out of the limits along both directions, and then limits in -#' projected coordinates are chosen such that the entire box is visible. This method -#' can yield plot regions that are too large. +#' @param lims_method The methods currently implemented include `"cross"` (the default), +#' `"box"`, `"projected"`, and `"geometry_bbox"`. For method `"cross"`, limits along +#' one direction (e.g., longitude) are applied at the midpoint of the other direction (e.g., +#' latitude). This method avoids excessively large limits for rotated coordinate +#' systems but means that sometimes limits need to be expanded a little further +#' if extreme data points are to be included in the final plot region. By contrast, +#' for method `"box"`, a box is generated out of the limits along both directions, +#' and then limits in projected coordinates are chosen such that the entire box is +#' visible. This method can yield plot regions that are too large. Finally, method +#' `"projected"` assumes limits are provided in projected coordinates, and method +#' `"geometry_bbox"` ignores all limit information except the bounding box of the +#' geometry. #' @param datum CRS that provides datum to use when generating graticules. #' @param label_axes Character vector or named list of character values #' specifying which graticule lines (meridians or parallels) should be labeled on @@ -630,7 +645,7 @@ coord_sf <- function(xlim = NULL, ylim = NULL, expand = TRUE, crs = NULL, default_crs = sf::st_crs(4326), datum = sf::st_crs(4326), label_graticule = waiver(), - label_axes = waiver(), lims_method = c("cross", "box"), + label_axes = waiver(), lims_method = c("cross", "box", "projected", "geometry_bbox"), ndiscr = 100, default = FALSE, clip = "on") { if (is.waive(label_graticule) && is.waive(label_axes)) { diff --git a/man/ggsf.Rd b/man/ggsf.Rd index bd93d53d08..78902c30e0 100644 --- a/man/ggsf.Rd +++ b/man/ggsf.Rd @@ -22,7 +22,7 @@ coord_sf( datum = sf::st_crs(4326), label_graticule = waiver(), label_axes = waiver(), - lims_method = c("cross", "box"), + lims_method = c("cross", "box", "projected", "geometry_bbox"), ndiscr = 100, default = FALSE, clip = "on" @@ -142,15 +142,18 @@ specified with \code{list(bottom = "E", left = "N")}. This parameter can be used alone or in combination with \code{label_graticule}.} -\item{lims_method}{Two methods are currently implemented, \code{"cross"} (the default) and -\code{"box"}. For method \code{"cross"}, limits along one direction (e.g., longitude) are -applied at the midpoint of the other direction (e.g., latitude). This method -avoids excessively large limits for rotated coordinate systems but means -that sometimes limits need to be expanded a little further if extreme data -points are to be included in the final plot region. By contrast, for method \code{"box"}, -a box is generated out of the limits along both directions, and then limits in -projected coordinates are chosen such that the entire box is visible. This method -can yield plot regions that are too large.} +\item{lims_method}{The methods currently implemented include \code{"cross"} (the default), +\code{"box"}, \code{"projected"}, and \code{"geometry_bbox"}. For method \code{"cross"}, limits along +one direction (e.g., longitude) are applied at the midpoint of the other direction (e.g., +latitude). This method avoids excessively large limits for rotated coordinate +systems but means that sometimes limits need to be expanded a little further +if extreme data points are to be included in the final plot region. By contrast, +for method \code{"box"}, a box is generated out of the limits along both directions, +and then limits in projected coordinates are chosen such that the entire box is +visible. This method can yield plot regions that are too large. Finally, method +\code{"projected"} assumes limits are provided in projected coordinates, and method +\code{"geometry_bbox"} ignores all limit information except the bounding box of the +geometry.} \item{ndiscr}{Number of segments to use for discretising graticule lines; try increasing this number when graticules look incorrect.} From 47438134f483754c1b2218d5c14d2142603e8ffa Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Mon, 10 Feb 2020 22:17:13 -0600 Subject: [PATCH 22/25] better limits methods --- R/coord-sf.R | 19 +++++++++++++------ man/ggsf.Rd | 6 +++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index af1e33a3ea..787b9d8664 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -558,8 +558,8 @@ calc_limits_bbox <- function(method, xlim, ylim, crs, default_crs) { x = c(NA_real_, NA_real_), y = c(NA_real_, NA_real_) ), - # For method "projected" we simply return what we are given - projected = list( + # For method "orthogonal" we simply return what we are given + orthogonal = list( x = xlim, y = ylim ), @@ -603,7 +603,7 @@ calc_limits_bbox <- function(method, xlim, ylim, crs, default_crs) { #' via `default_crs = NULL`, but at the cost of having to provide less intuitive #' numeric values for the limit parameters. #' @param lims_method The methods currently implemented include `"cross"` (the default), -#' `"box"`, `"projected"`, and `"geometry_bbox"`. For method `"cross"`, limits along +#' `"box"`, `"orthogonal"`, and `"geometry_bbox"`. For method `"cross"`, limits along #' one direction (e.g., longitude) are applied at the midpoint of the other direction (e.g., #' latitude). This method avoids excessively large limits for rotated coordinate #' systems but means that sometimes limits need to be expanded a little further @@ -611,7 +611,7 @@ calc_limits_bbox <- function(method, xlim, ylim, crs, default_crs) { #' for method `"box"`, a box is generated out of the limits along both directions, #' and then limits in projected coordinates are chosen such that the entire box is #' visible. This method can yield plot regions that are too large. Finally, method -#' `"projected"` assumes limits are provided in projected coordinates, and method +#' `"orthogonal"` applies limits separately along each axis, and method #' `"geometry_bbox"` ignores all limit information except the bounding box of the #' geometry. #' @param datum CRS that provides datum to use when generating graticules. @@ -645,7 +645,7 @@ coord_sf <- function(xlim = NULL, ylim = NULL, expand = TRUE, crs = NULL, default_crs = sf::st_crs(4326), datum = sf::st_crs(4326), label_graticule = waiver(), - label_axes = waiver(), lims_method = c("cross", "box", "projected", "geometry_bbox"), + label_axes = waiver(), lims_method = c("cross", "box", "orthogonal", "geometry_bbox"), ndiscr = 100, default = FALSE, clip = "on") { if (is.waive(label_graticule) && is.waive(label_axes)) { @@ -673,9 +673,16 @@ coord_sf <- function(xlim = NULL, ylim = NULL, expand = TRUE, label_graticule <- "" } + # switch limit method to "orthogonal" if not specified and default_crs indicates projected coords + if (is.null(default_crs) && is_missing(lims_method)) { + lims_method <- "orthogonal" + } else { + lims_method <- match.arg(lims_method) + } + ggproto(NULL, CoordSf, limits = list(x = xlim, y = ylim), - lims_method = match.arg(lims_method), + lims_method = lims_method, datum = datum, crs = crs, default_crs = default_crs, diff --git a/man/ggsf.Rd b/man/ggsf.Rd index 78902c30e0..2af9f80f2a 100644 --- a/man/ggsf.Rd +++ b/man/ggsf.Rd @@ -22,7 +22,7 @@ coord_sf( datum = sf::st_crs(4326), label_graticule = waiver(), label_axes = waiver(), - lims_method = c("cross", "box", "projected", "geometry_bbox"), + lims_method = c("cross", "box", "orthogonal", "geometry_bbox"), ndiscr = 100, default = FALSE, clip = "on" @@ -143,7 +143,7 @@ specified with \code{list(bottom = "E", left = "N")}. This parameter can be used alone or in combination with \code{label_graticule}.} \item{lims_method}{The methods currently implemented include \code{"cross"} (the default), -\code{"box"}, \code{"projected"}, and \code{"geometry_bbox"}. For method \code{"cross"}, limits along +\code{"box"}, \code{"orthogonal"}, and \code{"geometry_bbox"}. For method \code{"cross"}, limits along one direction (e.g., longitude) are applied at the midpoint of the other direction (e.g., latitude). This method avoids excessively large limits for rotated coordinate systems but means that sometimes limits need to be expanded a little further @@ -151,7 +151,7 @@ if extreme data points are to be included in the final plot region. By contrast, for method \code{"box"}, a box is generated out of the limits along both directions, and then limits in projected coordinates are chosen such that the entire box is visible. This method can yield plot regions that are too large. Finally, method -\code{"projected"} assumes limits are provided in projected coordinates, and method +\code{"orthogonal"} applies limits separately along each axis, and method \code{"geometry_bbox"} ignores all limit information except the bounding box of the geometry.} From da26d4d61dbef893a9a27277cdc99b5d5d28f48c Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Tue, 11 Feb 2020 22:39:18 -0600 Subject: [PATCH 23/25] simplify error message --- R/coord-sf.R | 23 +++++++++++++---------- man/ggsf.Rd | 21 ++++++++++++--------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index 787b9d8664..6b2cf20e24 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -535,7 +535,7 @@ sf_rescale01_x <- function(x, range) { # different limits methods calc_limits_bbox <- function(method, xlim, ylim, crs, default_crs) { if (any(!is.finite(c(xlim, ylim)))) { - abort("Scale limits cannot be mapped onto spatial coordinates. Are you mapping the right data columns to the `x` and/or `y` aesthetics?") + abort("Scale limits cannot be mapped onto spatial coordinates.") } bbox <- switch( @@ -602,18 +602,21 @@ calc_limits_bbox <- function(method, xlim, ylim, crs, default_crs) { #' crs. All these issues can be avoided by working in projected coordinates, #' via `default_crs = NULL`, but at the cost of having to provide less intuitive #' numeric values for the limit parameters. -#' @param lims_method The methods currently implemented include `"cross"` (the default), -#' `"box"`, `"orthogonal"`, and `"geometry_bbox"`. For method `"cross"`, limits along -#' one direction (e.g., longitude) are applied at the midpoint of the other direction (e.g., -#' latitude). This method avoids excessively large limits for rotated coordinate -#' systems but means that sometimes limits need to be expanded a little further -#' if extreme data points are to be included in the final plot region. By contrast, -#' for method `"box"`, a box is generated out of the limits along both directions, +#' @param lims_method Method specifying how scale limits are converted into +#' limits on the plot region. For a very non-linear CRS (e.g., a perspective centered +#' around the North pole), the available methods yield widely differing results, and +#' you may want to try various options. Methods currently implemented include `"cross"` +#' (the default), `"box"`, `"orthogonal"`, and `"geometry_bbox"`. For method `"cross"`, +#' limits along one direction (e.g., longitude) are applied at the midpoint of the +#' other direction (e.g., latitude). This method avoids excessively large limits for +#' rotated coordinate systems but means that sometimes limits need to be expanded a +#' little further if extreme data points are to be included in the final plot region. +#' By contrast, for method `"box"`, a box is generated out of the limits along both directions, #' and then limits in projected coordinates are chosen such that the entire box is #' visible. This method can yield plot regions that are too large. Finally, method #' `"orthogonal"` applies limits separately along each axis, and method -#' `"geometry_bbox"` ignores all limit information except the bounding box of the -#' geometry. +#' `"geometry_bbox"` ignores all limit information except the bounding boxes of any +#' objects in the `geometry` aesthetic. #' @param datum CRS that provides datum to use when generating graticules. #' @param label_axes Character vector or named list of character values #' specifying which graticule lines (meridians or parallels) should be labeled on diff --git a/man/ggsf.Rd b/man/ggsf.Rd index 2af9f80f2a..ccec07b0f9 100644 --- a/man/ggsf.Rd +++ b/man/ggsf.Rd @@ -142,18 +142,21 @@ specified with \code{list(bottom = "E", left = "N")}. This parameter can be used alone or in combination with \code{label_graticule}.} -\item{lims_method}{The methods currently implemented include \code{"cross"} (the default), -\code{"box"}, \code{"orthogonal"}, and \code{"geometry_bbox"}. For method \code{"cross"}, limits along -one direction (e.g., longitude) are applied at the midpoint of the other direction (e.g., -latitude). This method avoids excessively large limits for rotated coordinate -systems but means that sometimes limits need to be expanded a little further -if extreme data points are to be included in the final plot region. By contrast, -for method \code{"box"}, a box is generated out of the limits along both directions, +\item{lims_method}{Method specifying how scale limits are converted into +limits on the plot region. For a very non-linear CRS (e.g., a perspective centered +around the North pole), the available methods yield widely differing results, and +you may want to try various options. Methods currently implemented include \code{"cross"} +(the default), \code{"box"}, \code{"orthogonal"}, and \code{"geometry_bbox"}. For method \code{"cross"}, +limits along one direction (e.g., longitude) are applied at the midpoint of the +other direction (e.g., latitude). This method avoids excessively large limits for +rotated coordinate systems but means that sometimes limits need to be expanded a +little further if extreme data points are to be included in the final plot region. +By contrast, for method \code{"box"}, a box is generated out of the limits along both directions, and then limits in projected coordinates are chosen such that the entire box is visible. This method can yield plot regions that are too large. Finally, method \code{"orthogonal"} applies limits separately along each axis, and method -\code{"geometry_bbox"} ignores all limit information except the bounding box of the -geometry.} +\code{"geometry_bbox"} ignores all limit information except the bounding boxes of any +objects in the \code{geometry} aesthetic.} \item{ndiscr}{Number of segments to use for discretising graticule lines; try increasing this number when graticules look incorrect.} From ee0fb99e8ee56c95e4a736d974398bbe702dcb98 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Wed, 24 Jun 2020 18:05:22 -0500 Subject: [PATCH 24/25] fix error message if scale limits inversion problem --- R/coord-sf.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index c74955edc9..3d1db33e34 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -534,8 +534,8 @@ sf_rescale01_x <- function(x, range) { # different limits methods calc_limits_bbox <- function(method, xlim, ylim, crs, default_crs) { - if (any(!is.finite(c(xlim, ylim)))) { - abort("Scale limits cannot be mapped onto spatial coordinates.") + if (any(!is.finite(c(xlim, ylim))) && method != "geometry_bbox") { + abort("Scale limits cannot be mapped onto spatial coordinates.\nConsider setting `lims_method = \"geometry_bbox\"` or `default_crs = NULL`.") } bbox <- switch( From 942d74ce86881b20f66ccf5e953cb3bb3a0996f0 Mon Sep 17 00:00:00 2001 From: Claus Wilke Date: Wed, 24 Jun 2020 18:15:27 -0500 Subject: [PATCH 25/25] reword warnings and error messages. --- R/coord-sf.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/coord-sf.R b/R/coord-sf.R index 3d1db33e34..cf99837c00 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -199,7 +199,7 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, scales_yrange <- range(scales_bbox$y, coord_bbox$ymin, coord_bbox$ymax, na.rm = TRUE) } else if (any(!is.finite(scales_bbox$x) | !is.finite(scales_bbox$y))) { if (self$lims_method != "geometry_bbox") { - warn("Projection of x or y limits failed.\nConsider working in projected coordinates by setting `default_crs = NULL` in `coord_sf()`.") + warn("Projection of x or y limits failed in `coord_sf()`.\nConsider setting `lims_method = \"geometry_bbox\"` or `default_crs = NULL`.") } coord_bbox <- self$params$bbox scales_xrange <- c(coord_bbox$xmin, coord_bbox$xmax) %||% c(0, 0) @@ -535,7 +535,7 @@ sf_rescale01_x <- function(x, range) { # different limits methods calc_limits_bbox <- function(method, xlim, ylim, crs, default_crs) { if (any(!is.finite(c(xlim, ylim))) && method != "geometry_bbox") { - abort("Scale limits cannot be mapped onto spatial coordinates.\nConsider setting `lims_method = \"geometry_bbox\"` or `default_crs = NULL`.") + abort("Scale limits cannot be mapped onto spatial coordinates in `coord_sf()`.\nConsider setting `lims_method = \"geometry_bbox\"` or `default_crs = NULL`.") } bbox <- switch(