Skip to main content

optionstratlib_visualization/visualization/
utils.rs

1use crate::visualization::ColorScheme;
2
3#[cfg(feature = "plotly")]
4use {
5    crate::visualization::GraphConfig,
6    crate::visualization::{Label2D, Series2D, Surface3D, TraceMode},
7    plotly::common::{Font, Line, Mode},
8    plotly::{Scatter, Surface},
9    rust_decimal::Decimal,
10};
11
12/// Creates a Scatter trace from a Series2D and configuration
13#[cfg(feature = "plotly")]
14#[must_use]
15pub fn make_scatter(series: &Series2D) -> Box<Scatter<Decimal, Decimal>> {
16    let mode = match series.mode {
17        TraceMode::Lines => Mode::Lines,
18        TraceMode::Markers => Mode::Markers,
19        TraceMode::LinesMarkers => Mode::LinesMarkers,
20        TraceMode::TextLabels => Mode::Text,
21    };
22
23    // Create the scatter with coordinates and name
24    let mut trace = Scatter::new(series.x.clone(), series.y.clone())
25        .name(series.name.clone())
26        .mode(mode);
27
28    // Configure the line with color and width if specified
29    let mut line = Line::new();
30    if let Some(w) = series.line_width {
31        line = line.width(w);
32    }
33
34    if let Some(ref c) = series.line_color {
35        line = line.color(c.to_string());
36    }
37
38    trace = trace.line(line);
39
40    // Handle text labels mode
41    if matches!(series.mode, TraceMode::TextLabels) {
42        // Use the name as text for each point
43        let text_vec = vec![series.name.clone(); series.x.len()];
44
45        // Set the text for each point
46        trace = trace.text_array(text_vec);
47
48        // Configure the mode to show text
49        trace = trace.mode(Mode::Text);
50
51        // Configure the text to be visible with appropriate color
52        if let Some(ref color) = series.line_color {
53            // Increase font size for better visibility
54            let text_font = plotly::common::Font::new()
55                .color(color.to_string())
56                .size(16);
57            trace = trace.text_font(text_font);
58        }
59    } else if matches!(series.mode, TraceMode::Markers) && series.line_width == Some(0.0) {
60        // Backward compatibility for old implementation
61        // Use the name as text for each point
62        let text_vec = vec![series.name.clone(); series.x.len()];
63
64        // Set the text for each point
65        trace = trace.text_array(text_vec);
66
67        // Configure the text to be visible with appropriate color
68        if let Some(ref color) = series.line_color {
69            // Increase font size for better visibility
70            let text_font = plotly::common::Font::new()
71                .color(color.to_string())
72                .size(16);
73            trace = trace.text_font(text_font);
74
75            // Make the marker visible with appropriate color
76            let marker = plotly::common::Marker::new()
77                .size(10)
78                .color(color.to_string());
79            trace = trace.marker(marker);
80        }
81    }
82
83    trace
84}
85
86/// Pick a color from config based on index
87#[cfg(feature = "plotly")]
88#[must_use]
89pub fn pick_color(cfg: &GraphConfig, idx: usize) -> Option<String> {
90    get_color_from_scheme(&cfg.color_scheme, idx)
91}
92
93/// Creates a 3D surface representation wrapped in a `Box`, based on the input `Surface3D` structure.
94///
95/// This function converts the input `Surface3D` data into a `Surface` object, which includes the x, y, and z
96/// coordinate mappings. If any of the x, y, or z coordinate vectors in the input are empty, it creates a
97/// default 2x2 surface with all z values initialized to zero and x/y spanning the range `[0, 1]`.
98///
99/// # Parameters
100/// - `surf`: A reference to a `Surface3D` object, which contains x, y, and z coordinate vectors
101///   and an optional name for the surface.
102///
103/// # Returns
104/// - A `Box` containing a `Surface<Decimal, Decimal, Decimal>` object, where:
105///   - `x` and `y` axes are unique and sorted.
106///   - `z` values are represented in a 2D matrix where rows correspond to `y` values and columns
107///     correspond to `x` values.
108///
109/// # Logic Details
110/// 1. **Edge Case Handling**: If any of `surf.x`, `surf.y`, or `surf.z` is empty, create a default 2x2 surface:
111///    - All z values are initialized to `Decimal::ZERO`.
112///    - x and y coordinates are `[0, 1]`.
113///    - The surface name remains as provided in the `surf.name`.
114///
115/// 2. **Unique and Sorted Axes**:
116///    - Extract unique and sorted x and y coordinates using a `BTreeSet`.
117///    - These unique x and y coordinates are then stored in vectors (`x_unique` and `y_unique`).
118///
119/// 3. **Mapping Coordinates**:
120///    - Create mappings (`x_to_col` and `y_to_row`) to translate x and y coordinates into column and row
121///      indices of the `z_matrix`. Each coordinate is mapped to its respective index in the
122#[cfg(feature = "plotly")]
123#[must_use]
124pub fn make_surface(surf: &Surface3D) -> Box<Surface<Decimal, Decimal, Decimal>> {
125    if surf.x.is_empty() || surf.y.is_empty() || surf.z.is_empty() {
126        let z_matrix = vec![
127            vec![Decimal::ZERO, Decimal::ZERO],
128            vec![Decimal::ZERO, Decimal::ZERO],
129        ];
130        return Surface::new(z_matrix)
131            .x(vec![Decimal::ZERO, Decimal::ONE])
132            .y(vec![Decimal::ZERO, Decimal::ONE])
133            .name(&surf.name);
134    }
135
136    let mut x_set = std::collections::BTreeSet::new();
137    let mut y_set = std::collections::BTreeSet::new();
138
139    // One point per index the three coordinate vectors share.
140    let points = || surf.x.iter().zip(&surf.y).zip(&surf.z);
141
142    for ((&x, &y), _) in points() {
143        x_set.insert(x);
144        y_set.insert(y);
145    }
146
147    let x_unique: Vec<_> = x_set.into_iter().collect();
148    let y_unique: Vec<_> = y_set.into_iter().collect();
149
150    let mut x_to_col = std::collections::HashMap::new();
151    let mut y_to_row = std::collections::HashMap::new();
152
153    for (col, &x) in x_unique.iter().enumerate() {
154        x_to_col.insert(x, col);
155    }
156
157    for (row, &y) in y_unique.iter().enumerate() {
158        y_to_row.insert(y, row);
159    }
160
161    let mut z_matrix = vec![vec![Decimal::ZERO; x_unique.len()]; y_unique.len()];
162
163    for ((x, y), &z) in points() {
164        if let (Some(&row), Some(&col)) = (y_to_row.get(y), x_to_col.get(x))
165            && let Some(cell) = z_matrix.get_mut(row).and_then(|cells| cells.get_mut(col))
166        {
167            *cell = z;
168        }
169    }
170
171    Surface::new(z_matrix)
172        .x(x_unique)
173        .y(y_unique)
174        .name(&surf.name)
175}
176
177/// Utility function to convert TraceMode to Mode
178#[cfg(feature = "plotly")]
179#[must_use]
180pub fn to_plotly_mode(mode: &TraceMode) -> Mode {
181    match mode {
182        TraceMode::Lines => Mode::Lines,
183        TraceMode::Markers => Mode::Markers,
184        TraceMode::LinesMarkers => Mode::LinesMarkers,
185        TraceMode::TextLabels => Mode::Text,
186    }
187}
188
189/// Creates a Scatter trace from a Label2D
190///
191/// This function takes a Label2D structure and creates a plotly Scatter trace
192/// with the text label positioned at the point's coordinates.
193#[cfg(feature = "plotly")]
194#[allow(dead_code)]
195pub fn make_label_scatter(label: &Label2D) -> Box<Scatter<Decimal, Decimal>> {
196    // Create the scatter with the point coordinates
197    let mut trace = Scatter::new(vec![label.point.x], vec![label.point.y])
198        .name(label.point.name.clone())
199        .mode(Mode::Text);
200
201    // Set the text for the label
202    trace = trace.text_array(vec![label.label.clone()]);
203
204    // Configure the text to be visible with appropriate color
205    if let Some(ref color) = label.point.color {
206        // Set font properties for better visibility
207        let text_font = Font::new().color(color.to_string()).size(16);
208        trace = trace.text_font(text_font);
209    }
210
211    trace
212}
213
214/// Get color from a color scheme based on index
215#[must_use]
216pub fn get_color_from_scheme(scheme: &ColorScheme, idx: usize) -> Option<String> {
217    match scheme {
218        ColorScheme::Default => None,
219        ColorScheme::Viridis => {
220            // Expanded and shuffled viridis color scheme (40 colors)
221            let colors = vec![
222                "#481567", "#33638D", "#20A387", "#95D840", "#440154", "#39568C", "#1F968B",
223                "#73D055", "#482677", "#2D708E", "#29AF7F", "#B8DE29", "#453781", "#287D8E",
224                "#3CBB75", "#DCE319", "#404788", "#238A8D", "#55C667", "#FDE725", "#46337E",
225                "#1E9C89", "#79D151", "#471365", "#375B8D", "#23A884", "#A1DB32", "#45287C",
226                "#307B8E", "#34B778", "#C6DE2F", "#432D7A", "#288A8D", "#42C675", "#E3E419",
227                "#3F4889", "#21968A", "#5DC864", "#F0E51B", "#461C74",
228            ];
229            let color = colors.get(idx.checked_rem(colors.len())?)?;
230            Some(color.to_string())
231        }
232        ColorScheme::Plasma => {
233            // Expanded and shuffled plasma color scheme (40 colors)
234            let colors = vec![
235                "#0D0887", "#A01C9C", "#F2844B", "#2A0593", "#B02A8F", "#F89540", "#41049D",
236                "#BF3983", "#FCA636", "#5601A4", "#CB4678", "#FDB92F", "#6A00A8", "#D6556D",
237                "#FECE2F", "#7E03A8", "#E16462", "#FCFD35", "#8F0DA4", "#EA7457", "#330597",
238                "#B93779", "#F68F46", "#4C02A1", "#CC3F76", "#FBA238", "#6501AB", "#DC5267",
239                "#FDC229", "#7B04A7", "#E36159", "#FED330", "#8D0BA2", "#E7704F", "#FEE54F",
240                "#9E189B", "#EC7F45", "#FEF06F", "#1C0377", "#AC2294",
241            ];
242            let color = colors.get(idx.checked_rem(colors.len())?)?;
243            Some(color.to_string())
244        }
245        ColorScheme::Custom(list) => {
246            // Guard against `Custom(Vec::new())` — `idx % 0` would
247            // otherwise panic on integer division by zero.
248            list.get(idx.checked_rem(list.len())?).cloned()
249        }
250        ColorScheme::White => Some("#FFFFFF".to_string()),
251        ColorScheme::HighContrast => {
252            let colors = vec![
253                "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#1F77B4",
254                "#FF7F0E", "#2CA02C", "#D62728", "#9467BD", "#8C564B", "#E377C2", "#7F7F7F",
255                "#BCBD22", "#17BECF", "#800000", "#008000", "#000080", "#808000", "#800080",
256                "#008080", "#FF8C00", "#90EE90", "#ADD8E6", "#FFA07A", "#DA70D6", "#FFD700",
257                "#FF1493", "#4B0082", "#006400", "#483D8B", "#CD853F", "#8B4513", "#4682B4",
258                "#9ACD32", "#B22222", "#A52A2A", "#6A5ACD", "#778899", "#FF6347", "#7CFC00",
259                "#87CEFA", "#FFA500", "#9932CC", "#008B8B",
260            ];
261            let color = colors.get(idx.checked_rem(colors.len())?)?;
262            Some(color.to_string())
263        }
264    }
265}