Skip to main content

ggplot_rs/geom/
area.rs

1use crate::aes::Aesthetic;
2use crate::coord::Coord;
3use crate::data::{DataFrame, Value};
4use crate::position::stack::PositionStack;
5use crate::position::Position;
6use crate::render::backend::{DrawBackend, LineStyle, Linetype, RectStyle};
7use crate::render::RenderError;
8use crate::scale::ScaleSet;
9use crate::stat::align::StatAlign;
10use crate::stat::Stat;
11use crate::theme::Theme;
12
13use super::{Geom, GeomParams};
14
15/// Area geometry — filled polygon from line to x-axis baseline.
16pub struct GeomArea {
17    pub fill: (u8, u8, u8),
18    pub color: (u8, u8, u8),
19    pub alpha: f64,
20    pub line_width: f64,
21    /// How consecutive points are joined (straight by default; a smooth
22    /// `areaspline` with [`Interpolate::MonotoneCubic`](super::support::Interpolate::MonotoneCubic)).
23    pub interpolate: super::support::Interpolate,
24}
25
26impl Default for GeomArea {
27    fn default() -> Self {
28        GeomArea {
29            fill: (97, 156, 255),
30            color: (50, 50, 50),
31            alpha: 0.4,
32            line_width: 1.0,
33            interpolate: super::support::Interpolate::Linear,
34        }
35    }
36}
37
38impl Geom for GeomArea {
39    fn draw(
40        &self,
41        data: &DataFrame,
42        coord: &dyn Coord,
43        scales: &ScaleSet,
44        _theme: &Theme,
45        backend: &mut dyn DrawBackend,
46    ) -> Result<(), RenderError> {
47        let x_col = data
48            .column("x")
49            .ok_or(RenderError::MissingAesthetic("x".into()))?;
50        let y_col = data
51            .column("y")
52            .ok_or(RenderError::MissingAesthetic("y".into()))?;
53
54        // A fill/color aesthetic (or an explicit group) splits the data into one
55        // filled band per series — as in a grouped or stacked area chart.
56        let group_col = data
57            .column("fill")
58            .or_else(|| data.column("color"))
59            .or_else(|| data.column("group"));
60        // `ymin` is set by position="stack"/"fill" (the bottom of each segment);
61        // without it the band runs from the y=0 baseline.
62        let ymin_col = data.column("ymin");
63
64        let plot_area = backend.plot_area();
65        let x_scale = scales.get(&Aesthetic::X);
66        let y_scale = scales.get(&Aesthetic::Y);
67        let base_ny = y_scale.map(|s| s.map(&Value::Float(0.0))).unwrap_or(0.0);
68
69        // Group row indices by the series key, preserving first-seen order (which
70        // is the stacking order the position adjustment produced).
71        let mut groups: Vec<(String, Vec<usize>)> = Vec::new();
72        match group_col {
73            Some(gc) => {
74                for (i, v) in gc.iter().enumerate() {
75                    let key = v.to_group_key();
76                    match groups.iter_mut().find(|(k, _)| k == &key) {
77                        Some((_, idx)) => idx.push(i),
78                        None => groups.push((key, vec![i])),
79                    }
80                }
81            }
82            None => groups.push((String::new(), (0..data.nrows()).collect())),
83        }
84
85        for (_, indices) in &groups {
86            // Order this series left-to-right so the band is a clean polygon.
87            let mut idx = indices.clone();
88            idx.sort_by(|&a, &b| {
89                let xa = x_scale.map(|s| s.map(&x_col[a])).unwrap_or(0.0);
90                let xb = x_scale.map(|s| s.map(&x_col[b])).unwrap_or(0.0);
91                xa.total_cmp(&xb)
92            });
93
94            let fill = group_col
95                .and_then(|gc| scales.map_color(&Aesthetic::Fill, &gc[idx[0]]))
96                .or_else(|| {
97                    group_col.and_then(|gc| scales.map_color(&Aesthetic::Color, &gc[idx[0]]))
98                })
99                .unwrap_or(self.fill);
100
101            let mut upper_raw: Vec<(f64, f64)> = Vec::with_capacity(idx.len());
102            let mut lower_raw: Vec<(f64, f64)> = Vec::with_capacity(idx.len());
103            for &i in &idx {
104                let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
105                let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
106                let nb = ymin_col
107                    .and_then(|c| c[i].as_f64())
108                    .and_then(|v| y_scale.map(|s| s.map(&Value::Float(v))))
109                    .unwrap_or(base_ny);
110                upper_raw.push((nx, ny));
111                lower_raw.push((nx, nb));
112            }
113            let to_px = |pts: Vec<(f64, f64)>| -> Vec<(f64, f64)> {
114                super::support::interpolate(&pts, self.interpolate)
115                    .0
116                    .into_iter()
117                    .map(|p| coord.transform(p, &plot_area))
118                    .collect()
119            };
120            let (upper, mut lower) = (to_px(upper_raw), to_px(lower_raw));
121
122            let mut polygon = upper.clone();
123            lower.reverse();
124            polygon.extend(lower);
125            if polygon.len() >= 3 {
126                backend.draw_polygon(
127                    &polygon,
128                    &RectStyle {
129                        fill: Some(fill),
130                        stroke: None,
131                        stroke_width: 0.0,
132                        alpha: self.alpha,
133                        clip: true,
134                        corner_radius: 0.0,
135                    },
136                )?;
137            }
138            if upper.len() >= 2 {
139                // A single-series area keeps its dark outline; multi-series bands
140                // outline in their own fill so stacked bands stay legible.
141                let line_color = if groups.len() > 1 { fill } else { self.color };
142                backend.draw_line(
143                    &upper,
144                    &LineStyle {
145                        color: line_color,
146                        alpha: 1.0,
147                        width: self.line_width,
148                        linetype: Linetype::Solid,
149                        ..Default::default()
150                    },
151                )?;
152            }
153        }
154
155        Ok(())
156    }
157
158    fn required_aes(&self) -> Vec<Aesthetic> {
159        vec![Aesthetic::X, Aesthetic::Y]
160    }
161
162    fn default_stat(&self) -> Box<dyn Stat> {
163        // ggplot2 parity (#172): `geom_area()`'s default stat is
164        // `stat_align()`, so series sampled at different x stack without
165        // dipping where one has no row.
166        Box::new(StatAlign)
167    }
168    fn default_position(&self) -> Box<dyn Position> {
169        // ggplot2 parity (sipemu/ggplot-rs#171 review finding 5): `geom_area()`
170        // defaults to `position_stack()`, same as `geom_col()`/`geom_bar()`
171        // (#169, just above those geoms' own `default_position`). 0.23/0.24
172        // left this as `PositionIdentity`, the same gap #169 fixed for
173        // `geom_col()` — landing it in the same minor as #169 so every
174        // default-position parity break is in one release.
175        Box::new(PositionStack)
176    }
177    fn default_params(&self) -> GeomParams {
178        GeomParams::default()
179    }
180    fn name(&self) -> &str {
181        "area"
182    }
183
184    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
185        if let (true, Some(ink)) = (self.color == (50, 50, 50), el.ink) {
186            self.color = ink;
187        }
188    }
189
190    fn include_zero_baseline(&self) -> bool {
191        true
192    }
193
194    fn set_series_color(&mut self, color: (u8, u8, u8)) {
195        self.fill = color;
196    }
197}