Skip to main content

nova_plot/
data.rs

1//! Data source handling for charts.
2//!
3//! Supports CSV and JSON data formats.
4
5use crate::error::{Error, Result};
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8
9/// A data source for charts.
10#[derive(Debug, Clone)]
11pub enum DataSource {
12    /// Tabular data with columns.
13    Table(Table),
14    /// Series data for multi-line charts.
15    Series(Vec<Series>),
16}
17
18impl DataSource {
19    /// Load data from a CSV file.
20    ///
21    /// # Errors
22    ///
23    /// Returns an error if the file cannot be read or parsed.
24    pub fn from_csv(path: impl AsRef<Path>) -> Result<Self> {
25        let path = path.as_ref();
26        let mut reader = csv::Reader::from_path(path)?;
27
28        let headers: Vec<String> = reader.headers()?.iter().map(String::from).collect();
29
30        let mut rows = Vec::new();
31        for result in reader.records() {
32            let record = result?;
33            let row: Vec<Value> = record.iter().map(Value::parse).collect();
34            rows.push(row);
35        }
36
37        Ok(Self::Table(Table { headers, rows }))
38    }
39
40    /// Load data from a CSV string.
41    ///
42    /// # Errors
43    ///
44    /// Returns an error if parsing fails.
45    pub fn from_csv_string(content: &str) -> Result<Self> {
46        let mut reader = csv::Reader::from_reader(content.as_bytes());
47
48        let headers: Vec<String> = reader.headers()?.iter().map(String::from).collect();
49
50        let mut rows = Vec::new();
51        for result in reader.records() {
52            let record = result?;
53            let row: Vec<Value> = record.iter().map(Value::parse).collect();
54            rows.push(row);
55        }
56
57        Ok(Self::Table(Table { headers, rows }))
58    }
59
60    /// Load data from JSON.
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if parsing fails.
65    pub fn from_json(content: &str) -> Result<Self> {
66        let value: serde_json::Value = serde_json::from_str(content)?;
67        Self::from_json_value(value)
68    }
69
70    /// Load data from a JSON value.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if the format is invalid.
75    pub fn from_json_value(value: serde_json::Value) -> Result<Self> {
76        match value {
77            serde_json::Value::Array(arr) => {
78                if arr.is_empty() {
79                    return Err(Error::NoData);
80                }
81
82                // Check if it's an array of objects (table format)
83                if arr[0].is_object() {
84                    let headers: Vec<String> =
85                        arr[0].as_object().unwrap().keys().cloned().collect();
86
87                    let rows: Vec<Vec<Value>> = arr
88                        .iter()
89                        .filter_map(|obj| {
90                            obj.as_object().map(|o| {
91                                headers
92                                    .iter()
93                                    .map(|h| Value::from_json(o.get(h).cloned()))
94                                    .collect()
95                            })
96                        })
97                        .collect();
98
99                    Ok(Self::Table(Table { headers, rows }))
100                } else {
101                    // Array of values (single series)
102                    let values: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
103
104                    Ok(Self::Series(vec![Series {
105                        name: "data".to_string(),
106                        values,
107                    }]))
108                }
109            }
110            _ => Err(Error::InvalidData {
111                message: "expected JSON array".to_string(),
112            }),
113        }
114    }
115
116    /// Create data from inline points.
117    #[must_use]
118    pub fn from_points(points: Vec<(f64, f64)>) -> Self {
119        let headers = vec!["x".to_string(), "y".to_string()];
120        let rows: Vec<Vec<Value>> = points
121            .into_iter()
122            .map(|(x, y)| vec![Value::Number(x), Value::Number(y)])
123            .collect();
124
125        Self::Table(Table { headers, rows })
126    }
127
128    /// Get as table if possible.
129    #[must_use]
130    pub fn as_table(&self) -> Option<&Table> {
131        match self {
132            Self::Table(t) => Some(t),
133            _ => None,
134        }
135    }
136
137    /// Get as series if possible.
138    #[must_use]
139    pub fn as_series(&self) -> Option<&[Series]> {
140        match self {
141            Self::Series(s) => Some(s),
142            _ => None,
143        }
144    }
145}
146
147/// Tabular data with named columns.
148#[derive(Debug, Clone)]
149pub struct Table {
150    /// Column headers.
151    pub headers: Vec<String>,
152    /// Row data.
153    pub rows: Vec<Vec<Value>>,
154}
155
156impl Table {
157    /// Get the column index by name.
158    #[must_use]
159    pub fn column_index(&self, name: &str) -> Option<usize> {
160        self.headers.iter().position(|h| h == name)
161    }
162
163    /// Get a column's values as floats.
164    #[must_use]
165    pub fn column_as_f64(&self, name: &str) -> Option<Vec<f64>> {
166        let idx = self.column_index(name)?;
167        Some(
168            self.rows
169                .iter()
170                .filter_map(|row| row.get(idx).and_then(Value::as_f64))
171                .collect(),
172        )
173    }
174
175    /// Get a column's values as strings.
176    #[must_use]
177    pub fn column_as_str(&self, name: &str) -> Option<Vec<String>> {
178        let idx = self.column_index(name)?;
179        Some(
180            self.rows
181                .iter()
182                .filter_map(|row| row.get(idx).map(Value::to_string))
183                .collect(),
184        )
185    }
186
187    /// Get the number of rows.
188    #[must_use]
189    pub fn row_count(&self) -> usize {
190        self.rows.len()
191    }
192
193    /// Get the number of columns.
194    #[must_use]
195    pub fn column_count(&self) -> usize {
196        self.headers.len()
197    }
198}
199
200/// A named data series.
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct Series {
203    /// Series name.
204    pub name: String,
205    /// Data values.
206    pub values: Vec<f64>,
207}
208
209/// A data value (number or string).
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub enum Value {
212    /// Numeric value.
213    Number(f64),
214    /// String value.
215    String(String),
216    /// Null/missing value.
217    Null,
218}
219
220impl Value {
221    /// Parse a string into a value.
222    #[must_use]
223    pub fn parse(s: &str) -> Self {
224        if s.is_empty() {
225            Self::Null
226        } else if let Ok(n) = s.parse::<f64>() {
227            Self::Number(n)
228        } else {
229            Self::String(s.to_string())
230        }
231    }
232
233    /// Convert from JSON value.
234    #[must_use]
235    pub fn from_json(value: Option<serde_json::Value>) -> Self {
236        match value {
237            Some(serde_json::Value::Number(n)) => Self::Number(n.as_f64().unwrap_or(0.0)),
238            Some(serde_json::Value::String(s)) => Self::String(s),
239            Some(serde_json::Value::Null) | None => Self::Null,
240            Some(v) => Self::String(v.to_string()),
241        }
242    }
243
244    /// Get as f64 if numeric.
245    #[must_use]
246    pub fn as_f64(&self) -> Option<f64> {
247        match self {
248            Self::Number(n) => Some(*n),
249            Self::String(s) => s.parse().ok(),
250            Self::Null => None,
251        }
252    }
253
254    /// Check if null.
255    #[must_use]
256    pub fn is_null(&self) -> bool {
257        matches!(self, Self::Null)
258    }
259}
260
261impl std::fmt::Display for Value {
262    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263        match self {
264            Self::Number(n) => write!(f, "{n}"),
265            Self::String(s) => write!(f, "{s}"),
266            Self::Null => write!(f, ""),
267        }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn parse_csv_string() {
277        let csv = "x,y\n1,10\n2,20\n3,30";
278        let data = DataSource::from_csv_string(csv).unwrap();
279
280        let table = data.as_table().unwrap();
281        assert_eq!(table.headers, vec!["x", "y"]);
282        assert_eq!(table.row_count(), 3);
283    }
284
285    #[test]
286    fn table_column_access() {
287        let csv = "name,value\nalpha,100\nbeta,200";
288        let data = DataSource::from_csv_string(csv).unwrap();
289        let table = data.as_table().unwrap();
290
291        assert_eq!(table.column_index("name"), Some(0));
292        assert_eq!(table.column_index("value"), Some(1));
293        assert_eq!(table.column_index("missing"), None);
294
295        let values = table.column_as_f64("value").unwrap();
296        assert_eq!(values, vec![100.0, 200.0]);
297    }
298
299    #[test]
300    fn from_json_array_of_objects() {
301        let json = r#"[
302            {"x": 1, "y": 10},
303            {"x": 2, "y": 20}
304        ]"#;
305
306        let data = DataSource::from_json(json).unwrap();
307        let table = data.as_table().unwrap();
308        assert_eq!(table.row_count(), 2);
309    }
310
311    #[test]
312    fn from_points() {
313        let data = DataSource::from_points(vec![(1.0, 10.0), (2.0, 20.0)]);
314        let table = data.as_table().unwrap();
315
316        assert_eq!(table.headers, vec!["x", "y"]);
317        assert_eq!(table.row_count(), 2);
318    }
319
320    #[test]
321    fn value_parsing() {
322        assert!(matches!(Value::parse("42"), Value::Number(n) if (n - 42.0).abs() < f64::EPSILON));
323        assert!(matches!(Value::parse("hello"), Value::String(_)));
324        assert!(matches!(Value::parse(""), Value::Null));
325    }
326
327    #[test]
328    fn value_as_f64() {
329        assert_eq!(Value::Number(42.0).as_f64(), Some(42.0));
330        assert_eq!(Value::String("42".to_string()).as_f64(), Some(42.0));
331        assert_eq!(Value::String("hello".to_string()).as_f64(), None);
332        assert_eq!(Value::Null.as_f64(), None);
333    }
334}