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 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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,