R/map_autograph.R

Defines functions remove_isolates transition_edge_lst time_edges_lst .check_color map_dynamic grapht order_alphabetically is_ego_network graphs cart2pol map_nodes map_diff_model_nodes map_infected_nodes check_node_variables .infer_ncolor .infer_nshape .infer_nsize .infer_node_mapping map_edges map_directed_edges check_edge_variables .infer_line_type .infer_end_cap .infer_esize .infer_ecolor .infer_edge_mapping .infer_directed_edge_mapping reduce_categories .graph_labels .graph_nodes .graph_edges .graph_layout graphr

Documented in graphr graphs grapht

# Single graphs ####

#' Easily graph networks with sensible defaults
#' 
#' @description 
#'   This function provides users with an easy way to graph
#'   (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
#'   will often be sufficient to return a reasonable-looking graph.
#'   
#'   The function also makes it easy to modify many of the most
#'   commonly adapted aspects of a graph, 
#'   including node and edge size, colour, and shape,
#'   as arguments rather than additional functions that you need to remember.
#'   These can be defined outright, e.g. `node_size = 8`, or
#'   in reference to an attribute of the network, e.g. `node_size = "wealth"`.
#'   
#'   Lastly, `graphr()` uses `{ggplot2}`-related theme information, so
#'   it is easy to make colour palette and fonts institution-specific and consistent.
#'   See e.g. `theme_iheid()` for more.
#'   
#'   To learn more about what can be done visually,
#'   try `run_tute("Visualisation")`.
#' @name map_graphr
#' @family mapping
#' @param .data A manynet-consistent object.
#' @param layout An igraph, ggraph, or manynet layout algorithm.
#'   If not declared, defaults to "triad" for networks with 3 nodes,
#'   "quad" for networks with 4 nodes,
#'   "stress" for all other one mode networks,
#'   or "hierarchy" for two mode networks.
#'   For "hierarchy" layout, one can further split graph by
#'   declaring the "center" argument as the "events", "actors",
#'   or by declaring a node name.
#'   For "concentric" layout algorithm please declare the "membership" as an 
#'   extra argument.
#'   The "membership" argument expects either a quoted node attribute present
#'   in data or vector with the same length as nodes to draw concentric circles.
#'   For "multilevel" layout algorithm please declare the "level"
#'   as extra argument.
#'   The "level" argument expects either a quoted node attribute present
#'   in data or vector with the same length as nodes to hierarchically
#'   order categories.
#'   If "level" is missing, function will look for 'lvl' node attribute in data.
#'   The "lineage" layout ranks nodes in Y axis according to values.
#'   For "lineage" layout algorithm please declare the "rank"
#'   as extra argument.
#'   The "rank" argument expects either a quoted node attribute present
#'   in data or vector with the same length as nodes.
#' @param labels Logical, whether to print node names
#'   as labels if present.
#' @param node_shape Node variable to be used for shaping the nodes.
#'   It is easiest if this is added as a node attribute to
#'   the graph before plotting.
#'   Nodes can also be shaped by declaring a shape instead.
#' @param node_size Node variable to be used for sizing the nodes.
#'   This can be any continuous variable on the nodes of the network.
#'   Since this function expects this to be an existing variable,
#'   it is recommended to calculate all node-related statistics prior
#'   to using this function.
#'   Nodes can also be sized by declaring a numeric size or vector instead.
#' @param node_color,node_colour Node variable to be used for coloring the nodes.
#'   It is easiest if this is added as a node attribute to
#'   the graph before plotting.
#'   Nodes can also be colored by declaring a color instead.
#' @param node_group Node variable to be used for grouping the nodes.
#'   It is easiest if this is added as a hull over
#'   groups before plotting.
#'   Group variables should have a minimum of 3 nodes,
#'   if less, number groups will be reduced by
#'   merging categories with lower counts into one called "other".
#' @param edge_color,edge_colour Tie variable to be used for coloring the nodes.
#'   It is easiest if this is added as an edge or tie attribute 
#'   to the graph before plotting.
#'   Edges can also be colored by declaring a color instead.
#' @param edge_size Tie variable to be used for sizing the edges.
#'   This can be any continuous variable on the nodes of the network.
#'   Since this function expects this to be an existing variable,
#'   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 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.
#'   The last plot can be saved to the file system using `ggplot2::ggsave()`.
#' @importFrom ggraph geom_edge_link geom_node_text geom_conn_bundle
#'   get_con geom_node_point scale_edge_width_continuous geom_node_label
#' @importFrom ggplot2 aes arrow unit scale_color_brewer scale_fill_brewer
#' @examples
#' graphr(ison_adolescents)
#' ison_adolescents %>%
#'   mutate(color = rep(c("introvert","extrovert"), times = 4),
#'          size = ifelse(node_is_cutpoint(ison_adolescents), 6, 3)) %>%
#'   mutate_ties(ecolor = rep(c("friends", "acquaintances"), times = 5)) %>%
#'   graphr(node_color = "color", node_size = "size",
#'          edge_size = 1.5, edge_color = "ecolor")
#' @export
graphr <- function(.data, layout, labels = TRUE,
                   node_color, node_shape, node_size, node_group,
                   edge_color, edge_size, snap = FALSE, ...,
                   node_colour, edge_colour) {
  g <- as_tidygraph(.data)
  if (missing(layout)) {
    if (net_nodes(g) <= 6) {
      layout <- "configuration"
    } else if (is_twomode(g)) {
      layout <- "hierarchy"
    } else layout <- "stress"
  }
  if (missing(node_color) && missing(node_colour)) {
    node_color <- NULL
  } else if (missing(node_color)) {
    node_color <- as.character(substitute(node_colour))
  } else {
    node_color <- as.character(substitute(node_color))
  }
  if (missing(node_shape)) node_shape <- NULL else
    node_shape <- as.character(substitute(node_shape))
  if (missing(node_size)) node_size <- NULL else if (!is.numeric(node_size)) {
    node_size <- as.character(substitute(node_size))
  }
  if (missing(node_group)) node_group <- NULL else {
    node_group <- as.character(substitute(node_group))
    g <- tidygraph::activate(g, "nodes") %>%
      tidygraph::mutate(node_group = reduce_categories(g, node_group))
  }
  if (missing(edge_color) && missing(edge_colour)) {
    edge_color <- NULL
  } else if (missing(edge_color)) {
    edge_color <- as.character(substitute(edge_colour))
  } else {
    edge_color <- as.character(substitute(edge_color))
  }
  if (missing(edge_size)) edge_size <- NULL else if (!is.numeric(edge_size)) {
    edge_size <- as.character(substitute(edge_size))
  }
  # Add layout ----
  p <- .graph_layout(g, layout, labels, node_group, snap, ...)
  # Add background ----
  if(getOption("mnet_background", default = "#FFFFFF")!="#FFFFFF")
    p <- p + ggplot2::theme(panel.background = ggplot2::element_rect(fill = getOption("mnet_background", default = "#FFFFFF")))
  # Add edges ----
  p <- .graph_edges(p, g, edge_color, edge_size, node_size)
  # Add nodes ----
  p <- .graph_nodes(p, g, node_color, node_shape, node_size)
  # Add labels ----
  if (isTRUE(labels) & is_labelled(g)) {
    p <- .graph_labels(p, g, layout)
  }
  # assign("last.warning", NULL, envir = baseenv()) # to avoid persistent ggrepel
  p
}

