use std::io;
use std::path::Path;
use crate::Image;
use super::{Axes, Canvas, LineStyle, MarkerStyle};
pub const DEFAULT_SIZE: (usize, usize) = (640, 480);
pub struct Plot {
width: usize,
height: usize,
scale: usize,
inset_x: usize,
inset_y: usize,
axes: Axes,
series: Vec<PlotSeries>,
}
enum PlotSeries {
Line(Vec<(f64, f64)>, LineStyle),
Markers(Vec<(f64, f64)>, MarkerStyle),
}
impl Plot {
pub fn new() -> Self {
Self {
width: DEFAULT_SIZE.0,
height: DEFAULT_SIZE.1,
scale: 1,
inset_x: DEFAULT_SIZE.0 / 10,
inset_y: DEFAULT_SIZE.1 / 10,
axes: Axes::new(),
series: Vec::new(),
}
}
pub fn size(mut self, width: usize, height: usize) -> Self {
let x_ratio = self.inset_x as f64 / self.width.max(1) as f64;
let y_ratio = self.inset_y as f64 / self.height.max(1) as f64;
self.width = width;
self.height = height;
self.inset_x = (width as f64 * x_ratio).round() as usize;
self.inset_y = (height as f64 * y_ratio).round() as usize;
self
}
pub fn scale(mut self, factor: usize) -> Self {
assert!(factor > 0, "plot scale must be greater than zero");
self.scale = factor;
self
}
pub fn margins(mut self, horizontal: usize, vertical: usize) -> Self {
self.inset_x = horizontal;
self.inset_y = vertical;
self
}
pub fn axes(mut self, axes: Axes) -> Self {
self.axes = axes;
self
}
pub fn line(mut self, points: &[(f64, f64)], style: LineStyle) -> Self {
self.series.push(PlotSeries::Line(points.to_vec(), style));
self
}
pub fn markers(mut self, points: &[(f64, f64)], style: MarkerStyle) -> Self {
self.series
.push(PlotSeries::Markers(points.to_vec(), style));
self
}
pub fn render(self) -> Image {
let scale = self.scale;
let mut canvas = Canvas::with_inset(
self.width.saturating_mul(scale),
self.height.saturating_mul(scale),
self.axes,
self.inset_x.saturating_mul(scale),
self.inset_y.saturating_mul(scale),
);
canvas.set_render_scale(scale);
canvas.render();
for series in self.series {
match series {
PlotSeries::Line(points, mut style) => {
style.width = style.width.saturating_mul(scale);
canvas.line(&points, style);
}
PlotSeries::Markers(points, mut style) => {
style.size = style.size.saturating_mul(scale);
canvas.markers(&points, style);
}
}
}
canvas.into_image()
}
pub fn save(self, path: impl AsRef<Path>) -> io::Result<()> {
self.render().save(path)
}
}
impl Default for Plot {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Pixel;
#[test]
fn builder_renders_multiple_styled_series() {
let image = Plot::new()
.size(11, 11)
.margins(0, 0)
.axes(Axes::from_limits((0.0, 1.0), (0.0, 1.0)))
.line(
&[(0.0, 0.0), (1.0, 1.0)],
LineStyle::new().color(Pixel::rgb(0, 128, 0)),
)
.markers(
&[(0.0, 1.0)],
MarkerStyle::new().color(Pixel::rgb(255, 128, 0)),
)
.render();
assert_eq!(image.get_pixel(5, 5), Some(&Pixel::rgb(0, 128, 0)));
assert_eq!(image.get_pixel(0, 0), Some(&Pixel::rgb(255, 128, 0)));
}
#[test]
fn scale_increases_output_resolution() {
let image = Plot::new().size(100, 80).scale(2).render();
assert_eq!((image.width, image.height), (200, 160));
}
}