Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ LazyData: true
Roxygen: list(markdown = TRUE)
LinkingTo:
Rcpp
Imports:
Imports:
Rcpp,
igraph,
reticulate,
interp
Depends:
R (>= 3.5)
Expand Down
1 change: 0 additions & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export(edge_bundle_force)
export(edge_bundle_hammer)
export(edge_bundle_path)
export(edge_bundle_stub)
export(install_bundle_py)
export(metro_multicriteria)
export(tnss_dummies)
export(tnss_smooth)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* fixed `edge_bundle_stub()`: the angular grouping was broken (wrong circular-gap handling and a bundle-size cap that summed cluster ids instead of counting edges) and vertical edges could produce `NaN`. Bundling output changes as a result
* fixed `edge_bundle_path()`: the path-length used in the distortion test was computed along the wrong vertices, so edges were essentially never routed. Bundling output changes as a result
* added divided edge bundling for directed graphs via `edge_bundle_force(directed = TRUE)` (Selassie et al. 2011): edges running in opposite directions are kept in separate lanes
* `edge_bundle_hammer()` is now a native C++ implementation of KDE edge bundling (Hurter et al. 2012) and no longer depends on Python/`reticulate`/datashader. `reticulate` was dropped from Imports and `install_bundle_py()` was removed. Bundling output differs from the datashader-based version

# edgebundle 0.4.2

Expand Down
4 changes: 4 additions & 0 deletions R/RcppExports.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ force_bundle_iter <- function(edges_xy, K, C, P, P_rate, S, I, I_rate, compatibi
.Call(`_edgebundle_force_bundle_iter`, edges_xy, K, C, P, P_rate, S, I, I_rate, compatibility_threshold, eps)
}

kdeeb_iter <- function(edges_xy, npoints, niter, bw, decay, grid, step, smooth_passes) {
.Call(`_edgebundle_kdeeb_iter`, edges_xy, npoints, niter, bw, decay, grid, step, smooth_passes)
}

