Skip to main content

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}