Skip to main content

ggplot_rs/stat/
pareto.rs

1//! `StatPareto`: sum `y` per category, sort descending and accumulate — the
2//! data behind a Pareto chart's bars and cumulative-percentage line.
3
4use crate::aes::Aesthetic;
5use crate::data::{DataFrame, Value};
6use crate::scale::ScaleSet;
7
8use super::Stat;
9
10/// Pareto stat behind [`GeomPareto`](crate::geom::pareto::GeomPareto).
11///
12/// Output columns, one row per distinct x (largest first when `sort`):
13/// `x`, `y` (the category's summed value), `cum` (running total), `cum_pct`
14/// (running share of the grand total, 0–100), `ymin` (0) and `ymax`
15/// (= `cum`, so the y scale trains up to the grand total).
16#[derive(Clone, Debug)]
17pub struct StatPareto {
18    /// Order categories by descending value (ties keep data order). With
19    /// `false` the data order is kept.
20    pub sort: bool,
21}
22
23impl Default for StatPareto {
24    fn default() -> Self {
25        StatPareto { sort: true }
26    }
27}
28
29impl Stat for StatPareto {
30    fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
31        let (Some(x_col), Some(y_col)) = (data.column("x"), data.column("y")) else {
32            return DataFrame::new();
33        };
34        let mut cats: indexmap::IndexMap<String, (Value, f64)> = indexmap::IndexMap::new();
35        for (x, y) in x_col.iter().zip(y_col) {
36            let Some(v) = y.as_f64().filter(|v| v.is_finite()) else {
37                continue;
38            };
39            cats.entry(x.key_str().into_owned())
40                .or_insert_with(|| (x.clone(), 0.0))
41                .1 += v;
42        }
43        let mut rows: Vec<(Value, f64)> = cats.into_values().collect();
44        if self.sort {
45            rows.sort_by(|a, b| b.1.total_cmp(&a.1));
46        }
47        let total: f64 = rows.iter().map(|r| r.1).sum();
48        let mut cum = 0.0;
49        let (mut xs, mut ys, mut cums, mut pcts) = (vec![], vec![], vec![], vec![]);
50        for (x, v) in rows {
51            cum += v;
52            xs.push(x);
53            ys.push(Value::Float(v));
54            cums.push(Value::Float(cum));
55            pcts.push(if total != 0.0 {
56                Value::Float(cum / total * 100.0)
57            } else {
58                Value::Na
59            });
60        }
61        let n = xs.len();
62        let mut out = DataFrame::new();
63        out.add_column("x".into(), xs);
64        out.add_column("y".into(), ys);
65        out.add_column("cum".into(), cums.clone());
66        out.add_column("cum_pct".into(), pcts);
67        out.add_column("ymin".into(), vec![Value::Float(0.0); n]);
68        out.add_column("ymax".into(), cums);
69        out
70    }
71
72    fn required_aes(&self) -> Vec<Aesthetic> {
73        vec![Aesthetic::X, Aesthetic::Y]
74    }
75
76    /// The ranking and running total span every category of the panel.
77    fn panelwise(&self) -> bool {
78        true
79    }
80
81    fn name(&self) -> &str {
82        "pareto"
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn sums_sorts_and_accumulates() {
92        let mut d = DataFrame::new();
93        d.add_column(
94            "x".into(),
95            ["a", "b", "c", "b"]
96                .iter()
97                .map(|s| Value::Str(s.to_string()))
98                .collect(),
99        );
100        d.add_column(
101            "y".into(),
102            [10.0, 20.0, 50.0, 20.0]
103                .iter()
104                .map(|v| Value::Float(*v))
105                .collect(),
106        );
107        let out = StatPareto::default().compute_group(&d, &ScaleSet::new());
108        let keys: Vec<String> = out
109            .column("x")
110            .unwrap()
111            .iter()
112            .map(|v| v.key_str().into_owned())
113            .collect();
114        assert_eq!(keys, ["c", "b", "a"]);
115        let f = |c: &str| -> Vec<f64> {
116            out.column(c)
117                .unwrap()
118                .iter()
119                .map(|v| v.as_f64().unwrap())
120                .collect()
121        };
122        assert_eq!(f("y"), vec![50.0, 40.0, 10.0]);
123        assert_eq!(f("cum"), vec![50.0, 90.0, 100.0]);
124        assert_eq!(f("cum_pct"), vec![50.0, 90.0, 100.0]);
125    }
126}