.graph_layout <- function(g, layout, labels, node_group, snap, ...) {
  name <- NULL
  dots <- list(...)
  if ("x" %in% names(dots) & "y" %in% names(dots)) {
    lo <- ggraph::create_layout(g, layout = "manual",
                                x = dots[["x"]], y = dots[["y"]])
  } else lo <- suppressWarnings(ggraph::create_layout(g, layout, ...))
  if ("graph" %in% names(attributes(lo))) {
    if (!setequal(names(as.data.frame(attr(lo, "graph"))), names(lo))) {
      for (n in setdiff(names(as.data.frame(attr(lo, "graph"))), names(lo))) {
        lo[n] <- igraph::vertex_attr(g, n)
      }
    }
  }
  p <- ggraph::ggraph(lo) + ggplot2::theme_void()
  if (!is.null(node_group)) {
    x <- y <- NULL
    thisRequires("ggforce")
    thisRequires("concaveman")
    p <- p + 
      ggforce::geom_mark_hull(ggplot2::aes(x, y, fill = node_group,
                                           label = node_group), data = lo) +
      ggplot2::scale_fill_manual(values = colorsafe_palette,
                                 guide = ggplot2::guide_legend("Group"))
  }
  if(snap){
    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)
  }
  p
}

.graph_edges <- function(p, g, edge_color, edge_size, node_size) {
  if (is_directed(g)) {
    out <- .infer_directed_edge_mapping(g, edge_color, edge_size, node_size)
    p <- map_directed_edges(p, g, out)
  } else {
    out <- .infer_edge_mapping(g, edge_color, edge_size)
    p <- map_edges(p, g, out)
  }
  if (is_complex(g)) {
    p <- p + ggraph::geom_edge_loop0(edge_alpha = 0.4)
  }
  # Check legends
  if (length(unique(out[["esize"]])) == 1) {
    p <- p + ggplot2::guides(edge_width = "none")
  } else p <- p + ggraph::scale_edge_width_continuous(range = c(0.3, 3),
                                                      guide = ggplot2::guide_legend(
                                                        ifelse(is.null(edge_size) &
                                                                 is_weighted(g),
                                                               "Edge Weight", "Edge Size")))
  if (length(unique(out[["ecolor"]])) == 1) {
    p <- p + ggplot2::guides(edge_colour = "none")
  } else if (length(unique(out[["ecolor"]])) == 2){
    p <- p + ggraph::scale_edge_colour_manual(values = getOption("mnet_highlight", default = c("grey","black")),
                                                   guide = ggplot2::guide_legend(
                                                     ifelse(is.null(edge_color) &
                                                              is_signed(g),
                                                            "Edge Sign", "Edge Color")))
    } else p <- p + ggraph::scale_edge_colour_manual(values = getOption("mnet_cat", default = colorsafe_palette),
                                                   guide = ggplot2::guide_legend(
                                                     ifelse(is.null(edge_color) &
                                                              is_signed(g),
                                                            "Edge Sign", "Edge Color")))
  p
}

.graph_nodes <- function(p, g, node_color, node_shape, node_size) {
  out <- .infer_node_mapping(g, node_color, node_size, node_shape)
  if (is.null(node_color) & "Infected" %in% names(node_attribute(g))) {
    p <- map_infected_nodes(p, g, out)
  } else if (is.null(node_color) & any("diff_model" %in% names(attributes(g)))) {
    p <- map_diff_model_nodes(p, g, out)
  } else {
    p <- map_nodes(p, out)
    # Check legends
    if (length(unique(out[["nsize"]])) > 1)
      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(is_twomode(g) & is.null(node_shape), "Node Mode", "Node Shape")))
    if (length(unique(out[["ncolor"]])) > 1){
      if(length(unique(out[["ncolor"]])) == 2){
        p <- p + ggplot2::scale_colour_manual(values = getOption("mnet_highlight", default = c("grey","black")),
                                              guide = ggplot2::guide_legend("Node Color"))
      } else {
        p <- p + ggplot2::scale_colour_manual(values = getOption("mnet_cat", default = colorsafe_palette),
                                              guide = ggplot2::guide_legend("Node Color"))
      }
    }
  }
  # Consider rescaling nodes
  p <- p + ggplot2::scale_size(range = c(1/net_nodes(g)*50, 1/net_nodes(g)*100))
  p
}

