use crate::visualization::{ColorScheme, Graph, GraphConfig, GraphData, LineStyle};
#[cfg(feature = "static_export")]
use {crate::error::GraphError, std::path::Path};
pub trait Plottable {
fn plot(&self) -> PlotBuilder<Self>
where
Self: Sized + Graph;
}
pub struct PlotBuilder<T: Plottable + Graph> {
pub(crate) data: T,
pub(crate) options: GraphConfig,
}
impl<T: Plottable + Graph> PlotBuilder<T> {
pub fn title(mut self, title: impl Into<String>) -> Self {
self.options.title = title.into();
if self.options.legend.is_none() {
self.options.legend = Some(vec![self.options.title.clone()]);
}
self
}
pub fn x_label(mut self, label: impl Into<String>) -> Self {
self.options.x_label = Some(label.into());
self
}
pub fn y_label(mut self, label: impl Into<String>) -> Self {
self.options.y_label = Some(label.into());
self
}
pub fn z_label(mut self, label: impl Into<String>) -> Self {
self.options.z_label = Some(label.into());
self
}
pub fn line_style(mut self, line_style: LineStyle) -> Self {
self.options.line_style = line_style;
self
}
pub fn legend(mut self, legend: Vec<impl Into<String>>) -> Self {
let legend: Vec<String> = legend.into_iter().map(|l| l.into()).collect();
self.options.legend = Some(legend);
self
}
pub fn add_legend(mut self, legend: impl Into<String>) -> Self {
if let Some(ref mut legends) = self.options.legend {
legends.push(legend.into());
} else {
self.options.legend = Some(vec![legend.into()]);
}
self
}
pub fn dimensions(mut self, width: u32, height: u32) -> Self {
self.options.width = width;
self.options.height = height;
self
}
pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
self.options.color_scheme = color_scheme;
self
}
pub fn show_legend(mut self, show_legend: bool) -> Self {
self.options.show_legend = show_legend;
self
}
#[cfg(feature = "static_export")]
pub fn save(self, path: impl AsRef<Path>) -> Result<(), GraphError> {
let path = path.as_ref();
self.write_png(path).inspect_err(|error| {
tracing::debug!(path = %path.display(), %error, "failed to save plot");
})
}
}
impl<T: Plottable + Graph> Graph for PlotBuilder<T> {
fn graph_data(&self) -> GraphData {
self.data.graph_data()
}
fn graph_config(&self) -> GraphConfig {
GraphConfig {
title: self.options.title.clone(),
width: 1600,
height: 900,
x_label: self.options.x_label.clone(),
y_label: self.options.y_label.clone(),
z_label: self.options.z_label.clone(),
line_style: self.options.line_style,
color_scheme: self.options.color_scheme.clone(),
legend: self.options.legend.clone(),
show_legend: self.options.show_legend,
}
}
}
#[cfg(all(test, feature = "static_export"))]
mod tests {
use super::*;
use crate::visualization::{Series2D, TraceMode};
use rust_decimal_macros::dec;
struct Lopsided;
impl Graph for Lopsided {
fn graph_data(&self) -> GraphData {
GraphData::Series(Series2D {
x: vec![dec!(1), dec!(2)],
y: vec![dec!(1)],
name: "lopsided".to_string(),
mode: TraceMode::Lines,
line_color: None,
line_width: None,
})
}
}
impl Plottable for Lopsided {
fn plot(&self) -> PlotBuilder<Self> {
PlotBuilder {
data: Lopsided,
options: GraphConfig::default(),
}
}
}
#[test]
fn test_save_returns_the_export_error_typed() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let result = Lopsided.plot().save(dir.path().join("lopsided.png"));
assert!(
matches!(result, Err(GraphError::LengthMismatch { x: 2, y: 1, .. })),
"{result:?}"
);
Ok(())
}
}