diff --git a/DESCRIPTION b/DESCRIPTION index c8e12f8..2c6984a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: autograph Title: Automatic Plotting and Theming of Many Graphs -Version: 0.5.1 -Date: 2025-12-08 +Version: 0.6.0 +Date: 2026-02-27 Description: Visual exploration and presentation of networks should not be difficult. This package includes functions for plotting networks and network-related metrics with sensible and pretty defaults. It includes 'ggplot2'-based plot methods for many popular network package classes. @@ -25,7 +25,6 @@ Imports: patchwork, tidygraph Suggests: - BiocManager, gganimate, ggforce (>= 0.5.0), gifski, @@ -34,7 +33,6 @@ Suggests: testthat (>= 3.0.0) Enhances: ergm, - Rgraphviz, RSiena Authors@R: c(person(given = "James", diff --git a/NAMESPACE b/NAMESPACE index 54b5a6d..4b35911 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -79,37 +79,7 @@ export(layout_tetrad) export(layout_triad) export(layout_valence) export(match_color) -export(scale_color_centres) -export(scale_color_ethz) -export(scale_color_iheid) -export(scale_color_rug) -export(scale_color_sdgs) -export(scale_color_uzh) -export(scale_colour_centres) -export(scale_colour_ethz) export(scale_colour_hue) -export(scale_colour_iheid) -export(scale_colour_rug) -export(scale_colour_sdgs) -export(scale_colour_uzh) -export(scale_edge_color_centres) -export(scale_edge_color_ethz) -export(scale_edge_color_iheid) -export(scale_edge_color_rug) -export(scale_edge_color_sdgs) -export(scale_edge_color_uzh) -export(scale_edge_colour_centres) -export(scale_edge_colour_ethz) -export(scale_edge_colour_iheid) -export(scale_edge_colour_rug) -export(scale_edge_colour_sdgs) -export(scale_edge_colour_uzh) -export(scale_fill_centres) -export(scale_fill_ethz) -export(scale_fill_iheid) -export(scale_fill_rug) -export(scale_fill_sdgs) -export(scale_fill_uzh) export(scale_size) export(scale_x_continuous) export(set_stocnet_theme) diff --git a/NEWS.md b/NEWS.md index fe24692..47a7fdf 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,34 @@ +# autograph 0.6.0 + +## Package + +- Updated logo + +## Graphing + +- `graphr()` now auto-dispatches to `graphs()` when passed a list of graphs +- `graphs()` no longer requires `{patchwork}` to be loaded separately +- Closed #12 by changing node aesthetics from `color` to `fill` using fillable shape codes (21–25) for nodes to support fill-based colour scales +- Consolidated legend modifications into internal `graph_legends()` helper +- Improved legend labelling: + - edge sign legend now labelled "Sign" + - edge weight/width now labelled "Weight"/"Width" + - node mode/shape legend now labelled "Mode"/"Shape" +- Closed #8 by adding "isolates" argument to `graphr()`: + - `"legend"` (default) removes isolates from the graph but notes them in the legend + - `"caption"` removes isolates from the graph but notes them in the caption + - `"keep"` retains isolates in the graph as-is (closes #12) + +## Theming + +- Removed `scale_*()` functions as redundant with better theme support and `match_color()` + +## Layouts + +- Replaced `{Rgraphviz}` (Bioconductor) with `igraph::layout_with_sugiyama` for Sugiyama/hierarchy layouts, removing the Bioconductor dependency +- Improved Sugiyama layout with dummy node insertion and barycenter crossing minimisation for better edge routing +- Fixed #18 for lattice layout snapping by rotating the layout to optimise edge verticality and horizontality + # autograph 0.5.1 ## Plotting diff --git a/R/autograph_utilities.R b/R/autograph_utilities.R index 90612d0..0d50324 100644 --- a/R/autograph_utilities.R +++ b/R/autograph_utilities.R @@ -1,3 +1,4 @@ +# nocov start # Helper function for checking and downloading packages thisRequires <- function(pkgname){ if (!requireNamespace(pkgname, quietly = TRUE) & interactive()) { @@ -9,15 +10,7 @@ thisRequires <- function(pkgname){ } } } - -thisRequiresBio <- function(pkgname) { - if (!requireNamespace(pkgname, quietly = TRUE) & interactive()) { - if(utils::askYesNo(msg = paste("The", pkgname, - "package is required to run this function. Would you like to install", pkgname, "from BioConductor?"))) { - thisRequires("BiocManager") - BiocManager::install(pkgname) - }} -} +# nocov end seq_nodes <- function(.data){ seq.int(manynet::net_nodes(.data)) diff --git a/R/graph_edges.R b/R/graph_edges.R index b116077..5c3a6ac 100644 --- a/R/graph_edges.R +++ b/R/graph_edges.R @@ -16,7 +16,7 @@ graph_edges <- function(p, g, edge_color, edge_size, node_size) { guide = ggplot2::guide_legend( ifelse(is.null(edge_size) & manynet::is_weighted(g), - "Edge Weight", "Edge Size"))) + "Weight", "Width"))) if (length(unique(out[["ecolor"]])) == 1) { p <- p + ggplot2::guides(edge_colour = "none") } else if (length(unique(out[["ecolor"]])) == 2){ @@ -24,12 +24,12 @@ graph_edges <- function(p, g, edge_color, edge_size, node_size) { guide = ggplot2::guide_legend( ifelse(is.null(edge_color) & manynet::is_signed(g), - "Edge Sign", edge_color))) + "Sign", edge_color))) } else p <- p + ggraph::scale_edge_colour_manual(values = ag_qualitative(length(unique(out[["ecolor"]]))), guide = ggplot2::guide_legend( ifelse(is.null(edge_color) & manynet::is_signed(g), - "Edge Sign", edge_color))) + "Sign", edge_color))) p } diff --git a/R/graph_labels.R b/R/graph_labels.R index a0b8913..6776397 100644 --- a/R/graph_labels.R +++ b/R/graph_labels.R @@ -2,17 +2,20 @@ graph_labels <- function(p, g, layout) { if (layout == "circle" | layout == "concentric") { angles <- as.data.frame(.cart2pol(as.matrix(p[["data"]][,1:2]))) angles$degree <- angles$phi * 180/pi - angles <- dplyr::case_when(p[["data"]][,2] == 0 & p[["data"]][,1] == 0 ~ 0.1, - p[["data"]][,2] >= 0 & p[["data"]][,1] > 0 ~ angles$degree, - p[["data"]][,2] < 0 & p[["data"]][,1] > 0 ~ angles$degree, - p[["data"]][,1] == 1 ~ angles$degree, + # Extract x and y as vectors for case_when + x_coord <- p[["data"]][[1]] + y_coord <- p[["data"]][[2]] + angles <- dplyr::case_when(y_coord == 0 & x_coord == 0 ~ 0.1, + y_coord >= 0 & x_coord > 0 ~ angles$degree, + y_coord < 0 & x_coord > 0 ~ angles$degree, + x_coord == 1 ~ angles$degree, TRUE ~ angles$degree - 180) if (manynet::net_nodes(g) < 10) { - hj <- ifelse(p[["data"]][,1] >= 0, -0.8, 1.8) + hj <- ifelse(x_coord >= 0, -0.8, 1.8) } else if (manynet::net_nodes(g) < 20) { - hj <- ifelse(p[["data"]][,1] >= 0, -0.4, 1.4) + hj <- ifelse(x_coord >= 0, -0.4, 1.4) } else { - hj <- ifelse(p[["data"]][,1] >= 0, -0.2, 1.2) + hj <- ifelse(x_coord >= 0, -0.2, 1.2) } p <- p + ggraph::geom_node_text(ggplot2::aes(label = name), repel = TRUE, family = ag_font(), diff --git a/R/graph_layout.R b/R/graph_layout.R index 5b0993e..4ff2c5a 100644 --- a/R/graph_layout.R +++ b/R/graph_layout.R @@ -23,11 +23,19 @@ graph_layout <- function(g, layout, labels, node_group, snap, ...) { } if(snap){ manynet::snet_info("Snapping layout coordinates to grid.") - if(grepl("lattice", - igraph::graph_attr(attr(p$data, "graph"), "grand")$name, - ignore.case = TRUE)) - p$data[,c("x","y")] <- round(p$data[,c("x","y")]) - else p$data[,c("x","y")] <- depth_first_recursive_search(p) + if(grepl("lattice", manynet::net_name(g), ignore.case = TRUE)){ + + angles <- seq(0, pi/2, length.out = 180) + scores <- sapply(angles, function(a) { + lay2 <- .rotate_layout(lo, a) + .edge_angle_deviation(lay2, g) + }) + + best_angle <- angles[which.min(scores)] + rotated_coords <- .rotate_layout(lo, best_angle) + # Make sure that the coordinates, if rounded to integers, are still unique + p$data[,c("x","y")] <- round(rotated_coords[,c("x","y")]) + } else p$data[,c("x","y")] <- depth_first_recursive_search(p) } # Add background ---- if(getOption("snet_background", default = "#FFFFFF")!="#FFFFFF") @@ -35,3 +43,30 @@ graph_layout <- function(g, layout, labels, node_group, snap, ...) { default = "#FFFFFF"))) p } + +# Helper functions ---- + +.rotate_layout <- function(layout, angle) { + rot <- matrix(c(cos(angle), -sin(angle), + sin(angle), cos(angle)), ncol = 2) + coords <- as.matrix(layout[, c("x", "y")]) + newcoords <- coords %*% rot + layout$x <- newcoords[,1] + layout$y <- newcoords[,2] + layout +} + +.edge_angle_deviation <- function(layout, graph) { + ed <- igraph::as_edgelist(graph) + dx <- layout$x[ed[,2]] - layout$x[ed[,1]] + dy <- layout$y[ed[,2]] - layout$y[ed[,1]] + ang <- atan2(dy, dx) + + # deviation from nearest multiple of 90° + dev <- abs((ang %% (pi/2)) - pi/4) + mean(dev) +} + + + + diff --git a/R/graph_legends.R b/R/graph_legends.R new file mode 100644 index 0000000..0684d0f --- /dev/null +++ b/R/graph_legends.R @@ -0,0 +1,28 @@ + +graph_legends <- function(p, g, + node_color = NULL, node_shape = NULL, node_size = NULL, + edge_color = NULL, edge_size = NULL) { + p + + ggplot2::guides(fill = ggplot2::guide_legend(order = 1, + title = ifelse(is.null(node_color), + "Color", node_color)), + color = ggplot2::guide_legend(order = 2), + shape = ggplot2::guide_legend(order = 3, + title = ifelse(is.null(node_shape), + ifelse(manynet::is_twomode(g), "Mode", "Shape"), + node_shape)), + size = ggplot2::guide_legend(order = 4, + title = ifelse(is.null(node_size), + "Size", node_size)), + linetype = ggplot2::guide_legend(order = 5), + edge_colour = ggplot2::guide_legend(order = 6, + title = ifelse(is.null(edge_color), + ifelse(manynet::is_signed(g), "Sign", "Color"), + edge_color)), + edge_size = ggplot2::guide_legend(order = 7, + title = ifelse(is.null(edge_size), + ifelse(manynet::is_weighted(g), "Weight", "Size"), + edge_size)), + alpha = ggplot2::guide_legend(order = 99, + override.aes = list( alpha = 0, size = 0, shape = NA ))) +} \ No newline at end of file diff --git a/R/graph_nodes.R b/R/graph_nodes.R index 3d9ae70..76b6580 100644 --- a/R/graph_nodes.R +++ b/R/graph_nodes.R @@ -11,19 +11,22 @@ graph_nodes <- function(p, g, node_color, node_shape, node_size) { p <- p + ggplot2::guides(size = ggplot2::guide_legend(title = node_size)) if (length(unique(out[["nshape"]])) > 1) p <- p + ggplot2::guides(shape = ggplot2::guide_legend( - title = ifelse(manynet::is_twomode(g) & is.null(node_shape), "Node Mode", "Node Shape"))) + title = ifelse(manynet::is_twomode(g) & is.null(node_shape), + "Mode", node_shape))) if (length(unique(out[["ncolor"]])) > 1){ if(length(unique(out[["ncolor"]])) == 2){ - p <- p + ggplot2::scale_colour_manual(values = getOption("snet_highlight", default = c("grey","black")), + p <- p + ggplot2::scale_fill_manual(values = getOption("snet_highlight", + default = c("grey","black")), guide = ggplot2::guide_legend(node_color)) } else { - p <- p + ggplot2::scale_colour_manual(values = ag_qualitative(length(unique(out[["ncolor"]]))), + p <- p + ggplot2::scale_fill_manual(values = ag_qualitative(length(unique(out[["ncolor"]]))), guide = ggplot2::guide_legend(node_color)) } } } # Consider rescaling nodes - p <- p + ggplot2::scale_size(range = c(1/manynet::net_nodes(g)*50, 1/manynet::net_nodes(g)*100)) + p <- p + ggplot2::scale_size(range = c(1/manynet::net_nodes(g)*50, + 1/manynet::net_nodes(g)*100)) p } @@ -56,7 +59,7 @@ graph_nodes <- function(p, g, node_color, node_shape, node_size) { } else if (is_twomode(g) & is.null(node_shape)) { out <- ifelse(igraph::V(g)$type, "One", "Two") } else { - out <- "circle" + out <- 21 # Use fillable circle shape (was "circle") } out } @@ -104,9 +107,9 @@ graph_nodes <- function(p, g, node_color, node_shape, node_size) { "R" ~ "Recovered", "S" ~ "Susceptible") cols <- match_color(c("#d73027", "#4575b4", "#E6AB02", "#66A61E")) - p + ggraph::geom_node_point(ggplot2::aes(color = node_color), + p + ggraph::geom_node_point(ggplot2::aes(fill = node_color), size = out[["nsize"]], shape = out[["nshape"]]) + - ggplot2::scale_color_manual(name = NULL, guide = ggplot2::guide_legend(""), + ggplot2::scale_fill_manual(name = NULL, guide = ggplot2::guide_legend(""), values = c("Infected" = cols[1], "Susceptible" = cols[2], "Exposed" = cols[3], @@ -121,9 +124,9 @@ graph_nodes <- function(p, g, node_color, node_shape, node_size) { node_color <- ifelse(is.infinite(node_adopts), max(node_adopts[!is.infinite(node_adopts)]) + 1, node_adopts) - p + ggraph::geom_node_point(ggplot2::aes(shape = nshape, color = node_color), + p + ggraph::geom_node_point(ggplot2::aes(shape = nshape, fill = node_color), size = out[["nsize"]]) + - ggplot2::scale_color_gradient(low = match_color("#d73027"), high = match_color("#4575b4"), + ggplot2::scale_fill_gradient(low = match_color("#d73027"), high = match_color("#4575b4"), breaks=c(min(node_color)+1, ifelse(any(nshape=="Non-Adopter"), max(node_color)-1, @@ -132,40 +135,51 @@ graph_nodes <- function(p, g, node_color, node_shape, node_size) { name = "Time of\nAdoption\n") + ggplot2::scale_shape_manual(name = "", breaks = c("Seed(s)", "Adopter", "Non-Adopter"), - values = c("Seed(s)" = "triangle", - "Adopter" = "circle", - "Non-Adopter" = "square")) + - ggplot2::guides(color = ggplot2::guide_colorbar(order = 1, reverse = TRUE), + values = c("Seed(s)" = 24, # triangle + "Adopter" = 21, # circle + "Non-Adopter" = 22)) + # square + ggplot2::guides(fill = ggplot2::guide_colorbar(order = 1, reverse = TRUE), shape = ggplot2::guide_legend(order = 2)) } .map_nodes <- function(p, out) { - if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) == 1 & length(out[["nshape"]]) == 1) { - p <- p + ggraph::geom_node_point(colour = out[["ncolor"]], size = out[["nsize"]], + if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) == 1 & + length(out[["nshape"]]) == 1) { + p <- p + ggraph::geom_node_point(fill = out[["ncolor"]], size = out[["nsize"]], shape = out[["nshape"]]) - } else if (length(out[["ncolor"]]) > 1 & length(out[["nsize"]]) == 1 & length(out[["nshape"]]) == 1) { - p <- p + ggraph::geom_node_point(ggplot2::aes(colour = out[["ncolor"]]), + } else if (length(out[["ncolor"]]) > 1 & length(out[["nsize"]]) == 1 & + length(out[["nshape"]]) == 1) { + p <- p + ggraph::geom_node_point(ggplot2::aes(fill = out[["ncolor"]]), size = out[["nsize"]], shape = out[["nshape"]]) - } else if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) > 1 & length(out[["nshape"]]) == 1) { + } else if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) > 1 & + length(out[["nshape"]]) == 1) { p <- p + ggraph::geom_node_point(ggplot2::aes(size = out[["nsize"]]), - colour = out[["ncolor"]], shape = out[["nshape"]]) - } else if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) == 1 & length(out[["nshape"]]) > 1) { + fill = out[["ncolor"]], shape = out[["nshape"]]) + } else if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) == 1 & + length(out[["nshape"]]) > 1) { p <- p + ggraph::geom_node_point(ggplot2::aes(shape = out[["nshape"]]), - colour = out[["ncolor"]], size = out[["nsize"]]) - } else if (length(out[["ncolor"]]) > 1 & length(out[["nsize"]]) > 1 & length(out[["nshape"]]) == 1) { - p <- p + ggraph::geom_node_point(ggplot2::aes(colour = out[["ncolor"]], size = out[["nsize"]]), + fill = out[["ncolor"]], size = out[["nsize"]]) + } else if (length(out[["ncolor"]]) > 1 & length(out[["nsize"]]) > 1 & + length(out[["nshape"]]) == 1) { + p <- p + ggraph::geom_node_point(ggplot2::aes(fill = out[["ncolor"]], + size = out[["nsize"]]), shape = out[["nshape"]]) - } else if (length(out[["ncolor"]]) > 1 & length(out[["nsize"]]) == 1 & length(out[["nshape"]]) > 1) { - p <- p + ggraph::geom_node_point(ggplot2::aes(colour = out[["ncolor"]], shape = out[["nshape"]]), + } else if (length(out[["ncolor"]]) > 1 & length(out[["nsize"]]) == 1 & + length(out[["nshape"]]) > 1) { + p <- p + ggraph::geom_node_point(ggplot2::aes(fill = out[["ncolor"]], + shape = out[["nshape"]]), size = out[["nsize"]]) - } else if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) > 1 & length(out[["nshape"]]) > 1) { - p <- p + ggraph::geom_node_point(ggplot2::aes(size = out[["nsize"]], shape = out[["nshape"]]), - colour = out[["ncolor"]]) + } else if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) > 1 & + length(out[["nshape"]]) > 1) { + p <- p + ggraph::geom_node_point(ggplot2::aes(size = out[["nsize"]], + shape = out[["nshape"]]), + fill = out[["ncolor"]]) } else { - p <- p + ggraph::geom_node_point(ggplot2::aes(colour = out[["ncolor"]], + p <- p + ggraph::geom_node_point(ggplot2::aes(fill = out[["ncolor"]], shape = out[["nshape"]], size = out[["nsize"]])) } + p <- p + ggplot2::scale_shape_manual(values = c(21, 22, 24, 23, 25)) p } diff --git a/R/graphr.R b/R/graphr.R index 63ef8f2..742d887 100644 --- a/R/graphr.R +++ b/R/graphr.R @@ -5,12 +5,9 @@ #' (m)any network data for exploration, investigation, inspiration, #' and communication. #' -#' It builds upon `{ggplot2}` and `{ggraph}` to offer -#' pretty and extensible graphing solutions. -#' However, compared to those solutions, -#' `graphr()` contains various algorithms to provide better looking -#' graphs by default. -#' This means that just passing the function some network data +#' `graphr()` builds upon `{ggplot2}` and `{ggraph}` to offer +#' pretty, easy, and extensible graphing solutions. +#' Just passing the function some network data #' will often be sufficient to return a reasonable-looking graph. #' #' The function also makes it easy to modify many of the most @@ -84,6 +81,14 @@ #' it is recommended to calculate all edge-related statistics prior #' to using this function. #' Edges can also be sized by declaring a numeric size or vector instead. +#' @param isolates Character scalar, how to treat isolates. +#' "keep" will keep isolates in the graph as they are. +#' "legend" (default) will remove isolates from the graph but note them in the legend. +#' "caption" will remove isolates from the graph but note them in the caption. +#' If there are no isolates, this argument will be ignored. +#' If the default layout ("stress") is used, +#' we recommend that the "legend" option is used to avoid isolates crowding +#' out the giant component. #' @param snap Logical scalar, whether the layout should be snapped to a grid. #' @param ... Extra arguments to pass on to the layout algorithm, if necessary. #' @return A `ggplot2::ggplot()` object. @@ -103,9 +108,27 @@ #' @export graphr <- function(.data, layout = NULL, labels = TRUE, node_color, node_shape, node_size, node_group, - edge_color, edge_size, snap = FALSE, ..., + edge_color, edge_size, + isolates = c("legend","caption","keep"), snap = FALSE, ..., node_colour, edge_colour) { + if(manynet::is_list(.data)) return(graphs(.data, layout = layout, labels = labels, + node_color = node_color, node_shape = node_shape, node_size = node_size, node_group = node_group, + edge_color = edge_color, edge_size = edge_size, + isolates = isolates, snap = snap, ..., + node_colour = node_colour, edge_colour = edge_colour)) g <- manynet::as_tidygraph(.data) + + # Separate isolates ---- + isolates <- .infer_isolates(g, match.arg(isolates)) + if(isolates != "keep"){ + if(manynet::is_labelled(g)){ + isos <- manynet::node_names(g)[manynet::node_is_isolate(g)] + } else { + isos <- which(manynet::node_is_isolate(g)) + } + g <- manynet::to_no_isolates(g) + } + layout <- .infer_layout(g, layout) if (missing(node_color) && missing(node_colour)) { node_color <- NULL @@ -144,11 +167,36 @@ graphr <- function(.data, layout = NULL, labels = TRUE, if (isTRUE(labels) & manynet::is_labelled(g)) { p <- graph_labels(p, g, layout) } + + # Note isolates ---- + if(isolates == "legend"){ + if (length(isos) > 3) label_text <- paste(c(utils::head(isos, 3),"..."), collapse = "\n") else + label_text <- paste(isos, collapse = "\n") + p <- p + ggplot2::geom_point(aes(x=rep(0, manynet::net_nodes(g)), y=0, + alpha = "Isolates"), + size = 0) + + ggplot2::scale_alpha_manual(name = "+ Isolates", + values = c("Isolates" = 0.5), + labels = label_text) + } else if(isolates == "caption"){ + p <- p + ggplot2::labs(caption = paste("Isolates:", paste(isos, collapse = ", "))) + } + + # Add legends ---- + p <- graph_legends(p, g, + node_color, node_shape, node_size, + edge_color, edge_size) + # assign("last.warning", NULL, envir = baseenv()) # to avoid persistent ggrepel p } -# Helper functions for graphr() +# Helper functions for graphr() ---- +.infer_isolates <- function(g, isolates){ + if(!any(manynet::node_is_isolate(g))) isolates <- "keep" + isolates +} + .infer_layout <- function(g, layout) { if (is.null(layout)) { if(manynet::is_list(g)) diff --git a/R/graphs.R b/R/graphs.R index a808cfe..4fcdacd 100644 --- a/R/graphs.R +++ b/R/graphs.R @@ -41,7 +41,6 @@ #' @export graphs <- function(netlist, waves, based_on = c("first", "last", "both"), ...) { - thisRequires("patchwork") based_on <- match.arg(based_on) if (manynet::is_manynet(netlist) && manynet::is_changing(netlist)){ if (manynet::is_list(attr(netlist, "network"))) netlist <- attr(netlist, "network") else diff --git a/R/grapht.R b/R/grapht.R index 9359c36..3683a2b 100644 --- a/R/grapht.R +++ b/R/grapht.R @@ -1,3 +1,4 @@ +# nocov start # Dynamic networks #### #' Easily animate dynamic networks with sensible defaults @@ -195,10 +196,10 @@ grapht <- function(tlist, keep_isolates = TRUE, if (node_shape %in% names(nodes_out)) { node_shape <- as.factor(nodes_out[[node_shape]]) if (!any(grepl("circle|square|triangle", node_shape))) { - node_shape <- c("circle", "square", "triangle")[node_shape] + node_shape <- c(21, 22, 24)[node_shape] # circle, square, triangle (fillable) } } - } else node_shape <- "circle" + } else node_shape <- 21 # fillable circle (was "circle") if (!is.null(node_color)) { if (node_color %in% names(nodes_out)) { node_color <- .check_color(nodes_out[[node_color]]) @@ -228,9 +229,9 @@ grapht <- function(tlist, keep_isolates = TRUE, # Plot nodes #### if ("diffusion" %in% names(nodes_out)) { cols <- match_color(c("#d73027", "#4575b4", "#E6AB02", "#66A61E")) - p + ggraph::geom_node_point(ggplot2::aes(color = node_color), + p + ggraph::geom_node_point(ggplot2::aes(fill = node_color), size = node_size, shape = node_shape) + - ggplot2::scale_color_manual(name = NULL, guide = ggplot2::guide_legend(""), + ggplot2::scale_fill_manual(name = NULL, guide = ggplot2::guide_legend(""), values = c("Infected" = cols[1], "Susceptible" = cols[2], "Exposed" = cols[3], @@ -240,7 +241,7 @@ grapht <- function(tlist, keep_isolates = TRUE, } else { p <- p + ggplot2::geom_point(aes(x, y, group = name), alpha = alphad, size = node_size, data = nodes_out, - color = node_color, shape = node_shape, + fill = node_color, shape = node_shape, show.legend = FALSE) + ggplot2::theme_void() } @@ -317,3 +318,4 @@ grapht <- function(tlist, keep_isolates = TRUE, dplyr::mutate(status = ifelse(is.na(status), FALSE, TRUE)) %>% dplyr::distinct() } +# nocov end diff --git a/R/layout_partition.R b/R/layout_partition.R index 57e5f3f..6f2373e 100644 --- a/R/layout_partition.R +++ b/R/layout_partition.R @@ -3,11 +3,6 @@ #' @description #' These algorithms layout networks based on two or more partitions, #' and are recommended for use with `graphr()` or `{ggraph}`. -#' Note that these layout algorithms use `{Rgraphviz}`, -#' a package that is only available on Bioconductor. -#' It will first need to be downloaded using `BiocManager::install("Rgraphviz")`. -#' If it has not already been installed, there is a prompt the first time -#' these functions are used though. #' #' The "hierarchy" layout layers the first node set along the bottom, #' and the second node set along the top, @@ -167,7 +162,6 @@ layout_lineage <- function(.data, rank, circular = FALSE) { } else if (length(rank) != length(.data)) { rank <- as.numeric(manynet::node_attribute(.data, rank)) } - thisRequiresBio("Rgraphviz") out <- layout_tbl_graph_alluvial( manynet::as_igraph(mutate(.data, type = ifelse( rank > mean(rank), TRUE, FALSE)), twomode = TRUE)) @@ -183,6 +177,119 @@ layout_tbl_graph_lineage <- layout_lineage (vector - min(vector)) / (max(vector) - min(vector)) } +# Sugiyama-style layout with dummy nodes and barycenter heuristic +# for better edge crossing minimization +.sugiyama_layout <- function(g, layers = NULL, times = 100) { + n <- igraph::vcount(g) + el <- igraph::as_edgelist(g, names = FALSE) + # Layer assignment + if (is.null(layers)) { + lo <- igraph::layout_with_sugiyama(g, maxiter = times) + node_layer <- lo$layout[, 2] + } else { + node_layer <- layers + } + layer_vals <- sort(unique(node_layer)) + n_layers <- length(layer_vals) + if (n_layers < 2) { + return(cbind(seq_len(n), node_layer)) + } + # Map layers to 0-based indices (used as list keys offset by 1) + layer_idx <- match(node_layer, layer_vals) - 1L + # Build adjacency between original nodes + adj <- vector("list", n) + radj <- vector("list", n) + for (i in seq_len(n)) { adj[[i]] <- integer(0); radj[[i]] <- integer(0) } + if (nrow(el) > 0) { + for (i in seq_len(nrow(el))) { + u <- el[i, 1]; v <- el[i, 2] + adj[[u]] <- c(adj[[u]], v) + radj[[v]] <- c(radj[[v]], u) + } + } + # Insert dummy nodes for edges spanning multiple layers + dummy_id <- n + # For barycenter, we need per-layer node lists and inter-layer edges + all_layer <- layer_idx # will grow with dummies + # Build inter-layer edges (only between adjacent layers) + inter_edges <- list() + if (nrow(el) > 0) { + for (i in seq_len(nrow(el))) { + u <- el[i, 1]; v <- el[i, 2] + lu <- layer_idx[u]; lv <- layer_idx[v] + if (lu == lv) next + # Ensure direction goes from lower layer to higher + if (lu > lv) { tmp <- u; u <- v; v <- tmp; tmp <- lu; lu <- lv; lv <- tmp } + if (lv - lu == 1) { + inter_edges[[length(inter_edges) + 1]] <- c(u, v) + } else { + # Insert dummy nodes + prev <- u + for (k in (lu + 1):(lv - 1)) { + dummy_id <- dummy_id + 1 + all_layer <- c(all_layer, k) + inter_edges[[length(inter_edges) + 1]] <- c(prev, dummy_id) + prev <- dummy_id + } + inter_edges[[length(inter_edges) + 1]] <- c(prev, v) + } + } + } + total_nodes <- length(all_layer) + if (length(inter_edges) == 0) { + return(cbind(seq_len(n), node_layer)) + } + inter_edges_mat <- do.call(rbind, inter_edges) + # Build per-layer node lists + layer_nodes <- lapply(0:(n_layers - 1), function(k) which(all_layer == k)) + # Initialize x positions: sequential within each layer + x_pos <- rep(0, total_nodes) + for (k in seq_along(layer_nodes)) { + nodes_in_layer <- layer_nodes[[k]] + x_pos[nodes_in_layer] <- seq_along(nodes_in_layer) + } + # Build forward/backward adjacency for the expanded graph + fwd_adj <- vector("list", total_nodes) + bwd_adj <- vector("list", total_nodes) + for (i in seq_len(total_nodes)) { fwd_adj[[i]] <- integer(0); bwd_adj[[i]] <- integer(0) } + if (!is.null(inter_edges_mat) && nrow(inter_edges_mat) > 0) { + for (i in seq_len(nrow(inter_edges_mat))) { + u <- inter_edges_mat[i, 1]; v <- inter_edges_mat[i, 2] + fwd_adj[[u]] <- c(fwd_adj[[u]], v) + bwd_adj[[v]] <- c(bwd_adj[[v]], u) + } + } + # Barycenter crossing minimization sweeps + for (iter in seq_len(times)) { + # Forward sweep: layer 1 to n_layers-1 + for (k in 2:n_layers) { + nodes_k <- layer_nodes[[k]] + if (length(nodes_k) <= 1) next + bc <- sapply(nodes_k, function(nd) { + neighbors <- bwd_adj[[nd]] + if (length(neighbors) == 0) return(x_pos[nd]) + mean(x_pos[neighbors]) + }) + ord <- order(bc) + x_pos[nodes_k[ord]] <- seq_along(nodes_k) + } + # Backward sweep: layer n_layers-2 to 0 + for (k in (n_layers - 1):1) { + nodes_k <- layer_nodes[[k]] + if (length(nodes_k) <= 1) next + bc <- sapply(nodes_k, function(nd) { + neighbors <- fwd_adj[[nd]] + if (length(neighbors) == 0) return(x_pos[nd]) + mean(x_pos[neighbors]) + }) + ord <- order(bc) + x_pos[nodes_k[ord]] <- seq_along(nodes_k) + } + } + # Extract coordinates for original nodes only + cbind(x_pos[seq_len(n)], node_layer) +} + #' @rdname layout_partition #' @examples #' #graphr(ison_southern_women, layout = "hierarchy", center = "events", @@ -191,24 +298,24 @@ layout_tbl_graph_lineage <- layout_lineage layout_hierarchy <- function(.data, center = NULL, circular = FALSE, times = 1000) { if (is.null(center)) { - thisRequiresBio("Rgraphviz") - prep <- manynet::as_matrix(.data) - if (anyDuplicated(rownames(prep))) { - rownames(prep) <- seq_len(nrow(prep)) - colnames(prep) <- seq_len(ncol(prep)) + max(nrow(prep)) + g <- manynet::as_igraph(.data) + if (manynet::is_twomode(.data)) { + layers <- ifelse(igraph::V(g)$type, 2, 1) + } else { + layers <- NULL } - if (any(prep<0)) prep[prep<0] <- 0 - out <- manynet::as_graphAM(prep) - out <- suppressMessages(Rgraphviz::layoutGraph(out, layoutType = 'dot', - attrs = list(graph = list(rankdir = "BT")))) - nodeX <- .rescale(out@renderInfo@nodes$nodeX) - nodeY <- .rescale(out@renderInfo@nodes$nodeY) + lo <- .sugiyama_layout(g, layers = layers, times = times) + nodeX <- lo[, 1] + nodeY <- lo[, 2] + if (length(unique(nodeX)) > 1) nodeX <- .rescale(nodeX) + if (length(unique(nodeY)) > 1) nodeY <- .rescale(nodeY) if (manynet::is_twomode(.data) & "name" %in% igraph::vertex_attr_names(.data)) { names <- igraph::vertex_attr(.data, "name") + names(nodeX) <- igraph::vertex_attr(g, "name") + names(nodeY) <- igraph::vertex_attr(g, "name") nodeX <- nodeX[order(match(names(nodeX), names))] nodeY <- nodeY[order(match(names(nodeY), names))] } - # nodeY <- abs(nodeY - max(nodeY)) out <- .to_lo(cbind(nodeX, nodeY)) } else { if (!manynet::is_twomode(.data)) manynet::snet_abort("Please declare a two-mode network.") @@ -259,21 +366,20 @@ layout_tbl_graph_hierarchy <- layout_hierarchy #' #graphr(ison_southern_women, layout = "alluvial") #' @export layout_alluvial <- function(.data, - circular = FALSE, times = 1000){ - thisRequiresBio("Rgraphviz") - prep <- manynet::as_matrix(.data, twomode = FALSE) - if(anyDuplicated(rownames(prep))){ - rownames(prep) <- seq_len(nrow(prep)) - colnames(prep) <- seq_len(ncol(prep)) + circular = FALSE, times = 1000){ + g <- manynet::as_igraph(.data) + if (manynet::is_twomode(.data)) { + layers <- ifelse(igraph::V(g)$type, 2, 1) + } else { + layers <- NULL } - if(any(prep<0)) prep[prep<0] <- 0 - out <- manynet::as_graphAM(prep) - out <- suppressMessages(Rgraphviz::layoutGraph(out, layoutType = 'dot', - attrs = list(graph = list(rankdir = "LR")))) - nodeX <- .rescale(out@renderInfo@nodes$nodeX) - nodeY <- .rescale(out@renderInfo@nodes$nodeY) - # nodeY <- abs(nodeY - max(nodeY)) - .to_lo(cbind(nodeX, nodeY)) + lo <- .sugiyama_layout(g, layers = layers, times = times) + nodeX <- lo[, 1] + nodeY <- lo[, 2] + # Swap x and y for left-to-right layout (alluvial) + if (length(unique(nodeY)) > 1) nodeY <- .rescale(nodeY) + if (length(unique(nodeX)) > 1) nodeX <- .rescale(nodeX) + .to_lo(cbind(nodeY, nodeX)) } #' @rdname layout_partition diff --git a/R/theme_palettes.R b/R/theme_palettes.R index c08a0be..d504106 100644 --- a/R/theme_palettes.R +++ b/R/theme_palettes.R @@ -118,3 +118,4 @@ ggpizza <- function(colors, init.angle = 105, cex = 4, labcol = NULL) { ggplot2::coord_equal() + ggplot2::theme_void() } + diff --git a/R/theme_scales.R b/R/theme_scales.R deleted file mode 100644 index 5a574aa..0000000 --- a/R/theme_scales.R +++ /dev/null @@ -1,297 +0,0 @@ -#' Themed scales for further customization -#' -#' @description These functions enable to add color scales to be graphs. -#' @name theme_scales -#' @param direction Direction for using palette colors. -#' @param ... Extra arguments passed to `ggplot2::discrete_scale()`. -#' @examples -#' #ison_brandes %>% -#' #mutate(core = migraph::node_is_core(ison_brandes)) %>% -#' #graphr(node_color = "core") + -#' #scale_color_iheid() -#' #graphr(ison_physicians[[1]], edge_color = "type") + -#' #scale_edge_color_ethz() -NULL - -#' @rdname theme_scales -#' @export -scale_fill_iheid <- function(direction = 1, ...) { - ggplot2::discrete_scale("fill", - palette = palette_gen(palette = "IHEID", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_colour_iheid <- function(direction = 1, ...) { - ggplot2::discrete_scale("colour", - palette = palette_gen(palette = "IHEID", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_color_iheid <- scale_colour_iheid - -#' @rdname theme_scales -#' @export -scale_edge_colour_iheid <- function(direction = 1, ...) { - ggplot2::discrete_scale("edge_colour", - palette = palette_gen(palette = "IHEID", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_edge_color_iheid <- scale_edge_colour_iheid - -#' Centres color scales -#' -#' @rdname theme_scales -#' @export -scale_fill_centres <- function(direction = 1, ...) { - ggplot2::discrete_scale("fill", - palette = palette_gen(palette = "Centres", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_colour_centres <- function(direction = 1, ...) { - ggplot2::discrete_scale("colour", - palette = palette_gen(palette = "Centres", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_color_centres <- scale_colour_centres - -#' @rdname theme_scales -#' @export -scale_edge_colour_centres <- function(direction = 1, ...) { - ggplot2::discrete_scale("edge_colour", - palette = palette_gen(palette = "Centres", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_edge_color_centres <- scale_edge_colour_centres - -#' SDGs color scales -#' -#' @rdname theme_scales -#' @export -scale_fill_sdgs <- function(direction = 1, ...) { - ggplot2::discrete_scale("fill", - palette = palette_gen(palette = "SDGs", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_colour_sdgs <- function(direction = 1, ...) { - ggplot2::discrete_scale("colour", - palette = palette_gen(palette = "SDGs", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_color_sdgs <- scale_colour_sdgs - -#' @rdname theme_scales -#' @export -scale_edge_colour_sdgs <- function(direction = 1, ...) { - ggplot2::discrete_scale("edge_colour", - palette = palette_gen(palette = "SDGs", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_edge_color_sdgs <- scale_edge_colour_sdgs - -#' ETHZ color scales -#' -#' @rdname theme_scales -#' @export -scale_fill_ethz <- function(direction = 1, ...) { - ggplot2::discrete_scale("fill", - palette = palette_gen(palette = "ETHZ", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_colour_ethz <- function(direction = 1, ...) { - ggplot2::discrete_scale("colour", - palette = palette_gen(palette = "ETHZ", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_color_ethz <- scale_colour_ethz - -#' @rdname theme_scales -#' @export -scale_edge_colour_ethz <- function(direction = 1, ...) { - ggplot2::discrete_scale("edge_colour", - palette = palette_gen(palette = "ETHZ", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_edge_color_ethz <- scale_edge_colour_ethz - -#' UZH color scales -#' -#' @rdname theme_scales -#' @export -scale_fill_uzh <- function(direction = 1, ...) { - ggplot2::discrete_scale("fill", - palette = palette_gen(palette = "UZH", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_colour_uzh <- function(direction = 1, ...) { - ggplot2::discrete_scale("colour", - palette = palette_gen(palette = "UZH", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_color_uzh <- scale_colour_uzh - -#' @rdname theme_scales -#' @export -scale_edge_colour_uzh <- function(direction = 1, ...) { - ggplot2::discrete_scale("edge_colour", - palette = palette_gen(palette = "UZH", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_edge_color_uzh <- scale_edge_colour_uzh - -#' RUG color scales -#' -#' @rdname theme_scales -#' @export -scale_fill_rug <- function(direction = 1, ...) { - ggplot2::discrete_scale("fill", - palette = palette_gen(palette = "RUG", direction), - na.value = "grey", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_colour_rug <- function(direction = 1, ...) { - ggplot2::discrete_scale("colour", - palette = palette_gen(palette = "RUG", direction), - na.value = "grey", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_color_rug <- scale_colour_rug - -#' @rdname theme_scales -#' @export -scale_edge_colour_rug <- function(direction = 1, ...) { - ggplot2::discrete_scale("edge_colour", - palette = palette_gen(palette = "RUG", direction), - na.value = "black", name = "", ...) -} - -#' @rdname theme_scales -#' @export -scale_edge_color_rug <- scale_edge_colour_rug - -# Helper functions -corp_color <- function(...) { - corp_colors <- c(`IHEIDRed` = "#E20020", `IHEIDBlack` = "#000010", - `IHEIDGrey` = "#6f7072", `AHCD` = "#622550", - `CFFD` = "#0094D8", `CIES` = "#268D2B", - `CTEI` = "#008F92", `CGEN` = "#820C2B", - `CCDP` = "#3E2682", `GLGC` = "#006564", - `GLHC` = "#A8086E", `GLMC` = "#006EAA", - `NoPoverty` = "#e5243b", `ZeroHunger` = "#DDA63A", - `GoodHealth` = "#4C9F38", - `QualityEducation` = "#C5192D", - `GenderEquality` = "#FF3A21", `CleanWater` = "#26BDE2", - `CleanEnergy` = "#FCC30B", - `EconomicGrowth` = "#A21942", - `Innovation` = "#FD6925", - `ReducedInequalities` = "#DD1367", - `SustainableCities` = "#FD9D24", - `ResponsibleConsumption` = "#BF8B2E", - `ClimateAction` = "#3F7E44", `BelowWater` = "#0A97D9", - `OnLand` = "#56C02B", `StrongInstitutions` = "#00689D", - `GoalPartnerships` = "#19486A", - `ETHZ_Blue` = "#215CAF", `ETHZ_Petrol` = "#007894", - `ETHZ_Green` = "#627313", `ETHZ_Bronze` = "#8E6713", - `ETHZ_Red`= "#B7352D", `ETHZ_Purple` = "#A7117A", - `ETHZ_Grey` = "#6F6F6F", `UZH_Blue` = "#0028a5", - `UZH_Grey` = "#a3adb7", `UZH_Orange` = "#dc6027", - `RUG_Red` = "#dc002d", `RUG_Black` = "#000000") - cols <- c(...) - if (is.null(cols)) - return (corp_colors) - corp_colors[cols] -} - -corp_palette <- function(palette, ...) { - corp_palettes <- list(`IHEID` = corp_color("IHEIDRed", - "IHEIDBlack", - "IHEIDGrey"), - `Centres` = corp_color("AHCD", "CFFD", - "CIES", "CTEI", - "CGEN", "CCDP", - "GLGC", "GLHC", - "GLMC"), - `SDGs` = corp_color("NoPoverty", - "ZeroHunger", - "GoodHealth", - "QualityEducation", - "GenderEquality", - "CleanWater", - "CleanEnergy", - "EconomicGrowth", - "Innovation", - "ReducedInequalities", - "SustainableCities", - "ResponsibleConsumption", - "ClimateAction", - "BelowWater", - "OnLand", - "StrongInstitutions", - "GoalPartnerships"), - `ETHZ` = corp_color("ETHZ_Blue", "ETHZ_Petrol", - "ETHZ_Green", "ETHZ_Bronze", - "ETHZ_Red", "ETHZ_Purple", "ETHZ_Grey"), - `UZH` = corp_color("UZH_Blue", "UZH_Grey", "UZH_Orange"), - `RUG` = corp_color("RUG_Red", "RUG_Black")) - unlist(unname(corp_palettes[c(palette)])) -} - -palette_gen <- function(palette, direction = 1) { - function(n) { - if (n > length(corp_palette(palette))) - warning("Not enough colors in this palette!") - else { - all_colors <- corp_palette(palette) - all_colors <- unname(unlist(all_colors)) - all_colors <- if (direction >= 0) all_colors else rev(all_colors) - color_list <- all_colors[1:n] - } - } -} - diff --git a/R/zzz.R b/R/zzz.R index 6a7a3c2..ac4ca01 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,3 +1,4 @@ +# nocov start .onAttach <- function(...) { # suppressMessages(suppressPackageStartupMessages(library("manynet", warn.conflicts = FALSE))) @@ -42,5 +43,6 @@ } } +# nocov end diff --git a/man/figures/logo-old.png b/man/figures/logo-old.png new file mode 100644 index 0000000..bbaaaa0 Binary files /dev/null and b/man/figures/logo-old.png differ diff --git a/man/figures/logo.png b/man/figures/logo.png index bbaaaa0..f5a84fb 100644 Binary files a/man/figures/logo.png and b/man/figures/logo.png differ diff --git a/man/layout_partition.Rd b/man/layout_partition.Rd index bc57921..3bfa1f6 100644 --- a/man/layout_partition.Rd +++ b/man/layout_partition.Rd @@ -107,11 +107,6 @@ Defaults to NULL.} \description{ These algorithms layout networks based on two or more partitions, and are recommended for use with \code{graphr()} or \code{{ggraph}}. -Note that these layout algorithms use \code{{Rgraphviz}}, -a package that is only available on Bioconductor. -It will first need to be downloaded using \code{BiocManager::install("Rgraphviz")}. -If it has not already been installed, there is a prompt the first time -these functions are used though. The "hierarchy" layout layers the first node set along the bottom, and the second node set along the top, diff --git a/man/plot_graphr.Rd b/man/plot_graphr.Rd index 23af626..83add13 100644 --- a/man/plot_graphr.Rd +++ b/man/plot_graphr.Rd @@ -15,6 +15,7 @@ graphr( node_group, edge_color, edge_size, + isolates = c("legend", "caption", "keep"), snap = FALSE, ..., node_colour, @@ -87,6 +88,15 @@ it is recommended to calculate all edge-related statistics prior to using this function. Edges can also be sized by declaring a numeric size or vector instead.} +\item{isolates}{Character scalar, how to treat isolates. +"keep" will keep isolates in the graph as they are. +"legend" (default) will remove isolates from the graph but note them in the legend. +"caption" will remove isolates from the graph but note them in the caption. +If there are no isolates, this argument will be ignored. +If the default layout ("stress") is used, +we recommend that the "legend" option is used to avoid isolates crowding +out the giant component.} + \item{snap}{Logical scalar, whether the layout should be snapped to a grid.} \item{...}{Extra arguments to pass on to the layout algorithm, if necessary.} @@ -100,12 +110,9 @@ This function provides users with an easy way to graph (m)any network data for exploration, investigation, inspiration, and communication. -It builds upon \code{{ggplot2}} and \code{{ggraph}} to offer -pretty and extensible graphing solutions. -However, compared to those solutions, -\code{graphr()} contains various algorithms to provide better looking -graphs by default. -This means that just passing the function some network data +\code{graphr()} builds upon \code{{ggplot2}} and \code{{ggraph}} to offer +pretty, easy, and extensible graphing solutions. +Just passing the function some network data will often be sufficient to return a reasonable-looking graph. The function also makes it easy to modify many of the most diff --git a/man/theme_scales.Rd b/man/theme_scales.Rd deleted file mode 100644 index aa9b978..0000000 --- a/man/theme_scales.Rd +++ /dev/null @@ -1,112 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/theme_scales.R -\name{theme_scales} -\alias{theme_scales} -\alias{scale_fill_iheid} -\alias{scale_colour_iheid} -\alias{scale_color_iheid} -\alias{scale_edge_colour_iheid} -\alias{scale_edge_color_iheid} -\alias{scale_fill_centres} -\alias{scale_colour_centres} -\alias{scale_color_centres} -\alias{scale_edge_colour_centres} -\alias{scale_edge_color_centres} -\alias{scale_fill_sdgs} -\alias{scale_colour_sdgs} -\alias{scale_color_sdgs} -\alias{scale_edge_colour_sdgs} -\alias{scale_edge_color_sdgs} -\alias{scale_fill_ethz} -\alias{scale_colour_ethz} -\alias{scale_color_ethz} -\alias{scale_edge_colour_ethz} -\alias{scale_edge_color_ethz} -\alias{scale_fill_uzh} -\alias{scale_colour_uzh} -\alias{scale_color_uzh} -\alias{scale_edge_colour_uzh} -\alias{scale_edge_color_uzh} -\alias{scale_fill_rug} -\alias{scale_colour_rug} -\alias{scale_color_rug} -\alias{scale_edge_colour_rug} -\alias{scale_edge_color_rug} -\title{Themed scales for further customization} -\usage{ -scale_fill_iheid(direction = 1, ...) - -scale_colour_iheid(direction = 1, ...) - -scale_color_iheid(direction = 1, ...) - -scale_edge_colour_iheid(direction = 1, ...) - -scale_edge_color_iheid(direction = 1, ...) - -scale_fill_centres(direction = 1, ...) - -scale_colour_centres(direction = 1, ...) - -scale_color_centres(direction = 1, ...) - -scale_edge_colour_centres(direction = 1, ...) - -scale_edge_color_centres(direction = 1, ...) - -scale_fill_sdgs(direction = 1, ...) - -scale_colour_sdgs(direction = 1, ...) - -scale_color_sdgs(direction = 1, ...) - -scale_edge_colour_sdgs(direction = 1, ...) - -scale_edge_color_sdgs(direction = 1, ...) - -scale_fill_ethz(direction = 1, ...) - -scale_colour_ethz(direction = 1, ...) - -scale_color_ethz(direction = 1, ...) - -scale_edge_colour_ethz(direction = 1, ...) - -scale_edge_color_ethz(direction = 1, ...) - -scale_fill_uzh(direction = 1, ...) - -scale_colour_uzh(direction = 1, ...) - -scale_color_uzh(direction = 1, ...) - -scale_edge_colour_uzh(direction = 1, ...) - -scale_edge_color_uzh(direction = 1, ...) - -scale_fill_rug(direction = 1, ...) - -scale_colour_rug(direction = 1, ...) - -scale_color_rug(direction = 1, ...) - -scale_edge_colour_rug(direction = 1, ...) - -scale_edge_color_rug(direction = 1, ...) -} -\arguments{ -\item{direction}{Direction for using palette colors.} - -\item{...}{Extra arguments passed to \code{ggplot2::discrete_scale()}.} -} -\description{ -These functions enable to add color scales to be graphs. -} -\examples{ -#ison_brandes \%>\% -#mutate(core = migraph::node_is_core(ison_brandes)) \%>\% -#graphr(node_color = "core") + -#scale_color_iheid() -#graphr(ison_physicians[[1]], edge_color = "type") + -#scale_edge_color_ethz() -} diff --git a/tests/testthat/test-graphr.R b/tests/testthat/test-graphr.R index 3be6c8e..2c18a83 100644 --- a/tests/testthat/test-graphr.R +++ b/tests/testthat/test-graphr.R @@ -1,3 +1,15 @@ +data_objs <- mget(ls("package:manynet"), inherits = TRUE) +# Filter to relevant objects +data_objs <- data_objs[grepl("ison_|fict_|irps_|mpn_", names(data_objs))] +# data_objs <- data_objs[!grepl("starwars|physicians|potter", names(data_objs))] +for (nm in names(data_objs)) { + test_that(paste("graphr() works on", nm), { + skip_if(grepl("starwars|physicians|potter|marvel", nm)) + expect_error(graphr(data_objs[[nm]]), NA) }) +} + +fmrg <- to_giant(to_uniplex(fict_marvel, "relationship")) + test_that("unweighted, unsigned, undirected networks graph correctly", { skip_on_cran() # Unweighted, unsigned, undirected network @@ -10,13 +22,13 @@ test_that("unweighted, unsigned, undirected networks graph correctly", { expect_equal(test_brandes[["layers"]][[1]][["aes_params"]][["edge_linetype"]], "solid") # Node parameters expect_equal(round(test_brandes[["layers"]][[2]][["aes_params"]][["size"]]), 11) - expect_equal(as.character(test_brandes[["layers"]][[2]][["aes_params"]][["shape"]]), "circle") + expect_equal(test_brandes[["layers"]][[2]][["aes_params"]][["shape"]], 21) # fillable circle }) test_that("unweighted, signed, undirected networks graph correctly", { skip_on_cran() # Unweighted, signed, undirected network - test_marvel <- graphr(to_giant(ison_marvel_relationships)) + test_marvel <- graphr(fmrg) # Node position expect_equal(round(test_marvel[["data"]][["x"]][[1]]), -1) expect_equal(round(test_marvel[["data"]][["y"]][[1]]), 1) @@ -40,7 +52,7 @@ test_that("unweighted, unsigned, directed networks graph correctly", { #expect_equal(test_algebra[["layers"]][[1]][["mapping"]][["edge_colour"]], "black") # Node parameters expect_equal(round(test_algebra[["layers"]][[2]][["aes_params"]][["size"]]), 8) - expect_equal(test_algebra[["layers"]][[2]][["aes_params"]][["shape"]], "circle") + expect_equal(test_algebra[["layers"]][[2]][["aes_params"]][["shape"]], 21) # fillable circle }) test_that("weighted, unsigned, directed networks graph correctly", { @@ -65,15 +77,13 @@ test_that("fancy node mods graph correctly", { skip_on_cran() skip_on_ci() # one-mode network - ison_marvel_relationships <- dplyr::mutate(ison_marvel_relationships, - nodesize = Appearances/1000) - testcolnodes <- graphr(ison_marvel_relationships, node_color = "Gender", - node_size = "nodesize", node_shape = "Attractive") + fmrg <- dplyr::mutate(fmrg, nodesize = Appearances/1000) + testcolnodes <- graphr(fmrg, node_color = "Gender", + node_size = "Appearances", + node_shape = "Attractive") expect_s3_class(testcolnodes, c("ggraph","gg","ggplot")) - expect_equal(round(testcolnodes$data$x[1]), 4) - expect_equal(round(testcolnodes$data$y[1]), 3) expect_equal(nrow(testcolnodes[["plot_env"]][["lo"]]), - c(net_nodes(ison_marvel_relationships))) + c(net_nodes(fmrg))) # two-mode network ison_southern_women <- add_node_attribute(ison_southern_women, "group", c(sample(c("a", "b"), @@ -121,3 +131,52 @@ test_that("unquoted arguments plot correctly", { graphr(ison_lawfirm, node_color = gender)) }) +# Tests for fill aesthetic (color-to-fill change) +test_that("nodes use fill aesthetic instead of colour", { + skip_on_cran() + # Default node uses fill parameter + p <- graphr(ison_brandes) + expect_equal(p[["layers"]][[2]][["aes_params"]][["fill"]], "black") + # Mapped node_color uses fill in aes + p2 <- ison_brandes %>% + dplyr::mutate(color = c(rep(c(1, 2), 5), 1)) %>% + graphr(node_color = color) + expect_false(is.null(p2[["layers"]][[2]][["mapping"]][["fill"]])) +}) + +test_that("node_color with multiple values uses fill scale", { + skip_on_cran() + # More than 2 colors triggers scale_fill_manual with qualitative palette + p <- ison_brandes %>% + dplyr::mutate(grp = c(rep(c("a", "b", "c"), 3), "a", "b")) %>% + graphr(node_color = grp) + expect_s3_class(p, c("ggraph", "gg", "ggplot")) + # Check that fill scale is used (not colour) + scale_names <- vapply(p[["scales"]][["scales"]], function(s) { + paste(s[["aesthetics"]], collapse = ",") + }, character(1)) + expect_true(any(grepl("fill", scale_names))) +}) + +test_that("two-mode networks get correct node shapes", { + skip_on_cran() + p <- graphr(ison_southern_women) + expect_s3_class(p, c("ggraph", "gg", "ggplot")) + # Two-mode shape mapping should use "One"/"Two" labels + node_layer <- p[["layers"]][[2]] + expect_false(is.null(node_layer[["mapping"]][["shape"]])) +}) + +test_that("node_color with 2 values uses highlight palette", { + skip_on_cran() + p <- ison_brandes %>% + dplyr::mutate(grp = c(rep(c("x", "y"), 5), "x")) %>% + graphr(node_color = grp) + expect_s3_class(p, c("ggraph", "gg", "ggplot")) + # Should use scale_fill_manual with highlight defaults + scale_names <- vapply(p[["scales"]][["scales"]], function(s) { + paste(s[["aesthetics"]], collapse = ",") + }, character(1)) + expect_true(any(grepl("fill", scale_names))) +}) + diff --git a/tests/testthat/test-layout_partition.R b/tests/testthat/test-layout_partition.R index ff960e2..65a4001 100644 --- a/tests/testthat/test-layout_partition.R +++ b/tests/testthat/test-layout_partition.R @@ -1,10 +1,9 @@ # Layouts test_that("concentric and circular layouts graph correctly", { skip_on_cran() - test_circle <- graphr(to_giant(ison_marvel_relationships), - layout = "circle") - test_conc <- graphr(to_giant(ison_marvel_relationships), - layout = "concentric", membership = "Gender") + fmrg <- to_giant(to_uniplex(fict_marvel, "relationship")) + test_circle <- graphr(fmrg, layout = "circle") + test_conc <- graphr(fmrg, layout = "concentric", membership = "Gender") expect_equal(test_circle$plot_env$layout, "circle") expect_equal(test_conc$plot_env$layout, "concentric") expect_equal(eval(quote(pairlist(...)), @@ -23,7 +22,6 @@ test_that("concentric layout works when node names are missing", { test_that("hierarchy and lineage layouts graph correctly", { skip_on_cran() - skip_on_ci() test_lin <- ison_adolescents %>% mutate(year = rep(c(1985, 1990, 1995, 2000), times = 2)) %>% graphr(layout = "lineage", rank = "year") @@ -54,10 +52,61 @@ test_that("hierarchy and lineage layouts graph correctly", { test_that("hierarchy layout works for two mode networks", { skip_on_cran() - skip_on_ci() tm <- ison_brandes %>% mutate(type = twomode_type, name = LETTERS[1:11]) %>% graphr() expect_length(unique(tm$data[tm$data$type == TRUE, "y"]), 1) expect_length(unique(tm$data[tm$data$type == FALSE, "y"]), 1) }) + +test_that("default hierarchy layout uses sugiyama for two-mode networks", { + skip_on_cran() + p <- graphr(ison_southern_women, layout = "hierarchy") + expect_s3_class(p, c("ggraph", "gg", "ggplot")) + expect_equal(p$plot_env$layout, "hierarchy") + # Two-mode should have exactly 2 unique y values (layers) + expect_equal(length(unique(round(p$data$y, 6))), 2) +}) + +test_that("alluvial layout works", { + skip_on_cran() + p <- graphr(ison_southern_women, layout = "alluvial") + expect_s3_class(p, c("ggraph", "gg", "ggplot")) + expect_equal(p$plot_env$layout, "alluvial") +}) + +test_that("hierarchy layout minimises edge crossings", { + skip_on_cran() + # Helper: count bipartite edge crossings given x positions + count_crossings <- function(el, x_pos) { + crossings <- 0 + if (nrow(el) < 2) return(0) + for (i in 1:(nrow(el) - 1)) { + for (j in (i + 1):nrow(el)) { + a1 <- x_pos[el[i, 1]]; b1 <- x_pos[el[i, 2]] + a2 <- x_pos[el[j, 1]]; b2 <- x_pos[el[j, 2]] + if ((a1 - a2) * (b1 - b2) < 0) crossings <- crossings + 1 + } + } + crossings + } + # Test with ison_southern_women (18 women, 14 events, 89 ties) + g <- manynet::as_igraph(ison_southern_women) + n <- igraph::vcount(g) + el <- igraph::as_edgelist(g, names = FALSE) + layers <- ifelse(igraph::V(g)$type, 2, 1) + lo <- autograph:::.sugiyama_layout(g, layers = layers, times = 100) + x_pos <- lo[, 1] + # Naive layout: sequential ordering within each layer + naive_x <- rep(0, n) + naive_x[layers == 1] <- seq_len(sum(layers == 1)) + naive_x[layers == 2] <- seq_len(sum(layers == 2)) + optimised_crossings <- count_crossings(el, x_pos) + naive_crossings <- count_crossings(el, naive_x) + # The optimised layout should have fewer crossings than naive + expect_lt(optimised_crossings, naive_crossings) + # Verify all nodes got valid positions + expect_true(all(is.finite(x_pos))) + expect_equal(length(unique(lo[layers == 1, 2])), 1) + expect_equal(length(unique(lo[layers == 2, 2])), 1) +}) diff --git a/tests/testthat/test-theme_scales.R b/tests/testthat/test-theme_scales.R deleted file mode 100644 index 7ae5ae3..0000000 --- a/tests/testthat/test-theme_scales.R +++ /dev/null @@ -1,28 +0,0 @@ -##### test scales -test_that("scales graph correctly", { - test_sdg <- ison_brandes %>% - mutate(color = c(rep(c(1,2,3,4,5), 2), 1)) %>% - graphr(node_color = color) + - scale_color_sdgs() - test_iheid <- ison_brandes %>% - mutate(color = c(rep(c(1,2), 5), 3)) %>% - graphr(node_color = color) + - scale_color_iheid() - test_ethz <- ison_brandes %>% - mutate(color = c(rep(c(1,2,3), 3), 4, 5)) %>% - graphr(node_color = color) + - scale_color_ethz() - test_uzh <- ison_brandes %>% - mutate(color = c(rep(c(1,2,3), 3), 1, 2)) %>% - graphr(node_color = color) + - scale_color_uzh() - test_rug <- ison_brandes %>% - mutate(color = c(rep(c(1,2), 4), 1, 2, 1)) %>% - graphr(node_color = color) + - scale_color_rug() - expect_equal(as.character(test_sdg[["scales"]][["scales"]][[2]][["call"]]), "scale_color_sdgs") - expect_equal(as.character(test_iheid[["scales"]][["scales"]][[2]][["call"]]), "scale_color_iheid") - expect_equal(as.character(test_ethz[["scales"]][["scales"]][[2]][["call"]]), "scale_color_ethz") - expect_equal(as.character(test_uzh[["scales"]][["scales"]][[2]][["call"]]), "scale_color_uzh") - expect_equal(as.character(test_rug[["scales"]][["scales"]][[2]][["call"]]), "scale_color_rug") -})