egui_plotter/charts/
xytime.rs

1//! Animatable line chart. Can have X and Y points.
2
3use std::{cmp::Ordering, ops::Range, sync::Arc, time::Duration};
4
5use egui::Ui;
6use instant::Instant;
7use plotters::{
8    prelude::ChartBuilder,
9    series::LineSeries,
10    style::{
11        full_palette::{GREY, GREY_700, RED_900},
12        Color, FontDesc, RGBAColor, ShapeStyle, TextStyle, BLACK, WHITE,
13    },
14};
15use plotters_backend::{FontFamily, FontStyle};
16
17use crate::{mult_range, Chart, MouseConfig};
18
19const MIN_DELTA: f32 = 0.000_010;
20const DEFAULT_RATIO: f32 = 1.0;
21const X_MARGIN: i32 = 25;
22const Y_MARGIN: i32 = 25;
23const LABEL_AREA: i32 = 25;
24const CAPTION_SIZE: i32 = 10;
25
26#[derive(Clone)]
27struct XyTimeConfig {
28    /// Points to be plotted. A slice of X, Y f32 pairs.
29    points: Arc<[(f32, f32)]>,
30    /// Ranges at different time points.
31    range: (Range<f32>, Range<f32>),
32    /// Style of the plotted line.
33    line_style: ShapeStyle,
34    /// Style of the grid lines.
35    grid_style: ShapeStyle,
36    /// Style of the small grid lines.
37    subgrid_style: ShapeStyle,
38    /// Style of the axes.
39    axes_style: ShapeStyle,
40    /// Style of the text
41    text_color: RGBAColor,
42    /// Background color of the chart.
43    background_color: RGBAColor,
44    /// Unit of the X axis.
45    x_unit: Arc<str>,
46    /// Unit of the Y axis.
47    y_unit: Arc<str>,
48    /// Ratio between the X and Y axis units.
49    ratio: f32,
50    /// Caption of the chart.
51    caption: Arc<str>,
52}
53
54/// Animatable 2d line chart.
55///
56/// ## Usage
57/// **Ensure the `timechart` feature is enabled to use this type.**
58///
59/// Creating the chart is very simple. You only need to provide 4 parameters,
60/// 3 of which are just strings.
61///
62///  * `points`: A slice of tuples, arranged so that the first float is the x position, the second
63///  the y position, and the third is the time the next point is to be shown at(or in the case of
64///  the last point, the time the animation ends).
65///  * `x_unit`: String describing the data on the X axis.
66///  * `y_unit`: String describing the data on the Y axis.
67///  * `caption`: String to be shown as the caption of the chart.
68///
69/// This will create a basic line chart with nothing fancy, which you can easily
70/// add to your egui project. You can also animate this chart with `.toggle_playback()`
71/// and adjust various parameters with the many `.set_` functions included.
72pub struct XyTimeData {
73    playback_start: Option<Instant>,
74    pause_start: Option<Instant>,
75    playback_speed: f32,
76    points: Arc<[(f32, f32)]>,
77    ranges: Arc<[(Range<f32>, Range<f32>)]>,
78    times: Arc<[f32]>,
79    chart: Chart<XyTimeConfig>,
80}
81
82impl XyTimeData {
83    /// Create a new XyTimeData chart. See [Usage](#usage).
84    pub fn new(points: &[(f32, f32, f32)], x_unit: &str, y_unit: &str, caption: &str) -> Self {
85        let mut points = points.to_vec();
86
87        // Sort by the time of the point
88        points.sort_by(|a, b| {
89            let (_, _, a) = a;
90            let (_, _, b) = b;
91
92            a.partial_cmp(b).unwrap_or(Ordering::Equal)
93        });
94
95        let times: Vec<f32> = points
96            .iter()
97            .map(|point| {
98                let (_, _, time) = point;
99
100                *time
101            })
102            .collect();
103
104        let points: Vec<(f32, f32)> = points
105            .iter()
106            .map(|point| {
107                let (x, y, _) = point;
108
109                (*x, *y)
110            })
111            .collect();
112
113        // Ranges include the X range, Y range, and time in seconds
114        let mut ranges = Vec::<(Range<f32>, Range<f32>)>::with_capacity(points.len());
115
116        let mut min_x: f32 = f32::MAX;
117        let mut min_y: f32 = f32::MAX;
118        let mut max_x: f32 = f32::MIN;
119        let mut max_y: f32 = f32::MIN;
120
121        for point in &points {
122            let (x, y) = *point;
123
124            min_x = min_x.min(x);
125            min_y = min_y.min(y);
126            max_x = max_x.max(x);
127            max_y = max_y.max(y);
128
129            let range_x = min_x..max_x;
130            let range_y = min_y..max_y;
131
132            ranges.push((range_x, range_y));
133        }
134
135        // Turn all the vecs and strings into arcs since they are more or less read-only at
136        // this point
137
138        let points: Arc<[(f32, f32)]> = points.into();
139        let ranges: Arc<[(Range<f32>, Range<f32>)]> = ranges.into();
140        let times: Arc<[f32]> = times.into();
141
142        let x_unit: Arc<str> = x_unit.into();
143        let y_unit: Arc<str> = y_unit.into();
144        let caption: Arc<str> = caption.into();
145
146        let grid_style = ShapeStyle {
147            color: GREY.to_rgba(),
148            filled: false,
149            stroke_width: 2,
150        };
151
152        let subgrid_style = ShapeStyle {
153            color: GREY_700.to_rgba(),
154            filled: false,
155            stroke_width: 1,
156        };
157
158        let axes_style = ShapeStyle {
159            color: BLACK.to_rgba(),
160            filled: false,
161            stroke_width: 2,
162        };
163
164        let line_style = ShapeStyle {
165            color: RED_900.to_rgba(),
166            filled: false,
167            stroke_width: 2,
168        };
169
170        let background_color = WHITE.to_rgba();
171        let text_color = BLACK.to_rgba();
172
173        let config = XyTimeConfig {
174            points: points.clone(),
175            range: ranges.last().unwrap().clone(),
176            line_style,
177            grid_style,
178            subgrid_style,
179            axes_style,
180            text_color,
181            background_color,
182            x_unit,
183            y_unit,
184            ratio: DEFAULT_RATIO,
185            caption,
186        };
187
188        let chart = Chart::new(config)
189            .mouse(MouseConfig::enabled())
190            .builder_cb(Box::new(|area, _t, data| {
191                let area_ratio = {
192                    let (x_range, y_range) = area.get_pixel_range();
193
194                    let x_delta =
195                        ((x_range.end - x_range.start).abs() - (X_MARGIN * 2) - LABEL_AREA) as f32;
196                    let y_delta = ((y_range.end - y_range.start).abs()
197                        - (Y_MARGIN * 2)
198                        - LABEL_AREA
199                        - CAPTION_SIZE) as f32;
200
201                    x_delta / y_delta
202                };
203
204                // Return if the ratio is invalid(meaning the chart can't be drawn)
205                if !area_ratio.is_finite() {
206                    return;
207                }
208
209                let (x_range, y_range) = data.range.clone();
210
211                // The data ratio is inverse, as if our X range is smaller we
212                // want to make sure the X axis is expanded to compensate
213                let data_ratio = {
214                    let x_delta = (x_range.end - x_range.start).abs();
215                    let y_delta = (y_range.end - y_range.start).abs();
216
217                    y_delta / x_delta
218                };
219
220                let display_ratio = data.ratio * data_ratio * area_ratio;
221
222                let (x_range, y_range) =
223                    match display_ratio.partial_cmp(&1.0).unwrap_or(Ordering::Equal) {
224                        Ordering::Equal => (x_range, y_range),
225                        Ordering::Greater => (mult_range(x_range, display_ratio), y_range),
226                        Ordering::Less => (x_range, mult_range(y_range, 1.0 / display_ratio)),
227                    };
228
229                let font_style = FontStyle::Normal;
230                let font_family = FontFamily::Monospace;
231                let font_size = CAPTION_SIZE;
232
233                let font_desc = FontDesc::new(font_family, font_size as f64, font_style);
234
235                let text_style = TextStyle::from(font_desc).color(&data.text_color);
236
237                let mut chart = ChartBuilder::on(area)
238                    .caption(data.caption.clone(), text_style.clone())
239                    .x_label_area_size(LABEL_AREA)
240                    .y_label_area_size(LABEL_AREA)
241                    .margin_left(X_MARGIN)
242                    .margin_right(X_MARGIN)
243                    .margin_top(Y_MARGIN)
244                    .margin_bottom(Y_MARGIN)
245                    .build_cartesian_2d(x_range, y_range)
246                    .unwrap();
247
248                chart
249                    .configure_mesh()
250                    .label_style(text_style.clone())
251                    .bold_line_style(data.grid_style)
252                    .light_line_style(data.subgrid_style)
253                    .axis_style(data.axes_style)
254                    .x_desc(&data.x_unit.to_string())
255                    .set_all_tick_mark_size(4)
256                    .y_desc(&data.y_unit.to_string())
257                    .draw()
258                    .unwrap();
259
260                chart
261                    .draw_series(LineSeries::new(data.points.to_vec(), data.line_style))
262                    .unwrap();
263            }));
264
265        Self {
266            playback_start: None,
267            pause_start: None,
268            playback_speed: 1.0,
269            points,
270            ranges,
271            times,
272            chart,
273        }
274    }
275
276    /// Set the time to resume playback at. Time is in seconds.
277    pub fn set_time(&mut self, time: f32) {
278        let start_time = Some(Instant::now() - Duration::from_secs_f32(time));
279        match self.playback_start {
280            Some(_) => {
281                if let Some(_) = self.pause_start {
282                    self.pause_start = Some(Instant::now());
283                }
284
285                self.playback_start = start_time;
286            }
287            None => {
288                self.playback_start = start_time;
289                self.pause_start = Some(Instant::now());
290            }
291        }
292    }
293
294    #[inline]
295    /// Set the time to resume playback at. Time is in seconds. Consumes self.
296    pub fn time(mut self, time: f32) -> Self {
297        self.set_time(time);
298
299        self
300    }
301
302    #[inline]
303    /// Set the playback speed. 1.0 is normal speed, 2.0 is double, & 0.5 is half.
304    pub fn set_playback_speed(&mut self, speed: f32) {
305        self.playback_speed = speed;
306    }
307
308    #[inline]
309    /// Set the playback speed. 1.0 is normal speed, 2.0 is double, & 0.5 is half. Consumes self.
310    pub fn playback_speed(mut self, speed: f32) -> Self {
311        self.set_playback_speed(speed);
312
313        self
314    }
315
316    /// Set the style of the plotted line.
317    pub fn set_line_style(&mut self, line_style: ShapeStyle) {
318        self.chart.get_data_mut().line_style = line_style;
319    }
320
321    #[inline]
322    /// Set the style of the plotted line. Consumes self.
323    pub fn line_style(mut self, line_style: ShapeStyle) -> Self {
324        self.set_line_style(line_style);
325
326        self
327    }
328
329    /// Set the style of the grid.
330    pub fn set_grid_style(&mut self, grid_style: ShapeStyle) {
331        self.chart.get_data_mut().grid_style = grid_style
332    }
333
334    #[inline]
335    /// Set the style of the grid. Consumes self.
336    pub fn grid_style(mut self, grid_style: ShapeStyle) -> Self {
337        self.set_grid_style(grid_style);
338
339        self
340    }
341
342    /// Set the style of the subgrid.
343    pub fn set_subgrid_style(&mut self, subgrid_style: ShapeStyle) {
344        self.chart.get_data_mut().subgrid_style = subgrid_style
345    }
346
347    #[inline]
348    /// Set the style of the subgrid. Consumes self.
349    pub fn subgrid_style(mut self, subgrid_style: ShapeStyle) -> Self {
350        self.set_subgrid_style(subgrid_style);
351
352        self
353    }
354
355    /// Set the style of the axes.
356    pub fn set_axes_style(&mut self, axes_style: ShapeStyle) {
357        self.chart.get_data_mut().axes_style = axes_style
358    }
359
360    #[inline]
361    /// Set the style of the plotted line. Consumes self.
362    pub fn axes_style(mut self, axes_style: ShapeStyle) -> Self {
363        self.set_axes_style(axes_style);
364
365        self
366    }
367
368    /// Set the text color of the chart.
369    pub fn set_text_color<T>(&mut self, color: T)
370    where
371        T: Into<RGBAColor>,
372    {
373        let color: RGBAColor = color.into();
374
375        self.chart.get_data_mut().text_color = color
376    }
377
378    #[inline]
379    /// Set the text color of the chart. Consumes self.
380    pub fn text_color<T>(mut self, color: T) -> Self
381    where
382        T: Into<RGBAColor>,
383    {
384        self.set_text_color(color);
385
386        self
387    }
388
389    /// Set the background color of the chart.
390    pub fn set_background_color<T>(&mut self, color: T)
391    where
392        T: Into<RGBAColor>,
393    {
394        let color: RGBAColor = color.into();
395
396        self.chart.get_data_mut().background_color = color
397    }
398
399    #[inline]
400    /// Set the background color of the chart. Consumes self.
401    pub fn background_color<T>(mut self, color: T) -> Self
402    where
403        T: Into<RGBAColor>,
404    {
405        self.set_background_color(color);
406
407        self
408    }
409
410    #[inline]
411    /// Set the ratio between X and Y values, default being 1 x unit to 1 y unit.
412    pub fn set_ratio(&mut self, ratio: f32) {
413        self.chart.get_data_mut().ratio = ratio
414    }
415
416    #[inline]
417    /// Set the ratio between X and Y values, default being 1 x unit to 1 y unit. Consumes self.
418    pub fn ratio(mut self, ratio: f32) -> Self {
419        self.set_ratio(ratio);
420
421        self
422    }
423
424    /// Draw the chart to a Ui. Will also proceed to animate the chart if playback is currently
425    /// enabled.
426    pub fn draw(&mut self, ui: &Ui) {
427        if let Some(_) = self.playback_start {
428            let time = self.current_time();
429
430            let time_index = match self
431                .times
432                .binary_search_by(|probe| probe.partial_cmp(&time).unwrap_or(Ordering::Equal))
433            {
434                Ok(index) => index,
435                Err(index) => self.points.len().min(index),
436            };
437
438            // The time index is always a valid index, so ensure the range is inclusive
439            let points = &self.points[..=time_index];
440            let range = self.ranges[time_index].clone();
441
442            let config = self.chart.get_data_mut();
443            config.points = points.into();
444            config.range = range;
445        }
446
447        self.chart.draw(ui);
448    }
449
450    #[inline]
451    /// Start/enable playback of the chart.
452    pub fn start_playback(&mut self) {
453        self.playback_start = Some(Instant::now());
454        self.pause_start = None;
455    }
456
457    #[inline]
458    /// Stop/disable playback of the chart.
459    pub fn stop_playback(&mut self) {
460        self.playback_start = None;
461        self.pause_start = None;
462    }
463
464    /// Toggle playback of the chart.
465    pub fn toggle_playback(&mut self) {
466        match self.playback_start {
467            Some(playback_start) => match self.pause_start {
468                Some(pause_start) => {
469                    let delta = Instant::now().duration_since(pause_start);
470
471                    self.pause_start = None;
472                    self.playback_start = Some(playback_start + delta);
473                }
474                None => self.pause_start = Some(Instant::now()),
475            },
476
477            None => {
478                self.start_playback();
479            }
480        }
481    }
482
483    #[inline]
484    /// Return true if playback is currently enabled & underway.
485    pub fn is_playing(&self) -> bool {
486        self.playback_start != None && self.pause_start == None
487    }
488
489    #[inline]
490    /// Return the time the chart starts at when playback is enabled.
491    pub fn start_time(&self) -> f32 {
492        let time_start = *self.times.first().unwrap();
493
494        time_start
495    }
496
497    /// Return the current time to be animated when playback is enabled.
498    pub fn current_time(&mut self) -> f32 {
499        if let Some(playback_start) = self.playback_start {
500            let now = Instant::now();
501
502            let time_start = self.start_time();
503            let time_end = self.end_time();
504
505            let base_delta = time_end - time_start;
506
507            // Ensure deltas are over 10us, otherwise they can cause overflows
508            // in the plotters library
509            let current_delta = MIN_DELTA
510                + self.playback_speed
511                    * match self.pause_start {
512                        Some(pause_start) => {
513                            pause_start.duration_since(playback_start).as_secs_f32()
514                        }
515                        None => now.duration_since(playback_start).as_secs_f32(),
516                    };
517
518            match base_delta > current_delta {
519                true => current_delta + time_start,
520                false => {
521                    self.playback_start = None;
522
523                    time_end
524                }
525            }
526        } else {
527            self.start_time()
528        }
529    }
530
531    #[inline]
532    /// Return the time the chart finished animating at when playback is enabled.
533    pub fn end_time(&self) -> f32 {
534        let time_end = *self.times.last().unwrap();
535
536        time_end
537    }
538
539    #[inline]
540    /// Return the speed the chart is animated at.
541    pub fn get_playback_speed(&self) -> f32 {
542        self.playback_speed
543    }
544}