visu 0.2.4

Tool to simply visualise data
Documentation
use anyhow::Result;
use plotters::{
    backend::BitMapBackend,
    chart::{ChartBuilder, ChartContext, LabelAreaPosition, SeriesLabelPosition},
    coord::types::RangedCoordu128,
    drawing::IntoDrawingArea,
    element::PathElement,
    prelude::{
        Cartesian2d, LineSeries, RGBAColor, RGBColor, BLACK, BLUE, CYAN, GREEN, MAGENTA, RED, WHITE, YELLOW,
    },
    style::Color,
};

use crate::normalized_data::normalize_data;

const COLORS: [RGBColor; 7] = [GREEN, BLUE, MAGENTA, CYAN, YELLOW, RED, BLACK];

pub struct Graph<'a, const SIZE: usize> {
    pub title:       &'a str,
    pub x_title:     &'a str,
    pub y_title:     &'a str,
    pub data:        [&'a [u128]; SIZE],
    pub legend:      [&'a str; SIZE],
    pub output_file: &'a str,
}

impl<'a, const SIZE: usize> Default for Graph<'a, SIZE> {
    fn default() -> Self {
        Self {
            title:       "",
            x_title:     "",
            y_title:     "",
            data:        [&[]; SIZE],
            legend:      [""; SIZE],
            output_file: "",
        }
    }
}

pub fn render_chart<const SIZE: usize>(graph: Graph<SIZE>) -> Result<()> {
    let root = BitMapBackend::new(graph.output_file, (2800, 1800)).into_drawing_area();

    root.fill(&WHITE)?;

    let data = normalize_data(graph.data);

    let mut chart = ChartBuilder::on(&root)
        .set_label_area_size(LabelAreaPosition::Left, 100)
        .set_label_area_size(LabelAreaPosition::Bottom, 100)
        .margin(10)
        .caption(graph.title, ("sans-serif", 60))
        .build_cartesian_2d(data.x_range, data.y_range)?;

    #[allow(clippy::cast_precision_loss)]
    chart
        .configure_mesh()
        .y_label_style(("sans-serif", 40))
        .x_label_style(("sans-serif", 40))
        .x_desc(graph.x_title)
        .y_desc(graph.y_title)
        .y_label_formatter(&|value| {
            format!(
                "{:.1}",
                if data.y_divider > 0 {
                    value / data.y_divider
                } else {
                    *value
                }
            )
        })
        .draw()?;

    for (i, data) in data.data.iter().enumerate() {
        draw_graph(
            &mut chart,
            data.iter().enumerate().map(|(pos, val)| (pos as u128, *val)),
            graph.legend[i],
            COLORS[i],
        )?;
    }

    chart
        .configure_series_labels()
        .position(SeriesLabelPosition::UpperLeft)
        .legend_area_size(140)
        .label_font(("sans-serif", 60))
        .border_style(BLACK.stroke_width(4))
        .draw()?;

    root.present().expect("Unable to write result to file");
    Ok(())
}

fn draw_graph(
    chart: &mut ChartContext<BitMapBackend, Cartesian2d<RangedCoordu128, RangedCoordu128>>,
    data: impl IntoIterator<Item = (u128, u128)>,
    label: &str,
    color: impl Into<RGBAColor>,
) -> Result<()> {
    let color: RGBAColor = color.into();
    let series = LineSeries::new(data, color.stroke_width(8));
    chart
        .draw_series(series)?
        .label(label)
        .legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 100, y)], color.stroke_width(8)));

    Ok(())
}