1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
|
#' Create a temporary alternative representation of the graph to compute on
#'
#' The `morph`/`unmorph` verbs are used to create temporary representations of
#' the graph, such as e.g. its search tree or a subgraph. A morphed graph will
#' accept any of the standard `dplyr` verbs, and changes to the data is
#' automatically propagated to the original graph when unmorphing. Tidygraph
#' comes with a range of [morphers], but is it also possible to supply your own.
#' See Details for the requirement for custom morphers. The `crystallise` verb
#' is used to extract the temporary graph representation into a tibble
#' containing one separate graph per row and a `name` and `graph` column holding
#' the name of each graph and the graph itself respectively. `convert()` is a
#' shorthand for performing both `morph` and `crystallise` along with extracting
#' a single `tbl_graph` (defaults to the first). For morphs were you know they
#' only create a single graph, and you want to keep it, this is an easy way.
#'
#' @details
#' It is only possible to change and add to node and edge data from a
#' morphed state. Any filtering/removal of nodes and edges will not result in
#' removal from the main graph. However, nodes and edges not present in the
#' morphed state will be unaffected in the main graph when unmorphing (if new
#' columns were added during the morhped state they will be filled with `NA`).
#'
#' Morphing an already morhped graph will unmorph prior to applying the new
#' morph.
#'
#' During a morphed state, the mapping back to the original graph is stored in
#' `.tidygraph_node_index` and `.tidygraph_edge_index` columns. These are
#' accesible but protected, meaning that any changes to them with e.g. mutate
#' will be ignored. Furthermore, if the morph results in the merging of nodes
#' and/or edges the original data is stored in a `.data` column. This is
#' protected as well.
#'
#' When supplying your own morphers the morphing function should accept a
#' `tbl_graph` as its first input. The provided graph will already have nodes
#' and edges mapped with a `.tidygraph_node_index` and `.tidygraph_edge_index`
#' column. The return value must be a `tbl_graph` or a list of `tbl_graph`s and
#' these must contain either a `.tidygraph_node_index` column or a
#' `.tidygraph_edge_index` column (or both). Note that it is possible for the
#' morph to have the edges mapped back to the original nodes and vice versa
#' (e.g. as with [to_linegraph]). In that case the edge data in the morphed
#' graph(s) will contain a `.tidygraph_node_index` column and/or the node data a
#' `.tidygraph_edge_index` column. If the morphing results in the collapse of
#' multiple columns or edges the index columns should be converted to list
#' columns mapping the new node/edge back to all the nodes/edges it represents.
#' Furthermore the original node/edge data should be collapsed to a list of
#' tibbles, with the row order matching the order in the index column element.
#'
#' @param .data A `tbl_graph` or a `morphed_tbl_graph`
#'
#' @param .f A morphing function. See [morphers] for a list of provided one.
#'
#' @param ... Arguments passed on to the morpher
#'
#' @param .select The graph to return during `convert()`. Either an index or the
#' name as created during `crystallise()`.
#'
#' @param .clean Should references to the node and edge indexes in the original
#' graph be removed when using `convert`
#'
#' @return A `morphed_tbl_graph`
#'
#' @export
#'
#' @examples
#' create_notable('meredith') %>%
#' mutate(group = group_infomap()) %>%
#' morph(to_contracted, group) %>%
#' mutate(group_centrality = centrality_pagerank()) %>%
#' unmorph()
morph <- function(.data, .f, ...) {
UseMethod('morph')
}
#' @rdname morph
#' @export
unmorph <- function(.data) {
UseMethod('unmorph')
}
#' @rdname morph
#' @export
crystallise <- function(.data) {
UseMethod('crystallise')
}
#' @rdname morph
#' @export
crystallize <- crystallise
#' @rdname morph
#' @export
convert <- function(.data, .f, ..., .select = 1, .clean = FALSE) {
UseMethod('convert')
}
#' @export
#' @importFrom rlang as_quosure sym quo_text enquo
morph.tbl_graph <- function(.data, .f, ...) {
if (is.focused_tbl_graph(.data)) {
cli::cli_inform('Unfocusing prior to morphing')
.data <- unfocus(.data)
}
if (inherits(.data, 'grouped_tbl_graph')) {
cli::cli_inform('Ungrouping prior to morphing')
.data <- ungroup(.data)
}
.register_graph_context(.data)
morph_name <- quo_text(enquo(.f))
current_active <- as_quosure(sym(active(.data)), environment())
.data <- mutate(activate(.data, 'nodes'), .tidygraph_node_index = seq_len(n()))
.data <- mutate(activate(.data, 'edges'), .tidygraph_edge_index = seq_len(n()))
.data <- activate(.data, !! current_active)
morphed <- .f(.data, ...)
if (!inherits(morphed, 'list')) morphed <- list(morphed)
check_morph(morphed)
morphed[] <- lapply(morphed, activate, what = !!current_active)
structure(
morphed,
class = c('morphed_tbl_graph', 'list'),
.orig_graph = .data,
.morpher = morph_name
)
}
#' @export
morph.morphed_tbl_graph <- function(.data, .f, ...) {
cli::cli_inform('Unmorphing {.arg .data} first...')
.data <- unmorph(.data)
morph(.data, .f, ...)
}
#' @export
unmorph.morphed_tbl_graph <- function(.data) {
current_active <- as_quosure(sym(active(.data[[1]])), environment())
nodes <- bind_rows(lapply(.data, as_tibble, active = 'nodes'))
edges <- bind_rows(lapply(.data, as_tibble, active = 'edges'))
graph <- attr(.data, '.orig_graph')
real_nodes <- as_tibble(graph, active = 'nodes')
real_edges <- as_tibble(graph, active = 'edges')
if ('.tidygraph_node_index' %in% names(nodes)) {
real_nodes <- merge_meta(nodes, real_nodes, '.tidygraph_node_index')
} else if ('.tidygraph_node_index' %in% names(edges)) {
real_nodes <- merge_meta(edges, real_nodes, '.tidygraph_node_index')
}
if ('.tidygraph_edge_index' %in% names(nodes)) {
real_edges <- merge_meta(nodes, real_edges, '.tidygraph_edge_index')
} else if ('.tidygraph_edge_index' %in% names(edges)) {
real_edges <- merge_meta(edges, real_edges, '.tidygraph_edge_index')
}
real_nodes$.tidygraph_node_index <- NULL
real_edges$.tidygraph_edge_index <- NULL
graph <- set_node_attributes(graph, real_nodes)
graph <- set_edge_attributes(graph, real_edges)
activate(graph, !!current_active)
}
#' @importFrom tibble tibble
#' @importFrom rlang %||%
#' @export
crystallise.morphed_tbl_graph <- function(.data) {
class(.data) <- 'list'
attr(.data, '.orig_graph') <- NULL
attr(.data, '.morpher') <- NULL
name <- names(.data) %||% as.character(seq_along(.data))
graph <- unname(.data)
tibble(
name = name,
graph = graph
)
}
#' @export
convert.tbl_graph <- function(.data, .f, ..., .select = 1, .clean = FALSE) {
if (length(.select) != 1) cli::cli_abort("{.arg .select} must be a single scalar")
graphs <- crystallise(morph(.data, .f, ...))
if (is.character(.select)) {
.select <- which(.select == graphs$name)[1]
if (is.na(.select)) cli::cli_abort('{.arg .select} does not match any named graph')
}
if (.select > nrow(graphs)) cli::cli_abort(c('{.fn convert} did not create {.select} number of graphs', 'i' = 'Set a lower {.arg .select}'))
graph <- graphs$graph[[.select]]
if (.clean) {
nodes <- as_tibble(graph, active = 'nodes')
edges <- as_tibble(graph, active = 'edges')
nodes$.tidygraph_node_index <- NULL
edges$.tidygraph_edge_index <- NULL
graph <- set_node_attributes(graph, nodes)
graph <- set_edge_attributes(graph, edges)
}
graph
}
# HELPERS -----------------------------------------------------------------
#' @importFrom igraph vertex_attr_names edge_attr_names
check_morph <- function(morph) {
if (!all(vapply(morph, inherits, logical(1), 'tbl_graph'))) {
cli::cli_abort('{.arg morph} must consist of {.cls tbl_graphs}')
}
lapply(morph, function(m) {
attr_names <- c(vertex_attr_names(m), edge_attr_names(m))
if (!any(c('.tidygraph_node_index', '.tidygraph_edge_index') %in% attr_names)) {
cli::cli_abort('{.arg morph} must contain reference to either nodes or edges')
}
})
NULL
}
merge_meta <- function(new, into, col) {
if (is.list(new[[col]])) {
index <- new[[col]]
new[[col]] <- NULL
data <- new$.orig_data
new$.orig_data <- NULL
new <- new[rep(seq_along(index), lengths(index)), ]
new[[col]] <- unlist(index)
if (!is.null(data)) {
data <- bind_rows(data)
data <- data[, !names(data) %in% names(new)]
new <- bind_cols(new, data)
}
}
new <- new[!is.na(new[[col]]) & !duplicated(new[[col]]), ]
complete <- as_tibble(modifyList(
into[new[[col]], ],
new
))
complete <- bind_rows(complete, into[!into[[col]] %in% complete[[col]], ])
complete <- complete[order(complete[[col]]), ]
complete
}
protect_ind <- function(data, .f, ...) {
new_data <- .f(data, ...)
new_data$.tidygraph_node_index <- data$.tidygraph_node_index
new_data$.tidygraph_edge_index <- data$.tidygraph_edge_index
if ((is.list(data$.tidygraph_node_index) || is.list(data$.tidygraph_edge_index)) && !is.null(data$.data)) {
new_data$.data <- data$.data
}
new_data
}
|