criterion_angular_resolution <- function(adj, xy) {
.Call(`_edgebundle_criterion_angular_resolution`, adj, xy)
}
Expand Down
93 changes: 30 additions & 63 deletions R/bundle_hammer.R
Original file line number Diff line number Diff line change
@@ -1,74 +1,41 @@
#' @title hammer edge bundling
#' @description Implements the hammer edge bundling by Ian Calvert.
#' @details This function only wraps existing python code from the datashader library. Original code can be found at https://gitlab.com/ianjcalvert/edgehammer.
#' Datashader is a huge library with a lot of dependencies, so think twice if you want to install it just for edge bundling.
#' Check https://datashader.org/user_guide/Networks.html for help concerning parameters bw and decay.
#' To install all dependencies, use [install_bundle_py].
#' @description Implements hammer edge bundling via kernel density estimation (KDEEB).
#' @details Native re-implementation of the KDE-based bundling behind the
#' datashader "hammer" bundler, following Hurter, Ersoy and Telea (2012). Earlier
#' versions of this function wrapped datashader through `reticulate`; it now runs
#' entirely in C++ with no Python dependency. Edges are sampled into points, a
#' density field is estimated on a grid, points are advected up the density
#' gradient, and the bandwidth shrinks each iteration so bundles emerge along
#' density ridges.
#' @param object a graph object (igraph/network/tbl_graph)
#' @param xy coordinates of vertices
#' @param bw bandwidth parameter
#' @param decay decay parameter
#' @param bw initial bandwidth (fraction of the layout extent)
#' @param decay bandwidth decay per iteration (0-1)
#' @param npoints number of points sampled per edge
#' @param iterations number of bundling iterations
#' @param grid resolution of the density grid
#' @param step advection step size (multiple of the bandwidth)
#' @param smooth number of smoothing passes per iteration
#' @return data.frame containing the bundled edges
#' @author David Schoch
#' @details see [online](https://github.com/schochastics/edgebundle) for plotting tips
#' @seealso [edge_bundle_force],[edge_bundle_stub], [edge_bundle_path]
#' @references
#' Hurter, Christophe, Ozan Ersoy, and Alexandru Telea. "Graph Bundling by Kernel Density Estimation." Computer Graphics Forum 31, no. 3 (2012): 865-874.
#' @examples
#' library(igraph)
#' g <- graph_from_edgelist(
#' matrix(c(1, 12, 2, 11, 3, 10, 4, 9, 5, 8, 6, 7), ncol = 2, byrow = TRUE), FALSE
#' )
#' xy <- cbind(c(rep(0, 6), rep(1, 6)), c(1:6, 1:6))
#' edge_bundle_hammer(g, xy)
#' @export
edge_bundle_hammer <- function(object, xy, bw = 0.05, decay = 0.7, npoints = 50,
iterations = 12, grid = 256, step = 0.6, smooth = 1) {
edges_xy <- .bundle_inputs(object, xy)$exy
m <- nrow(edges_xy)

edge_bundle_hammer <- function(object, xy, bw = 0.05, decay = 0.7) {
if (!requireNamespace("reticulate", quietly = TRUE)) {
stop("The `reticulate` package is required for this functionality")
}
if (any(class(object) == "igraph")) {
if (!requireNamespace("igraph", quietly = TRUE)) {
stop("The `igraph` package is required for this functionality")
}
nodes <- data.frame(name = paste0("node", 0:(igraph::vcount(object) - 1)), x = xy[, 1], y = xy[, 2])
el <- igraph::as_edgelist(object, names = FALSE)
el1 <- data.frame(source = el[, 1] - 1, target = el[, 2] - 1)
} else if (any(class(object) == "tbl_graph")) {
if (!requireNamespace("tidygraph", quietly = TRUE)) {
stop("The `tidygraph` package is required for this functionality")
}
object <- tidygraph::as.igraph(object)
nodes <- data.frame(name = paste0("node", 0:(igraph::vcount(object) - 1)), x = xy[, 1], y = xy[, 2])
el <- igraph::as_edgelist(object, names = FALSE)
el1 <- data.frame(source = el[, 1] - 1, target = el[, 2] - 1)
} else if (any(class(object) == "network")) {
nodes <- data.frame(name = paste0("node", 0:(network::get.network.attribute(object, "n") - 1)), x = xy[, 1], y = xy[, 2])
el <- network::as.edgelist(object)
el1 <- data.frame(source = el[, 1] - 1, target = el[, 2] - 1)
} else {
stop("only `igraph`, `network` or `tbl_graph` objects supported.")
}
data_bundle <- shader_env$datashader_bundling$hammer_bundle(nodes, el1, initial_bandwidth = bw, decay = decay)
data_bundle$group <- is.na(data_bundle$y) + 0
data_bundle$group <- cumsum(data_bundle$group) + 1
data_bundle <- data_bundle[!is.na(data_bundle$y), ]
data_bundle$index <- unlist(sapply(table(data_bundle$group), function(x) seq(0, 1, length.out = x)))
data_bundle[, c("x", "y", "index", "group")]
}

#' @title install python dependencies for hammer bundling
#' @description install datashader and scikit-image
#' @param method Installation method (by default, "auto" automatically finds a
#' method that will work in the local environment, but note that the
#' "virtualenv" method is not available on Windows)
#' @param conda Path to conda executable (or "auto" to find conda using the PATH
#' and other conventional install locations)
#' @export
#'
install_bundle_py <- function(method = "auto", conda = "auto") {
if (!requireNamespace("reticulate", quietly = TRUE)) {
stop("The `reticulate` package is required for this functionality")
}
reticulate::py_install("datashader", method = method, conda = conda, pip = TRUE)
reticulate::py_install("scikit-image", method = method, conda = conda, pip = TRUE)
}

# Environment for globals
shader_env <- new.env(parent = emptyenv())
elist <- kdeeb_iter(edges_xy, npoints, iterations, bw, decay, grid, step, smooth)

.onLoad <- function(libname, pkgname) {
reticulate::configure_environment(pkgname)
assign("datashader_bundling", reticulate::import("datashader.bundling", delay_load = TRUE), shader_env)
.as_bundle_df(do.call("rbind", elist), m, npoints)
}
50 changes: 42 additions & 8 deletions man/edge_bundle_hammer.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 0 additions & 19 deletions man/install_bundle_py.Rd

This file was deleted.

19 changes: 19 additions & 0 deletions src/RcppExports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ BEGIN_RCPP
return rcpp_result_gen;
END_RCPP
}
// kdeeb_iter
List kdeeb_iter(NumericMatrix edges_xy, int npoints, int niter, double bw, double decay, int grid, double step, int smooth_passes);
RcppExport SEXP _edgebundle_kdeeb_iter(SEXP edges_xySEXP, SEXP npointsSEXP, SEXP niterSEXP, SEXP bwSEXP, SEXP decaySEXP, SEXP gridSEXP, SEXP stepSEXP, SEXP smooth_passesSEXP) {
BEGIN_RCPP
Rcpp::RObject rcpp_result_gen;
Rcpp::RNGScope rcpp_rngScope_gen;
Rcpp::traits::input_parameter< NumericMatrix >::type edges_xy(edges_xySEXP);
Rcpp::traits::input_parameter< int >::type npoints(npointsSEXP);
Rcpp::traits::input_parameter< int >::type niter(niterSEXP);
Rcpp::traits::input_parameter< double >::type bw(bwSEXP);
Rcpp::traits::input_parameter< double >::type decay(decaySEXP);
Rcpp::traits::input_parameter< int >::type grid(gridSEXP);
Rcpp::traits::input_parameter< double >::type step(stepSEXP);
Rcpp::traits::input_parameter< int >::type smooth_passes(smooth_passesSEXP);
rcpp_result_gen = Rcpp::wrap(kdeeb_iter(edges_xy, npoints, niter, bw, decay, grid, step, smooth_passes));
return rcpp_result_gen;
END_RCPP
}
// criterion_angular_resolution
double criterion_angular_resolution(List adj, NumericMatrix xy);
RcppExport SEXP _edgebundle_criterion_angular_resolution(SEXP adjSEXP, SEXP xySEXP) {
Expand Down Expand Up @@ -135,6 +153,7 @@ END_RCPP
static const R_CallMethodDef CallEntries[] = {
{"_edgebundle_divided_bundle_iter", (DL_FUNC) &_edgebundle_divided_bundle_iter, 13},
{"_edgebundle_force_bundle_iter", (DL_FUNC) &_edgebundle_force_bundle_iter, 10},
{"_edgebundle_kdeeb_iter", (DL_FUNC) &_edgebundle_kdeeb_iter, 8},
{"_edgebundle_criterion_angular_resolution", (DL_FUNC) &_edgebundle_criterion_angular_resolution, 2},
{"_edgebundle_criterion_edge_length", (DL_FUNC) &_edgebundle_criterion_edge_length, 3},
{"_edgebundle_criterion_balanced_edge_length", (DL_FUNC) &_edgebundle_criterion_balanced_edge_length, 2},
Expand Down
Loading
Loading