optionstratlib-visualization 0.23.1

Backend-neutral chart models, the Graph contract and its implementations for OptionStratLib types, with optional Plotly rendering and static export. Re-exported by the optionstratlib facade under its `visualization` feature.
Documentation
//! # Surface Visualization Module
//!
//! `Graph` and `Plottable` for a [`Surface`]: the surface charts as one
//! [`Surface3D`] holding the `x`, `y` and `z` coordinates of its points, in
//! point order.
//!
//! [`PlotBuilder`] configures the chart (title, axis labels, line style,
//! colours, legend). With `plotly` it renders to HTML; with `static_export`,
//! `PlotBuilder::save` writes a PNG.
//!
//! Building a plot cannot fail. Rendering and export return
//! [`GraphError`](crate::error::GraphError); no `SurfaceError` comes out of
//! the plot path.
//!
//! # Example Usage
//!
//! ```rust,no_run
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! # use std::fs;
//! use std::path::PathBuf;
//! use rust_decimal_macros::dec;
//! use optionstratlib_math::geometrics::GeometricObject;
//! use optionstratlib_visualization::visualization::Plottable;
//! use optionstratlib_math::surfaces::{Point3D, Surface};
//!
//! // A complete 2 x 2 grid: the PNG export refuses a grid with missing
//! // or repeated points instead of filling them in.
//! let p1 = Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0));
//! let p2 = Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0));
//! let p3 = Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0));
//! let p4 = Point3D::new(dec!(1.0), dec!(1.0), dec!(4.0));
//! let surface = Surface::from_vector(vec![&p1, &p2, &p3, &p4]);
//!
//! #[cfg(feature = "static_export")]
//! {
//!     let dst = PathBuf::from("surface_plot.png");
//!     // Plot the surface and save to a file
//!     surface.plot()
//!         .title("My Surface Plot")
//!         .x_label("X Axis")
//!         .y_label("Y Axis")
//!         .z_label("Z Axis") // Example of z-axis label
//!         .save(dst.clone())?;
//!     if dst.exists() {
//!         fs::remove_file(dst)?;
//!     }
//! }
//! # Ok(())
//! # }
//! ```
//!
use crate::visualization::{Graph, GraphData, PlotBuilder, Plottable, Surface3D};
use optionstratlib_math::surfaces::Surface;

/// `Graph` adapter for a [`Surface`]; lives in `visualization` because the
/// trait is visualization-owned and `Surface` is a math container
/// (ADR-0001 D2, M1-05).
impl Graph for Surface {
    fn graph_data(&self) -> GraphData {
        GraphData::GraphSurface(Surface3D {
            x: self.points.iter().map(|p| p.x).collect(),
            y: self.points.iter().map(|p| p.y).collect(),
            z: self.points.iter().map(|p| p.z).collect(),
            name: "Surface".to_string(),
        })
    }
}

/// Plottable implementation for single Surface
impl Plottable for Surface {
    fn plot(&self) -> PlotBuilder<Self>
    where
        Self: Sized,
    {
        PlotBuilder {
            data: self.clone(),
            options: self.graph_config(),
        }
    }
}

#[cfg(test)]
mod tests_extended {
    use super::*;
    use crate::visualization::{GraphConfig, GraphData, Series2D, TraceMode};
    use optionstratlib_math::surfaces::Point3D;
    use rust_decimal_macros::dec;
    use std::any::{Any, TypeId};
    use std::collections::BTreeSet;

    #[derive(Clone)]
    struct MockChart {
        pub x_desc: String,
        pub y_desc: String,
    }

    impl MockChart {
        pub fn new() -> Self {
            MockChart {
                x_desc: String::new(),
                y_desc: String::new(),
            }
        }

        pub fn configure_mesh(&mut self) -> &mut Self {
            self
        }

        pub fn x_label_formatter(&mut self, _formatter: &dyn Fn(f64) -> String) -> &mut Self {
            self
        }

        pub fn y_label_formatter(&mut self, _formatter: &dyn Fn(f64) -> String) -> &mut Self {
            self
        }

        pub fn x_desc(&mut self, desc: &str) -> &mut Self {
            self.x_desc = desc.to_string();
            self
        }

