Generate more realistic graph, render to SVG
This commit is contained in:
parent
0e85a3ddb1
commit
f2798a14d4
7 changed files with 366 additions and 131 deletions
197
Cargo.lock
generated
197
Cargo.lock
generated
|
|
@ -2,15 +2,6 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argp"
|
||||
version = "0.4.0"
|
||||
|
|
@ -33,19 +24,10 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "assert_approx_eq"
|
||||
version = "1.1.0"
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c07dab4369547dbe5114677b33fbbf724971019f3818172d59a97a61c774ffd"
|
||||
|
||||
[[package]]
|
||||
name = "assert_unordered"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74323b7881323eb351134e08ee5331594826789557afef8e309baf481b2264"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
]
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
|
|
@ -53,6 +35,12 @@ version = "2.9.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.3"
|
||||
|
|
@ -84,18 +72,25 @@ version = "0.8.21"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "doc-comment"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "forceatlas2"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c192796ef315da1fb02da466c03f2a16948d55fe1661925a002a9d1e41c571f"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"num-traits",
|
||||
"parking_lot",
|
||||
"rand",
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.24"
|
||||
|
|
@ -116,40 +111,21 @@ dependencies = [
|
|||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphrs"
|
||||
version = "0.11.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f62f4b6f1f9ec5757abcebef6a96983056b54109d5346aa6691c67a0e39ef32"
|
||||
dependencies = [
|
||||
"assert_approx_eq",
|
||||
"assert_unordered",
|
||||
"doc-comment",
|
||||
"itertools",
|
||||
"nohash",
|
||||
"quick-xml",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"rayon",
|
||||
"serde",
|
||||
"sorted-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
|
|
@ -157,10 +133,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "nohash"
|
||||
version = "0.2.0"
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0f889fb66f7acdf83442c35775764b51fed3c606ab9cee51500dbde2cf528ca"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
|
|
@ -192,15 +194,6 @@ dependencies = [
|
|||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
|
|
@ -260,49 +253,35 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reticulum-synthesis"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"argp",
|
||||
"graphrs",
|
||||
"forceatlas2",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sorted-vec"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f58d7b0190c7f12df7e8be6b79767a0836059159811b869d5ab55721fe14d0"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
|
|
@ -340,26 +319,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
|
|
|
|||
|
|
@ -7,9 +7,16 @@ authors = ["tuxmain <tuxmain@zettascript.org>"]
|
|||
repository = "https://git.txmn.tk/tuxmain/reticulum-synthesis"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[features]
|
||||
default = ["render"]
|
||||
|
||||
# Render graph to SVG
|
||||
render = ["dep:forceatlas2"]
|
||||
|
||||
[dependencies]
|
||||
argp = "0.4.0"
|
||||
graphrs = "0.11.13"
|
||||
forceatlas2 = { version = "0.8.0", optional = true }
|
||||
rand = "0.8.5"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ A simple program to generate and run random topologies of [Reticulum](https://re
|
|||
|
||||
The purpose is to test routing and applications easily.
|
||||
|
||||

|
||||
|
||||
You can contact `tuxmain` on the Reticulum Matrix channel for any request.
|
||||
|
||||
Development status: Early development, just for fun, to tinker with Reticulum and learn about mesh networks.
|
||||
|
|
@ -31,6 +33,10 @@ Option `--gen` (or `-g`) generates the network graph and creates the directories
|
|||
|
||||
Option `--run` (or `-r`) runs the generated nodes using `rnsd`. (tries to run for any subdirectory in the directory passed to `-d`)
|
||||
|
||||
Tune the graph density with the `-p <float>` option that accepts an edge probability number between 0 and 1.
|
||||
|
||||
When generating a graph, option `-R <path>` renders the graph to an SVG image. (may take some time to compute, depending on the number of nodes)
|
||||
|
||||
The network can then be tested using RNS tools: (from reticulum repository)
|
||||
|
||||
```
|
||||
|
|
|
|||
11
graph.svg
Normal file
11
graph.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 18 KiB |
35
src/generator.rs
Normal file
35
src/generator.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use rand::Rng;
|
||||
|
||||
fn get_neighbors(node: usize, edges: &[(usize, usize)]) -> impl Iterator<Item = usize> {
|
||||
edges.iter().filter_map(move |(n1, n2)| {
|
||||
if *n1 == node {
|
||||
Some(*n2)
|
||||
} else if *n2 == node {
|
||||
Some(*n1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate a random social graph
|
||||
pub fn generate(n: usize, proba: f32, mut rng: impl Rng) -> Vec<(usize, usize)> {
|
||||
let mut edges = Vec::new();
|
||||
for friend in 1..n {
|
||||
// Select a random anchor node in the graph.
|
||||
let anchor = rng.gen_range(0..friend);
|
||||
// The anchor is calling a friend to join the graph.
|
||||
let mut new_friends = vec![anchor];
|
||||
|
||||
// Each other anchor's friend is also a friend of the new one with some probability.
|
||||
for neighbor in get_neighbors(anchor, &edges) {
|
||||
if rng.r#gen::<f32>() < proba {
|
||||
new_friends.push(neighbor);
|
||||
}
|
||||
}
|
||||
for new_friend in new_friends {
|
||||
edges.push((new_friend, friend));
|
||||
}
|
||||
}
|
||||
edges
|
||||
}
|
||||
44
src/main.rs
44
src/main.rs
|
|
@ -1,3 +1,6 @@
|
|||
mod generator;
|
||||
mod render;
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, hash_map::Entry},
|
||||
io::Write,
|
||||
|
|
@ -19,6 +22,10 @@ struct Cli {
|
|||
/// Probability of edge
|
||||
#[argp(option, short = 'p', default = "0.5")]
|
||||
proba: f32,
|
||||
/// Output SVG image of the graph (only with --gen)
|
||||
#[cfg(feature = "render")]
|
||||
#[argp(option, short = 'R')]
|
||||
render: Option<String>,
|
||||
/// Start the Reticulum nodes
|
||||
#[argp(switch, short = 'r')]
|
||||
run: bool,
|
||||
|
|
@ -28,17 +35,22 @@ fn main() {
|
|||
let cli: Cli = argp::parse_args_or_exit(argp::DEFAULT);
|
||||
|
||||
if cli.generate {
|
||||
let graph = graphrs::generators::random::fast_gnp_random_graph(
|
||||
cli.nodes as _,
|
||||
cli.proba as _,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
assert!(
|
||||
cli.proba >= 0.0 && cli.proba <= 1.0,
|
||||
"Probability should be between 0 and 1."
|
||||
);
|
||||
let edges = generator::generate(cli.nodes, cli.proba, &mut rng);
|
||||
|
||||
let mut ports = HashMap::new();
|
||||
let mut port: u16 = 42000;
|
||||
assert!(graph.number_of_edges() < 65535 - port as usize);
|
||||
assert!(edges.len() < 65535 - port as usize);
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
if let Some(out_svg) = &cli.render {
|
||||
render::render_to_svg(cli.nodes, &edges, out_svg, &mut rng);
|
||||
}
|
||||
|
||||
let dir = PathBuf::from(&cli.dir);
|
||||
std::fs::DirBuilder::new()
|
||||
|
|
@ -76,11 +88,17 @@ loglevel = 4
|
|||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
for edge in graph.get_edges_for_node(node as _).unwrap() {
|
||||
let key = if edge.u < edge.v {
|
||||
(edge.u, edge.v)
|
||||
for edge in edges.iter().filter_map(|(n1, n2)| {
|
||||
if *n1 == node || *n2 == node {
|
||||
Some((*n1, *n2))
|
||||
} else {
|
||||
(edge.v, edge.u)
|
||||
None
|
||||
}
|
||||
}) {
|
||||
let key = if edge.0 < edge.1 {
|
||||
(edge.0, edge.1)
|
||||
} else {
|
||||
(edge.1, edge.0)
|
||||
};
|
||||
let mut edge_ports = (port, port + 1);
|
||||
match ports.entry(key) {
|
||||
|
|
@ -103,7 +121,7 @@ listen_port = {}
|
|||
forward_ip = 127.0.0.1
|
||||
forward_port = {}
|
||||
",
|
||||
edge.u, edge.v, edge_ports.0, edge_ports.1
|
||||
edge.0, edge.1, edge_ports.0, edge_ports.1
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
|
|
|
|||
195
src/render.rs
Normal file
195
src/render.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
use forceatlas2::{AbstractNode, Layout};
|
||||
use rand::Rng;
|
||||
use std::io::Write;
|
||||
|
||||
pub fn render_to_svg(n: usize, edges: &[(usize, usize)], path: &str, mut rng: impl Rng) {
|
||||
let width = 512;
|
||||
let height = 512;
|
||||
let node_radius = 12.0;
|
||||
|
||||
let mut layout = Layout::<f32, 2>::from_abstract(
|
||||
forceatlas2::Settings {
|
||||
theta: 0.5,
|
||||
ka: 1.0,
|
||||
kg: 1.0,
|
||||
kr: 1.0,
|
||||
lin_log: false,
|
||||
prevent_overlapping: None,
|
||||
speed: 0.1,
|
||||
strong_gravity: false,
|
||||
},
|
||||
(0..n).map(|i| {
|
||||
(
|
||||
i,
|
||||
AbstractNode {
|
||||
size: 1.0,
|
||||
mass: 1.0,
|
||||
},
|
||||
)
|
||||
}),
|
||||
edges.iter().map(|edge| ((edge.0, edge.1), 1.0)).collect(),
|
||||
&mut rng,
|
||||
);
|
||||
|
||||
// First run with normal forces to grossly form communities
|
||||
for _ in 0..1000 {
|
||||
layout.iteration();
|
||||
}
|
||||
|
||||
// Now collapse into a compact disk using strong gravity
|
||||
layout.set_settings(forceatlas2::Settings {
|
||||
theta: 0.1,
|
||||
ka: 1.0,
|
||||
kg: 1.0,
|
||||
kr: 1.0,
|
||||
lin_log: false,
|
||||
prevent_overlapping: None,
|
||||
speed: 0.01,
|
||||
strong_gravity: true,
|
||||
});
|
||||
|
||||
for _ in 0..1000 {
|
||||
layout.iteration();
|
||||
}
|
||||
|
||||
let mut x_min = 0.0;
|
||||
let mut x_max = 0.0;
|
||||
let mut y_min = 0.0;
|
||||
let mut y_max = 0.0;
|
||||
for node in layout.nodes.iter() {
|
||||
if node.pos.x() < x_min {
|
||||
x_min = node.pos.x();
|
||||
}
|
||||
if node.pos.x() > x_max {
|
||||
x_max = node.pos.x();
|
||||
}
|
||||
if node.pos.y() < y_min {
|
||||
y_min = node.pos.y();
|
||||
}
|
||||
if node.pos.y() > y_max {
|
||||
y_max = node.pos.y();
|
||||
}
|
||||
}
|
||||
let x_range = x_max - x_min;
|
||||
let y_range = y_max - y_min;
|
||||
let x_factor = (width as f32 - node_radius * 2.0) / x_range;
|
||||
let y_factor = (height as f32 - node_radius * 2.0) / y_range;
|
||||
|
||||
let node_size = node_radius / x_factor.min(y_factor);
|
||||
for node in layout.nodes.iter_mut() {
|
||||
node.size = node_size;
|
||||
}
|
||||
|
||||
// Finally remove overlapping
|
||||
layout.set_settings(forceatlas2::Settings {
|
||||
theta: 0.05,
|
||||
ka: 1.0,
|
||||
kg: 1.0,
|
||||
kr: 1.0,
|
||||
lin_log: false,
|
||||
prevent_overlapping: Some(100.0),
|
||||
speed: 0.01,
|
||||
strong_gravity: true,
|
||||
});
|
||||
|
||||
for _ in 0..100 {
|
||||
layout.iteration();
|
||||
}
|
||||
|
||||
let mut x_min = 0.0;
|
||||
let mut x_max = 0.0;
|
||||
let mut y_min = 0.0;
|
||||
let mut y_max = 0.0;
|
||||
for node in layout.nodes.iter() {
|
||||
if node.pos.x() < x_min {
|
||||
x_min = node.pos.x();
|
||||
}
|
||||
if node.pos.x() > x_max {
|
||||
x_max = node.pos.x();
|
||||
}
|
||||
if node.pos.y() < y_min {
|
||||
y_min = node.pos.y();
|
||||
}
|
||||
if node.pos.y() > y_max {
|
||||
y_max = node.pos.y();
|
||||
}
|
||||
}
|
||||
let x_range = x_max - x_min;
|
||||
let y_range = y_max - y_min;
|
||||
let x_factor = (width as f32 - node_radius * 2.0) / x_range;
|
||||
let y_factor = (height as f32 - node_radius * 2.0) / y_range;
|
||||
let x_mid = (x_min + x_max) * 0.5;
|
||||
let y_mid = (y_min + y_max) * 0.5;
|
||||
|
||||
let mut outfile = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(path)
|
||||
.unwrap();
|
||||
outfile
|
||||
.write_all(
|
||||
format!(
|
||||
r#"<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="{}" height="{}" viewBox="{} {} {} {}">
|
||||
<title>Network graph</title>
|
||||
<style type="text/css">
|
||||
.node-label {{
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
font-size: 12px;
|
||||
}}
|
||||
</style>
|
||||
"#,
|
||||
width,
|
||||
height,
|
||||
width as f32 * -0.5,
|
||||
height as f32 * -0.5,
|
||||
width,
|
||||
height
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for ((n1, n2), _weight) in layout.edges {
|
||||
let p1 = layout.nodes[n1].pos;
|
||||
let p2 = layout.nodes[n2].pos;
|
||||
outfile
|
||||
.write_all(
|
||||
format!(
|
||||
r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="black"/>"#,
|
||||
(p1.x() - x_mid) * x_factor,
|
||||
(p1.y() - y_mid) * y_factor,
|
||||
(p2.x() - x_mid) * x_factor,
|
||||
(p2.y() - y_mid) * y_factor
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
for (i, node) in layout.nodes.iter().enumerate() {
|
||||
let x = (node.pos.x() - x_mid) * x_factor;
|
||||
let y = (node.pos.y() - y_mid) * y_factor;
|
||||
outfile
|
||||
.write_all(
|
||||
format!(
|
||||
r#"<circle cx="{}" cy="{}" r="{}" stroke="black" fill="white"/>"#,
|
||||
x, y, node_radius
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
outfile
|
||||
.write_all(
|
||||
format!(
|
||||
r#"<text x="{}" y="{}" class="node-label">{}</text>"#,
|
||||
x, y, i
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
outfile.write_all(br#"</svg>"#).unwrap();
|
||||
outfile.flush().unwrap();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue