#[deny(unsafe_code)]
#[macro_export]
macro_rules! plot {
(
$( $( $variable:ident )? $( ( $x:ident, $y:ident ) )? $( as $name:literal )? ),* $(,)?
$( where $($key:ident = $value:expr),* $(,)? )?
) => {
#[cfg(any(debug_assertions, feature = "plot-release"))]
#[cfg(feature = "debug")]
{
use std::cell::RefCell;
use $crate::{PLOTS, Plottable, Plot, Options, Location};
let location = Location {
file: file!(),
line: line!(),
column: column!(),
};
PLOTS.with(|plots| {
let mut map = plots
.plots
.borrow_mut();
let plot = map
.entry(location.clone())
.or_insert_with(|| {
let names = [$({
$(
let name = stringify!($variable);
)?
$(
let name = stringify!($y);
)?
$(
let name = $name;
)?
name
}),*];
let options = Options {
$($(
$key: Some($value.into()),
)*)?
..Default::default()
};
Plot::new(names, location, options)
});
let iteration = plot.iteration();
plot.insert([$({
$(
let value = (iteration.to_plot_type(), $variable.to_plot_type());
)?
$(
let value = ($x.to_plot_type(), $y.to_plot_type());
)?
value
}),*]);
});
}
};
}
#[cfg(feature = "debug")]
pub use debug::*;
#[cfg(feature = "debug")]
mod debug {
use num_traits::cast::ToPrimitive;
use plotters::prelude::*;
use std::{cell::RefCell, collections::HashMap, fmt, ops::Range};
thread_local! {
pub static PLOTS: Plots = Plots::new();
}
pub struct Plots {
pub plots: RefCell<HashMap<Location, Plot>>,
}
impl Drop for Plots {
fn drop(&mut self) {
for (_, plot) in self.plots.borrow().iter() {
plot.plot().unwrap();
}
}
}
#[derive(Hash, PartialEq, Eq, Clone, Copy)]
pub struct Location {
pub file: &'static str,
pub line: u32,
pub column: u32,
}
impl fmt::Display for Location {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
}
}
impl Plots {
fn new() -> Self {
Plots {
plots: RefCell::new(HashMap::new()),
}
}
}
type PlotType = f64;
pub trait Plottable {
fn to_plot_type(self) -> PlotType;
}
impl<T> Plottable for T
where
T: ToPrimitive,
{
fn to_plot_type(self) -> PlotType {
self.to_f64().unwrap()
}
}
type Name = &'static str;
pub struct Plot {
values: Vec<Vec<(PlotType, PlotType)>>,
names: Vec<Name>,
location: Location,
options: Options,
iteration: u64,
}
impl Plot {
pub fn new<const N: usize>(
names: [&'static str; N],
location: Location,
options: Options,
) -> Plot {
Plot {
values: vec![Vec::new(); N],
names: names.to_vec(),
location,
options,
iteration: 0,
}
}
pub fn iteration(&self) -> u64 {
self.iteration
}
pub fn insert<const N: usize>(&mut self, values: [(PlotType, PlotType); N]) {
for (i, &value) in values.iter().enumerate() {
self.values[i].push(value)
}
self.iteration += 1;
}
fn x_min(&self) -> PlotType {
self.values
.iter()
.map(|values| values.iter().map(|(x, _)| x))
.flatten()
.fold(PlotType::MAX, |acc, &val| if val < acc { val } else { acc })
}
fn x_max(&self) -> PlotType {
self.values
.iter()
.map(|values| values.iter().map(|(x, _)| x))
.flatten()
.fold(PlotType::MIN, |acc, &val| if val > acc { val } else { acc })
}
fn y_min(&self) -> PlotType {
self.values
.iter()
.map(|values| values.iter().map(|(_, y)| y))
.flatten()
.fold(PlotType::MAX, |acc, &val| if val < acc { val } else { acc })
}
fn y_max(&self) -> PlotType {
self.values
.iter()
.map(|values| values.iter().map(|(_, y)| y))
.flatten()
.fold(PlotType::MIN, |acc, &val| if val > acc { val } else { acc })
}
fn plot(&self) -> Result<(), Box<dyn std::error::Error>> {
let default_caption = &format!("{}", self.location);
let caption = self.options.caption.as_ref().unwrap_or(default_caption);
let default_path =
&format!("plots/{}.png", caption.replace("/", "-").replace(" ", "_"));
let path = self.options.path.as_ref().unwrap_or(default_path);
let path = std::path::Path::new(&path);
println!("Saving plot \"{}\" to {:?}", caption, path);
std::fs::create_dir_all(&path.parent().unwrap()).unwrap();
let root = BitMapBackend::new(&path, self.options.size.unwrap_or((640, 480)))
.into_drawing_area();
root.fill(&WHITE)?;
let mut chart = ChartBuilder::on(&root)
.caption(caption, 30)
.margin(30)
.x_label_area_size(30)
.y_label_area_size(60)
.build_cartesian_2d(
self.options.x_range.clone().unwrap_or(self.x_min()..self.x_max()),
self.options.y_range.clone().unwrap_or(self.y_min()..self.y_max()),
)?;
let mut mesh = chart.configure_mesh();
if let Some(x_desc) = &self.options.x_desc {
mesh.x_desc(x_desc);
}
if let Some(y_desc) = &self.options.y_desc {
mesh.y_desc(y_desc);
}
mesh.draw()?;
for (i, (&name, values)) in self.names.iter().zip(self.values.iter()).enumerate() {
let color = HSLColor(i as f64 / self.names.len() as f64, 1.0, 0.5);
chart
.draw_series(LineSeries::new(values.iter().copied(), &color))?
.label(name)
.legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &color));
}
chart
.configure_series_labels()
.background_style(&WHITE.mix(0.8))
.border_style(&BLACK)
.draw()?;
Ok(())
}
}
#[derive(Default)]
pub struct Options {
pub caption: Option<String>,
pub size: Option<(u32, u32)>,
pub x_desc: Option<String>,
pub y_desc: Option<String>,
pub path: Option<String>,
pub x_range: Option<Range<PlotType>>,
pub y_range: Option<Range<PlotType>>,
}
}