Skip to main content

rust_ppm/plot/
plot.rs

1use std::io;
2use std::path::Path;
3
4use crate::Image;
5
6use super::{Axes, Canvas, LineStyle, MarkerStyle};
7
8/// Default image dimensions for a plot.
9pub const DEFAULT_SIZE: (usize, usize) = (640, 480);
10
11/// A high-level, image-backed plot builder.
12pub 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    /// Creates a 640x480 plot with automatic axis limits.
29    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    /// Sets the plotted image size in pixels.
42    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    /// Sets a render scale for higher-resolution output.
53    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    /// Sets horizontal and vertical plot-area margins in pixels.
60    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    /// Replaces the plot axes configuration.
67    pub fn axes(mut self, axes: Axes) -> Self {
68        self.axes = axes;
69        self
70    }
71
72    /// Adds a connected line series to the plot.
73    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    /// Adds a marker-based series to the plot.
79    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    /// Renders this plot into an RGB image.
86    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    /// Renders and saves this plot as a PPM image.
113    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}