Transparent or uniform colour for outer-most Voronoi polygons in a figure

115 views Asked by At

I'm trying to remove the outer polygons in a Voronoi diagram. The main plot looks like this:

require(ggplot2)
require(ggvoronoi)
set.seed(2023)

N <- 80
x <- runif(N)
y <- runif(N)
df <- data.frame(x, y)
df$dist <- rnorm(N)

ggplot(df, aes(x, y, fill = dist)) +
    geom_voronoi() +
    stat_voronoi(geom = "path") +
    theme_void()

main pic

Something that's close to what I would like occurs if I set a limit on the axes. However, there's still fill in some of the outer polygons:

ggplot(df, aes(x, y, fill = dist)) +
    geom_voronoi() +
    stat_voronoi(geom = "path") +
    theme_void() + 
    xlim(0.1, 0.9) + 
    ylim(0.1, 0.9)

[limited pic

What is the best approach to isolate/identify the outer-most polygons around the edge of the figure and make them transparent or a uniform colour?

2

There are 2 answers

4
margusl On BEST ANSWER

{sf}-based approach (GEOS implementation of st_voronoi) might look something like this:

library(sf)
#> Linking to GEOS 3.11.2, GDAL 3.6.2, PROJ 9.2.0; sf_use_s2() is TRUE
library(ggplot2)
set.seed(2023)

N <- 80
x <- runif(N)
y <- runif(N)
df <- data.frame(x, y)
df$dist <- rnorm(N)

# to sf object
points_sf <- st_as_sf(df, coords = c("x", "y"))
# for st_voronoi we need to go though st_union to create a MULTIPOINT, 
# as `dist` will be lost, we'll do a spatial join as a final step
# to add that attribute back;
# st_voronoi returns GEOMETRYCOLLECTION, 
# st_collection_extract() to extract POLYGONs and st_sf() to convert sfc to sf
voronoi_sf <- 
  points_sf |>
  st_union() |>
  st_voronoi() |>
  st_collection_extract() |>
  st_sf(geometry = _) |>
  st_join(points_sf) |> 
  # crop to the extent of points_sf to align with other implementations
  st_crop(points_sf)
#> Warning: attribute variables are assumed to be spatially constant throughout
#> all geometries

# boundary polygon (currently matches points_sf bbox)
boundary_sf <- st_union(voronoi_sf) |> st_boundary()

# use st_filter with st_disjoint predicate to keep only polygons that 
# have no common points with the boundary of union:
st_filter(voronoi_sf, boundary_sf, .predicate = st_disjoint) |>
  ggplot(aes(fill = dist)) +
  geom_sf() +
  theme_void()

# or flag outer polygons with st_disjoint(), 
# sparse = FALSE: returns dense logical matrix instead of sparse index list
# [,1]: selects 1st matrix column
voronoi_sf$inner <- st_disjoint(voronoi_sf, boundary_sf, sparse = FALSE)[,1]

voronoi_sf
#> Simple feature collection with 80 features and 2 fields
#> Geometry type: POLYGON
#> Dimension:     XY
#> Bounding box:  xmin: 0.03039173 ymin: 0.01943989 xmax: 0.9992733 ymax: 0.9911794
#> CRS:           NA
#> First 10 features:
#>          dist                       geometry inner
#> 1   0.1413685 POLYGON ((0.089608 0.837129... FALSE
#> 2   0.9269828 POLYGON ((0.09466375 0.6231... FALSE
#> 3   0.1516984 POLYGON ((0.05469083 0.3648... FALSE
#> 4   0.3005414 POLYGON ((0.05469083 0.3648...  TRUE
#> 5  -0.1206981 POLYGON ((0.08540603 0.4952... FALSE
#> 6   1.3876944 POLYGON ((0.1273276 0.32198... FALSE
#> 7   2.7046756 POLYGON ((0.2191197 0.09137... FALSE
#> 8   0.2651664 POLYGON ((0.6434843 0.04838... FALSE
#> 9   0.9245641 POLYGON ((0.04950097 0.1988... FALSE
#> 10 -0.3157660 POLYGON ((0.1273276 0.32198...  TRUE
ggplot() +
  geom_sf(data = voronoi_sf, aes(fill = ifelse(inner, dist, NA)), color = "grey10") +
  geom_sf(data = points_sf,  aes(fill = dist), size = 2, show.legend = FALSE, shape = 21) +
  scale_fill_continuous(na.value = "grey80", name = "dist") +
  theme_void()


Added st_crop(points_sf) to deal with a larger-than-optimal envelope size of st_voronoi(); this should probably be reconsidered depending on the actual dataset and application.
First revision - https://stackoverflow.com/revisions/77569621/1

0
stefan On

Unfortunately I wasn't able to install ggvoronoi. But here is an approach using ggforce::geom_tile_voronoi which perhaps also works for ggvoronoi. The approach is a bit hacky as I simply extract the data from the geom_tile_voronoi layer, then filter for polygons touching the borders and afterwards use a geom_polygon to draw the filtered data without the outer polygons:

library(ggplot2)
library(ggforce)
library(dplyr, warn=FALSE)

set.seed(2023)

N <- 80
x <- runif(N)
y <- runif(N)
df <- data.frame(x, y)
df$dist <- rnorm(N)

p <- ggplot(df, aes(x, y, fill = dist)) +
  geom_voronoi_tile() +
  theme_void()

dat <- layer_data(p, i = 1)

outer <- dat |>
  filter(x %in% range(x) | y %in% range(y)) |>
  distinct(group) |>
  pull(group)

dat |>
  filter(!group %in% outer) |>
  ggplot(aes(x, y, fill = fill)) +
  geom_voronoi_tile(
    data = df,
    aes(x, y),
    fill = "white", color = "black"
  ) +
  geom_polygon(aes(group = group), color = "black") +
  scale_fill_identity() +
  theme_void()


dat |>
  filter(!group %in% outer) |>
  ggplot(aes(x, y, fill = fill)) +
  geom_polygon(aes(group = group), color = "black") +
  scale_fill_identity() +
  theme_void()