Skip to main content

nova_plot/
chart.rs

1//! Chart configuration and building.
2
3use crate::data::DataSource;
4use crate::error::{Error, Result};
5use crate::render::Renderer;
6use crate::style::ChartStyle;
7
8/// Type of chart to render.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum ChartType {
11    /// Line chart.
12    #[default]
13    Line,
14    /// Bar chart.
15    Bar,
16    /// Scatter plot.
17    Scatter,
18    /// Pie chart.
19    Pie,
20    /// Area chart.
21    Area,
22}
23
24impl std::str::FromStr for ChartType {
25    type Err = Error;
26
27    fn from_str(s: &str) -> Result<Self> {
28        match s.to_lowercase().as_str() {
29            "line" => Ok(Self::Line),
30            "bar" => Ok(Self::Bar),
31            "scatter" => Ok(Self::Scatter),
32            "pie" => Ok(Self::Pie),
33            "area" => Ok(Self::Area),
34            _ => Err(Error::InvalidConfig {
35                message: format!("unknown chart type: {s}"),
36            }),
37        }
38    }
39}
40
41/// A chart configuration.
42#[derive(Debug, Clone)]
43pub struct Chart {
44    /// Chart type.
45    pub chart_type: ChartType,
46
47    /// Chart title.
48    pub title: Option<String>,
49
50    /// X-axis label.
51    pub x_label: Option<String>,
52
53    /// Y-axis label.
54    pub y_label: Option<String>,
55
56    /// X-axis column name.
57    pub x_column: Option<String>,
58
59    /// Y-axis column name.
60    pub y_column: Option<String>,
61
62    /// Data source.
63    pub data: Option<DataSource>,
64
65    /// Chart dimensions.
66    pub width: f64,
67    pub height: f64,
68
69    /// Visual style.
70    pub style: ChartStyle,
71}
72
73impl Chart {
74    /// Create a new chart with default settings.
75    #[must_use]
76    pub fn new(chart_type: ChartType) -> Self {
77        Self {
78            chart_type,
79            title: None,
80            x_label: None,
81            y_label: None,
82            x_column: None,
83            y_column: None,
84            data: None,
85            width: 600.0,
86            height: 400.0,
87            style: ChartStyle::default(),
88        }
89    }
90
91    /// Set the chart title.
92    #[must_use]
93    pub fn with_title(mut self, title: impl Into<String>) -> Self {
94        self.title = Some(title.into());
95        self
96    }
97
98    /// Set the X-axis label.
99    #[must_use]
100    pub fn with_x_label(mut self, label: impl Into<String>) -> Self {
101        self.x_label = Some(label.into());
102        self
103    }
104
105    /// Set the Y-axis label.
106    #[must_use]
107    pub fn with_y_label(mut self, label: impl Into<String>) -> Self {
108        self.y_label = Some(label.into());
109        self
110    }
111
112    /// Set the data source.
113    #[must_use]
114    pub fn with_data(mut self, data: DataSource) -> Self {
115        self.data = Some(data);
116        self
117    }
118
119    /// Set the X column.
120    #[must_use]
121    pub fn with_x_column(mut self, column: impl Into<String>) -> Self {
122        self.x_column = Some(column.into());
123        self
124    }
125
126    /// Set the Y column.
127    #[must_use]
128    pub fn with_y_column(mut self, column: impl Into<String>) -> Self {
129        self.y_column = Some(column.into());
130        self
131    }
132
133    /// Set the chart dimensions.
134    #[must_use]
135    pub fn with_size(mut self, width: f64, height: f64) -> Self {
136        self.width = width;
137        self.height = height;
138        self
139    }
140
141    /// Set the visual style.
142    #[must_use]
143    pub fn with_style(mut self, style: ChartStyle) -> Self {
144        self.style = style;
145        self
146    }
147
148    /// Render the chart to SVG.
149    ///
150    /// # Errors
151    ///
152    /// Returns an error if no data is provided or rendering fails.
153    pub fn render(&self) -> Result<String> {
154        let data = self.data.as_ref().ok_or(Error::NoData)?;
155        let renderer = Renderer::new(self);
156        renderer.render(data)
157    }
158
159    /// Render to SVG and save to file.
160    ///
161    /// # Errors
162    ///
163    /// Returns an error if rendering or writing fails.
164    pub fn save(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
165        let svg = self.render()?;
166        std::fs::write(path, svg)?;
167        Ok(())
168    }
169}
170
171impl Default for Chart {
172    fn default() -> Self {
173        Self::new(ChartType::Line)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use std::str::FromStr;
181
182    #[test]
183    fn chart_builder_pattern() {
184        let chart = Chart::new(ChartType::Line)
185            .with_title("Test Chart")
186            .with_x_label("X Axis")
187            .with_y_label("Y Axis")
188            .with_size(800.0, 600.0);
189
190        assert_eq!(chart.chart_type, ChartType::Line);
191        assert_eq!(chart.title, Some("Test Chart".to_string()));
192        assert_eq!(chart.width, 800.0);
193        assert_eq!(chart.height, 600.0);
194    }
195
196    #[test]
197    fn chart_type_from_str() {
198        assert_eq!(ChartType::from_str("line").unwrap(), ChartType::Line);
199        assert_eq!(ChartType::from_str("BAR").unwrap(), ChartType::Bar);
200        assert_eq!(ChartType::from_str("Scatter").unwrap(), ChartType::Scatter);
201        assert!(ChartType::from_str("invalid").is_err());
202    }
203
204    #[test]
205    fn render_without_data_fails() {
206        let chart = Chart::new(ChartType::Line);
207        assert!(matches!(chart.render(), Err(Error::NoData)));
208    }
209
210    #[test]
211    fn render_with_data() {
212        let data = DataSource::from_points(vec![(1.0, 10.0), (2.0, 20.0), (3.0, 15.0)]);
213        let chart = Chart::new(ChartType::Line)
214            .with_title("Test")
215            .with_data(data);
216
217        let svg = chart.render().unwrap();
218        assert!(svg.starts_with("<svg"));
219        assert!(svg.contains("Test")); // Title should be in SVG
220    }
221}