Skip to main content

ggplot_rs/geom/
pareto.rs

1//! Pareto chart: `geom_pareto`. Categories as bars sorted by value, plus the
2//! cumulative total as a line with points, read against a 0–100 % secondary
3//! axis ([`SecAxis::percent_of_max`](crate::scale::sec_axis::SecAxis::percent_of_max)).
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, LineStyle, Linetype, PointStyle, RectStyle};
11use crate::render::RenderError;
12use crate::scale::ScaleSet;
13use crate::stat::pareto::StatPareto;
14use crate::stat::Stat;
15use crate::theme::Theme;
16
17use super::{Geom, GeomParams};
18
19/// Pareto chart. Map `x` (categories) and `y` (values; repeated categories
20/// are summed). Bars are sorted descending unless `sort` is false.
21pub struct GeomPareto {
22    /// Bar fill.
23    pub fill: (u8, u8, u8),
24    /// Bar outline.
25    pub color: (u8, u8, u8),
26    /// Cumulative line and point colour.
27    pub line_color: (u8, u8, u8),
28    pub line_width: f64,
29    /// Point radius in pixels (0 hides the points).
30    pub point_size: f64,
31    /// Bar width as a fraction of the category slot.
32    pub width: f64,
33    pub alpha: f64,
34    /// Sort categories by descending value.
35    pub sort: bool,
36    /// Draw a dashed reference line at this cumulative share (e.g. `0.8`
37    /// for the 80/20 rule).
38    pub threshold: Option<f64>,
39}
40
41impl Default for GeomPareto {
42    fn default() -> Self {
43        GeomPareto {
44            fill: (97, 156, 255),
45            color: (50, 50, 50),
46            line_color: (0xe8, 0x59, 0x0c),
47            line_width: 1.5,
48            point_size: 3.0,
49            width: 0.8,
50            alpha: 1.0,
51            sort: true,
52            threshold: None,
53        }
54    }
55}
56
57impl GeomPareto {
58    /// Draw a dashed reference line at cumulative share `share` (0–1).
59    pub fn with_threshold(mut self, share: f64) -> Self {
60        self.threshold = Some(share);
61        self
62    }
63}
64
65impl Geom for GeomPareto {
66    fn draw(
67        &self,
68        data: &DataFrame,
69        coord: &dyn Coord,
70        scales: &ScaleSet,
71        _theme: &Theme,
72        backend: &mut dyn DrawBackend,
73    ) -> Result<(), RenderError> {
74        let col = |n: &str| {
75            data.column(n)
76                .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
77        };
78        let (x_col, y_col, cum_col) = (col("x")?, col("y")?, col("cum")?);
79        let pct_col = data.column("cum_pct");
80        let area = backend.plot_area();
81        let x_scale = scales.get(&Aesthetic::X);
82        let y_scale = scales.get(&Aesthetic::Y);
83        let my = |v: f64| y_scale.map(|s| s.map(&Value::Float(v))).unwrap_or(0.0);
84        let half = if x_scale.map(|s| s.is_discrete()).unwrap_or(false) {
85            let n = x_scale.map(|s| s.breaks().len()).unwrap_or(1).max(1) as f64;
86            let pad = if coord.discrete_expand() { 1.1 } else { 1.0 };
87            self.width / (n * pad) / 2.0
88        } else {
89            super::continuous_bar_half_width(
90                x_col.iter().filter_map(|v| x_scale.map(|s| s.map(v))),
91                self.width,
92                0.02,
93            )
94        };
95        let pct_txt = |i: usize| {
96            pct_col
97                .and_then(|c| c[i].as_f64())
98                .map(|p| {
99                    format!(
100                        "{}%",
101                        super::tip_value(&Value::Float((p * 10.0).round() / 10.0))
102                    )
103                })
104                .unwrap_or_default()
105        };
106
107        let mut line = Vec::with_capacity(data.nrows());
108        for i in 0..data.nrows() {
109            let (Some(v), Some(cum)) = (y_col[i].as_f64(), cum_col[i].as_f64()) else {
110                continue;
111            };
112            let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.5);
113            let x_txt = super::tip_value(&x_col[i]);
114            super::set_mark(
115                backend,
116                Some(format!(
117                    "{x_txt}: {} (cumulative {})",
118                    super::tip_value(&y_col[i]),
119                    pct_txt(i)
120                )),
121                Some(x_txt),
122                super::series_key(data, i),
123                super::raw_value(&y_col[i]),
124            );
125            super::set_mark_formatted(
126                backend,
127                scales,
128                Some(&x_col[i]),
129                &Aesthetic::Y,
130                Some(&y_col[i]),
131            );
132            super::set_row_extras(backend, data, i);
133            let (x0, y0) = coord.transform((nx - half, my(v)), &area);
134            let (x1, y1) = coord.transform((nx + half, my(0.0)), &area);
135            backend.draw_rect(
136                (x0.min(x1), y0.min(y1)),
137                (x0.max(x1), y0.max(y1)),
138                &RectStyle {
139                    fill: Some(self.fill),
140                    stroke: Some(self.color),
141                    stroke_width: 0.5,
142                    alpha: self.alpha,
143                    clip: true,
144                    corner_radius: 0.0,
145                },
146            )?;
147            line.push((i, nx, my(cum)));
148        }
149        super::clear_row_extras(backend);
150        line.sort_by(|a, b| a.1.total_cmp(&b.1));
151
152        if let (Some(share), Some(total)) = (
153            self.threshold,
154            cum_col.iter().filter_map(|v| v.as_f64()).reduce(f64::max),
155        ) {
156            super::clear_mark(backend);
157            let ny = my(share * total);
158            backend.draw_line(
159                &[
160                    coord.transform((0.0, ny), &area),
161                    coord.transform((1.0, ny), &area),
162                ],
163                &LineStyle {
164                    color: self.line_color,
165                    alpha: 0.7 * self.alpha,
166                    width: 1.0,
167                    linetype: Linetype::Dashed,
168                    ..Default::default()
169                },
170            )?;
171        }
172
173        let pts: Vec<(f64, f64)> = line
174            .iter()
175            .map(|&(_, nx, ny)| coord.transform((nx, ny), &area))
176            .collect();
177        if pts.len() > 1 {
178            super::set_mark(backend, None, None, Some("cumulative".into()), None);
179            backend.draw_line(
180                &pts,
181                &LineStyle {
182                    color: self.line_color,
183                    alpha: self.alpha,
184                    width: self.line_width,
185                    linetype: Linetype::Solid,
186                    ..Default::default()
187                },
188            )?;
189        }
190        if self.point_size > 0.0 {
191            for (&(i, _, _), &p) in line.iter().zip(&pts) {
192                let x_txt = super::tip_value(&x_col[i]);
193                super::set_mark(
194                    backend,
195                    Some(format!("{x_txt}: cumulative {}", pct_txt(i))),
196                    Some(x_txt),
197                    Some("cumulative".into()),
198                    pct_col.and_then(|c| super::raw_value(&c[i])),
199                );
200                backend.draw_circle(
201                    p,
202                    self.point_size,
203                    &PointStyle {
204                        color: self.line_color,
205                        alpha: self.alpha,
206                        filled: true,
207                        ..Default::default()
208                    },
209                )?;
210            }
211        }
212        super::clear_mark(backend);
213        Ok(())
214    }
215
216    fn required_aes(&self) -> Vec<Aesthetic> {
217        vec![Aesthetic::X, Aesthetic::Y]
218    }
219
220    fn default_stat(&self) -> Box<dyn Stat> {
221        Box::new(StatPareto { sort: self.sort })
222    }
223
224    fn default_position(&self) -> Box<dyn Position> {
225        Box::new(PositionIdentity)
226    }
227
228    fn default_params(&self) -> GeomParams {
229        GeomParams::default()
230    }
231
232    fn name(&self) -> &str {
233        "pareto"
234    }
235
236    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
237        if let (true, Some(ink)) = (self.color == (50, 50, 50), el.ink) {
238            self.color = ink;
239        }
240    }
241
242    fn consumes_tooltips(&self) -> bool {
243        true
244    }
245
246    fn include_zero_baseline(&self) -> bool {
247        true
248    }
249
250    fn set_series_color(&mut self, color: (u8, u8, u8)) {
251        self.fill = color;
252    }
253}