Chris
Chris

Reputation: 1475

ggplot: Mask Circles inside a non Geographic Shape

Is there a way within ggplot, to plot circles within a defined, non geographic shape, defined through a series of points, or alternatively an imported SVG?

The circles would be placed in rows and columns, similar to the simple example below. But then any circles, either with their circumference, or centre if that is more achievable, outside the shape would be excluded from the plot. So a kind of mask.

I know I could do this by through comparing the coordinates, but I'm interested to know if there is a more sophisticated masking function.

library(tidyverse)

maxX <- 12
maxY <- 9
circles <- data.frame(circleNo = seq(1, maxX * maxY, 1) - 1) %>%
  mutate(x = circleNo %% maxX, y = floor(circleNo / maxX))

# Set line end to coordinates for next point
shape <- data.frame(x = c(1, 1, 7, 7, 11, 11, 6, 5, 3, 2, 1), y = c(1, 8, 7, 5, 5, 1, 3, 3, 3, 1, 1)) %>%
  mutate(xend = lead(x), yend = lead(y))

# Set line end for last point to the first
shape[nrow(shape),3] = shape[1,1] 
shape[nrow(shape),4] = shape[1,2] 

ggplot(circles, aes(x = x, y = y)) +
  geom_point(shape = 1, size = 9, fill = NA) +
  geom_segment(data = shape, aes(x = x, xend = xend, y = y, yend = yend)) +
  theme_void() +
  coord_fixed(ratio = 1) 

enter image description here

Upvotes: 1

Views: 134

Answers (2)

Chris
Chris

Reputation: 1475

My thanks to @Jon above for the pointers. This is what I came up. Note that I added a hole in the middle of the polygon for good measure.

enter image description here

library(tidyverse)
library(ggplot)

# Create grid of circles
maxX <- 24
maxY <- 18
circles <- data.frame(circleNo = seq(1, maxX * maxY, 1) - 1)
circles <- circles %>%
  mutate(x = circleNo %% maxX, y = floor(circleNo / maxX))

# Create polygon
shape <- data.frame(x = c(2, 2, 14, 14, 22, 22, 12, 10, 6, 4, 2), y = c(2, 16, 14, 10, 10, 2, 6, 6, 6, 2, 2)) %>%
  # With line ends equal to the next point
  mutate(xend = lead(x), yend = lead(y))
# Except for the last, where it needs to equal the first
shape[nrow(shape),3] = shape[1,1] 
shape[nrow(shape),4] = shape[1,2] 

# Plot the circles and polygon without any masking
ggplot(circles, aes(x = x, y = y)) +
  geom_point(shape = 1, size = 5, fill = NA) +
  geom_segment(data = shape, aes(x = x, xend = xend, y = y, yend = yend)) +
  theme_void() +
  coord_fixed(ratio = 1) 

# Now do similar with SF which allows masking using the helpful posts below

# Create simple feature from a numeric vector, matrix or list
# https://r-spatial.github.io/sf/reference/st.html

# How to mark points by whether or not they are within a polygon
# https://stackoverflow.com/questions/50144222/how-to-mark-points-by-whether-or-not-they-are-within-a-polygon

library(sf)
# Create outer polygon
outer = matrix(c(2,2, 2,16, 14,14, 14,10, 22,10, 22,2, 12,6, 10,6, 6,6, 4,2, 2,2), ncol=2, byrow=TRUE)
# And for good measure, lets put a hole in it
hole1 = matrix(c(10,10, 10,12, 12,12, 12,10, 10,10),ncol=2, byrow=TRUE)
polygonList= list(outer, hole1)

# Convert to simple feature
combinedPoints = lapply(polygonList, function(x) cbind(x, 0))
polygons = st_polygon(combinedPoints)

# Plot these new polygons
ggplot(polygons) +
  geom_sf(aes())

# Not entirely sure why we need these two lines
polygonCast <- polygons %>% st_cast("POLYGON")
circlesSF <- st_as_sf(circles, coords = c("x", "y"))

# Detect which ones are inside the outer polygon and outside the inner one
circlesSF <- circlesSF %>% mutate(outside = lengths(st_within(circlesSF, polygonCast)))

# Convert to a data frame, extract out the coordinates and filter out the ones outside
circleCoords <- as.data.frame(st_coordinates(circlesSF))
circles2 <- circlesSF %>%
  as.data.frame() %>%
  cbind(circleCoords) %>%
  select(-geometry) %>%
  filter(outside > 0)

ggplot(circles2, aes(x = X, y = Y)) +
  geom_point(shape = 1, size = 5, fill = NA) +
  geom_segment(data = shape, aes(x = x, xend = xend, y = y, yend = yend)) +
  theme_void() +
  coord_fixed(ratio = 1) 

Upvotes: 2

Jon Spring
Jon Spring

Reputation: 66490

Here's one approach that is based on manipulating the pixels as a last step. It is not sophisticated enough to identify which circles are entirely within the polygon, though. For that, the sf package and this approach sound like what you want:

How to mark points by whether or not they are within a polygon

library(ggfx)
ggplot(circles, aes(x = x, y = y)) +
  as_reference(
    geom_polygon(data = shape),
    id = "mask_layer"
  ) +
  with_mask(
    geom_point(shape = 1, size = 9, fill = NA),
    mask = "mask_layer"
  ) +
  theme_void() +
  coord_fixed(ratio = 1) 

enter image description here

Upvotes: 3

Related Questions