Reputation: 1475
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)
Upvotes: 1
Views: 134
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.
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
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)
Upvotes: 3