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