From 9f5689fb4107c50c934586868623177b34d78bab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 00:42:17 +0000 Subject: [PATCH 01/10] Initial plan From ae9f04674b913bed4ecabb64b3265ba7731c1777 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 01:14:50 +0000 Subject: [PATCH 02/10] Fix edge.width not applied to grob lwd; update docs and vignette example --- NEWS.md | 4 +++ R/netplot.R | 27 +++++++++++++++++--- inst/tinytest/test_netplot.R | 21 ++++++++++++++++ vignettes/examples.Rmd | 48 ++++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 4 deletions(-) diff --git a/NEWS.md b/NEWS.md index d799f92..90eeabf 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # netplot 0.3-0 +* `edge.width` now correctly reflected in the drawn line widths. Previously, + the width was computed but not consistently applied via the graphical + parameters of each edge grob. Closes #17. + * Invalid arguments passed to `nplot()` now raise an error. * Figures with legends are not drawn twice. diff --git a/R/netplot.R b/R/netplot.R index b2648c6..117eb62 100644 --- a/R/netplot.R +++ b/R/netplot.R @@ -51,10 +51,12 @@ edge_color_mixer <- function(i, j, vcols, p = .5, alpha = .15) { #' @param edge.color A vector of length `ecount(x)`. In HEX or built in colors. #' Can be `NULL` in which case #' the color is picked as a mixture between ego and alters' `vertex.color` values. -#' @param edge.width Vector of length `ecount(x)` from 0 to 1. All edges will be -#' the same size. -#' @param edge.width.range Vector of length `ecount(x)` from 0 to 1. Adjusting -#' width according to weight. +#' @param edge.width Numeric vector of length `ecount(x)`. Relative edge widths. +#' Values are normalized and then mapped to the range specified by `edge.width.range`. +#' For `nplot.igraph`, defaults to the "weight" edge attribute if present. +#' @param edge.width.range Numeric vector of length 2. 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. #' @param edge.arrow.size Vector of length `ecount(x)` from 0 to 1. #' @param edge.curvature Numeric vector of length `ecount(x)`. Curvature of edges #' in terms of radians. @@ -774,6 +776,23 @@ nplot.default <- function( } } + # Explicitly set edge line widths to ensure edge.width is applied + if (!skip.edges) { + ans <- set_edge_gpar( + x = ans, + element = "line", + lwd = as.vector(netenv$edge.width) + ) + } + + if (!skip.arrows) { + ans <- set_edge_gpar( + x = ans, + element = "arrow", + lwd = as.vector(netenv$edge.width) + ) + } + ans } diff --git a/inst/tinytest/test_netplot.R b/inst/tinytest/test_netplot.R index 2d8dcbd..27fad90 100644 --- a/inst/tinytest/test_netplot.R +++ b/inst/tinytest/test_netplot.R @@ -2,4 +2,25 @@ # Placeholder with simple test expect_equal(1 + 1, 2) +# Test that edge.width values are reflected in the line-grob lwd gpar +if (requireNamespace("igraph", quietly = TRUE)) { + set.seed(1) + x <- igraph::make_ring(4, directed = FALSE) + igraph::E(x)$weight <- c(1, 2, 3, 4) + l <- igraph::layout_in_circle(x) + + g <- nplot(x, layout = l, skip.arrows = TRUE, + edge.width = igraph::E(x)$weight, + edge.width.range = c(1, 4)) + + lwds <- get_edge_gpar(g, element = "line", "lwd")$lwd + + # All lwd values should be within the specified range + expect_true(all(lwds >= 1 & lwds <= 4), + info = "edge.width values should be mapped to edge.width.range") + + # Edges with larger weights should have larger (or equal) lwd + expect_true(all(diff(lwds) >= 0), + info = "lwd values should be non-decreasing with increasing weight") +} diff --git a/vignettes/examples.Rmd b/vignettes/examples.Rmd index 8b23f65..6a5bae2 100644 --- a/vignettes/examples.Rmd +++ b/vignettes/examples.Rmd @@ -174,6 +174,54 @@ gridExtra::grid.arrange( ) ``` +## Edge width + +The `edge.width` parameter controls the relative width of each edge. Values are +normalized and mapped to the range `[min, max]` specified by `edge.width.range` +(in points). For `nplot.igraph`, this defaults to the `"weight"` edge attribute +when present. + +```{r edge-width-setup} +set.seed(1) +x_w <- make_ring(8) + +# Assign varying edge weights +E(x_w)$weight <- c(1, 3, 1, 5, 2, 4, 1, 6) +l_w <- layout_in_circle(x_w) +``` + +```{r edge-width, fig.width=7, fig.height=3, fig.cap="Effect of `edge.width` and `edge.width.range`. Left: uniform width (default). Middle: widths reflect edge weights with the default range (1–2 pt). Right: widths reflect edge weights with a wider range (1–6 pt), making differences more visible."} +gridExtra::grid.arrange( + nplot(x_w, layout = l_w, skip.arrows = TRUE, + vertex.size.range = c(.05, .05), + edge.width = 1, + edge.width.range = c(1, 2)), + nplot(x_w, layout = l_w, skip.arrows = TRUE, + vertex.size.range = c(.05, .05), + edge.width = E(x_w)$weight, + edge.width.range = c(1, 2)), + nplot(x_w, layout = l_w, skip.arrows = TRUE, + vertex.size.range = c(.05, .05), + edge.width = E(x_w)$weight, + edge.width.range = c(1, 6)), + ncol = 3, nrow = 1 +) +``` + +You can also modify edge widths after the plot has been created using +`set_edge_gpar()`: + +```{r edge-width-set-gpar, fig.width=7, fig.height=3, fig.cap="Using `set_edge_gpar()` to change edge widths after plotting. Left: all edges set to 1 pt. Right: all edges set to 4 pt."} +g <- nplot(x_w, layout = l_w, skip.arrows = TRUE, + vertex.size.range = c(.05, .05)) + +gridExtra::grid.arrange( + set_edge_gpar(g, element = "line", lwd = 1), + set_edge_gpar(g, element = "line", lwd = 4), + ncol = 2 +) +``` + # Node labels ```{r} From 72e18bbca2dedf58e309b382c4bb20ca1d6d4b3b Mon Sep 17 00:00:00 2001 From: "George G. Vega Yon" Date: Tue, 30 Jun 2026 20:21:29 +0000 Subject: [PATCH 03/10] Adding dev container environment --- .devcontainer/.vscode/settings.json | 4 +++ .devcontainer/Containerfile | 19 +++++++++++++ .devcontainer/devcontainer-lock.json | 9 ++++++ .devcontainer/devcontainer.json | 42 ++++++++++++++++++++++++++++ Makefile | 23 +++++---------- R/netplot.R | 2 +- vignettes/examples.Rmd | 9 +----- 7 files changed, 83 insertions(+), 25 deletions(-) create mode 100644 .devcontainer/.vscode/settings.json create mode 100644 .devcontainer/Containerfile create mode 100644 .devcontainer/devcontainer-lock.json create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/.vscode/settings.json b/.devcontainer/.vscode/settings.json new file mode 100644 index 0000000..cbd71a7 --- /dev/null +++ b/.devcontainer/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "r.plot.useHttpgd": true, + "r.alwaysUseActiveTerminal": true +} diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile new file mode 100644 index 0000000..c5cf877 --- /dev/null +++ b/.devcontainer/Containerfile @@ -0,0 +1,19 @@ +FROM ghcr.io/rocker-org/devcontainer/r-ver:4.5 + +# Architecture-specific variable (built in Docker BuildKit) +ARG TARGETARCH + +# Install quarto-cli and the orange-book extension +RUN wget https://github.com/quarto-dev/quarto-cli/releases/download/v1.9.35/quarto-1.9.35-linux-${TARGETARCH}.deb && \ + dpkg -i quarto-1.9.35-linux-${TARGETARCH}.deb && \ + rm quarto-1.9.35-linux-${TARGETARCH}.deb + +# Adding R packages +RUN install2.r --error knitr rmarkdown markdown igraphdata intergraph \ + ggraph gridGraphics ggplot2 gridExtra gridBase magrittr tinytest \ + sna igraph network + +RUN apt-get update && apt-get install --no-install-recommends -y \ + libglpk-dev + +CMD ["bash"] diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000..cdd6112 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,9 @@ +{ + "features": { + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "1.1.0", + "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671", + "integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..4e67667 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,42 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/cpp +{ + "name": "netplot", + "build": { + "dockerfile": "Containerfile" + }, + "customizations": { + "vscode": { + "extensions": [ + "reditorsupport.r", + "rdebugger.r-debugger", + "quarto.quarto", + "tianyishi.rmarkdown", + "github.vscode-github-actions", + "ms-vscode.live-server" + ] + } + }, + "mounts": [ + // Mount the .vscode configuration into the container + "source=${localWorkspaceFolder}/.devcontainer/.vscode,target=/workspaces/${localWorkspaceFolderBasename}/.vscode,type=bind,consistency=cached" + ], + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "1.1.0" + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "gcc -v", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + "remoteUser": "root" +} diff --git a/Makefile b/Makefile index 857dded..8e5379c 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,17 @@ -VERSION:=$(shell Rscript -e 'x<-readLines("DESCRIPTION");cat(gsub(".+[:]\\s*", "", x[grepl("^Vers", x)]))') -PKGNAME:=$(shell Rscript -e 'x<-readLines("DESCRIPTION");cat(gsub(".+[:]\\s*", "", x[grepl("^Package", x)]))') - -install: build - cd ../ && \ - R CMD INSTALL $(PKGNAME)_$(VERSION).tar.gz - -$(PKGNAME)_$(VERSION).tar.gz: - cd ../ && R CMD build $(PKGNAME)/ - -build: $(PKGNAME)_$(VERSION).tar.gz - inst/NEWS: NEWS.md Rscript -e "rmarkdown::pandoc_convert('NEWS.md', 'plain', output='inst/NEWS')"&& \ head -n 80 inst/NEWS -README.md: README.Rmd - Rscript -e 'rmarkdown::render("README.Rmd")' +README.md: README.qmd + quarto render README.qmd .PHONY: checfull checkv clean -check: $(PKGNAME)_$(VERSION).tar.gz - R CMD check --no-vignettes --no-manual $(PKGNAME)_$(VERSION).tar.gz +check: + Rscript -e "devtools::check()" + +install: + Rscript -e "devtools::install()" checkfull: R/*.R inst/NEWS README.md R CMD build . \&& diff --git a/R/netplot.R b/R/netplot.R index 117eb62..fed5c8d 100644 --- a/R/netplot.R +++ b/R/netplot.R @@ -236,7 +236,7 @@ nplot.network <- function( vertex.label.fontface = "plain", vertex.label.show = .3, vertex.label.range = c(5, 15), - edge.width = 1, + edge.width = network::get.edge.attribute(x, "weight"), edge.width.range = c(1, 2), edge.arrow.size = NULL, edge.color = ~ ego(alpha = .1, col = "gray") + alter, diff --git a/vignettes/examples.Rmd b/vignettes/examples.Rmd index 6a5bae2..da268ea 100644 --- a/vignettes/examples.Rmd +++ b/vignettes/examples.Rmd @@ -203,7 +203,7 @@ gridExtra::grid.arrange( nplot(x_w, layout = l_w, skip.arrows = TRUE, vertex.size.range = c(.05, .05), edge.width = E(x_w)$weight, - edge.width.range = c(1, 6)), + edge.width.range = c(1, 20)), ncol = 3, nrow = 1 ) ``` @@ -222,10 +222,3 @@ gridExtra::grid.arrange( ) ``` -# Node labels - -```{r} -data(UKfaculty, package = "igraphdata") -# fakenames <- sample -``` - From a1eee53f422a73be709886b04f01540057e76e50 Mon Sep 17 00:00:00 2001 From: "George G. Vega Yon" Date: Tue, 30 Jun 2026 20:41:03 +0000 Subject: [PATCH 04/10] Addressing comments from my personal review --- .Rbuildignore | 3 ++ AGENTS.md | 11 ++++++ DESCRIPTION | 2 +- R/geometry.R | 5 ++- R/netplot.R | 39 +++++++++++++------- R/netplot_base.R | 5 ++- inst/tinytest/test_netplot.R | 71 ++++++++++++++++++++++++++++++++++++ man/nplot.Rd | 25 ++++++++----- man/nplot_base.Rd | 18 ++++++--- vignettes/examples.Rmd | 13 ++++--- 10 files changed, 155 insertions(+), 37 deletions(-) create mode 100644 AGENTS.md diff --git a/.Rbuildignore b/.Rbuildignore index 4cb7506..0475d4b 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -14,3 +14,6 @@ netplot\._.*\.tar\.gz ^Rplots\.pdf$ ^doc$ ^Meta$ +^AGENTS\.md$ +^\.devcontainer$ +^\.vscode$ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ca5617e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,11 @@ +# What is this project about + +This is an R package that uses the `grid` package to create network plots. The package is focused on aesthetics and tries to provide an interface that has a lower footprint in dependencies than other packages, leveraging existing base R packages. + +## Development workflow and preferences + +- We follow the tinyverse style for code (so minimal dependencies, but still using `roxygen2` for documentation). + +- For testing the package, we use `tinytest`. + +- For checking the R package, we use `devtools::check()`, and for documenting we call `devtools::document()`. diff --git a/DESCRIPTION b/DESCRIPTION index 2223319..dac35bb 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,7 +17,7 @@ Depends: License: MIT + file LICENSE Encoding: UTF-8 LazyData: true -RoxygenNote: 7.3.1 +RoxygenNote: 7.3.3 Roxygen: list(markdown = TRUE) Imports: graphics, diff --git a/R/geometry.R b/R/geometry.R index 173522b..7ff5ff7 100644 --- a/R/geometry.R +++ b/R/geometry.R @@ -2,6 +2,7 @@ #' @param size Numeric vector. Size of the node (radious). #' @param rel Numeric vector of length 3. 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. #' #' @details #' This function is to be called after [plot.new], as it takes the parameter `usr` @@ -9,6 +10,9 @@ #' @noRd rescale_size <- function(size, rel=c(.01, .05, 1)) { + if (is.null(rel)) + return(size) + # Checking the rel size if (length(rel) == 2) rel <- c(rel, 1) @@ -204,4 +208,3 @@ map_attribute_to_shape <- function(x) { names(sides_lookup)[as.numeric(factor(x))] } - diff --git a/R/netplot.R b/R/netplot.R index fed5c8d..8896f49 100644 --- a/R/netplot.R +++ b/R/netplot.R @@ -30,9 +30,10 @@ edge_color_mixer <- function(i, j, vcols, p = .5, alpha = .15) { #' @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.range Numeric vector of length 3. Relative size for the +#' @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]`. +#' is used as `size^rel[3]`. If `NULL`, scaling is suppressed and `vertex.size` +#' is used as is. #' @param vertex.frame.color Vector of length `vcount(x)`. Border of vertex in #' HEX or built in colors. #' @param vertex.frame.prop Vector of length `vcount(x)`. What proportion of the @@ -46,17 +47,21 @@ edge_color_mixer <- function(i, j, vcols, p = .5, alpha = .15) { #' @param vertex.label.fontface See [grid::gpar] #' @param vertex.label.show Numeric scalar. Proportion of labels to show as the #' top ranking according to `vertex.size`. -#' @param vertex.label.range Numeric vector of size 2 or 3. Relative scale of -#' `vertex.label.fontsize` in points (see [grid::gpar]). +#' @param vertex.label.range Numeric vector of size 2 or 3, or `NULL`. Relative scale of +#' `vertex.label.fontsize` in points (see [grid::gpar]). If `NULL`, scaling is +#' suppressed. #' @param edge.color A vector of length `ecount(x)`. In HEX or built in colors. #' Can be `NULL` in which case #' the color is picked as a mixture between ego and alters' `vertex.color` values. #' @param edge.width Numeric vector of length `ecount(x)`. Relative edge widths. -#' Values are normalized and then mapped to the range specified by `edge.width.range`. -#' For `nplot.igraph`, defaults to the "weight" edge attribute if present. -#' @param edge.width.range Numeric vector of length 2. The minimum and maximum line +#' 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. +#' @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. +#' If `NULL`, scaling is suppressed and `edge.width` is used as is. #' @param edge.arrow.size Vector of length `ecount(x)` from 0 to 1. #' @param edge.curvature Numeric vector of length `ecount(x)`. Curvature of edges #' in terms of radians. @@ -252,6 +257,9 @@ nplot.network <- function( edgelist ) { + if (!length(edge.width)) + edge.width <- 1L + nplot.default( x = x, layout = layout, @@ -607,10 +615,17 @@ nplot.default <- function( netenv$vertex.size <- rep(0, netenv$N) # Rescaling edges - netenv$edge.width <- rescale_size( - netenv$edge.width/max(netenv$edge.width, na.rm=TRUE), - rel = netenv$edge.width.range - ) + if (is.null(netenv$edge.width.range)) { + netenv$edge.width <- rescale_size( + netenv$edge.width, + rel = netenv$edge.width.range + ) + } else { + netenv$edge.width <- rescale_size( + netenv$edge.width/max(netenv$edge.width, na.rm=TRUE), + rel = netenv$edge.width.range + ) + } # Rescaling arrows if (!length(netenv$edge.arrow.size)) @@ -908,5 +923,3 @@ locate_vertex <- function(x = NULL) { # Look at `chull` from `grDevices` - - diff --git a/R/netplot_base.R b/R/netplot_base.R index 2c205b7..573a25b 100644 --- a/R/netplot_base.R +++ b/R/netplot_base.R @@ -115,7 +115,10 @@ nplot_base <- function( edge.width <- rep(1.0, igraph::ecount(x)) # Rescaling edges - edge.width <- rescale_size(edge.width/max(edge.width, na.rm=TRUE), rel = edge.width.range) + if (is.null(edge.width.range)) + edge.width <- rescale_size(edge.width, rel = edge.width.range) + else + edge.width <- rescale_size(edge.width/max(edge.width, na.rm=TRUE), rel = edge.width.range) if (!length(edge.arrow.size)) edge.arrow.size <- vertex.size[E[,1]]/1.5 diff --git a/inst/tinytest/test_netplot.R b/inst/tinytest/test_netplot.R index 27fad90..f572dc1 100644 --- a/inst/tinytest/test_netplot.R +++ b/inst/tinytest/test_netplot.R @@ -2,6 +2,12 @@ # Placeholder with simple test expect_equal(1 + 1, 2) +expect_equal( + netplot:::rescale_size(c(.1, .2, .3), rel = NULL), + c(.1, .2, .3), + info = "NULL range should suppress size scaling" +) + # Test that edge.width values are reflected in the line-grob lwd gpar if (requireNamespace("igraph", quietly = TRUE)) { set.seed(1) @@ -22,5 +28,70 @@ if (requireNamespace("igraph", quietly = TRUE)) { # Edges with larger weights should have larger (or equal) lwd expect_true(all(diff(lwds) >= 0), info = "lwd values should be non-decreasing with increasing weight") + + g_raw <- nplot(x, layout = l, skip.arrows = TRUE, + vertex.size = rep(.05, 4), + vertex.size.range = NULL, + edge.width = igraph::E(x)$weight, + edge.width.range = NULL) + + raw_lwds <- get_edge_gpar(g_raw, element = "line", "lwd")$lwd + expect_equal( + raw_lwds, + igraph::E(x)$weight, + info = "edge.width.range = NULL should use edge.width values as is" + ) + + frame <- g_raw$children$graph$children$vertex.1$children$frame + frame_xy <- cbind(as.numeric(frame$x), as.numeric(frame$y)) + vertex_radius <- max(sqrt(rowSums( + (frame_xy - matrix(g_raw$.layout[1, ], nrow(frame_xy), 2, byrow = TRUE))^2 + ))) + + expect_equal( + vertex_radius, + .05, + tolerance = 1e-8, + info = "vertex.size.range = NULL should use vertex.size values as is" + ) + + base_raw <- nplot_base(x, layout = l, skip.arrows = TRUE, + vertex.size = rep(.05, 4), + vertex.size.range = NULL, + edge.width = igraph::E(x)$weight, + edge.width.range = NULL) + + expect_equal( + base_raw$edge.width, + igraph::E(x)$weight, + info = "nplot_base should also suppress edge-width scaling for NULL range" + ) } +if (requireNamespace("network", quietly = TRUE)) { + x_network <- network::network( + matrix(c( + 0, 1, 0, 1, + 1, 0, 1, 0, + 0, 1, 0, 1, + 1, 0, 1, 0 + ), nrow = 4, byrow = TRUE), + directed = FALSE, + matrix.type = "adjacency" + ) + + l_network <- cbind( + cos(seq(0, 2*pi, length.out = 5)[-5]), + sin(seq(0, 2*pi, length.out = 5)[-5]) + ) + + g_network <- nplot(x_network, layout = l_network, skip.arrows = TRUE, + edge.width.range = NULL) + + network_lwds <- get_edge_gpar(g_network, element = "line", "lwd")$lwd + expect_equal( + network_lwds, + rep(1, g_network$.M), + info = "nplot.network should use width 1 when no edge weight is present" + ) +} diff --git a/man/nplot.Rd b/man/nplot.Rd index a531682..35a14b4 100644 --- a/man/nplot.Rd +++ b/man/nplot.Rd @@ -96,7 +96,7 @@ nplot( vertex.label.fontface = "plain", vertex.label.show = 0.3, vertex.label.range = c(5, 15), - edge.width = 1, + edge.width = network::get.edge.attribute(x, "weight"), edge.width.range = c(1, 2), edge.arrow.size = NULL, edge.color = ~ego(alpha = 0.1, col = "gray") + alter, @@ -198,9 +198,10 @@ the vertex. E.g. three is a triangle, and 100 approximates a circle.} \item{vertex.color}{Vector of length \code{vcount(x)}. Vertex HEX or built in colors.} -\item{vertex.size.range}{Numeric vector of length 3. Relative size for the +\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 -is used as \code{size^rel[3]}.} +is used as \code{size^rel[3]}. If \code{NULL}, scaling is suppressed and \code{vertex.size} +is used as is.} \item{vertex.frame.color}{Vector of length \code{vcount(x)}. Border of vertex in HEX or built in colors.} @@ -224,14 +225,20 @@ vertex does the frame occupy (values between 0 and 1).} \item{vertex.label.show}{Numeric scalar. Proportion of labels to show as the top ranking according to \code{vertex.size}.} -\item{vertex.label.range}{Numeric vector of size 2 or 3. Relative scale of -\code{vertex.label.fontsize} in points (see \link[grid:gpar]{grid::gpar}).} +\item{vertex.label.range}{Numeric vector of size 2 or 3, or \code{NULL}. Relative scale of +\code{vertex.label.fontsize} in points (see \link[grid:gpar]{grid::gpar}). If \code{NULL}, scaling is +suppressed.} -\item{edge.width}{Vector of length \code{ecount(x)} from 0 to 1. All edges will be -the same size.} +\item{edge.width}{Numeric vector of length \code{ecount(x)}. Relative edge widths. +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.} -\item{edge.width.range}{Vector of length \code{ecount(x)} from 0 to 1. Adjusting -width according to weight.} +\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, +\code{c(1, 4)} maps the smallest edge weight to 1pt and the largest to 4pt. +If \code{NULL}, scaling is suppressed and \code{edge.width} is used as is.} \item{edge.arrow.size}{Vector of length \code{ecount(x)} from 0 to 1.} diff --git a/man/nplot_base.Rd b/man/nplot_base.Rd index 4f356dc..511484d 100644 --- a/man/nplot_base.Rd +++ b/man/nplot_base.Rd @@ -48,9 +48,10 @@ the vertex. E.g. three is a triangle, and 100 approximates a circle.} \item{vertex.color}{Vector of length \code{vcount(x)}. Vertex HEX or built in colors.} -\item{vertex.size.range}{Numeric vector of length 3. Relative size for the +\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 -is used as \code{size^rel[3]}.} +is used as \code{size^rel[3]}. If \code{NULL}, scaling is suppressed and \code{vertex.size} +is used as is.} \item{vertex.frame.color}{Vector of length \code{vcount(x)}. Border of vertex in HEX or built in colors.} @@ -61,11 +62,16 @@ elevation degree from which the polygon is drawn.} \item{vertex.frame.prop}{Vector of length \code{vcount(x)}. What proportion of the vertex does the frame occupy (values between 0 and 1).} -\item{edge.width}{Vector of length \code{ecount(x)} from 0 to 1. All edges will be -the same size.} +\item{edge.width}{Numeric vector of length \code{ecount(x)}. Relative edge widths. +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.} -\item{edge.width.range}{Vector of length \code{ecount(x)} from 0 to 1. Adjusting -width according to weight.} +\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, +\code{c(1, 4)} maps the smallest edge weight to 1pt and the largest to 4pt. +If \code{NULL}, scaling is suppressed and \code{edge.width} is used as is.} \item{edge.arrow.size}{Vector of length \code{ecount(x)} from 0 to 1.} diff --git a/vignettes/examples.Rmd b/vignettes/examples.Rmd index da268ea..0ee80af 100644 --- a/vignettes/examples.Rmd +++ b/vignettes/examples.Rmd @@ -103,10 +103,12 @@ gridExtra::grid.arrange( # Nodes -```{r node-scaling, fig.width=7, fig.height=7, fig.cap="Modifying `vertex.size.range`: Each figure shows a different parameter for the vertex size range. From left to right, and top down: (a) Has all vertices with the same scale of 2.5%, (b) "} +```{r node-scaling, fig.width=7, fig.height=7, fig.cap="Modifying `vertex.size.range`: use `NULL` to keep the supplied vertex sizes unchanged."} gridExtra::grid.arrange( - nplot(UKfaculty, layout = l_ukf, vertex.size.range = c(.025, .025)), + nplot(UKfaculty, layout = l_ukf, + vertex.size = rep(.025, vcount(UKfaculty)), + vertex.size.range = NULL), nplot(UKfaculty, layout = l_ukf, vertex.size.range = c(.01, .025)), nplot(UKfaculty, layout = l_ukf, vertex.size.range = c(.01, .025, 4)), nplot(UKfaculty, layout = l_ukf, vertex.size.range = c(.02, .05, 4)), @@ -179,7 +181,7 @@ gridExtra::grid.arrange( The `edge.width` parameter controls the relative width of each edge. Values are normalized and mapped to the range `[min, max]` specified by `edge.width.range` (in points). For `nplot.igraph`, this defaults to the `"weight"` edge attribute -when present. +when present. Use `edge.width.range = NULL` to keep supplied widths unchanged. ```{r edge-width-setup} set.seed(1) @@ -190,7 +192,7 @@ E(x_w)$weight <- c(1, 3, 1, 5, 2, 4, 1, 6) l_w <- layout_in_circle(x_w) ``` -```{r edge-width, fig.width=7, fig.height=3, fig.cap="Effect of `edge.width` and `edge.width.range`. Left: uniform width (default). Middle: widths reflect edge weights with the default range (1–2 pt). Right: widths reflect edge weights with a wider range (1–6 pt), making differences more visible."} +```{r edge-width, fig.width=7, fig.height=3, fig.cap="Effect of `edge.width` and `edge.width.range`. Left: uniform width. Middle: weights mapped to 1–2 pt. Right: raw weights with `edge.width.range = NULL`."} gridExtra::grid.arrange( nplot(x_w, layout = l_w, skip.arrows = TRUE, vertex.size.range = c(.05, .05), @@ -203,7 +205,7 @@ gridExtra::grid.arrange( nplot(x_w, layout = l_w, skip.arrows = TRUE, vertex.size.range = c(.05, .05), edge.width = E(x_w)$weight, - edge.width.range = c(1, 20)), + edge.width.range = NULL), ncol = 3, nrow = 1 ) ``` @@ -221,4 +223,3 @@ gridExtra::grid.arrange( ncol = 2 ) ``` - From 15019104d1e322f4f539bec3920fd156693c9f2f Mon Sep 17 00:00:00 2001 From: "George G. Vega Yon" Date: Tue, 30 Jun 2026 21:01:25 +0000 Subject: [PATCH 05/10] Adding comments by Codex --- Makefile | 41 +++++++----------------------------- R/netplot.R | 11 ++++++---- inst/tinytest/test_netplot.R | 10 +++++++++ man/nplot.Rd | 5 ++--- 4 files changed, 27 insertions(+), 40 deletions(-) diff --git a/Makefile b/Makefile index 8e5379c..e68b0e4 100644 --- a/Makefile +++ b/Makefile @@ -1,41 +1,16 @@ -inst/NEWS: NEWS.md - Rscript -e "rmarkdown::pandoc_convert('NEWS.md', 'plain', output='inst/NEWS')"&& \ - head -n 80 inst/NEWS - README.md: README.qmd quarto render README.qmd -.PHONY: checfull checkv clean +.PHONY: build check install checkfull checkv clean man docker checkd + +build: docs + R CMD build . -check: +check: docs Rscript -e "devtools::check()" -install: +install: docs Rscript -e "devtools::install()" -checkfull: R/*.R inst/NEWS README.md - R CMD build . \&& - R CMD check --as-cran $(PKGNAME)_$(VERSION).tar.gz - -checkv: $(PKGNAME)_$(VERSION).tar.gz - R CMD check --as-cran --use-valgrind $(PKGNAME)_$(VERSION).tar.gz - -clean: - cd ../ && \ - rm -rf $(PKGNAME).Rcheck $(PKGNAME)_$(VERSION).tar.gz - -.PHONY: man docker -man: R/* - Rscript --vanilla -e 'roxygen2::roxygenize()' - -docker: - docker run -v$(pwd):/pkg/ -w/pkg --rm -i uscbiostats/fmcmc:latest make check - -checkd: - apt update && \ - tlmgr install amsmath && \ - install2.r igraph gridGraphics gridBase gridExtra magrittr tinytest sna \ - ggraph rmarkdown knitr intergraph igraphdata pkgdown markdown && \ - apt install libglpk-dev --no-install-recommends -y && \ - R CMD build . && \ - R CMD check --no-manual $(PKGNAME)_$(VERSION).tar.gz +docs: + Rscript -e "devtools::document()" \ No newline at end of file diff --git a/R/netplot.R b/R/netplot.R index 8896f49..50db30d 100644 --- a/R/netplot.R +++ b/R/netplot.R @@ -47,9 +47,8 @@ edge_color_mixer <- function(i, j, vcols, p = .5, alpha = .15) { #' @param vertex.label.fontface See [grid::gpar] #' @param vertex.label.show Numeric scalar. Proportion of labels to show as the #' top ranking according to `vertex.size`. -#' @param vertex.label.range Numeric vector of size 2 or 3, or `NULL`. Relative scale of -#' `vertex.label.fontsize` in points (see [grid::gpar]). If `NULL`, scaling is -#' suppressed. +#' @param vertex.label.range Numeric vector of size 2 or 3. Relative scale of +#' `vertex.label.fontsize` in points (see [grid::gpar]). #' @param edge.color A vector of length `ecount(x)`. In HEX or built in colors. #' Can be `NULL` in which case #' the color is picked as a mixture between ego and alters' `vertex.color` values. @@ -635,11 +634,15 @@ nplot.default <- function( netenv$edge.arrow.size <- rep(0.0, length(netenv$edge.arrow.size)) # Rescaling text - if (!length(netenv$vertex.label.fontsize)) + if (!length(netenv$vertex.label.fontsize)) { + if (is.null(netenv$vertex.label.range)) + netenv$vertex.label.range <- c(5, 15) + netenv$vertex.label.fontsize <- rescale_size( netenv$vertex.size, rel = netenv$vertex.label.range ) + } # Computing label threshold netenv$label_threshold <- stats::quantile( diff --git a/inst/tinytest/test_netplot.R b/inst/tinytest/test_netplot.R index f572dc1..d562c40 100644 --- a/inst/tinytest/test_netplot.R +++ b/inst/tinytest/test_netplot.R @@ -55,6 +55,16 @@ if (requireNamespace("igraph", quietly = TRUE)) { info = "vertex.size.range = NULL should use vertex.size values as is" ) + g_label <- nplot(x, layout = l, skip.arrows = TRUE, + vertex.label = letters[seq_len(igraph::vcount(x))], + vertex.label.range = NULL) + + label_fontsizes <- get_vertex_gpar(g_label, element = "label", "fontsize")$fontsize + expect_true( + all(label_fontsizes >= 5 & label_fontsizes <= 15), + info = "vertex.label.range = NULL should fall back to visible default font sizes" + ) + base_raw <- nplot_base(x, layout = l, skip.arrows = TRUE, vertex.size = rep(.05, 4), vertex.size.range = NULL, diff --git a/man/nplot.Rd b/man/nplot.Rd index 35a14b4..4f75752 100644 --- a/man/nplot.Rd +++ b/man/nplot.Rd @@ -225,9 +225,8 @@ vertex does the frame occupy (values between 0 and 1).} \item{vertex.label.show}{Numeric scalar. Proportion of labels to show as the top ranking according to \code{vertex.size}.} -\item{vertex.label.range}{Numeric vector of size 2 or 3, or \code{NULL}. Relative scale of -\code{vertex.label.fontsize} in points (see \link[grid:gpar]{grid::gpar}). If \code{NULL}, scaling is -suppressed.} +\item{vertex.label.range}{Numeric vector of size 2 or 3. Relative scale of +\code{vertex.label.fontsize} in points (see \link[grid:gpar]{grid::gpar}).} \item{edge.width}{Numeric vector of length \code{ecount(x)}. Relative edge widths. Values are normalized and then mapped to the range specified by \code{edge.width.range}, From 57f2f0bb70b448b6de280ba79e727b9f846c2cef Mon Sep 17 00:00:00 2001 From: "George G. Vega Yon" Date: Tue, 30 Jun 2026 23:37:23 +0000 Subject: [PATCH 06/10] Fixing a bug in rotation --- R/grob_vertex.R | 60 ++++++++++++++----------- R/netplot_base.R | 2 + inst/tinytest/test_netplot.R | 87 ++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 26 deletions(-) diff --git a/R/grob_vertex.R b/R/grob_vertex.R index 59be43c..f89800a 100644 --- a/R/grob_vertex.R +++ b/R/grob_vertex.R @@ -1,7 +1,7 @@ sides_lookup <- list( line = list(sides = 2, rot = 0), # 1 triangle = list(sides = 3, rot = 0), # 2 - square = list(sides = 4, rot = pi/2), # 3 + square = list(sides = 4, rot = pi/4), # 3 diamond = list(sides = 4, rot = 0), # 4 pentagon = list(sides = 5, rot = 0), # 5 hexagon = list(sides = 6, rot = 0), # 6 @@ -10,6 +10,31 @@ circle = list(sides = 25, rot = 0) # 9 ) +resolve_vertex_shape <- function(nsides, rot) { + + if (!is.numeric(nsides) & !is.character(nsides)) { + stop("vertex.nsides must be numeric or character") + } + + rot <- as.numeric(rot) + + if (is.character(nsides)) { + + shape <- nsides + + if (shape %in% names(sides_lookup)) { + info <- sides_lookup[[shape]] + return(list(sides = info$sides, rot = info$rot + rot)) + } else { + stop("Invalid shape name: ", shape) + } + + } + + list(sides = nsides, rot = rot) + +} + #' Functions to calculate graph polygons coordinates #' @param netenv An object of class network environment. #' @param v,e Integer scalars. vertex or edge index. @@ -25,27 +50,10 @@ grob_vertex <- function(netenv, v) { # netenv$vertex.nsides <- eval(netenv$vertex.nsides, envir = data) # } - # Relax vertex.nsides validation - if(!is.numeric(netenv$vertex.nsides[v]) & !is.character(netenv$vertex.nsides[v])) { - stop("vertex.nsides must be numeric or character") - } - - # Handle shape names - if(is.character(netenv$vertex.nsides[v])) { - - shape <- netenv$vertex.nsides[v] - - if(shape %in% names(sides_lookup)) { - info <- sides_lookup[[shape]] - netenv$vertex.nsides[v] <- info$sides - netenv$vertex.rot[v] <- info$rot - } else { - stop("Invalid shape name: ", shape) - } - - } - - + vertex_shape <- resolve_vertex_shape( + nsides = netenv$vertex.nsides[v], + rot = netenv$vertex.rot[v] + ) if (netenv$skip.vertex) return( @@ -63,18 +71,18 @@ grob_vertex <- function(netenv, v) { coords <- npolygon( x = netenv$layout[v, 1], y = netenv$layout[v, 2], - n = as.integer(netenv$vertex.nsides[v]), + n = as.integer(vertex_shape$sides), r = netenv$vertex.size[v]*(1 - netenv$vertex.frame.prop[v]), - d = as.integer(netenv$vertex.rot[v]) + d = vertex_shape$rot ) # Frame coordinates framecoords <- npolygon( x = netenv$layout[v, 1], y = netenv$layout[v, 2], - n = as.integer(netenv$vertex.nsides[v]), + n = as.integer(vertex_shape$sides), r = netenv$vertex.size[v], - d = as.integer(netenv$vertex.rot[v]) + d = vertex_shape$rot ) # Create color palette diff --git a/R/netplot_base.R b/R/netplot_base.R index 573a25b..dafb63d 100644 --- a/R/netplot_base.R +++ b/R/netplot_base.R @@ -264,6 +264,8 @@ nplot_base <- function( edges = vertex.nsides[i], radius = vertex.size[i], doughnut = vertex.size[i]*vertex.frame.prop[i], + init.angle = vertex.rot[i]*180/pi, + last.angle = 360 + vertex.rot[i]*180/pi, rescale = FALSE, add = TRUE, skip.plot.slices = TRUE diff --git a/inst/tinytest/test_netplot.R b/inst/tinytest/test_netplot.R index d562c40..c038b12 100644 --- a/inst/tinytest/test_netplot.R +++ b/inst/tinytest/test_netplot.R @@ -55,6 +55,71 @@ if (requireNamespace("igraph", quietly = TRUE)) { info = "vertex.size.range = NULL should use vertex.size values as is" ) + g_rot <- nplot(x, layout = l, skip.edges = TRUE, skip.arrows = TRUE, + vertex.size = rep(.05, 4), + vertex.size.range = NULL, + vertex.frame.prop = 0, + vertex.nsides = rep(3, 4), + vertex.rot = pi/4) + core <- g_rot$children$graph$children$vertex.1$children$core + core_xy <- unname(cbind(as.numeric(core$x), as.numeric(core$y))) + expected_core_xy <- unname(npolygon( + g_rot$.layout[1, 1], g_rot$.layout[1, 2], + n = 3, r = .05, d = pi/4 + )) + + expect_equal( + core_xy, + expected_core_xy, + tolerance = 1e-8, + info = "vertex.rot should rotate grid vertex polygons using radians" + ) + + g_shape_rot <- nplot(x, layout = l, skip.edges = TRUE, skip.arrows = TRUE, + vertex.size = rep(.05, 4), + vertex.size.range = NULL, + vertex.frame.prop = 0, + vertex.nsides = "triangle", + vertex.rot = pi/4) + shape_core <- g_shape_rot$children$graph$children$vertex.1$children$core + shape_core_xy <- unname(cbind( + as.numeric(shape_core$x), + as.numeric(shape_core$y) + )) + expected_shape_core_xy <- unname(npolygon( + g_shape_rot$.layout[1, 1], g_shape_rot$.layout[1, 2], + n = 3, r = .05, d = pi/4 + )) + + expect_equal( + shape_core_xy, + expected_shape_core_xy, + tolerance = 1e-8, + info = "vertex.rot should add to named-shape vertex rotations" + ) + + g_square <- nplot(x, layout = l, skip.edges = TRUE, skip.arrows = TRUE, + vertex.size = rep(.05, 4), + vertex.size.range = NULL, + vertex.frame.prop = 0, + vertex.nsides = "square") + square_core <- g_square$children$graph$children$vertex.1$children$core + square_core_xy <- unname(cbind( + as.numeric(square_core$x), + as.numeric(square_core$y) + )) + expected_square_core_xy <- unname(npolygon( + g_square$.layout[1, 1], g_square$.layout[1, 2], + n = 4, r = .05, d = pi/4 + )) + + expect_equal( + square_core_xy, + expected_square_core_xy, + tolerance = 1e-8, + info = "the named square shape should use a square orientation" + ) + g_label <- nplot(x, layout = l, skip.arrows = TRUE, vertex.label = letters[seq_len(igraph::vcount(x))], vertex.label.range = NULL) @@ -76,6 +141,28 @@ if (requireNamespace("igraph", quietly = TRUE)) { igraph::E(x)$weight, info = "nplot_base should also suppress edge-width scaling for NULL range" ) + + base_rot <- nplot_base(x, layout = l, skip.edges = TRUE, skip.arrows = TRUE, + vertex.size = rep(.05, 4), + vertex.size.range = NULL, + vertex.frame.prop = .2, + vertex.nsides = rep(3, 4), + vertex.rot = pi/4) + layout_fit <- netplot:::fit_coords_to_dev(l) + frame_outer_xy <- unname(as.matrix( + base_rot$vertex.frame.coords[[1]][seq_len(3), ] + )) + expected_frame_outer_xy <- unname(npolygon( + layout_fit[1, 1], layout_fit[1, 2], + n = 3, r = .05, d = pi/4 + )) + + expect_equal( + frame_outer_xy, + expected_frame_outer_xy, + tolerance = 1e-8, + info = "nplot_base should rotate vertex frames with vertex.rot" + ) } if (requireNamespace("network", quietly = TRUE)) { From b576f98d6fa5b3a02a77c0076887bb0c06e61094 Mon Sep 17 00:00:00 2001 From: "George G. Vega Yon" Date: Wed, 1 Jul 2026 00:46:11 +0000 Subject: [PATCH 07/10] Adding verifications and also fixing issue with rotation argument --- R/netplot.R | 7 +++++ R/netplot_base.R | 28 ++++++++++++++++- inst/tinytest/test_netplot.R | 60 ++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/R/netplot.R b/R/netplot.R index 50db30d..6d5e651 100644 --- a/R/netplot.R +++ b/R/netplot.R @@ -580,6 +580,13 @@ nplot.default <- function( # end ------------------------------------------------------------------------ + if ( + length(edge.line.lty) && + length(edge.line.lty) != 1L && + length(edge.line.lty) != netenv$M + ) + stop("edge.line.lty must have length 1 or one value per plotted edge.") + # This function will repeat a patter taking into account the number of columns .rep <- function(x, .times) { if (grepl("range$", p) | inherits(x, "formula")) diff --git a/R/netplot_base.R b/R/netplot_base.R index dafb63d..06304de 100644 --- a/R/netplot_base.R +++ b/R/netplot_base.R @@ -104,9 +104,33 @@ nplot_base <- function( # Computing shapes ----------------------------------------------------------- E <- igraph::as_edgelist(x, names = FALSE) + M <- nrow(E) if (sample.edges < 1) { - sample.edges <- sample.int(nrow(E), floor(nrow(E)*sample.edges)) + sample.edges <- sample.int(M, floor(M*sample.edges)) + + edge_par <- c( + "edge.width", + "edge.arrow.size", + "edge.color.mix", + "edge.color.alpha", + "edge.curvature", + "edge.line.lty", + "edge.line.breaks" + ) + + for (epar in edge_par) { + if (is.null(get(epar))) + next + + if (is.matrix(get(epar)) || is.data.frame(get(epar))) { + if (nrow(get(epar)) == M) + assign(epar, get(epar)[sample.edges, , drop = FALSE]) + } else if (length(get(epar)) == M) { + assign(epar, get(epar)[sample.edges]) + } + } + E <- E[sample.edges, , drop=FALSE] } @@ -175,6 +199,8 @@ nplot_base <- function( edge.line.lty <- rep(1L, length(edge.coords)) else if (length(edge.line.lty) == 1) edge.line.lty <- rep(edge.line.lty, length(edge.coords)) + else if (length(edge.line.lty) != length(edge.coords)) + stop("edge.line.lty must have length 1 or one value per plotted edge.") if (!length(edge.color.alpha)) edge.color.alpha <- matrix(.5, nrow= length(edge.coords), ncol=2) diff --git a/inst/tinytest/test_netplot.R b/inst/tinytest/test_netplot.R index c038b12..42d1e80 100644 --- a/inst/tinytest/test_netplot.R +++ b/inst/tinytest/test_netplot.R @@ -42,6 +42,30 @@ if (requireNamespace("igraph", quietly = TRUE)) { info = "edge.width.range = NULL should use edge.width values as is" ) + edge_ltys <- c("solid", "dashed", "dotted", "dotdash") + g_lty <- nplot(x, layout = l, skip.arrows = TRUE, + edge.line.lty = edge_ltys) + expect_equal( + get_edge_gpar(g_lty, element = "line", "lty")$lty, + edge_ltys, + info = "edge.line.lty should set one line type per grid edge" + ) + + g_lty_scalar <- nplot(x, layout = l, skip.arrows = TRUE, + edge.line.lty = "dashed") + expect_equal( + get_edge_gpar(g_lty_scalar, element = "line", "lty")$lty, + rep("dashed", igraph::ecount(x)), + info = "scalar edge.line.lty should be recycled across grid edges" + ) + + expect_error( + nplot(x, layout = l, skip.arrows = TRUE, + edge.line.lty = edge_ltys[seq_len(2)]), + "edge.line.lty", + info = "short non-scalar edge.line.lty vectors should be rejected" + ) + frame <- g_raw$children$graph$children$vertex.1$children$frame frame_xy <- cbind(as.numeric(frame$x), as.numeric(frame$y)) vertex_radius <- max(sqrt(rowSums( @@ -142,6 +166,42 @@ if (requireNamespace("igraph", quietly = TRUE)) { info = "nplot_base should also suppress edge-width scaling for NULL range" ) + x_sample <- igraph::make_ring(5, directed = FALSE) + l_sample <- igraph::layout_in_circle(x_sample) + sampled_ltys <- c("solid", "not-a-line-type", "dotted", "dotdash", "longdash") + + set.seed(1) + g_sample_lty <- nplot(x_sample, layout = l_sample, skip.arrows = TRUE, + sample.edges = .4, edge.line.lty = sampled_ltys) + expect_equal( + get_edge_gpar(g_sample_lty, element = "line", "lty")$lty, + sampled_ltys[c(1, 4)], + info = "nplot should keep edge.line.lty aligned when sample.edges drops edges" + ) + + grDevices::pdf(NULL) + set.seed(1) + base_sample_lty <- tryCatch( + nplot_base(x_sample, layout = l_sample, skip.arrows = TRUE, + sample.edges = .4, edge.line.lty = sampled_ltys), + error = function(e) e + ) + grDevices::dev.off() + + expect_false( + inherits(base_sample_lty, "error"), + info = "nplot_base should subset edge.line.lty when sample.edges drops edges" + ) + + grDevices::pdf(NULL) + expect_error( + nplot_base(x, layout = l, skip.arrows = TRUE, + edge.line.lty = edge_ltys[seq_len(2)]), + "edge.line.lty", + info = "nplot_base should reject short non-scalar edge.line.lty vectors" + ) + grDevices::dev.off() + base_rot <- nplot_base(x, layout = l, skip.edges = TRUE, skip.arrows = TRUE, vertex.size = rep(.05, 4), vertex.size.range = NULL, From 6b13d33e7d2f2b2a432c666c4f47c5c4d769796d Mon Sep 17 00:00:00 2001 From: "George G. Vega Yon" Date: Wed, 22 Jul 2026 14:34:39 -0600 Subject: [PATCH 08/10] Correct NEWS wording for edge.width fix Describe the actual fix (nplot.network now uses the weight attribute) and the related changes (NULL ranges, vertex.rot radian fix, edge.line.lty validation) instead of the inaccurate gpar-clobbering rationale. Co-Authored-By: Claude Opus 4.8 --- NEWS.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index 90eeabf..b974904 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,8 +1,20 @@ # netplot 0.3-0 -* `edge.width` now correctly reflected in the drawn line widths. Previously, - the width was computed but not consistently applied via the graphical - parameters of each edge grob. Closes #17. +* `nplot.network()` now uses the `"weight"` edge attribute for `edge.width` + by default (like `nplot.igraph()` already did), so edge widths reflect edge + weights instead of being drawn uniformly. `edge.width` values are also + explicitly re-applied to each edge grob after color post-processing to keep + the drawn line widths robust. Closes #17. + +* `edge.width.range`, `vertex.size.range`, and `vertex.label.range` now accept + `NULL` to suppress scaling and use the supplied values as-is. + +* Fixed `vertex.rot`, which was truncated to an integer and so ignored + fractional (radian) rotations; the named `"square"` shape orientation was + corrected as well. + +* `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. * Invalid arguments passed to `nplot()` now raise an error. From 73bae128548f683e0b364adfdb3f6dfc7a526ba8 Mon Sep 17 00:00:00 2001 From: "George G. Vega Yon" Date: Wed, 22 Jul 2026 20:41:03 +0000 Subject: [PATCH 09/10] Updating GHA versions --- .github/workflows/ci.yml | 4 ++-- .github/workflows/website.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2fe87e..bc881ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: R_KEEP_PKG_SOURCE: yes steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: r-lib/actions/setup-pandoc@v2 @@ -69,7 +69,7 @@ jobs: error-on: '"warning"' # Upload the built package as an artifact - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v7 if: ${{ matrix.config.os == 'ubuntu-latest' && matrix.config.r == 'release' }} with: name: ${{ matrix.config.os }}-pkg diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index b689d4a..c5ccf60 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -10,7 +10,7 @@ jobs: runs-on: Ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v7 - name: Container run: docker pull ${cntr} From 4fc5ec872a65688c6bf192eca0475f762451a547 Mon Sep 17 00:00:00 2001 From: "George G. Vega Yon" Date: Wed, 22 Jul 2026 16:57:20 -0600 Subject: [PATCH 10/10] Address Copilot review comments on PR #18 - netplot_base(): cache each edge parameter once in the sample.edges block instead of calling get() repeatedly, so a (lazily-evaluated) argument is forced a single time before inspecting/subsetting it. - tests: point the throwaway graphics device at a tempfile instead of pdf(NULL) to make it explicit that no artifact is written. Co-Authored-By: Claude Opus 4.8 --- R/netplot_base.R | 16 ++++++++++------ inst/tinytest/test_netplot.R | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/R/netplot_base.R b/R/netplot_base.R index 06304de..c72a6ec 100644 --- a/R/netplot_base.R +++ b/R/netplot_base.R @@ -120,14 +120,18 @@ nplot_base <- function( ) for (epar in edge_par) { - if (is.null(get(epar))) + # Force and cache the (possibly lazily-evaluated) argument once, so we + # don't re-evaluate user-supplied expressions while inspecting/subsetting. + val <- get(epar) + + if (is.null(val)) next - if (is.matrix(get(epar)) || is.data.frame(get(epar))) { - if (nrow(get(epar)) == M) - assign(epar, get(epar)[sample.edges, , drop = FALSE]) - } else if (length(get(epar)) == M) { - assign(epar, get(epar)[sample.edges]) + if (is.matrix(val) || is.data.frame(val)) { + if (nrow(val) == M) + assign(epar, val[sample.edges, , drop = FALSE]) + } else if (length(val) == M) { + assign(epar, val[sample.edges]) } } diff --git a/inst/tinytest/test_netplot.R b/inst/tinytest/test_netplot.R index 42d1e80..45dc517 100644 --- a/inst/tinytest/test_netplot.R +++ b/inst/tinytest/test_netplot.R @@ -179,7 +179,7 @@ if (requireNamespace("igraph", quietly = TRUE)) { info = "nplot should keep edge.line.lty aligned when sample.edges drops edges" ) - grDevices::pdf(NULL) + grDevices::pdf(tempfile(fileext = ".pdf")) set.seed(1) base_sample_lty <- tryCatch( nplot_base(x_sample, layout = l_sample, skip.arrows = TRUE, @@ -193,7 +193,7 @@ if (requireNamespace("igraph", quietly = TRUE)) { info = "nplot_base should subset edge.line.lty when sample.edges drops edges" ) - grDevices::pdf(NULL) + grDevices::pdf(tempfile(fileext = ".pdf")) expect_error( nplot_base(x, layout = l, skip.arrows = TRUE, edge.line.lty = edge_ltys[seq_len(2)]),