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        column, container, grid, row, text,
28    },
29};
30use plotters::prelude::*;
31use plotters_iced2::{Chart, ChartBuilder, ChartWidget, DrawingBackend};
32use std::collections::VecDeque;
33
34use crate::{messages::Message, settings::ChartLineThickness};
35
36#[derive(Debug, Clone)]
37pub struct LineChart {
38    data: Vec<LineSeries>,
39    max_points: usize,
40    style: Style,
41    show_legend: bool,
42}
43
44#[derive(Debug, Clone)]
45pub struct LineSeries {
46    name: String,
47    data: VecDeque<f64>,
48    color: RGBColor,
49    max_points: usize,
50}
51
52#[derive(Debug, Clone)]
53pub struct Style {
54    pub y_axis_color: IColor,
55    pub line_thickness: u32,
56}
57
58impl Default for Style {
59    fn default() -> Self {
60        Self {
61            y_axis_color: IColor::WHITE,
62            line_thickness: ChartLineThickness::default().to_u32(),
63        }
64    }
65}
66
67fn to_rgbcolor(color: IColor) -> RGBColor {
68    let oc = color.into_rgba8();
69    RGBColor(oc[0], oc[1], oc[2])
70}
71
72fn to_icolor(color: RGBColor) -> IColor {
73    let (r, g, b) = (color.0, color.1, color.2);
74    IColor::from_rgb8(r, g, b)
75}
76
77impl LineSeries {
78    pub fn new(name: String, color: IColor, max_len: usize) -> Self {
79        Self {
80            name,
81            max_points: max_len,
82            color: to_rgbcolor(color),
83            data: VecDeque::with_capacity(max_len),
84        }
85    }
86
87    pub fn push(&mut self, value: f64) {
88        if self.data.len() > self.max_points {
89            self.data.pop_front();
90        }
91
92        self.data.push_back(value);
93    }
94}
95
96impl LineChart {
97    pub fn new() -> Self {
98        Self {
99            data: Vec::with_capacity(8),
100            max_points: 100,
101            style: Style::default(),
102            show_legend: true,
103        }
104    }
105
106    pub fn set_style(&mut self, theme: &Theme) {
107        let style = Style {
108            y_axis_color: theme.palette().text,
109            line_thickness: self.style.line_thickness,
110        };
111        self.style = style;
112    }
113
114    pub fn set_line_thickness(&mut self, thickness: ChartLineThickness) {
115        self.style.line_thickness = thickness.to_u32();
116    }
117
118    pub fn set_max_values(&mut self, value: usize) {
119        self.max_points = value;
120        for s in &mut self.data {
121            s.max_points = value;
122        }
123        self.update_axis();
124    }
125
126    pub fn series_count(&self) -> usize {
127        self.data.len()
128    }
129
130    pub fn push_series(&mut self, value: LineSeries) {
131        self.data.push(value);
132    }
133
134    pub fn push_to(&mut self, idx: usize, value: f64) {
135        if self.data.len() < idx {
136            return;
137        }
138        self.data[idx].push(value);
139    }
140
141    pub fn push_value(&mut self, value: f64, idx: usize) {
142        if self.data.len() < idx {
143            return;
144        }
145        self.update_axis();
146        self.data[idx].data.push_back(value);
147    }
148
149    pub fn set_show_legend(&mut self, show: bool) {
150        self.show_legend = show;
151    }
152
153    pub fn legend_parameters<'a>(&'a self) -> Element<'a, Message> {
154        let mut items = Vec::with_capacity(self.data.len());
155        let bold_font = {
156            let mut font = iced::Font::default();
157            font.weight = iced::font::Weight::Bold;
158            font
159        };
160
161        for line in &self.data {
162            let last = line.data.len() - 1;
163            let percent = line.data[last];
164            items.push(
165                row![
166                    text(format!("{}:", &line.name))
167                        .color(to_icolor(line.color))
168                        .font(bold_font),
169                    text(format!("{percent:.2}%")).style(move |t| if percent > 90. {
170                        text::danger(t)
171                    } else if percent > 70. {
172                        text::warning(t)
173                    } else {
174                        text::default(t)
175                    }),
176                ]
177                .spacing(3),
178            );
179        }
180
181        let mut gr = grid([]).columns(8).fluid(125.).height(iced::Length::Shrink);
182        for item in items {
183            gr = gr.push(item);
184        }
185        container(gr).into()
186    }
187
188    pub fn view<'a>(&'a self) -> Element<'a, Message> {
189        let chart = ChartWidget::new(self);
190        if self.show_legend {
191            column![chart, self.legend_parameters()].into()
192        } else {
193            chart.into()
194        }
195    }
196
197    fn update_axis(&mut self) {
198        'm: loop {
199            for s in &mut self.data {
200                if s.data.len() > self.max_points {
201                    s.data.pop_front();
202                } else {
203                    break 'm;
204                }
205            }
206        }
207    }
208}
209
210impl Chart<Message> for LineChart {
211    type State = ();
212
213    #[inline]
214    fn draw<R: plotters_iced2::Renderer, F: Fn(&mut Frame)>(
215        &self,
216        renderer: &R,
217        size: Size,
218        f: F,
219    ) -> Geometry {
220        renderer.draw_cache(&Cache::new(), size, f)
221    }
222
223    fn build_chart<DB: DrawingBackend>(&self, _state: &Self::State, mut builder: ChartBuilder<DB>) {
224        let mut chart = builder
225            .x_label_area_size(0)
226            .y_label_area_size(35)
227            .margin(5)
228            .build_cartesian_2d(0..(self.max_points), 0.0..100.0)
229            .expect("Failed to build chart");
230
231        chart
232            .configure_mesh()
233            .axis_style(to_rgbcolor(self.style.y_axis_color).mix(0.05))
234            .bold_line_style(to_rgbcolor(self.style.y_axis_color).mix(0.05))
235            .light_line_style(TRANSPARENT)
236            .disable_x_axis()
237            .disable_x_mesh()
238            .y_labels(8)
239            .x_labels(self.max_points)
240            .y_label_style(
241                ("sans-serif", 10)
242                    .into_font()
243                    .color(&to_rgbcolor(self.style.y_axis_color))
244                    .transform(FontTransform::Rotate270),
245            )
246            .y_label_formatter(&|y: &f64| format!("{y:.0}%"))
247            .draw()
248            .expect("Failed to draw chart mesh");
249
250        for series in &self.data {
251            chart
252                .draw_series(
253                    AreaSeries::new(
254                        series.data.iter().enumerate().map(|x| (x.0, *x.1 as f64)),
255                        0.,
256                        plotters::style::TRANSPARENT,
257                    )
258                    .border_style(
259                        ShapeStyle::from(series.color).stroke_width(self.style.line_thickness),
260                    ),
261                )
262                .expect("Failed to draw chart data")
263                .label(&series.name)
264                .legend(|(x, y)| {
265                    Rectangle::new([(x - 5, y - 3), (x + 15, y + 8)], series.color.filled())
266                });
267        }
268    }
269}