Skip to main content

ggplot_rs/geom/
network.rs

1//! Network graph: `geom_network`. Nodes from an edge list laid out by
2//! [`StatNetwork`] (force-directed by default); edges as lines whose width
3//! follows the weight (with arrowheads when directed), nodes sized by
4//! degree, optional labels.
5
6use crate::aes::Aesthetic;
7use crate::coord::Coord;
8use crate::data::{DataFrame, Value};
9use crate::position::identity::PositionIdentity;
10use crate::position::Position;
11use crate::render::backend::{
12    DrawBackend, FontFace, LineStyle, Linetype, PointStyle, TextAnchor, TextStyle,
13};
14use crate::render::RenderError;
15use crate::scale::ScaleSet;
16use crate::stat::network::{NetworkLayout, StatNetwork};
17use crate::stat::Stat;
18use crate::theme::Theme;
19
20use super::arrow::{arrow, draw_heads, heads, ArrowType};
21use super::{Geom, GeomParams};
22
23/// Network graph. Map `from`, `to` and optionally `weight`.
24pub struct GeomNetwork {
25    pub layout: NetworkLayout,
26    /// Draw arrowheads at the target end of each edge.
27    pub directed: bool,
28    /// Colour nodes by connected component (through the fill scale).
29    pub color_components: bool,
30    /// Node fill when not coloured by component.
31    pub fill: (u8, u8, u8),
32    /// Node radius range in pixels (by √degree).
33    pub node_size: (f64, f64),
34    pub edge_color: (u8, u8, u8),
35    /// Edge width range in pixels (by weight).
36    pub edge_width: (f64, f64),
37    pub edge_alpha: f64,
38    pub labels: bool,
39    pub label_size: f64,
40    pub label_color: (u8, u8, u8),
41}
42
43impl Default for GeomNetwork {
44    fn default() -> Self {
45        GeomNetwork {
46            layout: NetworkLayout::default(),
47            directed: false,
48            color_components: false,
49            fill: (31, 119, 180),
50            node_size: (4.0, 11.0),
51            edge_color: (150, 150, 150),
52            edge_width: (0.8, 4.0),
53            edge_alpha: 0.7,
54            labels: true,
55            label_size: 9.0,
56            label_color: (40, 40, 40),
57        }
58    }
59}
60
61impl Geom for GeomNetwork {
62    fn draw(
63        &self,
64        data: &DataFrame,
65        coord: &dyn Coord,
66        scales: &ScaleSet,
67        theme: &Theme,
68        backend: &mut dyn DrawBackend,
69    ) -> Result<(), RenderError> {
70        let col = |n: &str| {
71            data.column(n)
72                .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
73        };
74        let (part, xc, yc, xe, ye) = (
75            col("part")?,
76            col("x")?,
77            col("y")?,
78            col("xend")?,
79            col("yend")?,
80        );
81        let (node, from, to, value, degree) = (
82            col("node")?,
83            col("from")?,
84            col("to")?,
85            col("value")?,
86            col("degree")?,
87        );
88        let fill_col = data.column("fill");
89        let area = backend.plot_area();
90        let px = |x: f64, y: f64| {
91            let sx = scales.get(&Aesthetic::X).map(|s| s.map(&Value::Float(x)));
92            let sy = scales.get(&Aesthetic::Y).map(|s| s.map(&Value::Float(y)));
93            coord.transform((sx.unwrap_or(0.5), sy.unwrap_or(0.5)), &area)
94        };
95        let is = |i: usize, p: &str| part[i].key_str() == p;
96        let max_of = |p: &str, c: &[Value]| {
97            (0..data.nrows())
98                .filter(|&i| is(i, p))
99                .filter_map(|i| c[i].as_f64())
100                .fold(0.0, f64::max)
101        };
102        let (max_w, max_d) = (max_of("edge", value), max_of("node", degree));
103        let lerp = |(lo, hi): (f64, f64), t: f64| lo + (hi - lo) * t.clamp(0.0, 1.0);
104        let radius = |d: f64| {
105            lerp(
106                self.node_size,
107                if max_d > 0.0 { (d / max_d).sqrt() } else { 0.0 },
108            )
109        };
110        // Target node radius per name, to stop arrows at the node's rim.
111        let node_radius: std::collections::HashMap<String, f64> = (0..data.nrows())
112            .filter(|&i| is(i, "node"))
113            .map(|i| {
114                (
115                    node[i].key_str().into_owned(),
116                    radius(degree[i].as_f64().unwrap_or(0.0)),
117                )
118            })
119            .collect();
120        let head = arrow().kind(ArrowType::Closed).length(8.0);
121
122        for i in (0..data.nrows()).filter(|&i| is(i, "edge")) {
123            let (Some(x0), Some(y0), Some(x1), Some(y1)) = (
124                xc[i].as_f64(),
125                yc[i].as_f64(),
126                xe[i].as_f64(),
127                ye[i].as_f64(),
128            ) else {
129                continue;
130            };
131            let (a, mut b) = (px(x0, y0), px(x1, y1));
132            let w = lerp(
133                self.edge_width,
134                value[i]
135                    .as_f64()
136                    .map(|v| if max_w > 0.0 { v / max_w } else { 1.0 })
137                    .unwrap_or(0.0),
138            );
139            if self.directed {
140                let r = node_radius
141                    .get(to[i].key_str().as_ref())
142                    .copied()
143                    .unwrap_or(0.0)
144                    + 1.0;
145                let (dx, dy) = (b.0 - a.0, b.1 - a.1);
146                let len = dx.hypot(dy);
147                if len > r {
148                    b = (b.0 - dx / len * r, b.1 - dy / len * r);
149                }
150            }
151            let (f, t) = (super::tip_value(&from[i]), super::tip_value(&to[i]));
152            let sep = if self.directed { "→" } else { "—" };
153            super::set_mark(
154                backend,
155                Some(format!("{f} {sep} {t}: {}", super::tip_value(&value[i]))),
156                Some(f.clone()),
157                Some(f),
158                super::raw_value(&value[i]),
159            );
160            backend.draw_line(
161                &[a, b],
162                &LineStyle {
163                    color: self.edge_color,
164                    alpha: self.edge_alpha,
165                    width: w,
166                    linetype: Linetype::Solid,
167                    ..Default::default()
168                },
169            )?;
170            if self.directed {
171                let hs = heads(&[a, b], &head, w);
172                draw_heads(backend, &hs, &head, self.edge_color, self.edge_alpha, w)?;
173                backend.set_mark_part(None);
174            }
175        }
176
177        let mut labels = Vec::new();
178        for i in (0..data.nrows()).filter(|&i| is(i, "node")) {
179            let (Some(x), Some(y)) = (xc[i].as_f64(), yc[i].as_f64()) else {
180                continue;
181            };
182            let c = px(x, y);
183            let r = radius(degree[i].as_f64().unwrap_or(0.0));
184            let fill = fill_col
185                .and_then(|f| scales.map_color(&Aesthetic::Fill, &f[i]))
186                .unwrap_or(self.fill);
187            let name = super::tip_value(&node[i]);
188            super::set_mark(
189                backend,
190                Some(format!("{name} (degree {})", super::tip_value(&degree[i]))),
191                Some(name.clone()),
192                Some(name.clone()),
193                super::raw_value(&degree[i]),
194            );
195            backend.draw_circle(
196                c,
197                r,
198                &PointStyle {
199                    color: fill,
200                    alpha: 1.0,
201                    filled: true,
202                    ..Default::default()
203                },
204            )?;
205            if self.labels {
206                labels.push((name, (c.0 + r + 3.0, c.1)));
207            }
208        }
209        super::clear_mark(backend);
210        let family = super::resolve_font_family("", theme);
211        for (text, pos) in labels {
212            backend.draw_text(
213                &text,
214                pos,
215                &TextStyle {
216                    color: self.label_color,
217                    size: self.label_size,
218                    anchor: TextAnchor::Start,
219                    angle: 0.0,
220                    family: family.clone(),
221                    face: FontFace::Plain,
222                    bg_colour: Some((255, 255, 255)),
223                    bg_r: 1.5,
224                },
225            )?;
226        }
227        Ok(())
228    }
229
230    fn required_aes(&self) -> Vec<Aesthetic> {
231        vec![Aesthetic::From, Aesthetic::To]
232    }
233
234    fn default_stat(&self) -> Box<dyn Stat> {
235        Box::new(StatNetwork {
236            layout: self.layout,
237        })
238    }
239
240    fn default_position(&self) -> Box<dyn Position> {
241        Box::new(PositionIdentity)
242    }
243
244    fn default_params(&self) -> GeomParams {
245        GeomParams::default()
246    }
247
248    fn name(&self) -> &str {
249        "network"
250    }
251
252    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
253        if let Some(ink) = el.ink {
254            self.label_color = ink;
255        }
256    }
257
258    fn set_series_color(&mut self, color: (u8, u8, u8)) {
259        self.fill = color;
260    }
261}