        pub fn y_desc(&mut self, desc: &str) -> &mut Self {
            self.y_desc = desc.to_string();
            self
        }
    }

    impl Graph for MockChart {
        fn graph_data(&self) -> GraphData {
            GraphData::Series(Series2D {
                x: vec![dec!(1.0), dec!(2.0)],
                y: vec![dec!(3.0), dec!(4.0)],
                name: "Test Series".to_string(),
                mode: TraceMode::Lines,
                line_color: None,
                line_width: None,
            })
        }

        fn graph_config(&self) -> GraphConfig {
            GraphConfig::default()
        }
    }

    impl Plottable for MockChart {
        fn plot(&self) -> PlotBuilder<Self>
        where
            Self: Sized + Graph,
        {
            PlotBuilder {
                data: self.clone(),
                options: self.graph_config(),
            }
        }
    }

    #[test]
    fn test_configure_chart_mesh() {
        let mut chart = MockChart::new();
        chart
            .configure_mesh()
            .x_label_formatter(&|v| format!("{v:.2}"))
            .y_label_formatter(&|v| format!("{v:.2}"))
            .x_desc("X-axis")
            .y_desc("Y-axis");
        assert_eq!(chart.x_desc, "X-axis");
        assert_eq!(chart.y_desc, "Y-axis");
    }

    #[test]
    fn test_plot() {
        let mut chart = MockChart::new();
        chart
            .configure_mesh()
            .x_label_formatter(&|v| format!("{v:.2}"))
            .y_label_formatter(&|v| format!("{v:.2}"))
            .x_desc("X-axis")
            .y_desc("Y-axis");
        assert_eq!(chart.x_desc, "X-axis");
        assert_eq!(chart.y_desc, "Y-axis");

        let plot = chart.plot();
        assert_eq!(plot.data.graph_data(), chart.graph_data());
        assert_eq!(plot.options, chart.graph_config());
        assert_eq!(plot.data.graph_config(), chart.graph_config());
        assert_eq!(plot.options.width, 1280);
        assert_eq!(plot.options.height, 720);
        assert_eq!(plot.options.title, "Graph");
        assert_eq!(plot.options.x_label, None);
        assert_eq!(plot.options.y_label, None);
        assert_eq!(plot.options.z_label, None);
        assert_eq!(plot.data.x_desc, "X-axis");
        assert_eq!(plot.data.y_desc, "Y-axis");
    }
    #[test]
    fn test_plot_surface() {
        let points = BTreeSet::from_iter(vec![
            Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
            Point3D::new(dec!(0.5), dec!(0.0), dec!(1.0)),
            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
            Point3D::new(dec!(0.0), dec!(0.5), dec!(1.0)),
            Point3D::new(dec!(0.5), dec!(0.5), dec!(1.0)),
            Point3D::new(dec!(1.0), dec!(0.5), dec!(1.0)),
            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
            Point3D::new(dec!(0.5), dec!(1.0), dec!(1.0)),
            Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
        ]);
        let surface = Surface::new(points);
        let plot = surface.plot();
        // Check if the plot is type PlotBuilder<Surface>
        assert_eq!(plot.type_id(), TypeId::of::<PlotBuilder<Surface>>());
        assert_eq!(plot.data.graph_data(), surface.graph_data());
        assert_eq!(plot.options, surface.graph_config());
        assert_eq!(plot.data.graph_config(), surface.graph_config());
        assert_eq!(plot.options.width, 1280);
        assert_eq!(plot.options.height, 720);
        assert_eq!(plot.options.title, "Graph");
        assert_eq!(plot.options.x_label, None);
        assert_eq!(plot.options.y_label, None);
        assert_eq!(plot.options.z_label, None);
    }

    /// Relocated from `surfaces::surface` with the `Graph` impl: a surface
    /// projects to a `GraphSurface` payload.
    #[test]
    fn test_surface_graph_data_is_graph_surface() {
        let points = BTreeSet::from_iter(vec![
            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
            Point3D::new(dec!(0.5), dec!(0.5), dec!(1.5)),
        ]);
        let surface = Surface::new(points);
        let graph_data = surface.graph_data();
        assert!(matches!(
            graph_data,
            GraphData::GraphSurface(Surface3D { .. })
        ));
    }
}