.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,
                               TRUE ~ angles$degree - 180)
    if (net_nodes(g) < 10) {
      hj <- ifelse(p[["data"]][,1] >= 0, -0.8, 1.8)
    } else if (net_nodes(g) < 20) {
      hj <- ifelse(p[["data"]][,1] >= 0, -0.4, 1.4)
    } else {
      hj <- ifelse(p[["data"]][,1] >= 0, -0.2, 1.2)
    }
    p <- p + ggraph::geom_node_text(ggplot2::aes(label = name), repel = TRUE,
                                    size = 3, hjust = hj, angle = angles) +
      ggplot2::coord_cartesian(xlim=c(-1.2,1.2), ylim=c(-1.2,1.2))
  } else if (layout %in% c("bipartite", "railway") | layout == "hierarchy" &
             length(unique(p[["data"]][["y"]])) <= 2) {
    p <- p + ggraph::geom_node_text(ggplot2::aes(label = name), angle = 90,
                                    size = 3, hjust = "outward", repel = TRUE,
                                    nudge_y = ifelse(p[["data"]][,2] == 1,
                                                     0.05, -0.05)) +
      ggplot2::coord_cartesian(ylim=c(-0.2, 1.2))
  } else if (layout == "hierarchy" & length(unique(p[["data"]][["y"]])) > 2) {
    p <- p + ggraph::geom_node_text(ggplot2::aes(label = name),
                                    size = 3, hjust = "inward", repel = TRUE)
  } else if (layout %in% c("alluvial", "lineage")) {
    p <- p + ggraph::geom_node_label(ggplot2::aes(label = name), size = 3,
                                     repel = TRUE, nudge_x = ifelse(p[["data"]][,1] == 1, 
                                                                    0.02, -0.02))
  } else {
    p <- p + ggraph::geom_node_label(ggplot2::aes(label = name),
                                     repel = TRUE, seed = 1234, size = 3)
  }
}

# `graphr()` helper functions
reduce_categories <- function(g, node_group) {
  limit <- toCondense <- NULL
  if (sum(table(node_attribute(g, node_group)) <= 2) > 2 &
      length(unique(node_attribute(g, node_group))) > 2) {
    toCondense <- names(which(table(node_attribute(g, node_group)) <= 2))
    out <- ifelse(node_attribute(g, node_group) %in% toCondense,
                  "Other", node_attribute(g, node_group))
    snet_info("The number of groups was reduced since there were groups with less than 2 nodes.")
  } else if (sum(table(node_attribute(g, node_group)) <= 2) == 2 &
             length(unique(node_attribute(g, node_group))) > 2) {
    limit <- stats::reorder(node_attribute(g, node_group),
                            node_attribute(g, node_group),
                            FUN = length, decreasing = TRUE)
    if (sum(utils::tail(attr(limit, "scores"), 2))) {
      toCondense <- utils::tail(levels(limit), 3)
    } else {
      toCondense <- utils::tail(levels(limit), 2)
    }
    out <- ifelse(node_attribute(g, node_group) %in% toCondense, "Other",
                  node_attribute(g, node_group))
    snet_info("The number of groups was reduced since there were groups with less than 2 nodes.")
  } else if (sum(table(node_attribute(g, node_group)) <= 2) == 1 &
             length(unique(node_attribute(g, node_group))) > 2) {
    limit <- stats::reorder(node_attribute(g, node_group),
                            node_attribute(g, node_group),
                            FUN = length, decreasing = TRUE)
    toCondense <- utils::tail(levels(limit), 2)
    out <- ifelse(node_attribute(g, node_group) %in% toCondense, "Other",
                  node_attribute(g, node_group))
    snet_info("The number of groups was reduced since there were groups with less than 2 nodes.")
  } else if (sum(table(node_attribute(g, node_group)) <= 2) == 1 &
             length(unique(node_attribute(g, node_group))) == 2) {
    out <- as.factor(node_attribute(g, node_group))
    snet_info("Node groups with 2 nodes or less can be cause issues for plotting ...")
  } else out <- as.factor(node_attribute(g, node_group))
  out
}

.infer_directed_edge_mapping <- function(g, edge_color, edge_size, node_size) {
  check_edge_variables(g, edge_color, edge_size)
  list("ecolor" = .infer_ecolor(g, edge_color),
       "esize" = .infer_esize(g, edge_size),
       "line_type" = .infer_line_type(g),
       "end_cap" = .infer_end_cap(g, node_size))
}

.infer_edge_mapping <- function(g, edge_color, edge_size) {
  check_edge_variables(g, edge_color, edge_size)
  list("ecolor" = .infer_ecolor(g, edge_color),
       "esize" = .infer_esize(g, edge_size),
       "line_type" = .infer_line_type(g))
}

.infer_ecolor <- function(g, edge_color){
  if (!is.null(edge_color)) {
    if (edge_color %in% names(tie_attribute(g))) {
      if ("tie_mark" %in% class(tie_attribute(g, edge_color))) {
        out <- factor(as.character(tie_attribute(g, edge_color)),
                      levels = c("FALSE", "TRUE"))
      } else out <- as.factor(as.character(tie_attribute(g, edge_color)))
      if (length(unique(out)) == 1) {
        out <- rep("black", net_ties(g))
        snet_info("Please indicate a variable with more than one value or level when mapping edge colors.")
      }
    } else {
      out <- edge_color
    }
  } else if (is.null(edge_color) & is_signed(g)) {
    out <- factor(ifelse(igraph::E(g)$sign >= 0, "Positive", "Negative"),
                  levels = c("Positive", "Negative"))
    if (length(unique(out)) == 1) {
      out <- "black"
    }
  } else {
    out <- "black"
  }
  out
}

.infer_esize <- function(g, edge_size){
  if (!is.null(edge_size)) {
    if (any(edge_size %in% names(tie_attribute(g)))) {
      out <- tie_attribute(g, edge_size)
    } else {
      out <- edge_size
    }
  } else if (is.null(edge_size) & is_weighted(g)) {
    out <- tie_attribute(g, "weight")
  } else {
    out <- 0.5
  }
  out
}

.infer_end_cap <- function(g, node_size) {
  nsize <- .infer_nsize(g, node_size)/2
  # Accounts for rescaling
  if (length(unique(nsize)) == 1) {
    out <- rep(unique(nsize), net_ties(g))
  } else {
    out <- g %>%
      tidygraph::activate("edges") %>%
      data.frame() %>% 
      dplyr::left_join(data.frame(node_id = 1:length(node_names(g)),
                                  nsize = nsize),
                       by = c("to" = "node_id"))
    out <- out$nsize
    out <- ((out - min(out)) / (max(out) - min(out))) *
      ((1 / net_nodes(g) * 100) - (1 / net_nodes(g)*50)) + (1 / net_nodes(g) * 50)
  }
  out
}

