1use 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}