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}