Skip to main content

ggplot_rs/geom/
lollipop.rs

1//! Lollipop and dumbbell charts: `geom_lollipop` (a stem from the baseline
2//! to the value, capped with a dot) and `geom_dumbbell` (a segment between
3//! two values per category, with a dot at each end). One layer each; both
4//! turn horizontal under `coord_flip`.
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::{DrawBackend, LineStyle, Linetype, PointStyle};
12use crate::render::RenderError;
13use crate::scale::ScaleSet;
14use crate::stat::identity::StatIdentity;
15use crate::stat::Stat;
16use crate::theme::Theme;
17
18use super::{Geom, GeomParams};
19
20/// Lollipop chart: map `x` and `y` (and optionally `color`).
21pub struct GeomLollipop {
22    pub color: (u8, u8, u8),
23    /// Dot radius in pixels.
24    pub point_size: f64,
25    pub stem_width: f64,
26    /// Value the stems start from.
27    pub baseline: f64,
28    pub alpha: f64,
29}
30
31impl Default for GeomLollipop {
32    fn default() -> Self {
33        GeomLollipop {
34            color: (31, 119, 180),
35            point_size: 5.0,
36            stem_width: 1.5,
37            baseline: 0.0,
38            alpha: 1.0,
39        }
40    }
41}
42
43/// Dumbbell chart: map `x` (category) plus `ymin` and `ymax` (the two
44/// values); `coord_flip` for the usual horizontal layout.
45pub struct GeomDumbbell {
46    /// Dot colour at `ymin`.
47    pub low_color: (u8, u8, u8),
48    /// Dot colour at `ymax`.
49    pub high_color: (u8, u8, u8),
50    /// Connecting segment colour.
51    pub line_color: (u8, u8, u8),
52    pub point_size: f64,
53    pub line_width: f64,
54    pub alpha: f64,
55}
56
57impl Default for GeomDumbbell {
58    fn default() -> Self {
59        GeomDumbbell {
60            low_color: (160, 160, 160),
61            high_color: (31, 119, 180),
62            line_color: (190, 190, 190),
63            point_size: 5.0,
64            line_width: 3.0,
65            alpha: 1.0,
66        }
67    }
68}
69
70fn dot(
71    backend: &mut dyn DrawBackend,
72    at: (f64, f64),
73    r: f64,
74    color: (u8, u8, u8),
75    alpha: f64,
76) -> Result<(), RenderError> {
77    backend.draw_circle(
78        at,
79        r,
80        &PointStyle {
81            color,
82            alpha,
83            filled: true,
84            ..Default::default()
85        },
86    )
87}
88
89fn stem(
90    backend: &mut dyn DrawBackend,
91    a: (f64, f64),
92    b: (f64, f64),
93    color: (u8, u8, u8),
94    width: f64,
95    alpha: f64,
96) -> Result<(), RenderError> {
97    backend.draw_line(
98        &[a, b],
99        &LineStyle {
100            color,
101            alpha,
102            width,
103            linetype: Linetype::Solid,
104            ..Default::default()
105        },
106    )
107}
108
109impl Geom for GeomLollipop {
110    fn draw(
111        &self,
112        data: &DataFrame,
113        coord: &dyn Coord,
114        scales: &ScaleSet,
115        _theme: &Theme,
116        backend: &mut dyn DrawBackend,
117    ) -> Result<(), RenderError> {
118        let x_col = data
119            .column("x")
120            .ok_or(RenderError::MissingAesthetic("x".into()))?;
121        let y_col = data
122            .column("y")
123            .ok_or(RenderError::MissingAesthetic("y".into()))?;
124        let color_col = data.column("color");
125        let area = backend.plot_area();
126        let xm = super::support::XMapper::new(data, scales);
127        let my = |v: &Value| scales.get(&Aesthetic::Y).map(|s| s.map(v)).unwrap_or(0.0);
128        let base = my(&Value::Float(self.baseline));
129        for i in 0..data.nrows() {
130            let (nx, ny) = (xm.map(&x_col[i], i), my(&y_col[i]));
131            if !(nx.is_finite() && ny.is_finite()) {
132                continue;
133            }
134            let color = color_col
135                .and_then(|c| scales.map_color(&Aesthetic::Color, &c[i]))
136                .unwrap_or(self.color);
137            let x_txt = super::tip_value(&x_col[i]);
138            super::set_mark(
139                backend,
140                Some(format!("{x_txt}: {}", super::tip_value(&y_col[i]))),
141                Some(x_txt),
142                super::series_key(data, i),
143                super::raw_value(&y_col[i]),
144            );
145            super::set_mark_formatted(
146                backend,
147                scales,
148                Some(&x_col[i]),
149                &Aesthetic::Y,
150                Some(&y_col[i]),
151            );
152            super::set_row_extras(backend, data, i);
153            let (top, bottom) = (
154                coord.transform((nx, ny), &area),
155                coord.transform((nx, base), &area),
156            );
157            stem(backend, bottom, top, color, self.stem_width, self.alpha)?;
158            dot(backend, top, self.point_size, color, self.alpha)?;
159        }
160        super::clear_mark(backend);
161        super::clear_row_extras(backend);
162        Ok(())
163    }
164
165    fn required_aes(&self) -> Vec<Aesthetic> {
166        vec![Aesthetic::X, Aesthetic::Y]
167    }
168    fn default_stat(&self) -> Box<dyn Stat> {
169        Box::new(StatIdentity)
170    }
171    fn default_position(&self) -> Box<dyn Position> {
172        Box::new(PositionIdentity)
173    }
174    fn default_params(&self) -> GeomParams {
175        GeomParams::default()
176    }
177    fn name(&self) -> &str {
178        "lollipop"
179    }
180    fn include_zero_baseline(&self) -> bool {
181        self.baseline == 0.0
182    }
183    fn consumes_tooltips(&self) -> bool {
184        true
185    }
186    fn set_series_color(&mut self, color: (u8, u8, u8)) {
187        self.color = color;
188    }
189}
190
191impl Geom for GeomDumbbell {
192    fn draw(
193        &self,
194        data: &DataFrame,
195        coord: &dyn Coord,
196        scales: &ScaleSet,
197        _theme: &Theme,
198        backend: &mut dyn DrawBackend,
199    ) -> Result<(), RenderError> {
200        let col = |n: &str| {
201            data.column(n)
202                .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
203        };
204        let (x_col, lo_col, hi_col) = (col("x")?, col("ymin")?, col("ymax")?);
205        let area = backend.plot_area();
206        let xm = super::support::XMapper::new(data, scales);
207        let my = |v: &Value| scales.get(&Aesthetic::Y).map(|s| s.map(v)).unwrap_or(0.0);
208        for i in 0..data.nrows() {
209            let (nx, lo, hi) = (xm.map(&x_col[i], i), my(&lo_col[i]), my(&hi_col[i]));
210            if ![nx, lo, hi].iter().all(|v| v.is_finite()) {
211                continue;
212            }
213            let x_txt = super::tip_value(&x_col[i]);
214            let (l, h) = (super::tip_value(&lo_col[i]), super::tip_value(&hi_col[i]));
215            super::set_mark(
216                backend,
217                Some(format!("{x_txt}: {l} → {h}")),
218                Some(x_txt),
219                super::series_key(data, i),
220                super::raw_value(&hi_col[i]),
221            );
222            super::set_row_extras(backend, data, i);
223            let (a, b) = (
224                coord.transform((nx, lo), &area),
225                coord.transform((nx, hi), &area),
226            );
227            stem(backend, a, b, self.line_color, self.line_width, self.alpha)?;
228            dot(backend, a, self.point_size, self.low_color, self.alpha)?;
229            dot(backend, b, self.point_size, self.high_color, self.alpha)?;
230        }
231        super::clear_mark(backend);
232        super::clear_row_extras(backend);
233        Ok(())
234    }
235
236    fn required_aes(&self) -> Vec<Aesthetic> {
237        vec![Aesthetic::X, Aesthetic::Ymin, Aesthetic::Ymax]
238    }
239    fn default_stat(&self) -> Box<dyn Stat> {
240        Box::new(StatIdentity)
241    }
242    fn default_position(&self) -> Box<dyn Position> {
243        Box::new(PositionIdentity)
244    }
245    fn default_params(&self) -> GeomParams {
246        GeomParams::default()
247    }
248    fn name(&self) -> &str {
249        "dumbbell"
250    }
251    fn consumes_tooltips(&self) -> bool {
252        true
253    }
254    fn set_series_color(&mut self, color: (u8, u8, u8)) {
255        self.high_color = color;
256    }
257}