Skip to main content

ggplot_rs/geom/
arcdiagram.rs

1//! Arc diagram: `geom_arcdiagram`. Nodes sit on a line, links are arcs
2//! between them whose thickness is the link weight; node size is the total
3//! flow. Layout by [`StatArcdiagram`].
4
5use std::f64::consts::PI;
6
7use crate::aes::Aesthetic;
8use crate::coord::Coord;
9use crate::data::{DataFrame, Value};
10use crate::position::identity::PositionIdentity;
11use crate::position::Position;
12use crate::render::backend::{DrawBackend, FontFace, PointStyle, RectStyle, TextAnchor, TextStyle};
13use crate::render::RenderError;
14use crate::scale::ScaleSet;
15use crate::stat::arcdiagram::StatArcdiagram;
16use crate::stat::Stat;
17use crate::theme::Theme;
18
19use super::{Geom, GeomParams};
20
21/// Arc diagram. Map `from`, `to` and optionally `weight`; nodes and arcs are
22/// coloured through the fill scale by node name (arcs by their source).
23pub struct GeomArcdiagram {
24    /// Order nodes by descending total flow instead of first appearance.
25    pub sort_by_value: bool,
26    /// Draw right-to-left links below the line (shows direction).
27    pub directed_below: bool,
28    /// Node radius range in pixels (by √ of total flow).
29    pub node_size: (f64, f64),
30    /// Arc width range in pixels (by weight).
31    pub link_width: (f64, f64),
32    /// Colour used when no fill scale applies.
33    pub color: (u8, u8, u8),
34    pub link_alpha: f64,
35    /// Draw node names under the line.
36    pub labels: bool,
37    pub label_size: f64,
38    pub label_color: (u8, u8, u8),
39}
40
41impl Default for GeomArcdiagram {
42    fn default() -> Self {
43        GeomArcdiagram {
44            sort_by_value: false,
45            directed_below: false,
46            node_size: (3.0, 10.0),
47            link_width: (1.0, 8.0),
48            color: (97, 156, 255),
49            link_alpha: 0.5,
50            labels: true,
51            label_size: 10.0,
52            label_color: (40, 40, 40),
53        }
54    }
55}
56
57/// Outline of a polyline stroked `width` pixels wide: the points offset by
58/// ±width/2 along each vertex's averaged normal, as one closed polygon.
59pub(crate) fn stroke_band(pts: &[(f64, f64)], width: f64) -> Vec<(f64, f64)> {
60    let n = pts.len();
61    let h = width / 2.0;
62    let normal = |i: usize| {
63        let (a, b) = (pts[i.saturating_sub(1)], pts[(i + 1).min(n - 1)]);
64        let (dx, dy) = (b.0 - a.0, b.1 - a.1);
65        let len = dx.hypot(dy);
66        if len > 0.0 {
67            (-dy / len, dx / len)
68        } else {
69            (0.0, 0.0)
70        }
71    };
72    let mut band: Vec<(f64, f64)> = (0..n)
73        .map(|i| {
74            let (nx, ny) = normal(i);
75            (pts[i].0 + nx * h, pts[i].1 + ny * h)
76        })
77        .collect();
78    band.extend((0..n).rev().map(|i| {
79        let (nx, ny) = normal(i);
80        (pts[i].0 - nx * h, pts[i].1 - ny * h)
81    }));
82    band
83}
84
85impl Geom for GeomArcdiagram {
86    fn draw(
87        &self,
88        data: &DataFrame,
89        coord: &dyn Coord,
90        scales: &ScaleSet,
91        theme: &Theme,
92        backend: &mut dyn DrawBackend,
93    ) -> Result<(), RenderError> {
94        let col = |n: &str| {
95            data.column(n)
96                .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
97        };
98        let (part, xc, xe) = (col("part")?, col("x")?, col("xend")?);
99        let (node, from, to, value) = (col("node")?, col("from")?, col("to")?, col("value")?);
100        let fill_col = data.column("fill");
101        let area = backend.plot_area();
102        let map = |(x, y): (f64, f64)| {
103            let sx = scales.get(&Aesthetic::X).map(|s| s.map(&Value::Float(x)));
104            let sy = scales.get(&Aesthetic::Y).map(|s| s.map(&Value::Float(y)));
105            coord.transform((sx.unwrap_or(0.5), sy.unwrap_or(0.5)), &area)
106        };
107        let color_of = |i: usize| {
108            fill_col
109                .and_then(|c| scales.map_color(&Aesthetic::Fill, &c[i]))
110                .unwrap_or(self.color)
111        };
112        let is = |i: usize, p: &str| part[i].key_str() == p;
113        let max_of = |p: &str| {
114            (0..data.nrows())
115                .filter(|&i| is(i, p))
116                .filter_map(|i| value[i].as_f64())
117                .fold(0.0, f64::max)
118        };
119        let (max_w, max_v) = (max_of("link"), max_of("node"));
120        let lerp = |(lo, hi): (f64, f64), t: f64| lo + (hi - lo) * t.clamp(0.0, 1.0);
121
122        // Widest arcs first so thin ones stay visible on top.
123        let mut links: Vec<usize> = (0..data.nrows()).filter(|&i| is(i, "link")).collect();
124        links.sort_by(|&a, &b| {
125            let w = |i: usize| value[i].as_f64().unwrap_or(0.0);
126            w(b).total_cmp(&w(a))
127        });
128        for i in links {
129            let (Some(x0), Some(x1), Some(w)) = (xc[i].as_f64(), xe[i].as_f64(), value[i].as_f64())
130            else {
131                continue;
132            };
133            let (c, r) = ((x0 + x1) / 2.0, (x1 - x0).abs() / 2.0);
134            let below = self.directed_below && x1 < x0;
135            let steps = ((r * 24.0).ceil() as usize).clamp(12, 180);
136            let pts: Vec<(f64, f64)> = (0..=steps)
137                .map(|k| {
138                    let a = PI * k as f64 / steps as f64;
139                    let y = r * a.sin();
140                    map((c - r * a.cos(), if below { -y } else { y }))
141                })
142                .collect();
143            let (a, b) = (super::tip_value(&from[i]), super::tip_value(&to[i]));
144            super::set_mark(
145                backend,
146                Some(format!("{a} → {b}: {}", super::tip_value(&value[i]))),
147                Some(a.clone()),
148                Some(a),
149                super::raw_value(&value[i]),
150            );
151            let t = if max_w > 0.0 { w / max_w } else { 1.0 };
152            // A filled band (not a wide translucent stroke, whose segment
153            // joints double up the alpha on raster backends).
154            backend.draw_polygon(
155                &stroke_band(&pts, lerp(self.link_width, t)),
156                &RectStyle {
157                    fill: Some(color_of(i)),
158                    stroke: None,
159                    stroke_width: 0.0,
160                    alpha: self.link_alpha,
161                    clip: true,
162                    corner_radius: 0.0,
163                },
164            )?;
165        }
166
167        let mut labels = Vec::new();
168        for i in (0..data.nrows()).filter(|&i| is(i, "node")) {
169            let (Some(x), Some(v)) = (xc[i].as_f64(), value[i].as_f64()) else {
170                continue;
171            };
172            let p = map((x, 0.0));
173            let radius = lerp(
174                self.node_size,
175                if max_v > 0.0 { (v / max_v).sqrt() } else { 1.0 },
176            );
177            let name = super::tip_value(&node[i]);
178            super::set_mark(
179                backend,
180                Some(format!("{name}: {}", super::tip_value(&value[i]))),
181                Some(name.clone()),
182                Some(name.clone()),
183                super::raw_value(&value[i]),
184            );
185            backend.draw_circle(
186                p,
187                radius,
188                &PointStyle {
189                    color: color_of(i),
190                    alpha: 1.0,
191                    filled: true,
192                    ..Default::default()
193                },
194            )?;
195            if self.labels {
196                labels.push((name, (p.0, p.1 + self.node_size.1 + self.label_size * 0.8)));
197            }
198        }
199        super::clear_mark(backend);
200        let family = super::resolve_font_family("", theme);
201        for (text, pos) in labels {
202            backend.draw_text(
203                &text,
204                pos,
205                &TextStyle {
206                    color: self.label_color,
207                    size: self.label_size,
208                    anchor: TextAnchor::Middle,
209                    angle: 0.0,
210                    family: family.clone(),
211                    face: FontFace::Plain,
212                    bg_colour: None,
213                    bg_r: 0.0,
214                },
215            )?;
216        }
217        Ok(())
218    }
219
220    fn required_aes(&self) -> Vec<Aesthetic> {
221        vec![Aesthetic::From, Aesthetic::To]
222    }
223
224    fn default_stat(&self) -> Box<dyn Stat> {
225        Box::new(StatArcdiagram {
226            sort_by_value: self.sort_by_value,
227            directed_below: self.directed_below,
228        })
229    }
230
231    fn default_position(&self) -> Box<dyn Position> {
232        Box::new(PositionIdentity)
233    }
234
235    fn default_params(&self) -> GeomParams {
236        GeomParams::default()
237    }
238
239    fn name(&self) -> &str {
240        "arcdiagram"
241    }
242
243    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
244        if let Some(ink) = el.ink {
245            self.label_color = ink;
246        }
247    }
248}