Skip to main content

ggplot_rs/geom/
chord.rs

1//! Chord diagram / dependency wheel: `geom_chord`. Nodes are arcs around a
2//! circle; each link is a ribbon from its source's band to its target's band
3//! through the centre. Layout by [`StatChord`].
4
5use crate::aes::Aesthetic;
6use crate::coord::Coord;
7use crate::data::{DataFrame, Value};
8use crate::position::identity::PositionIdentity;
9use crate::position::Position;
10use crate::render::backend::{DrawBackend, FontFace, RectStyle, TextAnchor, TextStyle};
11use crate::render::RenderError;
12use crate::scale::ScaleSet;
13use crate::stat::chord::StatChord;
14use crate::stat::Stat;
15use crate::theme::Theme;
16
17use super::{Geom, GeomParams};
18
19/// Chord diagram. Map `from`, `to` and optionally `weight`; node arcs are
20/// coloured through the fill scale by name, ribbons by their source.
21pub struct GeomChord {
22    /// Gap between neighbouring node arcs, in radians.
23    pub pad_angle: f64,
24    /// Node arc thickness as a fraction of the radius.
25    pub arc_width: f64,
26    /// Fill used when no fill scale applies.
27    pub fill: (u8, u8, u8),
28    pub node_alpha: f64,
29    pub link_alpha: f64,
30    /// Draw node names around the circle.
31    pub labels: bool,
32    pub label_size: f64,
33    pub label_color: (u8, u8, u8),
34}
35
36impl Default for GeomChord {
37    fn default() -> Self {
38        GeomChord {
39            pad_angle: 0.04,
40            arc_width: 0.07,
41            fill: (97, 156, 255),
42            node_alpha: 1.0,
43            link_alpha: 0.55,
44            labels: true,
45            label_size: 10.0,
46            label_color: (40, 40, 40),
47        }
48    }
49}
50
51impl GeomChord {
52    /// The stat's trained half-extents `(x, y)`; the builder derives the
53    /// panel aspect ratio from them so the circle stays round.
54    pub(crate) fn extents(&self) -> (f64, f64) {
55        let s = StatChord::default();
56        (s.x_extent, s.y_extent)
57    }
58}
59
60/// Point on the circle of radius `r` at angle `a` (clockwise from 12 o'clock).
61fn polar(a: f64, r: f64) -> (f64, f64) {
62    (r * a.sin(), r * a.cos())
63}
64
65/// Points along the arc `a0..a1` at radius `r` (inclusive of both ends).
66fn arc(a0: f64, a1: f64, r: f64, out: &mut Vec<(f64, f64)>) {
67    let steps = (((a1 - a0).abs() / 0.03).ceil() as usize).clamp(2, 400);
68    for k in 0..=steps {
69        out.push(polar(a0 + (a1 - a0) * k as f64 / steps as f64, r));
70    }
71}
72
73/// Quadratic Bézier from `p` to `q` with the control point at the centre.
74fn through_centre(p: (f64, f64), q: (f64, f64), out: &mut Vec<(f64, f64)>) {
75    const STEPS: usize = 24;
76    for k in 1..STEPS {
77        let t = k as f64 / STEPS as f64;
78        let (u, v) = ((1.0 - t) * (1.0 - t), t * t);
79        out.push((u * p.0 + v * q.0, u * p.1 + v * q.1));
80    }
81}
82
83impl Geom for GeomChord {
84    fn draw(
85        &self,
86        data: &DataFrame,
87        coord: &dyn Coord,
88        scales: &ScaleSet,
89        theme: &Theme,
90        backend: &mut dyn DrawBackend,
91    ) -> Result<(), RenderError> {
92        let col = |n: &str| {
93            data.column(n)
94                .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
95        };
96        let (part, a0c, a1c, b0c, b1c) =
97            (col("part")?, col("a0")?, col("a1")?, col("b0")?, col("b1")?);
98        let (node, from, to, value) = (col("node")?, col("from")?, col("to")?, col("value")?);
99        let fill_col = data.column("fill");
100        let area = backend.plot_area();
101        let map = |(x, y): (f64, f64)| {
102            let sx = scales.get(&Aesthetic::X).map(|s| s.map(&Value::Float(x)));
103            let sy = scales.get(&Aesthetic::Y).map(|s| s.map(&Value::Float(y)));
104            coord.transform((sx.unwrap_or(0.5), sy.unwrap_or(0.5)), &area)
105        };
106        let fill_of = |i: usize| {
107            fill_col
108                .and_then(|c| scales.map_color(&Aesthetic::Fill, &c[i]))
109                .unwrap_or(self.fill)
110        };
111        let f = |c: &[Value], i: usize| c[i].as_f64().unwrap_or(f64::NAN);
112        let is = |i: usize, p: &str| part[i].key_str() == p;
113        let r_in = 1.0 - self.arc_width;
114        let style = |fill, alpha| RectStyle {
115            fill: Some(fill),
116            stroke: None,
117            stroke_width: 0.0,
118            alpha,
119            clip: true,
120            corner_radius: 0.0,
121        };
122
123        for i in (0..data.nrows()).filter(|&i| is(i, "link")) {
124            let (a0, a1, b0, b1) = (f(a0c, i), f(a1c, i), f(b0c, i), f(b1c, i));
125            if ![a0, a1, b0, b1].iter().all(|v| v.is_finite()) {
126                continue;
127            }
128            let r = r_in * 0.99;
129            let mut pts = Vec::new();
130            arc(a0, a1, r, &mut pts);
131            through_centre(polar(a1, r), polar(b0, r), &mut pts);
132            arc(b0, b1, r, &mut pts);
133            through_centre(polar(b1, r), polar(a0, r), &mut pts);
134            let pts: Vec<(f64, f64)> = pts.into_iter().map(map).collect();
135            let (a, b) = (super::tip_value(&from[i]), super::tip_value(&to[i]));
136            super::set_mark(
137                backend,
138                Some(format!("{a} → {b}: {}", super::tip_value(&value[i]))),
139                Some(a.clone()),
140                Some(a),
141                super::raw_value(&value[i]),
142            );
143            backend.draw_polygon(&pts, &style(fill_of(i), self.link_alpha))?;
144        }
145
146        let mut labels = Vec::new();
147        for i in (0..data.nrows()).filter(|&i| is(i, "node")) {
148            let (a0, a1) = (f(a0c, i), f(a1c, i));
149            if !(a0.is_finite() && a1.is_finite()) {
150                continue;
151            }
152            let mut pts = Vec::new();
153            arc(a0, a1, 1.0, &mut pts);
154            let mut inner = Vec::new();
155            arc(a0, a1, r_in, &mut inner);
156            pts.extend(inner.into_iter().rev());
157            let pts: Vec<(f64, f64)> = pts.into_iter().map(map).collect();
158            let name = super::tip_value(&node[i]);
159            super::set_mark(
160                backend,
161                Some(format!("{name}: {}", super::tip_value(&value[i]))),
162                Some(name.clone()),
163                Some(name.clone()),
164                super::raw_value(&value[i]),
165            );
166            backend.draw_polygon(&pts, &style(fill_of(i), self.node_alpha))?;
167            if self.labels {
168                let mid = (a0 + a1) / 2.0;
169                let side = mid.sin();
170                let anchor = if side.abs() < 0.12 {
171                    TextAnchor::Middle
172                } else if side > 0.0 {
173                    TextAnchor::Start
174                } else {
175                    TextAnchor::End
176                };
177                // Labels near 12/6 o'clock sit a little further out so they
178                // clear the arc vertically.
179                let r = 1.06 + 0.06 * mid.cos().abs().powi(4);
180                labels.push((name, map(polar(mid, r)), anchor));
181            }
182        }
183        super::clear_mark(backend);
184        let family = super::resolve_font_family("", theme);
185        for (text, pos, anchor) in labels {
186            backend.draw_text(
187                &text,
188                pos,
189                &TextStyle {
190                    color: self.label_color,
191                    size: self.label_size,
192                    anchor,
193                    angle: 0.0,
194                    family: family.clone(),
195                    face: FontFace::Plain,
196                    bg_colour: None,
197                    bg_r: 0.0,
198                },
199            )?;
200        }
201        Ok(())
202    }
203
204    fn required_aes(&self) -> Vec<Aesthetic> {
205        vec![Aesthetic::From, Aesthetic::To]
206    }
207
208    fn default_stat(&self) -> Box<dyn Stat> {
209        Box::new(StatChord {
210            pad_angle: self.pad_angle,
211            ..Default::default()
212        })
213    }
214
215    fn default_position(&self) -> Box<dyn Position> {
216        Box::new(PositionIdentity)
217    }
218
219    fn default_params(&self) -> GeomParams {
220        GeomParams::default()
221    }
222
223    fn name(&self) -> &str {
224        "chord"
225    }
226
227    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
228        if let Some(ink) = el.ink {
229            self.label_color = ink;
230        }
231    }
232}