Skip to main content

ggplot_rs/geom/
qq.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, PointShape, PointStyle};
7use crate::render::RenderError;
8use crate::scale::ScaleSet;
9use crate::stat::qq::{StatQQ, StatQQBand, StatQQLine};
10use crate::stat::Stat;
11use crate::theme::Theme;
12
13use super::{Geom, GeomParams};
14
15/// QQ plot geometry — draws points at theoretical vs sample quantiles.
16pub struct GeomQQ {
17    pub size: f64,
18    pub color: (u8, u8, u8),
19    pub alpha: f64,
20}
21
22impl Default for GeomQQ {
23    fn default() -> Self {
24        GeomQQ {
25            size: 3.0,
26            color: (0, 0, 0),
27            alpha: 1.0,
28        }
29    }
30}
31
32impl Geom for GeomQQ {
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        for i in 0..data.nrows() {
54            let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
55            let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
56            let (px, py) = coord.transform((nx, ny), &plot_area);
57
58            let pt_color = color_col
59                .and_then(|cc| scales.map_color(&Aesthetic::Color, &cc[i]))
60                .unwrap_or(self.color);
61
62            // Hover: "(theoretical, sample)", data-x = theoretical quantile,
63            // data-value = sample value.
64            let xy = format!(
65                "({}, {})",
66                super::tip_value(&x_col[i]),
67                super::tip_value(&y_col[i])
68            );
69            let series = super::series_key(data, i);
70            let tip = Some(match &series {
71                Some(s) => format!("{s}: {xy}"),
72                None => xy,
73            });
74            super::set_mark(
75                backend,
76                tip,
77                Some(super::tip_value(&x_col[i])),
78                series,
79                super::raw_value(&y_col[i]),
80            );
81            super::set_mark_formatted(
82                backend,
83                scales,
84                Some(&x_col[i]),
85                &Aesthetic::Y,
86                Some(&y_col[i]),
87            );
88
89            backend.draw_shape(
90                (px, py),
91                self.size,
92                &PointStyle {
93                    color: pt_color,
94                    alpha: self.alpha,
95                    filled: true,
96                    shape: PointShape::Circle,
97                },
98            )?;
99        }
100        super::clear_mark(backend);
101
102        Ok(())
103    }
104
105    fn required_aes(&self) -> Vec<Aesthetic> {
106        vec![Aesthetic::Y]
107    }
108
109    fn default_stat(&self) -> Box<dyn Stat> {
110        Box::new(StatQQ)
111    }
112
113    fn default_position(&self) -> Box<dyn Position> {
114        Box::new(PositionIdentity)
115    }
116
117    fn default_params(&self) -> GeomParams {
118        GeomParams::default()
119    }
120
121    fn name(&self) -> &str {
122        "qq"
123    }
124
125    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
126        if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
127            self.color = ink;
128        }
129    }
130
131    fn set_series_color(&mut self, color: (u8, u8, u8)) {
132        self.color = color;
133    }
134}
135
136/// QQ line geometry — reference line through Q1/Q3 on the QQ plot.
137pub struct GeomQQLine {
138    pub color: (u8, u8, u8),
139    pub width: f64,
140    pub alpha: f64,
141}
142
143impl Default for GeomQQLine {
144    fn default() -> Self {
145        GeomQQLine {
146            color: (255, 0, 0),
147            width: 1.0,
148            alpha: 1.0,
149        }
150    }
151}
152
153impl Geom for GeomQQLine {
154    fn draw(
155        &self,
156        data: &DataFrame,
157        coord: &dyn Coord,
158        scales: &ScaleSet,
159        _theme: &Theme,
160        backend: &mut dyn DrawBackend,
161    ) -> Result<(), RenderError> {
162        let x_col = data
163            .column("x")
164            .ok_or(RenderError::MissingAesthetic("x".into()))?;
165        let y_col = data
166            .column("y")
167            .ok_or(RenderError::MissingAesthetic("y".into()))?;
168
169        let plot_area = backend.plot_area();
170        let x_scale = scales.get(&Aesthetic::X);
171        let y_scale = scales.get(&Aesthetic::Y);
172
173        // One line per group (colour / group level).
174        for rows in super::support::row_groups(data) {
175            let points: Vec<(f64, f64)> = rows
176                .iter()
177                .map(|&i| {
178                    let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
179                    let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
180                    coord.transform((nx, ny), &plot_area)
181                })
182                .collect();
183            let first = rows[0];
184            let color = data
185                .column("color")
186                .and_then(|c| scales.map_color(&Aesthetic::Color, &c[first]))
187                .unwrap_or(self.color);
188            let series = super::series_key(data, first);
189            super::set_mark(backend, series.clone(), None, series, None);
190            super::set_mark_formatted(backend, scales, None, &Aesthetic::Y, None);
191            if points.len() >= 2 {
192                backend.draw_line(
193                    &points,
194                    &LineStyle {
195                        color,
196                        alpha: self.alpha,
197                        width: self.width,
198                        linetype: Linetype::Dashed,
199                        ..Default::default()
200                    },
201                )?;
202            }
203        }
204        super::clear_mark(backend);
205
206        Ok(())
207    }
208
209    fn required_aes(&self) -> Vec<Aesthetic> {
210        vec![Aesthetic::Y]
211    }
212
213    fn default_stat(&self) -> Box<dyn Stat> {
214        Box::new(StatQQLine)
215    }
216
217    fn default_position(&self) -> Box<dyn Position> {
218        Box::new(PositionIdentity)
219    }
220
221    fn default_params(&self) -> GeomParams {
222        GeomParams::default()
223    }
224
225    fn name(&self) -> &str {
226        "qq_line"
227    }
228
229    fn set_series_color(&mut self, color: (u8, u8, u8)) {
230        self.color = color;
231    }
232}
233
234/// QQ confidence band (qqplotr's `geom_qq_band`): a filled envelope between
235/// `ymin` and `ymax` along the theoretical quantiles, one polygon per group.
236/// Default stat [`StatQQBand`] (pointwise normal band at 95%). `±Inf`
237/// bounds (a KS band hitting probability 0 or 1) extend to the panel edge.
238pub struct GeomQQBand {
239    pub fill: (u8, u8, u8),
240    pub alpha: f64,
241}
242
243impl Default for GeomQQBand {
244    fn default() -> Self {
245        // qqplotr: rgb(0.6, 0.6, 0.6, 0.5)
246        GeomQQBand {
247            fill: (153, 153, 153),
248            alpha: 0.5,
249        }
250    }
251}
252
253impl Geom for GeomQQBand {
254    fn draw(
255        &self,
256        data: &DataFrame,
257        coord: &dyn Coord,
258        scales: &ScaleSet,
259        _theme: &Theme,
260        backend: &mut dyn DrawBackend,
261    ) -> Result<(), RenderError> {
262        super::ribbon::draw_bands(
263            data,
264            coord,
265            scales,
266            backend,
267            self.fill,
268            self.alpha,
269            super::ribbon::BandShape::Linear,
270        )
271    }
272
273    fn required_aes(&self) -> Vec<Aesthetic> {
274        vec![Aesthetic::Y]
275    }
276
277    fn default_stat(&self) -> Box<dyn Stat> {
278        Box::new(StatQQBand::default())
279    }
280
281    fn default_position(&self) -> Box<dyn Position> {
282        Box::new(PositionIdentity)
283    }
284
285    fn default_params(&self) -> GeomParams {
286        GeomParams::default()
287    }
288
289    fn name(&self) -> &str {
290        "qq_band"
291    }
292
293    fn allows_infinite(&self) -> bool {
294        true
295    }
296
297    fn set_series_color(&mut self, color: (u8, u8, u8)) {
298        self.fill = color;
299    }
300}