Skip to main content

esoc_chart/chart/
bar.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Bar series (vertical and horizontal).
3
4use esoc_gfx::canvas::Canvas;
5use esoc_gfx::color::Color;
6use esoc_gfx::element::{DrawElement, Element};
7use esoc_gfx::geom::Rect;
8use esoc_gfx::layer::Layer;
9use esoc_gfx::style::{Fill, Stroke};
10use esoc_gfx::transform::CoordinateTransform;
11
12use crate::series::{DataBounds, SeriesRenderer};
13use crate::theme::Theme;
14
15/// A bar chart series.
16#[derive(Clone, Debug)]
17pub struct BarSeries {
18    /// X positions (bar centers).
19    pub x: Vec<f64>,
20    /// Bar heights (or lengths for horizontal).
21    pub heights: Vec<f64>,
22    /// Optional series label.
23    pub label: Option<String>,
24    /// Override color.
25    pub color: Option<Color>,
26    /// Bar width in data units.
27    pub bar_width: f64,
28    /// Whether to draw horizontal bars.
29    pub horizontal: bool,
30}
31
32impl BarSeries {
33    /// Create a new vertical bar series.
34    pub fn new(x: &[f64], heights: &[f64]) -> Self {
35        Self {
36            x: x.to_vec(),
37            heights: heights.to_vec(),
38            label: None,
39            color: None,
40            bar_width: 0.8,
41            horizontal: false,
42        }
43    }
44}
45
46impl SeriesRenderer for BarSeries {
47    fn data_bounds(&self) -> DataBounds {
48        if self.horizontal {
49            let y_min = self.x.iter().copied().fold(f64::INFINITY, f64::min) - self.bar_width / 2.0;
50            let y_max =
51                self.x.iter().copied().fold(f64::NEG_INFINITY, f64::max) + self.bar_width / 2.0;
52            let x_max = self.heights.iter().copied().fold(0.0_f64, f64::max);
53            DataBounds::new(0.0, x_max, y_min, y_max)
54        } else {
55            let x_min = self.x.iter().copied().fold(f64::INFINITY, f64::min) - self.bar_width / 2.0;
56            let x_max =
57                self.x.iter().copied().fold(f64::NEG_INFINITY, f64::max) + self.bar_width / 2.0;
58            let y_max = self.heights.iter().copied().fold(0.0_f64, f64::max);
59            DataBounds::new(x_min, x_max, 0.0, y_max)
60        }
61    }
62
63    fn render(
64        &self,
65        canvas: &mut Canvas,
66        transform: &CoordinateTransform,
67        theme: &Theme,
68        series_index: usize,
69    ) {
70        let color = self
71            .color
72            .unwrap_or_else(|| theme.palette.get(series_index));
73
74        for (&x, &h) in self.x.iter().zip(self.heights.iter()) {
75            if self.horizontal {
76                let p_start = transform.to_pixel(0.0, x - self.bar_width / 2.0);
77                let p_end = transform.to_pixel(h, x + self.bar_width / 2.0);
78                let rx = p_start.x.min(p_end.x);
79                let ry = p_start.y.min(p_end.y);
80                let rw = (p_end.x - p_start.x).abs();
81                let rh = (p_end.y - p_start.y).abs();
82                canvas.add(DrawElement::new(
83                    Element::Rect {
84                        rect: Rect::new(rx, ry, rw, rh),
85                        fill: Fill::Solid(color),
86                        stroke: Some(Stroke::solid(color.with_alpha(0.8), 0.5)),
87                        rx: 0.0,
88                    },
89                    Layer::Data,
90                ));
91            } else {
92                let p_top = transform.to_pixel(x - self.bar_width / 2.0, h);
93                let p_bottom = transform.to_pixel(x + self.bar_width / 2.0, 0.0);
94                let rx = p_top.x.min(p_bottom.x);
95                let ry = p_top.y.min(p_bottom.y);
96                let rw = (p_bottom.x - p_top.x).abs();
97                let rh = (p_bottom.y - p_top.y).abs();
98                canvas.add(DrawElement::new(
99                    Element::Rect {
100                        rect: Rect::new(rx, ry, rw, rh),
101                        fill: Fill::Solid(color),
102                        stroke: Some(Stroke::solid(color.with_alpha(0.8), 0.5)),
103                        rx: 0.0,
104                    },
105                    Layer::Data,
106                ));
107            }
108        }
109    }
110
111    fn label(&self) -> Option<&str> {
112        self.label.as_deref()
113    }
114}