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