Skip to main content

ggplot_rs/geom/
flags.rs

1//! Event flags on time series: `geom_flags`. Each row (`x`, `label`) is a
2//! small flag on a stem, pinned either to the x axis or — with
3//! [`GeomFlags::on_series`] — to a series' value at that x (linearly
4//! interpolated). Flags at the same x stack upward instead of overlapping.
5
6use crate::aes::Aesthetic;
7use crate::coord::Coord;
8use crate::data::{DataFrame, Value};
9use crate::position::identity::PositionIdentity;
10use crate::position::Position;
11use crate::render::backend::{
12    DrawBackend, FontFace, LineStyle, Linetype, PointStyle, RectStyle, TextAnchor, TextStyle,
13};
14use crate::render::RenderError;
15use crate::scale::ScaleSet;
16use crate::stat::identity::StatIdentity;
17use crate::stat::Stat;
18use crate::theme::Theme;
19
20use super::{Geom, GeomParams};
21
22/// Flag glyph.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
24pub enum FlagShape {
25    /// A pennant box holding the label.
26    #[default]
27    Flag,
28    /// A circle holding the label's first character (full label on hover).
29    CirclePin,
30}
31
32/// Event flags. Map `x` and `label`, optionally `color`.
33pub struct GeomFlags {
34    pub shape: FlagShape,
35    /// Series the flags sit on: `(x, y)` points, interpolated at each flag.
36    pub series: Option<Vec<(f64, f64)>>,
37    pub color: (u8, u8, u8),
38    pub fill: (u8, u8, u8),
39    pub label_size: f64,
40    /// Stem length in pixels (each stacked flag adds a box height).
41    pub stem: f64,
42}
43
44impl Default for GeomFlags {
45    fn default() -> Self {
46        GeomFlags {
47            shape: FlagShape::Flag,
48            series: None,
49            color: (31, 119, 180),
50            fill: (255, 255, 255),
51            label_size: 9.0,
52            stem: 18.0,
53        }
54    }
55}
56
57impl GeomFlags {
58    /// Pin flags to the series given by `x` and `y` values (numbers or
59    /// date-times; rows with a missing value are skipped).
60    pub fn on_series(mut self, x: &[Value], y: &[Value]) -> Self {
61        let mut pts: Vec<(f64, f64)> = x
62            .iter()
63            .zip(y)
64            .filter_map(|(a, b)| Some((a.as_f64()?, b.as_f64()?)))
65            .collect();
66        pts.sort_by(|a, b| a.0.total_cmp(&b.0));
67        self.series = Some(pts);
68        self
69    }
70}
71
72/// Linear interpolation of `pts` (sorted by x) at `x`, clamped at the ends.
73pub(crate) fn interpolate_at(pts: &[(f64, f64)], x: f64) -> Option<f64> {
74    let (first, last) = (pts.first()?, pts.last()?);
75    if x <= first.0 {
76        return Some(first.1);
77    }
78    if x >= last.0 {
79        return Some(last.1);
80    }
81    let k = pts.partition_point(|p| p.0 <= x);
82    let (a, b) = (pts[k - 1], pts[k]);
83    Some(if b.0 > a.0 {
84        a.1 + (b.1 - a.1) * (x - a.0) / (b.0 - a.0)
85    } else {
86        a.1
87    })
88}
89
90impl Geom for GeomFlags {
91    fn draw(
92        &self,
93        data: &DataFrame,
94        coord: &dyn Coord,
95        scales: &ScaleSet,
96        theme: &Theme,
97        backend: &mut dyn DrawBackend,
98    ) -> Result<(), RenderError> {
99        let x_col = data
100            .column("x")
101            .ok_or(RenderError::MissingAesthetic("x".into()))?;
102        let (label_col, color_col) = (data.column("label"), data.column("color"));
103        let area = backend.plot_area();
104        let mx = |v: &Value| scales.get(&Aesthetic::X).map(|s| s.map(v)).unwrap_or(0.5);
105        let my = |v: f64| {
106            scales
107                .get(&Aesthetic::Y)
108                .map(|s| s.map(&Value::Float(v)))
109                .unwrap_or(0.0)
110        };
111        let family = super::resolve_font_family("", theme);
112        let box_h = self.label_size + 7.0;
113        let mut order: Vec<usize> = (0..data.nrows()).collect();
114        order.sort_by(|&a, &b| mx(&x_col[a]).total_cmp(&mx(&x_col[b])));
115        // Anchors already used: (pixel x, flags stacked there).
116        let mut stacks: Vec<(f64, usize)> = Vec::new();
117        for i in order {
118            let nx = mx(&x_col[i]);
119            let ny = match (&self.series, x_col[i].as_f64()) {
120                (Some(pts), Some(x)) => interpolate_at(pts, x).map(my).unwrap_or(0.0),
121                _ => 0.0,
122            };
123            let anchor = coord.transform((nx, ny), &area);
124            if !(anchor.0.is_finite() && anchor.1.is_finite()) {
125                continue;
126            }
127            let level = match stacks.iter_mut().find(|s| (s.0 - anchor.0).abs() < 3.0) {
128                Some(s) => {
129                    s.1 += 1;
130                    s.1 - 1
131                }
132                None => {
133                    stacks.push((anchor.0, 1));
134                    0
135                }
136            };
137            let color = color_col
138                .and_then(|c| scales.map_color(&Aesthetic::Color, &c[i]))
139                .unwrap_or(self.color);
140            let label = label_col
141                .map(|c| super::tip_value(&c[i]))
142                .unwrap_or_default();
143            let top = (
144                anchor.0,
145                anchor.1 - self.stem - level as f64 * (box_h + 3.0),
146            );
147            super::set_mark(
148                backend,
149                Some(format!("{}: {label}", super::tip_value(&x_col[i]))),
150                Some(super::tip_value(&x_col[i])),
151                super::series_key(data, i),
152                None,
153            );
154            super::set_row_extras(backend, data, i);
155            backend.draw_line(
156                &[anchor, top],
157                &LineStyle {
158                    color,
159                    alpha: 1.0,
160                    width: 1.0,
161                    linetype: Linetype::Solid,
162                    ..Default::default()
163                },
164            )?;
165            let text = match self.shape {
166                FlagShape::Flag => label.clone(),
167                FlagShape::CirclePin => label.chars().next().map(String::from).unwrap_or_default(),
168            };
169            let center = match self.shape {
170                FlagShape::Flag => {
171                    let w = crate::render::text::text_width(&text, self.label_size) + 8.0;
172                    backend.draw_rect(
173                        (top.0, top.1 - box_h),
174                        (top.0 + w, top.1),
175                        &RectStyle {
176                            fill: Some(self.fill),
177                            stroke: Some(color),
178                            stroke_width: 1.0,
179                            alpha: 1.0,
180                            clip: false,
181                            corner_radius: 0.0,
182                        },
183                    )?;
184                    (top.0 + w / 2.0, top.1 - box_h / 2.0)
185                }
186                FlagShape::CirclePin => {
187                    let c = (top.0, top.1 - box_h / 2.0);
188                    backend.draw_circle(
189                        c,
190                        box_h / 2.0,
191                        &PointStyle {
192                            color,
193                            alpha: 1.0,
194                            filled: true,
195                            ..Default::default()
196                        },
197                    )?;
198                    c
199                }
200            };
201            if !text.is_empty() {
202                super::clear_mark(backend);
203                backend.draw_text(
204                    &text,
205                    center,
206                    &TextStyle {
207                        color: if self.shape == FlagShape::CirclePin {
208                            super::treemap::contrast_text(color)
209                        } else {
210                            color
211                        },
212                        size: self.label_size,
213                        anchor: TextAnchor::Middle,
214                        angle: 0.0,
215                        family: family.clone(),
216                        face: FontFace::Bold,
217                        bg_colour: None,
218                        bg_r: 0.0,
219                    },
220                )?;
221            }
222        }
223        super::clear_mark(backend);
224        super::clear_row_extras(backend);
225        Ok(())
226    }
227
228    fn required_aes(&self) -> Vec<Aesthetic> {
229        vec![Aesthetic::X]
230    }
231    fn default_stat(&self) -> Box<dyn Stat> {
232        Box::new(StatIdentity)
233    }
234    fn default_position(&self) -> Box<dyn Position> {
235        Box::new(PositionIdentity)
236    }
237    fn default_params(&self) -> GeomParams {
238        GeomParams::default()
239    }
240    fn name(&self) -> &str {
241        "flags"
242    }
243    fn consumes_tooltips(&self) -> bool {
244        true
245    }
246    fn set_series_color(&mut self, color: (u8, u8, u8)) {
247        self.color = color;
248    }
249    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
250        if let (true, Some(paper)) = (self.fill == (255, 255, 255), el.paper) {
251            self.fill = paper;
252        }
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn interpolation_and_clamping() {
262        let p = [(0.0, 0.0), (10.0, 100.0)];
263        assert_eq!(interpolate_at(&p, 5.0), Some(50.0));
264        assert_eq!(interpolate_at(&p, -3.0), Some(0.0));
265        assert_eq!(interpolate_at(&p, 30.0), Some(100.0));
266        assert_eq!(interpolate_at(&[], 1.0), None);
267    }
268}