1use crate::data::DataSource;
4use crate::error::{Error, Result};
5use crate::render::Renderer;
6use crate::style::ChartStyle;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum ChartType {
11 #[default]
13 Line,
14 Bar,
16 Scatter,
18 Pie,
20 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#[derive(Debug, Clone)]
43pub struct Chart {
44 pub chart_type: ChartType,
46
47 pub title: Option<String>,
49
50 pub x_label: Option<String>,
52
53 pub y_label: Option<String>,
55
56 pub x_column: Option<String>,
58
59 pub y_column: Option<String>,
61
62 pub data: Option<DataSource>,
64
65 pub width: f64,
67 pub height: f64,
68
69 pub style: ChartStyle,
71}
72
73impl Chart {
74 #[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 #[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 #[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 #[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 #[must_use]
114 pub fn with_data(mut self, data: DataSource) -> Self {
115 self.data = Some(data);
116 self
117 }
118
119 #[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 #[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 #[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 #[must_use]
143 pub fn with_style(mut self, style: ChartStyle) -> Self {
144 self.style = style;
145 self
146 }
147
148 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 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")); }
221}