asciigraph/options/extensions.rs
1// Overlay annotation types — ZeroLine, Threshold, and StatAnnotations.
2//
3// All three structs serve the same conceptual role: opt-in horizontal
4// reference lines drawn on top of the graph at computed or user-specified
5// Y values. Keeping them together makes it easy to find and reason about
6// the annotation surface as a unit.
7
8use crate::color::AnsiColor;
9use crate::options::charset::DEFAULT_CHAR_SET;
10
11// ---------------------------------------------------------------------------
12// ZeroLine
13// ---------------------------------------------------------------------------
14
15/// A horizontal reference line drawn at Y = 0.0 across the data area.
16///
17/// The zero line is only rendered when the data range straddles zero — if all
18/// values are positive or all negative, this option has no effect. It is
19/// rendered before the data series so that series arc characters always appear
20/// on top.
21///
22/// # Example
23///
24/// ```rust
25/// use asciigraph::{plot, Config, ZeroLine, AnsiColor};
26///
27/// let data = vec![-3.0, -1.0, 0.0, 1.0, 3.0];
28/// let graph = plot(&data, Config::default().zero_line(ZeroLine::new()));
29/// ```
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31#[derive(Clone, Copy)]
32pub struct ZeroLine {
33 /// The ANSI color used to render the zero line.
34 /// Defaults to [`AnsiColor::DEFAULT`] (no color).
35 pub color: AnsiColor,
36
37 /// The character used to draw the zero line.
38 /// Defaults to `─` ([`DEFAULT_CHAR_SET`]`.horizontal`).
39 pub character: char,
40}
41
42impl ZeroLine {
43 /// Creates a zero line using the default horizontal character and no color.
44 pub fn new() -> Self {
45 ZeroLine {
46 color: AnsiColor::DEFAULT,
47 character: DEFAULT_CHAR_SET.horizontal,
48 }
49 }
50
51 /// Creates a zero line rendered in a specific ANSI color.
52 /// Uses the default horizontal character.
53 pub fn with_color(color: AnsiColor) -> Self {
54 ZeroLine {
55 color,
56 character: DEFAULT_CHAR_SET.horizontal,
57 }
58 }
59
60 /// Creates a zero line with both a custom character and a custom ANSI color.
61 pub fn with_char_and_color(character: char, color: AnsiColor) -> Self {
62 ZeroLine { color, character }
63 }
64}
65
66impl Default for ZeroLine {
67 fn default() -> Self {
68 ZeroLine::new()
69 }
70}
71
72// ---------------------------------------------------------------------------
73// Threshold
74// ---------------------------------------------------------------------------
75
76/// A horizontal reference line drawn at a user-specified Y value,
77/// associated with a specific data series.
78///
79/// Threshold lines are rendered as dashed lines (`╌`) across the data area
80/// at the given value, making limits, targets, or alert boundaries immediately
81/// visible on the graph. Multiple thresholds can be added to a single graph
82/// by calling [`Config::threshold()`] repeatedly.
83///
84/// Each threshold is associated with a series via `series_index`, which
85/// defaults to `0` (the first series). Two rules are applied before a
86/// threshold is drawn:
87///
88/// **Visibility rule** — the threshold value must fall within the min/max
89/// range of its associated series specifically, not just the global graph
90/// range. This means a threshold at Y = 80.0 associated with a series whose
91/// values only reach 40.0 will be silently skipped, even if another series
92/// on the same graph reaches 90.0.
93///
94/// **Color inheritance rule** — when no explicit color is set on the
95/// threshold (i.e. `color` is [`AnsiColor::DEFAULT`]), the threshold
96/// automatically inherits the color of its associated series from
97/// `Config::series_colors`. An explicitly set color always takes priority.
98///
99/// Series arc characters always render on top of threshold lines.
100///
101/// # Example
102///
103/// ```rust
104/// use asciigraph::{plot_many, Config, Threshold, AnsiColor};
105///
106/// let s1 = vec![60.0, 75.0, 85.0, 92.0, 78.0, 65.0];
107/// let s2 = vec![10.0, 18.0, 25.0, 35.0, 28.0, 15.0];
108///
109/// let graph = plot_many(
110/// &[&s1, &s2],
111/// Config::default()
112/// .series_colors(&[AnsiColor::BLUE, AnsiColor::GREEN])
113/// // Targets series 0 — inherits BLUE from series_colors.
114/// .threshold(Threshold::new(80.0))
115/// // Targets series 1 — overrides the inherited color.
116/// .threshold(Threshold {
117/// series_index: 1,
118/// ..Threshold::with_color(30.0, AnsiColor::RED)
119/// }),
120/// );
121/// println!("{}", graph);
122/// ```
123#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
124#[derive(Clone, Copy)]
125pub struct Threshold {
126 /// The Y value at which the threshold line is drawn.
127 pub value: f64,
128
129 /// The ANSI color used to render the threshold line.
130 /// Defaults to [`AnsiColor::DEFAULT`] (no color).
131 pub color: AnsiColor,
132
133 /// The character used to draw the threshold line.
134 /// Defaults to `╌` ([`DEFAULT_CHAR_SET`]`.dash_horizontal`).
135 pub character: char,
136
137 /// The index of the series this threshold is associated with.
138 ///
139 /// The threshold is only rendered if its value falls within the min/max
140 /// range of the series at this index. If the index is out of range or
141 /// the threshold value falls outside the series range, the threshold is
142 /// silently skipped. When no explicit color is set, the color of the
143 /// associated series is inherited automatically.
144 ///
145 /// Defaults to `0`, which associates the threshold with the first series.
146 pub series_index: usize,
147}
148
149impl Threshold {
150 /// Creates a threshold line at the given Y value using the default dashed
151 /// character and no color.
152 pub fn new(value: f64) -> Self {
153 Threshold {
154 value,
155 color: AnsiColor::DEFAULT,
156 character: DEFAULT_CHAR_SET.dash_horizontal,
157 series_index: 0,
158 }
159 }
160
161 /// Creates a threshold line at the given Y value rendered in a specific
162 /// ANSI color. Uses the default dashed character.
163 pub fn with_color(value: f64, color: AnsiColor) -> Self {
164 Threshold {
165 value,
166 color,
167 character: DEFAULT_CHAR_SET.dash_horizontal,
168 series_index: 0,
169 }
170 }
171
172 /// Creates a threshold line at the given Y value with both a custom
173 /// character and a custom ANSI color.
174 pub fn with_char_and_color(value: f64, character: char, color: AnsiColor) -> Self {
175 Threshold { value, color, character, series_index: 0 }
176 }
177}
178
179// ---------------------------------------------------------------------------
180// StatAnnotations
181// ---------------------------------------------------------------------------
182
183/// Opt-in statistical annotations rendered as horizontal reference lines
184/// at computed values across the data area.
185///
186/// The library computes each statistic from the data automatically — no
187/// manual calculation is required. Each annotation is individually
188/// controlled by a boolean flag, so you can display any combination of
189/// minimum, maximum, mean, median, and standard deviation.
190///
191/// By default, statistics are computed from the first series (`series_index
192/// = 0`). In a multi-series graph, set `series_index` to the index of the
193/// series you want to annotate. If the index is out of range, the function
194/// falls back to the first series silently.
195///
196/// Use [`StatAnnotations::new()`] to enable all five annotations at once,
197/// or set individual flags to `false` to disable specific ones. All
198/// annotations share a single color configured on the struct.
199///
200/// Annotations are rendered before the series, so series arc characters
201/// always appear on top where they overlap.
202///
203/// # Example
204///
205/// ```rust
206/// use asciigraph::{plot, Config, StatAnnotations, AnsiColor};
207///
208/// let data = vec![3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0];
209///
210/// // Enable all annotations with no color.
211/// let graph = plot(&data, Config::default().stat_annotations(StatAnnotations::new()));
212///
213/// // Enable only min and max in red.
214/// let graph = plot(
215/// &data,
216/// Config::default().stat_annotations(StatAnnotations {
217/// show_min: true,
218/// show_max: true,
219/// show_mean: false,
220/// show_median: false,
221/// show_std_dev: false,
222/// series_index: 0,
223/// color: AnsiColor::RED,
224/// }),
225/// );
226///
227/// // Annotate the second series in a multi-series graph.
228/// let graph = plot(
229/// &data,
230/// Config::default().stat_annotations(StatAnnotations {
231/// series_index: 1,
232/// ..StatAnnotations::new()
233/// }),
234/// );
235/// ```
236#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
237#[derive(Clone, Copy)]
238pub struct StatAnnotations {
239 /// Draws a reference line at the minimum value of the dataset.
240 pub show_min: bool,
241
242 /// Draws a reference line at the maximum value of the dataset.
243 pub show_max: bool,
244
245 /// Draws a reference line at the mean (average) value of the dataset.
246 pub show_mean: bool,
247
248 /// Draws a reference line at the median value of the dataset.
249 pub show_median: bool,
250
251 /// Draws a reference line at one standard deviation above and below
252 /// the mean, giving a visual indication of the data's spread.
253 pub show_std_dev: bool,
254
255 /// The ANSI color used to render all annotation lines.
256 /// Defaults to [`AnsiColor::DEFAULT`] (no color).
257 pub color: AnsiColor,
258
259 /// The index of the series to compute statistics from.
260 ///
261 /// In a single-series graph this is always `0`. In a multi-series graph,
262 /// set this to the index of the series you want to annotate. If the index
263 /// is out of range, the function falls back to the first series silently.
264 ///
265 /// Use struct update syntax to set this field without changing anything else:
266 ///
267 /// ```rust
268 /// use asciigraph::StatAnnotations;
269 ///
270 /// let annotations = StatAnnotations {
271 /// series_index: 1,
272 /// ..StatAnnotations::new()
273 /// };
274 /// ```
275 pub series_index: usize,
276}
277
278impl StatAnnotations {
279 /// Creates a `StatAnnotations` value with all five annotations enabled,
280 /// no color, and targeting the first series (`series_index = 0`).
281 pub fn new() -> Self {
282 StatAnnotations {
283 show_min: true,
284 show_max: true,
285 show_mean: true,
286 show_median: true,
287 show_std_dev: true,
288 color: AnsiColor::DEFAULT,
289 series_index: 0,
290 }
291 }
292
293 /// Creates a `StatAnnotations` value with all five annotations enabled,
294 /// rendered in a specific ANSI color, and targeting the first series.
295 ///
296 /// For multi-series graphs, override `series_index` with struct update syntax:
297 ///
298 /// ```rust
299 /// use asciigraph::{StatAnnotations, AnsiColor};
300 ///
301 /// let annotations = StatAnnotations {
302 /// series_index: 1,
303 /// ..StatAnnotations::with_color(AnsiColor::YELLOW)
304 /// };
305 /// ```
306 pub fn with_color(color: AnsiColor) -> Self {
307 StatAnnotations {
308 show_min: true,
309 show_max: true,
310 show_mean: true,
311 show_median: true,
312 show_std_dev: true,
313 series_index: 0,
314 color,
315 }
316 }
317}
318
319impl Default for StatAnnotations {
320 fn default() -> Self {
321 StatAnnotations::new()
322 }
323}