Skip to main content

ggplot_rs/geom/
tree.rs

1//! Tree diagrams: `geom_tree` (tidy tree), org charts (boxed nodes) and
2//! `geom_dendrogram`. Layout by [`StatTree`]; links drawn as elbows, steps,
3//! diagonals or straight lines.
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::{
11    DrawBackend, FontFace, LineStyle, Linetype, PointStyle, RectStyle, TextAnchor, TextStyle,
12};
13use crate::render::RenderError;
14use crate::scale::ScaleSet;
15use crate::stat::tree::{StatTree, TreeOrientation};
16use crate::stat::Stat;
17use crate::theme::Theme;
18
19use super::treemap::contrast_text;
20use super::{Geom, GeomParams};
21
22/// How a link joins parent and child.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
24pub enum TreeLink {
25    /// Orthogonal with the turn halfway between the levels (org charts).
26    #[default]
27    Elbow,
28    /// Orthogonal, turning at the parent's level (dendrograms).
29    Step,
30    /// A smooth S-curve (d3's `linkVertical` / `linkHorizontal`).
31    Diagonal,
32    /// A straight line.
33    Straight,
34}
35
36/// How nodes are drawn.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
38pub enum TreeNode {
39    /// A dot, labelled beside it.
40    #[default]
41    Point,
42    /// A rounded box with the name inside (org chart).
43    Box,
44    /// Nothing (dendrograms: only leaves are labelled).
45    None,
46}
47
48/// Tree diagram over parent → child links (`from` → `to`).
49pub struct GeomTree {
50    pub orientation: TreeOrientation,
51    pub align_leaves: bool,
52    /// Column holding each link's parent height (dendrograms).
53    pub height: Option<String>,
54    pub link: TreeLink,
55    pub node: TreeNode,
56    /// Node fill when `fill` is unmapped.
57    pub fill: (u8, u8, u8),
58    pub link_color: (u8, u8, u8),
59    pub link_width: f64,
60    pub labels: bool,
61    /// Label only the leaves.
62    pub leaf_labels_only: bool,
63    pub label_size: f64,
64    pub label_color: (u8, u8, u8),
65}
66
67impl Default for GeomTree {
68    fn default() -> Self {
69        GeomTree {
70            orientation: TreeOrientation::TopDown,
71            align_leaves: false,
72            height: None,
73            link: TreeLink::Diagonal,
74            node: TreeNode::Point,
75            fill: (31, 119, 180),
76            link_color: (150, 150, 150),
77            link_width: 1.2,
78            labels: true,
79            leaf_labels_only: false,
80            label_size: 9.0,
81            label_color: (40, 40, 40),
82        }
83    }
84}
85
86impl GeomTree {
87    /// An org chart: boxed nodes joined by elbows, top-down.
88    pub fn org_chart() -> Self {
89        GeomTree {
90            link: TreeLink::Elbow,
91            node: TreeNode::Box,
92            ..Default::default()
93        }
94    }
95
96    /// A dendrogram from [`dendrogram_links`](crate::stat::tree::dendrogram_links)
97    /// output: step links at merge heights, leaves labelled.
98    pub fn dendrogram() -> Self {
99        GeomTree {
100            height: Some("height".into()),
101            link: TreeLink::Step,
102            node: TreeNode::None,
103            leaf_labels_only: true,
104            link_color: (60, 60, 60),
105            ..Default::default()
106        }
107    }
108
109    /// Lay the tree out with this orientation.
110    pub fn oriented(mut self, orientation: TreeOrientation) -> Self {
111        self.orientation = orientation;
112        self
113    }
114
115    /// Link path from parent `p` to child `c` (data coordinates).
116    fn link_path(&self, p: (f64, f64), c: (f64, f64)) -> Vec<(f64, f64)> {
117        let horizontal = self.orientation == TreeOrientation::LeftRight;
118        match (self.link, self.orientation) {
119            (TreeLink::Straight, _)
120            | (TreeLink::Elbow | TreeLink::Step, TreeOrientation::Radial) => {
121                vec![p, c]
122            }
123            (TreeLink::Elbow, _) if horizontal => {
124                let m = (p.0 + c.0) / 2.0;
125                vec![p, (m, p.1), (m, c.1), c]
126            }
127            (TreeLink::Elbow, _) => {
128                let m = (p.1 + c.1) / 2.0;
129                vec![p, (p.0, m), (c.0, m), c]
130            }
131            (TreeLink::Step, _) if horizontal => vec![p, (p.0, c.1), c],
132            (TreeLink::Step, _) => vec![p, (c.0, p.1), c],
133            (TreeLink::Diagonal, _) => {
134                // Cubic Bézier with both controls halfway along the depth axis
135                // (towards the centre's ring for radial trees: plain midpoints).
136                let (c1, c2) = if horizontal {
137                    let m = (p.0 + c.0) / 2.0;
138                    ((m, p.1), (m, c.1))
139                } else if self.orientation == TreeOrientation::TopDown {
140                    let m = (p.1 + c.1) / 2.0;
141                    ((p.0, m), (c.0, m))
142                } else {
143                    let mid = ((p.0 + c.0) / 2.0, (p.1 + c.1) / 2.0);
144                    (mid, mid)
145                };
146                (0..=16)
147                    .map(|k| {
148                        let t = k as f64 / 16.0;
149                        let u = 1.0 - t;
150                        let b = |a: f64, b1: f64, b2: f64, d: f64| {
151                            u * u * u * a
152                                + 3.0 * u * u * t * b1
153                                + 3.0 * u * t * t * b2
154                                + t * t * t * d
155                        };
156                        (b(p.0, c1.0, c2.0, c.0), b(p.1, c1.1, c2.1, c.1))
157                    })
158                    .collect()
159            }
160        }
161    }
162}
163
164impl Geom for GeomTree {
165    fn draw(
166        &self,
167        data: &DataFrame,
168        coord: &dyn Coord,
169        scales: &ScaleSet,
170        theme: &Theme,
171        backend: &mut dyn DrawBackend,
172    ) -> Result<(), RenderError> {
173        let col = |n: &str| {
174            data.column(n)
175                .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
176        };
177        let (part, xc, yc, xe, ye) = (
178            col("part")?,
179            col("x")?,
180            col("y")?,
181            col("xend")?,
182            col("yend")?,
183        );
184        let (node, from, to, leaf) = (col("node")?, col("from")?, col("to")?, col("leaf")?);
185        let fill_col = data.column("fill");
186        let area = backend.plot_area();
187        let px = |(x, y): (f64, f64)| {
188            let sx = scales.get(&Aesthetic::X).map(|s| s.map(&Value::Float(x)));
189            let sy = scales.get(&Aesthetic::Y).map(|s| s.map(&Value::Float(y)));
190            coord.transform((sx.unwrap_or(0.5), sy.unwrap_or(0.5)), &area)
191        };
192        let is = |i: usize, p: &str| part[i].key_str() == p;
193        let family = super::resolve_font_family("", theme);
194        let text = |_s: &str, pos: (f64, f64), anchor, color, face| {
195            (
196                pos,
197                TextStyle {
198                    color,
199                    size: self.label_size,
200                    anchor,
201                    angle: 0.0,
202                    family: family.clone(),
203                    face,
204                    bg_colour: None,
205                    bg_r: 0.0,
206                },
207            )
208        };
209
210        for i in (0..data.nrows()).filter(|&i| is(i, "link")) {
211            let (Some(x0), Some(y0), Some(x1), Some(y1)) = (
212                xc[i].as_f64(),
213                yc[i].as_f64(),
214                xe[i].as_f64(),
215                ye[i].as_f64(),
216            ) else {
217                continue;
218            };
219            let pts: Vec<(f64, f64)> = self
220                .link_path((x0, y0), (x1, y1))
221                .into_iter()
222                .map(px)
223                .collect();
224            let (f, t) = (super::tip_value(&from[i]), super::tip_value(&to[i]));
225            super::set_mark(backend, Some(format!("{f} → {t}")), Some(t), Some(f), None);
226            backend.draw_line(
227                &pts,
228                &LineStyle {
229                    color: self.link_color,
230                    alpha: 1.0,
231                    width: self.link_width,
232                    linetype: Linetype::Solid,
233                    ..Default::default()
234                },
235            )?;
236        }
237
238        for i in (0..data.nrows()).filter(|&i| is(i, "node")) {
239            let (Some(x), Some(y)) = (xc[i].as_f64(), yc[i].as_f64()) else {
240                continue;
241            };
242            let c = px((x, y));
243            let name = super::tip_value(&node[i]);
244            let is_leaf = matches!(leaf[i], Value::Bool(true));
245            let fill = fill_col
246                .and_then(|f| scales.map_color(&Aesthetic::Fill, &f[i]))
247                .unwrap_or(self.fill);
248            super::set_mark(
249                backend,
250                Some(name.clone()),
251                Some(name.clone()),
252                Some(name.clone()),
253                None,
254            );
255            match self.node {
256                TreeNode::Box => {
257                    let w = name.chars().count() as f64 * self.label_size * 0.62 + 14.0;
258                    let h = self.label_size + 12.0;
259                    backend.draw_rect(
260                        (c.0 - w / 2.0, c.1 - h / 2.0),
261                        (c.0 + w / 2.0, c.1 + h / 2.0),
262                        &RectStyle {
263                            fill: Some(fill),
264                            stroke: None,
265                            stroke_width: 0.0,
266                            alpha: 1.0,
267                            clip: false,
268                            corner_radius: 4.0,
269                        },
270                    )?;
271                    if self.labels {
272                        super::clear_mark(backend);
273                        let (pos, style) = text(
274                            &name,
275                            c,
276                            TextAnchor::Middle,
277                            contrast_text(fill),
278                            FontFace::Bold,
279                        );
280                        backend.draw_text(&name, pos, &style)?;
281                    }
282                    continue;
283                }
284                TreeNode::Point => {
285                    backend.draw_circle(
286                        c,
287                        4.0,
288                        &PointStyle {
289                            color: fill,
290                            alpha: 1.0,
291                            filled: true,
292                            ..Default::default()
293                        },
294                    )?;
295                }
296                TreeNode::None => {}
297            }
298            if !self.labels || (self.leaf_labels_only && !is_leaf) {
299                continue;
300            }
301            super::clear_mark(backend);
302            let (pos, anchor) = match self.orientation {
303                TreeOrientation::TopDown if is_leaf => ((c.0, c.1 + 12.0), TextAnchor::Middle),
304                TreeOrientation::TopDown => ((c.0 + 7.0, c.1 - 8.0), TextAnchor::Start),
305                TreeOrientation::LeftRight => ((c.0 + 7.0, c.1), TextAnchor::Start),
306                TreeOrientation::Radial => {
307                    // Outward from the centre.
308                    let o = px((0.0, 0.0));
309                    let (dx, dy) = (c.0 - o.0, c.1 - o.1);
310                    let len = dx.hypot(dy);
311                    if len < 1e-6 {
312                        ((c.0, c.1 - 10.0), TextAnchor::Middle)
313                    } else {
314                        let at = (c.0 + dx / len * 8.0, c.1 + dy / len * 8.0);
315                        let anchor = if dx.abs() < 0.2 * len {
316                            TextAnchor::Middle
317                        } else if dx > 0.0 {
318                            TextAnchor::Start
319                        } else {
320                            TextAnchor::End
321                        };
322                        (at, anchor)
323                    }
324                }
325            };
326            let (pos, style) = text(&name, pos, anchor, self.label_color, FontFace::Plain);
327            backend.draw_text(&name, pos, &style)?;
328        }
329        super::clear_mark(backend);
330        Ok(())
331    }
332
333    fn required_aes(&self) -> Vec<Aesthetic> {
334        vec![Aesthetic::From, Aesthetic::To]
335    }
336
337    fn default_stat(&self) -> Box<dyn Stat> {
338        Box::new(StatTree {
339            orientation: self.orientation,
340            align_leaves: self.align_leaves,
341            height: self.height.clone(),
342        })
343    }
344
345    fn default_position(&self) -> Box<dyn Position> {
346        Box::new(PositionIdentity)
347    }
348
349    fn default_params(&self) -> GeomParams {
350        GeomParams::default()
351    }
352
353    fn name(&self) -> &str {
354        "tree"
355    }
356
357    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
358        if let Some(ink) = el.ink {
359            self.label_color = ink;
360        }
361    }
362
363    fn set_series_color(&mut self, color: (u8, u8, u8)) {
364        self.fill = color;
365    }
366}