use kuva::backend::svg::SvgBackend;
use kuva::plot::network::{NetworkLayout, NetworkPlot, NodeShape};
use kuva::render::{layout::Layout, plots::Plot, render::render_multiple};
#[test]
fn network_basic() {
let net = NetworkPlot::new()
.with_edge("A", "B", 1.0)
.with_edge("A", "C", 1.0)
.with_edge("B", "C", 1.0)
.with_edge("C", "D", 1.0)
.with_labels();
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Basic Network");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/network_basic.svg", svg).unwrap();
}
#[test]
fn network_directed() {
let net = NetworkPlot::new()
.with_edge("A", "B", 1.0)
.with_edge("A", "C", 2.0)
.with_edge("B", "C", 1.5)
.with_edge("C", "D", 3.0)
.with_edge("D", "A", 0.5)
.with_directed()
.with_labels();
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Directed Network");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/network_directed.svg", svg).unwrap();
}
#[test]
fn network_circle_layout() {
let net = NetworkPlot::new()
.with_edges([
("A", "B", 1.0),
("B", "C", 1.0),
("C", "D", 1.0),
("D", "E", 1.0),
("E", "F", 1.0),
("F", "A", 1.0),
("A", "D", 0.5),
("B", "E", 0.5),
("C", "F", 0.5),
])
.with_layout(NetworkLayout::Circle)
.with_labels();
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Circle Layout");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/network_circle.svg", svg).unwrap();
}
#[test]
fn network_self_loop() {
let net = NetworkPlot::new()
.with_edge("A", "B", 1.0)
.with_edge("B", "C", 1.0)
.with_edge("C", "C", 1.0) .with_edge("C", "A", 1.0)
.with_directed()
.with_labels();
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Self-Loop");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/network_self_loop.svg", svg).unwrap();
}
#[test]
fn network_matrix() {
let matrix = vec![
vec![0.0, 1.0, 1.0, 0.0],
vec![1.0, 0.0, 1.0, 1.0],
vec![1.0, 1.0, 0.0, 1.0],
vec![0.0, 1.0, 1.0, 0.0],
];
let net = NetworkPlot::new()
.with_matrix(matrix, ["Alpha", "Beta", "Gamma", "Delta"])
.with_labels();
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("From Adjacency Matrix");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/network_matrix.svg", svg).unwrap();
}
#[test]
fn network_groups_legend() {
let net = NetworkPlot::new()
.with_edge("A", "B", 1.0)
.with_edge("A", "C", 1.0)
.with_edge("B", "D", 1.0)
.with_edge("C", "D", 1.0)
.with_node_group("A", "Input")
.with_node_group("B", "Hidden")
.with_node_group("C", "Hidden")
.with_node_group("D", "Output")
.with_labels()
.with_legend("Layer");
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Grouped Network");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/network_groups_legend.svg", svg).unwrap();
}
#[test]
fn network_weighted() {
let net = NetworkPlot::new()
.with_edge("A", "B", 1.0)
.with_edge("A", "C", 5.0)
.with_edge("B", "C", 2.0)
.with_edge("C", "D", 10.0)
.with_edge("D", "E", 0.5)
.with_labels();
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Weighted Edges");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/network_weighted.svg", svg).unwrap();
}
#[test]
fn network_node_sizes() {
let net = NetworkPlot::new()
.with_edge("Hub", "A", 1.0)
.with_edge("Hub", "B", 1.0)
.with_edge("Hub", "C", 1.0)
.with_edge("Hub", "D", 1.0)
.with_edge("A", "B", 1.0)
.with_node_size("Hub", 20.0)
.with_node_size("A", 12.0)
.with_node_size("B", 8.0)
.with_node_size("C", 5.0)
.with_node_size("D", 3.0)
.with_labels();
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Variable Node Sizes");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/network_node_sizes.svg", svg).unwrap();
}
#[test]
fn network_disconnected() {
let net = NetworkPlot::new()
.with_edge("A1", "A2", 1.0)
.with_edge("A2", "A3", 1.0)
.with_edge("A3", "A1", 1.0)
.with_edge("B1", "B2", 1.0)
.with_edge("C1", "C2", 1.0)
.with_edge("C1", "C3", 1.0)
.with_edge("C1", "C4", 1.0)
.with_edge("C1", "C5", 1.0)
.with_node_group("A1", "Alpha")
.with_node_group("A2", "Alpha")
.with_node_group("A3", "Alpha")
.with_node_group("B1", "Beta")
.with_node_group("B2", "Beta")
.with_node_group("C1", "Gamma")
.with_node_group("C2", "Gamma")
.with_node_group("C3", "Gamma")
.with_node_group("C4", "Gamma")
.with_node_group("C5", "Gamma")
.with_labels()
.with_legend("Component");
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Disconnected Components");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/network_disconnected.svg", svg).unwrap();
}
#[test]
fn network_pinned_positions() {
let net = NetworkPlot::new()
.with_edge("A", "B", 1.0)
.with_edge("B", "C", 1.0)
.with_edge("C", "A", 1.0)
.with_node_position("A", 0.0, 0.0)
.with_node_position("C", 1.0, 1.0)
.with_labels();
let positions = net.compute_positions();
assert!(
(positions[0].0 - 0.0).abs() < 1e-6,
"pinned node A x should be 0.0"
);
assert!(
(positions[0].1 - 0.0).abs() < 1e-6,
"pinned node A y should be 0.0"
);
assert!(
(positions[2].0 - 1.0).abs() < 1e-6,
"pinned node C x should be 1.0"
);
assert!(
(positions[2].1 - 1.0).abs() < 1e-6,
"pinned node C y should be 1.0"
);
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Pinned Positions");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/network_pinned.svg", svg).unwrap();
}
#[test]
fn network_explicit_node_colors() {
let net = NetworkPlot::new()
.with_edge("A", "B", 1.0)
.with_edge("B", "C", 1.0)
.with_node_color("A", "#e41a1c")
.with_node_color("B", "#377eb8")
.with_node_color("C", "#4daf4a")
.with_node_group("A", "Group1")
.with_node_group("B", "Group1")
.with_node_group("C", "Group2")
.with_labels()
.with_legend("Groups");
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Explicit Colors Override Group");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
assert!(svg.contains("#e41a1c"), "node A should use explicit red");
assert!(svg.contains("#377eb8"), "node B should use explicit blue");
assert!(svg.contains("#4daf4a"), "node C should use explicit green");
std::fs::write("test_outputs/network_explicit_colors.svg", svg).unwrap();
}
#[test]
fn network_single_node_self_loop() {
let net = NetworkPlot::new()
.with_edge("X", "X", 1.0)
.with_directed()
.with_labels();
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Single Node Self-Loop");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
assert!(
svg.contains("<path"),
"single-node self-loop should produce a path"
);
std::fs::write("test_outputs/network_single_self_loop.svg", svg).unwrap();
}
#[test]
fn network_matrix_directed_order_independent() {
let matrix = vec![
vec![0.0, 1.0, 0.0],
vec![0.0, 0.0, 1.0],
vec![1.0, 0.0, 0.0],
];
let net = NetworkPlot::new()
.with_matrix(matrix, ["A", "B", "C"])
.with_directed();
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots);
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
let arrow_count = svg.matches("<path").count();
assert!(
arrow_count >= 3,
"directed matrix should produce at least 3 arrowhead paths, got {arrow_count}"
);
std::fs::write("test_outputs/network_matrix_directed.svg", svg).unwrap();
}
#[test]
fn network_kamada_kawai() {
let net = NetworkPlot::new()
.with_edge("A", "B", 1.0)
.with_edge("B", "C", 1.0)
.with_edge("C", "D", 1.0)
.with_edge("D", "E", 1.0)
.with_edge("E", "A", 1.0)
.with_edge("A", "C", 0.5)
.with_layout(NetworkLayout::KamadaKawai)
.with_labels();
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Kamada-Kawai Layout");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
assert!(svg.contains("<circle"), "KK layout should produce nodes");
std::fs::write("test_outputs/network_kamada_kawai.svg", svg).unwrap();
}
#[test]
fn network_edge_labels() {
let net = NetworkPlot::new()
.with_edge_label("A", "B", 0.95, "0.95")
.with_edge_label("B", "C", 0.72, "0.72")
.with_edge_label("C", "A", 0.45, "0.45")
.with_labels();
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Edge Labels");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
assert!(svg.contains("0.95"), "edge label should appear in SVG");
assert!(svg.contains("0.72"), "edge label should appear in SVG");
std::fs::write("test_outputs/network_edge_labels.svg", svg).unwrap();
}
#[test]
fn network_node_shapes() {
let net = NetworkPlot::new()
.with_edge("Circle", "Square", 1.0)
.with_edge("Square", "Diamond", 1.0)
.with_edge("Diamond", "Triangle", 1.0)
.with_edge("Triangle", "Circle", 1.0)
.with_node_shape("Circle", NodeShape::Circle)
.with_node_shape("Square", NodeShape::Square)
.with_node_shape("Diamond", NodeShape::Diamond)
.with_node_shape("Triangle", NodeShape::Triangle)
.with_layout(NetworkLayout::Circle)
.with_labels();
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Node Shapes");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
assert!(svg.contains("<circle"), "should have circle nodes");
assert!(svg.contains("<rect"), "should have square nodes");
std::fs::write("test_outputs/network_node_shapes.svg", svg).unwrap();
}
#[test]
fn network_antiparallel_curved() {
let net = NetworkPlot::new()
.with_edge_label("A", "B", 2.0, "strong")
.with_edge_label("B", "A", 1.0, "weak")
.with_edge("B", "C", 1.5)
.with_directed()
.with_labels();
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Antiparallel Curved Edges");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
assert!(
svg.contains(" Q "),
"antiparallel edges should use quadratic bezier curves"
);
assert!(svg.contains("strong"), "A→B edge label should appear");
assert!(svg.contains("weak"), "B→A edge label should appear");
std::fs::write("test_outputs/network_antiparallel.svg", svg).unwrap();
}
#[test]
fn network_repel_labels() {
let net = NetworkPlot::new()
.with_edge("Alpha", "Beta", 1.0)
.with_edge("Beta", "Gamma", 1.0)
.with_edge("Gamma", "Delta", 1.0)
.with_edge("Delta", "Epsilon", 1.0)
.with_edge("Epsilon", "Alpha", 1.0)
.with_labels()
.with_repel_labels();
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Label Repulsion");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
assert!(svg.contains("Alpha"), "labels should still be present");
std::fs::write("test_outputs/network_repel_labels.svg", svg).unwrap();
}
#[test]
fn network_dense_clusters() {
let mut net = NetworkPlot::new();
let a: Vec<&str> = vec!["A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8"];
for i in 0..a.len() {
for j in (i + 1)..a.len() {
net = net.with_edge(a[i], a[j], 1.0);
}
}
let b: Vec<&str> = vec!["B1", "B2", "B3", "B4", "B5", "B6"];
for i in 0..b.len() {
for j in (i + 1)..b.len() {
net = net.with_edge(b[i], b[j], 1.0);
}
}
let c: Vec<&str> = vec!["C1", "C2", "C3", "C4", "C5"];
for i in 0..c.len() {
for j in (i + 1)..c.len() {
net = net.with_edge(c[i], c[j], 1.0);
}
}
net = net
.with_edge("A1", "B1", 0.3)
.with_edge("B3", "C1", 0.3)
.with_edge("A5", "C3", 0.2);
for &label in &a {
net = net.with_node_group(label, "Cluster A");
}
for &label in &b {
net = net.with_node_group(label, "Cluster B");
}
for &label in &c {
net = net.with_node_group(label, "Cluster C");
}
net = net.with_labels().with_legend("Cluster");
let plots = vec![Plot::Network(net)];
let layout = Layout::auto_from_plots(&plots).with_title("Dense Clusters with Bridges");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
assert!(svg.contains("A1"), "cluster A nodes present");
assert!(svg.contains("B1"), "cluster B nodes present");
assert!(svg.contains("C1"), "cluster C nodes present");
let group_count = svg.matches("opacity=").count();
assert!(
group_count >= 50,
"dense graph should have many edge groups, got {group_count}"
);
std::fs::write("test_outputs/network_dense_clusters.svg", svg).unwrap();
}
#[test]
fn network_matrix_self_loop_directed() {
let matrix = vec![vec![2.0, 1.0], vec![1.0, 0.0]];
let mut net = NetworkPlot::new()
.with_matrix(matrix, ["A", "B"])
.with_directed();
net.resolve_matrix();
let self_loops: Vec<_> = net.edges.iter().filter(|e| e.source == e.target).collect();
assert_eq!(
self_loops.len(),
1,
"directed matrix with one nonzero diagonal entry should produce exactly one self-loop"
);
assert_eq!(
self_loops[0].source, 0,
"self-loop should be on node A (index 0)"
);
assert!(
(self_loops[0].weight - 2.0).abs() < 1e-9,
"self-loop weight should equal diagonal value"
);
}
#[test]
fn network_matrix_self_loop_undirected() {
let matrix = vec![
vec![5.0, 1.0, 1.0],
vec![1.0, 3.0, 1.0],
vec![1.0, 1.0, 7.0],
];
let mut net = NetworkPlot::new().with_matrix(matrix, ["A", "B", "C"]);
net.resolve_matrix();
let self_loops: Vec<_> = net.edges.iter().filter(|e| e.source == e.target).collect();
assert_eq!(
self_loops.len(),
0,
"undirected matrix should produce no self-loops from diagonal"
);
assert_eq!(
net.edges.len(),
3,
"undirected 3-node fully-connected matrix should have 3 edges"
);
}