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