Nothing
#' @title Base R Graphics Network Plotting
#' @description Network visualization using base R graphics (similar to qgraph).
#' @name splot
NULL
#' Plot Network with Base R Graphics
#'
#' Creates a network visualization using base R graphics functions (polygon,
#' lines, xspline, etc.) instead of grid graphics. This provides better
#' performance for large networks and uses the same snake_case parameter names
#' as soplot() for consistency.
#'
#' @param x Network input. Can be:
#' - A square numeric matrix (adjacency/weight matrix)
#' - A data frame with edge list (from, to, optional weight columns)
#' - An igraph object
#' - A cograph_network object
#' - A tna object (from tna package)
#' - A group_tna object (list of tna objects from tna package).
#' Use parameter `i` to select a specific group, or omit to plot all groups.
#' @param layout Layout algorithm: "circle", "spring", "groups", or a matrix
#' of x,y coordinates, or an igraph layout function. Also supports igraph
#' two-letter codes: "kk", "fr", "drl", "mds", "ni", etc.
#' @param directed Logical. Force directed interpretation. NULL for auto-detect.
#' @param seed Random seed for deterministic layouts. Default 42.
#' @param theme Theme name: "classic", "dark", "minimal", "colorblind", etc.
#'
#' @param node_size Node size(s). Single value or vector. Default 3.
#' @param node_size2 Secondary node size for ellipse/rectangle height.
#' @param scale_nodes_by Scale node sizes by a centrality measure. Can be:
#' \itemize{
#' \item A measure name: "degree", "strength", "betweenness", "closeness",
#' "eigenvector", "pagerank", "authority", "hub", "harmonic", etc.
#' \item A directional shorthand: "indegree", "outdegree", "instrength",
#' "outstrength", "incloseness", "outcloseness", "inharmonic",
#' "outharmonic", "ineccentricity", "outeccentricity".
#' \item A list with measure and parameters: list("pagerank", damping = 0.9)
#' }
#' When used, node_size is ignored. Use node_size_range to control the
#' min/max size. Default NULL (no centrality scaling).
#' @param node_size_range Size range for centrality-based scaling. Numeric
#' vector c(min_size, max_size). Default c(2, 8).
#' @param scale_nodes_scale Dampening exponent for centrality-based sizing.
#' Values < 1 compress differences (e.g., 0.5 applies square root), values > 1
#' exaggerate differences. Default 1 (linear).
#' @param node_shape Node shape(s): "circle", "square", "triangle", "diamond",
#' "pentagon", "hexagon", "star", "heart", "ellipse", "cross", or any custom
#' SVG shape registered with register_svg_shape().
#' @param node_svg Custom SVG for nodes: path to SVG file OR inline SVG string.
#' @param svg_preserve_aspect Logical: maintain SVG aspect ratio? Default TRUE.
#' @param node_fill Node fill color(s).
#' @param node_border_color Node border color(s).
#' @param node_border_width Node border width(s).
#' @param node_alpha Node transparency (0-1). Default 1.
#' @param labels Node labels: TRUE (use node names/indices), FALSE (none),
#' or character vector.
#' @param label_size Label character expansion factor.
#' @param label_color Label text color.
#' @param label_position Label position: "center", "above", "below", "left", "right".
#' @param label_fontface Font face for labels: "plain", "bold", "italic", "bold.italic". Default "plain".
#' @param label_fontfamily Font family for labels: "sans", "serif", "mono". Default "sans".
#' @param label_hjust Horizontal justification (0=left, 0.5=center, 1=right). Default 0.5.
#' @param label_vjust Vertical justification (0=bottom, 0.5=center, 1=top). Default 0.5.
#' @param label_angle Text rotation angle in degrees. Default 0.
#'
#' @param pie_values List of numeric vectors for pie chart nodes. Each element
#' corresponds to a node and contains values for pie segments. If a simple
#' numeric vector with values between 0 and 1 is provided (e.g., centrality scores),
#' it is automatically converted to donut_fill for convenience.
#' @param pie_colors List of color vectors for pie segments.
#' @param pie_border_width Border width for pie slice dividers. NULL uses node_border_width.
#' @param donut_fill Numeric value (0-1) for donut fill proportion. This is the
#' qgraph-style API: 0.1 = 10% filled, 0.5 = 50% filled, 1.0 = fully filled.
#' Can be a single value (all nodes) or vector (per-node values).
#' @param donut_values Deprecated. Use donut_fill for simple fill proportion.
#' @param donut_color Fill color(s) for the donut ring.
#' Single color sets fill for all nodes.
#' Two colors set fill and background for all nodes.
#' More than 2 colors set per-node fill colors (recycled to n_nodes).
#' Default: "maroon" fill, "gray90" background when node_shape="donut".
#' @param donut_colors Deprecated. Use donut_color instead.
#' @param donut_border_color Border color for donut rings. NULL uses node_border_color.
#' @param donut_border_width Border width for donut rings. NULL uses node_border_width.
#' @param donut_outer_border_color Color for outer boundary border (enables double border).
#' NULL (default) shows single border. Set to a color for double border effect.
#' Can be scalar or per-node vector.
#' @param donut_line_type Line type for donut borders: "solid", "dashed", "dotted", or
#' numeric (1=solid, 2=dashed, 3=dotted). Can be scalar or per-node vector.
#' @param donut_border_lty Deprecated. Use `donut_line_type` instead.
#' @param donut_inner_ratio Inner radius ratio for donut (0-1). Default 0.5.
#' @param donut_bg_color Background color for unfilled donut portion.
#' @param donut_shape Base shape for donut: "circle", "square", "hexagon", "triangle",
#' "diamond", "pentagon". Can be a single value or per-node vector.
#' Default inherits from node_shape (e.g., hexagon nodes get hexagon donuts).
#' Set explicitly to override (e.g., donut_shape = "hexagon" for hexagon donuts
#' on all nodes regardless of node_shape).
#' @param donut_show_value Logical: show value in donut center? Default FALSE.
#' @param donut_value_size Font size for donut center value.
#' @param donut_value_color Color for donut center value.
#' @param donut_value_fontface Font face for donut center value: "plain", "bold", "italic", "bold.italic". Default "bold".
#' @param donut_value_fontfamily Font family for donut center value: "sans", "serif", "mono". Default "sans".
#' @param donut_value_digits Decimal places for donut center value. Default 2.
#' @param donut_value_prefix Text before donut center value (e.g., "$"). Default "".
#' @param donut_value_suffix Text after donut center value (e.g., "%"). Default "".
#' @param donut_empty Logical: render empty donut rings for NA values? Default TRUE.
#' @param donut2_values List of values for inner donut ring (for double donut).
#' @param donut2_colors List of color vectors for inner donut ring segments.
#' @param donut2_inner_ratio Inner radius ratio for inner donut ring. Default 0.4.
#'
#' @param edge_color Edge color(s). If NULL, uses edge_positive_color/edge_negative_color based on weight.
#' @param edge_width Edge width(s). If NULL, scales by weight using edge_size and edge_width_range.
#' @param edge_size Base edge size for weight scaling. NULL (default) uses adaptive sizing
#' based on network size: `15 * exp(-n_nodes/90) + 1`. For directed networks, this
#' is halved. Larger values = thicker edges overall.
#' @param esize Deprecated. Use `edge_size` instead.
#' @param edge_width_range Output width range as c(min, max) for weight-based scaling.
#' Default c(0.5, 4). Edges are scaled to fit within this range.
#' @param edge_scale_mode Scaling mode for edge weights: "linear" (default, qgraph-style),
#' "log" (logarithmic for wide weight ranges), "sqrt" (moderate compression),
#' or "rank" (equal visual spacing regardless of weight distribution).
#' @param edge_cutoff Two-tier cutoff for edge width scaling. NULL (default) = auto-calculate
#' as 75th percentile of weights (qgraph behavior). 0 = disabled (continuous scaling).
#' Positive number = manual threshold. Edges below cutoff get minimal width variation.
#' @param cut Deprecated. Use `edge_cutoff` instead.
#' @param edge_alpha Edge transparency (0-1). Default 0.8.
#' @param edge_labels Edge labels: TRUE (show weights), FALSE (none),
#' or character vector.
#' @param edge_label_size Edge label size.
#' @param edge_label_color Edge label text color.
#' @param edge_label_bg Edge label background color.
#' @param edge_label_position Position along edge (0-1).
#' @param edge_label_offset Perpendicular offset for edge labels (0 = on line, positive = above).
#' @param edge_label_fontface Font face: "plain", "bold", "italic", "bold.italic".
#' @param edge_label_shadow Logical: enable drop shadow for edge labels? Default FALSE.
#' @param edge_label_shadow_color Color for edge label shadow. Default "gray40".
#' @param edge_label_shadow_offset Offset distance for shadow in points. Default 0.5.
#' @param edge_label_shadow_alpha Transparency for shadow (0-1). Default 0.5.
#' @param edge_label_halo Logical: enable white halo/outline around edge labels for
#' readability over dark edges? Default FALSE. When TRUE, overrides shadow settings.
#' @param edge_style Line type(s): 1=solid, 2=dashed, 3=dotted, etc.
#' @param curvature Edge curvature. 0 for straight, positive/negative for curves.
#' @param curve_scale Logical: auto-curve reciprocal edges?
#' @param curve_shape Spline tension (-1 to 1). Default 0.
#' @param curve_pivot Position along edge for curve control point (0-1).
#' @param curves Curve mode: TRUE (default) = single edges straight, reciprocal edges
#' curve as ellipse (two opposing curves); FALSE = all straight; "force" = all curved.
#' @param arrow_size Arrow head size.
#' @param arrow_angle Arrow head angle in radians. Default pi/6 (30 degrees).
#' @param show_arrows Logical or vector: show arrows on directed edges?
#' @param bidirectional Logical or vector: show arrows at both ends?
#' @param loop_rotation Angle(s) in radians for self-loop direction.
#' @param edge_start_style Style for the start segment of edges: "solid" (default),
#' "dashed", or "dotted". Use dashed/dotted to indicate edge direction (source node).
#' @param edge_start_length Fraction of edge length for the styled start segment (0-0.5).
#' Default 0.15 (15% of edge). Only applies when edge_start_style is not "solid".
#' @param edge_start_dot_density Pattern for dotted start segments. A two-character string
#' where the first digit is dot length and second is gap length (in line width units).
#' Default "12" (1 unit dot, 2 units gap). Use "11" for tighter dots, "13" for more spacing.
#' Only applies when edge_start_style = "dotted".
#'
#' @param edge_ci Numeric vector of CI widths (0-1 scale). Larger values = more uncertainty.
#' @param edge_ci_scale Width multiplier for underlay thickness. Default 2.
#' @param edge_ci_alpha Transparency for underlay (0-1). Default 0.15.
#' @param edge_ci_color Underlay color. NA (default) uses main edge color.
#' @param edge_ci_style Line type for underlay: 1=solid, 2=dashed, 3=dotted. Default 2.
#' @param edge_ci_arrows Logical: show arrows on underlay? Default FALSE.
#' @param edge_priority Numeric vector of edge priorities. Higher values render on top.
#' Useful for ensuring significant edges appear above non-significant ones.
#'
#' @param edge_label_style Preset style: "none", "estimate", "full", "range", "stars".
#' @param edge_label_template Template with placeholders: \{est\}, \{range\}, \{low\}, \{up\}, \{p\}, \{stars\}.
#' Overrides edge_label_style if provided.
#' @param edge_label_digits Decimal places for estimates. Default 2.
#' @param edge_label_leading_zero Logical: show leading zero for values < 1? Default TRUE.
#' Set to FALSE to display ".5" instead of "0.5".
#' @param edge_label_oneline Logical: single line format? Default TRUE.
#' @param edge_label_ci_format CI format: "bracket" for `[low, up]` or "dash" for `low-up`.
#' @param edge_ci_lower Numeric vector of lower CI bounds for labels.
#' @param edge_ci_upper Numeric vector of upper CI bounds for labels.
#' @param edge_label_p Numeric vector of p-values for edges.
#' @param edge_label_p_digits Decimal places for p-values. Default 3.
#' @param edge_label_p_prefix Prefix for p-values. Default "p=".
#' @param edge_label_stars Stars for labels: character vector, TRUE (compute from p),
#' or numeric (treated as p-values).
#'
#' @param weight_digits Number of decimal places to round edge weights to before
#' plotting. Edges that round to zero are automatically removed. Default 2.
#' Set NULL to disable rounding.
#' @param threshold Minimum absolute weight to display.
#' @param minimum Alias for threshold (qgraph compatibility). Uses max of threshold and minimum.
#' @param maximum Maximum weight for scaling. NULL for auto.
#' @param edge_positive_color Color for positive weights.
#' @param positive_color Deprecated. Use `edge_positive_color` instead.
#' @param edge_negative_color Color for negative weights.
#' @param negative_color Deprecated. Use `edge_negative_color` instead.
#' @param edge_duplicates How to handle duplicate edges in undirected networks.
#' NULL (default) = stop with error listing duplicates. Options: "sum", "mean",
#' "first", "max", "min", or a custom aggregation function.
#'
#' @param title Plot title.
#' @param title_size Title font size.
#' @param margins Margins as c(bottom, left, top, right).
#' @param background Background color.
#' @param rescale Logical: rescale layout to -1 to 1 range?
#' @param layout_scale Scale factor for layout. >1 expands (spreads nodes apart),
#' <1 contracts (brings nodes closer). Use "auto" to automatically scale based
#' on node count (compact for small networks, expanded for large). Default 1.
#' @param layout_margin Margin around the layout as fraction of range. Default 0.15.
#' Set to 0 for no extra margin (tighter fit). Affects white space around nodes.
#' @param aspect Logical: maintain aspect ratio?
#' @param use_pch Logical: use points() for simple circles (faster). Default FALSE.
#' @param usePCH Deprecated. Use `use_pch` instead.
#' @param scaling Scaling mode: "default" for qgraph-matched scaling where node_size=6
#' looks similar to qgraph vsize=6, or "legacy" to preserve pre-v2.0 behavior.
#'
#' @param legend Logical: show legend?
#' @param legend_position Position: "topright", "topleft", "bottomright", "bottomleft".
#' @param legend_size Legend text size.
#' @param legend_edge_colors Logical: show positive/negative edge colors in legend?
#' @param legend_node_sizes Logical: show node size scale in legend?
#' @param groups Group assignments for node coloring/legend.
#' @param node_names Alternative names for legend (separate from labels).
#' @param tna_styling Logical or NULL. If \code{TRUE}, applies TNA visual defaults
#' (oval layout, TNA color palette, edge labels as estimates, dotted edge starts,
#' etc.) as a base layer. Any explicitly provided argument overrides the TNA default.
#' If \code{FALSE}, no TNA styling is applied. If \code{NULL} (default),
#' automatically set to \code{TRUE} when \code{x} is a tna object, \code{FALSE}
#' otherwise. Can be used with any input type (matrix, igraph, cograph_network).
#' @param i Group index or name when x is a group_tna object. If NULL (default),
#' plots all groups in a grid. If specified (e.g., i = 1 or i = "Treatment"),
#' plots only that group.
#'
#' @param filetype Output format: "default" (screen), "png", "pdf", "svg", "jpeg", "tiff".
#' @param filename Output filename (without extension).
#' @param width Output width in inches.
#' @param height Output height in inches.
#' @param res Resolution in DPI for raster outputs (PNG, JPEG, TIFF). Default 600.
#' @param ... Additional arguments passed to layout functions.
#'
#' @details
#' ## Edge Curve Behavior
#' Edge curving is controlled by three parameters that interact:
#' \describe{
#' \item{\strong{curves}}{Mode for automatic curving. \code{FALSE} = all straight,
#' \code{TRUE} (default) = curve only reciprocal edge pairs as an ellipse,
#' \code{"force"} = curve all edges inward toward network center.}
#' \item{\strong{curvature}}{Manual curvature amount (0-1 typical). Sets the
#' magnitude of curves. Default 0 uses automatic 0.175 for curved edges.
#' Positive values curve edges; the direction is automatically determined.
#' }
#' \item{\strong{curve_scale}}{Not currently used; reserved for future scaling.}
#' }
#'
#' For reciprocal edges (A\code{->}B and B\code{->}A both exist), the edges curve
#' in opposite directions to form a visual ellipse, making bidirectional
#' relationships clear.
#'
#' ## Weight Scaling Modes (edge_scale_mode)
#' Controls how edge weights are mapped to visual widths:
#' \describe{
#' \item{\strong{linear} (default)}{Width proportional to weight. Best when
#' weights are similar in magnitude.}
#' \item{\strong{log}}{Logarithmic scaling. Best when weights span multiple
#' orders of magnitude (e.g., 0.01 to 100).}
#' \item{\strong{sqrt}}{Square root scaling. Moderate compression, good for
#' moderately skewed distributions.}
#'
#' \item{\strong{rank}}{Rank-based scaling. Ignores actual values; uses relative
#' ordering. All edges get equal visual spacing regardless of weight distribution.}
#' }
#'
#' ## Donut vs Pie vs Double Donut
#' Three ways to show additional data on nodes:
#' \describe{
#' \item{\strong{Donut (donut_fill)}}{Single ring showing a proportion (0-1).
#' Ideal for completion rates, probabilities, or any single metric per node.
#' Use \code{donut_color} for fill color and \code{donut_bg_color} for unfilled portion.}
#' \item{\strong{Pie (pie_values)}}{Multiple colored segments showing category
#' breakdown. Ideal for composition data. Values are normalized to sum to 1.
#' Use \code{pie_colors} for segment colors.}
#' \item{\strong{Double Donut (donut2_values)}}{Two concentric rings for comparing
#' two metrics per node. Outer ring uses \code{donut_fill}/\code{donut_color},
#' inner ring uses \code{donut2_values}/\code{donut2_colors}.}
#' }
#'
#' ## CI Underlay System
#' Confidence interval underlays draw a wider, semi-transparent edge behind the
#' main edge to visualize uncertainty:
#' \describe{
#' \item{\strong{edge_ci}}{Vector of CI widths (0-1 scale). Larger = more uncertainty.}
#' \item{\strong{edge_ci_scale}}{Multiplier for underlay width relative to main edge.
#' Default 2 means underlay is twice as wide as main edge at CI=1.}
#' \item{\strong{edge_ci_alpha}}{Transparency of underlay (0-1). Default 0.15.}
#' \item{\strong{edge_ci_style}}{Line type: 1=solid, 2=dashed (default), 3=dotted.}
#' }
#'
#' ## Edge Label Templates
#' For statistical output, use templates to format complex labels:
#' \describe{
#' \item{\strong{edge_label_template}}{Template string with placeholders:
#' \code{\{est\}} for estimate/weight, \code{\{low\}}/\code{\{up\}} for CI bounds,
#' \code{\{range\}} for formatted range, \code{\{p\}} for p-value, \code{\{stars\}}
#' for significance stars.}
#' \item{\strong{edge_label_style}}{Preset styles: \code{"estimate"} (weight only),
#' \code{"full"} (estimate + CI), \code{"range"} (CI only), \code{"stars"} (significance).}
#' }
#'
#' @return Invisibly returns the cograph_network object.
#'
#' @seealso
#' \code{\link{soplot}} for grid graphics rendering (alternative engine),
#' \code{\link{cograph}} for creating network objects,
#' \code{\link{sn_nodes}} for node customization,
#' \code{\link{sn_edges}} for edge customization,
#' \code{\link{sn_layout}} for layout algorithms,
#' \code{\link{sn_theme}} for visual themes,
#' \code{\link{from_qgraph}} and \code{\link{from_tna}} for converting external objects
#'
#' @export
#'
#' @examples
#' # Basic network from adjacency matrix
#' adj <- matrix(c(0, 1, 1, 0,
#' 0, 0, 1, 1,
#' 0, 0, 0, 1,
#' 0, 0, 0, 0), 4, 4, byrow = TRUE)
#' splot(adj)
#'
#' # With curved edges
#' splot(adj, curvature = 0.2)
#'
#' # Weighted network with colors
#' w_adj <- matrix(c(0, 0.5, -0.3, 0,
#' 0.8, 0, 0.4, -0.2,
#' 0, 0, 0, 0.6,
#' 0, 0, 0, 0), 4, 4, byrow = TRUE)
#' splot(w_adj, edge_positive_color = "darkgreen", edge_negative_color = "red")
#'
#' # Pie chart nodes
#' splot(adj, pie_values = list(c(1,2,3), c(2,2), c(1,1,1,1), c(3,1)))
#'
#' # Circle layout with labels
#' splot(adj, layout = "circle", labels = c("A", "B", "C", "D"))
#'
#' @export
splot <- function(
x,
layout = "oval",
directed = NULL,
seed = 42,
theme = NULL,
# Node aesthetics
node_size = NULL,
node_size2 = NULL,
scale_nodes_by = NULL,
node_size_range = c(2, 8),
scale_nodes_scale = 1,
node_shape = "circle",
node_svg = NULL,
svg_preserve_aspect = TRUE,
node_fill = NULL,
node_border_color = NULL,
node_border_width = 1,
node_alpha = 1,
labels = TRUE,
label_size = NULL,
label_color = "black",
label_position = "center",
label_fontface = "plain",
label_fontfamily = "sans",
label_hjust = 0.5,
label_vjust = 0.5,
label_angle = 0,
# Pie/Donut
pie_values = NULL,
pie_colors = NULL,
pie_border_width = NULL,
donut_fill = NULL,
donut_values = NULL,
donut_color = NULL,
donut_colors = NULL, # Deprecated: use donut_color
donut_border_color = NULL,
donut_border_width = NULL,
donut_outer_border_color = NULL,
donut_line_type = "solid",
donut_border_lty = NULL, # Deprecated: use donut_line_type
donut_inner_ratio = 0.8,
donut_bg_color = "gray90",
donut_shape = "circle",
donut_show_value = FALSE,
donut_value_size = 0.8,
donut_value_color = "black",
donut_value_fontface = "bold",
donut_value_fontfamily = "sans",
donut_value_digits = 2,
donut_value_prefix = "",
donut_value_suffix = "",
donut_empty = TRUE,
donut2_values = NULL,
donut2_colors = NULL,
donut2_inner_ratio = 0.4,
# Edge aesthetics
edge_color = NULL,
edge_width = NULL,
edge_size = NULL,
esize = NULL, # Deprecated: use edge_size
edge_width_range = c(0.1, 4),
edge_scale_mode = "linear",
edge_cutoff = NULL,
cut = NULL, # Deprecated: use edge_cutoff
edge_alpha = 0.8,
edge_labels = FALSE,
edge_label_size = 0.8,
edge_label_color = "gray30",
edge_label_bg = NA,
edge_label_position = 0.5,
edge_label_offset = 0,
edge_label_fontface = "plain",
edge_label_shadow = FALSE,
edge_label_shadow_color = "gray40",
edge_label_shadow_offset = 0.5,
edge_label_shadow_alpha = 0.5,
edge_label_halo = TRUE,
edge_style = 1,
curvature = 0,
curve_scale = TRUE,
curve_shape = 0,
curve_pivot = 0.5,
curves = TRUE,
arrow_size = 1,
arrow_angle = pi/6,
show_arrows = TRUE,
bidirectional = FALSE,
loop_rotation = NULL,
# Edge Start Style (for direction clarity)
edge_start_style = "solid",
edge_start_length = 0.15,
edge_start_dot_density = "12",
# Edge CI Underlays
edge_ci = NULL,
edge_ci_scale = 2.0,
edge_ci_alpha = 0.15,
edge_ci_color = NA,
edge_ci_style = 2,
edge_ci_arrows = FALSE,
edge_priority = NULL,
# Edge Label Templates
edge_label_style = "none",
edge_label_template = NULL,
edge_label_digits = 2,
edge_label_oneline = TRUE,
edge_label_ci_format = "bracket",
edge_label_leading_zero = TRUE,
edge_ci_lower = NULL,
edge_ci_upper = NULL,
edge_label_p = NULL,
edge_label_p_digits = 3,
edge_label_p_prefix = "p=",
edge_label_stars = NULL,
# Weight handling
weight_digits = 2,
threshold = 0,
minimum = 0,
maximum = NULL,
edge_positive_color = "#2E7D32",
positive_color = NULL, # Deprecated: use edge_positive_color
edge_negative_color = "#C62828",
negative_color = NULL, # Deprecated: use edge_negative_color
edge_duplicates = NULL,
# Plot settings
title = NULL,
title_size = 1.2,
margins = c(0.1, 0.1, 0.1, 0.1),
background = "white",
rescale = TRUE,
layout_scale = 1,
layout_margin = 0.15,
aspect = TRUE,
use_pch = FALSE,
usePCH = NULL, # Deprecated: use use_pch
scaling = "default",
# Legend
legend = FALSE,
legend_position = "topright",
legend_size = 0.8,
legend_edge_colors = TRUE,
legend_node_sizes = FALSE,
groups = NULL,
node_names = NULL,
# TNA styling
tna_styling = NULL,
# Group selection (for group_tna)
i = NULL,
# Output
filetype = "default",
filename = file.path(tempdir(), "splot"),
width = 7,
height = 7,
res = 600,
...
) {
# ============================================
# 1. INPUT PROCESSING
# ============================================
# --- Collect explicitly-provided user args (for dispatch forwarding) ---
# match.call only captures args the user actually typed, not defaults
.user_explicit <- as.list(match.call(expand.dots = FALSE))[-1]
.user_explicit$x <- NULL
.dots <- list(...)
# Translate qgraph-style args for tna-family objects (early, before any dispatch)
if (inherits(x, c("tna", "group_tna", "tna_bootstrap",
"tna_permutation", "group_tna_permutation"))) {
.dots <- .translate_qgraph_dots(.dots)
}
# Evaluate user-explicit args once from local scope (safe, no re-eval of AST)
# Exclude "..." — those are already captured in .dots
.user_args <- mget(setdiff(names(.user_explicit), "..."), envir = environment())
# Handle tna objects directly
if (inherits(x, "tna")) {
tna_params <- from_tna(x, engine = "splot", plot = FALSE)
# tna_styling is implicitly TRUE for tna objects unless user said FALSE
if (identical(tna_styling, FALSE)) {
# Strip visual defaults, keep only structural data
structural <- c("x", "labels", "directed", "weight_digits",
"donut_fill", "donut_inner_ratio", "donut_empty")
tna_params <- tna_params[intersect(names(tna_params), structural)]
}
call_args <- .collect_dispatch_args(.user_args, .dots, base = tna_params)
call_args$tna_styling <- NULL # consumed; don't pass to recursive call
return(do.call(splot, call_args))
}
# Handle group_tna objects (list of tna objects from tna package)
if (inherits(x, "group_tna")) {
n_groups <- length(x)
group_names <- names(x)
if (is.null(group_names)) group_names <- paste0("Group ", seq_len(n_groups))
# Build forwarded args: everything the user explicitly provided except x and i
fwd_args <- .collect_dispatch_args(.user_args, .dots, skip = c("x", "i"))
# If i is specified, plot just that group
if (!is.null(i)) {
# Resolve group index
if (is.character(i)) {
idx <- match(i, group_names)
if (is.na(idx)) {
stop("Group '", i, "' not found. Available groups: ",
paste(group_names, collapse = ", "), call. = FALSE)
}
} else {
idx <- as.integer(i)
if (idx < 1 || idx > n_groups) {
stop("Group index ", idx, " out of range. Available: 1 to ", n_groups, call. = FALSE)
}
}
# Set title to group name if not provided
if (is.null(fwd_args$title)) fwd_args$title <- group_names[idx]
return(do.call(splot, c(list(x = x[[idx]]), fwd_args)))
}
# No i specified: plot all groups in a grid
n_cols <- ceiling(sqrt(n_groups))
n_rows <- ceiling(n_groups / n_cols)
old_par <- graphics::par(mfrow = c(n_rows, n_cols), mar = c(1, 1, 2, 1))
on.exit(graphics::par(old_par), add = TRUE)
for (idx in seq_len(n_groups)) {
grp_fwd <- fwd_args
grp_fwd$title <- if (is.null(fwd_args$title)) {
group_names[idx]
} else {
paste(fwd_args$title, "-", group_names[idx])
}
do.call(splot, c(list(x = x[[idx]]), grp_fwd))
}
return(invisible(NULL))
}
# ============================================
# HANDLE cluster_summary / mcml
# ============================================
# Handle cluster_summary / mcml objects -> dispatch to plot_mcml
if (inherits(x, c("cluster_summary", "mcml"))) {
return(do.call(plot_mcml, c(list(x = x), .collect_dispatch_args(.user_args, .dots))))
}
# Dispatch to specialized methods for bootstrap objects
if (inherits(x, "tna_bootstrap")) {
return(do.call(splot.tna_bootstrap, c(list(x = x), .collect_dispatch_args(.user_args, .dots))))
}
# Dispatch to specialized methods for permutation test objects
if (inherits(x, "tna_permutation")) {
return(do.call(splot.tna_permutation, c(list(x = x), .collect_dispatch_args(.user_args, .dots))))
}
# Dispatch for group permutation tests
if (inherits(x, "group_tna_permutation")) {
return(do.call(splot.group_tna_permutation, c(list(x = x), .collect_dispatch_args(.user_args, .dots))))
}
# Dispatch for tna disparity filter results
if (inherits(x, "tna_disparity")) {
return(do.call(splot.tna_disparity, c(list(x = x), .collect_dispatch_args(.user_args, .dots))))
}
# Nestimate: base netobject — apply directed/undirected styling defaults
if (inherits(x, "netobject")) {
return(do.call(splot.netobject, c(list(x = x), .collect_dispatch_args(.user_args, .dots))))
}
# Nestimate: bootstrap object
if (inherits(x, "net_bootstrap")) {
return(do.call(splot.net_bootstrap, c(list(x = x), .collect_dispatch_args(.user_args, .dots))))
}
# Nestimate: permutation test object
if (inherits(x, "net_permutation")) {
return(do.call(splot.net_permutation, c(list(x = x), .collect_dispatch_args(.user_args, .dots))))
}
# Nestimate: glasso bootstrap object
if (inherits(x, "boot_glasso")) {
return(do.call(splot.boot_glasso, c(list(x = x), .collect_dispatch_args(.user_args, .dots))))
}
# Nestimate: group of netobjects
if (inherits(x, "netobject_group")) {
return(do.call(plot_netobject_group, c(list(x = x), .collect_dispatch_args(.user_args, .dots))))
}
# Nestimate: multilevel netobject
if (inherits(x, "netobject_ml")) {
return(do.call(plot_netobject_ml, c(list(x = x), .collect_dispatch_args(.user_args, .dots))))
}
# Nestimate: mixed wtna (transition + co-occurrence)
if (inherits(x, "wtna_mixed")) {
return(do.call(splot.wtna_mixed, c(list(x = x), .collect_dispatch_args(.user_args, .dots))))
}
# ============================================
# HANDLE DEPRECATED PARAMETERS
# ============================================
# Detect which arguments were explicitly provided by the user
explicit_args <- names(.user_explicit)
# For params with NULL defaults, simple check works
edge_size <- handle_deprecated_param(edge_size, esize, "edge_size", "esize")
edge_cutoff <- handle_deprecated_param(edge_cutoff, cut, "edge_cutoff", "cut")
# For params with non-NULL defaults, use new_val_was_set to check if user explicitly set them
use_pch <- handle_deprecated_param(
use_pch, usePCH, "use_pch", "usePCH",
new_val_was_set = "use_pch" %in% explicit_args
)
edge_positive_color <- handle_deprecated_param(
edge_positive_color, positive_color,
"edge_positive_color", "positive_color",
new_val_was_set = "edge_positive_color" %in% explicit_args
)
edge_negative_color <- handle_deprecated_param(
edge_negative_color, negative_color,
"edge_negative_color", "negative_color",
new_val_was_set = "edge_negative_color" %in% explicit_args
)
donut_line_type <- handle_deprecated_param(
donut_line_type, donut_border_lty,
"donut_line_type", "donut_border_lty",
new_val_was_set = "donut_line_type" %in% explicit_args
)
# Convert edge_label_fontface to numeric if string (for backwards compat with renderers)
edge_label_fontface_num <- fontface_to_numeric(edge_label_fontface)
# ============================================
# APPLY TNA STYLING DEFAULTS
# ============================================
# tna_styling = TRUE applies TNA visual defaults as a base layer.
# Any user-explicit arg always wins. NULL defaults are filled;
# non-NULL defaults are only overridden if the user didn't specify them.
if (isTRUE(tna_styling)) {
# Detect directedness for TNA defaults (matrix or network)
.tna_dir <- if (!is.null(directed)) {
directed
} else if (is.matrix(x)) {
!is_symmetric_matrix(x)
} else {
TRUE
}
.tna_n <- if (is.matrix(x)) nrow(x) else NULL
.tna_defs <- .tna_style_defaults(.tna_n, .tna_dir)
# Parameters with NULL defaults — fill if user didn't set them
if (is.null(node_fill) && !is.null(.tna_defs$node_fill))
node_fill <- .tna_defs$node_fill
if (is.null(node_size))
node_size <- .tna_defs$node_size
if (is.null(edge_color))
edge_color <- .tna_defs$edge_color
# Parameters with non-NULL defaults — only override if user didn't explicitly set
if (!"layout" %in% explicit_args)
layout <- .tna_defs$layout
if (!"edge_label_style" %in% explicit_args)
edge_label_style <- .tna_defs$edge_label_style
if (!"edge_label_leading_zero" %in% explicit_args)
edge_label_leading_zero <- .tna_defs$edge_label_leading_zero
if (!"edge_label_size" %in% explicit_args)
edge_label_size <- .tna_defs$edge_label_size
if (!"edge_label_position" %in% explicit_args)
edge_label_position <- .tna_defs$edge_label_position
if (!"minimum" %in% explicit_args)
minimum <- .tna_defs$minimum
# Directed-only defaults
if (isTRUE(.tna_dir)) {
if (!"arrow_size" %in% explicit_args && !is.null(.tna_defs$arrow_size))
arrow_size <- .tna_defs$arrow_size
if (!"edge_start_length" %in% explicit_args && !is.null(.tna_defs$edge_start_length))
edge_start_length <- .tna_defs$edge_start_length
if (!"edge_start_style" %in% explicit_args && !is.null(.tna_defs$edge_start_style))
edge_start_style <- .tna_defs$edge_start_style
}
}
# Round matrix weights to filter near-zero edges globally
if (is.matrix(x) && !is.null(weight_digits)) {
x <- round(x, weight_digits)
}
# Set seed for deterministic layouts, restoring RNG state on exit
if (!is.null(seed)) {
saved_rng <- .save_rng()
on.exit(.restore_rng(saved_rng), add = TRUE)
set.seed(seed)
}
# Convert to cograph_network if needed
network <- ensure_cograph_network(x, layout = layout, seed = seed, directed = directed, ...)
# Apply theme if specified
if (!is.null(theme)) {
th <- get_theme(theme)
if (!is.null(th)) {
# Extract theme colors
if (is.null(node_fill)) node_fill <- th$get("node_fill")
if (is.null(node_border_color)) node_border_color <- th$get("node_border_color")
if (is.null(background)) background <- th$get("background")
if (length(label_color) == 1 && label_color == "black") label_color <- th$get("label_color")
if (length(edge_positive_color) == 1 && edge_positive_color == "#2E7D32") edge_positive_color <- th$get("edge_positive_color")
if (length(edge_negative_color) == 1 && edge_negative_color == "#C62828") edge_negative_color <- th$get("edge_negative_color")
}
}
# Extract network data using getter functions
# This handles all formats: new list format, old attr format, and R6 wrapper
nodes <- get_nodes(network)
edges <- get_edges(network)
is_net_directed <- is_directed(network)
# Get layout coordinates from nodes if available
if ("x" %in% names(nodes) && !all(is.na(nodes$x))) {
layout_coords <- data.frame(x = nodes$x, y = nodes$y)
} else {
layout_coords <- NULL # nocov
}
# (oval layout uses elliptical spacing but nodes remain circular via aspect=TRUE)
n_nodes <- nrow(nodes)
n_edges <- if (!is.null(edges)) nrow(edges) else 0
# Determine if directed
if (is.null(directed)) {
directed <- is_net_directed
}
# Check for duplicate edges in undirected networks
edges <- check_duplicate_edges(edges, directed, edge_duplicates)
n_edges <- nrow(edges)
if (!is.null(network)) network$edges <- edges
# ============================================
# 2. LAYOUT HANDLING
# ============================================
if (is.null(layout_coords)) { # nocov start
stop("Layout coordinates not available", call. = FALSE)
} # nocov end
layout_mat <- as.matrix(layout_coords[, c("x", "y")])
# Rescale to [-1, 1]
if (rescale) {
layout_mat <- as.matrix(rescale_layout(layout_mat, mar = 0.1))
}
# Apply layout scale (expand/contract around center)
# Handle "auto" scaling based on node count
if (identical(layout_scale, "auto")) {
# Auto-scale formula:
# - Small networks (<10): compact (0.8-0.9)
# - Medium networks (10-30): normal (0.9-1.1)
# - Large networks (>30): expanded (1.1-1.4)
layout_scale <- 0.7 + 0.7 * (1 - exp(-n_nodes / 25))
}
if (is.numeric(layout_scale) && layout_scale != 1) {
center <- colMeans(layout_mat)
layout_mat <- t(t(layout_mat - center) * layout_scale + center)
}
# ============================================
# 2b. AUTO-CONVERT pie_values VECTOR TO donut_fill
# ============================================
# If pie_values is a numeric vector (not list) with values in [0,1],
# treat it as donut_fill instead (single proportion per node)
if (!is.null(pie_values) && is.numeric(pie_values) && !is.list(pie_values)) {
if (all(pie_values >= 0 & pie_values <= 1, na.rm = TRUE)) {
donut_fill <- pie_values
pie_values <- NULL
}
}
# ============================================
# 3. PARAMETER VECTORIZATION
# ============================================
# Get scale constants for current scaling mode
scale <- get_scale_constants(scaling)
# Node sizes (qgraph-style, using scale constants)
# Check for centrality-based scaling first
centrality_info <- NULL
if (!is.null(scale_nodes_by)) {
centrality_info <- resolve_centrality_sizes(
x = x,
scale_by = scale_nodes_by,
size_range = node_size_range,
n = n_nodes,
scaling = scaling,
scale_exp = scale_nodes_scale
)
vsize_usr <- centrality_info$sizes
} else {
vsize_usr <- resolve_node_sizes(node_size, n_nodes, scaling = scaling)
}
vsize2_usr <- if (!is.null(node_size2)) {
resolve_node_sizes(node_size2, n_nodes, scaling = scaling)
} else {
vsize_usr
}
# Node shapes
# Handle custom SVG if provided
if (!is.null(node_svg)) {
# Register SVG as a temporary shape
temp_svg_name <- paste0("_splot_svg_", format(Sys.time(), "%H%M%S"))
tryCatch({
register_svg_shape(temp_svg_name, node_svg)
node_shape <- temp_svg_name
}, error = function(e) {
warning("Failed to register SVG shape: ", e$message, ". Using default shape.",
call. = FALSE)
})
}
shapes <- resolve_shapes(node_shape, n_nodes)
# Node colors
node_colors <- resolve_node_colors(node_fill, n_nodes, nodes, groups)
# Vectorize node_alpha
node_alphas <- recycle_to_length(node_alpha, n_nodes)
# Apply alpha to node colors (skip if all alpha=1)
if (any(node_alphas < 1)) {
node_colors <- mapply(function(col, alpha) {
if (alpha < 1) adjust_alpha(col, alpha) else col
}, node_colors, node_alphas, SIMPLIFY = TRUE, USE.NAMES = FALSE)
}
# Border colors (compute on unique colors to avoid redundant col2rgb calls)
if (is.null(node_border_color)) {
unique_cols <- unique(node_colors)
darkened <- setNames(vapply(unique_cols, function(c) {
tryCatch(adjust_brightness(c, -0.3), error = function(e) "black")
}, character(1)), unique_cols)
node_border_color <- unname(darkened[node_colors])
}
border_colors <- recycle_to_length(node_border_color, n_nodes)
# Border widths
border_widths <- recycle_to_length(node_border_width, n_nodes)
# Labels
node_labels <- resolve_labels(labels, nodes, n_nodes)
# Label sizes (using new decoupled system)
label_cex <- resolve_label_sizes(label_size, vsize_usr, n_nodes, scaling = scaling)
label_colors <- recycle_to_length(label_color, n_nodes)
# ============================================
# 4. EDGE PROCESSING
# ============================================
# Use minimum threshold or explicit threshold
effective_threshold <- max(threshold, minimum)
if (n_edges > 0) {
# Filter by minimum weight (threshold)
orig_n_edges <- n_edges
orig_weights <- edges$weight
edges <- filter_edges_by_weight(edges, effective_threshold)
n_edges <- nrow(edges)
# Subset all per-edge vectors to match filtered edge count
if (n_edges < orig_n_edges) {
keep_idx <- which(abs(orig_weights) >= effective_threshold)
.subset_if_per_edge <- function(v) {
if (!is.null(v) && length(v) == orig_n_edges) v[keep_idx] else v
}
if (is.character(edge_labels) && length(edge_labels) == orig_n_edges)
edge_labels <- edge_labels[keep_idx]
edge_style <- .subset_if_per_edge(edge_style)
edge_color <- .subset_if_per_edge(edge_color)
edge_width <- .subset_if_per_edge(edge_width)
edge_priority <- .subset_if_per_edge(edge_priority)
edge_ci <- .subset_if_per_edge(edge_ci)
edge_ci_alpha <- .subset_if_per_edge(edge_ci_alpha)
edge_ci_scale <- .subset_if_per_edge(edge_ci_scale)
edge_ci_color <- .subset_if_per_edge(edge_ci_color)
edge_label_fontface <- .subset_if_per_edge(edge_label_fontface)
edge_label_position <- .subset_if_per_edge(edge_label_position)
edge_label_p <- .subset_if_per_edge(edge_label_p)
edge_ci_lower <- .subset_if_per_edge(edge_ci_lower)
edge_ci_upper <- .subset_if_per_edge(edge_ci_upper)
}
}
# ============================================
# EDGE CURVING BEHAVIOR
# ============================================
# curves = TRUE (default): single edges straight, reciprocal edges curved
# curves = "force": all edges curved
# curves = FALSE: all edges straight
#
# NOTE: We no longer duplicate edges for undirected networks.
# Only edges with actual reciprocal pairs (A→B AND B→A) will curve.
if (n_edges > 0) {
# Edge colors
edge_colors <- resolve_edge_colors(edges, edge_color, edge_positive_color, edge_negative_color)
# Vectorize edge_alpha and apply to edge colors (skip if all alpha=1)
edge_alphas <- recycle_to_length(edge_alpha, n_edges)
if (any(edge_alphas < 1)) {
edge_colors <- mapply(function(col, alpha) {
if (alpha < 1) adjust_alpha(col, alpha) else col
}, edge_colors, edge_alphas, SIMPLIFY = TRUE, USE.NAMES = FALSE)
}
# Apply edge_cutoff threshold for transparency: edges below cutoff are faded
if (!is.null(edge_cutoff) && edge_cutoff > 0 && "weight" %in% names(edges)) {
abs_weights <- abs(edges$weight)
below_cutoff <- abs_weights < edge_cutoff
if (any(below_cutoff)) {
# Scale alpha: edges at 0 get 20% of normal alpha, edges near cutoff get full alpha
fade_factor <- ifelse(below_cutoff, 0.2 + 0.8 * (abs_weights / edge_cutoff), 1)
edge_colors <- mapply(function(col, fade) {
if (fade < 1) adjust_alpha(col, fade) else col
}, edge_colors, fade_factor, SIMPLIFY = TRUE, USE.NAMES = FALSE)
}
}
# Edge widths
edge_widths <- resolve_edge_widths(
edges = edges,
edge.width = edge_width,
esize = edge_size,
n_nodes = n_nodes,
directed = directed,
maximum = maximum,
minimum = threshold,
cut = edge_cutoff,
edge_width_range = edge_width_range,
edge_scale_mode = edge_scale_mode,
scaling = scaling
)
# Line types and dotted-width adjustment
es <- resolve_edge_styles(edge_style, edge_widths, n_edges)
ltys <- es$ltys
edge_widths <- es$edge_widths
# Compute per-edge curvatures (reciprocal detection + direction)
curve_result <- compute_edge_curvatures(curvature, curves, edges, layout_mat)
curves_vec <- curve_result$curves_vec
is_reciprocal <- curve_result$is_reciprocal
curve_pivots <- recycle_to_length(curve_pivot, n_edges)
curve_shapes <- recycle_to_length(curve_shape, n_edges)
# Arrows
if (is.logical(show_arrows) && length(show_arrows) == 1) {
arrows_vec <- rep(directed && show_arrows, n_edges)
} else {
arrows_vec <- recycle_to_length(show_arrows, n_edges)
}
# Arrow size (using scale constants for consistency)
asize_scaled <- arrow_size * scale$arrow_factor
arrow_sizes <- recycle_to_length(asize_scaled, n_edges)
# Bidirectional
bidirectionals <- recycle_to_length(bidirectional, n_edges)
# Loop rotation
loop_rotations <- resolve_loop_rotation(loop_rotation, edges, layout_mat)
# Edge labels - check for template system first
if (!is.null(edge_label_template) || edge_label_style != "none") {
# Use template-based labels
edge_weights <- if ("weight" %in% names(edges)) edges$weight else NULL
edge_labels_vec <- build_edge_labels_from_template(
template = edge_label_template,
style = edge_label_style,
weights = edge_weights,
ci_lower = edge_ci_lower,
ci_upper = edge_ci_upper,
p_values = edge_label_p,
stars = edge_label_stars,
digits = edge_label_digits,
p_digits = edge_label_p_digits,
p_prefix = edge_label_p_prefix,
ci_format = edge_label_ci_format,
oneline = edge_label_oneline,
leading_zero = edge_label_leading_zero,
n = n_edges
)
} else {
# Use standard edge labels
edge_labels_vec <- resolve_edge_labels(edge_labels, edges, n_edges)
}
# CI underlay parameters
edge_ci_vec <- if (!is.null(edge_ci)) recycle_to_length(edge_ci, n_edges) else NULL
edge_ci_colors <- if (!is.null(edge_ci_vec)) {
if (length(edge_ci_color) == 1 && is.na(edge_ci_color)) {
# Use main edge colors
edge_colors
} else {
recycle_to_length(edge_ci_color, n_edges)
}
} else NULL
}
# ============================================
# 5. DEVICE SETUP
# ============================================
# Handle file output
if (filetype != "default") {
full_filename <- paste0(filename, ".", filetype)
if (filetype == "png") {
grDevices::png(full_filename, width = width, height = height,
units = "in", res = res)
} else if (filetype == "pdf") {
grDevices::pdf(full_filename, width = width, height = height)
} else if (filetype == "svg") {
grDevices::svg(full_filename, width = width, height = height)
} else if (filetype == "jpeg" || filetype == "jpg") {
grDevices::jpeg(full_filename, width = width, height = height,
units = "in", res = res, quality = 100)
} else if (filetype == "tiff") {
grDevices::tiff(full_filename, width = width, height = height,
units = "in", res = res, compression = "lzw")
} else {
stop("Unknown filetype: ", filetype, call. = FALSE)
}
on.exit(grDevices::dev.off(), add = TRUE)
}
# Set up plot area - only save/restore parameters we modify
old_mar <- graphics::par("mar")
on.exit(graphics::par(mar = old_mar), add = TRUE)
# Margins - ensure title has adequate space
# Default margins[3] (top) is 0.1 which is too small for titles
# Add extra space proportional to title_size when title is provided
title_space <- if (!is.null(title)) max(1.5, title_size * 1.2) else 0
graphics::par(mar = c(margins[1], margins[2], margins[3] + title_space, margins[4]))
# Calculate plot limits accounting for node radii, self-loops, and margins
lims <- compute_plot_limits(layout_mat, vsize_usr, layout_margin,
edges, n_edges, loop_rotations)
xlim <- lims$xlim
ylim <- lims$ylim
# Create plot
graphics::plot(
1, type = "n",
xlim = xlim,
ylim = ylim,
axes = FALSE,
ann = FALSE,
asp = if (aspect) 1 else NA,
xaxs = "i", yaxs = "i"
)
# Background
if (!is.null(background) && background != "transparent") {
graphics::rect(
xleft = xlim[1] - 1, ybottom = ylim[1] - 1,
xright = xlim[2] + 1, ytop = ylim[2] + 1,
col = background, border = NA
)
}
# Title
if (!is.null(title)) {
graphics::title(main = title, cex.main = title_size)
}
# ============================================
# 6. RENDER EDGES
# ============================================
if (n_edges > 0) {
render_edges_splot(
edges = edges,
layout = layout_mat,
node_sizes = vsize_usr,
shapes = shapes,
edge_color = edge_colors,
edge_width = edge_widths,
edge_style = ltys,
curvature = curves_vec,
curve_shape = curve_shapes,
curve_pivot = curve_pivots,
show_arrows = arrows_vec,
arrow_size = arrow_sizes,
arrow_angle = arrow_angle,
bidirectional = bidirectionals,
loop_rotation = loop_rotations,
edge_labels = edge_labels_vec,
edge_label_size = edge_label_size,
edge_label_color = edge_label_color,
edge_label_bg = edge_label_bg,
edge_label_position = edge_label_position,
edge_label_offset = edge_label_offset,
edge_label_fontface = edge_label_fontface,
edge_label_shadow = edge_label_shadow,
edge_label_shadow_color = edge_label_shadow_color,
edge_label_shadow_offset = edge_label_shadow_offset,
edge_label_shadow_alpha = edge_label_shadow_alpha,
edge_label_halo = edge_label_halo,
# CI underlay parameters
edge_ci = edge_ci_vec,
edge_ci_scale = edge_ci_scale,
edge_ci_alpha = edge_ci_alpha,
edge_ci_color = edge_ci_colors,
edge_ci_style = edge_ci_style,
edge_ci_arrows = edge_ci_arrows,
edge_priority = edge_priority,
is_reciprocal = is_reciprocal,
# Edge start style parameters
edge_start_style = edge_start_style,
edge_start_length = edge_start_length,
edge_start_dot_density = edge_start_dot_density
)
}
# ============================================
# 7. RENDER NODES
# ============================================
# Resolve donut parameters
dp <- resolve_donut_params(
donut_fill = donut_fill, donut_values = donut_values,
donut_color = donut_color, donut_colors = donut_colors,
donut_bg_color = donut_bg_color, donut_shape = donut_shape,
donut_border_color = donut_border_color,
donut_outer_border_color = donut_outer_border_color,
donut_line_type = donut_line_type, donut_empty = donut_empty,
shapes = shapes, n_nodes = n_nodes
)
render_nodes_splot(
layout = layout_mat,
node_size = vsize_usr,
node_size2 = vsize2_usr,
node_shape = shapes,
node_fill = node_colors,
node_border_color = border_colors,
node_border_width = border_widths,
pie_values = pie_values,
pie_colors = pie_colors,
pie_border_width = pie_border_width,
donut_values = dp$donut_values,
donut_colors = dp$donut_colors,
donut_border_color = dp$donut_border_color,
donut_border_width = donut_border_width,
donut_outer_border_color = dp$donut_outer_border_color,
donut_line_type = dp$donut_line_type,
donut_inner_ratio = donut_inner_ratio,
donut_bg_color = dp$bg_color,
donut_shape = dp$donut_shapes,
donut_show_value = donut_show_value,
donut_value_size = donut_value_size,
donut_value_color = donut_value_color,
donut_value_fontface = donut_value_fontface,
donut_value_fontfamily = donut_value_fontfamily,
donut_value_digits = donut_value_digits,
donut_value_prefix = donut_value_prefix,
donut_value_suffix = donut_value_suffix,
donut2_values = donut2_values,
donut2_colors = donut2_colors,
donut2_inner_ratio = donut2_inner_ratio,
labels = node_labels,
label_size = label_cex,
label_color = label_colors,
label_position = label_position,
label_fontface = label_fontface,
label_fontfamily = label_fontfamily,
label_hjust = label_hjust,
label_vjust = label_vjust,
label_angle = label_angle,
use_pch = use_pch
)
# ============================================
# 8. LEGEND
# ============================================
if (legend) {
# Determine if we have positive/negative weighted edges
has_pos_edges <- FALSE
has_neg_edges <- FALSE
if (n_edges > 0 && "weight" %in% names(edges)) {
has_pos_edges <- any(edges$weight > 0, na.rm = TRUE)
has_neg_edges <- any(edges$weight < 0, na.rm = TRUE)
}
render_legend_splot(
groups = groups,
node_names = node_names,
nodes = nodes,
node_colors = node_colors,
position = legend_position,
cex = legend_size,
show_edge_colors = legend_edge_colors,
positive_color = edge_positive_color,
negative_color = edge_negative_color,
has_pos_edges = has_pos_edges,
has_neg_edges = has_neg_edges,
show_node_sizes = legend_node_sizes,
node_size = vsize_usr
)
}
# ============================================
# 9. RETURN
# ============================================
invisible(network)
}
#' Render Edges for splot
#' @keywords internal
render_edges_splot <- function(edges, layout, node_sizes, shapes,
edge_color, edge_width, edge_style, curvature,
curve_shape, curve_pivot, show_arrows, arrow_size,
arrow_angle = pi/6, bidirectional, loop_rotation, edge_labels,
edge_label_size, edge_label_color, edge_label_bg,
edge_label_position, edge_label_offset = 0,
edge_label_fontface,
edge_label_shadow = FALSE, edge_label_shadow_color = "gray40",
edge_label_shadow_offset = 0.5, edge_label_shadow_alpha = 0.5,
edge_label_halo = TRUE,
edge_ci = NULL, edge_ci_scale = 2.0,
edge_ci_alpha = 0.15, edge_ci_color = NULL,
edge_ci_style = 2, edge_ci_arrows = FALSE,
edge_priority = NULL,
is_reciprocal = NULL,
edge_start_style = "solid", edge_start_length = 0.15,
edge_start_dot_density = "12") {
m <- nrow(edges)
if (m == 0) return(invisible())
n <- nrow(layout)
# Calculate network center for inward curve direction
center_x <- mean(layout[, 1])
center_y <- mean(layout[, 2])
# Get render order (weakest to strongest, low priority to high priority)
order_idx <- get_edge_order(edges, priority = edge_priority)
# Expand CI parameters to per-edge vectors
edge_ci_scales <- expand_param(edge_ci_scale, m, "edge_ci_scale")
edge_ci_alphas <- expand_param(edge_ci_alpha, m, "edge_ci_alpha")
edge_ci_arrows_vec <- expand_param(edge_ci_arrows, m, "edge_ci_arrows")
# Storage for label positions
label_positions <- vector("list", m)
# Validate and convert edge_start_style to lty value
# Accepts string values ("solid", "dashed", "dotted") or numeric (1, 2, 3)
if (is.numeric(edge_start_style)) {
if (!edge_start_style %in% c(1, 2, 3)) {
warning("edge_start_style numeric value should be 1 (solid), 2 (dashed), or 3 (dotted). ",
"Got: ", edge_start_style, ". Using solid.", call. = FALSE)
start_lty <- 1
} else if (edge_start_style == 3) {
# Dotted: use custom density pattern
start_lty <- edge_start_dot_density
} else {
start_lty <- edge_start_style
}
} else {
valid_styles <- c("solid", "dashed", "dotted")
if (!edge_start_style %in% valid_styles) {
stop("edge_start_style must be one of: ", paste(valid_styles, collapse = ", "),
", or numeric 1-3. Got: '", edge_start_style, "'", call. = FALSE)
}
start_lty <- switch(edge_start_style,
"solid" = 1,
"dashed" = 2,
"dotted" = edge_start_dot_density # Use custom density pattern
)
}
start_fraction <- if (identical(start_lty, 1) || identical(start_lty, 1L)) 0 else edge_start_length
# Helper function to calculate curve direction (bend INWARD toward center)
calc_curve_direction <- function(curve_val, start_x, start_y, end_x, end_y) {
# Defensive check: ensure all coordinates are valid scalars
if (length(start_x) == 0 || length(start_y) == 0 ||
length(end_x) == 0 || length(end_y) == 0 ||
any(is.na(c(start_x, start_y, end_x, end_y)))) {
return(if (length(curve_val) > 0) curve_val else 0) # nocov
}
if (length(curve_val) == 0 || is.na(curve_val)) { # nocov start
return(0)
} # nocov end
if (curve_val > 1e-6) {
mid_x <- (start_x + end_x) / 2
mid_y <- (start_y + end_y) / 2
dx <- end_x - start_x
dy <- end_y - start_y
to_center_x <- center_x - mid_x
to_center_y <- center_y - mid_y
# Perpendicular to edge direction (same as draw_curved_edge_base)
# Clockwise rotation: (dx, dy) -> (dy, -dx)
len <- sqrt(dx^2 + dy^2)
if (length(len) == 0 || is.na(len) || len < 1e-10) return(curve_val) # nocov
px <- dy / len
py <- -dx / len
# Dot product: positive = perpendicular points toward center
dot <- px * to_center_x + py * to_center_y
if (dot < 0) -abs(curve_val) else abs(curve_val)
} else {
curve_val
}
}
for (i in order_idx) {
from_idx <- edges$from[i]
to_idx <- edges$to[i]
# Skip invalid edges (NA or out-of-bounds indices)
if (length(from_idx) == 0 || length(to_idx) == 0 ||
is.na(from_idx) || is.na(to_idx) ||
from_idx < 1 || to_idx < 1 ||
from_idx > n || to_idx > n) {
next
}
x1 <- layout[from_idx, 1]
y1 <- layout[from_idx, 2]
x2 <- layout[to_idx, 1]
y2 <- layout[to_idx, 2]
# Skip if coordinates are invalid
if (length(x1) == 0 || length(y1) == 0 ||
length(x2) == 0 || length(y2) == 0 ||
any(is.na(c(x1, y1, x2, y2)))) {
next
}
# Self-loop
if (from_idx == to_idx) {
# PASS 1: Draw CI underlay for self-loop (if edge_ci provided)
if (!is.null(edge_ci) && !is.na(edge_ci[i]) && edge_ci[i] > 0) {
underlay_width <- edge_width[i] * (1 + edge_ci[i] * edge_ci_scales[i])
underlay_col <- if (!is.null(edge_ci_color)) edge_ci_color[i] else edge_color[i]
underlay_col <- adjust_alpha(underlay_col, edge_ci_alphas[i])
draw_self_loop_base(
x1, y1, node_sizes[from_idx],
col = underlay_col,
lwd = underlay_width,
lty = edge_ci_style,
rotation = loop_rotation[i],
arrow = edge_ci_arrows_vec[i],
asize = arrow_size[i],
arrow_angle = arrow_angle
)
}
# PASS 2: Draw main self-loop
draw_self_loop_base(
x1, y1, node_sizes[from_idx],
col = edge_color[i],
lwd = edge_width[i],
lty = edge_style[i],
rotation = loop_rotation[i],
arrow = show_arrows[i],
asize = arrow_size[i],
arrow_angle = arrow_angle
)
# Label position for self-loop
loop_dist <- node_sizes[from_idx] * 2.5
label_positions[[i]] <- list(
x = x1 + loop_dist * cos(loop_rotation[i]),
y = y1 + loop_dist * sin(loop_rotation[i])
)
next
}
# Calculate edge endpoints
angle_to <- splot_angle(x1, y1, x2, y2)
angle_from <- splot_angle(x2, y2, x1, y1)
start <- cent_to_edge(x1, y1, angle_to, node_sizes[from_idx], NULL, shapes[from_idx])
end <- cent_to_edge(x2, y2, angle_from, node_sizes[to_idx], NULL, shapes[to_idx])
# Determine curve direction
# For reciprocal edges, use pre-computed curvature directly (preserves opposite directions)
# For non-reciprocal edges, apply inward curve direction adjustment
if (!is.null(is_reciprocal) && is_reciprocal[i]) {
curve_i <- curvature[i]
} else {
curve_i <- calc_curve_direction(curvature[i], start$x, start$y, end$x, end$y)
}
# PASS 1: Draw CI underlay (if edge_ci provided)
if (!is.null(edge_ci) && !is.na(edge_ci[i]) && edge_ci[i] > 0) {
underlay_width <- edge_width[i] * (1 + edge_ci[i] * edge_ci_scales[i])
underlay_col <- if (!is.null(edge_ci_color)) edge_ci_color[i] else edge_color[i]
underlay_col <- adjust_alpha(underlay_col, edge_ci_alphas[i])
if (abs(curve_i) > 1e-6) {
draw_curved_edge_base(
start$x, start$y, end$x, end$y,
curve = curve_i,
curvePivot = curve_pivot[i],
col = underlay_col,
lwd = underlay_width,
lty = edge_ci_style,
arrow = edge_ci_arrows_vec[i],
asize = arrow_size[i],
bidirectional = FALSE,
arrow_angle = arrow_angle
)
} else {
draw_straight_edge_base(
start$x, start$y, end$x, end$y,
col = underlay_col,
lwd = underlay_width,
lty = edge_ci_style,
arrow = edge_ci_arrows_vec[i],
asize = arrow_size[i],
bidirectional = FALSE,
arrow_angle = arrow_angle
)
}
}
# PASS 2: Draw main edge
if (abs(curve_i) > 1e-6) {
draw_curved_edge_base(
start$x, start$y, end$x, end$y,
curve = curve_i,
curvePivot = curve_pivot[i],
col = edge_color[i],
lwd = edge_width[i],
lty = edge_style[i],
arrow = show_arrows[i],
asize = arrow_size[i],
bidirectional = bidirectional[i],
start_lty = start_lty,
start_fraction = start_fraction,
arrow_angle = arrow_angle
)
} else {
draw_straight_edge_base(
start$x, start$y, end$x, end$y,
col = edge_color[i],
lwd = edge_width[i],
lty = edge_style[i],
arrow = show_arrows[i],
asize = arrow_size[i],
bidirectional = bidirectional[i],
start_lty = start_lty,
start_fraction = start_fraction,
arrow_angle = arrow_angle
)
}
# Store edge start/end and curve info for label positioning
label_positions[[i]] <- list(
start_x = start$x, start_y = start$y,
end_x = end$x, end_y = end$y,
curve = curve_i,
curvePivot = curve_pivot[i]
)
}
# Draw edge labels
if (!is.null(edge_labels)) {
# Vectorize edge label parameters (strict: length 1 or m)
edge_label_sizes <- expand_param(edge_label_size, m, "edge_label_size")
edge_label_colors <- expand_param(edge_label_color, m, "edge_label_color")
edge_label_bgs <- expand_param(edge_label_bg, m, "edge_label_bg")
edge_label_positions_vec <- expand_param(edge_label_position, m, "edge_label_position")
edge_label_offsets <- expand_param(edge_label_offset, m, "edge_label_offset")
edge_label_shadows <- expand_param(edge_label_shadow, m, "edge_label_shadow")
edge_label_shadow_colors <- expand_param(edge_label_shadow_color, m, "edge_label_shadow_color")
edge_label_shadow_offsets <- expand_param(edge_label_shadow_offset, m, "edge_label_shadow_offset")
edge_label_shadow_alphas <- expand_param(edge_label_shadow_alpha, m, "edge_label_shadow_alpha")
# Apply halo effect if enabled (overrides shadow settings)
edge_label_halos <- expand_param(edge_label_halo, m, "edge_label_halo")
for (i in seq_len(m)) {
if (isTRUE(edge_label_halos[i])) {
edge_label_shadows[i] <- "halo"
edge_label_shadow_colors[i] <- "white"
edge_label_shadow_alphas[i] <- 1.0
if (edge_label_shadow_offsets[i] < 0.5) {
edge_label_shadow_offsets[i] <- 0.6
}
}
}
# Handle edge_label_fontface - convert strings to numbers if needed
edge_label_fontfaces <- expand_param(edge_label_fontface, m, "edge_label_fontface")
edge_label_fontfaces <- vapply(edge_label_fontfaces, fontface_to_numeric, numeric(1))
for (i in seq_len(m)) {
if (!is.null(edge_labels[i]) && !is.na(edge_labels[i]) && edge_labels[i] != "") {
edge_info <- label_positions[[i]]
# Self-loops have x, y directly; regular edges have start_x, start_y, etc.
if (!is.null(edge_info$x) && !is.null(edge_info$y)) {
# Self-loop: use stored position directly
pos <- list(x = edge_info$x, y = edge_info$y)
} else {
# Regular edge: compute position
pos <- get_edge_label_position(
edge_info$start_x, edge_info$start_y,
edge_info$end_x, edge_info$end_y,
position = edge_label_positions_vec[i],
curve = edge_info$curve,
curvePivot = edge_info$curvePivot,
label_offset = edge_label_offsets[i]
)
}
draw_edge_label_base(
pos$x, pos$y,
label = edge_labels[i],
cex = edge_label_sizes[i],
col = edge_label_colors[i],
bg = edge_label_bgs[i],
font = edge_label_fontfaces[i],
shadow = edge_label_shadows[i],
shadow_color = edge_label_shadow_colors[i],
shadow_offset = edge_label_shadow_offsets[i],
shadow_alpha = edge_label_shadow_alphas[i]
)
}
}
}
}
#' Render Nodes for splot
#'
#' @param donut_values List of values for donut chart. Each element is a single
#' numeric (0-1) representing fill proportion for that node.
#' @keywords internal
render_nodes_splot <- function(layout, node_size, node_size2, node_shape, node_fill,
node_border_color, node_border_width, pie_values, pie_colors,
pie_border_width, donut_values, donut_colors,
donut_border_color, donut_border_width,
donut_outer_border_color = NULL, donut_line_type = "solid",
donut_inner_ratio, donut_bg_color, donut_shape,
donut_show_value, donut_value_size, donut_value_color,
donut_value_fontface = "bold", donut_value_fontfamily = "sans",
donut_value_digits = 2, donut_value_prefix = "",
donut_value_suffix = "",
donut2_values, donut2_colors, donut2_inner_ratio,
labels, label_size, label_color, label_position,
label_fontface = "plain", label_fontfamily = "sans",
label_hjust = 0.5, label_vjust = 0.5, label_angle = 0,
use_pch = FALSE) {
n <- nrow(layout)
if (n == 0) return(invisible())
# Vectorize donut parameters (strict: length 1 or n)
donut_inner_ratios <- expand_param(donut_inner_ratio, n, "donut_inner_ratio")
donut_bg_colors <- expand_param(donut_bg_color, n, "donut_bg_color")
donut_show_values <- expand_param(donut_show_value, n, "donut_show_value")
donut_value_sizes <- expand_param(donut_value_size, n, "donut_value_size")
donut_value_colors <- expand_param(donut_value_color, n, "donut_value_color")
donut_value_fontfaces <- expand_param(donut_value_fontface, n, "donut_value_fontface")
donut_value_fontfamilies <- expand_param(donut_value_fontfamily, n, "donut_value_fontfamily")
# Render order: largest to smallest
order_idx <- get_node_order(node_size)
for (i in order_idx) {
x <- layout[i, 1]
y <- layout[i, 2]
# Check for pie/donut/donut2
has_pie <- !is.null(pie_values) && length(pie_values) >= i && !is.null(pie_values[[i]]) && length(pie_values[[i]]) > 0
# Check for donut: either node_shape is "donut" OR donut_values has a valid (non-NA) value
has_donut <- (node_shape[i] == "donut") ||
(!is.null(donut_values) && length(donut_values) >= i &&
!is.null(donut_values[[i]]) && length(donut_values[[i]]) > 0 && !anyNA(donut_values[[i]]))
has_donut2 <- !is.null(donut2_values) && length(donut2_values) >= i && !is.null(donut2_values[[i]])
if (has_donut2 || (has_donut && has_pie)) {
# Double donut with optional inner pie
# Or single donut with pie - both use the layered drawing approach
if (has_donut2) {
# Double donut case
donut_vals <- if (has_donut) donut_values[[i]] else NULL
donut_cols <- if (!is.null(donut_colors) && length(donut_colors) >= i) donut_colors[[i]] else NULL
donut2_vals <- donut2_values[[i]]
donut2_cols <- if (!is.null(donut2_colors) && length(donut2_colors) >= i) donut2_colors[[i]] else NULL
pie_vals <- if (has_pie) pie_values[[i]] else NULL
pie_cols <- if (!is.null(pie_colors) && length(pie_colors) >= i) pie_colors[[i]] else NULL
draw_double_donut_pie_node_base(
x, y, node_size[i],
donut_values = donut_vals,
donut_colors = donut_cols,
donut2_values = donut2_vals,
donut2_colors = donut2_cols,
pie_values = pie_vals,
pie_colors = pie_cols,
pie_default_color = node_fill[i],
outer_inner_ratio = donut_inner_ratios[i],
inner_inner_ratio = donut2_inner_ratio,
bg_color = donut_bg_colors[i],
border.col = node_border_color[i],
border.width = node_border_width[i],
pie_border.width = pie_border_width,
donut_border.width = donut_border_width
)
} else {
# Single donut with pie
donut_val <- if (length(donut_values[[i]]) == 1) donut_values[[i]] else 1
donut_col <- if (!is.null(donut_colors) && length(donut_colors) >= i) donut_colors[[i]][1] else node_fill[i]
pie_vals <- pie_values[[i]]
pie_cols <- if (!is.null(pie_colors) && length(pie_colors) >= i) pie_colors[[i]] else NULL
# Get per-node donut shape
current_donut_shape <- if (length(donut_shape) >= i) donut_shape[i] else "circle"
if (current_donut_shape != "circle") {
# Use polygon donut with pie for non-circular shapes
draw_polygon_donut_pie_node_base(
x, y, node_size[i],
donut_value = donut_val,
donut_color = donut_col,
donut_shape = current_donut_shape,
pie_values = pie_vals,
pie_colors = pie_cols,
pie_default_color = node_fill[i],
inner_ratio = donut_inner_ratios[i],
bg_color = donut_bg_colors[i],
border.col = node_border_color[i],
border.width = node_border_width[i],
pie_border.width = pie_border_width,
donut_border.width = donut_border_width
)
} else {
# Use circular donut with pie (default)
draw_donut_pie_node_base(
x, y, node_size[i],
donut_value = donut_val,
donut_color = donut_col,
pie_values = pie_vals,
pie_colors = pie_cols,
pie_default_color = node_fill[i],
inner_ratio = donut_inner_ratios[i],
bg_color = donut_bg_colors[i],
border.col = node_border_color[i],
border.width = node_border_width[i],
pie_border.width = pie_border_width,
donut_border.width = donut_border_width
)
}
}
} else if (has_donut) {
# Donut only
# Get donut value, defaulting to 1.0 if node_shape is "donut" but no explicit value
donut_vals <- if (!is.null(donut_values) && length(donut_values) >= i &&
!is.null(donut_values[[i]]) && length(donut_values[[i]]) > 0 && !anyNA(donut_values[[i]])) {
donut_values[[i]]
} else {
1.0 # Default to full ring when node_shape is "donut" but no explicit value
}
donut_cols <- if (!is.null(donut_colors) && length(donut_colors) >= i) donut_colors[[i]] else NULL
# Get per-node donut shape (donut_shape is now a vector)
current_donut_shape <- if (length(donut_shape) >= i) donut_shape[i] else "circle"
# Determine effective donut border color (use donut_border_color[i] if set, else node_border_color)
effective_donut_border_col <- if (!is.null(donut_border_color) && length(donut_border_color) >= i) {
donut_border_color[i]
} else {
node_border_color[i]
}
# Get per-node outer border color (for double border feature)
effective_outer_border_col <- if (!is.null(donut_outer_border_color) && length(donut_outer_border_color) >= i) {
donut_outer_border_color[i]
} else {
NULL
}
# Get per-node border line type
effective_border_lty <- if (length(donut_line_type) >= i) donut_line_type[i] else "solid"
if (current_donut_shape != "circle") {
# Use polygon donut for non-circular shapes
draw_polygon_donut_node_base(
x, y, node_size[i],
values = donut_vals,
colors = donut_cols,
default_color = node_fill[i],
inner_ratio = donut_inner_ratios[i],
bg_color = donut_bg_colors[i],
center_color = node_fill[i],
donut_shape = current_donut_shape,
border.col = effective_donut_border_col,
border.width = node_border_width[i],
donut_border.width = donut_border_width,
outer_border.col = effective_outer_border_col,
border.lty = effective_border_lty,
show_value = donut_show_values[i],
value_cex = donut_value_sizes[i],
value_col = donut_value_colors[i],
value_fontface = donut_value_fontfaces[i],
value_fontfamily = donut_value_fontfamilies[i],
value_digits = donut_value_digits,
value_prefix = donut_value_prefix,
value_suffix = donut_value_suffix
)
} else {
# Use circular donut (default)
draw_donut_node_base(
x, y, node_size[i],
values = donut_vals,
colors = donut_cols,
default_color = node_fill[i],
inner_ratio = donut_inner_ratios[i],
bg_color = donut_bg_colors[i],
center_color = node_fill[i],
border.col = effective_donut_border_col,
border.width = node_border_width[i],
donut_border.width = donut_border_width,
outer_border.col = effective_outer_border_col,
border.lty = effective_border_lty,
show_value = donut_show_values[i],
value_cex = donut_value_sizes[i],
value_col = donut_value_colors[i],
value_fontface = donut_value_fontfaces[i],
value_fontfamily = donut_value_fontfamilies[i],
value_digits = donut_value_digits,
value_prefix = donut_value_prefix,
value_suffix = donut_value_suffix
)
}
} else if (has_pie) {
# Pie only
pie_vals <- pie_values[[i]]
pie_cols <- if (!is.null(pie_colors) && length(pie_colors) >= i) pie_colors[[i]] else NULL
draw_pie_node_base(
x, y, node_size[i],
values = pie_vals,
colors = pie_cols,
default_color = node_fill[i],
border.col = node_border_color[i],
border.width = node_border_width[i],
pie_border.width = pie_border_width
)
} else {
# Standard node
if (use_pch && node_shape[i] == "circle") {
# Fast point-based rendering
graphics::points(x, y, pch = 21, cex = node_size[i] * 20,
bg = node_fill[i], col = node_border_color[i], lwd = node_border_width[i])
} else {
draw_node_base(
x, y, node_size[i], node_size2[i],
shape = node_shape[i],
col = node_fill[i],
border.col = node_border_color[i],
border.width = node_border_width[i]
)
}
}
}
# Render labels
if (!is.null(labels)) {
# Vectorize label parameters (strict: length 1 or n)
label_angles <- expand_param(label_angle, n, "label_angle")
label_positions <- expand_param(label_position, n, "label_position")
label_fontfaces <- expand_param(label_fontface, n, "label_fontface")
label_fontfamilies <- expand_param(label_fontfamily, n, "label_fontfamily")
label_hjusts <- expand_param(label_hjust, n, "label_hjust")
label_vjusts <- expand_param(label_vjust, n, "label_vjust")
for (i in seq_len(n)) {
if (!is.null(labels[i]) && !is.na(labels[i]) && labels[i] != "") {
lx <- layout[i, 1]
ly <- layout[i, 2]
# Adjust position based on per-node label_position
offset <- node_size[i] * 1.2
if (label_positions[i] == "above") {
ly <- ly + offset
} else if (label_positions[i] == "below") {
ly <- ly - offset
} else if (label_positions[i] == "left") {
lx <- lx - offset
} else if (label_positions[i] == "right") {
lx <- lx + offset
}
# "center" - no offset
# Convert fontface string to numeric (per-node)
fontface_num <- fontface_to_numeric(label_fontfaces[i])
draw_node_label_base(
lx, ly,
label = labels[i],
cex = label_size[i],
col = label_color[i],
font = fontface_num,
family = label_fontfamilies[i],
hjust = label_hjusts[i],
vjust = label_vjusts[i],
srt = label_angles[i]
)
}
}
}
}
#' Render Legend for splot
#'
#' Renders a comprehensive legend showing node groups, edge weight colors,
#' and optionally node sizes.
#'
#' @param groups Group assignments for nodes.
#' @param node_names Names for legend entries.
#' @param nodes Node data frame.
#' @param node_colors Vector of node colors.
#' @param position Legend position.
#' @param cex Text size.
#' @param show_edge_colors Logical: show positive/negative edge color legend?
#' @param positive_color Positive edge color.
#' @param negative_color Negative edge color.
#' @param has_pos_edges Logical: are there positive weighted edges?
#' @param has_neg_edges Logical: are there negative weighted edges?
#' @param show_node_sizes Logical: show node size legend?
#' @param node_size Vector of node sizes.
#' @keywords internal
render_legend_splot <- function(groups, node_names, nodes, node_colors,
position = "topright", cex = 0.8,
show_edge_colors = FALSE,
positive_color = "#2E7D32", negative_color = "#C62828",
has_pos_edges = FALSE, has_neg_edges = FALSE,
show_node_sizes = FALSE, node_size = NULL) {
n <- length(node_colors)
# Collect all legend components
legend_labels <- character(0)
legend_colors <- character(0)
legend_pch <- integer(0)
legend_lty <- integer(0)
legend_lwd <- numeric(0)
legend_pt_cex <- numeric(0)
# =========================================
# 1. NODE GROUPS (filled squares)
# =========================================
if (!is.null(groups)) {
unique_groups <- unique(groups)
# Get color for each group (first node of that group)
group_colors <- sapply(unique_groups, function(g) {
idx <- which(groups == g)[1]
node_colors[idx]
})
group_labels <- if (!is.null(node_names)) {
sapply(unique_groups, function(g) {
idx <- which(groups == g)[1]
if (length(node_names) >= idx) node_names[idx] else as.character(g)
})
} else {
as.character(unique_groups)
}
legend_labels <- c(legend_labels, group_labels)
legend_colors <- c(legend_colors, group_colors)
legend_pch <- c(legend_pch, rep(22, length(unique_groups))) # filled square
legend_lty <- c(legend_lty, rep(NA, length(unique_groups)))
legend_lwd <- c(legend_lwd, rep(NA, length(unique_groups)))
legend_pt_cex <- c(legend_pt_cex, rep(2, length(unique_groups)))
}
# =========================================
# 2. EDGE COLORS (lines)
# =========================================
if (show_edge_colors && (has_pos_edges || has_neg_edges)) {
# Add separator if we have groups
if (length(legend_labels) > 0) {
legend_labels <- c(legend_labels, "")
legend_colors <- c(legend_colors, NA)
legend_pch <- c(legend_pch, NA)
legend_lty <- c(legend_lty, 0)
legend_lwd <- c(legend_lwd, NA)
legend_pt_cex <- c(legend_pt_cex, NA)
}
if (has_pos_edges) {
legend_labels <- c(legend_labels, "Positive")
legend_colors <- c(legend_colors, positive_color)
legend_pch <- c(legend_pch, NA)
legend_lty <- c(legend_lty, 1)
legend_lwd <- c(legend_lwd, 2)
legend_pt_cex <- c(legend_pt_cex, NA)
}
if (has_neg_edges) {
legend_labels <- c(legend_labels, "Negative")
legend_colors <- c(legend_colors, negative_color)
legend_pch <- c(legend_pch, NA)
legend_lty <- c(legend_lty, 1)
legend_lwd <- c(legend_lwd, 2)
legend_pt_cex <- c(legend_pt_cex, NA)
}
}
# =========================================
# 3. NODE SIZES (circles of different sizes)
# =========================================
if (show_node_sizes && !is.null(node_size) && length(unique(node_size)) > 1) {
# Add separator
if (length(legend_labels) > 0) {
legend_labels <- c(legend_labels, "")
legend_colors <- c(legend_colors, NA)
legend_pch <- c(legend_pch, NA)
legend_lty <- c(legend_lty, 0)
legend_lwd <- c(legend_lwd, NA)
legend_pt_cex <- c(legend_pt_cex, NA)
}
# Show min, median, max sizes
size_range <- range(node_size)
size_med <- median(node_size)
size_vals <- c(size_range[1], size_med, size_range[2])
size_labels <- c(
paste0("Small (", round(size_range[1], 1), ")"),
paste0("Medium (", round(size_med, 1), ")"),
paste0("Large (", round(size_range[2], 1), ")")
)
# Scale for legend display
scale_factor <- 15 # Adjust for visual appearance
size_cex <- size_vals * scale_factor
legend_labels <- c(legend_labels, size_labels)
legend_colors <- c(legend_colors, rep("gray50", 3))
legend_pch <- c(legend_pch, rep(21, 3)) # filled circle
legend_lty <- c(legend_lty, rep(NA, 3))
legend_lwd <- c(legend_lwd, rep(NA, 3))
legend_pt_cex <- c(legend_pt_cex, size_cex)
}
# =========================================
# Draw legend if we have entries
# =========================================
if (length(legend_labels) == 0) {
return(invisible())
}
# Replace NA colors with transparent for proper rendering
legend_colors[is.na(legend_colors)] <- "transparent"
# Determine which elements to show
has_points <- any(!is.na(legend_pch) & legend_pch > 0)
has_lines <- any(!is.na(legend_lty) & legend_lty > 0)
# Build legend
graphics::legend(
position,
legend = legend_labels,
col = legend_colors,
pch = if (has_points) legend_pch else NULL,
lty = if (has_lines) legend_lty else NULL,
lwd = if (has_lines) legend_lwd else NULL,
pt.cex = if (has_points) legend_pt_cex else NULL,
pt.bg = if (has_points) legend_colors else NULL,
bty = "o",
bg = "white",
cex = cex,
seg.len = 1.5
)
}
#' Collect user-explicit args for dispatch forwarding
#'
#' Merges evaluated user args and dots, optionally starting from a base list.
#' Used by splot() to forward all user-provided parameters across dispatch
#' boundaries (bootstrap, permutation, cluster, etc.).
#'
#' @param user_args Named list of evaluated user-explicit args.
#' @param dots The ... args (already evaluated).
#' @param skip Character vector of arg names to exclude (default "x").
#' @param base Optional base list to merge on top of (e.g., tna_params).
#' @return Named list of args ready for do.call().
#' @noRd
.collect_dispatch_args <- function(user_args, dots, skip = "x", base = list()) {
nms <- setdiff(names(user_args), skip)
result <- base
result[nms] <- user_args[nms]
result[names(dots)] <- dots
result
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.