Skip to main content

plotkit_core/
series.rs

1//! Data series abstraction for chart input.
2
3/// A sequence of `f64` values representing one dimension of chart data.
4///
5/// `Series` is the canonical data container used throughout the charting
6/// pipeline. Users rarely construct one directly; instead they pass any
7/// type that implements [`IntoSeries`] (slices, vectors, arrays, ranges,
8/// etc.) and the conversion happens automatically.
9#[derive(Debug, Clone)]
10pub struct Series {
11    /// The underlying data values.
12    pub data: Vec<f64>,
13}
14
15impl Series {
16    /// Creates a new series from a vector of values.
17    ///
18    /// # Examples
19    ///
20    /// ```
21    /// use plotkit_core::series::Series;
22    ///
23    /// let s = Series::new(vec![1.0, 2.0, 3.0]);
24    /// assert_eq!(s.len(), 3);
25    /// ```
26    pub fn new(data: Vec<f64>) -> Self {
27        Self { data }
28    }
29
30    /// Returns the number of data points.
31    pub fn len(&self) -> usize {
32        self.data.len()
33    }
34
35    /// Returns `true` if the series contains no data points.
36    pub fn is_empty(&self) -> bool {
37        self.data.is_empty()
38    }
39
40    /// Returns the minimum finite value, or `None` if the series is empty
41    /// or contains no finite values.
42    ///
43    /// Non-finite values (`NaN`, `+Inf`, `-Inf`) are ignored.
44    pub fn min(&self) -> Option<f64> {
45        self.data
46            .iter()
47            .copied()
48            .filter(|v| v.is_finite())
49            .reduce(f64::min)
50    }
51
52    /// Returns the maximum finite value, or `None` if the series is empty
53    /// or contains no finite values.
54    ///
55    /// Non-finite values (`NaN`, `+Inf`, `-Inf`) are ignored.
56    pub fn max(&self) -> Option<f64> {
57        self.data
58            .iter()
59            .copied()
60            .filter(|v| v.is_finite())
61            .reduce(f64::max)
62    }
63
64    /// Returns `(min, max)` of the finite values, or `None` if the series
65    /// is empty or contains no finite values.
66    pub fn bounds(&self) -> Option<(f64, f64)> {
67        Some((self.min()?, self.max()?))
68    }
69}
70
71// ---------------------------------------------------------------------------
72// IntoSeries
73// ---------------------------------------------------------------------------
74
75/// Trait for types that can be converted into a [`Series`].
76///
77/// This is the primary entry-point for user data. Any function that accepts
78/// chart data should be generic over `impl IntoSeries` so that callers can
79/// pass slices, vectors, arrays, integer collections, or ranges without
80/// manual conversion.
81pub trait IntoSeries {
82    /// Converts this value into a [`Series`].
83    fn into_series(self) -> Series;
84}
85
86// -- f64 containers --------------------------------------------------------
87
88impl IntoSeries for Vec<f64> {
89    /// Zero-copy conversion from an owned `Vec<f64>`.
90    fn into_series(self) -> Series {
91        Series::new(self)
92    }
93}
94
95impl IntoSeries for &[f64] {
96    /// Clones the slice data into a new [`Series`].
97    fn into_series(self) -> Series {
98        Series::new(self.to_vec())
99    }
100}
101
102impl IntoSeries for &Vec<f64> {
103    /// Clones the vector data into a new [`Series`].
104    fn into_series(self) -> Series {
105        Series::new(self.clone())
106    }
107}
108
109impl<const N: usize> IntoSeries for [f64; N] {
110    /// Converts a fixed-size array of `f64` into a [`Series`].
111    fn into_series(self) -> Series {
112        Series::new(self.to_vec())
113    }
114}
115
116impl<const N: usize> IntoSeries for &[f64; N] {
117    /// Clones a fixed-size array reference into a [`Series`].
118    fn into_series(self) -> Series {
119        Series::new(self.to_vec())
120    }
121}
122
123// -- Identity --------------------------------------------------------------
124
125impl IntoSeries for Series {
126    /// Identity conversion — returns `self` unchanged.
127    fn into_series(self) -> Series {
128        self
129    }
130}
131
132// -- Range -----------------------------------------------------------------
133
134impl IntoSeries for std::ops::Range<i32> {
135    /// Converts an integer range into a [`Series`] of `f64` values.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use plotkit_core::series::IntoSeries;
141    ///
142    /// let s = (0..5).into_series();
143    /// assert_eq!(s.data, vec![0.0, 1.0, 2.0, 3.0, 4.0]);
144    /// ```
145    fn into_series(self) -> Series {
146        Series::new(self.map(|v| v as f64).collect())
147    }
148}
149
150// -- i32 containers --------------------------------------------------------
151
152impl IntoSeries for Vec<i32> {
153    /// Converts a `Vec<i32>` into a [`Series`] by casting each element to `f64`.
154    fn into_series(self) -> Series {
155        Series::new(self.into_iter().map(|v| v as f64).collect())
156    }
157}
158
159impl IntoSeries for &[i32] {
160    /// Converts an `i32` slice into a [`Series`] by casting each element to `f64`.
161    fn into_series(self) -> Series {
162        Series::new(self.iter().map(|&v| v as f64).collect())
163    }
164}
165
166// -- f32 containers --------------------------------------------------------
167
168impl IntoSeries for Vec<f32> {
169    /// Converts a `Vec<f32>` into a [`Series`] by casting each element to `f64`.
170    fn into_series(self) -> Series {
171        Series::new(self.into_iter().map(|v| v as f64).collect())
172    }
173}
174
175impl IntoSeries for &[f32] {
176    /// Converts an `f32` slice into a [`Series`] by casting each element to `f64`.
177    fn into_series(self) -> Series {
178        Series::new(self.iter().map(|&v| v as f64).collect())
179    }
180}
181
182// ---------------------------------------------------------------------------
183// Categories
184// ---------------------------------------------------------------------------
185
186/// Categorical labels for bar charts, pie charts, and other discrete-axis
187/// visualizations.
188///
189/// Use [`IntoCategories`] to convert common string collections into this
190/// type automatically.
191#[derive(Debug, Clone)]
192pub struct Categories {
193    /// The category labels.
194    pub labels: Vec<String>,
195}
196
197impl Categories {
198    /// Creates a new `Categories` from a vector of label strings.
199    pub fn new(labels: Vec<String>) -> Self {
200        Self { labels }
201    }
202
203    /// Returns the number of categories.
204    pub fn len(&self) -> usize {
205        self.labels.len()
206    }
207
208    /// Returns `true` if there are no categories.
209    pub fn is_empty(&self) -> bool {
210        self.labels.is_empty()
211    }
212}
213
214// ---------------------------------------------------------------------------
215// IntoCategories
216// ---------------------------------------------------------------------------
217
218/// Trait for types that can be converted into [`Categories`].
219pub trait IntoCategories {
220    /// Converts this value into [`Categories`].
221    fn into_categories(self) -> Categories;
222}
223
224impl IntoCategories for &[&str] {
225    /// Converts a slice of string slices into [`Categories`].
226    fn into_categories(self) -> Categories {
227        Categories::new(self.iter().map(|s| (*s).to_owned()).collect())
228    }
229}
230
231impl IntoCategories for Vec<String> {
232    /// Zero-copy conversion from an owned `Vec<String>`.
233    fn into_categories(self) -> Categories {
234        Categories::new(self)
235    }
236}
237
238impl IntoCategories for &[String] {
239    /// Clones the string slice into [`Categories`].
240    fn into_categories(self) -> Categories {
241        Categories::new(self.to_vec())
242    }
243}
244
245impl IntoCategories for Vec<&str> {
246    /// Converts a vector of string slices into [`Categories`].
247    fn into_categories(self) -> Categories {
248        Categories::new(self.into_iter().map(|s| s.to_owned()).collect())
249    }
250}
251
252// ---------------------------------------------------------------------------
253// Tests
254// ---------------------------------------------------------------------------
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn series_from_vec_f64() {
262        let s = vec![1.0, 2.0, 3.0].into_series();
263        assert_eq!(s.len(), 3);
264        assert_eq!(s.data, vec![1.0, 2.0, 3.0]);
265    }
266
267    #[test]
268    fn series_from_slice_f64() {
269        let data: &[f64] = &[4.0, 5.0];
270        let s = data.into_series();
271        assert_eq!(s.data, vec![4.0, 5.0]);
272    }
273
274    #[test]
275    fn series_from_vec_ref() {
276        let v = vec![1.0, 2.0];
277        let s = (&v).into_series();
278        assert_eq!(s.data, vec![1.0, 2.0]);
279    }
280
281    #[test]
282    fn series_from_array() {
283        let s = [10.0, 20.0, 30.0].into_series();
284        assert_eq!(s.data, vec![10.0, 20.0, 30.0]);
285    }
286
287    #[test]
288    fn series_from_array_ref() {
289        let arr = [7.0, 8.0];
290        let s = (&arr).into_series();
291        assert_eq!(s.data, vec![7.0, 8.0]);
292    }
293
294    #[test]
295    fn series_identity() {
296        let original = Series::new(vec![1.0]);
297        let s = original.into_series();
298        assert_eq!(s.data, vec![1.0]);
299    }
300
301    #[test]
302    fn series_from_range() {
303        let s = (0..4).into_series();
304        assert_eq!(s.data, vec![0.0, 1.0, 2.0, 3.0]);
305    }
306
307    #[test]
308    fn series_from_vec_i32() {
309        let s = vec![1i32, 2, 3].into_series();
310        assert_eq!(s.data, vec![1.0, 2.0, 3.0]);
311    }
312
313    #[test]
314    fn series_from_slice_i32() {
315        let data: &[i32] = &[10, 20];
316        let s = data.into_series();
317        assert_eq!(s.data, vec![10.0, 20.0]);
318    }
319
320    #[test]
321    fn series_from_vec_f32() {
322        let s = vec![1.5f32, 2.5].into_series();
323        assert_eq!(s.data, vec![1.5f64, 2.5]);
324    }
325
326    #[test]
327    fn series_from_slice_f32() {
328        let data: &[f32] = &[0.1, 0.2];
329        let s = data.into_series();
330        assert_eq!(s.len(), 2);
331    }
332
333    #[test]
334    fn series_empty() {
335        let s = Series::new(vec![]);
336        assert!(s.is_empty());
337        assert_eq!(s.min(), None);
338        assert_eq!(s.max(), None);
339        assert_eq!(s.bounds(), None);
340    }
341
342    #[test]
343    fn series_min_max_bounds() {
344        let s = vec![3.0, 1.0, 4.0, 1.5, 9.0].into_series();
345        assert_eq!(s.min(), Some(1.0));
346        assert_eq!(s.max(), Some(9.0));
347        assert_eq!(s.bounds(), Some((1.0, 9.0)));
348    }
349
350    #[test]
351    fn series_min_max_ignores_nan() {
352        let s = vec![f64::NAN, 2.0, f64::INFINITY, 1.0, f64::NEG_INFINITY].into_series();
353        assert_eq!(s.min(), Some(1.0));
354        assert_eq!(s.max(), Some(2.0));
355    }
356
357    #[test]
358    fn categories_from_str_slice() {
359        let cats: &[&str] = &["a", "b", "c"];
360        let c = cats.into_categories();
361        assert_eq!(c.labels, vec!["a", "b", "c"]);
362    }
363
364    #[test]
365    fn categories_from_vec_string() {
366        let c = vec!["x".to_string(), "y".to_string()].into_categories();
367        assert_eq!(c.labels, vec!["x", "y"]);
368    }
369
370    #[test]
371    fn categories_from_string_slice() {
372        let v = vec!["p".to_string(), "q".to_string()];
373        let c = v.as_slice().into_categories();
374        assert_eq!(c.labels, vec!["p", "q"]);
375    }
376
377    #[test]
378    fn categories_from_vec_str_ref() {
379        let c = vec!["foo", "bar"].into_categories();
380        assert_eq!(c.labels, vec!["foo", "bar"]);
381        assert_eq!(c.len(), 2);
382        assert!(!c.is_empty());
383    }
384}