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}