.infer_line_type <- function(g) {
  if (is_signed(g)) {
    out <- ifelse(as.numeric(tie_signs(g)) >= 0,
           "solid", "dashed")
    # ifelse(length(unique(out)) == 1, unique(out), out)
  } else out <- "solid"
  out
}

check_edge_variables <- function(g, edge_color, edge_size) {
  if (!is.null(edge_color)) {
    if (any(!tolower(edge_color) %in% tolower(igraph::edge_attr_names(g))) &
        any(!edge_color %in% grDevices::colors())) {
      snet_info("Please make sure you spelled `edge_color` variable correctly.")
    } 
  }
  if (!is.null(edge_size)) {
    if (!is.numeric(edge_size) & any(!tolower(edge_size) %in% tolower(igraph::edge_attr_names(g)))) {
      snet_info("Please make sure you spelled `edge_size` variable correctly.")
    } 
  }
}

map_directed_edges <- function(p, g, out) {
  if (length(out[["ecolor"]]) == 1 & length(out[["esize"]]) == 1) {
    p <- p + ggraph::geom_edge_arc(ggplot2::aes(end_cap = ggraph::circle(c(out[["end_cap"]]), 'mm')),
                                   edge_colour = out[["ecolor"]], edge_width = out[["esize"]],
                                   edge_linetype = out[["line_type"]],
                                   edge_alpha = 0.4, strength = ifelse(igraph::which_mutual(g), 0.2, 0),
                                   arrow = ggplot2::arrow(angle = 15, type = "closed",
                                                          length = ggplot2::unit(2, 'mm')))
  } else if (length(out[["ecolor"]]) > 1 & length(out[["esize"]]) == 1) {
    p <- p + ggraph::geom_edge_arc(ggplot2::aes(edge_colour = out[["ecolor"]],
                                                end_cap = ggraph::circle(c(out[["end_cap"]]), 'mm')),
                                   edge_width = out[["esize"]], edge_linetype = out[["line_type"]],
                                   edge_alpha = 0.4, strength = ifelse(igraph::which_mutual(g), 0.2, 0),
                                   arrow = ggplot2::arrow(angle = 15, type = "closed",
                                                          length = ggplot2::unit(2, 'mm')))
  } else if (length(out[["ecolor"]]) == 1 & length(out[["esize"]]) > 1) {
    p <- p + ggraph::geom_edge_arc(ggplot2::aes(edge_width = out[["esize"]],
                                                end_cap = ggraph::circle(c(out[["end_cap"]]), 'mm')),
                                   edge_colour = out[["ecolor"]], edge_linetype = out[["line_type"]],
                                   edge_alpha = 0.4, strength = ifelse(igraph::which_mutual(g), 0.2, 0),
                                   arrow = ggplot2::arrow(angle = 15, type = "closed",
                                                          length = ggplot2::unit(2, 'mm')))
  } else {
    p <- p + ggraph::geom_edge_arc(ggplot2::aes(edge_colour = getOption("mnet_cat")[out[["ecolor"]]],
                                                edge_width = out[["esize"]],
                                                end_cap = ggraph::circle(c(out[["end_cap"]]), 'mm')),
                                   edge_linetype = out[["line_type"]],
                                   edge_alpha = 0.4, strength = ifelse(igraph::which_mutual(g), 0.2, 0),
                                   arrow = ggplot2::arrow(angle = 15, type = "closed",
                                                          length = ggplot2::unit(2, 'mm')))
  }
  p
}

map_edges <- function(p, g, out) {
  if (length(out[["ecolor"]]) == 1 & length(out[["esize"]]) == 1) {
    p <- p + ggraph::geom_edge_link0(edge_width = out[["esize"]],
                                     edge_colour = out[["ecolor"]],
                                     edge_alpha = 0.4,
                                     edge_linetype = out[["line_type"]])
  } else if (length(out[["ecolor"]]) > 1 & length(out[["esize"]]) == 1) {
    p <- p + ggraph::geom_edge_link0(ggplot2::aes(edge_colour = out[["ecolor"]]),
                                     edge_width = out[["esize"]],
                                     edge_alpha = 0.4,
                                     edge_linetype = out[["line_type"]])
  } else if (length(out[["ecolor"]]) == 1 & length(out[["esize"]]) > 1) {
    p <- p + ggraph::geom_edge_link0(ggplot2::aes(edge_width = out[["esize"]]),
                                     edge_colour = out[["ecolor"]],
                                     edge_alpha = 0.4,
                                     edge_linetype = out[["line_type"]])
  } else {
    p <- p + ggraph::geom_edge_link0(ggplot2::aes(edge_width = out[["esize"]],
                                                  edge_colour = out[["ecolor"]]),
                                     edge_alpha = 0.4, edge_linetype = out[["line_type"]])
  }
}

.infer_node_mapping <- function(g, node_color, node_size, node_shape) {
  check_node_variables(g, node_color, node_size)
  list("nshape" = .infer_nshape(g, node_shape),
       "nsize" = .infer_nsize(g, node_size),
       "ncolor" = .infer_ncolor(g, node_color))
}

.infer_nsize <- function(g, node_size) {
  if (!is.null(node_size)) {
    if (is.character(node_size)) {
      out <- node_attribute(g, node_size)
    } else out <- node_size
    if (length(node_size > 1) & all(out <= 1 & out >= 0)) out <- out * 10
  } else {
    out <- min(20, (250 / net_nodes(g)) / 2)
  }
  as.numeric(out)
}

.infer_nshape <- function(g, node_shape) {
  if (!is.null(node_shape)) {
    if (node_shape %in% names(node_attribute(g))) {
      out <- as.factor(as.character(node_attribute(g, node_shape)))
    } else out <- node_shape
  } else if (is_twomode(g) & is.null(node_shape)) {
    out <- ifelse(igraph::V(g)$type, "One", "Two")
  } else {
    out <- "circle"
  }
  out
}

.infer_ncolor <- function(g, node_color) {
  if (!is.null(node_color)) {
    if (node_color %in% names(node_attribute(g))) {
      if ("node_mark" %in% class(node_attribute(g, node_color))) {
        out <- factor(as.character(node_attribute(g, node_color)),
                      levels = c("FALSE", "TRUE"))
      } else out <- as.factor(as.character(node_attribute(g, node_color)))
      if (length(unique(out)) == 1) {
        out <- rep("black", net_nodes(g))
        snet_info("Please indicate a variable with more than one value or level when mapping node colors.")
      }
    } else out <- node_color
  } else {
    out <- "black"
  }
  out
}

