reda-spice 0.1.0

Spice simulate and parse library
Documentation
mod error;
use std::path::Path;

use derive_builder::Builder;
pub use error::*;

use plotters::{
    chart::ChartBuilder, 
    prelude::{BitMapBackend, IntoDrawingArea, PathElement}, 
    series::LineSeries, 
    style::{Color, Palette, Palette99, RGBColor, BLACK, RED, WHITE}
};

#[derive(Debug, Clone, Builder)]
#[builder(setter(strip_option, into))]
pub struct Drawer {
    #[builder(default = "false")]
    pub split: bool,

    #[builder(default = "1280")]
    pub width: u32,

    #[builder(default = "720")]
    pub height: u32,
    
    #[builder(default = "WHITE")]
    pub background_color: RGBColor,

    #[builder(default = "RED")]
    pub line_color: RGBColor,

    #[builder(default = "(\"sans-serif\", 15)")]
    pub font: (&'static str, u32),
}

impl Default for Drawer {
    fn default() -> Self {
        DrawerBuilder::default().build().unwrap()
    }
}

impl Drawer {
    pub fn draw<P: AsRef<Path>>(
        &self, 
        x_label: &str,
        y_label: &str,
        x: &[f64], 
        ys: &[(String, Vec<f64>)], 
        path: P
    ) -> Result<(), DrawerError> { 
        if self.split {
            self.draw_split(x_label, y_label, x, ys, path)
        } else {
            self.draw_combined(x_label, y_label, x, ys, path)
        }
    } 

    pub fn draw_split<P: AsRef<Path>>(
        &self, 
        x_label: &str,
        y_label: &str,
        x: &[f64], 
        ys: &[(String, Vec<f64>)], 
        path: P
    ) -> Result<(), DrawerError> {    
        let root = BitMapBackend::new(path.as_ref(), (self.width, self.height)).into_drawing_area();
        root
            .fill(&self.background_color)
            .map_err(|e| DrawerError::FillBackground(e.to_string()))?;
    
        let n = ys.len().max(1);
        let rows = n;
    
        let areas = root.split_evenly((rows, 1));
    
        for ((label, values), area) in ys.iter().zip(areas) {
            let (min_y, max_y) = values.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &v| {
                (min.min(v), max.max(v))
            });
    
            let mut chart = ChartBuilder::on(&area)
                .margin(5)
                .x_label_area_size(20)
                .y_label_area_size(40)
                .caption(label, self.font)
                .build_cartesian_2d(
                    x.first().copied().unwrap_or(0.0)..x.last().copied().unwrap_or(1.0),
                    min_y..max_y,
                )
                .map_err(|e| DrawerError::BuildCartesian(e.to_string()))?;
    
            chart
                .configure_mesh()
                .x_desc(x_label)
                .y_desc(y_label)
                .draw()
                .map_err(|e| DrawerError::DrawChart(e.to_string()))?;
    
            chart.draw_series(LineSeries::new(
                x.iter().cloned().zip(values.iter().cloned()),
                &self.line_color,
            ))
            .map_err(|e| DrawerError::DrawLine(label.clone(), e.to_string()))?;
        }
    
        Ok(())
    }

    pub fn draw_combined<P: AsRef<Path>>(
        &self,
        x_label: &str,
        y_label: &str,
        x: &[f64], 
        ys: &[(String, Vec<f64>)], 
        path: P,
    ) -> Result<(), DrawerError> {
        let root = BitMapBackend::new(path.as_ref(), (self.width, self.height)).into_drawing_area();
        root
            .fill(&self.background_color)
            .map_err(|e| DrawerError::FillBackground(e.to_string()))?;

        let (min_y, max_y) = ys
            .iter()
            .flat_map(|(_, v)| v.iter())
            .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &v| {
                (min.min(v), max.max(v))
            });

        let mut chart = ChartBuilder::on(&root)
            .margin(20)
            .caption("Combined Plot", self.font)
            .x_label_area_size(30)
            .y_label_area_size(50)
            .build_cartesian_2d(
                x.first().copied().unwrap_or(0.0)..x.last().copied().unwrap_or(1.0),
                min_y..max_y,
            )
            .map_err(|e| DrawerError::BuildCartesian(e.to_string()))?;

        chart
            .configure_mesh()
            .x_desc(x_label)
            .y_desc(y_label)
            .draw()
            .map_err(|e| DrawerError::DrawChart(e.to_string()))?;

        for (i, (label, values)) in ys.iter().enumerate() {
            let color = Palette99::pick(i).mix(0.9);
            chart
                .draw_series(LineSeries::new(
                    x.iter().cloned().zip(values.iter().cloned()),
                    &color,
                ))
                .map_err(|e| DrawerError::DrawLine(label.clone(), e.to_string()))?
                .label(label)
                .legend(move |(x, y)| {
                    PathElement::new([(x, y), (x + 20, y)], &color)
                });
        }

        chart
            .configure_series_labels()
            .border_style(&BLACK)
            .draw()
            .map_err(|e| DrawerError::DrawChart(e.to_string()))?;

        Ok(())
    }
}