use crate::{
chart::Chart,
error::{ChartError, Result},
model::ChartModel,
option::{
AxisOption, ChartOption, ColorOption, DataPoint, GridOption, LegendOption,
LineSeriesOption, RadarOption, SeriesOption, TextStyleOption, TitleOption,
},
theme::{Theme, ThemeRegistry},
};
#[derive(Debug, Clone)]
pub struct ChartBuilder {
theme_registry: ThemeRegistry,
option: ChartOption,
}
impl Default for ChartBuilder {
fn default() -> Self {
Self::new()
}
}
impl ChartBuilder {
pub fn new() -> Self {
Self {
theme_registry: ThemeRegistry::new(),
option: ChartOption::default(),
}
}
pub fn from_option(option: ChartOption) -> Self {
Self {
theme_registry: ThemeRegistry::new(),
option,
}
}
pub fn from_option_json(option: &str) -> Result<Self> {
Ok(Self {
theme_registry: ThemeRegistry::new(),
option: serde_json::from_str(option)?,
})
}
pub fn register_theme(mut self, theme: Theme) -> Self {
self.theme_registry.register(theme);
self
}
pub fn with_theme(mut self, theme: Theme) -> Self {
self.option.theme = Some(theme.name.clone());
self.theme_registry.register(theme);
self
}
pub fn with_title(mut self, title: TitleOption) -> Self {
self.option.title = Some(title);
self
}
pub fn with_legend(mut self, legend: LegendOption) -> Self {
self.option.legend = Some(legend);
self
}
pub fn with_grid(mut self, grid: GridOption) -> Self {
self.option.grid.push(grid);
self
}
pub fn with_x_axis(mut self, axis: AxisOption) -> Self {
self.option.x_axis.push(axis);
self
}
pub fn with_y_axis(mut self, axis: AxisOption) -> Self {
self.option.y_axis.push(axis);
self
}
pub fn with_series(mut self, series: SeriesOption) -> Self {
self.option.series.push(series);
self
}
pub fn add_function(
mut self,
name: impl Into<String>,
range: std::ops::RangeInclusive<f64>,
steps: usize,
f: impl Fn(f64) -> f64,
) -> Self {
let data = function_data(range, steps, f);
self.option.series.push(SeriesOption::Line(
LineSeriesOption::new(name, data).smooth(true),
));
self
}
pub fn with_radar(mut self, radar: RadarOption) -> Self {
self.option.radar = Some(radar);
self
}
pub fn with_color(mut self, colors: Vec<ColorOption>) -> Self {
self.option.color = Some(colors);
self
}
pub fn with_background_color(mut self, color: ColorOption) -> Self {
self.option.background_color = Some(color);
self
}
pub fn with_text_style(mut self, style: TextStyleOption) -> Self {
self.option.text_style = Some(style);
self
}
pub fn build_model(self) -> Result<ChartModel> {
let theme =
match self.option.theme.as_deref() {
Some(name) => self.theme_registry.get(name).cloned().ok_or_else(|| {
ChartError::ThemeNotFound(format!("Theme not found: {}", name))
})?,
None => Theme::echarts(),
};
ChartModel::new(self.option, theme)
}
pub fn build(self, width: u32, height: u32) -> Result<Chart> {
let model = self.build_model()?;
Ok(Chart::new(model, width, height))
}
pub fn render_to_image(self, width: u32, height: u32, path: &str) -> Result<()> {
self.build(width, height)?.render_to_image(path)
}
pub fn render_to_svg(self, width: u32, height: u32, path: &str) -> Result<()> {
self.build(width, height)?.render_to_svg(path)
}
pub fn render_png(self, width: u32, height: u32) -> Result<Vec<u8>> {
self.build(width, height)?.render_png()
}
pub fn render_svg(self, width: u32, height: u32) -> Result<String> {
self.build(width, height)?.render_svg()
}
}
pub fn function_data(
range: std::ops::RangeInclusive<f64>,
steps: usize,
f: impl Fn(f64) -> f64,
) -> Vec<DataPoint> {
let start = *range.start();
let end = *range.end();
let step = if steps > 0 {
(end - start) / steps as f64
} else {
end - start
};
(0..=steps)
.map(|i| {
let x = start + i as f64 * step;
DataPoint::from((x, f(x)))
})
.collect()
}