#' custom igraph vertex shape coloredrectangle
#'
#' custom igraph vertex shape coloredrectangle
#'
#' This function defines the plotting function for custom igraph vertex
#' shape coloredrectangle. The coloredrectangle shape is described as:
#'
#' * Each vertex is drawn as a rectangle, filled with squares which are
#' each individually colored.
#' * The squares are arrayed into a number of columns and rows,
#' which are defined for each vertex using `coloredrect.ncol` and
#' `coloredrect.nrow`, respectively.
#' * The vector of colors is arrayed as values of a matrix, therefore
#' `coloredrect.byrow` is logical to indicate whether colors should fill
#' the matrix by row, similar to how `byrow=TRUE` is used.
#' * The colors for each vertex are defined by `coloredrect.color`,
#' which is a `list` of `character` color vectors.
#' When `coloredrect.color` does not exist, values from `pie.color`
#' will be used if they exist.
#' * Any missing colors are displayed as `NA` which applies no color.
#' For example, when a vertex has 3 columns, 2 rows, and only 5 colors,
#' the colors are not recycled. Instead, the last color is `NA` to
#' render no color in that position.
#' * Each square may also have an optional border, defined by
#' `coloredrect.border`, `coloredrect.lwd`, and `coloredrect.lty`.
#' These borders are drawn as inner borders so they do not overlap
#' optional frame border.
#' * Each node may have a frame border, defined by `frame.color`,
#' `frame.lwd`, and `frame.lty`. The frame border is drawn as an outer
#' border so it does not overlap inner borders, adding some height and width
#' to the final node. The frame border is drawn before the
#' node squares are drawn.
#' * The size of each square inside the rectangle is defined by `size2`,
#' such that the rectangle width is `coloredrect.ncol * size2` and
#' the rectangle height is `coloredrect.nrow * size2`.
#' * Node sizes may be adjusted by enabling `equalize_sizes`
#' in one of two ways:
#'
#' 1. `options(coloredrectangle.equalize_sizes=TRUE)`
#' (priority); or
#' 2. `vertex.coloredrect.equalize_sizes=TRUE`,
#' which is equivalent to `igraph::V(g)$coloredrect.equalize_sizes=TRUE`.
#' in the latter case, only the first value is recognized.
#'
#' * The behavior of `equalize_sizes` is described below:
#'
#' * `equalize_sizes=FALSE` (default), nodes are exactly `size2`
#' multiples of `coloredrect.ncol` and `coloredrect.nrow`.
#' * `equalize_sizes=TRUE` or `1`, the shortest side of each node
#' is scaled to `size2`. This option is useful to ensure nodes are never
#' smaller than `size2` width or height, but can be larger.
#' * `equalize_sizes=2`, the longest side of each node is scaled to
#' `size2`. This option is useful to ensure nodes fit insides a square
#' with `size2` sides.
#'
#' * Nodes are drawn using vectorized processes where possible.
#' However, the primary function `graphics::symbols()` only permits
#' vectorized plotting for one `lwd`/`lty` combination at a time,
#' so rendering is split into unique combinations of `lwd`/`lty`.
#' Vectorized rendering is substantially faster than iterative rendering,
#' and the majority of circumstances uses only one `lwd`/`lty` combination
#' for all nodes. Line width is not an optimal way to convey a quantitative
#' measurement, however it can be useful to highlight particular nodes
#' of interest.
#' * When `coloredrect.ncol` does not exist, the `ncol` will be defined
#' by allowing up to two rows by default, enough to accommodate
#' the number of colors in `coloredrect.color`.
#' * When `coloredrect.ncol * coloredrect.nrow` will not fit all
#' colors in `coloredrect.color`, the `coloredrect.ncol` will be
#' extended to create enough positions to display all colors.
#' * When `coloredrect.nrow` does not exist, it will use a value
#' based upon `coloredrect.ncol` and the length of `coloredrect.color`.
#'
#' The values `coloredrect.color` and other variables described above refer to
#' `igraph` vertex attributes, and can be accessed for a given `igraph`
#' object `g` as follows:
#'
#' * `igraph::V(g)$coloredrect.color`
#' * `igraph::vertex_attr(g, "coloredrect.color")`
#' * or during plotting the value can be defined using the syntax
#' `igraph::plot(g, vertex.coloredrect.color=list(...))`
#'
#' Note that blank positions inside coloredrectangle nodes can be removed
#' via `removeIgraphBlanks()`, which also has the effect of modifying the
#' `coloredrect.ncol` and `coloredrect.nrow`, by applying the appropriate
#' logic.
#'
#' @return the plot function returns an invisible `list` of
#' `data.frame` objects which were used to draw the rectangle objects.
#' However the purpose of this function is the by-product that it
#' draws rectangles onto an igraph graph.
#' The clip function returns coordinates corresponding to the outer
#' node edge based upon argument `end`:
#' * `end="both"` returns 4 columns, the x,y 'edge from' and
#' x,y 'edge to' coordinates, respectively
#' * `end="from"` returns 2 columns, the x,y 'edge from' coordinates
#' * `end="to"` returns 2 columns, the x,y 'edge to' coordinates
#'
#' @seealso `igraph::shapes()`
#'
#' @family jam igraph shapes
#'
#' @param coords two-column numeric matrix with x- and y-coordinates,
#' respectively.
#' @param v optional ids of the vertices to plot. It should match
#' the number of rows in the `coords` argument.
#' @param params a function object that can be called to query
#' vertex/edge/plot graphical parameters. The first argument of
#' the function is `'vertex'`, `'edge'` or `'plot'` to decide
#' the type of the parameter, the second is a character string
#' giving the name of the parameter.
#'
#' @examples
#' # prepare example igraph object
#' am <- matrix(ncol=5, nrow=5,
#' data=0,
#' dimnames=list(LETTERS[1:5], LETTERS[1:5]))
#' am[2:5, 1] <- 1;
#' g1 <- igraph::graph_from_adjacency_matrix(am)
#' igraph::graph_attr(g1, "layout") <- cbind(x=c(0, 1, 1, -1, -1),
#' y=c(0, 1, -0.5, 0.5, -1))
#' colorset <- c("firebrick3", "gold", "deepskyblue",
#' "mediumpurple3", "orchid1")
#' colorset <- c("firebrick3", "dodgerblue3");
#' vseq <- seq_len(igraph::vcount(g1));
#' vsizes <- c(3, 2, 2, 2, 1);
#' set.seed(1);
#' igraph::V(g1)$coloredrect.border <- lapply(vseq, function(i){
#' sample(colorset,
#' replace=TRUE,
#' size=vsizes[i])
#' })
#' igraph::V(g1)$coloredrect.color <- lapply(vseq, function(i){
#' jamba::alpha2col(alpha=0.5,
#' igraph::V(g1)$coloredrect.border[[i]])
#' })
#' igraph::V(g1)$coloredrect.ncol <- c(2, 2, 2, 1, 1);
#' igraph::V(g1)$coloredrect.nrow <- c(2, 1, 1, 2, 1);
#' igraph::V(g1)$coloredrect.lwd <- rep(3, igraph::vcount(g1))
#' igraph::V(g1)$frame.lwd <- c(2, 1, 1, 1, 1);
#' igraph::V(g1)$frame.color <- "black"
#' igraph::V(g1)$size2 <- 10;
#' igraph::V(g1)$shape <- "coloredrectangle";
#'
#' plot(g1, vertex.label="")
#' title(font.main=1, line=1.5, main=paste0(
#' "Each square is consistent size by vertex.size2\n"))
#' title(font.main=1, cex.main=1, line=0.5, main=paste0(
#' "'vertex.coloredrect.equalize_sizes=FALSE' or\n",
#' "'options(coloredrectangle.equalize_sizes=FALSE'"))
#'
#' # equalize shortest side to size2
#' plot(g1, vertex.label="", vertex.coloredrect.equalize_sizes=TRUE)
#' title(font.main=1, line=1.5, main=paste0(
#' "The shortest side is fixed by vertex.size2\n"))
#' title(font.main=1, cex.main=1, line=0.5, main=paste0(
#' "'vertex.coloredrect.equalize_sizes=TRUE' or\n",
#' "'options(coloredrectangle.equalize_sizes=TRUE'"))
#'
#' # equalize longest side to size2
#' plot(g1, vertex.label="", vertex.coloredrect.equalize_sizes=2)
#' title(font.main=1, line=1.5, main=paste0(
#' "The longest side is fixed by vertex.size2\n"))
#' title(font.main=1, cex.main=1, line=0.5, main=paste0(
#' "'vertex.coloredrect.equalize_sizes=2' or\n",
#' "'options(coloredrectangle.equalize_sizes=2'"))
#'
#' @export
shape.coloredrectangle.plot <- function
(coords,
v=NULL,
params)
{
## Purpose is to extend igraph:::.igraph.shape.rectangle.plot
## to handle the custom colored rectangle node type.
## Shape "coloredrectangle" has these features for each vertex:
## * the vertex is a rectangle filled with squares, where each
## square has its own assigned color.
## * the squares are arrayed into a number of columns defined
## by "coloredrect.ncol", and a number of rows defined by
## "coloredrect.nrow". Each vertex can have its own number
## of columns and rows, and its own number of colors.
## * the size of the overall rectangle is defined by "size2",
## which is normally the height of a rectangle shape. The
## intention was to allow using "size" for shapes such as
## "circle" but "size2" for
##
## This function combines some logic from
## igraph:::.igraph.shape.pie.plot() as well.
getparam <- function(pname) {
p <- params("vertex", pname)
if (length(p) != 1 && !is.null(v)) {
p <- p[v]
}
p;
}
vertex.coloredrect.equalize_sizes <- getparam("coloredrect.equalize_sizes");
if (length(vertex.coloredrect.equalize_sizes) == 0) {
vertex.coloredrect.equalize_sizes <- FALSE;
}
vertex.coloredrect.equalize_sizes <- head(vertex.coloredrect.equalize_sizes,
1);
# get relevant global options
verbose <- getOption("verbose", FALSE);
equalize_sizes <- getOption("coloredrectangle.equalize_sizes",
vertex.coloredrect.equalize_sizes)
vertex.color <- getparam("color");
vertex.frame.color <- getparam("frame.color");
if (length(vertex.frame.color) == 0) {
vertex.frame.color <- "grey30";
}
vertex.frame.lwd <- getparam("frame.lwd");
if (length(vertex.frame.lwd) == 0) {
vertex.frame.lwd <- rep(1,
length.out=length(vertex.frame.color));
}
vertex.frame.lwd <- ifelse(
is.na(vertex.frame.color) | jamba::col2alpha(vertex.frame.color) < 0.01,
0,
vertex.frame.lwd);
if (length(vertex.frame.lwd) == 0) {
vertex.frame.lwd <- 1;
}
vertex.coloredrect.color <- getparam("coloredrect.color");
if (length(vertex.coloredrect.color) == 0) {
vertex.coloredrect.color <- getparam("pie.color");
}
vertex.coloredrect.border <- getparam("coloredrect.border");
if (length(vertex.coloredrect.border) == 0) {
vertex.coloredrect.border <- "grey30";
#vertex.coloredrect.border <- vertex.frame.color;
} else if (is.list(vertex.coloredrect.border)) {
if (all(lengths(vertex.coloredrect.border) == 0)) {
vertex.coloredrect.border <- rep(
list("grey30"),
length.out=length(vertex.coloredrect.border));
}
}
if (!is.list(vertex.coloredrect.border)) {
vertex.coloredrect.border <- rep(
list(vertex.coloredrect.border),
length.out=length(vertex.coloredrect.color))
}
if (verbose) {
jamba::printDebug("shape.coloredrectangle.plot(): ",
"vertex.coloredrect.color:");
print(vertex.coloredrect.color);
}
vertex.coloredrect.byrow <- getparam("coloredrect.byrow") > 0;
if (length(vertex.coloredrect.byrow) == 0) {
vertex.coloredrect.byrow <- TRUE;
}
vertex.coloredrect.ncol <- getparam("coloredrect.ncol");
if (length(vertex.coloredrect.ncol) == 0) {
vertex.coloredrect.ncol <- ceiling(lengths(vertex.coloredrect.color)/2);
}
vertex.coloredrect.nrow <- getparam("coloredrect.nrow");
if (length(vertex.coloredrect.nrow) == 0) {
vertex.coloredrect.nrow <- ceiling(
lengths(vertex.coloredrect.color) /
vertex.coloredrect.ncol);
}
# validate the number of colors will fit inside ncol,nrow
vertex.coloredrect.ncells <- vertex.coloredrect.ncol * vertex.coloredrect.nrow;
if (any(lengths(vertex.coloredrect.color) > vertex.coloredrect.ncells)) {
# expand ncol to accommodate all colors
vertex.coloredrect.ncol <- pmax(vertex.coloredrect.ncol,
ceiling(lengths(vertex.coloredrect.color) / vertex.coloredrect.nrow))
}
vertex.coloredrect.lty <- getparam("coloredrect.lty");
if (length(vertex.coloredrect.lty) == 0) {
vertex.coloredrect.lty <- 1;
}
vertex.coloredrect.lwd <- getparam("coloredrect.lwd");
if (length(vertex.coloredrect.lwd) == 0) {
vertex.coloredrect.lwd <- 1;
}
vertex.coloredrect.lwd <- rep(vertex.coloredrect.lwd,
length.out=length(vertex.coloredrect.color))
vertex.coloredrect.lwd <- lapply(seq_along(vertex.coloredrect.border), function(i){
iborder <- vertex.coloredrect.border[[i]];
ilwd <- vertex.coloredrect.lwd[[i]];
ifelse(
is.na(iborder) | jamba::col2alpha(iborder) < 0.01,
0,
ilwd)
})
vertex.coloredrect.lwd.max <- sapply(vertex.coloredrect.lwd, max, na.rm=TRUE)
# version 0.0.68.900: refactor size1, size2 calculations
vertex.size1 <- getparam("size");
vertex.size2 <- getparam("size2");
if (length(vertex.size2) == 0) {
vertex.size2 <- vertex.size1;
}
if (any(is.na(vertex.size2))) {
vertex.size2 <- ifelse(is.na(vertex.size2), vertex.size1, vertex.size2);
}
# replace NA size1 with size2 / 2 (why divide by 2?)
size1_na <- is.na(vertex.size1)
if (any(size1_na)) {
vertex.size1[size1_na] <- vertex.size2[size1_na] / 2;
}
# convert size to graph coordinates
vertex.size1 <- rep(1/200 * vertex.size1, length.out=nrow(coords));
vertex.size2 <- rep(1/200 * vertex.size2, length.out=nrow(coords));
# Use size2 to define the size of each square
# with consistent square size for all nodes
vertex.size1 <- vertex.size2 * 5 * vertex.coloredrect.ncol;
vertex.size2 <- vertex.size2 * 5 * vertex.coloredrect.nrow;
# optionally adjust nodes for consistent max height/width
if (equalize_sizes) {
if (2 %in% equalize_sizes) {
# method 2: the longest side is fixed at size2
vertex.size1 <- vertex.size1 /
pmax(vertex.coloredrect.ncol, vertex.coloredrect.nrow);
vertex.size2 <- vertex.size2 /
pmax(vertex.coloredrect.ncol, vertex.coloredrect.nrow);
} else {
# method 3: the shortest side is fixed at size2
vertex.size1 <- vertex.size1 /
pmin(vertex.coloredrect.ncol, vertex.coloredrect.nrow);
vertex.size2 <- vertex.size2 /
pmin(vertex.coloredrect.ncol, vertex.coloredrect.nrow);
}
}
if (verbose) {
jamba::printDebug("shape.coloredrectangle.plot(): ",
"vertex.size1:", vertex.size1,
", vertex.coloredrect.nrow:", vertex.coloredrect.nrow);
jamba::printDebug("shape.coloredrectangle.plot(): ",
"vertex.size2:", vertex.size2,
", vertex.coloredrect.ncol:", vertex.coloredrect.ncol);
}
vertex.size <- cbind(vertex.size1, vertex.size2);
## Define custom function to help vectorize drawing, by creating a
## data.frame of coordinates for each square and rectangle
mycoloredrectangle <- function
(x,
y,
size1,
size2,
col,
ncol,
nrow,
border,
lty,
lwd=1,
frame.lwd=0.5,
frame.color="grey30",
byrow=TRUE,
...)
{
## Purpose is to draw a rectangle filles with multi-color squares
nrow <- rep(nrow, length.out=length(x));
ncol <- rep(ncol, length.out=length(x));
ncells <- nrow * ncol;
border <- rep(border, length.out=length(x));
lty <- rep(lty, length.out=length(x));
lwd <- rep(lwd, length.out=length(x));
frame.lwd <- rep(frame.lwd, length.out=length(x));
frame.color <- rep(frame.color, length.out=length(x));
byrow <- rep(byrow, length.out=length(x));
## Iterate each vertex, create a data.frame describing
## frame and square colors, then combine into one large
## data.frame for vectorized drawing.
rectDF <- jamba::rbindList(lapply(seq_along(x), function(k){
xk <- x[[k]];
yk <- y[[k]];
ncellk <- ncells[k];
size1k <- size1[[k]];
size2k <- size2[[k]];
nrowk <- jamba::rmNULL(nullValue=1, nrow[[k]]);
ncolk <- jamba::rmNULL(nullValue=1, ncol[[k]]);
byrowk <- byrow[[k]];
## Individual rectangles
x01 <- seq(from=xk-(size1k/2),
to=xk+(size1k/2),
length.out=ncolk+1);
y01 <- seq(from=yk-(size2k/2),
to=yk+(size2k/2),
length.out=nrowk+1);
numk <- ncolk * nrowk;
size1v <- rep(min(diff(unique(x01))), numk);
size2v <- rep(min(diff(unique(y01))), numk);
if (byrowk) {
x01v <- rep(head(x01, -1), nrowk)+(size1v/2);
y01v <- rep(rev(tail(y01, -1)), each=ncolk)-(size2v/2);
} else {
x01v <- rep(head(x01, -1), each=nrowk)+(size1v/2);
y01v <- rep(rev(tail(y01, -1)), ncolk)-(size2v/2);
}
## Make one large rectangle border
x01all <- mean(x01v);
y01all <- mean(y01v);
##
# colk <- rep(col[[k]], length.out=length(x01v));
colk <- rep(NA, length.out=length(x01v));
colk[seq_along(col[[k]])] <- col[[k]];
borderk <- rep(border[[k]], length.out=length(colk));
# borderk <- rep(NA, length.out=length(x01v));
# borderk[seq_along(border[[k]])] <- border[[k]];
ltyk <- rep(lty[[k]], length.out=length(colk));
lwdk <- rep(lwd[[k]], length.out=length(colk));
if (TRUE %in% getOption("debug", FALSE)) {
jamba::printDebug("k:", k,
", numk:", numk,
", byrowk:", byrowk,
", ncolk:", ncolk,
", nrowk:", nrowk,
", xk:", xk,
", x01v:", signif(digits=3, x01v),
", x01:", signif(digits=3, x01),
", yk:", yk,
", y01v:", signif(digits=3, y01v),
", y01:", signif(digits=3, y01),
", colk:", colk,
", size1v:", signif(digits=3, size1v),
", size2v:", signif(digits=3, size2v)
);
}
kDF <- data.frame(
stringsAsFactors=FALSE,
x=c(xk, x01v),
y=c(yk, y01v),
bg=c("transparent", colk),
fg=c(head(frame.color[[k]],1),
borderk),
rectx=c(head(size1v,1)*ncolk, size1v),
recty=c(head(size2v,1)*nrowk, size2v),
lty=c(head(ltyk,1), ltyk),
## lwd=c(head(frame.lwd[[k]], 1)*2.5, wdk/4),
# lwd=c(head(frame.lwd[[k]], 1) * 1, lwdk),
lwd=c(frame.lwd[[k]], lwdk),
rect_type=rep(factor(c("frame","square")),
c(1,length(borderk)))
);
kDF;
}));
## Split into a list of data.frames, because symbols()
## can only use one value for lwd and lty.
rectDFL <- split(rectDF,
jamba::pasteByRowOrdered(rectDF[,c("rect_type","lwd","lty")]));
if (verbose) {
jamba::printDebug("shape.coloredrectangle.plot(): ",
"names(rectDFL):", names(rectDFL));
}
rect_order <- jamba::provigrep(c("frame", "square", "."), names(rectDFL));
for (rectDFi in rectDFL[rect_order]) {
rectDFi$lwd <- ifelse(is.na(rectDFi$fg), 1, rectDFi$lwd);
rectDFi$lwd <- ifelse(is.na(rectDFi$lwd) | rectDFi$lwd == 0,
1, rectDFi$lwd);
# jamba::printDebug("rectDFi$lwd:");print(rectDFi$lwd);
# jamba::printDebug("head(rectDFi):");print(head(rectDFi));
if ("square" %in% rectDFi$rect_type[1]) {
# jamba::printDebug("inner lwd: ", rectDFi$lwd);
inner_coords <- adjust_rect_border(
x=rectDFi$x,
y=rectDFi$y,
rectangles=as.matrix(rectDFi[,c("rectx","recty")]),
type="inner",
lwd=rectDFi$lwd);
rectDFi$rectx <- inner_coords$rectangles[,1];
rectDFi$recty <- inner_coords$rectangles[,2];
} else {
# jamba::printDebug("outer lwd: ", rectDFi$lwd);
outer_coords <- adjust_rect_border(
x=rectDFi$x,
y=rectDFi$y,
rectangles=as.matrix(rectDFi[,c("rectx","recty")]),
type="outer",
lwd=rectDFi$lwd);
rectDFi$rectx <- outer_coords$rectangles[,1];
rectDFi$recty <- outer_coords$rectangles[,2];
}
graphics::symbols(x=rectDFi$x,
y=rectDFi$y,
bg=rectDFi$bg,
fg=rectDFi$fg,
rectangles=as.matrix(rectDFi[,c("rectx","recty")]),
add=TRUE,
inches=FALSE,
lty=rectDFi$lty,
lwd=rectDFi$lwd,
xpd=TRUE);
}
return(rectDFL);
}
## For reference, the code below draws a single rectangle
if (FALSE) {
graphics::symbols(x=coords[, 1],
y=coords[, 2],
bg=vertex.color,
fg=vertex.frame.color,
rectangles=2*vertex.size,
add=TRUE,
inches=FALSE);
}
mcr <- mycoloredrectangle(x=coords[,1],
y=coords[,2],
size1=vertex.size1,
size2=vertex.size2,
col=vertex.coloredrect.color,
ncol=vertex.coloredrect.ncol,
nrow=vertex.coloredrect.nrow,
border=vertex.coloredrect.border,
lty=vertex.coloredrect.lty,
lwd=vertex.coloredrect.lwd,
frame.lwd=vertex.frame.lwd,
frame.color=vertex.frame.color,
byrow=vertex.coloredrect.byrow);
}
#' @details
#' Todo: The clip function should adjust the node boundary to account
#' for `frame.color` and `frame.lwd` when present, since the frame is
#' drawn as an outer border and slightly increases the size of the node.
#'
#' @family jam igraph shapes
#'
#' @rdname shape.coloredrectangle.plot
#'
#' @export
shape.coloredrectangle.clip <- function
(coords,
el,
params,
end=c("both", "from", "to"))
{
end <- match.arg(end)
if (length(coords) == 0) {
return(coords)
}
getparam <- function(pname) {
p <- params("vertex", pname)
p;
}
vertex.coloredrect.equalize_sizes <- getparam("coloredrect.equalize_sizes");
if (length(vertex.coloredrect.equalize_sizes) == 0) {
vertex.coloredrect.equalize_sizes <- FALSE;
}
vertex.coloredrect.equalize_sizes <- head(vertex.coloredrect.equalize_sizes,
1);
# get relevant global options
verbose <- getOption("verbose", FALSE);
equalize_sizes <- getOption("coloredrectangle.equalize_sizes", vertex.coloredrect.equalize_sizes)
vertex.coloredrect.color <- getparam("coloredrect.color");
if (length(vertex.coloredrect.color) == 0) {
vertex.coloredrect.color <- getparam("pie.color");
}
vertex.coloredrect.ncol <- getparam("coloredrect.ncol");
if (length(vertex.coloredrect.ncol) == 0) {
vertex.coloredrect.ncol <- ceiling(lengths(vertex.coloredrect.color)/2);
}
vertex.coloredrect.nrow <- getparam("coloredrect.nrow");
if (length(vertex.coloredrect.nrow) == 0) {
vertex.coloredrect.nrow <- ceiling(
lengths(vertex.coloredrect.color) /
vertex.coloredrect.ncol);
}
# validate the number of colors will fit inside ncol,nrow
vertex.coloredrect.ncells <- vertex.coloredrect.ncol * vertex.coloredrect.nrow;
if (any(lengths(vertex.coloredrect.color) > vertex.coloredrect.ncells)) {
# expand ncol to accommodate all colors
vertex.coloredrect.ncol <- pmax(vertex.coloredrect.ncol,
ceiling(lengths(vertex.coloredrect.color) / vertex.coloredrect.nrow))
}
# version 0.0.68.900: refactor size1, size2 calculations
vertex.size1 <- getparam("size");
vertex.size2 <- getparam("size2");
if (length(vertex.size2) == 0) {
vertex.size2 <- vertex.size1;
}
if (any(is.na(vertex.size2))) {
vertex.size2 <- ifelse(is.na(vertex.size2), vertex.size1, vertex.size2);
}
# replace NA size1 with size2 / 2 (why divide by 2?)
size1_na <- is.na(vertex.size1)
if (any(size1_na)) {
vertex.size1[size1_na] <- vertex.size2[size1_na] / 2;
}
# convert size to graph coordinates
vertex.size1 <- rep(1/200 * vertex.size1, length.out=nrow(coords));
vertex.size2 <- rep(1/200 * vertex.size2, length.out=nrow(coords));
# Use size2 to define the size of each square
# with consistent square size for all nodes
vertex.size1 <- vertex.size2 * 2.5 * vertex.coloredrect.ncol;
vertex.size2 <- vertex.size2 * 2.5 * vertex.coloredrect.nrow;
# optionally adjust nodes for consistent max height/width
if (equalize_sizes) {
if (2 %in% equalize_sizes) {
# method 2: the longest side is fixed at size2
vertex.size1 <- vertex.size1 /
pmax(vertex.coloredrect.ncol, vertex.coloredrect.nrow);
vertex.size2 <- vertex.size2 /
pmax(vertex.coloredrect.ncol, vertex.coloredrect.nrow);
} else {
# method 1: the shortest side is fixed at size2
vertex.size1 <- vertex.size1 /
pmin(vertex.coloredrect.ncol, vertex.coloredrect.nrow);
vertex.size2 <- vertex.size2 /
pmin(vertex.coloredrect.ncol, vertex.coloredrect.nrow);
}
}
vertex.size <- vertex.size1;
if (verbose) {
jamba::printDebug("vertex.size1: ", vertex.size1);
jamba::printDebug("vertex.size2: ", vertex.size2);
}
rec.shift <- function(x0, y0, x1, y1, vsize, vsize2) {
m <- (y0 - y1)/(x0 - x1)
l <- cbind(
x1 - vsize/m,
y1 - vsize2,
x1 - vsize,
y1 - vsize * m,
x1 + vsize2/m,
y1 + vsize2,
x1 + vsize,
y1 + vsize * m)
v <- cbind(
x1 - vsize <= l[, 1] &
l[, 1] <= x1 + vsize &
y1 - vsize2 <= l[, 2] &
l[, 2] <= y1 + vsize2,
x1 - vsize <= l[, 3] &
l[, 3] <= x1 + vsize &
y1 - vsize2 <= l[, 4] &
l[, 4] <= y1 + vsize2,
x1 - vsize <= l[, 5] &
l[, 5] <= x1 + vsize &
y1 - vsize2 <= l[, 6] &
l[, 6] <= y1 + vsize2,
x1 - vsize <= l[, 7] &
l[, 7] <= x1 + vsize &
y1 - vsize2 <= l[, 8] &
l[, 8] <= y1 + vsize2)
d <- cbind(
(l[, 1] - x0)^2 + (l[, 2] - y0)^2,
(l[, 3] - x0)^2 + (l[, 4] - y0)^2,
(l[, 5] - x0)^2 + (l[, 6] - y0)^2,
(l[, 7] - x0)^2 + (l[, 8] - y0)^2)
t(sapply(seq_len(nrow(l)), function(x) {
d[x, ][!v[x, ]] <- Inf
m <- which.min(d[x, ])
l[x, c(m * 2 - 1, m * 2)]
}))
}
if (end %in% c("from", "both")) {
vsize <- if (length(vertex.size) == 1) {
vertex.size
} else {
vertex.size[el[, 1]]
}
if (length(vertex.size2) == 1) {
vsize2 <- vertex.size2
} else {
vsize2 <- vertex.size2[el[, 1]]
}
res <- res1 <- rec.shift(
coords[, 3],
coords[, 4],
coords[, 1],
coords[, 2],
vsize,
vsize2)
}
if (end %in% c("to", "both")) {
if (length(vertex.size) == 1) {
vsize <- vertex.size
} else {
vsize <- vertex.size[el[, 2]]
}
if (length(vertex.size2) == 1) {
vsize2 <- vertex.size2
} else {
vsize2 <- vertex.size2[el[, 2]]
}
res <- res2 <- rec.shift(
coords[, 1],
coords[, 2],
coords[, 3],
coords[, 4],
vsize,
vsize2)
}
if (end == "both") {
res <- cbind(res1, res2)
}
res
}
#' plot function for igraph vertex shape ellipse
#'
#' plot function for igraph vertex shape ellipse
#'
#' This function defines the plotting function for custom igraph vertex
#' shape ellipse.
#'
#' @family jam igraph shapes
#'
#' @export
shape.ellipse.plot <- function
(coords,
v=NULL,
params)
{
vertex.color <- params("vertex", "color");
if (length(vertex.color) != 1 && !is.null(v)) {
vertex.color <- vertex.color[v];
}
vertex.frame.color <- params("vertex", "frame.color");
if (length(vertex.frame.color) != 1 && !is.null(v)) {
vertex.frame.color <- vertex.frame.color[v];
}
vertex.frame.width <- params("vertex", "frame.width");
if (length(vertex.frame.width) == 0) {
vertex.frame.width <- 1;
}
if (length(vertex.frame.width) != 1 && !is.null(v)) {
vertex.frame.width <- vertex.frame.width[v];
}
vertex.size <- 1/200 * params("vertex", "size");
if (length(vertex.size) != 1 && !is.null(v)) {
vertex.size <- vertex.size[v];
}
vertex.ellipse.ratio <- params("vertex", "ellipse.ratio");
if (length(vertex.ellipse.ratio) == 0) {
vertex.ellipse.ratio <- 2;
}
if (length(vertex.ellipse.ratio) != 1 && !is.null(v)) {
vertex.ellipse.ratio <- vertex.ellipse.ratio[v];
}
# angle of rotation of the ellipse, clockwise in degrees
vertex.ellipse.angle <- params("vertex", "ellipse.angle");
if (length(vertex.ellipse.angle) == 0) {
vertex.ellipse.angle <- 0;
}
if (length(vertex.ellipse.angle) != 1 && !is.null(v)) {
vertex.ellipse.angle <- vertex.ellipse.angle[v];
}
drawEllipse(x=coords[,1],
y=coords[,2],
a=vertex.size,
b=vertex.size/vertex.ellipse.ratio,
col=vertex.color,
border=vertex.frame.color,
lwd=vertex.frame.width,
angle=vertex.ellipse.angle,
draw=TRUE);
}
#' clip function for igraph vertex shape ellipse
#'
#' clip function for igraph vertex shape ellipse
#'
#' This function defines the clipping function for custom igraph vertex
#' shape ellipse.
#'
#' @family jam igraph shapes
#'
#' @export
shape.ellipse.clip <- function
(coords,
el,
params,
end=c("both", "from", "to"))
{
end <- match.arg(end)
if (length(coords) == 0) {
return(coords)
}
# vertex size
vertex.size <- 1/200 * params("vertex", "size");
# vertex ellipse ratio, height:width
vertex.ellipse.ratio <- params("vertex", "ellipse.ratio");
if (length(vertex.ellipse.ratio) == 0) {
vertex.ellipse.ratio <- 2;
}
vertex.ellipse.angle <- params("vertex", "ellipse.angle");
if (length(vertex.ellipse.angle) == 0) {
vertex.ellipse.angle <- 0;
}
# angle of rotation of the ellipse, clockwise in degrees
vertex.ellipse.angle <- params("vertex", "ellipse.angle");
if (length(vertex.ellipse.angle) == 0) {
vertex.ellipse.angle <- 0;
}
vertex_count <- max(c(
length(vertex.size),
length(vertex.ellipse.ratio),
length(vertex.ellipse.angle)))
vertex.size <- rep(vertex.size, length.out=vertex_count);
vertex.ellipse.ratio <- rep(vertex.ellipse.ratio, length.out=vertex_count);
vertex.ellipse.angle <- rep(vertex.ellipse.angle, length.out=vertex_count);
# ellipse width (a) and height (b) for un-rotated ellipses
# a <- vertex.size;
# b <- vertex.size / vertex.ellipse.ratio;
if (end %in% c("from", "both")) {
if (length(vertex.size) == 1) {
vsize.from <- vertex.size
} else {
vsize.from <- vertex.size[el[, 1]]
}
if (length(vertex.ellipse.ratio) == 1) {
vratio.from <- vertex.ellipse.ratio
} else {
vratio.from <- vertex.ellipse.ratio[el[, 1]]
}
if (length(vertex.ellipse.angle) == 1) {
vangle.from <- vertex.ellipse.angle;
} else {
vangle.from <- vertex.ellipse.angle[el[, 1]]
}
a <- vsize.from;
b <- vsize.from / vratio.from;
x <- coords[, 1];
y <- coords[, 2];
x1 <- coords[, 3];
y1 <- coords[, 4];
angle <- rep(-vangle.from,
length.out=nrow(coords));
# rotation matrix adjustments
co <- cos(-jamba::deg2rad(angle));
si <- sin(-jamba::deg2rad(angle));
# points rotated along with ellipse, around ellipse center
x2 <- co * (x1 - x) - si * (y1 - y) + x;
y2 <- si * (x1 - x) + co * (y1 - y) + y;
# points relative to ellipse center
x20 <- x2 - x;
y20 <- y2 - y;
# intercept with ellipse
x_int <- (a * b) / sqrt(a^2 * y20^2 + b^2 * x20^2) * x20 + x
y_int <- (a * b) / sqrt(a^2 * y20^2 + b^2 * x20^2) * y20 + y
# un-rotate intersecting point
co2 <- cos(-jamba::deg2rad(-angle));
si2 <- sin(-jamba::deg2rad(-angle));
x_int_rot <- co2 * (x_int - x) - si2 * (y_int - y) + x;
y_int_rot <- si2 * (x_int - x) + co2 * (y_int - y) + y;
x_from <- x_int_rot;
y_from <- y_int_rot
} else {
x_from <- coords[, 1];
y_from <- coords[, 2];
}
# end="to"
if (end %in% c("to", "both")) {
if (length(vertex.size) == 1) {
vsize.to <- vertex.size
} else {
vsize.to <- vertex.size[el[, 2]]
}
if (length(vertex.ellipse.ratio) == 1) {
vratio.to <- vertex.ellipse.ratio
} else {
vratio.to <- vertex.ellipse.ratio[el[, 2]]
}
if (length(vertex.ellipse.angle) == 1) {
vangle.to <- vertex.ellipse.angle
} else {
vangle.to <- vertex.ellipse.angle[el[, 2]]
}
a <- vsize.to;
b <- vsize.to / vratio.to;
x <- coords[, 3];
y <- coords[, 4];
x1 <- coords[, 1];
y1 <- coords[, 2];
angle <- rep(-vangle.to,
length.out=nrow(coords));
# rotation matrix adjustments
co <- cos(-jamba::deg2rad(angle));
si <- sin(-jamba::deg2rad(angle));
# points rotated along with ellipse, around ellipse center
x2 <- co * (x1 - x) - si * (y1 - y) + x;
y2 <- si * (x1 - x) + co * (y1 - y) + y;
# points relative to ellipse center
x20 <- x2 - x;
y20 <- y2 - y;
# intercept with ellipse
x_int <- (a * b) / sqrt(a^2 * y20^2 + b^2 * x20^2) * x20 + x
y_int <- (a * b) / sqrt(a^2 * y20^2 + b^2 * x20^2) * y20 + y
# un-rotate intersecting point
co2 <- cos(-jamba::deg2rad(-angle));
si2 <- sin(-jamba::deg2rad(-angle));
x_int_rot <- co2 * (x_int - x) - si2 * (y_int - y) + x;
y_int_rot <- si2 * (x_int - x) + co2 * (y_int - y) + y;
x_to <- x_int_rot;
y_to <- y_int_rot
} else {
x_to <- coords[, 1];
y_to <- coords[, 2];
}
if (end == "from") {
res <- cbind(
x_from,
y_from);
} else if (end == "to") {
res <- cbind(
x_to,
y_to);
} else if (end == "both") {
res <- cbind(
x_from,
y_from,
x_to,
y_to);
}
res
}
#' custom igraph vertex shape jampie
#'
#' custom igraph vertex shape jampie
#'
#' This function is a vectorized replacement for plotting
#' vertex shape `"pie"` in much more efficient manner.
#'
#' It is substantially faster to use `shape.jampie.plot()` than
#' default igraph plotting, even for only 20 pie nodes, the speed
#' becomes even more dramatically faster for larger networks with
#' 200+ nodes. Minutes reduced to 1-2 seconds rendering time.
#'
#' Pie nodes with only one large
#' 100% wedge no longer display the small line from origin,
#' which is a change and improvement from default `igraph` rendering.
#'
#' Attribute `vertex.pie.border` can be used to draw a border around
#' each pie wedge, for each node. It should be a `list` with
#' `lengths(vertex.pie.border)` equal to `lengths(vertex.pie)`.
#' To disable, use `pie.border=NA` on the entire attribute, or individual
#' nodes.
#'
#' Attribute `vertex.frame.color` can be used to draw a single circular
#' border around the entire pie node. The `length(vertex.frame.color)`
#' should equal the number of nodes in the graph, for example
#' determined with `igraph::vcount(g)`.
#' Note that `frame.color` is drawn for each node after the pie
#' wedges, on top of `pie.border` if defined, so it is
#' recommended to use only one form of border for each node.
#'
#' Each pie node is drawn completely, in order: pie wedges including optional
#' `pie.border` outline for each pie wedge, then `frame.color`
#' around the entire node circle; then the next pie node is drawn.
#' This ordering ensures each entire pie node will overlap, or be
#' overlapped by other nodes, without artifacts of the `frame.color`
#' being shown on top of pie nodes that are otherwise beneath
#' visibility.
#'
#' To disable `pie.border` set to `NA` with `vertex.pie.border=NA`
#' or `V(g)[[2]]$pie.border <- NA`.
#'
#' To disable `frame.color` set to `NA` with `vertex.frame.color=NA`
#' or `V(g)[2]$frame.color <- NA`.
#'
#' @family jam igraph shapes
#'
#' @examples
#' # prepare example igraph object
#' am <- matrix(ncol=5, nrow=5,
#' data=0,
#' dimnames=list(LETTERS[1:5], LETTERS[1:5]))
#' am[2:5, 1] <- 1;
#' g1 <- igraph::graph_from_adjacency_matrix(am)
#' igraph::graph_attr(g1, "layout") <- cbind(x=c(0, 1, 1, -1, -1),
#' y=c(0, 1, -0.5, 0.5, -1))
#' colorset <- c("firebrick3", "dodgerblue3");
#' vseq <- seq_len(igraph::vcount(g1));
#' vsizes <- c(3, 2, 2, 2, 1);
#' igraph::V(g1)$pie <- lapply(vseq, function(i){
#' rep(1, vsizes[i])
#' })
#' set.seed(1);
#' igraph::V(g1)$pie.border <- lapply(vseq, function(i){
#' sample(colorset,
#' replace=TRUE,
#' size=vsizes[i])
#' })
#' igraph::V(g1)$pie.color <- lapply(vseq, function(i){
#' jamba::alpha2col(alpha=0.5,
#' igraph::V(g1)$pie.border[[i]])
#' })
#' igraph::V(g1)$pie.lwd <- rep(5, igraph::vcount(g1))
#' igraph::V(g1)$frame.lwd <- c(2, 1, 1, 1, 1)*2;
#' igraph::V(g1)$frame.color <- NA
#' igraph::V(g1)$size <- c(45, 25, 25, 25, 25);
#' igraph::V(g1)$shape <- "jampie";
#'
#' par("mar"=c(2, 2, 3, 2))
#' igraph::plot.igraph(g1, vertex.label="",
#' vertex.shape="pie", vertex.frame.color="grey45",
#' main="shape='pie'\nigraph::plot.igraph()")
#'
#' jam_igraph(g1, vertex.label="",
#' main="shape='jampie', frame.color=NA\njam_igraph()")
#'
#'
#' jam_igraph(g1, vertex.label="",
#' vertex.frame.lwd=2,
#' main="shape='jampie', frame.color='black'\njam_igraph()",
#' vertex.shape="jampie", vertex.frame.color="black")
#'
#' jam_igraph(g1, vertex.label="", vertex.frame.color="black")
#'
#' # print pie.color data
#' print(igraph::vertex_attr(g1)$pie.color);
#'
#' @export
shape.jampie.plot <- function
(coords,
v=NULL,
params)
{
getparam <- function(pname) {
p <- params("vertex", pname)
if (length(p) != 1 && !is.null(v)) {
p <- p[v]
}
p
}
vertex.color <- getparam("color")
vertex.frame.color <- getparam("frame.color")
vertex.frame.lwd <- getparam("frame.lwd")
vertex.size <- rep(1/200 * getparam("size"),
length.out=nrow(coords))
vertex.pie <- getparam("pie")
vertex.pie.color <- getparam("pie.color")
# fill empty vertex.pie with uniform value=1
if (length(vertex.pie) == 0 &&
length(vertex.pie.color) > 0) {
vertex.pie <- lapply(vertex.pie.color, function(i){
rep(1, length(i))
})
}
vertex.pie.border <- getparam("pie.border")
if (!is.list(vertex.pie.border)) {
if (length(vertex.pie.border) == 1) {
vertex.pie.border <- rep(vertex.pie.border,
length.out=length(vertex.pie))
}
vertex.pie.border <- rep(vertex.pie.border,
lengths(vertex.pie));
vertex.pie.border <- split(vertex.pie.border,
rep(seq_along(vertex.pie),
lengths(vertex.pie)))
} else {
vertex.pie.border <- rep(vertex.pie.border,
length.out=length(vertex.pie))
}
vertex.pie.angle <- getparam("pie.angle")
vertex.pie.density <- getparam("pie.density")
vertex.pie.lty <- getparam("pie.lty")
if (length(vertex.pie.lty) == 0) {
vertex.pie.lty <- default_igraph_values()$vertex$pie.lty;
}
if (!is.list(vertex.pie.lty)) {
if (length(vertex.pie.lty) == 1) {
vertex.pie.lty <- rep(vertex.pie.lty,
length.out=length(vertex.pie))
}
vertex.pie.lty <- rep(vertex.pie.lty,
lengths(vertex.pie))
vertex.pie.lty <- split(vertex.pie.lty,
rep(seq_along(vertex.pie),
lengths(vertex.pie)))
} else {
vertex.pie.lty <- rep(vertex.pie.lty,
length.out=length(vertex.pie))
}
vertex.pie.lwd <- getparam("pie.lwd")
if (length(vertex.pie.lwd) == 0) {
vertex.pie.lwd <- default_igraph_values()$vertex$pie.lwd;
}
if (!is.list(vertex.pie.lwd)) {
if (length(vertex.pie.lwd) == 1) {
vertex.pie.lwd <- rep(vertex.pie.lwd,
length.out=length(vertex.pie))
}
vertex.pie.lwd <- rep(vertex.pie.lwd,
lengths(vertex.pie))
vertex.pie.lwd <- split(vertex.pie.lwd,
rep(seq_along(vertex.pie),
lengths(vertex.pie)))
} else {
vertex.pie.lwd <- rep(vertex.pie.lwd,
length.out=length(vertex.pie))
}
vertex.pie.lwd.max <- sapply(vertex.pie.lwd, max, na.rm=TRUE);
# calculate one lwd to rule them all
# because polygon() does not accept multiple lwd values in one call
overall_lwd <- rep(
pmax(
vertex.frame.lwd,
sapply(vertex.pie.lwd, max, na.rm=TRUE),
na.rm=TRUE),
length.out=nrow(coords))
## Convert for loop to lapply that obtains polygon coordinates
if (TRUE) {
#jamba::printDebug("Calculating pie node polygons using ",
# "jam_mypie()");
poly_df <- jamba::rbindList(lapply(seq_len(nrow(coords)), function(i){
pie <- if (length(vertex.pie) == 1) {
vertex.pie[[1]]
} else {
vertex.pie[[i]]
}
col <- if (length(vertex.pie.color) == 1) {
vertex.pie.color[[1]]
} else if (length(vertex.pie.color) == 0) {
NA
} else {
vertex.pie.color[[i]]
}
border <- if (length(vertex.pie.border) == 1) {
vertex.pie.border[[1]]
} else if (length(vertex.pie.border) == 0) {
NA
} else {
vertex.pie.border[[i]]
}
frame.color <- if (length(vertex.frame.color) == 1) {
vertex.frame.color[[1]]
} else if (length(vertex.frame.color) == 0) {
NA
} else {
vertex.frame.color[[i]]
}
frame.lwd <- if (length(vertex.frame.lwd) == 1) {
vertex.frame.lwd[[1]]
} else if (length(vertex.frame.lwd) == 0) {
1
} else {
vertex.frame.lwd[[i]]
}
jam_mypie(
x=coords[i, 1],
y=coords[i, 2],
pie,
radius=vertex.size[i],
edges=200,
col=col,
angle=na.omit(vertex.pie.angle[c(i, 1)])[1],
density=na.omit(vertex.pie.density[c(i, 1)])[1],
border=border,
frame.color=frame.color,
frame.lwd=frame.lwd,
# frame.lwd=overall_lwd[i],
lty=vertex.pie.lty[[i]],
lwd=vertex.pie.lwd[[i]])
# lty=na.omit(vertex.pie.lty[c(i, 1)])[1],
# lwd=vertex.pie.lwd[c(i, 1)])
# lwd=overall_lwd[i])
# lwd = na.omit(vertex.pie.lwd[c(i, 1)])[1])
}));
# enable basic vectorized lwd
poly_dfs <- split(poly_df, poly_df$lwd);
for (poly_df in poly_dfs) {
poly_x <- unlist(lapply(poly_df$x, function(ix1){
c(ix1, NA)
}))
poly_y <- unlist(lapply(poly_df$y, function(ix1){
c(ix1, NA)
}))
polygon(x=poly_x,
y=poly_y,
density=poly_df$density,
angle=poly_df$angle,
border=poly_df$border,
col=poly_df$col,
lty=poly_df$lty,
lwd=ifelse(poly_df$lwd == 0, 1, poly_df$lwd),
ljoin="mitre",
lend="square")
}
} else {
## Legacy for loop below
for (i in seq_len(nrow(coords))) {
pie <- if (length(vertex.pie) == 1) {
vertex.pie[[1]]
} else {
vertex.pie[[i]]
}
col <- if (length(vertex.pie.color) == 1) {
vertex.pie.color[[1]]
} else {
vertex.pie.color[[i]]
}
igraph:::mypie(x = coords[i, 1],
y = coords[i, 2],
pie,
radius = vertex.size[i],
edges = 200,
col = col,
angle = na.omit(vertex.pie.angle[c(i, 1)])[1],
density = na.omit(vertex.pie.density[c(i, 1)])[1],
border = na.omit(vertex.frame.color[c(i, 1)])[1],
lty = na.omit(vertex.pie.lty[c(i, 1)])[1])
}
}
}
#' clip function for igraph vertex shape jampie
#'
#' clip function for igraph vertex shape jampie
#'
#' This function defines the clipping function for custom igraph vertex
#' shape jampie.
#'
#' @rdname shape.jampie.plot
#'
#' @family jam igraph shapes
#'
#' @export
shape.jampie.clip <- function
(coords,
el,
params,
end=c("both", "from", "to"))
{
end <- match.arg(end)
if (length(coords) == 0) {
return(coords)
}
vertex.pie <- params("vertex", "pie")
vertex.size <- 1/200 * params("vertex", "size")
# vmax <- max(c(el[,1], el[,2]));
vmax <- length(vertex.pie);
vertex.size <- rep(vertex.size, length.out=vmax);
vertex.frame.lwd <- params("vertex", "frame.lwd")
vertex.frame.color <- params("vertex", "frame.color")
vertex.pie.lwd <- params("vertex", "pie.lwd")
if (length(vertex.pie.lwd) == 0) {
vertex.pie.lwd <- 1;
}
if (!is.list(vertex.pie.lwd)) {
if (length(vertex.pie.lwd) == 1) {
vertex.pie.lwd <- rep(vertex.pie.lwd,
length.out=length(vertex.pie))
}
vertex.pie.lwd <- rep(vertex.pie.lwd,
lengths(vertex.pie))
}
vertex.pie.lwd.max <- sapply(vertex.pie.lwd, max)
if (length(vertex.frame.lwd) == 0) {
vertex.frame.lwd <- 1
}
vertex.frame.lwd <- rep(vertex.frame.lwd,
length.out=vmax);
if (length(vertex.frame.color) == 0) {
vertex.frame.color <- NA
}
vertex.frame.color <- rep(vertex.frame.color,
length.out=vmax);
vertex.frame.lwd <- ifelse(
is.na(vertex.frame.color) | jamba::col2alpha(vertex.frame.color) < 0.01,
0,
vertex.frame.lwd);
new.frame.lwd <- ifelse(vertex.frame.lwd > vertex.pie.lwd.max & vertex.pie.lwd.max > 0,
vertex.pie.lwd.max,
vertex.frame.lwd)
vertex.frame.lwd <- new.frame.lwd;
overall_lwd <- rep(
pmax(
vertex.frame.lwd,
sapply(vertex.pie.lwd, max, na.rm=TRUE),
na.rm=TRUE),
length.out=length(vertex.size))
# note that jam_mypie() adds radius to account for frame width for consistency
inner_cex <- 1.01;
fig_adj <- diff(par("usr")[1:2]) / par("pin")[1];
inner_adjustment <- (vertex.pie.lwd.max / 96) * fig_adj;
# inner_adjustment <- (overall_lwd / 96) * fig_adj;
outer_adjustment <- (vertex.frame.lwd / 96) * fig_adj;
radius_add <- inner_adjustment*1.01 - outer_adjustment;
vertex.size <- vertex.size + radius_add;
if (end %in% c("both", "from")) {
phi <- atan2(coords[, 4] - coords[, 2],
coords[, 3] - coords[, 1])
if (length(vertex.size) == 1) {
vsize.from <- vertex.size
} else {
vsize.from <- vertex.size[el[, 1]]
}
x_from <- coords[, 1] + vsize.from * cos(phi);
y_from <- coords[, 2] + vsize.from * sin(phi);
} else {
x_from <- coords[, 1];
y_from <- coords[, 2];
}
if (end %in% c("both", "to")) {
# phi <- atan2(coords[, 4] - coords[, 2],
# coords[, 3] - coords[, 1])
phi <- atan2(coords[, 4] - coords[, 2],
coords[, 3] - coords[, 1]) + pi;
# phi <- atan2(coords[, 2] - coords[, 4],
# coords[, 1] - coords[, 3])
# r <- sqrt(
# (coords[, 3] - coords[, 1])^2 +
# (coords[, 4] - coords[, 2])^2)
if (length(vertex.size) == 1) {
vsize.to <- vertex.size
} else {
vsize.to <- vertex.size[el[, 2]]
}
# x_to <- coords[, 1] + (r - vsize.to*2) * cos(phi);
# y_to <- coords[, 2] + (r - vsize.to*2) * sin(phi);
x_to <- coords[, 3] + vsize.to * cos(phi);
y_to <- coords[, 4] + vsize.to * sin(phi);
} else {
x_to <- coords[, 3];
y_to <- coords[, 4];
}
if (end == "from") {
res <- cbind(
x_from,
y_from);
} else if (end == "to") {
res <- cbind(
x_to,
y_to);
} else if (end == "both") {
res <- cbind(
x_from,
y_from,
x_to,
y_to);
}
res
}
#' Vectorized mypie() function for igraph vertex pie polygons
#'
#' Vectorized mypie() function for igraph vertex pie polygons
#'
#' This is a function called internally by `shape.jampie.plot()`,
#' not intended for direct use.
#'
#' See `reorder_igraph_nodes()` for visual examples.
#'
#' This function is a light rewrite of `igraph:::mypie()`, except
#' that this function determines polygon coordinates without
#' drawing them, instead returns the polygon coordinates to
#' the calling function `shape.jampie.plot()` which in turn
#' draws all polygons once using the vectorized approach
#' described for `graphics::polygon()`.
#'
#' See `shape.jampie.plot()` for more detail.
#'
#' To disable `pie.border` set to `NA` with `vertex.pie.border=NA`
#' or `V(g)[[2]]$pie.border <- NA`.
#'
#' To disable `frame.color` set to `NA` with `vertex.frame.color=NA`
#' or `V(g)[2]$frame.color <- NA`.
#'
#' @return `data.frame` with columns suitable for use in `polygon()`
#' after each column is expanded to vector form. Columns `x` and `y`
#' are stored `AsIs()` in `list` format, and should be combined
#' with one `NA` value between each `numeric` vector. The `NA` values
#' cause `polygon()` to draw a series of separate polygons in
#' one vectorized step, much faster than drawing a series of
#' polygons in an iterative loop.
#'
#' @family jam igraph shapes
#'
#' @param x,y `numeric` coordinate for the center of each `igraph` node.
#' @param values `numeric` vector of relative pie wedge sizes.
#' @param radius `numeric` radius of pie
#' @param edges `integer` number of edges to make a circle
#' @param col `character` vector of R colors to fill each pie wedge, in order
#' @param angle,density used to draw lines to fill each pie node, passed
#' to `polygon()`
#' @param border `character` vector of R colors for each pie wedge border
#' @param frame.color `character` R color used around the entire pie circle.
#' @param lty `numeric` or `character` line type
#' @param init.angle `numeric` angle in degrees (0 to 360) where `0` is the
#' top of the circle, proceeding clockwide.
#' @param inner_pie_border `logical` whether to apply `pie.border` colors
#' only along the inside of each pie wedge polygon, so that adjacent
#' colors will be seen beside each other without overlapping adjacent
#' borders. This method is currently in development.
#' @param overclose_polygons `logical` indicating whether to close polygon
#' coordinates with an extra couple points, to ensure the line join
#' `"ljoin"` is properly called when drawing each polygon.
#' @param ... additional arguments are passed to `polygon()`
#'
#' @examples
#' # See reorder_igraph_nodes() for visual examples.
#'
#' @export
jam_mypie <- function
(x,
y,
values,
radius,
edges=200,
col=NULL,
angle=45,
density=NULL,
border=NULL,
frame.color=NULL,
frame.lwd=par("lwd"),
lty=NULL,
lwd=par("lwd"),
init.angle=90,
inner_pie_border=getOption("inner_pie_border", TRUE),
inner_cex=1.01,
xy_max=NULL,
overclose_polygons=FALSE,
...)
{
values <- c(0, cumsum(values)/sum(values))
dx <- diff(values)
nx <- length(dx)
twopi <- 2 * pi
if (is.null(col)) {
if (is.null(density)) {
col <- c("white",
"lightblue",
"mistyrose",
"lightcyan",
"lavender",
"cornsilk")
} else {
col <- par("fg")
}
}
col <- rep(jamba::rmNULL(nullValue=NA, col), length.out=nx)
border <- rep(jamba::rmNULL(nullValue=NA, border), length.out=nx)
frame.color <- rep(jamba::rmNULL(nullValue=NA, frame.color), length.out=nx)
frame.lwd <- rep(jamba::rmNULL(nullValue=1, frame.lwd), length.out=nx)
lty <- rep(jamba::rmNULL(nullValue=NA, lty), length.out=nx)
lwd <- rep(jamba::rmNULL(nullValue=1, lwd), length.out=nx)
lwd.max <- max(lwd, na.rm=TRUE);
angle <- rep(jamba::rmNULL(nullValue=NA, angle), length.out=nx)
density <- rep(jamba::rmNULL(nullValue=NA, density), length.out=nx)
t2xy <- function(t, init.angle=90, radius=1) {
t2p <- 2 * pi * t + init.angle * pi/180;
list(
x=radius * cos(t2p),
y=radius * sin(t2p))
}
# extra adjustment for frame.color
# when frame.lwd=0, the frame.color is set to NA to prevent
# rendering any color
frame.color <- ifelse(frame.lwd <= 0 | is.na(frame.lwd),
NA,
frame.color)
# special adjustment for frame.lwd
frame.lwd <- ifelse(
is.na(frame.color) | jamba::col2alpha(frame.color) < 0.01,
0,
frame.lwd);
new.frame.lwd <- ifelse(frame.lwd > lwd.max & lwd.max > 0,
lwd.max,
frame.lwd)
frame.lwd <- new.frame.lwd;
# frame border is adjusted based upon frame.lwd
# however for frame border to be drawn with proper vectorization
# order alongside pie wedges, it should share the same wedge lwd
# since the wedge is drawn over the frame border, hiding all
# but the effective frame.lwd.
use.frame.lwd <- ifelse(lwd.max > 0 & frame.lwd < lwd.max,
lwd.max, frame.lwd);
if (TRUE %in% inner_pie_border) {
# assume line width is "point"
# each line width is 1/96 inches, so: 96 points / inch
# 96 * (device_width inches) / x_width = points per x-coordinate
# 1inch/96pt * x_width/dev_inch = x_width/pt
# inner_adjustment <- (par("lwd")/96) * diff(par("usr")[1:2]) / par("pin")[1]
inner_adjustment <- (lwd / 96) * diff(par("usr")[1:2]) / par("pin")[1]
outer_adjustment <- (frame.lwd / 96) * diff(par("usr")[1:2]) / par("pin")[1]
# radii <- radius + (inner_adjustment) - max(outer_adjustment);
radii <- radius + (inner_adjustment)*(max(inner_adjustment)/inner_adjustment) - max(outer_adjustment);
radius <- radius + max(inner_adjustment) - max(outer_adjustment);
if (length(frame.color) == 0 || all(is.na(frame.color))) {
# no frame is displayed, therefore make the node slightly larger
# so the output is consistent for all nodes
# radius <- radius + inner_adjustment;
}
}
## convert this loop to return matrix of polygon coordinates
## then stitch multiple polygons with empty row between each
## so polygon() can be called once on the whole set of polygons
##
## bonus points: for one-section pie graph, draw a circle without segment
poly_df <- jamba::rbindList(lapply(seq_len(nx), function(i){
n <- max(2, floor(edges * dx[i]))
P <- t2xy(
t=seq.int(values[i],
values[i + 1],
length.out = n),
init.angle=init.angle,
# radius=radius)
radius=radii[i])
if (nx == 1) {
xvals <- (x + c(P$x));
yvals <- (y + c(P$y));
} else {
xvals <- (x + c(P$x, 0));
yvals <- (y + c(P$y, 0));
}
# original polygon
polydf <- data.frame(
stringsAsFactors=FALSE,
x=I(list(xvals)),
y=I(list(yvals)),
density=density[i],
angle=angle[i],
border=border[i],
col=col[i],
lty=lty[i],
lwd=lwd[i])
# optional inner border polygon
if (TRUE %in% inner_pie_border &&
length(border[i]) == 1 &&
!is.na(border[i])) {
# slightly shrink each polygon
polym <- cbind(x=xvals, y=yvals);
polym <- rbind(polym, head(polym, 1));
psf <- sf::st_polygon(list(polym));
# jamba::printDebug("inner_adjustment:", inner_adjustment);
use_adjustment <- -inner_adjustment[i]/(2*inner_cex);
# use_adjustment <- -inner_adjustment[i]/(2*inner_cex) *
# (max(inner_adjustment) / inner_adjustment[i]);
psf1 <- sf::st_buffer(x=psf,
dist=use_adjustment);
polym2 <- as(psf1, "matrix");
if (TRUE %in% overclose_polygons) {
xvals <- c(polym2[,1], head(polym2[,1], 2))
yvals <- c(polym2[,2], head(polym2[,2], 2))
} else {
xvals <- head(polym2[,1], -1);
yvals <- head(polym2[,2], -1);
}
# version 0.0.88.910: col=NA changed to col="#FFFFFF01"
polydf2 <- data.frame(
stringsAsFactors=FALSE,
x=I(list(xvals)),
y=I(list(yvals)),
density=NA,
angle=angle[i],
border=border[i],
# col=NA,
col="#FFFFFF01",
lty=lty[i],
lwd=lwd[i])
polydf[1, "border"] <- NA;
polydf <- rbind(polydf, polydf2);
}
polydf
}));
if (length(frame.color) >= 1 && !all(is.na(frame.color))) {
# new method re-calculates the full circle
n <- edges
P <- t2xy(
t=seq.int(0,
1,
length.out = n),
init.angle=init.angle,
radius=radius)
border_x <- (x + c(P$x));
border_y <- (y + c(P$y));
if (TRUE %in% inner_pie_border) {
# slightly enlarge each polygon to draw the border outside
polym <- cbind(x=border_x, y=border_y);
polym <- rbind(polym, head(polym, 1));
psf <- sf::st_polygon(list(polym));
psf1 <- sf::st_buffer(x=psf,
dist=outer_adjustment/(2*inner_cex));
polym2 <- as(psf1, "matrix");
border_x <- polym2[,1];
border_y <- polym2[,2];
}
# previous method
# border_x <- unlist(lapply(poly_df[[1]], function(i){head(i, -2)}));
# border_y <- unlist(lapply(poly_df[[2]], function(i){head(i, -2)}));
border_df <- data.frame(
stringsAsFactors=FALSE,
x=I(list(border_x)),
y=I(list(border_y)),
density=-1,
angle=45,
border=head(frame.color, 1),
# border=frame.color[i],
# col=NA,
col="#FFFFFF01",
lty=head(lty, 1),
lwd=head(use.frame.lwd, 1))
# lwd=head(frame.lwd, 1))
return_df <- jamba::rbindList(list(
border_df,
poly_df
));
return(return_df);
}
# more bonus points: optionally draw frame.color around each circle
return(poly_df);
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.