sb_rust_library/plotter/
plot.rs

1use image::{ImageError, RgbImage};
2use std::ops::Range;
3
4use crate::math::{Bounds, Point};
5use super::color::{BLACK, Color, GREY, WHITE};
6use super::plot_frame::PlotFrame;
7use super::shapes::{Circle, Orientation};
8
9/// Struct used to plot data and generate images.
10///
11/// A plot represents a single drawing area. The region of the image that data
12/// is plotted to is refered to as the `canvas`, which by default, is inset
13/// within the full image. The size of the canvas can be changed with
14/// [`with_drawing_bounds`](#method.with_drawing_bounds).
15///
16/// A plot has two notions of coordinates: "logical" coordinates which are real-
17/// valued coordinates that are plotted and "physical" coordinates which are the
18/// integer locations of actual pixels. The range of logical coordinates
19/// displayed can be changed with [`with_plotting_range`](#method.with_plotting_range).
20///
21/// Currently data can only be plotted with the low level primitives
22/// [`Circle`](struct.Circle.html) and [`Line`](struct.Line.html). This makes
23/// plotting somewhat cumbersome, but it also makes this system flexible.
24///
25/// # Examples
26///
27/// Basic sine wave without axis labels
28///
29/// ```
30/// let mut p = Plot::new(300, 200)
31///   .with_canvas_color(GREY)
32///   .with_bg_color(BLUE)
33///   .with_plotting_range(0.0..(2.0 * 3.14), -1.1..1.1)
34///   .with_drawing_bounds(0.05..0.95, 0.1..0.95);
35///
36/// let x_values: Vec<f64> = (0..1000).map(|i| (i as f64) / 1000.0 * 2.0 * 3.14).collect();
37/// let points: Vec<Point> = x_values.into_iter().map(|x| {
38///   (x, x.sin()).into()
39/// }).collect();
40///
41/// let circle = Circle::new(RED, 1);
42/// p.draw_circles(&circle, &points);
43/// p.save("sine-wave.bmp").unwrap();
44/// ```
45pub struct Plot {
46  /// The width of the output image.
47  pub width: i64, // more efficient to store as i64 due to `put_pixel_safe`
48  /// The width of the output image.
49  pub height: i64, // more efficient to store as i64 due to `put_pixel_safe`
50  /// Internal only: TODO: Make private
51  pub frame: PlotFrame,
52  bg_color: Color,
53  canvas_color: Color,
54  canvas_bounds: Bounds<i64>,
55  frame_color: Option<Color>,
56  image: image::RgbImage,
57}
58
59impl Plot {
60
61  /// Creates a new plot with the given image dimensions.
62  pub fn new(width: u32, height: u32) -> Plot {
63    let mut p = Plot {
64      bg_color: WHITE,
65      canvas_color: GREY,
66      width: width as i64,
67      height: height as i64,
68      canvas_bounds: (0..(width as i64), 0..(height as i64)).into(),
69      frame_color: Some(BLACK),
70      frame: PlotFrame::new(
71        (0..(width as i64), 0..(height as i64)).into(),
72        (0.0..1.0, 0.0..1.0).into()),
73      image: RgbImage::new(width, height),
74    }
75      .with_drawing_bounds(0.05..0.95, 0.05..0.95);
76    p.clear();
77    p
78  }
79
80  /// Clears all plotted primitives, redrawing the plot frame and canvas.
81  pub fn clear(&mut self) {
82    let x_bounds = self.canvas_bounds.x.clone();
83    let y_bounds = self.canvas_bounds.y.clone();
84    for y in 0..self.height {
85      for x in 0..self.width {
86        if x_bounds.contains(&x) && y_bounds.contains(&y) {
87          self.image.put_pixel(x as u32, y as u32, self.canvas_color);
88        } else {
89          self.image.put_pixel(x as u32, y as u32, self.bg_color);
90        }
91      }
92    }
93    if let Some(color) = self.frame_color {
94      for y in y_bounds.clone() {
95        self.image.put_pixel(x_bounds.start as u32, y as u32, color);
96        self.image.put_pixel(x_bounds.end as u32, y as u32, color);
97      }
98      for x in x_bounds.clone() {
99        self.image.put_pixel(x as u32, y_bounds.start as u32, color);
100        self.image.put_pixel(x as u32, y_bounds.end as u32, color);
101      }
102    }
103  }
104
105  /// Draws a line segment with the given logical coordinate endpoints.
106  pub fn draw_line<P>(&mut self, point1: P, point2: P, color: Color)
107  where P: Into<(f64, f64)> {
108    let p1 = self.frame.pt_to_px(point1.into());
109    let p2 = self.frame.pt_to_px(point2.into());
110    let dx = p2.0 - p1.0;
111    let dy = p2.1 - p1.1;
112
113    if dx.abs() > dy.abs() {
114      if dx > 0 {
115        for x in p1.0..p2.0 {
116          let y = p1.1 + dy * (x - p1.0) / dx;
117          self.put_pixel_safe(x, y, color);
118        }
119      } else {
120        for x in p2.0..p1.0 {
121          let y = p1.1 + dy * (x - p1.0) / dx;
122          self.put_pixel_safe(x, y, color);
123        }
124      }
125    } else {
126      if dy > 0 {
127        for y in p1.1..p2.1 {
128          let x = p1.0 + dx * (y - p1.1) / dy;
129          self.put_pixel_safe(x, y, color);
130        }
131      } else {
132        for y in p2.1..p1.1 {
133          let x = p1.0 + dx * (y - p1.1) / dy;
134          self.put_pixel_safe(x, y, color);
135        }
136      }
137    }
138    self.put_pixel_safe(p2.0, p2.1, color);
139  }
140
141  /// Draws a line segment with the given physical pixel endpoints.
142  pub fn draw_pixel_line(&mut self, p1: (f64, f64), p2: (f64, f64), color: Color) {
143    let dx = p2.0 - p1.0;
144    let dy = p2.1 - p1.1;
145
146    if dx.abs() > dy.abs() {
147      if dx > 0.0 {
148        let px_x_1 = p1.0.round() as i64;
149        let px_x_2 = p2.0.round() as i64;
150        for x in px_x_1..px_x_2 {
151          let y = p1.1 + dy * (x as f64 - p1.0) / dx;
152          self.put_pixel_safe(x, y.round() as i64, color);
153        }
154      } else {
155        let px_x_1 = p1.0.round() as i64;
156        let px_x_2 = p2.0.round() as i64;
157        for x in px_x_2..px_x_1 {
158          let y = p1.1 + dy * (x as f64 - p1.0) / dx;
159          self.put_pixel_safe(x, y.round() as i64, color);
160        }
161      }
162    } else {
163      if dy > 0.0 {
164        let px_y_1 = p1.1.round() as i64;
165        let px_y_2 = p2.1.round() as i64;
166        for y in px_y_1..px_y_2 {
167          let x = p1.0 + dx * (y as f64 - p1.1) / dy;
168          self.put_pixel_safe(x.round() as i64, y, color);
169        }
170      } else {
171        let px_y_1 = p1.1.round() as i64;
172        let px_y_2 = p2.1.round() as i64;
173        for y in px_y_2..px_y_1 {
174          let x = p1.0 + dx * (y as f64 - p1.1) / dy;
175          self.put_pixel_safe(x.round() as i64, y, color);
176        }
177      }
178    }
179    self.put_pixel_safe(p2.0.round() as i64, p2.1.round() as i64, BLACK);
180  }
181
182  /// Plots data as line segments with the given logical positions and angles.
183  pub fn draw_orientations(&mut self, orientation: &Orientation, positions: &[Point], angles: &[f64]) {
184    for i in 0..positions.len() {
185      orientation.draw(self, positions[i], angles[i]);
186    }
187  }
188
189  /// Plots data as circles with the given logical positions.
190  pub fn draw_circles(&mut self, circle: &Circle, positions: &[Point]) {
191    for i in 0..positions.len() {
192      circle.draw(self, positions[i]);
193    }
194  }
195
196  /// Saves contents of plot as an image.
197  pub fn save(&self, filename: &str) -> Result<(), ImageError> {
198    self.image.save(filename)
199  }
200
201  /// Sets a pixel value, respecting bounds of plotting frame.
202  pub fn put_pixel_safe(&mut self, x: i64, y: i64, c: Color) {
203    if self.canvas_bounds.x.contains(&x) && self.canvas_bounds.y.contains(&y) {
204      self.image.put_pixel(x as u32, y as u32, c)
205    }
206  }
207
208  /// Changes the fraction of the plot image that the plotting canvas takes up.
209  ///
210  /// This accepts ranges of fractions between 0.0 and 1.0 indicating the
211  /// portion of the plot that the plottable area takes up. All plotted data is
212  /// displayed in this area. A bordering frame is drawn around the plotting
213  /// canvas.
214  pub fn with_drawing_bounds<R: Into<Range<f64>>>(mut self, x_bounds: R, y_bounds: R) -> Self {
215    let w = self.width as f64;
216    let h = self.height as f64;
217    let x = x_bounds.into();
218    let y = y_bounds.into();
219    self.canvas_bounds.x = ((x.start * w + 0.5) as i64)..((x.end * w + 0.5) as i64);
220    self.canvas_bounds.y = ((y.start * h + 0.5) as i64)..((y.end * h + 0.5) as i64);
221    self.frame.set_drawing_bounds(self.canvas_bounds.x.clone(), self.canvas_bounds.y.clone());
222    self
223  }
224
225  /// Changes the logical coordinate range displayed in the plot.
226  ///
227  /// For example, `(0.0, 2.0 * PI), (-1.0, 1.0)` would correspond to displaying
228  /// a single period of a standard sine function.
229  pub fn with_plotting_range<R: Into<Range<f64>>>(mut self, x_range: R, y_range: R) -> Self {
230    self.frame.set_plotting_range(x_range, y_range);
231    self
232  }
233
234  /// Changes the background color of the plot image (behind the canvas).
235  pub fn with_bg_color(mut self, color: Color) -> Self {
236    self.bg_color = color;
237    self
238  }
239
240  /// Changes the background color of the plotting canvas.
241  pub fn with_canvas_color(mut self, color: Color) -> Self {
242    self.canvas_color = color;
243    self
244  }
245
246  /// Changes the color of the border around the plotting canvas.
247  pub fn with_frame_color(mut self, color: Option<Color>) -> Self {
248    self.frame_color = color;
249    self
250  }
251
252}