Voronoi diagram is a special type of tessellation that partitions a plane into regions close to each of a given set of points. The diagram is constructed by drawing a line segment between each pair of points, and then perpendicular bisecting each line segment. The intersection points of these perpendicular bisectors define the boundaries of the Voronoi regions.
Voronoi diagrams are a powerful tool for visualizing data, and they can be used to show a wide variety of data types. If you are looking for a way to visualize nested data or spatial data, I recommend considering using Voronoi diagrams. Voronoi diagrams can be used to show nested data by assigning different colours or patterns to the regions of the diagram based on the level of nesting.
Several R packages offer solutions to produce Voronoi graphs, such as the voronoiTreemap, ggvoronoi, and the deldir libraries. Each of these packages, however, has their own weaknesses. Deldir does not seem to work well with non-spatial data, plots from voronoiTreemap are difficult to export to high-resolution images, and ggvoronoi does not seem to support nested data. The VoronoiPlus algorithm offers some promising solutions though and rather disappointed to see it gives errors in multi-level-nestedness situations.
I, therefore, started tweaking its source code and came up a possible solution, which may not be the best, may not be the fastest, but it DOES work.
We will need several packages to run the code:
library(VoronoiPlus) library(sf) library(tidyterra) library(terra) library(ggplot2) library(Polychrome)
The main function:
# generates voronoid maps based on the combination of several groups # df: the data frame containing all variables # variable: numeric variable along which the map will be summarised # fgroups: a character vector showing the columns along which the grouping will happen # shape: what the general shape of the voronoid map should be. Default is a circle. voron_tree<-function(df, variable, fgroups, iter = 50, shape){ for(n in 1:length(fgroups)){ gr = fgroups[n] if(missing(shape)) { shape<-terra::buffer(terra::vect(cbind(0, 0), crs = "+proj=utm +zone=1"), 1, 30) } # df = data_4_voronoid # gr = "class" # variable = "weight" # shape = start_shape if(n==1){ dat<-df %>% group_by(!!sym(gr)) %>% summarise(weight = sum(!!sym(variable))) %>% as.data.frame() res<-voronoi_map(values = dat[,"weight"], groups = dat[, gr], iter = iter, accuracy = 0.0001, shape = shape) assign("dat1", dat) assign("res1", res) if(length(fgroups)==1){return(res)} else {res<-NULL}} else { #oldres<-res #oldgr = "order" #gr = "ID" oldres<-get(paste0("res", n-1)) oldgr = fgroups[n-1] for(x in unique(oldres$groups)){ # print(oldres$groups) # print(oldgr) # print(gr) newdat<-df %>% filter(!!sym(oldgr) == x) %>% group_by(!!sym(gr)) %>% summarise(weight = sum(!!sym(variable))) %>% as.data.frame() # print(newdat) out<-voronoi_map(values = newdat$weight, groups = newdat[,gr], iter = 50, shape = oldres$geom[which(oldres$groups==x)]) if(is.null(res)){res<-list(geom = out$geom, sites = out$sites, areas = out$areas, groups = out$groups, values = out$values)} else{ a <- geom(res$geom, wkt=TRUE) b <- geom(out$geom, wkt=TRUE) ab<-c(a,b) res$geom<-terra::vect(ab, "polygons") # res$geom<-terra::union(res$geom, out$geom) res$sites<-terra::union(res$sites, out$sites) # kt[[1]]$shape<-terra::union(kt[[1]]$shape, # kt[[n]]$shape) res$areas<-c(res$areas, out$areas) res$groups<-c(as.character(res$groups), as.character(out$groups)) res$values<-c(res$values, out$values) } # print(sapply(res, length)) } if(n==length(fgroups)){ res<-structure(res, class = "voronoi_map") cat("Process has finished.") return(res)} else {assign(paste0("res", n), res) res<-NULL} } }}
In this code, I highly rely on VoroidPlus but rebuild the nested system and plot it nicely. However, for plotting the outlines of the larger category, I had to re-run the function again.
# the main map, containing all groupings qq<-voron_tree(dat, "weight", fgroups = c("class", "order", "ID"), iter = 500, shape = start_shape) # the parent groups to facilitate colouring qq1<-voron_tree(dat, "weight", fgroups = "class", iter = 500, shape = start_shape) # setting both maps to the same crs crs(qq$geom)<-crs(qq1$geom) # setting the colour col_vector<-c('#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4') P36<-createPalette(40, col_vector) # change number to as many colours as you want
Should not forget to set the projection for both maps, and, of course, choose the right colours.
s<-sf::st_as_sf(qq$geom) s$ind_nonind<-dat$ind_nonind s$order<-dat$order s$class<-dat$class ggplot(s) + geom_sf_pattern(data = s, aes(fill = order, color = class, pattern_shape = ind_nonind, pattern_color = class, pattern_fill = order), pattern = "pch", pattern_spacing = 0.07, pattern_size = 0.3 ) + geom_spatvector(data = qq1$geom, fill = NA, aes(color = qq1$groups), linewidth = 1.5)+ scale_fill_manual(values = ordcols)+ scale_color_manual(values = classcols, guide = "none")+ scale_pattern_shape_manual(values = ind_nonind_pch, guide = "none")+ scale_pattern_fill_manual(values = ordcols, guide = "none")+ scale_pattern_color_manual(values = classcols, guide = "none")+ theme_void()+ theme(legend.position = "none", plot.margin = margin(1,0.1,0.1,0.1, "cm"))
For now, you can see the code but soon an explanation and a detailed investigation of how Voroid diagrams can work in ecology will come.