Skip to main content

esoc_chart/chart/
errorbar.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Error bar series (symmetric and asymmetric errors).
3
4use esoc_gfx::canvas::Canvas;
5use esoc_gfx::color::Color;
6use esoc_gfx::element::DrawElement;
7use esoc_gfx::layer::Layer;
8use esoc_gfx::style::{Fill, Stroke};
9use esoc_gfx::transform::CoordinateTransform;
10
11use crate::series::{DataBounds, SeriesRenderer};
12use crate::theme::Theme;
13
14/// An error bar series showing data uncertainty.
15#[derive(Clone, Debug)]
16pub struct ErrorBarSeries {
17    /// X values.
18    pub x: Vec<f64>,
19    /// Y values (centers).
20    pub y: Vec<f64>,
21    /// Error magnitudes (symmetric if `err_neg` is `None`).
22    pub err: Vec<f64>,
23    /// Optional negative error magnitudes (for asymmetric errors).
24    pub err_neg: Option<Vec<f64>>,
25    /// Optional series label.
26    pub label: Option<String>,
27    /// Override color.
28    pub color: Option<Color>,
29    /// Cap width in pixels.
30    pub cap_width: f64,
31}
32
33impl ErrorBarSeries {
34    /// Create a new symmetric error bar series.
35    pub fn new(x: &[f64], y: &[f64], err: &[f64]) -> Self {
36        Self {
37            x: x.to_vec(),
38            y: y.to_vec(),
39            err: err.to_vec(),
40            err_neg: None,
41            label: None,
42            color: None,
43            cap_width: 6.0,
44        }
45    }
46
47    /// Create asymmetric error bars.
48    pub fn asymmetric(x: &[f64], y: &[f64], err_pos: &[f64], err_neg: &[f64]) -> Self {
49        Self {
50            x: x.to_vec(),
51            y: y.to_vec(),
52            err: err_pos.to_vec(),
53            err_neg: Some(err_neg.to_vec()),
54            label: None,
55            color: None,
56            cap_width: 6.0,
57        }
58    }
59}
60
61impl SeriesRenderer for ErrorBarSeries {
62    fn data_bounds(&self) -> DataBounds {
63        let x_min = self.x.iter().copied().fold(f64::INFINITY, f64::min);
64        let x_max = self.x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
65
66        let mut y_min = f64::INFINITY;
67        let mut y_max = f64::NEG_INFINITY;
68        for i in 0..self.y.len() {
69            let lo = self.y[i] - self.err_neg.as_ref().map_or(self.err[i], |en| en[i]);
70            let hi = self.y[i] + self.err[i];
71            y_min = y_min.min(lo);
72            y_max = y_max.max(hi);
73        }
74
75        DataBounds::new(x_min, x_max, y_min, y_max)
76    }
77
78    fn render(
79        &self,
80        canvas: &mut Canvas,
81        transform: &CoordinateTransform,
82        theme: &Theme,
83        series_index: usize,
84    ) {
85        let color = self
86            .color
87            .unwrap_or_else(|| theme.palette.get(series_index));
88        let stroke = Stroke::solid(color, 1.5);
89
90        for i in 0..self.x.len() {
91            let x = self.x[i];
92            let y = self.y[i];
93            let err_pos = self.err[i];
94            let err_neg = self.err_neg.as_ref().map_or(err_pos, |en| en[i]);
95
96            let p_center = transform.to_pixel(x, y);
97            let p_top = transform.to_pixel(x, y + err_pos);
98            let p_bot = transform.to_pixel(x, y - err_neg);
99
100            // Vertical error bar
101            canvas.add(DrawElement::line(
102                p_top.x,
103                p_top.y,
104                p_bot.x,
105                p_bot.y,
106                stroke.clone(),
107                Layer::Data,
108            ));
109
110            // Top cap
111            canvas.add(DrawElement::line(
112                p_top.x - self.cap_width / 2.0,
113                p_top.y,
114                p_top.x + self.cap_width / 2.0,
115                p_top.y,
116                stroke.clone(),
117                Layer::Data,
118            ));
119
120            // Bottom cap
121            canvas.add(DrawElement::line(
122                p_bot.x - self.cap_width / 2.0,
123                p_bot.y,
124                p_bot.x + self.cap_width / 2.0,
125                p_bot.y,
126                stroke.clone(),
127                Layer::Data,
128            ));
129
130            // Center dot
131            canvas.add(DrawElement::circle(
132                p_center.x,
133                p_center.y,
134                3.0,
135                Fill::Solid(color),
136                Layer::Data,
137            ));
138        }
139    }
140
141    fn label(&self) -> Option<&str> {
142        self.label.as_deref()
143    }
144}