check_node_variables <- function(g, node_color, node_size) {
  if (!is.null(node_color)) {
    if (any(!tolower(node_color) %in% tolower(igraph::vertex_attr_names(g))) &
        any(!node_color %in% grDevices::colors())) {
      snet_info("Please make sure you spelled `node_color` variable correctly.")
    } 
  }
  if (!is.null(node_size)) {
    if (!is.numeric(node_size) & any(!tolower(node_size) %in% tolower(igraph::vertex_attr_names(g)))) {
      snet_info("Please make sure you spelled `node_size` variable correctly.")
    }
  }
}

map_infected_nodes<- function(p, g, out) {
  node_color <- as.factor(ifelse(node_attribute(g, "Exposed"), "Exposed",
                                 ifelse(node_attribute(g, "Infected"),"Infected", 
                                        ifelse(node_attribute(g, "Recovered"), "Recovered",
                                               "Susceptible"))))
  p + ggraph::geom_node_point(ggplot2::aes(color = node_color),
                              size = out[["nsize"]], shape = out[["nshape"]]) +
    ggplot2::scale_color_manual(name = NULL, guide = ggplot2::guide_legend(""),
                                values = c("Infected" = "#d73027",
                                           "Susceptible" = "#4575b4",
                                           "Exposed" = "#E6AB02",
                                           "Recovered" = "#66A61E"))
}

map_diff_model_nodes <- function(p, g, out) {
  node_adopts <- node_adoption_time(attr(g,"diff_model"))
  nshape <- ifelse(node_adopts == min(node_adopts), "Seed(s)",
                   ifelse(node_adopts == Inf, "Non-Adopter", "Adopter"))
  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),
                              size = out[["nsize"]]) +
    ggplot2::scale_color_gradient(low = "#d73027", high = "#4575b4",
                                  breaks=c(min(node_color)+1, 
                                           ifelse(any(nshape=="Non-Adopter"),
                                                  max(node_color)-1,
                                                  max(node_color))),
                                  labels=c("Early\nadoption", "Late\nadoption"),
                                  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),
                    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"]],
                                     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"]]), 
                                     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(size = out[["nsize"]]),
                                     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(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"]]),
                                     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"]]),
                                     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 {
    p <- p + ggraph::geom_node_point(ggplot2::aes(colour = out[["ncolor"]],
                                                  shape = out[["nshape"]],
                                                  size = out[["nsize"]]))
  }
  p
}


cart2pol <- function(xyz){
  stopifnot(is.numeric(xyz))
  if (is.vector(xyz) && (length(xyz) == 2 || length(xyz) == 
                         3)) {
    x <- xyz[1]
    y <- xyz[2]
    m <- 1
    n <- length(xyz)
  }
  else if (is.matrix(xyz) && (ncol(xyz) == 2 || ncol(xyz) == 
                              3)) {
    x <- xyz[, 1]
    y <- xyz[, 2]
    m <- nrow(xyz)
    n <- ncol(xyz)
  }
  else snet_abort("Input must be a vector of length 3 or a matrix with 3 columns.")
  phi <- atan2(y, x)
  r <- hypot(x, y)
  if (n == 2) {
    if (m == 1) 
      prz <- c(phi, r)
    else prz <- cbind(phi, r)
  }
  else {
    if (m == 1) {
      z <- xyz[3]
      prz <- c(phi, r, z)
    }
    else {
      z <- xyz[, 3]
      prz <- cbind(phi, r, z)
    }
  }
  return(prz)
}

hypot <- function (x, y) {
  if ((length(x) == 0 && is.numeric(y) && length(y) <= 1) || 
      (length(y) == 0 && is.numeric(x) && length(x) <= 1)) 
    return(vector())
  if (!is.numeric(x) && !is.complex(x) || !is.numeric(y) && 
      !is.complex(y)) 
    snet_abort("Arguments 'x' and 'y' must be numeric or complex.")
  if (length(x) == 1 && length(y) > 1) {
    x <- rep(x, length(y))
    dim(x) <- dim(y)
  }
  else if (length(x) > 1 && length(y) == 1) {
    y <- rep(y, length(x))
    dim(y) <- dim(x)
  }
  if ((is.vector(x) && is.vector(y) && length(x) != length(y)) || 
      (is.matrix(x) && is.matrix(y) && dim(x) != dim(y)) || 
      (is.vector(x) && is.matrix(y)) || is.matrix(x) && is.vector(y)) 
    snet_abort("Arguments 'x' and 'y' must be of the same size.")
  x <- abs(x)
  y <- abs(y)
  m <- pmin(x, y)
  M <- pmax(x, y)
  ifelse(M == 0, 0, M * sqrt(1 + (m/M)^2))
}

# Longitudinal or comparative networks ####

