I'm trying to create some art with Voronoi polygons. I want to run lines across the graph, where any polygon below the line is one colour and any polygon above the line is another colour. The lines have random noise added so they're jagged.
I've created random dots and built Voronois around the dots. I've created lines to run across the graph and cut the Voronois so that if they're intersected by a line they're split into two polygons. I also recalculated the polygon's centroids after the cut, if that helps.
Where I'm stuck is being able to look at the lines over the graph and make any polygon beneath a line one colour and any polygon above a line another colour.
How might I be able to do that?
pacman::p_load(
"tidyverse",
"grDevices",
"ggplot2",
"deldir",
"scales",
"purrr",
"sf"
)
df_lines <- tibble(
x = seq(
from = 0,
to = 100,
by = 2
)
) %>%
mutate( # Create the y-values for each ridge
r1 = sin(x/50)*25,
r2 = sin(x/150)*75 + 0.2,
r3 = r2*1.5 + 10
) %>%
pivot_longer( # Pivot longer so we can graph with a single geom_line()
cols = starts_with("r"),
names_to = "ridge",
values_to = "y"
) %>%
mutate( # Adjust the x values
x = case_when(
ridge == "r1" ~ x + 50,
.default = x
),
y = y + rnorm(n = nrow(.), mean = 0, sd = 1)
)
df_lines <- df_lines %>%
st_as_sf(coords = c('x', 'y'), remove = FALSE) %>%
group_by(ridge) %>%
dplyr::summarize(do_union = FALSE) %>% # do_union=FALSE doesn't work as well
st_cast("LINESTRING")
set.seed(1)
# Create random dots
df_dots <- tibble(
x = runif(min = 0, max = 100, 500),
y = runif(min = 0, max = 100, 500)
) %>% # Convert to sf object but preserve x and y vars
st_as_sf(coords = c('x', 'y'), remove = FALSE)
# Pull out the voronoi shapes
df_voronoi <- st_voronoi(st_combine(df_dots)) %>%
st_collection_extract("POLYGON") %>%
st_sf() %>%
mutate(id = row_number())
# Join the voronois back into the dots df
df_dots <- df_dots %>%
st_join(df_voronoi, join = st_within)
df_voronoi <- df_voronoi %>%
lwgeom::st_split(
x = .,
y = df_lines
) %>%
st_collection_extract("POLYGON") %>%
st_sf() %>%
mutate(
centroid = st_centroid(geometry),
x = st_coordinates(centroid)[,1],
y = st_coordinates(centroid)[,2]
) %>%
select(-centroid)
# Plot
df_voronoi %>%
ggplot() +
geom_sf(
aes(),
colour = "black"
) +
geom_point(
aes(
x = x,
y = y
)
) +
geom_sf(
data = df_lines,
colour = "red",
linewidth = 1
) +
scale_colour_identity() +
scale_fill_identity() +
coord_sf(
xlim = c(5, 95),
ylim = c(5, 95)
)
And the final graph would look something like
You could split your target bounding box with lines to create polygons with colour bands / regions. Resulting shape can be used with st_intersection()
to classify each Voronoi polygon. Those that intersect the lines are also split, so this would partly cover what you are doing in your Cut voronoi polys by linestring step. And it would probably make sense to just (re-)build centroids as a last step and avoid that ugly (x, y) wrangling and filtering that I've included in this example.
library(sf)
library(ggplot2)
library(dplyr)
library(tidyr)
# create bbox shape and split it by lines into 4 ploygons
bands_sf <-
st_bbox(c(xmin = 5, ymin = 5, xmax = 95, ymax = 95)) |>
st_as_sfc() |>
lwgeom::st_split(df_lines) |>
st_collection_extract() |>
st_sf(geometry = _) |>
tibble::rowid_to_column("c_band")
bands_sf
#> Simple feature collection with 4 features and 1 field
#> Geometry type: POLYGON
#> Dimension: XY
#> Bounding box: xmin: 5 ymin: 5 xmax: 95 ymax: 95
#> CRS: NA
#> c_band geometry
#> 1 1 POLYGON ((9.389897 5, 5 5, ...
#> 2 2 POLYGON ((62.21236 5, 9.389...
#> 3 3 POLYGON ((95 19.50577, 95 5...
#> 4 4 POLYGON ((5 14.77211, 5 95,...
plot(bands_sf)
# crop df_voronoi and get intersections with bands_sf,
# resulting polygons get a c_band attribute;
# add 2nd geometry column for dots from x,y,
# those outside of the bbox or too close to the edge are set to NA
voronoi_cb <-
st_crop(df_voronoi, bands_sf) |>
st_intersection(bands_sf) |>
st_collection_extract("POLYGON") |>
mutate(geom_dot = st_as_sf(pick(x, y), coords = c("x", "y")) |> st_geometry()) |>
mutate(
geom_dot = if_else(
lengths(st_within(geom_dot, st_buffer(st_union(geometry), -.5))) > 0,
geom_dot,
NA
)
)
# assign colours to c_band values and plot,
# aes(geometry = geometry) to explicitly choose geometry column from sf object
voronoi_cb |>
mutate(c_band = case_match(c_band, 1 ~ "#569e4d", 2 ~ "#43a6dd", 3 ~ "#f55544", 4 ~ "#e5e5e5" )) |>
ggplot() +
geom_sf(aes(geometry = geometry, fill = c_band), colour = "black") +
geom_sf(aes(geometry = geom_dot)) +
# crop lines with ggplot() data, i.e. voronoi_cb
geom_sf(data = ~st_crop(df_lines, .), colour = "red", linewidth = 1) +
scale_fill_identity() +
coord_sf(expand = FALSE)