--- 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. ```