#' Easily graph a set of networks with sensible defaults
#' 
#' @description 
#'   This function provides users with an easy way to graph
#'   lists of network data for comparison.
#'   
#'   It builds upon this package's `graphr()` function, 
#'   and inherits all the same features and arguments.
#'   See `graphr()` for more.
#'   However, it uses the `{patchwork}` package to plot the graphs
#'   side by side and, if necessary, in successive rows.
#'   This is useful for lists of networks that represent, for example, 
#'   ego or component subgraphs of a network,
#'   or a list of a network's different types of tie or across time.
#'   By default just the first and last network will be plotted,
#'   but this can be overridden by the "waves" parameter.
#'   
#'   Where the graphs are of the same network (same nodes),
#'   the graphs may share a layout to facilitate comparison.
#'   By default, successive graphs will use the layout calculated for 
#'   the "first" network, but other options include the "last" layout,
#'   or a mix, "both", of them.
#' @family mapping
#' @param netlist A list of manynet-compatible networks.
#' @param waves Numeric, the number of plots to be displayed side-by-side.
#'   If missing, the number of plots will be reduced to the first and last
#'   when there are more than four plots.
#'   This argument can also be passed a vector selecting the waves to plot.
#' @param based_on Whether the layout of the joint plots should
#'   be based on the "first" or the "last" network, or "both".
#' @param ... Additional arguments passed to `graphr()`.
#' @return Multiple `ggplot2::ggplot()` objects displayed side-by-side.
#' @name map_graphs
#' @examples
#' #graphs(to_egos(ison_adolescents))
#' #graphs(to_egos(ison_adolescents), waves = 8)
#' #graphs(to_egos(ison_adolescents), waves = c(2, 4, 6))
#' #graphs(play_diffusion(ison_adolescents))
#' @export
graphs <- function(netlist, waves,
                   based_on = c("first", "last", "both"), ...) {
  thisRequires("patchwork")
  based_on <- match.arg(based_on)
  if (any(class(netlist) == "diff_model")){
    if (is_list(attr(netlist, "network"))) netlist <- attr(netlist, "network") else
        netlist <- to_waves(netlist)
  } 
  if (missing(waves)) {
    if (length(netlist) > 4) {
      netlist <- netlist[c(1, length(netlist))]
      snet_info("Plotting first and last waves side-by-side. \nTo set the waves plotted use the 'waves = ' argument.")
    }
  } else if (!missing(waves)) {
    if (length(waves) == 1) netlist <- netlist[c(1:waves)] else 
      netlist <- netlist[waves]
  }
  if (is.null(names(netlist))) names(netlist) <- rep("", length(netlist))
  if (length(unique(lapply(netlist, length))) == 1) {
    if (based_on == "first") {
      lay <- graphr(netlist[[1]], ...)
      x <- lay$data$x
      y <- lay$data$y
    } else if (based_on == "last") {
      lay <- graphr(netlist[[length(netlist)]], ...)
      x <- lay$data$x
      y <- lay$data$y
    } else if (based_on == "both") {
      lay <- graphr(netlist[[1]], ...)
      x1 <- lay$data$x
      y1 <- lay$data$y
      lay1 <- graphr(netlist[[length(netlist)]], ...)
      x <- (lay1$data$x + x1)/2
      y <- (lay1$data$y + y1)/2
    }
    gs <- lapply(1:length(netlist), function(i)
      graphr(netlist[[i]], x = x, y = y, ...) + ggtitle(names(netlist)[i]))
  } else {
    thisRequires("methods")
    if (!methods::hasArg("layout") & is_ego_network(netlist)) {
      gs <- lapply(1:length(netlist), function(i)
        graphr(netlist[[i]], layout = "star", center = names(netlist)[[i]], ...) + 
          ggtitle(names(netlist)[i]))
    } else {
      snet_info("Layouts were not standardised since not all nodes appear across waves.")  
      gs <- lapply(1:length(netlist), function(i)
        graphr(netlist[[i]], ...) + ggtitle(names(netlist)[i]))
    }
  }
  # if (all(c("Infected", "Exposed", "Recovered") %in% names(gs[[1]]$data))) {
  #   gs <- .collapse_guides(gs)
  # }
  do.call(patchwork::wrap_plots, c(gs, list(guides = "collect")))
}

# `graphs()` helper functions
is_ego_network <- function(nlist) {
  if (all(unique(names(nlist)) != "")) {
    length(names(nlist)) == length(unique(unlist(unname(lapply(nlist, node_names))))) &
    all(order_alphabetically(names(nlist)) ==
          order_alphabetically(unique(unlist(unname(lapply(nlist, node_names))))))
  } else FALSE
}

order_alphabetically <- function(v) {
  v[order(names(stats::setNames(v, v)))]
}

# Dynamic networks ####

