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 @@
[](https://cran.r-project.org/package=netplot)
-[](https://cran.r-project.org/package=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)
+[](https://cran.r-project.org/package=netplot)
+[](https://cran.r-project.org/package=rgexf)
[](https://github.com/USCCANA/netplot/actions/workflows/ci.yml)
+[](https://ci.appveyor.com/project/gvegayon/netplot)
[](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
+
+**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
[](https://ci.appveyor.com/project/gvegayon/netplot)
[](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
+
+
+**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.
+```