5

I'm trying to plot bodies of water on my map and struggling with islands in ggplot2. I understand the right/left-hand rule for exterior/interior rings but there is still a problem going from island to island. The question is how do I plot a polygon with lots of holes/islands in ggplot2? I believe the trick is order, but what order?

Here is the MWE I have built to try and understand and fix the problem:

library(ggplot2)

ids <- letters[1]

# IDs and values to use for fill colour
values <- data.frame(
  id = ids,
  value = c(5)
)

# Example of good polygon and holes
good_positions <- data.frame(
  id = rep(ids, each = 5),
  #     shape        hole       hole       hole
  x = c(1,10,10,1,1, 2,2,3,3,2, 7,7,8,8,7, 5,5,6,6,5 ),
  y = c(1,1,10,10,1, 2,3,3,2,2, 7,8,8,7,7, 5,6,6,5,5)
)

# Example of good polygon and holes
bad_positions <- data.frame(
  id = rep(ids, each = 5),
  #     shape        hole       hole       hole
  x = c(1,10,10,1,1, 2,2,3,3,2, 7,7,8,8,7, 5,5,6,6,5 ),
  y = c(1,1,10,10,1, 2,3,3,2,2, 7,8,8,7,7, 3,4,4,3,3)
)


# Merge positions and values
good_datapoly <- merge(values, good_positions, by=c("id"))
bad_datapoly <- merge(values, bad_positions, by=c("id"))

# Plot polygons
good_plot <- ggplot(good_datapoly, aes(x=x, y=y)) + 
  geom_polygon(aes(group=id, fill=factor(value))) +
  scale_fill_discrete("Key")

bad_plot <- ggplot(bad_datapoly, aes(x=x, y=y)) + 
  geom_polygon(aes(group=id, fill=factor(value))) +
  scale_fill_discrete("Key")

good_plot
bad_plot

good_plot bad_plot

2

It looks like @rcs had the right idea, although generifying that into a function is a little more difficult. This blog post suggests that the 'easy way' is to use the non-CRAN package ggspatial (which looks like it has a geom_spatial() for use with Spatial* objects), but also contains the following non-package-ified function for use with SpatialPolygons objects (that you could probably apply to any example without worrying about coordinate order):

library(ggplot2)
library(dplyr)

fixfeature <- function(df) {
  ringstarts <- which(!duplicated(df$group))
  if(length(ringstarts) < 2) {
    return(df)
  } else {
    ringstarts <- c(ringstarts, nrow(df))
    indicies <- c(1:(ringstarts[2]-1), do.call(c, lapply(2:(length(ringstarts)-1), function(x) {
      c(1, ringstarts[x]:(ringstarts[x+1]-1))
    })), nrow(df))
    return(df[indicies,])
  }
}

custom_fortify <- function(x, ...) {
  df <- fortify(x, ...)
  df %>% group_by(id) %>% do(fixfeature(.))
}

The example from the blog post uses data from the ggspatial package to illustrate this:

devtools::install_github("paleolimbot/ggspatial")
library(ggspatial)
data(longlake_waterdf)

spdf <- longlake_waterdf[is.na(longlake_waterdf$label),]
ggplot(spdf, aes(x=long, y=lat)) + geom_polygon(aes(group=id)) # doesn't work

enter image description here

ggplot(custom_fortify(spdf), aes(x=long, y=lat)) + 
  geom_polygon(aes(group=id)) # does work

enter image description here

ggplot() + geom_spatial(spdf) # produces the same as above

enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.