Skip to main content

ggplot_rs/geom/
density.rs

1use crate::aes::Aesthetic;
2use crate::coord::Coord;
3use crate::data::{DataFrame, Value};
4use crate::position::identity::PositionIdentity;
5use crate::position::Position;
6use crate::render::backend::{DrawBackend, LineStyle, Linetype, PointShape, PointStyle, RectStyle};
7use crate::render::RenderError;
8use crate::scale::ScaleSet;
9use crate::stat::density::StatDensity;
10use crate::stat::Stat;
11use crate::theme::Theme;
12
13use super::{Geom, GeomParams};
14
15/// Density curve geometry — filled area under a kernel density estimate.
16pub struct GeomDensity {
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 GeomDensity {
24    fn default() -> Self {
25        GeomDensity {
26            fill: (97, 156, 255),
27            color: (50, 50, 50),
28            alpha: 0.3,
29            line_width: 1.0,
30        }
31    }
32}
33
34impl Geom for GeomDensity {
35    fn key_fill(&self) -> Option<((u8, u8, u8), f64)> {
36        Some((self.fill, self.alpha))
37    }
38
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        let fill_col = data.column("fill");
54        let color_col = data.column("color");
55
56        let plot_area = backend.plot_area();
57        let x_scale = scales.get(&Aesthetic::X);
58        let y_scale = scales.get(&Aesthetic::Y);
59
60        let base_ny = y_scale.map(|s| s.map(&Value::Float(0.0))).unwrap_or(0.0);
61
62        // Determine groups from color or fill column
63        let group_col = color_col.or(fill_col);
64        let groups: Vec<(String, Vec<usize>)> = if let Some(gc) = group_col {
65            let mut groups: Vec<(String, Vec<usize>)> = Vec::new();
66            for (i, v) in gc.iter().enumerate() {
67                let key = v.to_group_key();
68                if let Some(entry) = groups.iter_mut().find(|(k, _)| k == &key) {
69                    entry.1.push(i);
70                } else {
71                    groups.push((key, vec![i]));
72                }
73            }
74            groups
75        } else {
76            // Single group with all indices
77            vec![("".to_string(), (0..data.nrows()).collect())]
78        };
79
80        for (group_label, indices) in &groups {
81            if indices.is_empty() {
82                continue;
83            }
84            let first_idx = indices[0];
85
86            // Resolve fill: use fill scale if fill column exists, else use color scale, else default
87            let fill_color = fill_col
88                .and_then(|fc| scales.map_color(&Aesthetic::Fill, &fc[first_idx]))
89                .or_else(|| {
90                    color_col.and_then(|cc| scales.map_color(&Aesthetic::Color, &cc[first_idx]))
91                })
92                .unwrap_or(self.fill);
93
94            // Resolve line: use color scale if color column exists, else use fill scale, else default
95            let line_color = color_col
96                .and_then(|cc| scales.map_color(&Aesthetic::Color, &cc[first_idx]))
97                .or_else(|| {
98                    fill_col.and_then(|fc| scales.map_color(&Aesthetic::Fill, &fc[first_idx]))
99                })
100                .unwrap_or(self.color);
101
102            let mut upper: Vec<(f64, f64)> = Vec::new();
103            let mut lower: Vec<(f64, f64)> = Vec::new();
104
105            for &i in indices {
106                let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
107                let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
108                upper.push(coord.transform((nx, ny), &plot_area));
109                lower.push(coord.transform((nx, base_ny), &plot_area));
110            }
111
112            // Filled polygon
113            let mut polygon = upper.clone();
114            lower.reverse();
115            polygon.extend(lower);
116
117            if polygon.len() >= 3 {
118                backend.draw_polygon(
119                    &polygon,
120                    &RectStyle {
121                        fill: Some(fill_color),
122                        stroke: None,
123                        stroke_width: 0.0,
124                        alpha: self.alpha,
125                        clip: true,
126                        corner_radius: 0.0,
127                    },
128                )?;
129            }
130
131            // Top line
132            if upper.len() >= 2 {
133                backend.draw_line(
134                    &upper,
135                    &LineStyle {
136                        color: line_color,
137                        alpha: 1.0,
138                        width: self.line_width,
139                        linetype: Linetype::Solid,
140                        ..Default::default()
141                    },
142                )?;
143            }
144
145            // Sparse invisible hover points: the curve carries no marks, so emit a
146            // handful of transparent points along it (tagged with the x value and
147            // density) for the axis-pointer to read on hover.
148            let step = (indices.len() / 48).max(1);
149            for (k, &i) in indices.iter().enumerate() {
150                if k % step != 0 {
151                    continue;
152                }
153                let dens = y_col[i]
154                    .as_f64()
155                    .map(|f| format!("{f:.4}"))
156                    .unwrap_or_default();
157                let tip = if group_label.is_empty() {
158                    format!("density: {dens}")
159                } else {
160                    format!("{group_label}: {dens}")
161                };
162                super::set_mark(
163                    backend,
164                    Some(tip),
165                    Some(super::tip_value(&x_col[i])),
166                    super::series_key(data, i),
167                    super::measured_value(data, i),
168                );
169                super::set_mark_formatted(
170                    backend,
171                    scales,
172                    Some(&x_col[i]),
173                    &Aesthetic::Y,
174                    super::measured_raw(data, i),
175                );
176                backend.draw_shape(
177                    upper[k],
178                    0.6,
179                    &PointStyle {
180                        color: line_color,
181                        alpha: 0.0,
182                        filled: true,
183                        shape: PointShape::Circle,
184                    },
185                )?;
186            }
187            super::clear_mark(backend);
188        }
189
190        Ok(())
191    }
192
193    fn required_aes(&self) -> Vec<Aesthetic> {
194        vec![Aesthetic::X]
195    }
196
197    fn default_stat(&self) -> Box<dyn Stat> {
198        Box::new(StatDensity::default())
199    }
200
201    fn default_position(&self) -> Box<dyn Position> {
202        Box::new(PositionIdentity)
203    }
204
205    fn default_params(&self) -> GeomParams {
206        GeomParams::default()
207    }
208
209    fn name(&self) -> &str {
210        "density"
211    }
212
213    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
214        if let (true, Some(ink)) = (self.color == (50, 50, 50), el.ink) {
215            self.color = ink;
216        }
217    }
218
219    fn set_series_color(&mut self, color: (u8, u8, u8)) {
220        self.fill = color;
221    }
222}