Skip to main content

optionstratlib_visualization/error/
graph.rs

1//! Target crate (ADR-0001 D6, roadmap M1-14): **visualization**. Owns `GraphError`.
2
3use optionstratlib_backtest::error::BacktestError;
4use optionstratlib_core::error::DecimalError;
5use optionstratlib_math::error::{CurveError, SurfaceError};
6use rust_decimal::Decimal;
7use thiserror::Error;
8
9/// Represents errors that can occur during graph generation and rendering operations.
10///
11/// This error type encapsulates failures that may happen when creating, rendering,
12/// or saving graphical representations of financial data such as option chains,
13/// volatility surfaces, or strategy payoffs.
14///
15/// The PNG and SVG export (`static_export`) reports data it cannot draw
16/// faithfully with its own variants rather than repairing it: a surface
17/// with missing or repeated grid points, series whose coordinate arrays
18/// differ in length, a colour or a label character it cannot reproduce.
19/// The enum is `#[non_exhaustive]`: match it with a wildcard arm, since
20/// later releases may add variants.
21#[derive(Error, Debug)]
22#[non_exhaustive]
23pub enum GraphError {
24    /// Represents errors that occur during the rendering process.
25    /// Contains a descriptive message about what went wrong.
26    #[error("Render error: {0}")]
27    Render(String),
28
29    /// Represents I/O errors that occur when reading from or writing to files
30    /// (e.g., when saving graphs to disk).
31    #[error("IO error: {0}")]
32    Io(#[from] std::io::Error),
33
34    /// Error from curve operations.
35    #[error(transparent)]
36    Curve(CurveError),
37
38    /// Error from surface operations.
39    #[error(transparent)]
40    Surface(SurfaceError),
41
42    /// A figure a report derives for display (a total, an average) left the
43    /// representable `Decimal` range.
44    #[error(transparent)]
45    Decimal(Box<DecimalError>),
46
47    /// The backtest figures a report shows could not be computed, for
48    /// example when the statistics of a `SimulationStats` accumulator
49    /// overflow `Decimal`.
50    #[error(transparent)]
51    Backtest(Box<BacktestError>),
52
53    /// The configured figure width or height is zero or larger than the
54    /// 16384 pixels the static export draws.
55    #[error("invalid dimensions {width}x{height}: both must be 1 to 16384 pixels")]
56    InvalidDimensions {
57        /// Configured width in pixels.
58        width: u32,
59        /// Configured height in pixels.
60        height: u32,
61    },
62
63    /// The figure is too small to lay the chart out.
64    #[error(
65        "{width}x{height} is too small for this chart; it needs at least {min_width}x{min_height}"
66    )]
67    TooSmall {
68        /// Configured width in pixels.
69        width: u32,
70        /// Configured height in pixels.
71        height: u32,
72        /// Smallest width the chart is laid out in.
73        min_width: u32,
74        /// Smallest height the chart is laid out in.
75        min_height: u32,
76    },
77
78    /// The title, legend or colour bar leave no pixels to draw the chart in.
79    #[error("{width}x{height} leaves no room for the chart under its title, legend or colour bar")]
80    NoRoom {
81        /// Configured width in pixels.
82        width: u32,
83        /// Configured height in pixels.
84        height: u32,
85    },
86
87    /// A line width is not a finite number of pixels between 0 and 100.
88    #[error("series {name:?} has line width {width}: expected a finite width in 0..=100 px")]
89    InvalidLineWidth {
90        /// Name of the series.
91        name: String,
92        /// The width given.
93        width: f64,
94    },
95
96    /// A 2D series has a different number of x and y values. Plotly drew
97    /// the shorter length; the static export does not drop points.
98    #[error("series {name:?} has {x} x values and {y} y values")]
99    LengthMismatch {
100        /// Name of the series.
101        name: String,
102        /// Number of x values.
103        x: usize,
104        /// Number of y values.
105        y: usize,
106    },
107
108    /// A surface has a different number of x, y and z values.
109    #[error("surface {name:?} has {x} x, {y} y and {z} z values")]
110    SurfaceLengthMismatch {
111        /// Name of the surface.
112        name: String,
113        /// Number of x values.
114        x: usize,
115        /// Number of y values.
116        y: usize,
117        /// Number of z values.
118        z: usize,
119    },
120
121    /// A surface needs at least two distinct x and two distinct y values.
122    #[error("surface {name:?} has a {columns}x{rows} grid; at least 2x2 is needed")]
123    DegenerateSurface {
124        /// Name of the surface.
125        name: String,
126        /// Distinct x values.
127        columns: usize,
128        /// Distinct y values.
129        rows: usize,
130    },
131
132    /// A surface grid point appears more than once. Plotly kept the last
133    /// value; the static export does not choose.
134    #[error("surface {name:?} has more than one z value at ({x}, {y})")]
135    DuplicateSurfacePoint {
136        /// Name of the surface.
137        name: String,
138        /// x of the repeated point.
139        x: Decimal,
140        /// y of the repeated point.
141        y: Decimal,
142    },
143
144    /// A surface grid point is missing: `expected` is distinct x times
145    /// distinct y, `found` the number of points given. Plotly filled the
146    /// gaps with zero; the static export does not invent values.
147    #[error("surface {name:?} is an incomplete grid: {found} of {expected} points")]
148    IncompleteSurface {
149        /// Name of the surface.
150        name: String,
151        /// Points a complete grid has.
152        expected: usize,
153        /// Points the surface has.
154        found: usize,
155    },
156
157    /// A value has no `f64` representation for drawing.
158    #[error("value {0} cannot be represented as f64")]
159    NotRepresentable(Decimal),
160
161    /// A colour string is none of the CSS Color 4 forms the static export
162    /// accepts: a named colour, `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`,
163    /// `rgb()`/`rgba()` or `hsl()`/`hsla()`.
164    #[error("unsupported colour {color:?}: {reason}")]
165    InvalidColor {
166        /// The string as given.
167        color: String,
168        /// Why it was rejected.
169        reason: String,
170    },
171
172    /// A label uses a character the bundled font has no glyph for. The
173    /// static export never substitutes or drops it.
174    #[error("the bundled font has no glyph for {character:?} (U+{code:04X}) in label {text:?}")]
175    MissingGlyph {
176        /// The character without a glyph.
177        character: char,
178        /// Its code point, for display.
179        code: u32,
180        /// The whole label.
181        text: String,
182    },
183
184    /// A label, or the chart title or description an SVG carries, contains
185    /// a character XML 1.0 cannot represent: a C0 control other than tab,
186    /// line feed and carriage return, U+FFFE or U+FFFF.
187    #[error("{character:?} (U+{code:04X}) cannot appear in XML; found in {text:?}")]
188    NotXmlText {
189        /// The character XML cannot carry.
190        character: char,
191        /// Its code point, for display.
192        code: u32,
193        /// The string it was found in.
194        text: String,
195    },
196
197    /// Text the static export cannot draw: a font style other than the
198    /// bundled regular face, or SVG text outside the form it converts.
199    #[error("unsupported text: {0}")]
200    UnsupportedText(String),
201}
202
203impl From<BacktestError> for GraphError {
204    #[cold]
205    #[inline(never)]
206    fn from(err: BacktestError) -> Self {
207        GraphError::Backtest(Box::new(err))
208    }
209}
210
211impl From<DecimalError> for GraphError {
212    #[cold]
213    #[inline(never)]
214    fn from(err: DecimalError) -> Self {
215        GraphError::Decimal(Box::new(err))
216    }
217}
218
219impl From<CurveError> for GraphError {
220    #[cold]
221    #[inline(never)]
222    fn from(err: CurveError) -> Self {
223        GraphError::Curve(err)
224    }
225}
226
227impl From<SurfaceError> for GraphError {
228    #[cold]
229    #[inline(never)]
230    fn from(err: SurfaceError) -> Self {
231        GraphError::Surface(err)
232    }
233}