use crate::data::DataSource;
use crate::error::{Error, Result};
use crate::render::Renderer;
use crate::style::ChartStyle;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ChartType {
#[default]
Line,
Bar,
Scatter,
Pie,
Area,
}
impl std::str::FromStr for ChartType {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"line" => Ok(Self::Line),
"bar" => Ok(Self::Bar),
"scatter" => Ok(Self::Scatter),
"pie" => Ok(Self::Pie),
"area" => Ok(Self::Area),
_ => Err(Error::InvalidConfig {
message: format!("unknown chart type: {s}"),
}),
}
}
}
#[derive(Debug, Clone)]
pub struct Chart {
pub chart_type: ChartType,
pub title: Option<String>,
pub x_label: Option<String>,
pub y_label: Option<String>,
pub x_column: Option<String>,
pub y_column: Option<String>,
pub data: Option<DataSource>,
pub width: f64,
pub height: f64,
pub style: ChartStyle,
}
impl Chart {
#[must_use]
pub fn new(chart_type: ChartType) -> Self {
Self {
chart_type,
title: None,
x_label: None,
y_label: None,
x_column: None,
y_column: None,
data: None,
width: 600.0,
height: 400.0,
style: ChartStyle::default(),
}
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn with_x_label(mut self, label: impl Into<String>) -> Self {
self.x_label = Some(label.into());
self
}
#[must_use]
pub fn with_y_label(mut self, label: impl Into<String>) -> Self {
self.y_label = Some(label.into());
self
}
#[must_use]
pub fn with_data(mut self, data: DataSource) -> Self {
self.data = Some(data);
self
}
#[must_use]
pub fn with_x_column(mut self, column: impl Into<String>) -> Self {
self.x_column = Some(column.into());
self
}
#[must_use]
pub fn with_y_column(mut self, column: impl Into<String>) -> Self {
self.y_column = Some(column.into());
self
}
#[must_use]
pub fn with_size(mut self, width: f64, height: f64) -> Self {
self.width = width;
self.height = height;
self
}
#[must_use]
pub fn with_style(mut self, style: ChartStyle) -> Self {
self.style = style;
self
}
pub fn render(&self) -> Result<String> {
let data = self.data.as_ref().ok_or(Error::NoData)?;
let renderer = Renderer::new(self);
renderer.render(data)
}
pub fn save(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
let svg = self.render()?;
std::fs::write(path, svg)?;
Ok(())
}
}
impl Default for Chart {
fn default() -> Self {
Self::new(ChartType::Line)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn chart_builder_pattern() {
let chart = Chart::new(ChartType::Line)
.with_title("Test Chart")
.with_x_label("X Axis")
.with_y_label("Y Axis")
.with_size(800.0, 600.0);
assert_eq!(chart.chart_type, ChartType::Line);
assert_eq!(chart.title, Some("Test Chart".to_string()));
assert_eq!(chart.width, 800.0);
assert_eq!(chart.height, 600.0);
}
#[test]
fn chart_type_from_str() {
assert_eq!(ChartType::from_str("line").unwrap(), ChartType::Line);
assert_eq!(ChartType::from_str("BAR").unwrap(), ChartType::Bar);
assert_eq!(ChartType::from_str("Scatter").unwrap(), ChartType::Scatter);
assert!(ChartType::from_str("invalid").is_err());
}
#[test]
fn render_without_data_fails() {
let chart = Chart::new(ChartType::Line);
assert!(matches!(chart.render(), Err(Error::NoData)));
}
#[test]
fn render_with_data() {
let data = DataSource::from_points(vec![(1.0, 10.0), (2.0, 20.0), (3.0, 15.0)]);
let chart = Chart::new(ChartType::Line)
.with_title("Test")
.with_data(data);
let svg = chart.render().unwrap();
assert!(svg.starts_with("<svg"));
assert!(svg.contains("Test")); }
}