#' Easily animate dynamic networks with sensible defaults
#' 
#' @description 
#'   This function provides users with an easy way to graph
#'   dynamic network data for exploration and presentation.
#'   
#'   It builds upon this package's `graphr()` function, 
#'   and inherits all the same features and arguments.
#'   See `graphr()` for more.
#'   However, it uses the `{gganimate}` package to animate the changes
#'   between successive iterations of a network.
#'   This is useful for networks in which the ties and/or the node or tie
#'   attributes are changing.
#'   
#'   A progress bar is shown if it takes some time to encoding all the
#'   .png files into a .gif.
#' @name map_grapht
#' @family mapping
#' @param tlist The same migraph-compatible network listed according to
#'   a time attribute, waves, or slices.
#' @param keep_isolates Logical, whether to keep isolate nodes in the graph.
#'   TRUE by default.
#'   If FALSE, removes nodes from each frame they are isolated in.
#' @inheritParams map_graphr
#' @importFrom igraph gsize as_data_frame get.edgelist
#' @importFrom ggplot2 ggplot geom_segment geom_point geom_text
#' scale_alpha_manual theme_void
#' @importFrom ggraph create_layout
#' @importFrom dplyr mutate select distinct left_join %>%
#' @source https://blog.schochastics.net/posts/2021-09-15_animating-network-evolutions-with-gganimate/
#' @return Shows a .gif image. Assigning the result of the function
#'   saves the gif to a temporary folder and the object holds the path to this file.
#' @examples
#' #ison_adolescents %>%
#' #  mutate_ties(year = sample(1995:1998, 10, replace = TRUE)) %>%
#' #  to_waves(attribute = "year", cumulative = TRUE) %>%
#' #  grapht()
#' #ison_adolescents %>% 
#' #  mutate(gender = rep(c("male", "female"), times = 4),
#' #         hair = rep(c("black", "brown"), times = 4),
#' #         age = sample(11:16, 8, replace = TRUE)) %>%
#' #  mutate_ties(year = sample(1995:1998, 10, replace = TRUE),
#' #              links = sample(c("friends", "not_friends"), 10, replace = TRUE),
#' #              weekly_meetings = sample(c(3, 5, 7), 10, replace = TRUE)) %>%
#' #  to_waves(attribute = "year") %>%
#' #  grapht(layout = "concentric", membership = "gender",
#' #             node_shape = "gender", node_color = "hair",
#' #             node_size =  "age", edge_color = "links",
#' #             edge_size = "weekly_meetings")
#' #grapht(play_diffusion(ison_adolescents, seeds = 5))
#' @export
grapht <- function(tlist, keep_isolates = TRUE,
                   layout, labels = TRUE,
                   node_color, node_shape, node_size,
                   edge_color, edge_size, ...,
                   node_colour, edge_colour) {
  thisRequires("gganimate")
  thisRequires("gifski")
  # thisRequires("png")
  x <- y <- name <- status <- frame <- NULL
  # Check arguments
  if (missing(layout)) {
    if (length(tlist[[1]]) == 3) {
      layout <- "triad" 
    } else if (length(tlist[[1]]) == 4) {
      layout <- "quad" 
    } else if (is_twomode(tlist[[1]])) {
      layout <- "hierarchy"
    } else layout <- "stress"
  }
  if (missing(node_color) && missing(node_colour)) {
    node_color <- NULL
  } else if (missing(node_color)) {
    node_color <- as.character(substitute(node_colour))
  } else {
    node_color <- as.character(substitute(node_color))
  }
  if (missing(node_shape)) node_shape <- NULL else
    node_shape <- as.character(substitute(node_shape))
  if (missing(node_size)) node_size <- NULL else if (!is.numeric(node_size)) {
    node_size <- as.character(substitute(node_size))
  }
  if (missing(edge_color) && missing(edge_colour)) {
    edge_color <- NULL
  } else if (missing(edge_color)) {
    edge_color <- as.character(substitute(edge_colour))
  } else {
    edge_color <- as.character(substitute(edge_color))
  }
  if (missing(edge_size)) edge_size <- NULL else if (!is.numeric(edge_size)) {
    edge_size <- as.character(substitute(edge_size))
  }
  # Check if diffusion model
  if (inherits(tlist, "diff_model")) tlist <- to_waves(tlist)
  # Check if object is a list of lists
  if (!is.list(tlist[[1]])) {
    snet_abort("Please declare a migraph-compatible network listed according
         to a time attribute, waves, or slices.")
  }
  # Remove lists without edges
  tlist <- Filter(function(x) igraph::gsize(x) > 0, tlist)
  # Check names for groups
  if (!"name" %in% names(node_attribute(tlist[[1]]))) {
    labels <- FALSE
    for (i in seq_len(length(tlist))) {
      tlist[[i]] <- add_node_attribute(tlist[[i]], "name",
                                       as.character(seq_len(igraph::vcount(tlist[[i]]))))
    }
  }
  # Create an edge list
  edges_lst <- lapply(1:length(tlist), function(i)
    cbind(igraph::as_data_frame(tlist[[i]], "edges"),
          frame = ifelse(is.null(names(tlist)), i, names(tlist)[i])))
  # Check if all names are present in all lists
  if (length(unique(unname(lapply(tlist, length)))) != 1) {
    if (any(c(node_shape, node_color, node_size) %in% names(node_attribute(tlist[[1]])))) {
      node_info <- dplyr::distinct(do.call(rbind, lapply(1:length(tlist), function(i)
        tlist[[i]] %>% activate("nodes") %>% data.frame()))) # keep node info for latter
    } else node_info <- NULL
    tlist <- to_waves(as_tidygraph(do.call("rbind", edges_lst)), attribute = "frame")
  } else node_info <- NULL
  # Add separate layouts for each time point
  lay <- lapply(1:length(tlist), function(i)
    ggraph::create_layout(tlist[[i]], layout, ...))
  # Create a node list for each time point
  nodes_lst <- lapply(1:length(tlist), function(i) {
    cbind(igraph::as_data_frame(tlist[[i]], "vertices"),
          x = lay[[i]][, 1], y = lay[[i]][, 2],
          frame = ifelse(is.null(names(tlist)), i, names(tlist)[i]))
  })
  # Create an edge list for each time point
  edges_lst <- time_edges_lst(tlist, edges_lst, nodes_lst)
  # Get edge IDs for all edges
  all_edges <- do.call("rbind", lapply(tlist, igraph::get.edgelist))
  all_edges <- all_edges[!duplicated(all_edges), ]
  all_edges <- cbind(all_edges, paste0(all_edges[, 1], "-", all_edges[, 2]))
  # Add edges level information for edge transitions
  edges_lst <- transition_edge_lst(tlist, edges_lst, nodes_lst, all_edges)
  # Bind nodes and edges list
  edges_out <- do.call("rbind", edges_lst)
  nodes_out <- do.call("rbind", nodes_lst)
  if (!is.null(node_info)) {
    nodes_out <- dplyr::left_join(nodes_out, node_info[!duplicated(node_info$name),], by = "name")
  }
  # Delete nodes for each frame if isolate
  if (isFALSE(keep_isolates)) {
    nodes_out <- remove_isolates(edges_out, nodes_out)
  } else {
    if (nrow(nodes_out)/length(unique(nodes_out$frame)) > 30 &
        any(unlist(lapply(tlist, node_is_isolate)) == TRUE)) {
      snet_info("Please considering deleting isolates to improve visualisation.")
    } 
    nodes_out$status <- TRUE
  }
  # Plot with ggplot2/ggraph and animate with gganimate
  p <- map_dynamic(edges_out, nodes_out, edge_color, node_shape,
                   node_color, node_size, edge_size, labels) +
    gganimate::transition_states(states = frame, transition_length = 5,
                                 state_length = 10, wrap = FALSE) +
    gganimate::enter_fade() +
    gganimate::exit_fade() +
    ggplot2::labs(title = "{closest_state}")
  gganimate::animate(p, duration = 2*length(tlist), start_pause = 5,
                     end_pause = 10, renderer = gganimate::gifski_renderer())
}

map_dynamic <- function(edges_out, nodes_out, edge_color, node_shape,
                        node_color, node_size, edge_size, labels) {
  x <- xend <- y <- yend <- id <- status <- Infected <- name <- NULL
  alphad <- ifelse(nodes_out$status == TRUE, 1, 0)
  alphae <- ifelse(edges_out$status == TRUE, 1, 0)
  if (all(unique(alphae) == 1)) alphae <- 0.8
  # Plot edges
  if (!is.null(edge_color)) {
    # Remove NAs in edge color, if declared
    if (edge_color %in% names(edges_out)) {
      edge_color <- .check_color(edges_out[[edge_color]])
    }
  } else edge_color <- "black"
  if (!is.null(edge_size)) {
    if (edge_size %in% names(edges_out)) {
      edge_size <- as.numeric(edges_out[[edge_size]])
      edge_size <- ifelse(is.na(edge_size), 0.5, edge_size)
    }
  } else edge_size <- 0.5
  p <- ggplot2::ggplot() + 
    ggplot2::geom_segment(aes(x = x, xend = xend, y = y, yend = yend, group = id),
                          alpha = alphae, data = edges_out, color = edge_color,
                          linewidth = edge_size, show.legend = FALSE)
  # Set node shape, color, and size
  if (!is.null(node_shape)) {
    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]
      }
    }
  } else node_shape <- "circle"
  if (!is.null(node_color)) {
    if (node_color %in% names(nodes_out)) {
      node_color <- .check_color(nodes_out[[node_color]])
    }
  } else if (is.null(node_color) & "Infected" %in% names(nodes_out)) {
    node_color <- as.factor(ifelse(nodes_out[["Exposed"]], "Exposed",
                                   ifelse(nodes_out[["Infected"]],"Infected", 
                                          ifelse(nodes_out[["Recovered"]], "Recovered",
                                                 "Susceptible"))))
  } else node_color <- "darkgray"
  if (!is.null(node_size)) {
    if (node_size %in% names(nodes_out)) {
      node_size <- nodes_out[[node_size]]
    }
  } else if (nrow(nodes_out) > 100) {
    node_size <- 3
  } else node_size <- nrow(nodes_out)/length(unique(nodes_out$frame))
  # Add labels
  if (isTRUE(labels)) {
    p <- p + ggplot2::geom_text(aes(x, y, label = name), alpha = alphad,
                                data = nodes_out, color = "black",
                                hjust = -0.2, vjust = -0.2, show.legend = FALSE)
  }
  # Plot nodes
  if ("Infected" %in% names(nodes_out)) {
    p <- p + ggplot2::geom_point(aes(x, y, group = name, color = node_color),
                                 size = node_size, shape = node_shape, data = nodes_out) +
      ggplot2::scale_color_manual(name = NULL, values = c("Infected" = "#d73027",
                                                          "Susceptible" = "#4575b4",
                                                          "Exposed" = "#E6AB02",
                                                          "Recovered" = "#66A61E")) +
      ggplot2::theme_void() +
      ggplot2::theme(legend.position = "bottom")
  } 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,
                                 show.legend = FALSE) +
      ggplot2::theme_void()
  }
  p
}

