Skip to main content

asciigraph/options/
config.rs

1// Config — the builder-pattern configuration struct for graph rendering.
2
3use crate::color::AnsiColor;
4use crate::options::charset::{CharSet};
5use crate::options::extensions::{ZeroLine, Threshold, StatAnnotations};
6
7// ---------------------------------------------------------------------------
8// Config
9// ---------------------------------------------------------------------------
10
11/// Configuration for controlling the appearance and behavior of a graph.
12///
13/// `Config` uses a builder pattern — start with [`Config::default()`] and
14/// chain the methods for the options you want to set. Every method consumes
15/// and returns `Self`, so calls can be chained fluently.
16///
17/// # Example
18///
19/// ```rust
20/// use asciigraph::{plot, Config, AnsiColor};
21///
22/// let data = vec![1.0, 2.0, 3.0, 2.0, 1.0];
23/// let graph = plot(
24///     &data,
25///     Config::default()
26///         .height(5)
27///         .caption("My Graph")
28///         .axis_color(AnsiColor::GREEN),
29/// );
30/// ```
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct Config {
33    /// Target width of the data area in columns. `0` means auto-size to the
34    /// number of data points.
35    pub width: usize,
36
37    /// Target height of the graph in rows. `0` means auto-size based on the
38    /// data range.
39    pub height: usize,
40
41    /// Optional lower bound for the Y-axis. Ignored if the data minimum is
42    /// already below this value.
43    pub lower_bound: Option<f64>,
44
45    /// Optional upper bound for the Y-axis. Ignored if the data maximum is
46    /// already above this value.
47    pub upper_bound: Option<f64>,
48
49    /// Number of columns reserved for the Y-axis label area. Defaults to `3`.
50    pub offset: usize,
51
52    /// Caption string rendered below the graph body.
53    pub caption: String,
54
55    /// Number of decimal places for Y-axis labels. `None` means auto-detect.
56    pub precision: Option<usize>,
57
58    /// ANSI color for the caption text.
59    pub caption_color: AnsiColor,
60
61    /// ANSI color for axis lines and tick characters.
62    pub axis_color: AnsiColor,
63
64    /// ANSI color for Y-axis labels.
65    pub label_color: AnsiColor,
66
67    /// Per-series ANSI colors. The first color applies to the first series,
68    /// the second to the second, and so on.
69    pub series_colors: Vec<AnsiColor>,
70
71    /// Per-series legend labels rendered below the graph.
72    pub series_legends: Vec<String>,
73
74    /// Line ending sequence. Defaults to `"\n"`. Use `"\r\n"` for Windows
75    /// raw terminals.
76    pub line_ending: String,
77
78    /// Per-series character sets. Falls back to [`DEFAULT_CHAR_SET`] for any
79    /// series that does not have an explicit entry.
80    pub series_chars: Vec<CharSet>,
81
82    /// Number of tick marks on the X-axis. `0` means auto-calculate.
83    /// Minimum when set explicitly is `2`.
84    pub x_axis_tick_count: usize,
85
86    /// The `[min, max]` domain mapped onto the X-axis. Setting this enables
87    /// the X-axis.
88    pub x_axis_range: Option<[f64; 2]>,
89
90    /// Custom formatter for X-axis tick labels. Accepts any closure of the
91    /// form `Fn(f64) -> String`.
92    #[cfg_attr(feature = "serde", serde(skip))]
93    pub x_axis_value_formatter: Option<Box<dyn Fn(f64) -> String>>,
94
95    /// Custom formatter for Y-axis labels. Accepts any closure of the form
96    /// `Fn(f64) -> String`.
97    #[cfg_attr(feature = "serde", serde(skip))]
98    pub y_axis_value_formatter: Option<Box<dyn Fn(f64) -> String>>,
99
100    /// Optional zero line drawn at Y = 0.0 across the data area.
101    pub zero_line: Option<ZeroLine>,
102
103    /// Horizontal reference lines drawn at user-specified Y values.
104    pub thresholds: Vec<Threshold>,
105
106    /// Window size for the moving average overlay. `None` means disabled.
107    pub moving_average_window: Option<usize>,
108
109    /// Descriptive label rendered flush left above the graph body.
110    /// Set via [`Config::y_axis_label()`].
111    pub y_axis_label: Option<String>,
112
113    /// Descriptive label rendered inline on the same row as the X-axis line,
114    /// to the right of the tick marks. Only visible when [`Config::x_axis_range()`]
115    /// is also configured.
116    pub x_axis_label: Option<String>,
117
118    /// Optional statistical annotations rendered as horizontal reference lines
119    /// at computed values — minimum, maximum, mean, median, and standard deviation.
120    /// Set via [`Config::stat_annotations()`].
121    pub stat_annotations: Option<StatAnnotations>,
122}
123
124impl Default for Config {
125    fn default() -> Self {
126        Config {
127            width:                  0,
128            height:                 0,
129            lower_bound:            None,
130            upper_bound:            None,
131            offset:                 3,
132            caption:                String::new(),
133            precision:              None,
134            caption_color:          AnsiColor::DEFAULT,
135            axis_color:             AnsiColor::DEFAULT,
136            label_color:            AnsiColor::DEFAULT,
137            series_colors:          Vec::new(),
138            series_legends:         Vec::new(),
139            line_ending:            String::from("\n"),
140            series_chars:           Vec::new(),
141            x_axis_tick_count:      0,
142            x_axis_range:           None,
143            x_axis_value_formatter: None,
144            y_axis_value_formatter: None,
145            zero_line:              None,
146            thresholds:             Vec::new(),
147            moving_average_window:  None,
148            x_axis_label:           None,
149            y_axis_label:           None,
150            stat_annotations:       None,
151        }
152    }
153}
154
155impl Config {
156    /// Sets the graph width in columns.
157    ///
158    /// When set to a positive value, the input data is interpolated to produce
159    /// exactly this many data columns regardless of how many points were
160    /// provided. Pass `0` to auto-size the width to the number of data points.
161    pub fn width(mut self, w: usize) -> Self {
162        self.width = w;
163        self
164    }
165
166    /// Sets the graph height in rows.
167    ///
168    /// Pass `0` to auto-size the height based on the data range, which is the
169    /// default behavior.
170    pub fn height(mut self, h: usize) -> Self {
171        self.height = h;
172        self
173    }
174
175    /// Sets an optional lower bound for the Y-axis.
176    ///
177    /// This value is only applied if it is lower than the actual data minimum.
178    /// It will not compress the visible range — it can only expand it downward.
179    pub fn lower_bound(mut self, min: f64) -> Self {
180        self.lower_bound = Some(min);
181        self
182    }
183
184    /// Sets an optional upper bound for the Y-axis.
185    ///
186    /// This value is only applied if it is higher than the actual data maximum.
187    /// It will not compress the visible range — it can only expand it upward.
188    pub fn upper_bound(mut self, max: f64) -> Self {
189        self.upper_bound = Some(max);
190        self
191    }
192
193    /// Sets the number of columns reserved for the Y-axis label area.
194    ///
195    /// Increase this value if your Y-axis labels are wider than the default
196    /// allows. Defaults to `3` when not set.
197    pub fn offset(mut self, off: usize) -> Self {
198        self.offset = off;
199        self
200    }
201
202    /// Sets the number of decimal places used in Y-axis labels.
203    ///
204    /// When not set, the library auto-detects appropriate precision based
205    /// on the data range — more decimal places for small values, fewer for
206    /// large ones.
207    pub fn precision(mut self, p: usize) -> Self {
208        self.precision = Some(p);
209        self
210    }
211
212    /// Sets the caption rendered below the graph body.
213    ///
214    /// The caption is centered over the data area. Leading and trailing
215    /// whitespace is trimmed before rendering.
216    pub fn caption(mut self, c: &str) -> Self {
217        self.caption = c.trim().to_string();
218        self
219    }
220
221    /// Sets the ANSI color for the caption text.
222    pub fn caption_color(mut self, color: AnsiColor) -> Self {
223        self.caption_color = color;
224        self
225    }
226
227    /// Sets the ANSI color for axis lines and tick characters.
228    pub fn axis_color(mut self, color: AnsiColor) -> Self {
229        self.axis_color = color;
230        self
231    }
232
233    /// Sets the ANSI color for Y-axis labels.
234    pub fn label_color(mut self, color: AnsiColor) -> Self {
235        self.label_color = color;
236        self
237    }
238
239    /// Sets per-series ANSI colors.
240    ///
241    /// The first color applies to the first series, the second to the second,
242    /// and so on. Series without a corresponding color entry are rendered in
243    /// the terminal default color.
244    pub fn series_colors(mut self, colors: &[AnsiColor]) -> Self {
245        self.series_colors = colors.to_vec();
246        self
247    }
248
249    /// Sets per-series legend labels rendered below the graph.
250    ///
251    /// The first label corresponds to the first series, the second to the
252    /// second, and so on. Labels are rendered alongside a colored square
253    /// marker matching the series color.
254    pub fn series_legends(mut self, text: &[&str]) -> Self {
255        self.series_legends = text.iter().map(|s| s.to_string()).collect();
256        self
257    }
258
259    /// Sets the line-ending sequence used between rows.
260    ///
261    /// Defaults to `"\n"`. Use `"\r\n"` for Windows raw terminals or any
262    /// environment that requires CRLF line endings.
263    pub fn line_ending(mut self, ending: &str) -> Self {
264        self.line_ending = ending.to_string();
265        self
266    }
267
268    /// Sets per-series character sets.
269    ///
270    /// The first [`CharSet`] applies to the first series, the second to the
271    /// second, and so on. Series without a corresponding entry fall back to
272    /// [`DEFAULT_CHAR_SET`]. Use [`create_char_set`] to create a uniform set,
273    /// or struct update syntax to override individual characters.
274    ///
275    /// [`create_char_set`]: crate::options::create_char_set
276    pub fn series_chars(mut self, cs: &[CharSet]) -> Self {
277        self.series_chars = cs.to_vec();
278        self
279    }
280
281    /// Sets the number of tick marks on the X-axis, overriding the automatic
282    /// calculation.
283    ///
284    /// When this is not called, the library automatically determines a sensible
285    /// tick count based on the available graph width and the estimated width of
286    /// the tick labels. The minimum accepted value is `2` — values below `2`
287    /// are ignored and the automatic calculation is used instead.
288    ///
289    /// Only takes effect when an X-axis range has been set via
290    /// [`Config::x_axis_range()`].
291    pub fn x_axis_tick_count(mut self, count: usize) -> Self {
292        if count >= 2 {
293            self.x_axis_tick_count = count;
294        }
295        self
296    }
297
298    /// Enables the X-axis and maps the domain `[min, max]` onto the plot width.
299    ///
300    /// Once set, an X-axis line and tick labels are rendered below the graph
301    /// body. The number of ticks is calculated automatically — call
302    /// [`Config::x_axis_tick_count()`] to override.
303    ///
304    /// # Example
305    ///
306    /// ```rust
307    /// use asciigraph::Config;
308    ///
309    /// let config = Config::default().x_axis_range(0.0, 100.0);
310    /// ```
311    pub fn x_axis_range(mut self, min: f64, max: f64) -> Self {
312        self.x_axis_range = Some([min, max]);
313        self
314    }
315
316    /// Sets a custom formatter for X-axis tick labels.
317    ///
318    /// Accepts any closure of the form `Fn(f64) -> String`. Use this to add
319    /// units, control decimal places, or apply any other formatting to the
320    /// values printed below the X-axis ticks.
321    ///
322    /// # Example
323    ///
324    /// ```rust
325    /// use asciigraph::Config;
326    ///
327    /// let config = Config::default()
328    ///     .x_axis_range(0.0, 1000.0)
329    ///     .x_axis_value_formatter(Box::new(|v| format!("{:.0}ms", v)));
330    /// ```
331    pub fn x_axis_value_formatter(mut self, formatter: Box<dyn Fn(f64) -> String>) -> Self {
332        self.x_axis_value_formatter = Some(formatter);
333        self
334    }
335
336    /// Sets a custom formatter for Y-axis labels.
337    ///
338    /// Accepts any closure of the form `Fn(f64) -> String`. Use this to add
339    /// units, convert between scales, or apply any other formatting to the
340    /// values printed on the Y-axis.
341    ///
342    /// # Example
343    ///
344    /// ```rust
345    /// use asciigraph::Config;
346    ///
347    /// let config = Config::default()
348    ///     .y_axis_value_formatter(Box::new(|v| format!("{:.1} GiB", v / 1024.0)));
349    /// ```
350    pub fn y_axis_value_formatter(mut self, formatter: Box<dyn Fn(f64) -> String>) -> Self {
351        self.y_axis_value_formatter = Some(formatter);
352        self
353    }
354
355    /// Enables a horizontal reference line at Y = 0.0 across the data area.
356    ///
357    /// The line is only visible when the data range straddles zero. Series arc
358    /// characters always render on top of the zero line.
359    ///
360    /// # Example
361    ///
362    /// ```rust
363    /// use asciigraph::{plot, Config, ZeroLine, AnsiColor};
364    ///
365    /// let data = vec![-2.0, -1.0, 0.0, 1.0, 2.0];
366    /// let graph = plot(
367    ///     &data,
368    ///     Config::default().zero_line(ZeroLine::with_color(AnsiColor::RED)),
369    /// );
370    /// ```
371    pub fn zero_line(mut self, zero_line: ZeroLine) -> Self {
372        self.zero_line = Some(zero_line);
373        self
374    }
375
376    /// Adds a horizontal reference line at a user-specified Y value.
377    ///
378    /// Call this method multiple times to add more than one threshold. Series
379    /// arc characters always render on top of threshold lines.
380    ///
381    /// # Example
382    ///
383    /// ```rust
384    /// use asciigraph::{plot, Config, Threshold, AnsiColor};
385    ///
386    /// let data = vec![60.0, 70.0, 85.0, 92.0, 78.0, 65.0];
387    /// let graph = plot(
388    ///     &data,
389    ///     Config::default()
390    ///         .threshold(Threshold::with_color(80.0, AnsiColor::YELLOW))
391    ///         .threshold(Threshold::with_color(90.0, AnsiColor::RED)),
392    /// );
393    /// ```
394    pub fn threshold(mut self, t: Threshold) -> Self {
395        self.thresholds.push(t);
396        self
397    }
398
399    /// Enables a moving average overlay rendered as an additional series.
400    ///
401    /// The smoothed series is drawn on top of the original data using the next
402    /// available series color and character set. A window of `0` or `1` has no
403    /// effect.
404    ///
405    /// # Example
406    ///
407    /// ```rust
408    /// use asciigraph::{plot, Config};
409    ///
410    /// let data = vec![3.0, 1.0, 5.0, 2.0, 4.0, 1.0, 6.0, 2.0, 5.0, 1.0];
411    /// let graph = plot(&data, Config::default().moving_average(3));
412    /// ```
413    pub fn moving_average(mut self, window: usize) -> Self {
414        self.moving_average_window = Some(window);
415        self
416    }
417
418    /// Sets a descriptive label for the X-axis.
419    ///
420    /// Rendered inline on the same row as the axis line, to the right of the
421    /// tick marks. Only visible when [`Config::x_axis_range()`] is also set.
422    ///
423    /// # Example
424    ///
425    /// ```rust
426    /// use asciigraph::Config;
427    ///
428    /// let config = Config::default()
429    ///     .x_axis_range(0.0, 100.0)
430    ///     .x_axis_label("Time (seconds)");
431    /// ```
432    pub fn x_axis_label(mut self, label: &str) -> Self {
433        self.x_axis_label = Some(label.to_string());
434        self
435    }
436
437    /// Sets a descriptive label for the Y-axis.
438    ///
439    /// Rendered flush left above the graph body.
440    ///
441    /// # Example
442    ///
443    /// ```rust
444    /// use asciigraph::Config;
445    ///
446    /// let config = Config::default().y_axis_label("Memory (MB)");
447    /// ```
448    pub fn y_axis_label(mut self, label: &str) -> Self {
449        self.y_axis_label = Some(label.to_string());
450        self
451    }
452
453    /// Enables statistical annotations as horizontal reference lines across
454    /// the data area.
455    ///
456    /// Supports minimum, maximum, mean, median, and standard deviation.
457    /// Annotations are rendered before the series so series arc characters
458    /// always appear on top.
459    ///
460    /// # Example
461    ///
462    /// ```rust
463    /// use asciigraph::{plot, Config, StatAnnotations, AnsiColor};
464    ///
465    /// let data = vec![3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0];
466    /// let graph = plot(
467    ///     &data,
468    ///     Config::default()
469    ///         .stat_annotations(StatAnnotations::with_color(AnsiColor::YELLOW)),
470    /// );
471    /// ```
472    pub fn stat_annotations(mut self, sa: StatAnnotations) -> Self {
473        self.stat_annotations = Some(sa);
474        self
475    }
476}