diff --git a/.Rbuildignore b/.Rbuildignore index 0475d4b..cb13bba 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -17,3 +17,9 @@ netplot\._.*\.tar\.gz ^AGENTS\.md$ ^\.devcontainer$ ^\.vscode$ +^_pkgdown\.yml$ +^docs$ +^pkgdown$ +^\.claude$ +^CLAUDE\.md$ +figures/logo\.svg diff --git a/DESCRIPTION b/DESCRIPTION index dac35bb..4b9f5ab 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: netplot Title: Beautiful Graph Drawing -Version: 0.3-0 +Version: 0.4-0 Authors@R: c( person("George", "Vega Yon", email = "g.vegayon@gmail.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-3171-0844")), @@ -17,7 +17,6 @@ Depends: License: MIT + file LICENSE Encoding: UTF-8 LazyData: true -RoxygenNote: 7.3.3 Roxygen: list(markdown = TRUE) Imports: graphics, @@ -39,5 +38,6 @@ Suggests: gridBase, magrittr, tinytest VignetteBuilder: knitr -URL: https://github.com/USCCANA/netplot +URL: https://github.com/USCCANA/netplot, https://usccana.github.io/netplot/ BugReports: https://github.com/USCCANA/netplot/issues +Config/roxygen2/version: 8.0.0 diff --git a/NEWS.md b/NEWS.md index b974904..7faa402 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,23 @@ -# netplot 0.3-0 +# netplot 0.4-0 + +* Attribute formulas for `vertex.nsides`, `vertex.size`, and `edge.width` now + evaluate the right-hand side against the graph's attributes, so expressions + such as `edge.width = ~ log1p(weight)` or `vertex.size = ~ degree ^ 2` work in + addition to bare attribute names. Previously any non-trivial right-hand side + failed with a cryptic `the condition has length > 1` error. Missing attributes + now produce a clear, informative message. + +* `vertex.color = ~ attr` now draws a legend appropriate to the attribute type: + a categorical key for discrete attributes (factor, logical, low-cardinality + integer) and a continuous **color bar** for continuous ones. + +* Documentation overhaul: `nplot()` now documents the formula interface for + mapping vertex/edge aesthetics from graph attributes, the README highlights + what sets netplot apart, a package-level help page was added, and the pkgdown + reference is organized by feature. + +* New vignette `"formulas"` demonstrating how to color, shape, and size vertices + (and scale edge widths) from graph attributes using formulas. * `nplot.network()` now uses the `"weight"` edge attribute for `edge.width` by default (like `nplot.igraph()` already did), so edge widths reflect edge @@ -16,6 +35,9 @@ * `edge.line.lty` is now validated to be length 1 or one value per plotted edge, and is subset correctly when `sample.edges` drops edges. + +# netplot 0.3-0 + * Invalid arguments passed to `nplot()` now raise an error. * Figures with legends are not drawn twice. diff --git a/R/attribute-extraction.R b/R/attribute-extraction.R index ea93a06..b6b295a 100644 --- a/R/attribute-extraction.R +++ b/R/attribute-extraction.R @@ -68,6 +68,84 @@ get_edge_attribute.network <- function(graph, attribute) { } +#' Retrieve all vertex/edge attributes of a graph as a named list +#' +#' Used as the data environment when evaluating attribute formulas (e.g. +#' `~ log(weight)`). +#' @param graph A graph object of class `igraph` or `network`. +#' @param type Either `"vertex"` or `"edge"`. +#' @return A named list, one element per attribute. +#' @noRd +all_graph_attributes <- function(graph, type = c("vertex", "edge")) { + + type <- match.arg(type) + + if (inherits(graph, "igraph")) { + + if (type == "vertex") + igraph::vertex_attr(graph) + else + igraph::edge_attr(graph) + + } else if (inherits(graph, "network")) { + + nms <- if (type == "vertex") + network::list.vertex.attributes(graph) + else + network::list.edge.attributes(graph) + + getter <- if (type == "vertex") + network::get.vertex.attribute + else + network::get.edge.attribute + + stats::setNames(lapply(nms, function(a) getter(graph, a)), nms) + + } else { + + stop( + "Attribute formulas are only supported for 'igraph' and 'network' ", + "objects (got '", class(graph)[1], "').", + call. = FALSE + ) + + } + +} + +#' Evaluate a one-sided formula against a graph's vertex/edge attributes +#' +#' The right-hand side of the formula is evaluated with the graph's attributes +#' available as variables, so both bare attribute names (`~ weight`) and +#' expressions (`~ log(weight)`, `~ 2 * weight`) are supported. +#' @param graph A graph object of class `igraph` or `network`. +#' @param formula A one-sided formula. +#' @param type Either `"vertex"` or `"edge"`. +#' @return The evaluated vector. +#' @noRd +eval_attribute_formula <- function(graph, formula, type = c("vertex", "edge")) { + + type <- match.arg(type) + attrs <- all_graph_attributes(graph, type) + rhs <- formula[[length(formula)]] + enclos <- environment(formula) + if (is.null(enclos)) + enclos <- parent.frame() + + tryCatch( + eval(rhs, envir = attrs, enclos = enclos), + error = function(e) stop( + "Could not evaluate the ", type, " formula `", deparse(rhs), "`. ", + "Make sure the ", type, " attribute(s) it references exist in the graph. ", + "Available ", type, " attributes: ", + if (length(attrs)) paste(names(attrs), collapse = ", ") else "(none)", + ". Original error: ", conditionMessage(e), + call. = FALSE + ) + ) + +} + #' @keywords internal get_edge_attribute.default <- function(graph, attribute) { diff --git a/R/color_nodes_function.R b/R/color_nodes_function.R index 8bcba48..96e7d70 100644 --- a/R/color_nodes_function.R +++ b/R/color_nodes_function.R @@ -153,18 +153,12 @@ color_nodes_legend <- function(object) { stop("Object is not of class netplot_color_nodes") } - # Acts depending on the type - values <- if (attr(x, "attr_type") %in% c("logical", "factor", "integer")) { - attr(x, "cpal") - } else { - - # Generating values - structure( - attr(x, "cpal")(c(0, .25, .5, .75, 1)), - names = stats::quantile(attr(x, "value"), probs = c(0, .25, .5, .75, 1)) - ) + # Continuous attributes get a color bar rather than a set of discrete keys + if (legend_is_continuous(x)) + return(color_nodes_legend_continuous(object)) - } + # Discrete attributes (factor/logical/small integer): categorical legend + values <- attr(x, "cpal") print(nplot_legend( object, @@ -175,6 +169,101 @@ color_nodes_legend <- function(object) { } +#' Decide whether a color mapping should use a continuous (color bar) legend +#' +#' Factors and logicals are always discrete. Numeric attributes are continuous. +#' Integer attributes are treated as continuous only when they take many +#' distinct values (otherwise a categorical legend is clearer). +#' @param x A `netplot_color_nodes` object. +#' @noRd +legend_is_continuous <- function(x) { + + attr_type <- attr(x, "attr_type") + + if (attr_type == "numeric") + return(TRUE) + + if (attr_type == "integer") + return(length(unique(attr(x, "value"))) > 15L) + + FALSE + +} + +#' Draw a `netplot` object together with a continuous color-bar legend +#' +#' Called by [print.netplot()] when `vertex.color` was mapped from a continuous +#' attribute. The network is drawn first and the color bar is overlaid in the +#' top-right corner of the device. +#' @param object A `netplot` object with a continuous `.legend_vertex_fill`. +#' @noRd +color_nodes_legend_continuous <- function(object) { + + x <- object$.legend_vertex_fill + values <- attr(x, "value") + palette <- attr(x, "palette") + main <- attr(x, "attr_name") + + rng <- range(values, na.rm = TRUE) + cols <- grDevices::colorRampPalette(palette)(100) # low -> high + + # Tick marks within the observed range + ticks <- pretty(rng, n = 4L) + ticks <- ticks[ticks >= rng[1] & ticks <= rng[2]] + if (length(ticks) < 2L) + ticks <- rng + + # Draw the network first (without recursing into the legend logic) + print(object, legend = FALSE, newpage = TRUE) + + # Overlay the color bar in the top-right corner of the device + vp <- grid::viewport( + x = grid::unit(1, "npc") - grid::unit(1.5, "lines"), + y = grid::unit(0.78, "npc"), + width = grid::unit(0.6, "lines"), + height = grid::unit(0.30, "npc"), + just = c("right", "center"), + yscale = rng, + name = "netplot-colorkey" + ) + + grid::pushViewport(vp) + on.exit(grid::upViewport(), add = TRUE) + + # The gradient (top = high value) + grid::grid.raster( + grDevices::as.raster(matrix(rev(cols), ncol = 1L)), + width = grid::unit(1, "npc"), + height = grid::unit(1, "npc"), + interpolate = TRUE + ) + + # A thin frame around the bar + grid::grid.rect(gp = grid::gpar(fill = NA, col = "gray40", lwd = .5)) + + # Tick labels to the right of the bar + grid::grid.text( + label = format(ticks, trim = TRUE), + x = grid::unit(1, "npc") + grid::unit(0.3, "lines"), + y = grid::unit(ticks, "native"), + just = "left", + gp = grid::gpar(fontsize = 8) + ) + + # Title above the bar + if (length(main) && nzchar(main)) + grid::grid.text( + label = main, + x = grid::unit(0.5, "npc"), + y = grid::unit(1, "npc") + grid::unit(0.7, "lines"), + just = "bottom", + gp = grid::gpar(fontsize = 9, fontface = "bold") + ) + + invisible(object) + +} + if (FALSE) { diff --git a/R/coloring.R b/R/coloring.R index ef63eac..09564b2 100644 --- a/R/coloring.R +++ b/R/coloring.R @@ -132,13 +132,35 @@ netplot_edge_formulae <- function(x, fm) { }) } -#' Formulas in `netplot` +#' Edge-color formulas in `netplot` #' #' Edge colors in both [nplot()] and [set_edge_gpar()] can be specified using #' a formula based on `ego()` and `alter()` (source and target). This way the #' user can set various types of combination varying the mixing of the colors, #' the alpha levels, and the actual mixing colors to create edge colors. #' +#' @details +#' The formula is one-sided and combines the special terms `ego()` (the source +#' vertex) and `alter()` (the target vertex) with `+`. Each term draws its color +#' from the corresponding endpoint's `vertex.color` unless a `col` is supplied, +#' and accepts: +#' +#' - `col`: the base color to use for that endpoint. +#' - `alpha`: transparency, from 0 (fully transparent) to 1 (opaque). +#' - `mix`: how much weight that endpoint's color receives when the two are +#' blended along the edge. +#' +#' For instance, `~ ego(alpha = .1, col = "gray") + alter` (the default) fades +#' each edge from a faint gray at the source to the target's color, producing the +#' characteristic netplot look. +#' +#' This grammar is specific to *edge colors*. To map other aesthetics (vertex +#' color, shape, size, or edge width) from a graph attribute, pass a formula +#' naming the attribute directly to [nplot()] (e.g. `vertex.color = ~ group`); +#' see the "Mapping attributes with formulas" section of [nplot()]. +#' +#' @seealso [nplot()] for the attribute-mapping formulas, and [set_edge_gpar()]. +#' #' @param col Any valid color. Can be a single color or a vector. #' @param alpha Number. Alpha levels #' @param mix Number. For mixing colors between `ego` and `alter` diff --git a/R/netplot-package.R b/R/netplot-package.R index 6eb8285..2e9ef4f 100644 --- a/R/netplot-package.R +++ b/R/netplot-package.R @@ -1,2 +1,13 @@ +#' netplot: Beautiful Graph Drawing #' -NULL +#' netplot is a graph visualization engine that emphasizes aesthetics while +#' providing default parameters that yield out-of-the-box nice visualizations. +#' It is built on top of the **grid** graphics system (the same engine behind +#' **ggplot2**) and works seamlessly with `igraph` and `network` objects. +#' +#' The main entry point is [nplot()]. See `vignette("examples", package = +#' "netplot")` for a general overview and `vignette("formulas", package = +#' "netplot")` for mapping vertex/edge aesthetics from graph attributes. +#' +#' @keywords internal +"_PACKAGE" diff --git a/R/netplot.R b/R/netplot.R index 6d5e651..94916e4 100644 --- a/R/netplot.R +++ b/R/netplot.R @@ -20,16 +20,38 @@ edge_color_mixer <- function(i, j, vcols, p = .5, alpha = .15) { #' Plot a network #' -#' This is a description. +#' `nplot()` is the main function of the **netplot** package. It draws a network +#' using the **grid** graphics system (the same engine that powers **ggplot2**), +#' emphasizing aesthetics and providing sensible defaults that yield +#' out-of-the-box nice visualizations. Compared with base `igraph`/`network` +#' plots, `nplot()` auto-scales vertices and edges relative to the plotting +#' device, draws truly curved edges, mixes edge colors from their endpoints, +#' and fills the device efficiently. +#' +#' Vertex and edge aesthetics can be set directly (passing a vector) or, +#' conveniently, mapped from graph attributes using a *formula* interface (see +#' the "Mapping attributes with formulas" section below). The returned object is +#' a **grid** `grob`, so it can be further edited with [set_vertex_gpar()] / +#' [set_edge_gpar()], combined with other grid graphics (e.g. via +#' `gridExtra::grid.arrange()`), or annotated with a legend through +#' [nplot_legend()]. #' #' @param x A graph. It supports networks stored as `igraph`, `network`, and #' matrices objects (see details). #' @param bg.col Color of the background. #' @param layout Numeric two-column matrix with the graph layout in x/y positions of the vertices. -#' @param vertex.size Numeric vector of length `vcount(x)`. Absolute size of the vertex from 0 to 1. -#' @param vertex.nsides Numeric vector of length `vcount(x)`. Number of sizes of -#' the vertex. E.g. three is a triangle, and 100 approximates a circle. -#' @param vertex.color Vector of length `vcount(x)`. Vertex HEX or built in colors. +#' @param vertex.size Numeric vector of length `vcount(x)`. Absolute size of the +#' vertex from 0 to 1. Can also be a one-sided formula (e.g. `~ degree`) naming a +#' numeric vertex attribute to map sizes from (see "Mapping attributes with +#' formulas"). +#' @param vertex.nsides Numeric vector of length `vcount(x)`. Number of sides of +#' the vertex. E.g. three is a triangle, and 100 approximates a circle. Can also +#' be a one-sided formula (e.g. `~ group`) naming a vertex attribute; each unique +#' value is then mapped to a distinct shape (see "Mapping attributes with +#' formulas"). +#' @param vertex.color Vector of length `vcount(x)`. Vertex HEX or built in +#' colors. Can also be a one-sided formula (e.g. `~ group`) naming a vertex +#' attribute to color vertices by (see "Mapping attributes with formulas"). #' @param vertex.size.range Numeric vector of length 2 or 3, or `NULL`. Relative size for the #' minimum and maximum of the plot, and curvature of the scale. The third number #' is used as `size^rel[3]`. If `NULL`, scaling is suppressed and `vertex.size` @@ -56,7 +78,9 @@ edge_color_mixer <- function(i, j, vcols, p = .5, alpha = .15) { #' Values are normalized and then mapped to the range specified by `edge.width.range`, #' unless `edge.width.range` is `NULL`. #' For `nplot.igraph` and `nplot.network`, defaults to the "weight" edge -#' attribute if present; otherwise all edges use width 1. +#' attribute if present; otherwise all edges use width 1. Can also be a one-sided +#' formula (e.g. `~ weight`) naming a numeric edge attribute (see "Mapping +#' attributes with formulas"). #' @param edge.width.range Numeric vector of length 2, or `NULL`. The minimum and maximum line #' widths (in points) to use when mapping `edge.width` values. For example, #' `c(1, 4)` maps the smallest edge weight to 1pt and the largest to 4pt. @@ -82,7 +106,33 @@ edge_color_mixer <- function(i, j, vcols, p = .5, alpha = .15) { #' @details #' When `x` is of class [matrix], it will be passed to [igraph::graph_from_adjacency_matrix()]. #' -#' In the case of `edge.color`, the user can specify colors using [netplot-formulae]. +#' @section Mapping attributes with formulas: +#' +#' Several aesthetics can be mapped directly from graph attributes by passing a +#' one-sided formula naming the attribute, instead of building the vector by +#' hand. The mapping depends on the aesthetic: +#' +#' - `vertex.color = ~ attr` colors vertices by the vertex attribute `attr`. +#' Character/factor attributes are mapped to a categorical palette, numeric +#' attributes to a continuous gradient, and logical attributes to two colors. +#' When used this way, `print()`-ing the resulting plot also draws a matching +#' legend: a categorical key for discrete attributes and a continuous color +#' bar for continuous ones. +#' - `vertex.nsides = ~ attr` maps each unique value of `attr` to a distinct +#' vertex shape (triangle, square, pentagon, ...). +#' - `vertex.size = ~ attr` and `edge.width = ~ attr` scale sizes/widths from a +#' numeric vertex/edge attribute. +#' - `edge.color` uses a different, richer formula grammar based on `ego()` and +#' `alter()` to mix the endpoints' colors; see [netplot-formulae]. +#' +#' For `vertex.nsides`, `vertex.size`, and `edge.width` the right-hand side of +#' the formula is *evaluated* with the graph's attributes in scope, so besides +#' bare names you can use expressions, e.g. `edge.width = ~ log1p(weight)` or +#' `vertex.size = ~ degree ^ 2`. +#' +#' For example, `nplot(x, vertex.color = ~ gender, vertex.size = ~ degree)` +#' colors vertices by the `gender` attribute and sizes them by `degree`. The +#' same attribute-mapping formulas also work in [set_vertex_gpar()]. #' @return An object of class `c("netplot", "gTree", "grob", "gDesc")`. The object #' has an additional set of attributes: #' * `.xlim, .ylim` vector of size two with the x-asis/y-axis limits. @@ -97,6 +147,16 @@ edge_color_mixer <- function(i, j, vcols, p = .5, alpha = .15) { #' #' plot(x) # ala igraph #' nplot(x) # ala netplot +#' +#' # Mapping aesthetics from vertex attributes using formulas +#' V(x)$grp <- sample(letters[1:3], vcount(x), replace = TRUE) +#' V(x)$deg <- degree(x) +#' nplot( +#' x, +#' vertex.color = ~ grp, # color by the categorical attribute +#' vertex.nsides = ~ grp, # and give each group a distinct shape +#' vertex.size = ~ deg # size by a numeric attribute +#' ) #' @name nplot #' @aliases netplot NULL @@ -508,12 +568,12 @@ nplot.default <- function( # Mapping attributes --------------------------------------------------------- - # Nsides + # Nsides. The formula RHS is evaluated against the graph's vertex attributes, + # so both bare names (~ group) and expressions (~ cut(age, 3)) work. if (length(vertex.nsides) && inherits(vertex.nsides, "formula")) { - rhs <- as.character(vertex.nsides[[2]]) vertex.nsides <- map_attribute_to_shape( - get_vertex_attribute(graph = x, attribute = rhs) + eval_attribute_formula(graph = x, formula = vertex.nsides, type = "vertex") ) } @@ -521,8 +581,9 @@ nplot.default <- function( # And size if (length(vertex.size) && inherits(vertex.size, "formula")) { - rhs <- as.character(vertex.size[[2]]) - vertex.size <- get_vertex_attribute(graph = x, attribute = rhs) + vertex.size <- eval_attribute_formula( + graph = x, formula = vertex.size, type = "vertex" + ) # Now check if it is numeric. If not, it should return an error if (!is.numeric(vertex.size)) { @@ -534,8 +595,9 @@ nplot.default <- function( # Edges width if (length(edge.width) && inherits(edge.width, "formula")) { - rhs <- as.character(edge.width[[2]]) - edge.width <- get_edge_attribute(graph = x, attribute = rhs) + edge.width <- eval_attribute_formula( + graph = x, formula = edge.width, type = "edge" + ) # Now check if it is numeric. If not, it should return an error if (!is.numeric(edge.width)) { diff --git a/README.md b/README.md index 6c23d7f..ad030ce 100644 --- a/README.md +++ b/README.md @@ -3,28 +3,64 @@ [![CRAN -status](https://www.r-pkg.org/badges/version/netplot)](https://cran.r-project.org/package=netplot) -[![CRAN](https://cranlogs.r-pkg.org/badges/netplot)](https://cran.r-project.org/package=netplot) -[![Downloads](https://cranlogs.r-pkg.org/badges/grand-total/netplot)](https://cran.r-project.org/package=netplot) +status](https://www.r-pkg.org/badges/version/netplot.png)](https://cran.r-project.org/package=netplot) +[![CRAN](https://cranlogs.r-pkg.org/badges/netplot.png)](https://cran.r-project.org/package=netplot) +[![Downloads](https://cranlogs.r-pkg.org/badges/grand-total/rgexf.png)](https://cran.r-project.org/package=rgexf) [![R](https://github.com/USCCANA/netplot/actions/workflows/ci.yml/badge.svg)](https://github.com/USCCANA/netplot/actions/workflows/ci.yml) +[![Build +status](https://ci.appveyor.com/api/projects/status/3k2m3oq6o99qcs0r?svg=true.png)](https://ci.appveyor.com/project/gvegayon/netplot) [![USC’s Department of Preventive Medicine](https://raw.githubusercontent.com/USCbiostats/badges/master/tommy-uscprevmed-badge.svg)](https://preventivemedicine.usc.edu) -# netplot - -An alternative graph visualization tool that emphasizes aesthetics, -providing default parameters that deliver out-of-the-box lovely -visualizations. - -Some features: - -1. Auto-scaling of vertices using sizes relative to the plotting +# netplot rgexf hex sticker logo + +**netplot** is a graph visualization engine for R that emphasizes +*aesthetics*. Its defaults are chosen so that a single call to `nplot()` +produces a publication-quality figure out of the box, while still giving +you fine-grained control when you need it. It works directly with +`igraph`, `network`, and adjacency-matrix objects. + +## Why netplot? + +Compared with the base `plot()` methods in `igraph` and `sna`/`network`, +netplot aims to make the *common case beautiful* and the *hard case +possible*: + +- **Beautiful defaults.** Vertices, edges, arrows, and labels are + auto-scaled *relative to the plotting device*, so figures look right + regardless of size or aspect ratio and fill the plotting area + instead of floating in whitespace. +- **Map data to aesthetics with formulas.** Color, shape, and size + vertices (and scale edge widths) straight from graph attributes: + `nplot(g, vertex.color = ~ group, vertex.nsides = ~ group, vertex.size = ~ degree)`. + Categorical, numeric, and logical attributes are each handled + sensibly, and a legend is added automatically. See + `vignette("formulas")`. +- **Smart edges.** True curved edges with user-defined curvature, an + embedded edge-color mixer that blends each edge between its + endpoints’ colors, and edge-width/arrow scaling that respects the + layout. +- **Built on `grid`.** Because netplot draws with the `grid` system + (the same engine as `ggplot2`), plots are first-class grid objects: + you can post-edit them with `set_vertex_gpar()` / `set_edge_gpar()`, + arrange several with `gridExtra::grid.arrange()`, add gradients, and + export cleanly. +- **Lightweight.** Following the “tinyverse” philosophy, netplot leans + on base R graphics facilities and keeps its dependency footprint + small. + +A quick feature checklist: + +1. Auto-scaling of vertices, edges, and labels relative to the plotting device. -2. Embedded edge color mixer. -3. True curved edges drawing. -4. User-defined edge curvature. -5. Nicer vertex frame color. -6. Better use of space-filling the plotting device. +2. Formula interface to map colors, shapes, and sizes from graph + attributes. +3. Embedded edge color mixer (blends edges between endpoint colors). +4. True curved edges with user-defined curvature. +5. Nicer vertex frame colors and vertex shapes. +6. Automatic legends and color keys. +7. Gradient fills for vertices and edges. +8. Better use of space, filling the plotting device. The package uses the `grid` plotting system (just like `ggplot2`). @@ -69,8 +105,8 @@ set.seed(1) data("UKfaculty", package = "igraphdata") l <- layout_with_fr(UKfaculty) #> This graph was created by an old(er) igraph version. -#> Call upgrade_graph() on it to use with the current igraph version -#> For now we convert it on the fly... +#> ℹ Call `igraph::upgrade_graph()` on it to use with the current igraph version. +#> For now we convert it on the fly... plot(UKfaculty, layout = l) # ala igraph ``` @@ -142,8 +178,8 @@ data(USairports, package="igraphdata") # Generating a layout naively layout <- V(USairports)$Position #> This graph was created by an old(er) igraph version. -#> Call upgrade_graph() on it to use with the current igraph version -#> For now we convert it on the fly... +#> ℹ Call `igraph::upgrade_graph()` on it to use with the current igraph version. +#> For now we convert it on the fly... layout <- do.call(rbind, lapply(layout, function(x) strsplit(x, " ")[[1]])) layout[] <- stringr::str_remove(layout, "^[a-zA-Z]+") layout <- matrix(as.numeric(layout[]), ncol=2) diff --git a/README.qmd b/README.qmd index 82e6d95..e5429a4 100644 --- a/README.qmd +++ b/README.qmd @@ -21,19 +21,48 @@ status](https://www.r-pkg.org/badges/version/netplot)](https://cran.r-project.or [![Build status](https://ci.appveyor.com/api/projects/status/3k2m3oq6o99qcs0r?svg=true)](https://ci.appveyor.com/project/gvegayon/netplot) [![USC's Department of Preventive Medicine](https://raw.githubusercontent.com/USCbiostats/badges/master/tommy-uscprevmed-badge.svg)](https://preventivemedicine.usc.edu) -# netplot - - -An alternative graph visualization tool that emphasizes aesthetics, providing default parameters that deliver out-of-the-box lovely visualizations. - -Some features: - -1. Auto-scaling of vertices using sizes relative to the plotting device. -2. Embedded edge color mixer. -3. True curved edges drawing. -4. User-defined edge curvature. -5. Nicer vertex frame color. -6. Better use of space-filling the plotting device. +# netplot rgexf hex sticker logo + + +**netplot** is a graph visualization engine for R that emphasizes *aesthetics*. +Its defaults are chosen so that a single call to `nplot()` produces a +publication-quality figure out of the box, while still giving you fine-grained +control when you need it. It works directly with `igraph`, `network`, and +adjacency-matrix objects. + +## Why netplot? + +Compared with the base `plot()` methods in `igraph` and `sna`/`network`, +netplot aims to make the *common case beautiful* and the *hard case possible*: + +- **Beautiful defaults.** Vertices, edges, arrows, and labels are auto-scaled + *relative to the plotting device*, so figures look right regardless of size or + aspect ratio and fill the plotting area instead of floating in whitespace. +- **Map data to aesthetics with formulas.** Color, shape, and size vertices (and + scale edge widths) straight from graph attributes: `nplot(g, vertex.color = ~ + group, vertex.nsides = ~ group, vertex.size = ~ degree)`. Categorical, + numeric, and logical attributes are each handled sensibly, and a legend is + added automatically. See `vignette("formulas")`. +- **Smart edges.** True curved edges with user-defined curvature, an embedded + edge-color mixer that blends each edge between its endpoints' colors, and + edge-width/arrow scaling that respects the layout. +- **Built on `grid`.** Because netplot draws with the `grid` system (the same + engine as `ggplot2`), plots are first-class grid objects: you can post-edit + them with `set_vertex_gpar()` / `set_edge_gpar()`, arrange several with + `gridExtra::grid.arrange()`, add gradients, and export cleanly. +- **Lightweight.** Following the "tinyverse" philosophy, netplot leans on base R + graphics facilities and keeps its dependency footprint small. + +A quick feature checklist: + +1. Auto-scaling of vertices, edges, and labels relative to the plotting device. +2. Formula interface to map colors, shapes, and sizes from graph attributes. +3. Embedded edge color mixer (blends edges between endpoint colors). +4. True curved edges with user-defined curvature. +5. Nicer vertex frame colors and vertex shapes. +6. Automatic legends and color keys. +7. Gradient fills for vertices and edges. +8. Better use of space, filling the plotting device. The package uses the `grid` plotting system (just like `ggplot2`). diff --git a/_pkgdown.yml b/_pkgdown.yml new file mode 100644 index 0000000..e19b093 --- /dev/null +++ b/_pkgdown.yml @@ -0,0 +1,53 @@ +url: https://usccana.github.io/netplot/ + +template: + bootstrap: 5 + +reference: +- title: "Drawing networks" + desc: > + The main function of the package. `nplot()` draws a network using the grid + graphics system and works with igraph, network, and matrix objects. + contents: + - nplot + +- title: "Mapping attributes with formulas" + desc: > + Map vertex and edge aesthetics (colors, shapes, sizes, widths) directly from + graph attributes. Edge colors use the richer `ego()`/`alter()` grammar. + contents: + - netplot-formulae + +- title: "Editing and querying a plot" + desc: > + Retrieve and modify the graphical parameters of an existing `netplot` + object, or interactively locate a vertex. + contents: + - set_gpar + - locate_vertex + +- title: "Legends and color keys" + desc: "Annotate a plot with legends and color keys." + contents: + - nplot_legend + - colorkey + +- title: "Colors" + desc: "Helpers for building color scales and gradients." + contents: + - colorRamp2 + - make_colors + - segments_gradient + +- title: "Low-level drawing primitives" + desc: > + Building blocks used internally by `nplot()`, exposed for advanced grid + graphics work. + contents: + - npolygon + - piechart + +- title: "Base-graphics backend" + desc: "An experimental base-graphics implementation of nplot." + contents: + - nplot_base diff --git a/inst/tinytest/test_netplot.R b/inst/tinytest/test_netplot.R index 45dc517..30326fb 100644 --- a/inst/tinytest/test_netplot.R +++ b/inst/tinytest/test_netplot.R @@ -179,6 +179,55 @@ if (requireNamespace("igraph", quietly = TRUE)) { info = "nplot should keep edge.line.lty aligned when sample.edges drops edges" ) + # Attribute formulas ------------------------------------------------------- + x_fm <- igraph::make_ring(5, directed = FALSE) + igraph::E(x_fm)$w <- c(1, 2, 3, 4, 5) + igraph::V(x_fm)$deg <- igraph::degree(x_fm) + l_fm <- igraph::layout_in_circle(x_fm) + + grDevices::pdf(tempfile(fileext = ".pdf")) + + # Bare attribute name and an equivalent explicit vector should match + g_fm_name <- nplot(x_fm, layout = l_fm, skip.arrows = TRUE, + edge.width = ~ w, edge.width.range = c(1, 5)) + g_fm_vec <- nplot(x_fm, layout = l_fm, skip.arrows = TRUE, + edge.width = igraph::E(x_fm)$w, edge.width.range = c(1, 5)) + expect_equal( + get_edge_gpar(g_fm_name, element = "line", "lwd")$lwd, + get_edge_gpar(g_fm_vec, element = "line", "lwd")$lwd, + info = "edge.width formula ~ w should match the explicit weight vector" + ) + + # Expression on the RHS must be evaluated (regression: used to error with + # 'the condition has length > 1') + g_fm_expr <- nplot(x_fm, layout = l_fm, skip.arrows = TRUE, + edge.width = ~ w * 2, edge.width.range = c(1, 5)) + expect_equal( + get_edge_gpar(g_fm_expr, element = "line", "lwd")$lwd, + get_edge_gpar(g_fm_name, element = "line", "lwd")$lwd, + info = "edge.width = ~ w * 2 should be evaluated (monotone rescaling unchanged)" + ) + + # Formulas also work for vertex.size and vertex.nsides + g_fm_vertex <- tryCatch( + nplot(x_fm, layout = l_fm, skip.arrows = TRUE, + vertex.size = ~ log(deg + 1), vertex.nsides = ~ deg), + error = function(e) e + ) + expect_false( + inherits(g_fm_vertex, "error"), + info = "vertex.size/vertex.nsides expression formulas should evaluate" + ) + + # A helpful error when the referenced attribute does not exist + expect_error( + nplot(x_fm, layout = l_fm, skip.arrows = TRUE, edge.width = ~ nope), + "does not|exist|evaluate", + info = "missing attribute in an edge.width formula should raise a clear error" + ) + + grDevices::dev.off() + grDevices::pdf(tempfile(fileext = ".pdf")) set.seed(1) base_sample_lty <- tryCatch( diff --git a/man/figures/README-example-1.png b/man/figures/README-example-1.png index e36b935..a7eb35c 100644 Binary files a/man/figures/README-example-1.png and b/man/figures/README-example-1.png differ diff --git a/man/figures/README-example-2.png b/man/figures/README-example-2.png index d345577..1551ab8 100644 Binary files a/man/figures/README-example-2.png and b/man/figures/README-example-2.png differ diff --git a/man/figures/README-example-3.png b/man/figures/README-example-3.png index 3b859f6..d3f489c 100644 Binary files a/man/figures/README-example-3.png and b/man/figures/README-example-3.png differ diff --git a/man/figures/README-fig-uk-faculty-1.png b/man/figures/README-fig-uk-faculty-1.png index 7d47972..907348c 100644 Binary files a/man/figures/README-fig-uk-faculty-1.png and b/man/figures/README-fig-uk-faculty-1.png differ diff --git a/man/figures/README-fig-uk-faculty-gradient-1.png b/man/figures/README-fig-uk-faculty-gradient-1.png index 758b89b..ee7a401 100644 Binary files a/man/figures/README-fig-uk-faculty-gradient-1.png and b/man/figures/README-fig-uk-faculty-gradient-1.png differ diff --git a/man/figures/README-fig-us-airports-1.png b/man/figures/README-fig-us-airports-1.png index d6ee474..f23f9ed 100644 Binary files a/man/figures/README-fig-us-airports-1.png and b/man/figures/README-fig-us-airports-1.png differ diff --git a/man/figures/logo.png b/man/figures/logo.png new file mode 100644 index 0000000..5cb9666 Binary files /dev/null and b/man/figures/logo.png differ diff --git a/man/figures/logo.svg b/man/figures/logo.svg new file mode 100644 index 0000000..d1fb9a5 --- /dev/null +++ b/man/figures/logo.svg @@ -0,0 +1,5591 @@ + + + + diff --git a/man/netplot-formulae.Rd b/man/netplot-formulae.Rd index 74e2c2d..33b4a83 100644 --- a/man/netplot-formulae.Rd +++ b/man/netplot-formulae.Rd @@ -5,7 +5,7 @@ \alias{color_formula} \alias{ego} \alias{alter} -\title{Formulas in \code{netplot}} +\title{Edge-color formulas in \code{netplot}} \usage{ color_formula(x, col, alpha, env, type, mix = 1, postfix = NULL) @@ -36,6 +36,27 @@ a formula based on \code{ego()} and \code{alter()} (source and target). This way user can set various types of combination varying the mixing of the colors, the alpha levels, and the actual mixing colors to create edge colors. } +\details{ +The formula is one-sided and combines the special terms \code{ego()} (the source +vertex) and \code{alter()} (the target vertex) with \code{+}. Each term draws its color +from the corresponding endpoint's \code{vertex.color} unless a \code{col} is supplied, +and accepts: +\itemize{ +\item \code{col}: the base color to use for that endpoint. +\item \code{alpha}: transparency, from 0 (fully transparent) to 1 (opaque). +\item \code{mix}: how much weight that endpoint's color receives when the two are +blended along the edge. +} + +For instance, \code{~ ego(alpha = .1, col = "gray") + alter} (the default) fades +each edge from a faint gray at the source to the target's color, producing the +characteristic netplot look. + +This grammar is specific to \emph{edge colors}. To map other aesthetics (vertex +color, shape, size, or edge width) from a graph attribute, pass a formula +naming the attribute directly to \code{\link[=nplot]{nplot()}} (e.g. \code{vertex.color = ~ group}); +see the "Mapping attributes with formulas" section of \code{\link[=nplot]{nplot()}}. +} \examples{ if (require(gridExtra) & require(magrittr)) { library(igraph) @@ -55,3 +76,6 @@ if (require(gridExtra) & require(magrittr)) { ) } } +\seealso{ +\code{\link[=nplot]{nplot()}} for the attribute-mapping formulas, and \code{\link[=set_edge_gpar]{set_edge_gpar()}}. +} diff --git a/man/netplot-package.Rd b/man/netplot-package.Rd new file mode 100644 index 0000000..28f5a96 --- /dev/null +++ b/man/netplot-package.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/netplot-package.R +\docType{package} +\name{netplot-package} +\alias{netplot-package} +\title{netplot: Beautiful Graph Drawing} +\description{ +netplot is a graph visualization engine that emphasizes aesthetics while +providing default parameters that yield out-of-the-box nice visualizations. +It is built on top of the \strong{grid} graphics system (the same engine behind +\strong{ggplot2}) and works seamlessly with \code{igraph} and \code{network} objects. +} +\details{ +The main entry point is \code{\link[=nplot]{nplot()}}. See \code{vignette("examples", package = "netplot")} for a general overview and \code{vignette("formulas", package = "netplot")} for mapping vertex/edge aesthetics from graph attributes. +} +\seealso{ +Useful links: +\itemize{ + \item \url{https://github.com/USCCANA/netplot} + \item \url{https://usccana.github.io/netplot/} + \item Report bugs at \url{https://github.com/USCCANA/netplot/issues} +} + +} +\author{ +\strong{Maintainer}: George Vega Yon \email{g.vegayon@gmail.com} (\href{https://orcid.org/0000-0002-3171-0844}{ORCID}) + +Authors: +\itemize{ + \item George Vega Yon \email{g.vegayon@gmail.com} (\href{https://orcid.org/0000-0002-3171-0844}{ORCID}) + \item Porter Bischoff \email{portergbischoff@gmail.com} (\href{https://orcid.org/0009-0004-6742-6281}{ORCID}) +} + +} +\keyword{internal} diff --git a/man/nplot.Rd b/man/nplot.Rd index 4f75752..9055b9b 100644 --- a/man/nplot.Rd +++ b/man/nplot.Rd @@ -189,14 +189,22 @@ matrices objects (see details).} \item{layout}{Numeric two-column matrix with the graph layout in x/y positions of the vertices.} -\item{vertex.size}{Numeric vector of length \code{vcount(x)}. Absolute size of the vertex from 0 to 1.} +\item{vertex.size}{Numeric vector of length \code{vcount(x)}. Absolute size of the +vertex from 0 to 1. Can also be a one-sided formula (e.g. \code{~ degree}) naming a +numeric vertex attribute to map sizes from (see "Mapping attributes with +formulas").} \item{bg.col}{Color of the background.} -\item{vertex.nsides}{Numeric vector of length \code{vcount(x)}. Number of sizes of -the vertex. E.g. three is a triangle, and 100 approximates a circle.} +\item{vertex.nsides}{Numeric vector of length \code{vcount(x)}. Number of sides of +the vertex. E.g. three is a triangle, and 100 approximates a circle. Can also +be a one-sided formula (e.g. \code{~ group}) naming a vertex attribute; each unique +value is then mapped to a distinct shape (see "Mapping attributes with +formulas").} -\item{vertex.color}{Vector of length \code{vcount(x)}. Vertex HEX or built in colors.} +\item{vertex.color}{Vector of length \code{vcount(x)}. Vertex HEX or built in +colors. Can also be a one-sided formula (e.g. \code{~ group}) naming a vertex +attribute to color vertices by (see "Mapping attributes with formulas").} \item{vertex.size.range}{Numeric vector of length 2 or 3, or \code{NULL}. Relative size for the minimum and maximum of the plot, and curvature of the scale. The third number @@ -232,7 +240,9 @@ top ranking according to \code{vertex.size}.} Values are normalized and then mapped to the range specified by \code{edge.width.range}, unless \code{edge.width.range} is \code{NULL}. For \code{nplot.igraph} and \code{nplot.network}, defaults to the "weight" edge -attribute if present; otherwise all edges use width 1.} +attribute if present; otherwise all edges use width 1. Can also be a one-sided +formula (e.g. \code{~ weight}) naming a numeric edge attribute (see "Mapping +attributes with formulas").} \item{edge.width.range}{Numeric vector of length 2, or \code{NULL}. The minimum and maximum line widths (in points) to use when mapping \code{edge.width} values. For example, @@ -282,7 +292,7 @@ has an additional set of attributes: } In the case of \code{nplot.default}, an object of class \code{netplot} and \code{grob} (see -\link[grid:grid.grob]{grid::grob}) with the following slots: +\link[grid:grob]{grid::grob}) with the following slots: \itemize{ \item \code{children} The main \code{grob} of the object. \item \code{name} Character scalar. The name of the plot @@ -301,13 +311,56 @@ of the figure. } } \description{ -This is a description. +\code{nplot()} is the main function of the \strong{netplot} package. It draws a network +using the \strong{grid} graphics system (the same engine that powers \strong{ggplot2}), +emphasizing aesthetics and providing sensible defaults that yield +out-of-the-box nice visualizations. Compared with base \code{igraph}/\code{network} +plots, \code{nplot()} auto-scales vertices and edges relative to the plotting +device, draws truly curved edges, mixes edge colors from their endpoints, +and fills the device efficiently. } \details{ +Vertex and edge aesthetics can be set directly (passing a vector) or, +conveniently, mapped from graph attributes using a \emph{formula} interface (see +the "Mapping attributes with formulas" section below). The returned object is +a \strong{grid} \code{grob}, so it can be further edited with \code{\link[=set_vertex_gpar]{set_vertex_gpar()}} / +\code{\link[=set_edge_gpar]{set_edge_gpar()}}, combined with other grid graphics (e.g. via +\code{gridExtra::grid.arrange()}), or annotated with a legend through +\code{\link[=nplot_legend]{nplot_legend()}}. + When \code{x} is of class \link{matrix}, it will be passed to \code{\link[igraph:graph_from_adjacency_matrix]{igraph::graph_from_adjacency_matrix()}}. +} +\section{Mapping attributes with formulas}{ + + +Several aesthetics can be mapped directly from graph attributes by passing a +one-sided formula naming the attribute, instead of building the vector by +hand. The mapping depends on the aesthetic: +\itemize{ +\item \code{vertex.color = ~ attr} colors vertices by the vertex attribute \code{attr}. +Character/factor attributes are mapped to a categorical palette, numeric +attributes to a continuous gradient, and logical attributes to two colors. +When used this way, \code{print()}-ing the resulting plot also draws a matching +legend: a categorical key for discrete attributes and a continuous color +bar for continuous ones. +\item \code{vertex.nsides = ~ attr} maps each unique value of \code{attr} to a distinct +vertex shape (triangle, square, pentagon, ...). +\item \code{vertex.size = ~ attr} and \code{edge.width = ~ attr} scale sizes/widths from a +numeric vertex/edge attribute. +\item \code{edge.color} uses a different, richer formula grammar based on \code{ego()} and +\code{alter()} to mix the endpoints' colors; see \link{netplot-formulae}. +} -In the case of \code{edge.color}, the user can specify colors using \link{netplot-formulae}. +For \code{vertex.nsides}, \code{vertex.size}, and \code{edge.width} the right-hand side of +the formula is \emph{evaluated} with the graph's attributes in scope, so besides +bare names you can use expressions, e.g. \code{edge.width = ~ log1p(weight)} or +\code{vertex.size = ~ degree ^ 2}. + +For example, \code{nplot(x, vertex.color = ~ gender, vertex.size = ~ degree)} +colors vertices by the \code{gender} attribute and sizes them by \code{degree}. The +same attribute-mapping formulas also work in \code{\link[=set_vertex_gpar]{set_vertex_gpar()}}. } + \examples{ library(igraph) library(netplot) @@ -316,6 +369,16 @@ x <- sample_smallworld(1, 200, 5, 0.03) plot(x) # ala igraph nplot(x) # ala netplot + +# Mapping aesthetics from vertex attributes using formulas +V(x)$grp <- sample(letters[1:3], vcount(x), replace = TRUE) +V(x)$deg <- degree(x) +nplot( + x, + vertex.color = ~ grp, # color by the categorical attribute + vertex.nsides = ~ grp, # and give each group a distinct shape + vertex.size = ~ deg # size by a numeric attribute +) } \seealso{ \link{nplot_base} diff --git a/man/nplot_base.Rd b/man/nplot_base.Rd index 511484d..41cfa71 100644 --- a/man/nplot_base.Rd +++ b/man/nplot_base.Rd @@ -39,14 +39,22 @@ matrices objects (see details).} \item{layout}{Numeric two-column matrix with the graph layout in x/y positions of the vertices.} -\item{vertex.size}{Numeric vector of length \code{vcount(x)}. Absolute size of the vertex from 0 to 1.} +\item{vertex.size}{Numeric vector of length \code{vcount(x)}. Absolute size of the +vertex from 0 to 1. Can also be a one-sided formula (e.g. \code{~ degree}) naming a +numeric vertex attribute to map sizes from (see "Mapping attributes with +formulas").} \item{bg.col}{Color of the background.} -\item{vertex.nsides}{Numeric vector of length \code{vcount(x)}. Number of sizes of -the vertex. E.g. three is a triangle, and 100 approximates a circle.} +\item{vertex.nsides}{Numeric vector of length \code{vcount(x)}. Number of sides of +the vertex. E.g. three is a triangle, and 100 approximates a circle. Can also +be a one-sided formula (e.g. \code{~ group}) naming a vertex attribute; each unique +value is then mapped to a distinct shape (see "Mapping attributes with +formulas").} -\item{vertex.color}{Vector of length \code{vcount(x)}. Vertex HEX or built in colors.} +\item{vertex.color}{Vector of length \code{vcount(x)}. Vertex HEX or built in +colors. Can also be a one-sided formula (e.g. \code{~ group}) naming a vertex +attribute to color vertices by (see "Mapping attributes with formulas").} \item{vertex.size.range}{Numeric vector of length 2 or 3, or \code{NULL}. Relative size for the minimum and maximum of the plot, and curvature of the scale. The third number @@ -66,7 +74,9 @@ vertex does the frame occupy (values between 0 and 1).} Values are normalized and then mapped to the range specified by \code{edge.width.range}, unless \code{edge.width.range} is \code{NULL}. For \code{nplot.igraph} and \code{nplot.network}, defaults to the "weight" edge -attribute if present; otherwise all edges use width 1.} +attribute if present; otherwise all edges use width 1. Can also be a one-sided +formula (e.g. \code{~ weight}) naming a numeric edge attribute (see "Mapping +attributes with formulas").} \item{edge.width.range}{Numeric vector of length 2, or \code{NULL}. The minimum and maximum line widths (in points) to use when mapping \code{edge.width} values. For example, diff --git a/man/nplot_legend.Rd b/man/nplot_legend.Rd index c311958..7d4633c 100644 --- a/man/nplot_legend.Rd +++ b/man/nplot_legend.Rd @@ -27,7 +27,7 @@ nplot_legend( \item{...}{Further arguments passed to \code{\link[grid:legendGrob]{grid::legendGrob()}}.} -\item{packgrob.args}{List of arguments passed to \code{\link[grid:grid.pack]{grid::packGrob()}}.} +\item{packgrob.args}{List of arguments passed to \code{\link[grid:packGrob]{grid::packGrob()}}.} \item{x}{An object of class \code{netplot_legend}.} diff --git a/man/set_gpar.Rd b/man/set_gpar.Rd index 1f20147..39bb3ca 100644 --- a/man/set_gpar.Rd +++ b/man/set_gpar.Rd @@ -32,7 +32,7 @@ get_gpar(x, type, element, ..., idx, simplify = TRUE) \item{idx}{(optional) Integer vector. Indices of the elements to be modified. When missing, all elements are modified.} -\item{...}{Parameters to be modified/retrieved. This is passed to \link[grid:grid.edit]{grid::editGrob} +\item{...}{Parameters to be modified/retrieved. This is passed to \link[grid:editGrob]{grid::editGrob} via \link[grid:gpar]{grid::gpar}.} \item{simplify}{Logical. When \code{TRUE} it tries to simplify the result. diff --git a/vignettes/base-and-grid.Rmd b/vignettes/base-and-grid.Rmd index ff073e9..0dd76ec 100644 --- a/vignettes/base-and-grid.Rmd +++ b/vignettes/base-and-grid.Rmd @@ -4,7 +4,7 @@ author: "George G. Vega Yon" date: 2023-05-22 output: rmarkdown::html_vignette vignette: > - %\VignetteIndexEntry{Example with baseplots} + %\VignetteIndexEntry{Mixing netplot with base graphics} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- diff --git a/vignettes/formulas.Rmd b/vignettes/formulas.Rmd new file mode 100644 index 0000000..7244507 --- /dev/null +++ b/vignettes/formulas.Rmd @@ -0,0 +1,214 @@ +--- +title: "Mapping aesthetics with formulas" +author: "George G. Vega Yon" +date: "`r Sys.Date()`" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Mapping aesthetics with formulas} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r setup, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + fig.width = 6, + fig.height = 6, + warning = FALSE, + message = FALSE +) +``` + +One of the most convenient features of `nplot()` is that many aesthetics can be +mapped **directly from graph attributes** using a one-sided formula, instead of +building the vector of colors, shapes, or sizes by hand. This vignette walks +through the different formula interfaces available in **netplot**. + +At a glance, `nplot()` understands two flavors of formula: + +| Aesthetic | Formula | What it does | +|-----------------|------------------------|---------------------------------------------------------------------| +| `vertex.color` | `~ attr` | Colors vertices by a vertex attribute (categorical, numeric, logical). | +| `vertex.nsides` | `~ attr` | Maps each unique value of a vertex attribute to a distinct shape. | +| `vertex.size` | `~ attr` | Scales vertex sizes from a numeric vertex attribute. | +| `edge.width` | `~ attr` | Scales edge widths from a numeric edge attribute. | +| `edge.color` | `~ ego(...) + alter(...)` | Blends edge colors from the two endpoints (see the last section).| + +```{r pkgs} +library(netplot) +library(igraph) +``` + +# A working example + +We will use the `UKfaculty` network from the **igraphdata** package, a +friendship network among faculty at a UK university. It already carries a +`Group` vertex attribute (the school/department each person belongs to). + +```{r data} +data("UKfaculty", package = "igraphdata") + +set.seed(225) +l <- layout_with_fr(UKfaculty) + +# A couple of extra attributes to play with. We qualify igraph::degree() +# explicitly because other packages (e.g. sna) also define a degree(). +V(UKfaculty)$indeg <- igraph::degree(UKfaculty, mode = "in") +V(UKfaculty)$is_hub <- V(UKfaculty)$indeg > stats::median(V(UKfaculty)$indeg) +``` + +# Coloring vertices: `vertex.color = ~ attr` + +Passing `vertex.color = ~ Group` colors each vertex according to its `Group` +attribute. netplot detects the type of the attribute and picks a sensible +scale automatically: + +* **character / factor** attributes are mapped to a categorical palette, +* **numeric** attributes to a continuous gradient, and +* **logical** attributes to two contrasting colors. + +When you *print* a plot built this way, netplot also draws a matching legend. + +```{r color-factor, fig.cap="Vertices colored by the categorical `Group` attribute, with an automatic legend."} +nplot(UKfaculty, layout = l, vertex.color = ~ Group) +``` + +The same syntax works for a **numeric** attribute, in which case a continuous +color gradient is used. netplot detects that the attribute is continuous and +draws a **color bar** (rather than a set of discrete keys) as the legend: + +```{r color-numeric, fig.cap="Vertices colored by in-degree (a numeric attribute) using a continuous gradient, with a color-bar legend."} +nplot(UKfaculty, layout = l, vertex.color = ~ indeg) +``` + +And for a **logical** attribute, mapping the two values to two colors: + +```{r color-logical, fig.cap="Vertices colored by a logical attribute (hub vs. non-hub)."} +nplot(UKfaculty, layout = l, vertex.color = ~ is_hub) +``` + +# Shaping vertices: `vertex.nsides = ~ attr` + +`vertex.nsides` controls the number of sides of each vertex polygon (3 = a +triangle, 4 = a square, and larger numbers approximate a circle). Passing a +formula maps every unique value of the attribute to a **distinct shape**, which +is handy for encoding a second categorical variable alongside color: + +```{r shape, fig.cap="Vertex color *and* shape mapped from the same `Group` attribute."} +nplot( + UKfaculty, + layout = l, + vertex.color = ~ Group, + vertex.nsides = ~ Group +) +``` + +Because each group gets both a color and a shape, the figure stays readable even +when printed in grayscale. + +# Sizing vertices: `vertex.size = ~ attr` + +`vertex.size` accepts a formula naming a **numeric** vertex attribute. Values +are rescaled to the range given by `vertex.size.range`, so more central actors +show up as larger nodes: + +```{r size, fig.cap="Vertex size mapped from in-degree."} +nplot( + UKfaculty, + layout = l, + vertex.size = ~ indeg, + vertex.size.range = c(.01, .04, 4) +) +``` + +# Putting it together + +The formula interfaces compose, so a single `nplot()` call can encode several +variables at once — color, shape, and size — with very little code: + +```{r combined, fig.cap="Color, shape, and size all mapped from graph attributes in a single call."} +nplot( + UKfaculty, + layout = l, + vertex.color = ~ Group, # color by department + vertex.nsides = ~ Group, # distinct shape per department + vertex.size = ~ indeg, # size by popularity (in-degree) + vertex.size.range = c(.01, .04, 4) +) +``` + +# Scaling edges: `edge.width = ~ attr` + +Edges have their own numeric attributes too. `edge.width = ~ attr` scales edge +widths from a numeric **edge** attribute; the values are normalized and mapped +to `edge.width.range`. Here we use the friendship `weight`: + +```{r edge-width, fig.cap="Edge width mapped from the numeric `weight` edge attribute."} +nplot( + UKfaculty, + layout = l, + edge.width = ~ weight, + edge.width.range = c(1, 4, 4), + skip.arrows = TRUE +) +``` + +For `vertex.nsides`, `vertex.size`, and `edge.width`, the right-hand side of the +formula is *evaluated* with the graph's attributes in scope, so you are not +limited to bare attribute names — expressions work too, e.g. +`edge.width = ~ log1p(weight)` or `vertex.size = ~ degree ^ 2`. + +# Coloring edges: `edge.color = ~ ego(...) + alter(...)` + +Edge *colors* use a richer, dedicated grammar built from two special terms: + +* `ego()` — the **source** vertex of the edge, and +* `alter()` — the **target** vertex. + +Each term borrows its color from the corresponding endpoint's `vertex.color` +(unless you pass an explicit `col`), and each accepts three tweaks: + +* `col` — the base color, +* `alpha` — transparency, from 0 (transparent) to 1 (opaque), and +* `mix` — how much weight that endpoint contributes when the two colors are + blended along the edge. + +The default, `~ ego(alpha = .1, col = "gray") + alter`, fades each edge from a +faint gray at the source to the target's color. The panels below vary `mix` to +shift the blend from *alter only* to *ego only*: + +```{r edge-color, fig.width=7, fig.height=3, fig.cap="Varying the `mix` between `ego` and `alter` in the edge color formula. Left: alter only. Middle: an even blend. Right: ego only."} +gridExtra::grid.arrange( + nplot(UKfaculty, layout = l, vertex.color = ~ Group, + edge.color = ~ ego(mix = 0, alpha = .1) + alter(mix = 1)), + nplot(UKfaculty, layout = l, vertex.color = ~ Group, + edge.color = ~ ego(mix = .5, alpha = .1) + alter(mix = .5)), + nplot(UKfaculty, layout = l, vertex.color = ~ Group, + edge.color = ~ ego(mix = 1, alpha = .1) + alter(mix = 0)), + ncol = 3 +) +``` + +# Applying formulas after the fact + +The attribute-mapping formulas for vertex color also work with +`set_vertex_gpar()`, so you can recolor an existing plot without rebuilding it: + +```{r set-gpar, fig.cap="Recoloring an existing plot by attribute using `set_vertex_gpar()`."} +np <- nplot(UKfaculty, layout = l) + +set_vertex_gpar(np, element = "core", fill = ~ Group) +``` + +# Summary + +* Use `vertex.color = ~ attr` to color vertices by an attribute (with an + automatic legend), `vertex.nsides = ~ attr` to give each category a shape, and + `vertex.size = ~ attr` / `edge.width = ~ attr` to scale sizes and widths from + numeric attributes. +* Use `edge.color = ~ ego(...) + alter(...)` to blend edge colors from their + endpoints; see `?\`netplot-formulae\`` for the full grammar. +* All of these compose, letting a single `nplot()` call encode several variables + at once. +```