From 60e4f4845a1156ec3010d6a1d4510ed61b4d1f65 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Mon, 11 Feb 2019 12:53:07 +0100 Subject: [PATCH 1/7] Add support for holed polygons using an optional subgroup aesthetic --- R/geom-polygon.r | 87 +++++++++++++++++++++++++++++++++++---------- R/ggplot-global.R | 2 ++ man/geom_map.Rd | 1 + man/geom_polygon.Rd | 22 +++++++++++- 4 files changed, 92 insertions(+), 20 deletions(-) diff --git a/R/geom-polygon.r b/R/geom-polygon.r index 9e9c7525ee..55fbcbea94 100644 --- a/R/geom-polygon.r +++ b/R/geom-polygon.r @@ -3,7 +3,10 @@ #' Polygons are very similar to paths (as drawn by [geom_path()]) #' except that the start and end points are connected and the inside is #' coloured by `fill`. The `group` aesthetic determines which cases -#' are connected together into a polygon. +#' are connected together into a polygon. From R 3.6 and onwards it is possible +#' to draw polygons with holes by providing a subgroup aesthetic that +#' differentiate the outer ring points from those describing holes in the +#' polygon. #' #' @eval rd_aesthetics("geom", "polygon") #' @seealso @@ -52,6 +55,22 @@ #' #' # And if the positions are in longitude and latitude, you can use #' # coord_map to produce different map projections. +#' +#' # As of R version 3.6 geom_polygon supports polygons with holes +#' # Use the subgroup aesthetic to differentiate holes from the main polygon +#' +#' holes <- do.call(rbind, lapply(split(datapoly, datapoly$id), function(df) { +#' df$x <- df$x + 0.5 * (mean(df$x) - df$x) +#' df$y <- df$y + 0.5 * (mean(df$y) - df$y) +#' df +#' })) +#' datapoly$subid <- 1L +#' holes$subid <- 2L +#' datapoly <- rbind(datapoly, holes) +#' +#' p <- ggplot(datapoly, aes(x = x, y = y)) + +#' geom_polygon(aes(fill = value, group = id, subgroup = subid)) +#' p geom_polygon <- function(mapping = NULL, data = NULL, stat = "identity", position = "identity", ..., @@ -83,30 +102,60 @@ GeomPolygon <- ggproto("GeomPolygon", Geom, if (n == 1) return(zeroGrob()) munched <- coord_munch(coord, data, panel_params) - # Sort by group to make sure that colors, fill, etc. come in same order - munched <- munched[order(munched$group), ] - # For gpar(), there is one entry per polygon (not one entry per point). - # We'll pull the first value from each group, and assume all these values - # are the same within each group. - first_idx <- !duplicated(munched$group) - first_rows <- munched[first_idx, ] + if (is.null(munched$subgroup)) { + # Sort by group to make sure that colors, fill, etc. come in same order + munched <- munched[order(munched$group), ] + + # For gpar(), there is one entry per polygon (not one entry per point). + # We'll pull the first value from each group, and assume all these values + # are the same within each group. + first_idx <- !duplicated(munched$group) + first_rows <- munched[first_idx, ] - ggname("geom_polygon", - polygonGrob(munched$x, munched$y, default.units = "native", - id = munched$group, - gp = gpar( - col = first_rows$colour, - fill = alpha(first_rows$fill, first_rows$alpha), - lwd = first_rows$size * .pt, - lty = first_rows$linetype - ) + ggname("geom_polygon", + polygonGrob(munched$x, munched$y, default.units = "native", + id = munched$group, + gp = gpar( + col = first_rows$colour, + fill = alpha(first_rows$fill, first_rows$alpha), + lwd = first_rows$size * .pt, + lty = first_rows$linetype + ) + ) ) - ) + } else { + if (!grid_has_multipath) { + stop("Polygons with holes are only supported in R versions from 3.6 and onwards", call. = FALSE) + } + # Sort by group to make sure that colors, fill, etc. come in same order + munched <- munched[order(munched$group, munched$subgroup), ] + id <- match(munched$subgroup, unique(munched$subgroup)) + + # For gpar(), there is one entry per polygon (not one entry per point). + # We'll pull the first value from each group, and assume all these values + # are the same within each group. + first_idx <- !duplicated(munched$group) + first_rows <- munched[first_idx, ] + + ggname("geom_polygon", + pathGrob(munched$x, munched$y, default.units = "native", + id = id, pathId = munched$group, + rule = "evenodd", + gp = gpar( + col = first_rows$colour, + fill = alpha(first_rows$fill, first_rows$alpha), + lwd = first_rows$size * .pt, + lty = first_rows$linetype + ) + ) + ) + } + }, default_aes = aes(colour = "NA", fill = "grey20", size = 0.5, linetype = 1, - alpha = NA), + alpha = NA, subgroup = NULL), handle_na = function(data, params) { data diff --git a/R/ggplot-global.R b/R/ggplot-global.R index e9b871ae6c..081fc2d89f 100644 --- a/R/ggplot-global.R +++ b/R/ggplot-global.R @@ -44,3 +44,5 @@ ggplot_global$all_aesthetics <- .all_aesthetics ) ggplot_global$base_to_ggplot <- .base_to_ggplot + +grid_has_multipath <- TRUE #packageVersion('grid') >= "3.6" diff --git a/man/geom_map.Rd b/man/geom_map.Rd index 3bab7a983b..07eb885020 100644 --- a/man/geom_map.Rd +++ b/man/geom_map.Rd @@ -68,6 +68,7 @@ This is pure annotation, so does not affect position scales. \item \code{group} \item \code{linetype} \item \code{size} +\item \code{subgroup} } Learn more about setting these aesthetics in \code{vignette("ggplot2-specs")}. } diff --git a/man/geom_polygon.Rd b/man/geom_polygon.Rd index 0d91e6aeaa..0cb13b30f6 100644 --- a/man/geom_polygon.Rd +++ b/man/geom_polygon.Rd @@ -57,7 +57,10 @@ the default plot specification, e.g. \code{\link[=borders]{borders()}}.} Polygons are very similar to paths (as drawn by \code{\link[=geom_path]{geom_path()}}) except that the start and end points are connected and the inside is coloured by \code{fill}. The \code{group} aesthetic determines which cases -are connected together into a polygon. +are connected together into a polygon. From R 3.6 and onwards it is possible +to draw polygons with holes by providing a subgroup aesthetic that +differentiate the outer ring points from those describing holes in the +polygon. } \section{Aesthetics}{ @@ -71,6 +74,7 @@ are connected together into a polygon. \item \code{group} \item \code{linetype} \item \code{size} +\item \code{subgroup} } Learn more about setting these aesthetics in \code{vignette("ggplot2-specs")}. } @@ -115,6 +119,22 @@ p + geom_line(data = stream, colour = "grey30", size = 5) # And if the positions are in longitude and latitude, you can use # coord_map to produce different map projections. + +# As of R version 3.6 geom_polygon supports polygons with holes +# Use the subgroup aesthetic to differentiate holes from the main polygon + +holes <- do.call(rbind, lapply(split(datapoly, datapoly$id), function(df) { + df$x <- df$x + 0.5 * (mean(df$x) - df$x) + df$y <- df$y + 0.5 * (mean(df$y) - df$y) + df +})) +datapoly$subid <- 1L +holes$subid <- 2L +datapoly <- rbind(datapoly, holes) + +p <- ggplot(datapoly, aes(x = x, y = y)) + + geom_polygon(aes(fill = value, group = id, subgroup = subid)) +p } \seealso{ \code{\link[=geom_path]{geom_path()}} for an unfilled polygon, From 858ca8fddbfb12a0521a3de6be2336e75500a8d7 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Mon, 11 Feb 2019 13:05:08 +0100 Subject: [PATCH 2/7] Fix temporary overwrite of support flag --- R/ggplot-global.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/ggplot-global.R b/R/ggplot-global.R index 081fc2d89f..37b4915106 100644 --- a/R/ggplot-global.R +++ b/R/ggplot-global.R @@ -45,4 +45,4 @@ ggplot_global$all_aesthetics <- .all_aesthetics ggplot_global$base_to_ggplot <- .base_to_ggplot -grid_has_multipath <- TRUE #packageVersion('grid') >= "3.6" +grid_has_multipath <- packageVersion('grid') >= "3.6" From 2d8b1ff639d1b3ba9b9c9e25696c0adec2dc9eb7 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Mon, 11 Feb 2019 22:02:46 +0100 Subject: [PATCH 3/7] Small style fixes suggested by Claus --- R/geom-polygon.r | 50 ++++++++++++++++++++++++--------------------- man/geom_polygon.Rd | 4 ++-- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/R/geom-polygon.r b/R/geom-polygon.r index 55fbcbea94..c8c00cef8c 100644 --- a/R/geom-polygon.r +++ b/R/geom-polygon.r @@ -5,7 +5,7 @@ #' coloured by `fill`. The `group` aesthetic determines which cases #' are connected together into a polygon. From R 3.6 and onwards it is possible #' to draw polygons with holes by providing a subgroup aesthetic that -#' differentiate the outer ring points from those describing holes in the +#' differentiates the outer ring points from those describing holes in the #' polygon. #' #' @eval rd_aesthetics("geom", "polygon") @@ -56,7 +56,7 @@ #' # And if the positions are in longitude and latitude, you can use #' # coord_map to produce different map projections. #' -#' # As of R version 3.6 geom_polygon supports polygons with holes +#' # As of R version 3.6 geom_polygon() supports polygons with holes #' # Use the subgroup aesthetic to differentiate holes from the main polygon #' #' holes <- do.call(rbind, lapply(split(datapoly, datapoly$id), function(df) { @@ -113,16 +113,18 @@ GeomPolygon <- ggproto("GeomPolygon", Geom, first_idx <- !duplicated(munched$group) first_rows <- munched[first_idx, ] - ggname("geom_polygon", - polygonGrob(munched$x, munched$y, default.units = "native", - id = munched$group, - gp = gpar( - col = first_rows$colour, - fill = alpha(first_rows$fill, first_rows$alpha), - lwd = first_rows$size * .pt, - lty = first_rows$linetype - ) - ) + ggname( + "geom_polygon", + polygonGrob( + munched$x, munched$y, default.units = "native", + id = munched$group, + gp = gpar( + col = first_rows$colour, + fill = alpha(first_rows$fill, first_rows$alpha), + lwd = first_rows$size * .pt, + lty = first_rows$linetype + ) + ) ) } else { if (!grid_has_multipath) { @@ -138,17 +140,19 @@ GeomPolygon <- ggproto("GeomPolygon", Geom, first_idx <- !duplicated(munched$group) first_rows <- munched[first_idx, ] - ggname("geom_polygon", - pathGrob(munched$x, munched$y, default.units = "native", - id = id, pathId = munched$group, - rule = "evenodd", - gp = gpar( - col = first_rows$colour, - fill = alpha(first_rows$fill, first_rows$alpha), - lwd = first_rows$size * .pt, - lty = first_rows$linetype - ) - ) + ggname( + "geom_polygon", + pathGrob( + munched$x, munched$y, default.units = "native", + id = id, pathId = munched$group, + rule = "evenodd", + gp = gpar( + col = first_rows$colour, + fill = alpha(first_rows$fill, first_rows$alpha), + lwd = first_rows$size * .pt, + lty = first_rows$linetype + ) + ) ) } diff --git a/man/geom_polygon.Rd b/man/geom_polygon.Rd index 0cb13b30f6..bf2affeddc 100644 --- a/man/geom_polygon.Rd +++ b/man/geom_polygon.Rd @@ -59,7 +59,7 @@ except that the start and end points are connected and the inside is coloured by \code{fill}. The \code{group} aesthetic determines which cases are connected together into a polygon. From R 3.6 and onwards it is possible to draw polygons with holes by providing a subgroup aesthetic that -differentiate the outer ring points from those describing holes in the +differentiates the outer ring points from those describing holes in the polygon. } \section{Aesthetics}{ @@ -120,7 +120,7 @@ p + geom_line(data = stream, colour = "grey30", size = 5) # And if the positions are in longitude and latitude, you can use # coord_map to produce different map projections. -# As of R version 3.6 geom_polygon supports polygons with holes +# As of R version 3.6 geom_polygon() supports polygons with holes # Use the subgroup aesthetic to differentiate holes from the main polygon holes <- do.call(rbind, lapply(split(datapoly, datapoly$id), function(df) { From 1901c51cac1a7232c22131b05c5a20b48f28c545 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Tue, 12 Feb 2019 15:07:40 +0100 Subject: [PATCH 4/7] Move grid version check inside draw_panel --- R/geom-polygon.r | 4 ++-- R/ggplot-global.R | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/R/geom-polygon.r b/R/geom-polygon.r index c8c00cef8c..dbd631d4fb 100644 --- a/R/geom-polygon.r +++ b/R/geom-polygon.r @@ -127,8 +127,8 @@ GeomPolygon <- ggproto("GeomPolygon", Geom, ) ) } else { - if (!grid_has_multipath) { - stop("Polygons with holes are only supported in R versions from 3.6 and onwards", call. = FALSE) + if (utils::packageVersion('grid') < "3.6") { + stop("Polygons with holes requires R 3.6 or above", call. = FALSE) } # Sort by group to make sure that colors, fill, etc. come in same order munched <- munched[order(munched$group, munched$subgroup), ] diff --git a/R/ggplot-global.R b/R/ggplot-global.R index 37b4915106..e9b871ae6c 100644 --- a/R/ggplot-global.R +++ b/R/ggplot-global.R @@ -44,5 +44,3 @@ ggplot_global$all_aesthetics <- .all_aesthetics ) ggplot_global$base_to_ggplot <- .base_to_ggplot - -grid_has_multipath <- packageVersion('grid') >= "3.6" From 99f0166e5de4590ef8038e5bd6c1bfd7b093db71 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Tue, 12 Feb 2019 15:08:52 +0100 Subject: [PATCH 5/7] Add rule argument to geom_polygon --- R/geom-polygon.r | 10 ++++++++-- man/borders.Rd | 4 ++++ man/geom_polygon.Rd | 9 +++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/R/geom-polygon.r b/R/geom-polygon.r index dbd631d4fb..5395952000 100644 --- a/R/geom-polygon.r +++ b/R/geom-polygon.r @@ -15,6 +15,10 @@ #' @export #' @inheritParams layer #' @inheritParams geom_point +#' @param rule Either `"evenodd"` or `"winding"`. If polygons with holes are +#' being drawn (using the `subgroup` aesthetic) this argument defines how the +#' hole coordinates are interpreted. See the examples in [grid::pathGrob()] for +#' an explanation. #' @examples #' # When using geom_polygon, you will typically need two data frames: #' # one contains the coordinates of each polygon (positions), and the @@ -73,6 +77,7 @@ #' p geom_polygon <- function(mapping = NULL, data = NULL, stat = "identity", position = "identity", + rule = "evenodd", ..., na.rm = FALSE, show.legend = NA, @@ -87,6 +92,7 @@ geom_polygon <- function(mapping = NULL, data = NULL, inherit.aes = inherit.aes, params = list( na.rm = na.rm, + rule = rule, ... ) ) @@ -97,7 +103,7 @@ geom_polygon <- function(mapping = NULL, data = NULL, #' @usage NULL #' @export GeomPolygon <- ggproto("GeomPolygon", Geom, - draw_panel = function(data, panel_params, coord) { + draw_panel = function(data, panel_params, coord, rule = "evenodd") { n <- nrow(data) if (n == 1) return(zeroGrob()) @@ -145,7 +151,7 @@ GeomPolygon <- ggproto("GeomPolygon", Geom, pathGrob( munched$x, munched$y, default.units = "native", id = id, pathId = munched$group, - rule = "evenodd", + rule = rule, gp = gpar( col = first_rows$colour, fill = alpha(first_rows$fill, first_rows$alpha), diff --git a/man/borders.Rd b/man/borders.Rd index 07544c8cad..eff17e1e2b 100644 --- a/man/borders.Rd +++ b/man/borders.Rd @@ -21,6 +21,10 @@ polygons, see \code{\link[maps:map]{maps::map()}} for details.} \item{...}{Arguments passed on to \code{geom_polygon} \describe{ + \item{rule}{Either \code{"evenodd"} or \code{"winding"}. If polygons with holes are +being drawn (using the \code{subgroup} aesthetic) this argument defines how the +hole coordinates are interpreted. See the examples in \code{\link[grid:pathGrob]{grid::pathGrob()}} for +an explanation.} \item{mapping}{Set of aesthetic mappings created by \code{\link[=aes]{aes()}} or \code{\link[=aes_]{aes_()}}. If specified and \code{inherit.aes = TRUE} (the default), it is combined with the default mapping at the top level of the diff --git a/man/geom_polygon.Rd b/man/geom_polygon.Rd index bf2affeddc..e771ad803c 100644 --- a/man/geom_polygon.Rd +++ b/man/geom_polygon.Rd @@ -5,8 +5,8 @@ \title{Polygons} \usage{ geom_polygon(mapping = NULL, data = NULL, stat = "identity", - position = "identity", ..., na.rm = FALSE, show.legend = NA, - inherit.aes = TRUE) + position = "identity", rule = "evenodd", ..., na.rm = FALSE, + show.legend = NA, inherit.aes = TRUE) } \arguments{ \item{mapping}{Set of aesthetic mappings created by \code{\link[=aes]{aes()}} or @@ -34,6 +34,11 @@ layer, as a string.} \item{position}{Position adjustment, either as a string, or the result of a call to a position adjustment function.} +\item{rule}{Either \code{"evenodd"} or \code{"winding"}. If polygons with holes are +being drawn (using the \code{subgroup} aesthetic) this argument defines how the +hole coordinates are interpreted. See the examples in \code{\link[grid:pathGrob]{grid::pathGrob()}} for +an explanation.} + \item{...}{Other arguments passed on to \code{\link[=layer]{layer()}}. These are often aesthetics, used to set an aesthetic to a fixed value, like \code{colour = "red"} or \code{size = 3}. They may also be parameters From 5f6925f838b9cc1a850fc810d00fe653d8c4ee2e Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Tue, 12 Feb 2019 15:24:10 +0100 Subject: [PATCH 6/7] make last polygon example conditional on grid version --- R/geom-polygon.r | 29 ++++++++++++++++------------- man/geom_polygon.Rd | 31 +++++++++++++++++-------------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/R/geom-polygon.r b/R/geom-polygon.r index 5395952000..f8ed81bae9 100644 --- a/R/geom-polygon.r +++ b/R/geom-polygon.r @@ -60,21 +60,24 @@ #' # And if the positions are in longitude and latitude, you can use #' # coord_map to produce different map projections. #' -#' # As of R version 3.6 geom_polygon() supports polygons with holes -#' # Use the subgroup aesthetic to differentiate holes from the main polygon +#' if (packageVersion("grid") >= "3.6") { +#' # As of R version 3.6 geom_polygon() supports polygons with holes +#' # Use the subgroup aesthetic to differentiate holes from the main polygon #' -#' holes <- do.call(rbind, lapply(split(datapoly, datapoly$id), function(df) { -#' df$x <- df$x + 0.5 * (mean(df$x) - df$x) -#' df$y <- df$y + 0.5 * (mean(df$y) - df$y) -#' df -#' })) -#' datapoly$subid <- 1L -#' holes$subid <- 2L -#' datapoly <- rbind(datapoly, holes) +#' holes <- do.call(rbind, lapply(split(datapoly, datapoly$id), function(df) { +#' df$x <- df$x + 0.5 * (mean(df$x) - df$x) +#' df$y <- df$y + 0.5 * (mean(df$y) - df$y) +#' df +#' })) +#' datapoly$subid <- 1L +#' holes$subid <- 2L +#' datapoly <- rbind(datapoly, holes) +#' +#' p <- ggplot(datapoly, aes(x = x, y = y)) + +#' geom_polygon(aes(fill = value, group = id, subgroup = subid)) +#' p +#' } #' -#' p <- ggplot(datapoly, aes(x = x, y = y)) + -#' geom_polygon(aes(fill = value, group = id, subgroup = subid)) -#' p geom_polygon <- function(mapping = NULL, data = NULL, stat = "identity", position = "identity", rule = "evenodd", diff --git a/man/geom_polygon.Rd b/man/geom_polygon.Rd index e771ad803c..cdf7b7a527 100644 --- a/man/geom_polygon.Rd +++ b/man/geom_polygon.Rd @@ -125,21 +125,24 @@ p + geom_line(data = stream, colour = "grey30", size = 5) # And if the positions are in longitude and latitude, you can use # coord_map to produce different map projections. -# As of R version 3.6 geom_polygon() supports polygons with holes -# Use the subgroup aesthetic to differentiate holes from the main polygon - -holes <- do.call(rbind, lapply(split(datapoly, datapoly$id), function(df) { - df$x <- df$x + 0.5 * (mean(df$x) - df$x) - df$y <- df$y + 0.5 * (mean(df$y) - df$y) - df -})) -datapoly$subid <- 1L -holes$subid <- 2L -datapoly <- rbind(datapoly, holes) +if (packageVersion("grid") >= "3.6") { + # As of R version 3.6 geom_polygon() supports polygons with holes + # Use the subgroup aesthetic to differentiate holes from the main polygon + + holes <- do.call(rbind, lapply(split(datapoly, datapoly$id), function(df) { + df$x <- df$x + 0.5 * (mean(df$x) - df$x) + df$y <- df$y + 0.5 * (mean(df$y) - df$y) + df + })) + datapoly$subid <- 1L + holes$subid <- 2L + datapoly <- rbind(datapoly, holes) + + p <- ggplot(datapoly, aes(x = x, y = y)) + + geom_polygon(aes(fill = value, group = id, subgroup = subid)) + p +} -p <- ggplot(datapoly, aes(x = x, y = y)) + - geom_polygon(aes(fill = value, group = id, subgroup = subid)) -p } \seealso{ \code{\link[=geom_path]{geom_path()}} for an unfilled polygon, From 5cdc09a66644379ceec5648c21b7ac9c4cb241fd Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Tue, 12 Feb 2019 19:39:34 +0100 Subject: [PATCH 7/7] Add news bullet --- NEWS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS.md b/NEWS.md index 23a49eedf3..d94a40d433 100644 --- a/NEWS.md +++ b/NEWS.md @@ -49,6 +49,9 @@ * `stat_bin()` now handles data with only one unique value (@yutannihilation #3047). +* `geom_polygon()` can now draw polygons with holes using the new `subgroup` + aesthetic. This functionality requires R 3.6 (@thomasp85, #3128) + # ggplot2 3.1.0 ## Breaking changes