fdg_img/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::error::Error;
4
5use fdg_sim::{
6    glam::Vec3,
7    petgraph::{
8        visit::{EdgeRef, IntoEdgeReferences},
9        EdgeType, Undirected,
10    },
11    Dimensions, ForceGraph, Simulation, SimulationParameters,
12};
13use plotters::prelude::*;
14
15pub use plotters::style;
16
17#[cfg(feature = "wasm")]
18mod wasm;
19
20#[cfg(feature = "wasm")]
21pub use wasm::*;
22
23/// Parameters for drawing the SVG image.
24pub struct Settings<N, E, Ty = Undirected> {
25    /// Simulation Parameters
26    pub sim_parameters: SimulationParameters<N, E, Ty>,
27    /// Number of times to run the simulation
28    pub iterations: usize,
29    /// "Granularity of simulation updates"
30    pub dt: f32,
31    /// The radius of the nodes
32    pub node_size: u32,
33    /// RGBA color of the nodes
34    pub node_color: RGBAColor,
35    /// Width of the edge lines
36    pub edge_size: u32,
37    /// RGBA color of the edge lines
38    pub edge_color: RGBAColor,
39    /// RGBA background color
40    pub background_color: RGBAColor,
41    /// If true, the simulation will be printed on each
42    pub print_progress: bool,
43    /// If supplied, the names of nodes will be written
44    pub text_style: Option<TextStyle<'static>>,
45}
46
47impl<N, E, Ty: EdgeType> Default for Settings<N, E, Ty> {
48    fn default() -> Self {
49        Self {
50            sim_parameters: SimulationParameters::default(),
51            iterations: 2000,
52            dt: 0.035,
53            node_size: 10,
54            node_color: RGBAColor(0, 0, 0, 1.0),
55            edge_size: 3,
56            edge_color: RGBAColor(255, 0, 0, 1.0),
57            background_color: RGBAColor(255, 255, 255, 1.0),
58            print_progress: false,
59            text_style: None,
60        }
61    }
62}
63
64/// Generate an image from a graph and a force.
65pub fn gen_image<N, E, Ty: EdgeType>(
66    graph: ForceGraph<N, E, Ty>,
67    settings: Option<Settings<N, E, Ty>>,
68) -> Result<String, Box<dyn Error>> {
69    // set up the simulation and settings
70    let settings = settings.unwrap_or_default();
71    let mut sim = Simulation::from_graph(graph, settings.sim_parameters);
72    sim.parameters_mut().dimensions = Dimensions::Two;
73
74    // get the nodes to their x/y positions through the simulation.
75    for i in 0..settings.iterations {
76        if settings.print_progress && i % 10 == 0 {
77            println!("{}/{}", i, settings.iterations);
78        }
79        sim.update(settings.dt);
80    }
81
82    // get the size of the graph (avg of width and height to try to account for oddly shaped graphs)
83    let (graph_x, graph_y): (f32, f32) = {
84        let mut top = 0.0;
85        let mut bottom = 0.0;
86        let mut left = 0.0;
87        let mut right = 0.0;
88
89        for node in sim.get_graph().node_weights() {
90            let loc = node.location;
91
92            // add text width to the rightmost point to make sure text doesn't get cut off
93            let rightmost = match settings.text_style.clone() {
94                Some(ts) => {
95                    loc.x
96                        + ts.font
97                            .box_size(&node.name)
98                            .ok()
99                            .map(|x| x.0 as f32)
100                            .unwrap_or(0.0)
101                }
102                None => loc.x,
103            };
104
105            if rightmost > right {
106                right = rightmost;
107            }
108
109            if loc.x < left {
110                left = loc.x;
111            }
112
113            if loc.y > top {
114                top = loc.y
115            }
116
117            if loc.y < bottom {
118                bottom = loc.y;
119            }
120        }
121
122        (
123            ((right + settings.node_size as f32) - (left - settings.node_size as f32)),
124            ((top + settings.node_size as f32) - (bottom - settings.node_size as f32)),
125        )
126    };
127
128    let image_scale = 1.5;
129    let (image_x, image_y) = (
130        (graph_x * image_scale) as u32,
131        (graph_y * image_scale) as u32,
132    );
133
134    // translate all points by graph average to (0,0)
135    let mut location_sum = Vec3::ZERO;
136    for node in sim.get_graph().node_weights() {
137        location_sum += node.location;
138    }
139
140    let avg_vec = location_sum / sim.get_graph().node_count() as f32;
141    for node in sim.get_graph_mut().node_weights_mut() {
142        node.location -= avg_vec;
143    }
144
145    // translate all the points over into the image coordinate space
146    for node in sim.get_graph_mut().node_weights_mut() {
147        node.location.x += (image_x / 2) as f32;
148        node.location.y += (image_y / 2) as f32;
149    }
150
151    // SVG string buffer
152    let mut buffer = String::new();
153
154    // Plotters (who makes it very easy to make SVGs) backend
155    let backend = SVGBackend::with_string(&mut buffer, (image_x, image_y)).into_drawing_area();
156
157    // fill in the background
158    backend.fill(&settings.background_color).unwrap();
159
160    // draw all the edges
161    for edge in sim.get_graph().edge_references() {
162        let source = &sim.get_graph()[edge.source()].location;
163        let target = &sim.get_graph()[edge.target()].location;
164
165        backend.draw(&PathElement::new(
166            vec![
167                (source.x as i32, source.y as i32),
168                (target.x as i32, target.y as i32),
169            ],
170            ShapeStyle {
171                color: settings.edge_color,
172                filled: true,
173                stroke_width: settings.edge_size,
174            },
175        ))?;
176    }
177
178    // draw all the nodes
179    for node in sim.get_graph().node_weights() {
180        backend.draw(&Circle::new(
181            (node.location.x as i32, node.location.y as i32),
182            settings.node_size,
183            ShapeStyle {
184                color: settings.node_color,
185                filled: true,
186                stroke_width: 1,
187            },
188        ))?;
189    }
190
191    // draw the text by nodes
192    if let Some(text_style) = settings.text_style {
193        for node in sim.get_graph().node_weights() {
194            let pos = (
195                node.location.x as i32 + (text_style.font.get_size() / 2.0) as i32,
196                node.location.y as i32,
197            );
198            backend.draw_text(node.name.as_str(), &text_style, pos)?;
199        }
200    }
201
202    drop(backend);
203
204    Ok(buffer)
205}