rtplot/
figure.rs

1use crate::utils;
2use crate::window::{Vertex, Window};
3use cgmath::Point2;
4use glium::glutin::platform::desktop::EventLoopExtDesktop;
5use itertools_num::linspace;
6use num::Complex;
7use slice_deque::SliceDeque;
8
9#[derive(Copy, Clone, Debug)]
10pub enum PlotType {
11    /// Draws a continuous line between points.
12    Line,
13
14    /// Each point is drawn as a small diamond.
15    Dot,
16}
17
18impl Default for PlotType {
19    fn default() -> Self {
20        PlotType::Dot
21    }
22}
23
24#[derive(Clone, Default)]
25pub struct FigureConfig<'a> {
26    /// The min and max bounds of the x axis. If set to None, x-axis will be
27    /// autoscaled. Defaults to None.
28    pub xlim: Option<[f32; 2]>,
29
30    /// The min and max bounds of the y axis. If set to None, y-axis will be
31    /// autoscaled. Defaults to None.
32    pub ylim: Option<[f32; 2]>,
33
34    /// A label for the x-axis. Defaults to None.
35    pub xlabel: Option<&'a str>,
36
37    /// A label for the y-axis. Defaults to None.
38    pub ylabel: Option<&'a str>,
39
40    /// The color of points or lines to be drawn onto the graph. Defaults to
41    /// 0x000000, or black.
42    pub color: [u8; 3],
43
44    /// The type of plot to draw. Defaults to a dot plot.
45    pub plot_type: PlotType,
46}
47
48#[derive(Default)]
49/// Creates a figure that will wait to receive samples, then draw them onto the
50/// plot.
51pub struct Figure<'a> {
52    window: Window<'a>,
53    config: FigureConfig<'a>,
54
55    /// A queue holding samples if the figure is going to be used for streaming
56    /// plotting. Size is capped at `queue_size`.
57    samples: SliceDeque<f32>,
58
59    /// A queue holding complex samples as above.
60    complex_samples: SliceDeque<Complex<f32>>,
61
62    /// The number of points. Defaults to 0.
63    queue_size: usize,
64
65    /// Indicates whether the x axis is dynamic.
66    x_dynamic: bool,
67
68    /// Indicates whether the y axis is dynamic.
69    y_dynamic: bool,
70}
71
72impl<'a> Figure<'a> {
73    /// Create a figure with default settings.
74    pub fn new(queue_size: usize) -> Self {
75        Self {
76            window: Window::new(),
77            config: FigureConfig::default(),
78            samples: SliceDeque::new(),
79            complex_samples: SliceDeque::new(),
80            queue_size,
81            x_dynamic: true,
82            y_dynamic: true,
83        }
84    }
85
86    /// Create a figure from an existing configuration. Useful if you don't
87    /// want to use the builder pattern to initialize a figure from scratch.
88    pub fn new_with_config(config: FigureConfig<'a>, queue_size: usize) -> Self {
89        let x_dynamic = config.xlim.is_none();
90        let y_dynamic = config.ylim.is_none();
91        Self {
92            window: Window::new(),
93            config,
94            samples: SliceDeque::new(),
95            complex_samples: SliceDeque::new(),
96            queue_size,
97            x_dynamic,
98            y_dynamic,
99        }
100    }
101
102    /// Sets the x min and max limits for plotting.
103    pub fn xlim(mut self, xlim: [f32; 2]) -> Self {
104        self.config.xlim = Some(xlim);
105        self.x_dynamic = false;
106        self
107    }
108
109    /// Sets the y min and max limits for plotting.
110    pub fn ylim(mut self, ylim: [f32; 2]) -> Self {
111        self.config.ylim = Some(ylim);
112        self.y_dynamic = false;
113        self
114    }
115
116    /// Sets the x label to display.
117    pub fn xlabel(mut self, xlabel: &'a str) -> Self {
118        self.config.xlabel = Some(xlabel);
119        self
120    }
121
122    /// Sets the y label to display.
123    pub fn ylabel(mut self, ylabel: &'a str) -> Self {
124        self.config.ylabel = Some(ylabel);
125        self
126    }
127
128    /// Sets the color of the line to draw.
129    pub fn color(mut self, r: u8, g: u8, b: u8) -> Self {
130        self.config.color = [r, g, b];
131        self
132    }
133
134    /// Sets the type of plot to generate.
135    pub fn plot_type(mut self, plot_type: PlotType) -> Self {
136        self.config.plot_type = plot_type;
137        self
138    }
139
140    /// Checks events to see if the figure should close or not. Returns
141    /// true if the window received a close event, false otherwise. In
142    /// most cases, you don't need to handle events yourself; use
143    /// Figure::display() instead.
144    pub fn should_close_window(&mut self) -> bool {
145        let mut should_close_window = false;
146
147        let events_loop = &mut self.window.events_loop;
148
149        events_loop.run_return(|event, _, control_flow| {
150            use glium::glutin::event::{Event, WindowEvent};
151            use glium::glutin::event_loop::ControlFlow;
152            #[allow(clippy::single_match)]
153            match event {
154                Event::WindowEvent { event, .. } => match event {
155                    WindowEvent::Destroyed | WindowEvent::CloseRequested => {
156                        should_close_window = true
157                    }
158                    _ => (),
159                },
160                _ => (),
161            }
162            *control_flow = ControlFlow::Exit;
163        });
164        should_close_window
165    }
166
167    /// Normalizes the received points to [-0.5, 0.5] for drawing in OpenGL.
168    fn normalize(&mut self, points: &[Point2<f32>]) -> Vec<Vertex> {
169        let [min_x, max_x] = if self.x_dynamic {
170            let xlims = utils::calc_xlims(points);
171            self.config.xlim = Some(xlims);
172            xlims
173        } else {
174            self.config.xlim.unwrap()
175        };
176        let [min_y, max_y] = if self.y_dynamic {
177            let ylims = utils::calc_ylims(points);
178            self.config.ylim = Some(ylims);
179            ylims
180        } else {
181            self.config.ylim.unwrap()
182        };
183        let mut vertices = vec![];
184        for point in points {
185            // If there are points outside the min and max range, skip over
186            // them since we won't draw them anyways.
187            if point.x > max_x || point.x < min_x || point.y > max_y || point.y < min_y {
188                continue;
189            }
190            let error: f32 = 0.0;
191            let x = if (max_x - min_x).abs() > error {
192                1.5 * (point.x - min_x) / (max_x - min_x) - 0.75
193            } else {
194                1.5 * point.x - 0.75
195            };
196            let y = if (max_y - min_y).abs() > error {
197                1.5 * (point.y - min_y) / (max_y - min_y) - 0.75
198            } else {
199                1.5 * point.y - 0.75
200            };
201            vertices.push(Vertex::new(x, y, self.config.color));
202        }
203        vertices
204    }
205
206    /// A helper function for normalizing and drawing points to the window.
207    fn plot(&mut self, points: &[Point2<f32>]) {
208        let vertices = self.normalize(&points);
209        self.window.draw(&vertices, &self.config);
210    }
211
212    /// Take an array of 2D points and draw them to the plot. This overrides
213    /// any samples in the queue.
214    pub fn plot_xy<T>(&mut self, points: &[(T, T)])
215    where
216        T: Into<f32> + Copy,
217    {
218        let points: Vec<Point2<f32>> = points
219            .iter()
220            .map(|pt| Point2::new(pt.0.into(), pt.1.into()))
221            .collect();
222        self.plot(&points);
223    }
224
225    /// Takes a series of real samples and draws them onto the plot. This
226    /// overrides any samples in the queue. The x-axis will be interpolated.
227    pub fn plot_y<T>(&mut self, y_coords: &[T])
228    where
229        T: Into<f32> + Copy,
230    {
231        let x_coords = linspace(-0.5f32, 0.5f32, y_coords.len());
232        let points: Vec<Point2<f32>> = x_coords
233            .zip(y_coords.iter())
234            .map(|(x, y)| Point2::new(x, (*y).into()))
235            .collect();
236        self.plot(&points);
237    }
238
239    /// Takes a series of real samples and draws them onto the plot. Samples
240    /// received from the stream are appended to the queue and any samples
241    /// exceeding the queue size are removed. The x-axis will be interpolated.
242    pub fn plot_stream<T>(&mut self, y_coords: &[T])
243    where
244        T: Into<f32> + Copy,
245    {
246        if self.samples.len() >= self.queue_size + y_coords.len() {
247            for _ in 0..self.samples.len() - self.queue_size + y_coords.len() {
248                self.samples.pop_front();
249            }
250        }
251        let y: Vec<f32> = y_coords.iter().map(|y| (*y).into()).collect();
252        for point in &y {
253            self.samples.push_back(*point);
254        }
255        let x_coords = linspace(-0.5f32, 0.5f32, self.queue_size);
256        let points: Vec<Point2<f32>> = x_coords
257            .zip(self.samples.iter())
258            .map(|(x, y)| Point2::new(x, *y))
259            .collect();
260        let vertices = self.normalize(&points);
261        self.window.draw(&vertices, &self.config);
262    }
263
264    /// Takes a slice of complex samples and draws them onto the plot. Samples
265    /// received from the stream are appended to the queue and any samples
266    /// exceeding the queue size are removed.
267    pub fn plot_complex_stream<T>(&mut self, points: &[Complex<T>])
268    where
269        T: Into<f32> + Copy,
270    {
271        if self.complex_samples.len() >= self.queue_size + points.len() {
272            for _ in 0..self.complex_samples.len() - self.queue_size + points.len() {
273                self.complex_samples.pop_front();
274            }
275        }
276
277        let points: Vec<Complex<f32>> = points
278            .iter()
279            .map(|x| Complex::new(x.re.into(), x.im.into()))
280            .collect();
281        for point in points {
282            self.complex_samples.push_back(point);
283        }
284
285        let points: Vec<Point2<f32>> = self
286            .complex_samples
287            .iter()
288            .map(|x| Point2::new(x.re, x.im))
289            .collect();
290        let vertices = self.normalize(&points);
291        self.window.draw(&vertices, &self.config);
292    }
293
294    /// Takes a slice of complex samples and draws them onto the plot. This
295    /// overrides any existing samples in the queue.
296    pub fn plot_complex<T>(&mut self, coords: &[Complex<T>])
297    where
298        T: Into<f32> + Copy,
299    {
300        let points: Vec<Point2<f32>> = coords
301            .iter()
302            .map(|pt| Point2::new(pt.re.into(), pt.im.into()))
303            .collect();
304        self.plot(&points);
305    }
306
307    /// Hijacks the current thread to run the plotting and event loop.
308    pub fn display(figure: &mut Figure, mut plot_fn: impl FnMut(&mut Figure)) {
309        while !figure.should_close_window() {
310            plot_fn(figure);
311        }
312    }
313}