Skip to main content

ggplot_rs/geom/
freqpoly.rs

1use crate::aes::Aesthetic;
2use crate::coord::Coord;
3use crate::data::DataFrame;
4use crate::position::identity::PositionIdentity;
5use crate::position::Position;
6use crate::render::backend::{DrawBackend, LineStyle, Linetype};
7use crate::render::RenderError;
8use crate::scale::ScaleSet;
9use crate::stat::bin::StatBin;
10use crate::stat::Stat;
11use crate::theme::Theme;
12
13use super::{Geom, GeomParams};
14
15/// Frequency polygon — line through bin centers (StatBin output).
16pub struct GeomFreqpoly {
17    pub color: (u8, u8, u8),
18    pub width: f64,
19    pub alpha: f64,
20}
21
22impl Default for GeomFreqpoly {
23    fn default() -> Self {
24        GeomFreqpoly {
25            color: (0, 0, 0),
26            width: 1.5,
27            alpha: 1.0,
28        }
29    }
30}
31
32impl Geom for GeomFreqpoly {
33    fn draw(
34        &self,
35        data: &DataFrame,
36        coord: &dyn Coord,
37        scales: &ScaleSet,
38        _theme: &Theme,
39        backend: &mut dyn DrawBackend,
40    ) -> Result<(), RenderError> {
41        let x_col = data
42            .column("x")
43            .ok_or(RenderError::MissingAesthetic("x".into()))?;
44        let y_col = data
45            .column("y")
46            .ok_or(RenderError::MissingAesthetic("y".into()))?;
47        let color_col = data.column("color");
48
49        let plot_area = backend.plot_area();
50        let x_scale = scales.get(&Aesthetic::X);
51        let y_scale = scales.get(&Aesthetic::Y);
52
53        // #197: `StatBin` is `panelwise` (bins shared across every
54        // `aes(color=)` group) and emits every group's bins in one call, so
55        // draw one line per group — the same `row_groups` idiom
56        // `GeomQQLine` uses — instead of connecting every group's points
57        // into one zigzag with a single colour.
58        for rows in super::support::row_groups(data) {
59            let points: Vec<(f64, f64)> = rows
60                .iter()
61                .map(|&i| {
62                    let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
63                    let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
64                    coord.transform((nx, ny), &plot_area)
65                })
66                .collect();
67            let first = rows[0];
68            let line_color = color_col
69                .and_then(|cc| scales.map_color(&Aesthetic::Color, &cc[first]))
70                .unwrap_or(self.color);
71            if points.len() >= 2 {
72                backend.draw_line(
73                    &points,
74                    &LineStyle {
75                        color: line_color,
76                        alpha: self.alpha,
77                        width: self.width,
78                        linetype: Linetype::Solid,
79                        ..Default::default()
80                    },
81                )?;
82            }
83        }
84
85        Ok(())
86    }
87
88    fn required_aes(&self) -> Vec<Aesthetic> {
89        vec![Aesthetic::X]
90    }
91
92    fn default_stat(&self) -> Box<dyn Stat> {
93        Box::new(StatBin::default())
94    }
95
96    fn default_position(&self) -> Box<dyn Position> {
97        Box::new(PositionIdentity)
98    }
99
100    fn default_params(&self) -> GeomParams {
101        GeomParams::default()
102    }
103
104    fn name(&self) -> &str {
105        "freqpoly"
106    }
107
108    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
109        if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
110            self.color = ink;
111        }
112    }
113
114    fn set_series_color(&mut self, color: (u8, u8, u8)) {
115        self.color = color;
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use crate::data::Value;
122    use crate::prelude::*;
123
124    /// #197: `StatBin` became `panelwise` (bins shared across groups) and
125    /// now hands every group's rows to the geom in one call — this must
126    /// still draw one connected line per group, each in its own colour, not
127    /// one zigzag line through every group's points in a single colour.
128    #[test]
129    fn grouped_freqpoly_draws_one_line_per_group() {
130        let n = 30;
131        let v: Vec<Value> = (0..n).map(|i| Value::Float((i % 10) as f64)).collect();
132        let g: Vec<Value> = (0..n)
133            .map(|i| Value::Str(if i < n / 2 { "a".into() } else { "b".into() }))
134            .collect();
135        let svg = GGPlot::new(vec![("v".into(), v), ("g".into(), g)])
136            .aes(Aes::new().x("v").color("g"))
137            .geom_freqpoly()
138            .render_svg_native_with_size(500, 350)
139            .unwrap();
140        // Two `data-geom="freqpoly"` lines (one per group, not counting
141        // grid lines/ticks), not a single big one connecting both groups'
142        // bins.
143        let mut colors = std::collections::BTreeSet::new();
144        let mut rest = svg.as_str();
145        let mut freqpoly_lines = 0;
146        while let Some(i) = rest.find("<polyline ") {
147            let end = i + rest[i..].find('>').unwrap();
148            let tag = &rest[i..end];
149            if tag.contains("data-geom=\"freqpoly\"") {
150                freqpoly_lines += 1;
151                let k = tag.find(" stroke=\"").unwrap() + 9;
152                colors.insert(tag[k..k + tag[k..].find('"').unwrap()].to_string());
153            }
154            rest = &rest[end + 1..];
155        }
156        assert_eq!(freqpoly_lines, 2, "{svg}");
157        assert_eq!(colors.len(), 2, "{colors:?}");
158    }
159}