Skip to main content

optionstratlib_visualization/visualization/
plot_builder.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 21/1/25
5******************************************************************************/
6//! `Plottable` and `PlotBuilder`: the fluent plotting entry point for any
7//! `Graph` implementor. Visualization-owned (ADR-0001 D2, M1-05).
8
9use crate::visualization::{ColorScheme, Graph, GraphConfig, GraphData, LineStyle};
10#[cfg(feature = "static_export")]
11use {crate::error::GraphError, std::path::Path};
12
13/// Trait for defining objects that can be visualized as plots.
14///
15/// The `Plottable` trait provides a standardized interface for types that can be
16/// represented graphically. It enables visualization of data structures through
17/// a fluent builder pattern, allowing for customizable plot creation.
18///
19/// Implementers of this trait can be visualized using the plotting system with
20/// configurable options for appearance, labels, colors, and other visual attributes.
21///
22/// Building a plot cannot fail; rendering and export are the fallible steps,
23/// and they return [`GraphError`](crate::error::GraphError) whatever the
24/// plotted type, so the trait carries no error type of its own.
25pub trait Plottable {
26    /// Creates a plot builder for configuring and generating visualizations.
27    ///
28    /// Returns a `PlotBuilder` instance that provides a fluent interface for
29    /// customizing plot appearance and behavior before rendering.
30    fn plot(&self) -> PlotBuilder<Self>
31    where
32        Self: Sized + Graph;
33}
34
35/// A builder for creating and configuring data visualizations.
36///
37/// `PlotBuilder` provides a fluent interface for customizing plots with various
38/// styling and labeling options. It works with any type that implements the
39/// `Plottable` trait, allowing for consistent visualization capabilities across
40/// different data structures.
41///
42/// This builder is typically created via the `plot()` method on types that implement
43/// the `Plottable` trait. After configuring the plot with the desired options,
44/// it can be rendered and saved using the methods from `PlotBuilderExt`.
45///
46pub struct PlotBuilder<T: Plottable + Graph> {
47    /// The data to be visualized in the plot.
48    ///
49    /// This field holds the instance of a type implementing the `Plottable` trait
50    /// which contains the actual data points to be represented in the visualization.
51    pub(crate) data: T,
52
53    /// Configuration settings that control the appearance and behavior of the plot.
54    ///
55    /// This includes visual styling like colors, dimensions, and line widths,
56    /// as well as textual elements like titles and axis labels.
57    /// See `PlotOptions` for the complete set of available configuration options.
58    pub(crate) options: GraphConfig,
59}
60
61impl<T: Plottable + Graph> PlotBuilder<T> {
62    /// Sets the title of the plot.
63    ///
64    /// This method configures the main title that appears at the top of the visualization.
65    ///
66    /// # Parameters
67    /// * `title` - The text to display as the plot title
68    ///
69    /// # Returns
70    /// The `PlotBuilder` instance with the updated title setting
71    ///
72    pub fn title(mut self, title: impl Into<String>) -> Self {
73        self.options.title = title.into();
74        if self.options.legend.is_none() {
75            self.options.legend = Some(vec![self.options.title.clone()]);
76        }
77        self
78    }
79
80    /// Sets the label for the x-axis.
81    ///
82    /// This method configures the descriptive text displayed along the horizontal axis.
83    ///
84    /// # Parameters
85    /// * `label` - The text to display as the x-axis label
86    ///
87    /// # Returns
88    /// The `PlotBuilder` instance with the updated x-axis label
89    ///
90    pub fn x_label(mut self, label: impl Into<String>) -> Self {
91        self.options.x_label = Some(label.into());
92        self
93    }
94
95    /// Sets the label for the y-axis.
96    ///
97    /// This method configures the descriptive text displayed along the vertical axis.
98    ///
99    /// # Parameters
100    /// * `label` - The text to display as the y-axis label
101    ///
102    /// # Returns
103    /// The `PlotBuilder` instance with the updated y-axis label
104    ///
105    pub fn y_label(mut self, label: impl Into<String>) -> Self {
106        self.options.y_label = Some(label.into());
107        self
108    }
109
110    /// Sets the label for the z-axis.
111    ///
112    /// This method configures the descriptive text displayed along the z-axis
113    /// in three-dimensional plots.
114    ///
115    /// # Parameters
116    /// * `label` - The text to display as the z-axis label
117    ///
118    /// # Returns
119    /// The `PlotBuilder` instance with the updated z-axis label
120    ///
121    pub fn z_label(mut self, label: impl Into<String>) -> Self {
122        self.options.z_label = Some(label.into());
123        self
124    }
125
126    /// Sets the colors for data series lines.
127    ///
128    /// This method configures the colors used to render each data series or curve
129    /// in the visualization.
130    ///
131    /// # Parameters
132    /// * `colors` - A vector of RGB colors to use for the plot lines
133    ///
134    /// # Returns
135    /// The `PlotBuilder` instance with the updated line colors
136    ///
137    pub fn line_style(mut self, line_style: LineStyle) -> Self {
138        self.options.line_style = line_style;
139        self
140    }
141
142    /// Sets the legend for the current instance.
143    ///
144    /// This method accepts a vector of items that can be converted into strings and sets the `legend`
145    /// field of the current instance's options. It returns the modified instance for further chaining.
146    ///
147    /// # Arguments
148    ///
149    /// * `legend` - A `Vec` containing items that implement the `Into<String>` trait. Each element
150    ///   will be converted into a `String` and assigned to the legend.
151    ///
152    /// # Returns
153    ///
154    /// Returns the modified instance with the legend updated.
155    ///
156    /// In this example, the legend is updated to include "Item 1", "Item 2", and "Item 3".
157    ///
158    pub fn legend(mut self, legend: Vec<impl Into<String>>) -> Self {
159        let legend: Vec<String> = legend.into_iter().map(|l| l.into()).collect();
160        self.options.legend = Some(legend);
161        self
162    }
163
164    /// Adds a legend entry to the existing legend configuration or initializes a new legend
165    /// with the provided entry if none exists.
166    ///
167    /// This method allows chaining, modifying the `legend` configuration within the `options`
168    /// of the current object. If a `legend` already exists, the provided legend entry will
169    /// be appended to it. If no `legend` exists, a new legend will be created containing the
170    /// provided entry.
171    ///
172    /// # Arguments
173    ///
174    /// * `legend` - An item implementing `Into<String>` that represents the legend entry
175    ///   to add.
176    ///
177    /// # Returns
178    ///
179    /// Returns `Self` (the modified object) to allow method chaining.
180    ///
181    /// In this example, two legend entries, `"Legend 1"` and `"Legend 2"`, are added to the chart's
182    /// legend configuration.
183    pub fn add_legend(mut self, legend: impl Into<String>) -> Self {
184        if let Some(ref mut legends) = self.options.legend {
185            legends.push(legend.into());
186        } else {
187            self.options.legend = Some(vec![legend.into()]);
188        }
189        self
190    }
191
192    /// Sets the overall dimensions of the plot.
193    ///
194    /// This method configures the width and height of the generated plot image.
195    ///
196    /// # Parameters
197    /// * `width` - The width of the plot in pixels
198    /// * `height` - The height of the plot in pixels
199    ///
200    /// # Returns
201    /// The `PlotBuilder` instance with the updated dimensions
202    ///
203    pub fn dimensions(mut self, width: u32, height: u32) -> Self {
204        self.options.width = width;
205        self.options.height = height;
206        self
207    }
208
209    /// Sets the color scheme for the current instance.
210    ///
211    /// This method allows you to specify a `ColorScheme` to customize the appearance or theme
212    /// of the associated object. The method updates the `color_scheme` field in the `options`
213    /// struct with the provided value and returns an updated instance of `self`.
214    ///
215    /// # Parameters
216    /// - `color_scheme`: The desired `ColorScheme` to be applied. This defines the visual style
217    ///   or theme to be used by the object.
218    ///
219    /// # Returns
220    /// An updated instance of `Self` with the new color scheme applied.
221    ///
222    pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
223        self.options.color_scheme = color_scheme;
224        self
225    }
226
227    /// Sets the visibility of the legend in the chart or visualization.
228    ///
229    /// # Parameters
230    /// - `show_legend`: A boolean specifying whether the legend should be displayed.
231    ///   - `true`: The legend will be displayed.
232    ///   - `false`: The legend will be hidden.
233    ///
234    /// # Returns
235    /// - Returns an updated instance of `Self` with the `show_legend` option set
236    ///   according to the provided parameter.
237    ///
238    pub fn show_legend(mut self, show_legend: bool) -> Self {
239        self.options.show_legend = show_legend;
240        self
241    }
242
243    /// Saves the configured plot to a file.
244    ///
245    /// This method renders the plot with all configured options and writes
246    /// the result to the specified file path.
247    ///
248    /// # Parameters
249    /// * `path` - The file path where the plot should be saved
250    ///
251    /// # Returns
252    /// A `Result` indicating success or containing an error if the save operation failed
253    ///
254    /// # Errors
255    /// Whatever [`Graph::write_png`] returns, unchanged: the typed variant
256    /// for data it cannot draw, [`GraphError::Io`] when `path` cannot be
257    /// written, [`GraphError::Render`] when drawing fails.
258    ///
259    #[cfg(feature = "static_export")]
260    pub fn save(self, path: impl AsRef<Path>) -> Result<(), GraphError> {
261        let path = path.as_ref();
262        self.write_png(path).inspect_err(|error| {
263            tracing::debug!(path = %path.display(), %error, "failed to save plot");
264        })
265    }
266}
267
268impl<T: Plottable + Graph> Graph for PlotBuilder<T> {
269    fn graph_data(&self) -> GraphData {
270        self.data.graph_data()
271    }
272
273    fn graph_config(&self) -> GraphConfig {
274        GraphConfig {
275            title: self.options.title.clone(),
276            width: 1600,
277            height: 900,
278            x_label: self.options.x_label.clone(),
279            y_label: self.options.y_label.clone(),
280            z_label: self.options.z_label.clone(),
281            line_style: self.options.line_style,
282            color_scheme: self.options.color_scheme.clone(),
283            legend: self.options.legend.clone(),
284            show_legend: self.options.show_legend,
285        }
286    }
287}
288
289#[cfg(all(test, feature = "static_export"))]
290mod tests {
291    use super::*;
292    use crate::visualization::{Series2D, TraceMode};
293    use rust_decimal_macros::dec;
294
295    /// A chart with more x than y values.
296    struct Lopsided;
297
298    impl Graph for Lopsided {
299        fn graph_data(&self) -> GraphData {
300            GraphData::Series(Series2D {
301                x: vec![dec!(1), dec!(2)],
302                y: vec![dec!(1)],
303                name: "lopsided".to_string(),
304                mode: TraceMode::Lines,
305                line_color: None,
306                line_width: None,
307            })
308        }
309    }
310
311    impl Plottable for Lopsided {
312        fn plot(&self) -> PlotBuilder<Self> {
313            PlotBuilder {
314                data: Lopsided,
315                options: GraphConfig::default(),
316            }
317        }
318    }
319
320    #[test]
321    fn test_save_returns_the_export_error_typed() -> Result<(), Box<dyn std::error::Error>> {
322        let dir = tempfile::tempdir()?;
323        let result = Lopsided.plot().save(dir.path().join("lopsided.png"));
324        assert!(
325            matches!(result, Err(GraphError::LengthMismatch { x: 2, y: 1, .. })),
326            "{result:?}"
327        );
328        Ok(())
329    }
330}