1use iced::{
39 Color, Pixels, Theme,
40 alignment::{Horizontal, Vertical},
41 time::Instant,
42};
43use iced_aksel::{
44 Axis, Chart, Measure, PlotPoint, State, Stroke,
45 axis::{self, TickResult},
46 plot::{Plot, PlotData},
47 scale::Linear,
48 shape::{Area, Label, Polygon, Polyline, Rectangle},
49 style::DashStyle,
50};
51use std::collections::{HashMap, VecDeque};
52
53type AxisID = String;
54
55#[derive(Debug, Clone)]
56pub struct LineSeries {
57 pub name: String,
58 pub current_values: VecDeque<f64>,
59 pub target_values: VecDeque<f64>,
60 pub y_key: String,
61 pub color: Color,
62 pub width: f32,
63 pub show_markers: bool,
64 pub fill_color: Option<Color>,
65 pub max_displayed_values: usize,
66}
67
68impl LineSeries {
69 pub fn new(name: impl Into<String>, color: Color, mx: usize) -> Self {
70 Self {
71 name: name.into(),
72 current_values: VecDeque::new(),
73 target_values: VecDeque::new(),
74 y_key: "Y".to_string(),
75 color,
76 width: 1.5,
77 show_markers: false,
78 fill_color: None,
79 max_displayed_values: mx,
80 }
81 }
82
83 pub fn set_max_values(&mut self, mx: usize) {
84 self.max_displayed_values = mx;
85 }
86
87 pub fn axis(mut self, y_id: impl Into<String>) -> Self {
88 self.y_key = y_id.into();
89 self
90 }
91
92 pub const fn width(mut self, width: f32) -> Self {
93 self.width = width;
94 self
95 }
96
97 pub const fn markers(mut self, show: bool) -> Self {
98 self.show_markers = show;
99 self
100 }
101
102 pub const fn fill(mut self, color: Color) -> Self {
103 self.fill_color = Some(color);
104 self
105 }
106
107 pub fn push(&mut self, val: f64) {
108 let start_val = self.current_values.iter().last().copied().unwrap_or(0.);
109 self.current_values.push_back(start_val);
110 self.target_values.push_back(val);
111
112 if self.current_values.len() >= self.max_displayed_values + 1 {
113 self.current_values.pop_front();
114 }
115 if self.target_values.len() >= self.max_displayed_values + 1 {
116 self.target_values.pop_front();
117 }
118 }
119
120 pub fn extend(&mut self, vals: impl IntoIterator<Item = f64>) {
121 for v in vals {
122 self.push(v);
123 }
124 }
125
126 fn tick(&mut self, alpha: f64) {
127 if self.current_values.len() < self.target_values.len() {
128 self.current_values.resize(self.target_values.len(), 0.);
129 }
130 for (cur, tgt) in self
131 .current_values
132 .iter_mut()
133 .zip(self.target_values.iter())
134 {
135 let diff = *tgt - *cur;
136 if diff.abs() > 1e-5 {
137 *cur += diff * alpha;
138 } else {
139 *cur = *tgt;
140 }
141 }
142 }
143
144 fn snap(&mut self) {
145 self.current_values = self.target_values.clone();
146 }
147}
148
149#[derive(Debug)]
151pub struct LineChart {
152 state: State<AxisID, f64>,
153 series: Vec<LineSeries>,
154 labels: Vec<String>,
155 defined_axes: Vec<String>,
156 show_legend: bool,
157 show_y_axis: bool,
158
159 animation_speed: Option<f64>,
163 last_tick: Option<Instant>,
164
165 current_stack_factor: f64,
169 target_stack_factor: f64,
170
171 fill_enabled: bool,
172 current_fill_alpha: f32,
173 target_fill_alpha: f32,
174
175 current_x_domain: (f64, f64),
176 current_y_domains: HashMap<String, (f64, f64)>,
177
178 max_values: usize,
179 max_y_val: Option<f64>,
180}
181
182impl LineChart {
183 pub const X: &'static str = "X";
184 pub const Y: &'static str = "Y";
185
186 pub fn new() -> Self {
187 Self {
188 state: State::new(),
189 series: vec![],
190 labels: vec![],
191 defined_axes: vec![],
192 show_legend: true,
193 show_y_axis: true,
194 animation_speed: None,
195 last_tick: None,
196 current_stack_factor: 0.,
197 target_stack_factor: 0.,
198 fill_enabled: false,
199 current_fill_alpha: 0.,
200 target_fill_alpha: 0.2,
201 current_x_domain: (0.0, 1.0),
202 current_y_domains: HashMap::new(),
203 max_values: 50,
204 max_y_val: None,
205 }
206 }
207
208 pub fn max_values(mut self, mx: usize) -> Self {
209 self.max_values = mx;
210 for i in &mut self.series {
211 i.set_max_values(self.max_values);
212 }
213 self
214 }
215
216 pub fn set_max_values(&mut self, mx: usize) {
217 self.max_values = mx;
218 for i in &mut self.series {
219 i.set_max_values(self.max_values);
220 }
221 }
222
223 pub fn set_max_y(&mut self, y: f64) {
224 self.max_y_val = Some(y);
225 }
226
227 pub fn with_default_axes(mut self) -> Self {
228 self.with_axis(
229 Self::X,
230 Axis::new(Linear::new(0., 1.), axis::Position::Bottom),
231 );
232 self.with_axis(Self::Y, y_axis(0., 1.));
233 self
234 }
235
236 pub const fn animated(mut self, speed: f64) -> Self {
237 self.animation_speed = Some(speed.max(0.0).min(1.0));
238 self
239 }
240
241 pub const fn legend(mut self, show: bool) -> Self {
242 self.show_legend = show;
243 self
244 }
245
246 pub const fn y_axis(mut self, show: bool) -> Self {
247 self.show_y_axis = show;
248 self
249 }
250
251 pub fn fill_alpha(mut self, alpha: f32) -> Self {
252 self.target_fill_alpha = alpha.clamp(0., 1.);
253 if self.fill_enabled && self.animation_speed.is_none() {
256 self.current_fill_alpha = self.target_fill_alpha;
257 self.update_series_fill();
258 }
259 self
260 }
261
262 pub fn toggle_fill(&mut self) {
263 self.fill_enabled = !self.fill_enabled;
264 let target = if self.fill_enabled {
265 self.target_fill_alpha
266 } else {
267 0.
268 };
269 if self.animation_speed.is_none() {
270 self.current_fill_alpha = target;
271 self.update_series_fill();
272 }
273 }
274
275 fn update_series_fill(&mut self) {
276 for s in &mut self.series {
277 let mut color = s.color;
278 color.a = self.current_fill_alpha;
279 s.fill_color = if self.current_fill_alpha > 0. {
280 Some(color)
281 } else {
282 None
283 };
284 }
285 }
286
287 pub fn stacked(mut self, stacked: bool) -> Self {
288 self.set_stacked(stacked);
289 self
290 }
291
292 pub fn set_stacked(&mut self, stacked: bool) {
293 self.target_stack_factor = if stacked { 1. } else { 0. };
294 self.fill_enabled = stacked;
296
297 if self.animation_speed.is_none() {
298 self.current_stack_factor = self.target_stack_factor;
299 self.current_fill_alpha = if self.fill_enabled {
300 self.target_fill_alpha
301 } else {
302 0.
303 };
304 self.update_series_fill();
305 self.snap_axes();
306 }
307 }
308
309 pub fn toggle_stacked(&mut self) {
310 let new_stacked_state = self.target_stack_factor <= 0.5;
311 self.set_stacked(new_stacked_state);
312 }
313
314 pub fn with_axis(&mut self, id: impl Into<String>, axis: Axis<f64>) {
315 let key = id.into();
316 self.state.set_axis(key.clone(), axis);
317 if !self.defined_axes.contains(&key) {
318 self.defined_axes.push(key);
319 }
320 }
321
322 pub fn push_series(&mut self, mut series: LineSeries) {
323 if self.current_fill_alpha > 0.0 {
324 let mut color = series.color;
325 color.a = self.current_fill_alpha;
326 series.fill_color = Some(color);
327 }
328 self.ensure_axes_exist(&series);
329 self.series.push(series);
330 if self.animation_speed.is_none() {
331 self.snap_axes();
332 }
333 }
334
335 pub fn clear(&mut self) {
336 self.series.clear();
337 self.labels.clear();
338 self.snap_axes();
339 }
340
341 pub fn get_last(&self) -> Option<&LineSeries> {
342 self.series.last()
343 }
344
345 pub fn tick(&mut self, now: Instant) {
346 let Some(speed_normalized) = self.animation_speed else {
347 return;
348 };
349
350 let dt = self
351 .last_tick
352 .map_or(0.0, |last| (now - last).as_secs_f32() as f64);
353 self.last_tick = Some(now);
354
355 let physics_speed = speed_normalized * 10.0;
356 let alpha = 1.0 - (-physics_speed * dt).exp();
357
358 let (target_x, target_ys) = self.calculate_targets();
360
361 let next_x0 =
363 (target_x.0 - self.current_x_domain.0).mul_add(alpha, self.current_x_domain.0);
364 let next_x1 =
365 (target_x.1 - self.current_x_domain.1).mul_add(alpha, self.current_x_domain.1);
366
367 self.current_x_domain = (next_x0, next_x1);
368
369 if let Some(axis) = self.state.axis_mut_opt(&Self::X.to_string()) {
370 axis.set_domain(self.current_x_domain.0, self.current_x_domain.1);
371 }
372
373 for (id, target) in target_ys {
374 let current = self.current_y_domains.entry(id.clone()).or_insert(target);
375 current.0 += (target.0 - current.0) * alpha;
376 current.1 += (target.1 - current.1) * alpha;
377
378 if let Some(axis) = self.state.axis_mut_opt(&id) {
379 axis.set_domain(current.0, current.1);
380 }
381 }
382
383 for s in &mut self.series {
385 s.tick(alpha);
386 }
387
388 let diff_stack = self.target_stack_factor - self.current_stack_factor;
390 if diff_stack.abs() > 1e-5 {
391 self.current_stack_factor += diff_stack * alpha;
392 } else {
393 self.current_stack_factor = self.target_stack_factor;
394 }
395
396 let target_alpha = if self.fill_enabled {
398 self.target_fill_alpha
399 } else {
400 0.0
401 };
402 let diff_alpha = target_alpha - self.current_fill_alpha;
403 if diff_alpha.abs() > 1e-5 {
404 self.current_fill_alpha += diff_alpha * (alpha as f32);
405 self.update_series_fill();
406 } else if self.current_fill_alpha != target_alpha {
407 self.current_fill_alpha = target_alpha;
408 self.update_series_fill();
409 }
410 }
411
412 fn snap_axes(&mut self) {
413 let (tx, tys) = self.calculate_targets();
414 self.current_x_domain = tx;
415 self.current_y_domains = tys;
416
417 if let Some(axis) = self.state.axis_mut_opt(&Self::X.to_string()) {
418 axis.set_domain(tx.0, tx.1);
419 }
420 for (id, d) in &self.current_y_domains {
421 if let Some(axis) = self.state.axis_mut_opt(id) {
422 axis.set_domain(d.0, d.1);
423 }
424 }
425 }
426
427 fn calculate_targets(&self) -> ((f64, f64), HashMap<String, (f64, f64)>) {
428 if self.series.is_empty() {
429 return ((0.0, 1.0), HashMap::new());
430 }
431
432 let max_len = self
433 .series
434 .iter()
435 .map(|s| s.target_values.len())
436 .max()
437 .unwrap_or(0);
438 let x_max = (max_len as f64 - 1.0).max(0.0);
439 let target_x = (0.0, x_max);
440
441 let mut target_ys = HashMap::new();
442 let mut stacked_sums: HashMap<String, Vec<f64>> = HashMap::new();
443 let factor = self.target_stack_factor;
444
445 for s in &self.series {
446 let sums = stacked_sums.entry(s.y_key.clone()).or_default();
447 if s.target_values.len() > sums.len() {
448 sums.resize(s.target_values.len(), 0.0);
449 }
450
451 let entry = target_ys
452 .entry(s.y_key.clone())
453 .or_insert((f64::MAX, f64::MIN));
454
455 for (i, &val) in s.target_values.iter().enumerate() {
456 let baseline = sums[i];
457 let effective_val = baseline.mul_add(factor, val);
458 entry.0 = entry.0.min(effective_val);
459 entry.1 = entry.1.max(effective_val);
460 sums[i] += val;
461 }
462 }
463
464 for (_, bounds) in target_ys.iter_mut() {
465 let (min, max) = match self.max_y_val {
466 Some(max_y_val) => (bounds.0, max_y_val),
467 None => *bounds,
468 };
469 let padding = if max > min { (max - min) * 0.05 } else { 1.0 };
470 let final_min = if factor > 0.1 {
471 min.min(0.0)
472 } else {
473 if min < 0. { min } else { 0.0 }
474 };
475 *bounds = (final_min, max + padding);
476 }
477
478 (target_x, target_ys)
479 }
480
481 pub fn push(&mut self, label: impl Into<String>, value: f64) {
482 let label = label.into();
483 if self.series.is_empty() {
484 let default_series =
485 LineSeries::new("Data", Color::from_rgb(0.2, 0.4, 0.8), self.max_values);
486 self.push_series(default_series);
487 }
488
489 let needs_label_update = if let Some(last) = self.series.last() {
490 last.target_values.len() >= self.labels.len()
491 } else {
492 false
493 };
494
495 if needs_label_update {
496 self.labels.push(label);
497 self.update_x_axis_labels();
498 }
499
500 if let Some(last) = self.series.last_mut() {
501 last.push(value);
502 }
503
504 if self.animation_speed.is_none() {
505 for s in &mut self.series {
506 s.current_values = s.target_values.clone();
507 }
508 self.snap_axes();
509 }
510 }
511
512 pub fn push_value(&mut self, value: f64) {
513 self.push("", value);
514 }
515
516 pub fn push_to(&mut self, index: usize, label: impl Into<String>, value: f64) {
517 let needs_label_update = if let Some(series) = self.series.get(index) {
518 series.target_values.len() >= self.labels.len()
519 } else {
520 false
521 };
522
523 if needs_label_update {
524 self.labels.push(label.into());
525 self.update_x_axis_labels();
526 }
527
528 if let Some(series) = self.series.get_mut(index) {
529 series.push(value);
530 }
531
532 if self.animation_speed.is_none() {
533 if let Some(s) = self.series.get_mut(index) {
534 s.snap();
535 }
536 self.snap_axes();
537 }
538 }
539
540 pub fn push_value_to(&mut self, index: usize, value: f64) {
541 self.push_to(index, "", value);
542 }
543
544 pub fn push_value_last_series(&mut self, value: f64) {
545 self.push_value(value);
546 }
547
548 pub const fn series_count(&self) -> usize {
549 self.series.len()
550 }
551
552 fn update_x_axis_labels(&mut self) {
553 let labels = self.labels.clone();
554 let x_axis = self.state.axis_mut(&Self::X.to_string());
555
556 x_axis.set_tick_renderer(move |ctx| {
557 let idx = ctx.tick.value.round();
558 if (ctx.tick.value - idx).abs() > 0.001 {
559 return TickResult::default();
560 }
561 let idx = idx as usize;
562 let valid_idx = idx < labels.len();
563
564 let label = valid_idx.then(|| ctx.label(labels[idx].clone()));
565 let tick_line = valid_idx.then(|| ctx.tickline());
566
567 TickResult {
568 label,
569 tick_line,
570 ..Default::default()
571 }
572 });
573 }
574
575 fn ensure_axes_exist(&mut self, series: &LineSeries) {
576 let x_key = Self::X.to_string();
577 if !self.defined_axes.contains(&x_key) {
578 self.state.set_axis(
579 x_key.clone(),
580 Axis::new(Linear::new(0.0, 1.0), axis::Position::Bottom),
581 );
582 self.defined_axes.push(x_key);
583 self.update_x_axis_labels();
584 }
585
586 if !self.defined_axes.contains(&series.y_key) {
587 self.state.set_axis(
588 series.y_key.clone(),
589 if self.show_y_axis {
590 y_axis(0., 1.)
591 } else {
592 y_axis_without_label(0., 1.)
593 },
594 );
595 self.defined_axes.push(series.y_key.clone());
596 }
597 }
598
599 pub fn chart<Message>(&self) -> Chart<'_, AxisID, f64, Message> {
600 let mut chart = Chart::new(&self.state);
601 let first_y = self
602 .series
603 .first()
604 .map(|s| s.y_key.clone())
605 .unwrap_or_else(|| Self::Y.to_string());
606 chart = chart.plot_data(self, Self::X.to_string(), first_y);
607 chart
608 }
609}
610
611impl PlotData<f64> for LineChart {
612 fn draw(&self, plot: &mut Plot<f64>, theme: &Theme) {
613 let chart_floor = self
614 .state
615 .axis_opt(&Self::Y.to_string())
616 .map_or(0.0, |axis| *axis.domain().0);
617
618 let mut baseline: Vec<f64> = Vec::new();
619
620 for s in &self.series {
621 if s.current_values.len() < 2 {
622 continue;
623 }
624
625 if baseline.len() < s.current_values.len() {
626 baseline.resize(s.current_values.len(), 0.0);
627 }
628
629 let points: Vec<PlotPoint<f64>> = s
630 .current_values
631 .iter()
632 .enumerate()
633 .map(|(i, &v)| {
634 let effective_base = baseline[i] * self.current_stack_factor;
635 let total = effective_base + v;
636 PlotPoint::new(i as f64, total)
637 })
638 .collect();
639
640 if self.current_fill_alpha > 0.0 {
641 let mut fill_poly = points.clone();
642 for (i, _) in s
643 .current_values
644 .iter()
645 .enumerate()
646 .take(s.current_values.len())
647 .rev()
648 {
649 let base_val = baseline[i] * self.current_stack_factor;
650 let floor = chart_floor * (1.0 - self.current_stack_factor)
651 + base_val * self.current_stack_factor;
652 fill_poly.push(PlotPoint::new(i as f64, floor));
653 }
654 let mut color = s.color;
655 color.a = self.current_fill_alpha;
656 plot.add_shape(Area::new(fill_poly).fill(color));
657 }
658
659 plot.add_shape(Polyline {
660 points: points.clone(),
661 stroke: Some(Stroke::new(s.color, Measure::Screen(s.width))),
662 extend_start: false,
663 extend_end: false,
664 arrow_start: false,
665 arrow_end: false,
666 arrow_size: 10.0,
667 });
668
669 if s.show_markers {
670 for point in &points {
671 let marker_size = Measure::Screen(s.width.mul_add(2.0, 2.0));
672 plot.add_shape(Polygon::new(*point, marker_size, 4).fill(s.color));
673 }
674 }
675
676 for (i, &v) in s.current_values.iter().enumerate() {
677 baseline[i] += v;
678 }
679 }
680
681 if self.show_legend {
682 let palette = theme.palette();
683 if let (Some(x_axis), Some(y_axis)) = (
684 self.state.axis_opt(&Self::X.to_string()),
685 self.state.axis_opt(&Self::Y.to_string()),
686 ) {
687 let (x_min, x_max) = x_axis.domain();
688 let (y_min, y_max) = y_axis.domain();
689
690 let max_cols = 6;
691 let item_cnt = self.series.len();
692 let cols_per_row = if item_cnt <= 2 { item_cnt } else { max_cols };
693
694 let start_x = (x_max - x_min).mul_add(0.02, *x_min);
695 let start_y = (y_max - y_min).mul_add(-0.05, *y_max);
696 let step_y = (y_max - y_min) * 0.1;
697
698 let avail_width = (x_max - x_min) * 0.8;
699 let col_width = if cols_per_row > 0 {
700 avail_width / cols_per_row as f64
701 } else {
702 avail_width
703 };
704
705 for (i, series) in self.series.iter().enumerate() {
706 let row = i / cols_per_row;
707 let col = i % cols_per_row;
708
709 let x_pos = start_x + (col as f64) * col_width;
710 let y_pos = start_y - (row as f64) * step_y;
711
712 plot.add_shape(
714 Rectangle::centered(
715 PlotPoint::new(x_pos, y_pos),
716 Measure::Screen(10.0),
717 Measure::Screen(10.0),
718 )
719 .fill(series.color),
720 );
721 let text_offset = (x_max - x_min) * 0.01;
722 plot.add_shape(
723 Label::new(&series.name, PlotPoint::new(x_pos + text_offset, y_pos))
724 .fill(palette.text)
725 .size(Measure::Screen(12.))
726 .align(Horizontal::Left, Vertical::Center),
727 );
728 }
729 }
730 }
731 }
732}
733
734fn y_axis(min_y: f64, max_y: f64) -> Axis<f64> {
735 Axis::new(Linear::new(min_y, max_y), axis::Position::Right)
736 .with_tick_renderer(|ctx| {
737 let line_color = ctx.gridline().color;
738
739 match ctx.tick.level {
740 0 => TickResult {
741 label: Some(ctx.label(format!("{}%", ctx.tick.value))),
742 grid_line: Some(ctx.gridline()),
743 tick_line: Some({
744 let mut line = ctx.tickline();
745 line.color = line_color;
746 line.width = Pixels::ZERO;
747 line.length = Pixels::ZERO;
748 line
749 }),
750 ..Default::default()
751 },
752 _ => TickResult::default(),
753 }
754 })
755 .style(|s| {
756 s.spine.width = Pixels::ZERO;
757 s.tick.width = Pixels::ZERO;
758 s.label.size = Pixels::from(10.);
759 s.grid.dashed = Some(DashStyle::new(1., 2.));
760 })
761}
762
763fn y_axis_without_label(min_y: f64, max_y: f64) -> Axis<f64> {
764 Axis::new(Linear::new(min_y, max_y), axis::Position::Right)
765 .with_tick_renderer(|_| TickResult::default())
766}