Skip to main content

tui_lipan/widgets/chart/
mod.rs

1//! Chart widget.
2
3mod layout;
4mod node;
5mod reconcile;
6
7pub use layout::measure_chart;
8pub use node::ChartNode;
9pub use reconcile::reconcile_chart;
10
11use std::sync::Arc;
12
13use crate::core::element::{Element, ElementKind};
14use crate::style::{BorderStyle, Length, Padding, Style};
15
16/// Rendering mode for a chart series.
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
18pub enum ChartSeriesMode {
19    /// Draw connected trend points.
20    #[default]
21    Line,
22    /// Draw a connected high-resolution trace using a 2x4 braille subcell grid.
23    Braille,
24    /// Draw vertical bars.
25    Bars,
26}
27
28/// Single data series rendered on a chart.
29#[derive(Clone, Debug)]
30pub struct ChartSeries {
31    pub(crate) name: Arc<str>,
32    pub(crate) data: Arc<[f64]>,
33    pub(crate) mode: ChartSeriesMode,
34    pub(crate) style: Style,
35    pub(crate) point_char: char,
36    pub(crate) line_char: char,
37    pub(crate) bar_char: char,
38}
39
40impl ChartSeries {
41    /// Create a line series with a display name and numeric samples.
42    pub fn new(name: impl Into<Arc<str>>, data: impl IntoIterator<Item = f64>) -> Self {
43        Self {
44            name: name.into(),
45            data: data.into_iter().collect::<Vec<_>>().into(),
46            mode: ChartSeriesMode::Line,
47            style: Style::default(),
48            point_char: '●',
49            line_char: '─',
50            bar_char: '█',
51        }
52    }
53
54    /// Set series data from a shared slice.
55    pub fn data_arc(mut self, data: Arc<[f64]>) -> Self {
56        self.data = data;
57        self
58    }
59
60    /// Set series rendering mode.
61    pub fn mode(mut self, mode: ChartSeriesMode) -> Self {
62        self.mode = mode;
63        self
64    }
65
66    /// Set style for this series.
67    pub fn style(mut self, style: Style) -> Self {
68        self.style = style;
69        self
70    }
71
72    /// Override the point glyph used for line mode.
73    pub fn point_char(mut self, point_char: char) -> Self {
74        self.point_char = point_char;
75        self
76    }
77
78    /// Override the connector glyph used for line mode.
79    pub fn line_char(mut self, line_char: char) -> Self {
80        self.line_char = line_char;
81        self
82    }
83
84    /// Override the bar glyph used for bar mode.
85    pub fn bar_char(mut self, bar_char: char) -> Self {
86        self.bar_char = bar_char;
87        self
88    }
89}
90
91/// Axis configuration.
92#[derive(Clone, Debug)]
93pub struct ChartAxis {
94    pub(crate) show: bool,
95    pub(crate) min: Option<f64>,
96    pub(crate) max: Option<f64>,
97    pub(crate) ticks: u16,
98    pub(crate) tick_labels: Arc<[Arc<str>]>,
99    pub(crate) label: Option<Arc<str>>,
100    pub(crate) style: Style,
101}
102
103impl Default for ChartAxis {
104    fn default() -> Self {
105        Self {
106            show: true,
107            min: None,
108            max: None,
109            ticks: 4,
110            tick_labels: Arc::from([]),
111            label: None,
112            style: Style::default(),
113        }
114    }
115}
116
117impl ChartAxis {
118    /// Create default axis configuration.
119    pub fn new() -> Self {
120        Self::default()
121    }
122
123    /// Toggle axis visibility.
124    pub fn show(mut self, show: bool) -> Self {
125        self.show = show;
126        self
127    }
128
129    /// Set explicit numeric range.
130    pub fn range(mut self, min: f64, max: f64) -> Self {
131        self.min = Some(min);
132        self.max = Some(max);
133        self
134    }
135
136    /// Set preferred tick count.
137    pub fn ticks(mut self, ticks: u16) -> Self {
138        self.ticks = ticks.max(2);
139        self
140    }
141
142    /// Replace the numeric endpoint labels with explicit tick labels.
143    ///
144    /// Labels are spread evenly across the axis: the first sits at the low end,
145    /// the last at the high end, the rest centred on their fractional position.
146    /// A label that would collide with the previous one is skipped, so a dense
147    /// set degrades gracefully in a narrow plot instead of overprinting.
148    pub fn tick_labels<S: Into<Arc<str>>>(mut self, labels: impl IntoIterator<Item = S>) -> Self {
149        self.tick_labels = labels.into_iter().map(Into::into).collect();
150        self
151    }
152
153    /// Set optional axis label.
154    pub fn label(mut self, label: impl Into<Arc<str>>) -> Self {
155        self.label = Some(label.into());
156        self
157    }
158
159    /// Set axis style.
160    pub fn style(mut self, style: Style) -> Self {
161        self.style = style;
162        self
163    }
164}
165
166/// Horizontal threshold reference line.
167#[derive(Clone, Debug)]
168pub struct ChartThreshold {
169    pub(crate) value: f64,
170    pub(crate) label: Option<Arc<str>>,
171    pub(crate) style: Style,
172    pub(crate) glyph: char,
173}
174
175impl ChartThreshold {
176    /// Create a new threshold line at a numeric value.
177    pub fn new(value: f64) -> Self {
178        Self {
179            value,
180            label: None,
181            style: Style::default(),
182            glyph: '┈',
183        }
184    }
185
186    /// Set threshold label.
187    pub fn label(mut self, label: impl Into<Arc<str>>) -> Self {
188        self.label = Some(label.into());
189        self
190    }
191
192    /// Set threshold style.
193    pub fn style(mut self, style: Style) -> Self {
194        self.style = style;
195        self
196    }
197
198    /// Set glyph used for the threshold line.
199    pub fn glyph(mut self, glyph: char) -> Self {
200        self.glyph = glyph;
201        self
202    }
203}
204
205/// Multi-series chart with axes, grid, legend, and thresholds.
206#[derive(Clone)]
207pub struct Chart {
208    pub(crate) series: Arc<[ChartSeries]>,
209    pub(crate) thresholds: Arc<[ChartThreshold]>,
210    pub(crate) x_axis: ChartAxis,
211    pub(crate) y_axis: ChartAxis,
212    pub(crate) style: Style,
213    pub(crate) axis_style: Style,
214    pub(crate) grid_style: Style,
215    pub(crate) legend_style: Style,
216    pub(crate) show_grid: bool,
217    pub(crate) show_legend: bool,
218    pub(crate) legend_separator: Arc<str>,
219    pub(crate) viewport_start: usize,
220    pub(crate) viewport_len: Option<usize>,
221    /// Padding inside the chart frame.
222    /// Default: `Padding::default()`.
223    pub(crate) padding: Padding,
224    pub(crate) border: bool,
225    /// Border style.
226    /// Default: `BorderStyle::Plain`.
227    pub(crate) border_style: BorderStyle,
228    /// Requested width.
229    /// Default: `Length::Flex(1)`.
230    pub(crate) width: Length,
231    /// Requested height.
232    /// Default: `Length::Px(10)`.
233    pub(crate) height: Length,
234}
235
236impl Default for Chart {
237    fn default() -> Self {
238        Self {
239            series: Arc::new([]),
240            thresholds: Arc::new([]),
241            x_axis: ChartAxis::default(),
242            y_axis: ChartAxis::default(),
243            style: Style::default(),
244            axis_style: Style::default(),
245            grid_style: Style::default(),
246            legend_style: Style::default(),
247            show_grid: true,
248            show_legend: true,
249            legend_separator: Arc::from("  "),
250            viewport_start: 0,
251            viewport_len: None,
252            padding: Padding::default(),
253            border: false,
254            border_style: BorderStyle::Plain,
255            width: Length::Flex(1),
256            height: Length::Px(10),
257        }
258    }
259}
260
261impl Chart {
262    /// Create an empty chart.
263    pub fn new() -> Self {
264        Self::default()
265    }
266
267    /// Replace all chart series.
268    pub fn series(mut self, series: impl IntoIterator<Item = ChartSeries>) -> Self {
269        self.series = series.into_iter().collect::<Vec<_>>().into();
270        self
271    }
272
273    /// Set series from a shared slice.
274    pub fn series_arc(mut self, series: Arc<[ChartSeries]>) -> Self {
275        self.series = series;
276        self
277    }
278
279    /// Add one chart series.
280    pub fn add_series(mut self, series: ChartSeries) -> Self {
281        let mut next = self.series.to_vec();
282        next.push(series);
283        self.series = next.into();
284        self
285    }
286
287    /// Replace threshold definitions.
288    pub fn thresholds(mut self, thresholds: impl IntoIterator<Item = ChartThreshold>) -> Self {
289        self.thresholds = thresholds.into_iter().collect::<Vec<_>>().into();
290        self
291    }
292
293    /// Set X axis config.
294    pub fn x_axis(mut self, axis: ChartAxis) -> Self {
295        self.x_axis = axis;
296        self
297    }
298
299    /// Set Y axis config.
300    pub fn y_axis(mut self, axis: ChartAxis) -> Self {
301        self.y_axis = axis;
302        self
303    }
304
305    /// Set base chart style.
306    pub fn style(mut self, style: Style) -> Self {
307        self.style = style;
308        self
309    }
310
311    /// Set axis style.
312    pub fn axis_style(mut self, style: Style) -> Self {
313        self.axis_style = style;
314        self
315    }
316
317    /// Set grid style.
318    pub fn grid_style(mut self, style: Style) -> Self {
319        self.grid_style = style;
320        self
321    }
322
323    /// Set legend style.
324    pub fn legend_style(mut self, style: Style) -> Self {
325        self.legend_style = style;
326        self
327    }
328
329    /// Toggle plot grid rendering.
330    pub fn show_grid(mut self, show_grid: bool) -> Self {
331        self.show_grid = show_grid;
332        self
333    }
334
335    /// Toggle legend rendering.
336    pub fn show_legend(mut self, show_legend: bool) -> Self {
337        self.show_legend = show_legend;
338        self
339    }
340
341    /// Set separator between legend items.
342    pub fn legend_separator(mut self, legend_separator: impl Into<Arc<str>>) -> Self {
343        self.legend_separator = legend_separator.into();
344        self
345    }
346
347    /// Set viewport start index.
348    pub fn viewport_start(mut self, viewport_start: usize) -> Self {
349        self.viewport_start = viewport_start;
350        self
351    }
352
353    /// Set optional viewport sample length.
354    pub fn viewport_len(mut self, viewport_len: Option<usize>) -> Self {
355        self.viewport_len = viewport_len;
356        self
357    }
358
359    /// Set chart padding.
360    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
361        self.padding = padding.into();
362        self
363    }
364
365    /// Enable or disable chart border.
366    pub fn border(mut self, border: bool) -> Self {
367        self.border = border;
368        self
369    }
370
371    /// Set border style.
372    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
373        self.border_style = border_style;
374        self
375    }
376
377    /// Set requested chart width.
378    pub fn width(mut self, width: Length) -> Self {
379        self.width = width;
380        self
381    }
382
383    /// Set requested chart height.
384    pub fn height(mut self, height: Length) -> Self {
385        self.height = height;
386        self
387    }
388}
389
390impl From<Chart> for Element {
391    fn from(value: Chart) -> Self {
392        Element::new(ElementKind::Chart(Box::new(value)))
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399    use std::sync::Arc;
400
401    #[test]
402    fn series_arc_preserves_shared_slice() {
403        let series: Arc<[ChartSeries]> = Arc::from([ChartSeries::new("cpu", [1.0, 2.0, 3.0])]);
404        let chart = Chart::new().series_arc(Arc::clone(&series));
405        assert!(Arc::ptr_eq(&chart.series, &series));
406    }
407
408    #[test]
409    fn chart_series_data_arc_preserves_shared_slice() {
410        let data: Arc<[f64]> = Arc::from([1.0, 2.0, 3.0]);
411        let series = ChartSeries::new("cpu", []).data_arc(Arc::clone(&data));
412        assert!(Arc::ptr_eq(&series.data, &data));
413    }
414}