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}