Skip to main content

ggplot_rs/geom/
errorbarh.rs

1//! Horizontal error bars (`geom_errorbarh`): a line from `(xmin, y)` to
2//! `(xmax, y)` with vertical end caps — horizontal confidence intervals in a
3//! coefficient forest or a time-to-event plot.
4
5use crate::aes::Aesthetic;
6use crate::coord::Coord;
7use crate::data::DataFrame;
8use crate::position::identity::PositionIdentity;
9use crate::position::Position;
10use crate::render::backend::{DrawBackend, LineStyle, Linetype};
11use crate::render::RenderError;
12use crate::scale::ScaleSet;
13use crate::stat::identity::StatIdentity;
14use crate::stat::Stat;
15use crate::theme::Theme;
16
17use super::{Geom, GeomParams};
18
19/// Horizontal error bar geometry (requires `xmin`, `xmax`, `y`).
20pub struct GeomErrorbarh {
21    pub color: (u8, u8, u8),
22    /// Line width.
23    pub width: f64,
24    /// Half-height of the end caps, in normalized panel units (like
25    /// `GeomErrorbar::cap_width`).
26    pub cap_height: f64,
27    pub alpha: f64,
28}
29
30impl Default for GeomErrorbarh {
31    fn default() -> Self {
32        GeomErrorbarh {
33            color: (0, 0, 0),
34            width: 1.0,
35            cap_height: 0.02,
36            alpha: 1.0,
37        }
38    }
39}
40
41impl Geom for GeomErrorbarh {
42    fn draw(
43        &self,
44        data: &DataFrame,
45        coord: &dyn Coord,
46        scales: &ScaleSet,
47        _theme: &Theme,
48        backend: &mut dyn DrawBackend,
49    ) -> Result<(), RenderError> {
50        let y_col = data
51            .column("y")
52            .ok_or(RenderError::MissingAesthetic("y".into()))?;
53        let xmin_col = data
54            .column("xmin")
55            .ok_or(RenderError::MissingAesthetic("xmin".into()))?;
56        let xmax_col = data
57            .column("xmax")
58            .ok_or(RenderError::MissingAesthetic("xmax".into()))?;
59        let plot_area = backend.plot_area();
60        let x_scale = scales.get(&Aesthetic::X);
61        let y_scale = scales.get(&Aesthetic::Y);
62
63        for i in 0..data.nrows() {
64            let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
65            let nx0 = x_scale.map(|s| s.map(&xmin_col[i])).unwrap_or(0.0);
66            let nx1 = x_scale.map(|s| s.map(&xmax_col[i])).unwrap_or(0.0);
67            if !(ny.is_finite() && nx0.is_finite() && nx1.is_finite()) {
68                continue;
69            }
70            let color = data
71                .column("color")
72                .and_then(|c| scales.map_color(&Aesthetic::Color, &c[i]))
73                .unwrap_or(self.color);
74            let style = LineStyle {
75                color,
76                alpha: self.alpha,
77                width: self.width,
78                linetype: Linetype::Solid,
79                ..Default::default()
80            };
81            let series = super::series_key(data, i);
82            let range = format!(
83                "[{}, {}]",
84                super::tip_value(&xmin_col[i]),
85                super::tip_value(&xmax_col[i])
86            );
87            let tip = Some(match &series {
88                Some(s) => format!("{s}: {}: {range}", super::tip_value(&y_col[i])),
89                None => format!("{}: {range}", super::tip_value(&y_col[i])),
90            });
91            super::set_mark(
92                backend,
93                tip,
94                Some(super::tip_value(&y_col[i])),
95                series,
96                super::raw_value(&xmin_col[i])
97                    .zip(super::raw_value(&xmax_col[i]))
98                    .map(|(a, b)| format!("{a} {b}")),
99            );
100            // The "value" here is an [xmin, xmax] range, not a single raw
101            // value `Scale::format_value` can take — reset only.
102            super::set_mark_formatted(backend, scales, None, &Aesthetic::X, None);
103            super::set_row_extras(backend, data, i);
104            let h = self.cap_height;
105            let t = |p: (f64, f64)| coord.transform(p, &plot_area);
106            // One connected polyline per bar (cap, bar, cap) so each
107            // interval is a single hoverable mark.
108            backend.draw_line(
109                &[
110                    t((nx0, ny - h)),
111                    t((nx0, ny + h)),
112                    t((nx0, ny)),
113                    t((nx1, ny)),
114                    t((nx1, ny + h)),
115                    t((nx1, ny - h)),
116                ],
117                &style,
118            )?;
119        }
120        super::clear_mark(backend);
121        super::clear_row_extras(backend);
122        Ok(())
123    }
124
125    fn required_aes(&self) -> Vec<Aesthetic> {
126        vec![Aesthetic::Y, Aesthetic::Xmin, Aesthetic::Xmax]
127    }
128    fn default_stat(&self) -> Box<dyn Stat> {
129        Box::new(StatIdentity)
130    }
131    fn default_position(&self) -> Box<dyn Position> {
132        Box::new(PositionIdentity)
133    }
134    fn default_params(&self) -> GeomParams {
135        GeomParams::default()
136    }
137    fn name(&self) -> &str {
138        "errorbarh"
139    }
140
141    /// A mapped `Aes::tooltip` / `tooltip()` template replaces the default
142    /// `x: [lo, hi]` text (#485).
143    fn consumes_tooltips(&self) -> bool {
144        true
145    }
146    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
147        if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
148            self.color = ink;
149        }
150    }
151    fn set_series_color(&mut self, color: (u8, u8, u8)) {
152        self.color = color;
153    }
154}