Skip to main content

optionstratlib_visualization/visualization/
curves.rs

1//! # Curve Visualization Module
2//!
3//! Provides a flexible plotting trait for mathematical curves and collections of curves.
4//!
5//! ## Features
6//! - `Graph` and `Plottable` for a single [`Curve`] and for a `Vec<Curve>`
7//!   drawn on one chart
8//! - Customizable plot configuration through [`PlotBuilder`]
9//! - HTML output with `plotly`, PNG and SVG with `static_export`
10//!
11//! Building a plot cannot fail. Rendering and export return
12//! [`GraphError`](crate::error::GraphError); no `CurveError` comes out of
13//! the plot path.
14//!
15//! ## Usage Examples
16//! ```rust,no_run
17//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
18//! // Plot a single curve
19//! use std::fs;
20//! use std::path::{Path, PathBuf};
21//! use rust_decimal::Decimal;
22//! use optionstratlib_math::curves::{Curve, Point2D};
23//! use optionstratlib_math::geometrics::GeometricObject;
24//! use optionstratlib_visualization::visualization::Plottable;
25//!
26//! let curve = Curve::from_vector(vec![
27//!             Point2D::new(Decimal::ZERO, Decimal::ZERO), // p11
28//!             Point2D::new(Decimal::ONE, Decimal::ONE),   // p12
29//!             Point2D::new(Decimal::ZERO, Decimal::ONE),  // p21
30//!             Point2D::new(Decimal::ONE, Decimal::TWO),   // p22
31//!         ]);
32//! #[cfg(feature = "static_export")]
33//! {
34//!     let filename = PathBuf::from("single_curve.png");
35//!     curve.plot()
36//!         .title("Single Curve")
37//!         .save(filename.clone())?;
38//!     if filename.exists() {
39//!        fs::remove_file(&filename)?;
40//!     }
41//!     // Plot multiple curves
42//!     let curve1 = curve.clone();
43//!     let curve2 = curve.clone();
44//!     let curve3 = curve.clone();
45//!     let curves = vec![curve1, curve2, curve3];
46//!     let filename = "multiple_curves.png";
47//!     curves.plot()
48//!         .title("Curve Comparison")
49//!         .save(filename)?;
50//!
51//!     if Path::new(filename).exists() {
52//!        fs::remove_file(&filename)?;
53//!     }
54//! }
55//! # Ok(())
56//! # }
57//! ```
58
59use crate::visualization::{Graph, GraphData, PlotBuilder, Plottable};
60use optionstratlib_math::curves::Curve;
61
62/// `Graph` adapter for a single [`Curve`]; lives in `visualization` because
63/// the trait is visualization-owned and `Curve` is a math container
64/// (ADR-0001 D2, M1-05).
65impl Graph for Curve {
66    fn graph_data(&self) -> GraphData {
67        self.clone().into()
68    }
69}
70
71/// `Graph` adapter for a family of curves drawn on one chart.
72impl Graph for Vec<Curve> {
73    fn graph_data(&self) -> GraphData {
74        self.clone().into()
75    }
76}
77
78/// Plottable implementation for single Curve
79impl Plottable for Curve {
80    fn plot(&self) -> PlotBuilder<Self>
81    where
82        Self: Sized,
83    {
84        PlotBuilder {
85            data: self.clone(),
86            options: self.graph_config(),
87        }
88    }
89}
90
91/// Implementation of the `Plottable` trait for `Vec<Curve>`.
92///
93/// This implementation enables a vector of `Curve` instances to be plotted
94/// using the `plot` method. The method creates a `PlotBuilder` instance, which
95/// allows for flexible and configurable visualization of the curves.
96///
97/// # Overview
98/// By implementing the `Plottable` trait, a vector of `Curve` objects gains the
99/// ability to leverage plot-building functionality. The `plot` method clones the
100/// data (to ensure immutability of the original input) and pairs it with
101/// default plotting options (`PlotOptions`) for further configuration and
102/// rendering.
103///
104/// The `PlotBuilder` struct, which is returned by this implementation, acts as a
105/// pipeline for customizing and generating the final plot. Once the plot is fully
106/// configured in terms of styling and layout, it can be saved to a file, rendered
107/// in memory, or manipulated further depending on the builder's available methods.
108///
109/// # Method Details
110/// - **`plot`**:
111///   - Creates a `PlotBuilder` instance containing the data from the `Vec<Curve>`
112///     and populates it with default plot options.
113///   - Returns a configurable tool for building curve visualizations.
114///
115/// # Considerations
116/// - This implementation assumes that it is appropriate to clone the data from
117///   the vector of `Curve` instances. If the cloning behavior is expensive or not
118///   necessary, further optimization may be required.
119/// - `PlotOptions` default values provide a reasonable starting point, but
120///   most real-world applications will override some of these values for more
121///   customization.
122///
123/// # Example Behavior
124/// A vector of `Curve` objects can be passed to the `plot` method to generate
125/// a plot tailored to the desired styling and configuration. Methods available
126/// on `PlotBuilder` can then be chained to adjust plot dimensions,
127/// colors, titles, labels, and more.
128///
129/// # Returns
130/// - A `PlotBuilder` instance configured with the cloned curve data (`self.clone()`)
131///   and fully initialized with default `PlotOptions`.
132///
133/// # Default Settings
134/// - The default `PlotOptions`, as used in this implementation, include:
135///   - White background
136///   - Line width of 2 pixels
137///   - Default dimensions (800x600 pixels)
138///   - No title or axis labels
139///   - No default line colors
140///
141/// # Errors
142/// - While creating a `PlotBuilder` instance does not directly raise errors, subsequent
143///   operations (e.g., saving a plot or generating a view) may encounter runtime issues
144///   related to file I/O, data validity, or plot rendering.
145///
146/// # See Also
147/// - [`Plottable`]: The trait allowing
148///   generalized plotting functionality.
149/// - [`PlotBuilder`]: The plot generation
150///   and configuration builder.
151///
152/// # Modules
153/// This implementation works in conjunction with the `Curve` struct,
154/// `PlotBuilder`, and `PlotOptions`. These modules provide the functionality
155/// required to create, configure, and render curve plots.
156impl Plottable for Vec<Curve> {
157    fn plot(&self) -> PlotBuilder<Self>
158    where
159        Self: Sized,
160    {
161        PlotBuilder {
162            data: self.clone(),
163            options: self.graph_config(),
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use optionstratlib_math::curves::Point2D;
172    use optionstratlib_math::geometrics::GeometricObject;
173    use rust_decimal_macros::dec;
174    #[cfg(feature = "plotly")]
175    use {std::fs, std::path::Path, tracing::error};
176
177    #[cfg(feature = "plotly")]
178    fn cleanup_image(filename: &Path) {
179        match fs::remove_file(filename) {
180            Ok(_) => {}
181            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
182                // File already gone - that's exactly what we wanted
183            }
184            Err(e) => {
185                error!("Failed to remove {}: {}", filename.display(), e);
186            }
187        }
188    }
189
190    fn create_test_curves() -> (Curve, Curve, Curve) {
191        // Create points first so they live long enough
192        let p1_1 = Point2D::new(dec!(0.0), dec!(0.0));
193        let p1_2 = Point2D::new(dec!(1.0), dec!(1.0));
194        let p1_3 = Point2D::new(dec!(2.0), dec!(4.0));
195
196        let p2_1 = Point2D::new(dec!(0.0), dec!(1.0));
197        let p2_2 = Point2D::new(dec!(1.0), dec!(2.0));
198        let p2_3 = Point2D::new(dec!(2.0), dec!(5.0));
199
200        let p3_1 = Point2D::new(dec!(0.0), dec!(2.0));
201        let p3_2 = Point2D::new(dec!(1.0), dec!(3.0));
202        let p3_3 = Point2D::new(dec!(2.0), dec!(6.0));
203
204        let points1 = vec![&p1_1, &p1_2, &p1_3];
205        let points2 = vec![&p2_1, &p2_2, &p2_3];
206        let points3 = vec![&p3_1, &p3_2, &p3_3];
207
208        (
209            Curve::from_vector(points1),
210            Curve::from_vector(points2),
211            Curve::from_vector(points3),
212        )
213    }
214
215    #[test]
216    fn test_single_curve_plot_bis() {
217        let p1 = Point2D::new(dec!(0.0), dec!(0.0));
218        let p2 = Point2D::new(dec!(1.0), dec!(1.0));
219        let p3 = Point2D::new(dec!(2.0), dec!(4.0));
220        let points = vec![&p1, &p2, &p3];
221        let curve = Curve::from_vector(points);
222
223        // Plot single curve
224        curve
225            .plot()
226            .title("Test Curve")
227            .x_label("X Axis")
228            .y_label("Y Axis")
229            .dimensions(800, 600);
230
231        #[cfg(feature = "plotly")]
232        {
233            let file_path_html = "single_curve_bis_test.html".as_ref();
234
235            curve.write_html(file_path_html).unwrap();
236            cleanup_image(file_path_html);
237        }
238    }
239
240    #[test]
241    fn test_multiple_curves_plot() {
242        let p1_1 = Point2D::new(dec!(0.0), dec!(0.0));
243        let p1_2 = Point2D::new(dec!(1.0), dec!(1.0));
244        let p1_3 = Point2D::new(dec!(2.0), dec!(4.0));
245
246        let p2_1 = Point2D::new(dec!(0.0), dec!(1.0));
247        let p2_2 = Point2D::new(dec!(1.0), dec!(2.0));
248        let p2_3 = Point2D::new(dec!(2.0), dec!(5.0));
249
250        let points1 = vec![&p1_1, &p1_2, &p1_3];
251        let points2 = vec![&p2_1, &p2_2, &p2_3];
252
253        let curve1 = Curve::from_vector(points1);
254        let curve2 = Curve::from_vector(points2);
255
256        let curve_vector = vec![curve1.clone(), curve2.clone()];
257
258        // Plot multiple curves
259        curve_vector
260            .plot()
261            .title("Multiple Curves")
262            .x_label("X Axis")
263            .y_label("Y Axis")
264            .dimensions(800, 600);
265
266        #[cfg(feature = "plotly")]
267        {
268            let file_path_html = "multiple_curves_test.html".as_ref();
269            curve_vector.write_html(file_path_html).unwrap();
270            cleanup_image(file_path_html);
271        }
272    }
273
274    #[test]
275    fn test_single_curve_plot() {
276        let p1_1 = Point2D::new(dec!(0.0), dec!(0.0));
277        let p1_2 = Point2D::new(dec!(1.0), dec!(1.0));
278        let p1_3 = Point2D::new(dec!(2.0), dec!(4.0));
279
280        let points = vec![&p1_1, &p1_2, &p1_3];
281        let curve = Curve::from_vector(points);
282
283        // Plot single curve
284        curve
285            .plot()
286            .title("Test Curve")
287            .x_label("X Axis")
288            .y_label("Y Axis")
289            .dimensions(800, 600);
290
291        #[cfg(feature = "plotly")]
292        {
293            let file_path_html = "single_curve_test.html".as_ref();
294            curve.write_html(file_path_html).unwrap();
295            cleanup_image(file_path_html);
296        }
297    }
298
299    #[test]
300    fn test_multiple_curves_plot_bis() {
301        let (curve1, curve2, curve3) = create_test_curves();
302        let curve_vector = vec![curve1.clone(), curve2.clone(), curve3.clone()];
303
304        // Plot multiple curves
305        curve_vector
306            .plot()
307            .title("Multiple Curves")
308            .x_label("X Axis")
309            .y_label("Y Axis")
310            .dimensions(1000, 700);
311
312        #[cfg(feature = "plotly")]
313        {
314            let file_path_html = "multiple_curves_bis_test.html".as_ref();
315            curve_vector.write_html(file_path_html).unwrap();
316            cleanup_image(file_path_html);
317        }
318    }
319
320    #[test]
321    fn test_plot_with_custom_line_width() {
322        let (curve1, curve2, _) = create_test_curves();
323        let curve_vector = vec![curve1.clone(), curve2.clone()];
324
325        // Custom line width
326        curve_vector
327            .plot()
328            .title("Thick Line Curves")
329            .dimensions(800, 600);
330
331        #[cfg(feature = "plotly")]
332        {
333            let file_path_html = "thick_line_curves_test.html".as_ref();
334            curve_vector.write_html(file_path_html).unwrap();
335            cleanup_image(file_path_html);
336        }
337    }
338}
339
340#[cfg(test)]
341mod tests_extended {
342    struct MockChart {
343        pub x_desc: String,
344        pub y_desc: String,
345    }
346
347    impl MockChart {
348        pub fn new() -> Self {
349            MockChart {
350                x_desc: String::new(),
351                y_desc: String::new(),
352            }
353        }
354
355        pub fn configure_mesh(&mut self) -> &mut Self {
356            self
357        }
358
359        pub fn x_label_formatter(&mut self, _formatter: &dyn Fn(f64) -> String) -> &mut Self {
360            self
361        }
362
363        pub fn y_label_formatter(&mut self, _formatter: &dyn Fn(f64) -> String) -> &mut Self {
364            self
365        }
366
367        pub fn x_desc(&mut self, desc: &str) -> &mut Self {
368            self.x_desc = desc.to_string();
369            self
370        }
371
372        pub fn y_desc(&mut self, desc: &str) -> &mut Self {
373            self.y_desc = desc.to_string();
374            self
375        }
376    }
377
378    #[test]
379    fn test_configure_chart_mesh() {
380        let mut chart = MockChart::new(); // Simular un gráfico
381        chart
382            .configure_mesh()
383            .x_label_formatter(&|v| format!("{v:.2}"))
384            .y_label_formatter(&|v| format!("{v:.2}"))
385            .x_desc("X-axis")
386            .y_desc("Y-axis");
387        assert_eq!(chart.x_desc, "X-axis");
388        assert_eq!(chart.y_desc, "Y-axis");
389    }
390}