Skip to main content

esoc_chart/chart/
line.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Line series.
3
4use esoc_gfx::canvas::Canvas;
5use esoc_gfx::color::Color;
6use esoc_gfx::element::{DrawElement, Element};
7use esoc_gfx::geom::Point;
8use esoc_gfx::layer::Layer;
9use esoc_gfx::style::{DashPattern, Stroke};
10use esoc_gfx::transform::CoordinateTransform;
11
12use crate::series::{DataBounds, SeriesRenderer};
13use crate::theme::Theme;
14
15/// A line series connecting data points with a polyline.
16#[derive(Clone, Debug)]
17pub struct LineSeries {
18    /// X values.
19    pub x: Vec<f64>,
20    /// Y values.
21    pub y: Vec<f64>,
22    /// Optional series label.
23    pub label: Option<String>,
24    /// Override color.
25    pub color: Option<Color>,
26    /// Override line width.
27    pub width: Option<f64>,
28    /// Dash pattern.
29    pub dash: Option<DashPattern>,
30}
31
32impl LineSeries {
33    /// Create a new line series.
34    pub fn new(x: &[f64], y: &[f64]) -> Self {
35        Self {
36            x: x.to_vec(),
37            y: y.to_vec(),
38            label: None,
39            color: None,
40            width: None,
41            dash: None,
42        }
43    }
44
45    /// Build a stroke from this series' config and the theme.
46    pub fn build_stroke(&self, theme: &Theme, series_index: usize) -> Stroke {
47        let color = self
48            .color
49            .unwrap_or_else(|| theme.palette.get(series_index));
50        let width = self.width.unwrap_or(theme.line_width);
51        match &self.dash {
52            Some(dash) => Stroke::dashed(color, width, &dash.dashes),
53            None => Stroke::solid(color, width),
54        }
55    }
56}
57
58impl SeriesRenderer for LineSeries {
59    fn data_bounds(&self) -> DataBounds {
60        let x_min = self.x.iter().copied().fold(f64::INFINITY, f64::min);
61        let x_max = self.x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
62        let y_min = self.y.iter().copied().fold(f64::INFINITY, f64::min);
63        let y_max = self.y.iter().copied().fold(f64::NEG_INFINITY, f64::max);
64        DataBounds::new(x_min, x_max, y_min, y_max)
65    }
66
67    fn render(
68        &self,
69        canvas: &mut Canvas,
70        transform: &CoordinateTransform,
71        theme: &Theme,
72        series_index: usize,
73    ) {
74        if self.x.is_empty() {
75            return;
76        }
77
78        let points: Vec<Point> = self
79            .x
80            .iter()
81            .zip(self.y.iter())
82            .map(|(&x, &y)| transform.to_pixel(x, y))
83            .collect();
84
85        let stroke = self.build_stroke(theme, series_index);
86        canvas.add(DrawElement::new(
87            Element::polyline(points, stroke),
88            Layer::Data,
89        ));
90    }
91
92    fn label(&self) -> Option<&str> {
93        self.label.as_deref()
94    }
95}