Skip to main content

ggplot_rs/stat/
partition.rs

1//! `StatPartition`: hierarchical partition layout (icicle / sunburst).
2//!
3//! Each input row is a leaf path given by the `levels` columns (outermost
4//! first) with a `weight` (1 when unmapped). Every prefix of a path is a
5//! node whose value is the sum of its leaves. Nodes at depth `d` occupy
6//! `y ∈ [d, d + 1]`; siblings split their parent's x span in
7//! proportion to value (first-appearance order, or largest first with
8//! `sort`). The root spans x ∈ [0, 1]. A missing (`Na`/empty) level ends a
9//! path early, so ragged hierarchies work.
10//!
11//! Output: one row per node with `xmin`/`xmax`/`ymin`/`ymax`, `depth`
12//! (0-based), `node` (its own level value), `path` (`"A / B / C"`), `root`
13//! (the depth-0 ancestor, for colouring), `value` and `share` (of the total).
14
15use crate::aes::Aesthetic;
16use crate::data::{DataFrame, Value};
17use crate::scale::ScaleSet;
18
19use super::Stat;
20
21/// Partition stat behind [`GeomPartition`](crate::geom::partition::GeomPartition).
22#[derive(Clone, Debug, Default)]
23pub struct StatPartition {
24    /// Hierarchy columns, outermost level first.
25    pub levels: Vec<String>,
26    /// Order siblings by descending value instead of first appearance.
27    pub sort: bool,
28    /// Grow downward from the top (an icicle with the root on top):
29    /// depth `d` spans `y ∈ [-(d + 1), -d]`.
30    pub downward: bool,
31}
32
33#[derive(Default)]
34struct Node {
35    name: String,
36    value: f64,
37    children: indexmap::IndexMap<String, Node>,
38}
39
40impl Stat for StatPartition {
41    fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
42        let cols: Vec<&[Value]> = self.levels.iter().filter_map(|l| data.column(l)).collect();
43        if cols.is_empty() || cols.len() != self.levels.len() {
44            return DataFrame::new();
45        }
46        let weight = data.column("weight");
47        let mut root = Node::default();
48        for i in 0..data.nrows() {
49            let w = match weight {
50                Some(c) => match c[i].as_f64() {
51                    Some(w) if w.is_finite() && w > 0.0 => w,
52                    _ => continue,
53                },
54                None => 1.0,
55            };
56            root.value += w;
57            let mut node = &mut root;
58            for col in &cols {
59                let key = col[i].key_str().into_owned();
60                if col[i].is_na() || key.is_empty() {
61                    break;
62                }
63                node = node.children.entry(key.clone()).or_insert_with(|| Node {
64                    name: key,
65                    ..Default::default()
66                });
67                node.value += w;
68            }
69        }
70        if root.value <= 0.0 {
71            return DataFrame::new();
72        }
73
74        struct Row {
75            x: (f64, f64),
76            depth: usize,
77            node: String,
78            path: String,
79            root: String,
80            value: f64,
81        }
82        fn walk(
83            node: &Node,
84            (x0, span): (f64, f64),
85            depth: usize,
86            path: &str,
87            root: &str,
88            sort: bool,
89            out: &mut Vec<Row>,
90        ) {
91            let mut kids: Vec<&Node> = node.children.values().collect();
92            if sort {
93                kids.sort_by(|a, b| b.value.total_cmp(&a.value));
94            }
95            let mut x = x0;
96            for k in kids {
97                let w = if node.value > 0.0 {
98                    span * k.value / node.value
99                } else {
100                    0.0
101                };
102                let p = if path.is_empty() {
103                    k.name.clone()
104                } else {
105                    format!("{path} / {}", k.name)
106                };
107                let r = if depth == 0 { k.name.as_str() } else { root };
108                out.push(Row {
109                    x: (x, x + w),
110                    depth,
111                    node: k.name.clone(),
112                    path: p.clone(),
113                    root: r.to_string(),
114                    value: k.value,
115                });
116                walk(k, (x, w), depth + 1, &p, r, sort, out);
117                x += w;
118            }
119        }
120        let mut rows = Vec::new();
121        walk(&root, (0.0, 1.0), 0, "", "", self.sort, &mut rows);
122
123        let total = root.value;
124        let f = |g: &dyn Fn(&Row) -> f64| rows.iter().map(|r| Value::Float(g(r))).collect();
125        let s = |g: &dyn Fn(&Row) -> String| rows.iter().map(|r| Value::Str(g(r))).collect();
126        let mut out = DataFrame::new();
127        out.add_column("xmin".into(), f(&|r| r.x.0));
128        out.add_column("xmax".into(), f(&|r| r.x.1));
129        let band = |r: &Row| {
130            let (lo, hi) = (r.depth as f64, r.depth as f64 + 1.0);
131            if self.downward {
132                (-hi, -lo)
133            } else {
134                (lo, hi)
135            }
136        };
137        out.add_column("ymin".into(), f(&|r| band(r).0));
138        out.add_column("ymax".into(), f(&|r| band(r).1));
139        out.add_column("depth".into(), f(&|r| r.depth as f64));
140        out.add_column("node".into(), s(&|r| r.node.clone()));
141        out.add_column("path".into(), s(&|r| r.path.clone()));
142        out.add_column("root".into(), s(&|r| r.root.clone()));
143        out.add_column("value".into(), f(&|r| r.value));
144        out.add_column("share".into(), f(&|r| r.value / total));
145        out
146    }
147
148    fn required_aes(&self) -> Vec<Aesthetic> {
149        Vec::new()
150    }
151
152    /// The partition spans every row of the panel.
153    fn panelwise(&self) -> bool {
154        true
155    }
156
157    fn name(&self) -> &str {
158        "partition"
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn nested_spans_sum_to_parents() {
168        let mut d = DataFrame::new();
169        let s = |v: &[&str]| v.iter().map(|x| Value::Str(x.to_string())).collect();
170        d.add_column("a".into(), s(&["X", "X", "X", "Y"]));
171        d.add_column("b".into(), s(&["p", "p", "q", ""]));
172        d.add_column(
173            "weight".into(),
174            [1.0, 2.0, 3.0, 4.0]
175                .iter()
176                .map(|&v| Value::Float(v))
177                .collect(),
178        );
179        let out = StatPartition {
180            levels: vec!["a".into(), "b".into()],
181            sort: false,
182            downward: false,
183        }
184        .compute_group(&d, &ScaleSet::new());
185        // X, X/p, X/q, Y (Y's empty child ends its path).
186        assert_eq!(out.nrows(), 4);
187        let paths: Vec<String> = out
188            .column("path")
189            .unwrap()
190            .iter()
191            .map(|v| v.key_str().into_owned())
192            .collect();
193        assert_eq!(paths, ["X", "X / p", "X / q", "Y"]);
194        let f = |c: &str| -> Vec<f64> {
195            out.column(c)
196                .unwrap()
197                .iter()
198                .map(|v| v.as_f64().unwrap())
199                .collect()
200        };
201        assert_eq!(f("value"), vec![6.0, 3.0, 3.0, 4.0]);
202        assert_eq!(f("xmin"), vec![0.0, 0.0, 0.3, 0.6]);
203        assert_eq!(f("xmax"), vec![0.6, 0.3, 0.6, 1.0]);
204        assert_eq!(f("ymin"), vec![0.0, 1.0, 1.0, 0.0]);
205        assert_eq!(out.column("root").unwrap()[2], Value::Str("X".into()));
206    }
207}