1use std::io;
2use std::path::Path;
3
4use crate::Image;
5
6use super::{Axes, Canvas, LineStyle, MarkerStyle};
7
8pub const DEFAULT_SIZE: (usize, usize) = (640, 480);
10
11pub struct Plot {
13 width: usize,
14 height: usize,
15 scale: usize,
16 inset_x: usize,
17 inset_y: usize,
18 axes: Axes,
19 series: Vec<PlotSeries>,
20}
21
22enum PlotSeries {
23 Line(Vec<(f64, f64)>, LineStyle),
24 Markers(Vec<(f64, f64)>, MarkerStyle),
25}
26
27impl Plot {
28 pub fn new() -> Self {
30 Self {
31 width: DEFAULT_SIZE.0,
32 height: DEFAULT_SIZE.1,
33 scale: 1,
34 inset_x: DEFAULT_SIZE.0 / 10,
35 inset_y: DEFAULT_SIZE.1 / 10,
36 axes: Axes::new(),
37 series: Vec::new(),
38 }
39 }
40
41 pub fn size(mut self, width: usize, height: usize) -> Self {
43 let x_ratio = self.inset_x as f64 / self.width.max(1) as f64;
44 let y_ratio = self.inset_y as f64 / self.height.max(1) as f64;
45 self.width = width;
46 self.height = height;
47 self.inset_x = (width as f64 * x_ratio).round() as usize;
48 self.inset_y = (height as f64 * y_ratio).round() as usize;
49 self
50 }
51
52 pub fn scale(mut self, factor: usize) -> Self {
54 assert!(factor > 0, "plot scale must be greater than zero");
55 self.scale = factor;
56 self
57 }
58
59 pub fn margins(mut self, horizontal: usize, vertical: usize) -> Self {
61 self.inset_x = horizontal;
62 self.inset_y = vertical;
63 self
64 }
65
66 pub fn axes(mut self, axes: Axes) -> Self {
68 self.axes = axes;
69 self
70 }
71
72 pub fn line(mut self, points: &[(f64, f64)], style: LineStyle) -> Self {
74 self.series.push(PlotSeries::Line(points.to_vec(), style));
75 self
76 }
77
78 pub fn markers(mut self, points: &[(f64, f64)], style: MarkerStyle) -> Self {
80 self.series
81 .push(PlotSeries::Markers(points.to_vec(), style));
82 self
83 }
84
85 pub fn render(self) -> Image {
87 let scale = self.scale;
88 let mut canvas = Canvas::with_inset(
89 self.width.saturating_mul(scale),
90 self.height.saturating_mul(scale),
91 self.axes,
92 self.inset_x.saturating_mul(scale),
93 self.inset_y.saturating_mul(scale),
94 );
95 canvas.set_render_scale(scale);
96 canvas.render();
97 for series in self.series {
98 match series {
99 PlotSeries::Line(points, mut style) => {
100 style.width = style.width.saturating_mul(scale);
101 canvas.line(&points, style);
102 }
103 PlotSeries::Markers(points, mut style) => {
104 style.size = style.size.saturating_mul(scale);
105 canvas.markers(&points, style);
106 }
107 }
108 }
109 canvas.into_image()
110 }
111
112 pub fn save(self, path: impl AsRef<Path>) -> io::Result<()> {
114 self.render().save(path)
115 }
116}
117
118impl Default for Plot {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::Pixel;
128
129 #[test]
130 fn builder_renders_multiple_styled_series() {
131 let image = Plot::new()
132 .size(11, 11)
133 .margins(0, 0)
134 .axes(Axes::from_limits((0.0, 1.0), (0.0, 1.0)))
135 .line(
136 &[(0.0, 0.0), (1.0, 1.0)],
137 LineStyle::new().color(Pixel::rgb(0, 128, 0)),
138 )
139 .markers(
140 &[(0.0, 1.0)],
141 MarkerStyle::new().color(Pixel::rgb(255, 128, 0)),
142 )
143 .render();
144
145 assert_eq!(image.get_pixel(5, 5), Some(&Pixel::rgb(0, 128, 0)));
146 assert_eq!(image.get_pixel(0, 0), Some(&Pixel::rgb(255, 128, 0)));
147 }
148
149 #[test]
150 fn scale_increases_output_resolution() {
151 let image = Plot::new().size(100, 80).scale(2).render();
152 assert_eq!((image.width, image.height), (200, 160));
153 }
154}