Skip to main content

optionstratlib_visualization/visualization/
surfaces.rs

1//! # Surface Visualization Module
2//!
3//! `Graph` and `Plottable` for a [`Surface`]: the surface charts as one
4//! [`Surface3D`] holding the `x`, `y` and `z` coordinates of its points, in
5//! point order.
6//!
7//! [`PlotBuilder`] configures the chart (title, axis labels, line style,
8//! colours, legend). With `plotly` it renders to HTML; with `static_export`,
9//! `PlotBuilder::save` writes a PNG.
10//!
11//! Building a plot cannot fail. Rendering and export return
12//! [`GraphError`](crate::error::GraphError); no `SurfaceError` comes out of
13//! the plot path.
14//!
15//! # Example Usage
16//!
17//! ```rust,no_run
18//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
19//! # use std::fs;
20//! use std::path::PathBuf;
21//! use rust_decimal_macros::dec;
22//! use optionstratlib_math::geometrics::GeometricObject;
23//! use optionstratlib_visualization::visualization::Plottable;
24//! use optionstratlib_math::surfaces::{Point3D, Surface};
25//!
26//! // A complete 2 x 2 grid: the PNG export refuses a grid with missing
27//! // or repeated points instead of filling them in.
28//! let p1 = Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0));
29//! let p2 = Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0));
30//! let p3 = Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0));
31//! let p4 = Point3D::new(dec!(1.0), dec!(1.0), dec!(4.0));
32//! let surface = Surface::from_vector(vec![&p1, &p2, &p3, &p4]);
33//!
34//! #[cfg(feature = "static_export")]
35//! {
36//!     let dst = PathBuf::from("surface_plot.png");
37//!     // Plot the surface and save to a file
38//!     surface.plot()
39//!         .title("My Surface Plot")
40//!         .x_label("X Axis")
41//!         .y_label("Y Axis")
42//!         .z_label("Z Axis") // Example of z-axis label
43//!         .save(dst.clone())?;
44//!     if dst.exists() {
45//!         fs::remove_file(dst)?;
46//!     }
47//! }
48//! # Ok(())
49//! # }
50//! ```
51//!
52use crate::visualization::{Graph, GraphData, PlotBuilder, Plottable, Surface3D};
53use optionstratlib_math::surfaces::Surface;
54
55/// `Graph` adapter for a [`Surface`]; lives in `visualization` because the
56/// trait is visualization-owned and `Surface` is a math container
57/// (ADR-0001 D2, M1-05).
58impl Graph for Surface {
59    fn graph_data(&self) -> GraphData {
60        GraphData::GraphSurface(Surface3D {
61            x: self.points.iter().map(|p| p.x).collect(),
62            y: self.points.iter().map(|p| p.y).collect(),
63            z: self.points.iter().map(|p| p.z).collect(),
64            name: "Surface".to_string(),
65        })
66    }
67}
68
69/// Plottable implementation for single Surface
70impl Plottable for Surface {
71    fn plot(&self) -> PlotBuilder<Self>
72    where
73        Self: Sized,
74    {
75        PlotBuilder {
76            data: self.clone(),
77            options: self.graph_config(),
78        }
79    }
80}
81
82#[cfg(test)]
83mod tests_extended {
84    use super::*;
85    use crate::visualization::{GraphConfig, GraphData, Series2D, TraceMode};
86    use optionstratlib_math::surfaces::Point3D;
87    use rust_decimal_macros::dec;
88    use std::any::{Any, TypeId};
89    use std::collections::BTreeSet;
90
91    #[derive(Clone)]
92    struct MockChart {
93        pub x_desc: String,
94        pub y_desc: String,
95    }
96
97    impl MockChart {
98        pub fn new() -> Self {
99            MockChart {
100                x_desc: String::new(),
101                y_desc: String::new(),
102            }
103        }
104
105        pub fn configure_mesh(&mut self) -> &mut Self {
106            self
107        }
108
109        pub fn x_label_formatter(&mut self, _formatter: &dyn Fn(f64) -> String) -> &mut Self {
110            self
111        }
112
113        pub fn y_label_formatter(&mut self, _formatter: &dyn Fn(f64) -> String) -> &mut Self {
114            self
115        }
116
117        pub fn x_desc(&mut self, desc: &str) -> &mut Self {
118            self.x_desc = desc.to_string();
119            self
120        }
121
122        pub fn y_desc(&mut self, desc: &str) -> &mut Self {
123            self.y_desc = desc.to_string();
124            self
125        }
126    }
127
128    impl Graph for MockChart {
129        fn graph_data(&self) -> GraphData {
130            GraphData::Series(Series2D {
131                x: vec![dec!(1.0), dec!(2.0)],
132                y: vec![dec!(3.0), dec!(4.0)],
133                name: "Test Series".to_string(),
134                mode: TraceMode::Lines,
135                line_color: None,
136                line_width: None,
137            })
138        }
139
140        fn graph_config(&self) -> GraphConfig {
141            GraphConfig::default()
142        }
143    }
144
145    impl Plottable for MockChart {
146        fn plot(&self) -> PlotBuilder<Self>
147        where
148            Self: Sized + Graph,
149        {
150            PlotBuilder {
151                data: self.clone(),
152                options: self.graph_config(),
153            }
154        }
155    }
156
157    #[test]
158    fn test_configure_chart_mesh() {
159        let mut chart = MockChart::new();
160        chart
161            .configure_mesh()
162            .x_label_formatter(&|v| format!("{v:.2}"))
163            .y_label_formatter(&|v| format!("{v:.2}"))
164            .x_desc("X-axis")
165            .y_desc("Y-axis");
166        assert_eq!(chart.x_desc, "X-axis");
167        assert_eq!(chart.y_desc, "Y-axis");
168    }
169
170    #[test]
171    fn test_plot() {
172        let mut chart = MockChart::new();
173        chart
174            .configure_mesh()
175            .x_label_formatter(&|v| format!("{v:.2}"))
176            .y_label_formatter(&|v| format!("{v:.2}"))
177            .x_desc("X-axis")
178            .y_desc("Y-axis");
179        assert_eq!(chart.x_desc, "X-axis");
180        assert_eq!(chart.y_desc, "Y-axis");
181
182        let plot = chart.plot();
183        assert_eq!(plot.data.graph_data(), chart.graph_data());
184        assert_eq!(plot.options, chart.graph_config());
185        assert_eq!(plot.data.graph_config(), chart.graph_config());
186        assert_eq!(plot.options.width, 1280);
187        assert_eq!(plot.options.height, 720);
188        assert_eq!(plot.options.title, "Graph");
189        assert_eq!(plot.options.x_label, None);
190        assert_eq!(plot.options.y_label, None);
191        assert_eq!(plot.options.z_label, None);
192        assert_eq!(plot.data.x_desc, "X-axis");
193        assert_eq!(plot.data.y_desc, "Y-axis");
194    }
195    #[test]
196    fn test_plot_surface() {
197        let points = BTreeSet::from_iter(vec![
198            Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
199            Point3D::new(dec!(0.5), dec!(0.0), dec!(1.0)),
200            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
201            Point3D::new(dec!(0.0), dec!(0.5), dec!(1.0)),
202            Point3D::new(dec!(0.5), dec!(0.5), dec!(1.0)),
203            Point3D::new(dec!(1.0), dec!(0.5), dec!(1.0)),
204            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
205            Point3D::new(dec!(0.5), dec!(1.0), dec!(1.0)),
206            Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
207        ]);
208        let surface = Surface::new(points);
209        let plot = surface.plot();
210        // Check if the plot is type PlotBuilder<Surface>
211        assert_eq!(plot.type_id(), TypeId::of::<PlotBuilder<Surface>>());
212        assert_eq!(plot.data.graph_data(), surface.graph_data());
213        assert_eq!(plot.options, surface.graph_config());
214        assert_eq!(plot.data.graph_config(), surface.graph_config());
215        assert_eq!(plot.options.width, 1280);
216        assert_eq!(plot.options.height, 720);
217        assert_eq!(plot.options.title, "Graph");
218        assert_eq!(plot.options.x_label, None);
219        assert_eq!(plot.options.y_label, None);
220        assert_eq!(plot.options.z_label, None);
221    }
222
223    /// Relocated from `surfaces::surface` with the `Graph` impl: a surface
224    /// projects to a `GraphSurface` payload.
225    #[test]
226    fn test_surface_graph_data_is_graph_surface() {
227        let points = BTreeSet::from_iter(vec![
228            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
229            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
230            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
231            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
232            Point3D::new(dec!(0.5), dec!(0.5), dec!(1.5)),
233        ]);
234        let surface = Surface::new(points);
235        let graph_data = surface.graph_data();
236        assert!(matches!(
237            graph_data,
238            GraphData::GraphSurface(Surface3D { .. })
239        ));
240    }
241}