Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ netplot\._.*\.tar\.gz
^AGENTS\.md$
^\.devcontainer$
^\.vscode$
^_pkgdown\.yml$
^docs$
^pkgdown$
^\.claude$
^CLAUDE\.md$
figures/logo\.svg
6 changes: 3 additions & 3 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -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")),
Expand All @@ -17,7 +17,6 @@ Depends:
License: MIT + file LICENSE
Encoding: UTF-8
LazyData: true
RoxygenNote: 7.3.3
Roxygen: list(markdown = TRUE)
Imports:
graphics,
Expand All @@ -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
24 changes: 23 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand Down
78 changes: 78 additions & 0 deletions R/attribute-extraction.R
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down
111 changes: 100 additions & 11 deletions R/color_nodes_function.R
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {

Expand Down
24 changes: 23 additions & 1 deletion R/coloring.R
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
13 changes: 12 additions & 1 deletion R/netplot-package.R
Original file line number Diff line number Diff line change
@@ -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"
Loading