Skip to main content

oino_ui/widgets/
chart.rs

1//! ratatui `Chart` (§4.5.1).
2
3use serde::{Deserialize, Serialize};
4
5use crate::node::{Common, IntoNode, Node};
6use crate::style::Style;
7use crate::text::{Alignment, Line, Span};
8
9/// Point marker glyph set.
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub enum Marker {
12    /// One pixel per point.
13    Dot,
14    /// Braille-density pixels.
15    #[default]
16    Braille,
17    /// Half-block pixels.
18    Block,
19    /// Whole-block pixels.
20    Bar,
21    /// Half-block pixels, other half.
22    HalfBlock,
23}
24
25/// How a dataset is drawn.
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub enum GraphType {
28    /// Points only.
29    #[default]
30    Scatter,
31    /// Points joined left to right.
32    Line,
33    /// Vertical bars from the x axis.
34    Bar,
35}
36
37/// Legend position (Appendix A `legendPosition`; the schema also allows `null` =
38/// hide the legend, which parses to `None`).
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
40pub enum LegendPosition {
41    /// Top centre.
42    #[default]
43    Top,
44    /// Top right.
45    TopRight,
46    /// Top left.
47    TopLeft,
48    /// Left edge.
49    Left,
50    /// Right edge.
51    Right,
52    /// Bottom centre.
53    Bottom,
54    /// Bottom right.
55    BottomRight,
56    /// Bottom left.
57    BottomLeft,
58}
59
60/// One chart axis.
61#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct Axis {
64    /// Axis title.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub title: Option<Line>,
67    /// Value range `[min, max]`.
68    #[serde(default)]
69    pub bounds: [f64; 2],
70    /// Fixed tick labels.
71    #[serde(default, skip_serializing_if = "Vec::is_empty")]
72    pub labels: Vec<Span>,
73    /// Tick-label alignment.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub labels_alignment: Option<Alignment>,
76    /// Axis style.
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub style: Option<Style>,
79}
80
81impl Axis {
82    /// An axis over the given bounds.
83    #[must_use]
84    pub fn bounds(min: f64, max: f64) -> Self {
85        Self {
86            title: None,
87            bounds: [min, max],
88            labels: Vec::new(),
89            labels_alignment: None,
90            style: None,
91        }
92    }
93
94    /// Sets the axis title.
95    #[must_use]
96    pub fn title(mut self, title: impl Into<Line>) -> Self {
97        self.title = Some(title.into());
98        self
99    }
100
101    /// Sets fixed tick labels.
102    #[must_use]
103    pub fn labels(mut self, labels: impl IntoIterator<Item = Span>) -> Self {
104        self.labels = labels.into_iter().collect();
105        self
106    }
107
108    /// Sets the tick-label alignment.
109    #[must_use]
110    pub fn labels_alignment(mut self, alignment: Alignment) -> Self {
111        self.labels_alignment = Some(alignment);
112        self
113    }
114
115    /// Sets the axis style.
116    #[must_use]
117    pub fn style(mut self, style: Style) -> Self {
118        self.style = Some(style);
119        self
120    }
121}
122
123/// One chart dataset.
124#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct Dataset {
127    /// Legend name.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub name: Option<Line>,
130    /// `[x, y]` points (Σ points ≤ 10 000, §4.8).
131    pub data: Vec<[f64; 2]>,
132    /// Point marker.
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub marker: Option<Marker>,
135    /// Rendering mode.
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub graph_type: Option<GraphType>,
138    /// Dataset style.
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub style: Option<Style>,
141}
142
143impl Dataset {
144    /// A dataset from `[x, y]` points.
145    #[must_use]
146    pub fn new(data: impl IntoIterator<Item = [f64; 2]>) -> Self {
147        Self {
148            data: data.into_iter().collect(),
149            ..Self::default()
150        }
151    }
152
153    /// Sets the legend name.
154    #[must_use]
155    pub fn name(mut self, name: impl Into<Line>) -> Self {
156        self.name = Some(name.into());
157        self
158    }
159
160    /// Sets the point marker.
161    #[must_use]
162    pub fn marker(mut self, marker: Marker) -> Self {
163        self.marker = Some(marker);
164        self
165    }
166
167    /// Sets the rendering mode.
168    #[must_use]
169    pub fn graph_type(mut self, graph_type: GraphType) -> Self {
170        self.graph_type = Some(graph_type);
171        self
172    }
173
174    /// Sets the dataset style.
175    #[must_use]
176    pub fn style(mut self, style: Style) -> Self {
177        self.style = Some(style);
178        self
179    }
180}
181
182/// ratatui `Chart` (§4.5.1).
183#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct Chart {
186    /// Common node props (`key`, `style`, `block`, `fallback`).
187    #[serde(flatten)]
188    pub common: Common,
189    /// Datasets (Σ points ≤ 10 000, §4.8).
190    pub datasets: Vec<Dataset>,
191    /// X axis.
192    #[serde(default)]
193    pub x_axis: Axis,
194    /// Y axis.
195    #[serde(default)]
196    pub y_axis: Axis,
197    /// Legend position (absent = renderer default; `null` = hide).
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub legend_position: Option<LegendPosition>,
200}
201
202impl Chart {
203    /// A chart from datasets.
204    #[must_use]
205    pub fn new(datasets: impl IntoIterator<Item = Dataset>) -> Self {
206        Self {
207            datasets: datasets.into_iter().collect(),
208            ..Self::default()
209        }
210    }
211
212    /// Sets the x axis.
213    #[must_use]
214    pub fn x_axis(mut self, axis: Axis) -> Self {
215        self.x_axis = axis;
216        self
217    }
218
219    /// Sets the y axis.
220    #[must_use]
221    pub fn y_axis(mut self, axis: Axis) -> Self {
222        self.y_axis = axis;
223        self
224    }
225
226    /// Sets the legend position.
227    #[must_use]
228    pub fn legend_position(mut self, position: LegendPosition) -> Self {
229        self.legend_position = Some(position);
230        self
231    }
232
233    /// Hides the legend (wire `null`).
234    #[must_use]
235    pub fn hide_legend(mut self) -> Self {
236        self.legend_position = None;
237        self
238    }
239}
240
241impl IntoNode for Chart {}
242impl From<Chart> for Node {
243    fn from(c: Chart) -> Self {
244        Node::Chart(c)
245    }
246}