use crate::{XResult, utils::minmax};
use derive_builder::Builder;
use either::Either;
use plotters::{prelude::*, style::Color as _};
use std::{f64, ops::Range, path::PathBuf};
pub use plotters::prelude::FontStyle;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum LineStyle {
#[default]
Solid,
Dashed,
Dotted,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum PlotterBackend {
#[default]
BitMap,
SVG,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Color {
Red,
Blue,
Green,
Black,
White,
Yellow,
Cyan,
Magenta,
RGB(u8, u8, u8),
}
impl From<Color> for RGBColor {
fn from(color: Color) -> Self {
match color {
Color::Red => RED,
Color::Blue => BLUE,
Color::Green => GREEN,
Color::Black => BLACK,
Color::White => WHITE,
Color::Yellow => YELLOW,
Color::Cyan => CYAN,
Color::Magenta => MAGENTA,
Color::RGB(r, g, b) => RGBColor(r, g, b),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Copy)]
pub enum LegendPosition {
UpperLeft,
MiddleLeft,
LowerLeft,
UpperMiddle,
MiddleMiddle,
LowerMiddle,
#[default]
UpperRight,
MiddleRight,
LowerRight,
Coordinate(i32, i32),
}
impl From<LegendPosition> for SeriesLabelPosition {
fn from(position: LegendPosition) -> Self {
match position {
LegendPosition::UpperLeft => SeriesLabelPosition::UpperLeft,
LegendPosition::MiddleLeft => SeriesLabelPosition::MiddleLeft,
LegendPosition::LowerLeft => SeriesLabelPosition::LowerLeft,
LegendPosition::UpperMiddle => SeriesLabelPosition::UpperMiddle,
LegendPosition::MiddleMiddle => SeriesLabelPosition::MiddleMiddle,
LegendPosition::LowerMiddle => SeriesLabelPosition::LowerMiddle,
LegendPosition::UpperRight => SeriesLabelPosition::UpperRight,
LegendPosition::MiddleRight => SeriesLabelPosition::MiddleRight,
LegendPosition::LowerRight => SeriesLabelPosition::LowerRight,
LegendPosition::Coordinate(x, y) => SeriesLabelPosition::Coordinate(x, y),
}
}
}
#[derive(Builder, Clone)]
#[builder(pattern = "mutable")]
pub struct PlotConfig {
#[builder(default)]
pub(crate) backend: PlotterBackend,
#[builder(default = "Color::White")]
pub(crate) background_color: Color,
#[builder(default = "\"\".into()", setter(into))]
pub(crate) title: String,
#[builder(default = "sans-serif".into(), setter(into))]
pub(crate) title_font_family: String,
#[builder(default = "50.0", setter(into))]
pub(crate) title_font_size: f64,
#[builder(default = "FontStyle::Normal")]
pub(crate) title_font_style: FontStyle,
#[builder(default = "\"Trajectory\".into()", setter(into))]
pub(crate) caption: String,
#[builder(default = "sans-serif".into(), setter(into))]
pub(crate) caption_font_family: String,
#[builder(default = "30.0", setter(into))]
pub(crate) caption_font_size: f64,
#[builder(default = "FontStyle::Normal")]
pub(crate) caption_font_style: FontStyle,
#[builder(default = "5", setter(into))]
pub(crate) margin: u32,
#[builder(default = "30", setter(into))]
pub(crate) x_label_area_size: u32,
#[builder(default = "30", setter(into))]
pub(crate) y_label_area_size: u32,
#[builder(setter(into, strip_option), default)]
pub(crate) x_spec: Option<Range<f64>>,
#[builder(setter(into, strip_option), default)]
pub(crate) y_spec: Option<Range<f64>>,
#[builder(default = "String::from(\"Time\")", setter(into))]
pub(crate) x_label: String,
#[builder(default = "String::from(\"Position\")", setter(into))]
pub(crate) y_label: String,
#[builder(default = "0.01", setter(into))]
pub(crate) time_step: f64,
#[builder(default = "(800, 600)", setter(into))]
pub(crate) size: (u32, u32),
#[builder(default = "PathBuf::from(\"result.png\")", setter(into))]
pub(crate) output_path: PathBuf,
#[builder(default = "true")]
pub(crate) show_grid: bool,
#[builder(default = "Color::Blue")]
pub(crate) line_color: Color,
#[builder(default)]
pub(crate) line_style: LineStyle,
#[builder(default = "true")]
pub(crate) show_legend: bool,
#[builder(default = "\"Trajectory\".into()", setter(into))]
pub(crate) legend: String,
#[builder(default = "false")]
pub(crate) show_points: bool,
#[builder(default = "3", setter(into))]
pub(crate) point_size: u32,
#[builder(default = "false")]
pub(crate) filled: bool,
#[builder(default = "[5, 10, 1]", setter(into))]
pub(crate) dash_style: [u32; 3],
#[builder(default = "[1, 1]", setter(into))]
pub(crate) dot_style: [u32; 2],
#[builder(default = "true")]
pub(crate) stairs: bool,
#[builder(default)]
pub(crate) legend_position: LegendPosition,
}
impl PlotConfig {
pub(crate) fn plot<Backend: DrawingBackend>(
&self,
backend: Backend,
traj: (Vec<f64>, Vec<f64>),
) -> XResult<()> {
let (times, positions) = traj;
let max_time = *times.last().unwrap();
let (min_x, max_x) = minmax(&positions);
let meta = (max_time, min_x, max_x);
let points: Vec<(f64, f64)> = times.iter().zip(positions).map(|(&t, x)| (t, x)).collect();
set_config(self, backend, points, meta, false)
}
pub(crate) fn stair<Backend: DrawingBackend>(
&self,
backend: Backend,
traj: (Vec<f64>, Vec<f64>),
) -> XResult<()> {
let (times, positions) = traj;
let max_time = *times.last().unwrap();
let (min_x, max_x) = minmax(&positions);
let meta = (max_time, min_x, max_x);
let points: Vec<(f64, f64)> = times
.iter()
.zip(positions)
.enumerate()
.flat_map(|(i, (&t, y))| {
if i == times.len() - 1 {
vec![(t, y)]
} else {
vec![(t, y), (times[i + 1], y)]
}
})
.collect();
set_config(self, backend, points, meta, false)
}
}
pub(crate) fn set_config<Backend: DrawingBackend>(
config: &PlotConfig,
backend: Backend,
data: Vec<(f64, f64)>,
meta: (f64, f64, f64),
log_scale: bool,
) -> XResult<()> {
let (max_time, min_x, max_x) = meta;
let x_spec = match config.x_spec.clone() {
Some(x_spec) => x_spec,
None => 0.0..max_time,
};
let y_spec = match config.y_spec.clone() {
Some(y_spec) => y_spec,
None => {
let min_x = min_x * 1.25;
let max_x = max_x * 1.25;
min_x..max_x
}
};
let title_font_familiy = config.title_font_family.as_str().into();
let title_font = FontDesc::new(
title_font_familiy,
config.title_font_size,
config.title_font_style,
);
let root = backend.into_drawing_area();
let root = root.titled(&config.title, title_font)?;
let background_color: RGBColor = config.background_color.clone().into();
root.fill(&background_color)?;
let caption_font_familiy = config.caption_font_family.as_str().into();
let caption_font = FontDesc::new(
caption_font_familiy,
config.caption_font_size,
config.caption_font_style,
);
let mut chart_builder = ChartBuilder::on(&root);
let chart_builder = chart_builder
.caption(&config.caption, caption_font)
.margin(config.margin)
.x_label_area_size(config.x_label_area_size)
.y_label_area_size(config.y_label_area_size);
let mut chart = if log_scale {
Either::Left(chart_builder.build_cartesian_2d(x_spec.log_scale(), y_spec.log_scale())?)
} else {
Either::Right(chart_builder.build_cartesian_2d(x_spec, y_spec)?)
};
if config.show_grid {
match &mut chart {
Either::Left(chart) => {
chart
.configure_mesh()
.x_desc(&config.x_label)
.y_desc(&config.y_label)
.draw()?;
}
Either::Right(chart) => {
chart
.configure_mesh()
.x_desc(&config.x_label)
.y_desc(&config.y_label)
.draw()?;
}
}
} else {
match &mut chart {
Either::Left(chart) => {
chart
.configure_mesh()
.disable_mesh()
.x_desc(&config.x_label)
.y_desc(&config.y_label)
.draw()?;
}
Either::Right(chart) => {
chart
.configure_mesh()
.disable_mesh()
.x_desc(&config.x_label)
.y_desc(&config.y_label)
.draw()?;
}
}
};
let line_color: RGBColor = config.line_color.clone().into();
let legend_color = line_color;
let dash_shape_style = ShapeStyle {
color: line_color.into(),
filled: config.filled,
stroke_width: config.dash_style[2],
};
let dot_shape_style = ShapeStyle {
color: line_color.into(),
filled: config.filled,
stroke_width: config.dot_style[0],
};
let tmp = match config.line_style {
LineStyle::Solid => {
let line = if config.show_points {
if config.filled {
LineSeries::new(data, line_color.filled()).point_size(config.point_size)
} else {
LineSeries::new(data, line_color).point_size(config.point_size)
}
} else {
LineSeries::new(data, line_color)
};
match &mut chart {
Either::Left(chart) => chart.draw_series(line)?,
Either::Right(chart) => chart.draw_series(line)?,
}
}
LineStyle::Dashed => {
let line = DashedLineSeries::new(
data,
config.dash_style[0],
config.dash_style[1],
dash_shape_style,
);
match &mut chart {
Either::Left(chart) => chart.draw_series(line)?,
Either::Right(chart) => chart.draw_series(line)?,
}
}
LineStyle::Dotted => {
let line = DashedLineSeries::new(
data,
config.dot_style[0],
config.dot_style[1],
dot_shape_style,
);
match &mut chart {
Either::Left(chart) => chart.draw_series(line)?,
Either::Right(chart) => chart.draw_series(line)?,
}
}
};
tmp.label(&config.legend);
if config.show_legend {
tmp.legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], legend_color));
}
match &mut chart {
Either::Left(chart) => {
chart
.configure_series_labels()
.position(config.legend_position.into())
.background_style(background_color)
.border_style(BLACK)
.draw()?;
}
Either::Right(chart) => {
chart
.configure_series_labels()
.position(config.legend_position.into())
.background_style(background_color)
.border_style(BLACK)
.draw()?;
}
}
root.present()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plot_config_builder() {
let config = PlotConfigBuilder::default()
.title("Test Plot")
.x_label("Time")
.y_label("Position")
.size((800, 600))
.output_path("test_plot.png")
.build()
.unwrap();
assert_eq!(config.title, "Test Plot");
assert_eq!(config.x_label, "Time");
assert_eq!(config.y_label, "Position");
assert_eq!(config.size, (800, 600));
assert_eq!(config.output_path, PathBuf::from("test_plot.png"));
}
}