Skip to main content

ferrix_app/widgets/
line_charts.rs

1/* line_charts.rs
2 *
3 * Copyright 2025-2026 Michail Krasnov <mskrasnov07@ya.ru>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21//! Linear charts
22
23use iced::{
24    Color as IColor, Element, Size, Theme,
25    widget::{
26        canvas::{Cache, Frame, Geometry},
27        container,
28    },
29};
30use plotters::prelude::*;
31use plotters_iced2::{Chart, ChartBuilder, ChartWidget, DrawingBackend};
32use std::collections::VecDeque;
33
34use crate::messages::Message;
35
36#[derive(Debug, Clone)]
37pub struct LineChart {
38    data: Vec<LineSeries>,
39    max_points: usize,
40    style: Style,
41}
42
43#[derive(Debug, Clone)]
44pub struct LineSeries {
45    name: String,
46    data: VecDeque<f64>,
47    color: RGBColor,
48    max_points: usize,
49}
50
51#[derive(Debug, Clone)]
52pub struct Style {
53    pub text_size: usize,
54    pub text_color: IColor,
55    pub y_axis_color: IColor,
56}
57
58impl Default for Style {
59    fn default() -> Self {
60        Self {
61            text_size: 11,
62            text_color: IColor::WHITE,
63            y_axis_color: IColor::WHITE,
64        }
65    }
66}
67
68fn to_rgbcolor(color: IColor) -> RGBColor {
69    let oc = color.into_rgba8();
70    RGBColor(oc[0], oc[1], oc[2])
71}
72
73impl LineSeries {
74    pub fn new(name: String, color: IColor, max_len: usize) -> Self {
75        Self {
76            name,
77            max_points: max_len,
78            color: to_rgbcolor(color),
79            data: VecDeque::with_capacity(max_len),
80        }
81    }
82
83    pub fn push(&mut self, value: f64) {
84        if self.data.len() > self.max_points {
85            self.data.pop_front();
86        }
87
88        self.data.push_back(value);
89    }
90}
91
92impl LineChart {
93    pub fn new() -> Self {
94        Self {
95            data: Vec::with_capacity(8),
96            max_points: 100,
97            style: Style::default(),
98        }
99    }
100
101    pub fn set_style(&mut self, theme: &Theme) {
102        let style = Style {
103            text_size: 12,
104            text_color: theme.palette().text,
105            y_axis_color: theme.palette().text,
106        };
107        self.style = style;
108    }
109
110    pub fn set_max_values(&mut self, value: usize) {
111        self.max_points = value;
112        for s in &mut self.data {
113            s.max_points = value;
114        }
115        self.update_axis();
116    }
117
118    pub fn series_count(&self) -> usize {
119        self.data.len()
120    }
121
122    pub fn push_series(&mut self, value: LineSeries) {
123        self.data.push(value);
124    }
125
126    pub fn push_to(&mut self, idx: usize, value: f64) {
127        if self.data.len() < idx {
128            return;
129        }
130        self.data[idx].push(value);
131    }
132
133    pub fn push_value(&mut self, value: f64, idx: usize) {
134        if self.data.len() < idx {
135            return;
136        }
137        self.update_axis();
138        self.data[idx].data.push_back(value);
139    }
140
141    pub fn view<'a>(&'a self) -> Element<'a, Message> {
142        let chart = ChartWidget::new(self);
143        container(chart).into()
144    }
145
146    fn update_axis(&mut self) {
147        'm: loop {
148            for s in &mut self.data {
149                if s.data.len() > self.max_points {
150                    s.data.pop_front();
151                } else {
152                    break 'm;
153                }
154            }
155        }
156    }
157}
158
159impl Chart<Message> for LineChart {
160    type State = ();
161
162    #[inline]
163    fn draw<R: plotters_iced2::Renderer, F: Fn(&mut Frame)>(
164        &self,
165        renderer: &R,
166        size: Size,
167        f: F,
168    ) -> Geometry {
169        renderer.draw_cache(&Cache::new(), size, f)
170    }
171
172    fn build_chart<DB: DrawingBackend>(&self, _state: &Self::State, mut builder: ChartBuilder<DB>) {
173        let mut chart = builder
174            .x_label_area_size(0)
175            .y_label_area_size(0)
176            .margin(20)
177            .build_cartesian_2d(0..(self.max_points), 0.0..100.0)
178            .expect("Failed to build chart");
179
180        chart
181            .configure_mesh()
182            .bold_line_style(to_rgbcolor(self.style.y_axis_color).mix(0.05))
183            .disable_x_axis()
184            .disable_x_mesh()
185            .light_line_style(TRANSPARENT)
186            .y_labels(8)
187            .x_labels(self.max_points)
188            .draw()
189            .expect("Failed to draw chart mesh");
190
191        for series in &self.data {
192            chart
193                .draw_series(
194                    AreaSeries::new(
195                        series.data.iter().enumerate().map(|x| (x.0, *x.1 as f64)),
196                        0.,
197                        plotters::style::TRANSPARENT,
198                    )
199                    .border_style(ShapeStyle::from(series.color).stroke_width(2)),
200                )
201                .expect("Failed to draw chart data")
202                .label(&series.name)
203                .legend(|(x, y)| {
204                    Rectangle::new([(x - 5, y - 5), (x + 5, y + 5)], series.color.filled())
205                });
206        }
207
208        if !self.data.is_empty() {
209            chart
210                .configure_series_labels()
211                .label_font(
212                    ("sans-serif", self.style.text_size as i32)
213                        .into_font()
214                        .color(&to_rgbcolor(self.style.text_color)),
215                )
216                .draw()
217                .expect("Failed to draw chart");
218        }
219    }
220}