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 set_series_color(&mut self, color: (u8, u8, u8)) {
126        self.color = color;
127    }
128}
129
130/// QQ line geometry — reference line through Q1/Q3 on the QQ plot.
131pub struct GeomQQLine {
132    pub color: (u8, u8, u8),
133    pub width: f64,
134    pub alpha: f64,
135}
136
137impl Default for GeomQQLine {
138    fn default() -> Self {
139        GeomQQLine {
140            color: (255, 0, 0),
141            width: 1.0,
142            alpha: 1.0,
143        }
144    }
145}
146
147impl Geom for GeomQQLine {
148    fn draw(
149        &self,
150        data: &DataFrame,
151        coord: &dyn Coord,
152        scales: &ScaleSet,
153        _theme: &Theme,
154        backend: &mut dyn DrawBackend,
155    ) -> Result<(), RenderError> {
156        let x_col = data
157            .column("x")
158            .ok_or(RenderError::MissingAesthetic("x".into()))?;
159        let y_col = data
160            .column("y")
161            .ok_or(RenderError::MissingAesthetic("y".into()))?;
162
163        let plot_area = backend.plot_area();
164        let x_scale = scales.get(&Aesthetic::X);
165        let y_scale = scales.get(&Aesthetic::Y);
166
167        // One line per group (colour / group level).
168        for rows in super::support::row_groups(data) {
169            let points: Vec<(f64, f64)> = rows
170                .iter()
171                .map(|&i| {
172                    let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
173                    let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
174                    coord.transform((nx, ny), &plot_area)
175                })
176                .collect();
177            let first = rows[0];
178            let color = data
179                .column("color")
180                .and_then(|c| scales.map_color(&Aesthetic::Color, &c[first]))
181                .unwrap_or(self.color);
182            let series = super::series_key(data, first);
183            super::set_mark(backend, series.clone(), None, series, None);
184            super::set_mark_formatted(backend, scales, None, &Aesthetic::Y, None);
185            if points.len() >= 2 {
186                backend.draw_line(
187                    &points,
188                    &LineStyle {
189                        color,
190                        alpha: self.alpha,
191                        width: self.width,
192                        linetype: Linetype::Dashed,
193                        ..Default::default()
194                    },
195                )?;
196            }
197        }
198        super::clear_mark(backend);
199
200        Ok(())
201    }
202
203    fn required_aes(&self) -> Vec<Aesthetic> {
204        vec![Aesthetic::Y]
205    }
206
207    fn default_stat(&self) -> Box<dyn Stat> {
208        Box::new(StatQQLine)
209    }
210
211    fn default_position(&self) -> Box<dyn Position> {
212        Box::new(PositionIdentity)
213    }
214
215    fn default_params(&self) -> GeomParams {
216        GeomParams::default()
217    }
218
219    fn name(&self) -> &str {
220        "qq_line"
221    }
222
223    fn set_series_color(&mut self, color: (u8, u8, u8)) {
224        self.color = color;
225    }
226}
227
228/// QQ confidence band (qqplotr's `geom_qq_band`): a filled envelope between
229/// `ymin` and `ymax` along the theoretical quantiles, one polygon per group.
230/// Default stat [`StatQQBand`] (pointwise normal band at 95%). `±Inf`
231/// bounds (a KS band hitting probability 0 or 1) extend to the panel edge.
232pub struct GeomQQBand {
233    pub fill: (u8, u8, u8),
234    pub alpha: f64,
235}
236
237impl Default for GeomQQBand {
238    fn default() -> Self {
239        // qqplotr: rgb(0.6, 0.6, 0.6, 0.5)
240        GeomQQBand {
241            fill: (153, 153, 153),
242            alpha: 0.5,
243        }
244    }
245}
246
247impl Geom for GeomQQBand {
248    fn draw(
249        &self,
250        data: &DataFrame,
251        coord: &dyn Coord,
252        scales: &ScaleSet,
253        _theme: &Theme,
254        backend: &mut dyn DrawBackend,
255    ) -> Result<(), RenderError> {
256        super::ribbon::draw_bands(
257            data,
258            coord,
259            scales,
260            backend,
261            self.fill,
262            self.alpha,
263            super::ribbon::BandShape::Linear,
264        )
265    }
266
267    fn required_aes(&self) -> Vec<Aesthetic> {
268        vec![Aesthetic::Y]
269    }
270
271    fn default_stat(&self) -> Box<dyn Stat> {
272        Box::new(StatQQBand::default())
273    }
274
275    fn default_position(&self) -> Box<dyn Position> {
276        Box::new(PositionIdentity)
277    }
278
279    fn default_params(&self) -> GeomParams {
280        GeomParams::default()
281    }
282
283    fn name(&self) -> &str {
284        "qq_band"
285    }
286
287    fn allows_infinite(&self) -> bool {
288        true
289    }
290
291    fn set_series_color(&mut self, color: (u8, u8, u8)) {
292        self.fill = color;
293    }
294}