# `graphd()` helper functions
.check_color <- function(v) {
  color <- grDevices::colors()
  color <- color[!color %in% "black"]
  v <- ifelse(is.na(v), "black", v)
  if (!any(grepl(paste(color, collapse = "|"), v)) | any(grepl("^#", v))) {
    for(i in unique(v)) {
      if (i != "black") {
        v[v == i] <- sample(color, 1)
      }
    }
  }
  v
}

time_edges_lst <- function(tlist, edges_lst, nodes_lst, edge_color) {
  lapply(1:length(tlist), function(i) {
    edges_lst[[i]]$x <- nodes_lst[[i]]$x[match(edges_lst[[i]]$from,
                                               nodes_lst[[i]]$name)]
    edges_lst[[i]]$y <- nodes_lst[[i]]$y[match(edges_lst[[i]]$from,
                                               nodes_lst[[i]]$name)]
    edges_lst[[i]]$xend <- nodes_lst[[i]]$x[match(edges_lst[[i]]$to,
                                                  nodes_lst[[i]]$name)]
    edges_lst[[i]]$yend <- nodes_lst[[i]]$y[match(edges_lst[[i]]$to,
                                                  nodes_lst[[i]]$name)]
    edges_lst[[i]]$id <- paste0(edges_lst[[i]]$from, "-", edges_lst[[i]]$to)
    edges_lst[[i]]$status <- TRUE
    edges_lst[[i]]
  })
}

transition_edge_lst <- function(tlist, edges_lst, nodes_lst, all_edges) {
  x <- lapply(1:length(tlist), function(i) {
    idx <- which(!all_edges[, 3] %in% edges_lst[[i]]$id)
    if (length(idx) != 0) {
      tmp <- data.frame(from = all_edges[idx, 1], to = all_edges[idx, 2],
                        id = all_edges[idx, 3])
      tmp$x <- nodes_lst[[i]]$x[match(tmp$from, nodes_lst[[i]]$name)]
      tmp$y <- nodes_lst[[i]]$y[match(tmp$from, nodes_lst[[i]]$name)]
      tmp$xend <- nodes_lst[[i]]$x[match(tmp$to, nodes_lst[[i]]$name)]
      tmp$yend <- nodes_lst[[i]]$y[match(tmp$to, nodes_lst[[i]]$name)]
      tmp$frame <- ifelse(is.null(names(tlist)), i, names(tlist)[i])
      tmp$status <- FALSE
      edges_lst[[i]] <- dplyr::bind_rows(edges_lst[[i]], tmp)
    }
    edges_lst[[i]]
  })
}

remove_isolates <- function(edges_out, nodes_out) {
  status <- frame <- from <- to <- framen <- NULL
  # Create node metadata for node presence in certain frame
  meta <- edges_out %>%
    dplyr::filter(status == TRUE) %>%
    dplyr::mutate(framen = match(frame, unique(frame)),
                  meta = ifelse(framen > 1, paste0(from, (framen - 1)), from)) %>%
    dplyr::select(meta, status) %>%
    dplyr::distinct()
  metab <- edges_out %>%
    dplyr::filter(status == TRUE) %>%
    dplyr::mutate(framen = match(frame, unique(frame)),
                  meta = ifelse(framen > 1, paste0(to, (framen - 1)), to)) %>%
    dplyr::select(meta, status) %>%
    rbind(meta) %>%
    dplyr::distinct()
  # Mark nodes that are isolates
  nodes_out$meta <- rownames(nodes_out)
  # Join data
  nodes_out <- dplyr::left_join(nodes_out, metab, by = "meta") %>%
    dplyr::mutate(status = ifelse(is.na(status), FALSE, TRUE)) %>%
    dplyr::distinct()
}

Try the manynet package in your browser

Any scripts or data that you put into this service are public.

manynet documentation built on June 23, 2025, 9:07 a.m.