use super::chart_builder::LegendPosition;
use crate::graphics::Color;
use crate::text::Font;
#[derive(Debug, Clone)]
pub struct DataSeries {
pub name: String,
pub data: Vec<(f64, f64)>,
pub color: Color,
pub line_width: f64,
pub show_markers: bool,
pub marker_size: f64,
pub fill_area: bool,
pub fill_color: Option<Color>,
}
impl DataSeries {
pub fn new<S: Into<String>>(name: S, color: Color) -> Self {
Self {
name: name.into(),
data: Vec::new(),
color,
line_width: 2.0,
show_markers: true,
marker_size: 4.0,
fill_area: false,
fill_color: None,
}
}
pub fn y_data(mut self, values: Vec<f64>) -> Self {
self.data = values
.into_iter()
.enumerate()
.map(|(i, y)| (i as f64, y))
.collect();
self
}
pub fn xy_data(mut self, data: Vec<(f64, f64)>) -> Self {
self.data = data;
self
}
pub fn line_style(mut self, width: f64) -> Self {
self.line_width = width;
self
}
pub fn markers(mut self, show: bool, size: f64) -> Self {
self.show_markers = show;
self.marker_size = size;
self
}
pub fn fill_area(mut self, fill_color: Option<Color>) -> Self {
self.fill_area = true;
self.fill_color = fill_color;
self
}
pub fn x_range(&self) -> (f64, f64) {
if self.data.is_empty() {
return (0.0, 1.0);
}
let xs: Vec<f64> = self.data.iter().map(|(x, _)| *x).collect();
let min_x = xs.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_x = xs.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
(min_x, max_x)
}
pub fn y_range(&self) -> (f64, f64) {
if self.data.is_empty() {
return (0.0, 1.0);
}
let ys: Vec<f64> = self.data.iter().map(|(_, y)| *y).collect();
let min_y = ys.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_y = ys.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
(min_y, max_y)
}
}
#[derive(Debug, Clone)]
pub struct LineChart {
pub title: String,
pub series: Vec<DataSeries>,
pub x_axis_label: String,
pub y_axis_label: String,
pub title_font: Font,
pub title_font_size: f64,
pub label_font: Font,
pub label_font_size: f64,
pub axis_font: Font,
pub axis_font_size: f64,
pub legend_position: LegendPosition,
pub background_color: Option<Color>,
pub show_grid: bool,
pub grid_color: Color,
pub axis_color: Color,
pub x_range: Option<(f64, f64)>,
pub y_range: Option<(f64, f64)>,
pub grid_lines: usize,
}
impl LineChart {
pub fn new() -> Self {
Self {
title: String::new(),
series: Vec::new(),
x_axis_label: String::new(),
y_axis_label: String::new(),
title_font: Font::HelveticaBold,
title_font_size: 16.0,
label_font: Font::Helvetica,
label_font_size: 12.0,
axis_font: Font::Helvetica,
axis_font_size: 10.0,
legend_position: LegendPosition::Right,
background_color: None,
show_grid: true,
grid_color: Color::rgb(0.9, 0.9, 0.9),
axis_color: Color::black(),
x_range: None,
y_range: None,
grid_lines: 5,
}
}
pub fn combined_x_range(&self) -> (f64, f64) {
if let Some(range) = self.x_range {
return range;
}
if self.series.is_empty() {
return (0.0, 1.0);
}
let mut min_x = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
for series in &self.series {
let (series_min, series_max) = series.x_range();
min_x = min_x.min(series_min);
max_x = max_x.max(series_max);
}
let range = max_x - min_x;
let padding = range * 0.1;
(min_x - padding, max_x + padding)
}
pub fn combined_y_range(&self) -> (f64, f64) {
if let Some(range) = self.y_range {
return range;
}
if self.series.is_empty() {
return (0.0, 1.0);
}
let mut min_y = f64::INFINITY;
let mut max_y = f64::NEG_INFINITY;
for series in &self.series {
let (series_min, series_max) = series.y_range();
min_y = min_y.min(series_min);
max_y = max_y.max(series_max);
}
let range = max_y - min_y;
let padding = range * 0.1;
(min_y - padding, max_y + padding)
}
}
impl Default for LineChart {
fn default() -> Self {
Self::new()
}
}
pub struct LineChartBuilder {
chart: LineChart,
}
impl LineChartBuilder {
pub fn new() -> Self {
Self {
chart: LineChart::new(),
}
}
pub fn title<S: Into<String>>(mut self, title: S) -> Self {
self.chart.title = title.into();
self
}
pub fn add_series(mut self, series: DataSeries) -> Self {
self.chart.series.push(series);
self
}
pub fn axis_labels<S: Into<String>>(mut self, x_label: S, y_label: S) -> Self {
self.chart.x_axis_label = x_label.into();
self.chart.y_axis_label = y_label.into();
self
}
pub fn title_font(mut self, font: Font, size: f64) -> Self {
self.chart.title_font = font;
self.chart.title_font_size = size;
self
}
pub fn label_font(mut self, font: Font, size: f64) -> Self {
self.chart.label_font = font;
self.chart.label_font_size = size;
self
}
pub fn axis_font(mut self, font: Font, size: f64) -> Self {
self.chart.axis_font = font;
self.chart.axis_font_size = size;
self
}
pub fn legend_position(mut self, position: LegendPosition) -> Self {
self.chart.legend_position = position;
self
}
pub fn background_color(mut self, color: Color) -> Self {
self.chart.background_color = Some(color);
self
}
pub fn grid(mut self, show: bool, color: Color, lines: usize) -> Self {
self.chart.show_grid = show;
self.chart.grid_color = color;
self.chart.grid_lines = lines;
self
}
pub fn x_range(mut self, min: f64, max: f64) -> Self {
self.chart.x_range = Some((min, max));
self
}
pub fn y_range(mut self, min: f64, max: f64) -> Self {
self.chart.y_range = Some((min, max));
self
}
pub fn add_simple_series<S: Into<String>>(
mut self,
name: S,
values: Vec<f64>,
color: Color,
) -> Self {
let series = DataSeries::new(name, color).y_data(values);
self.chart.series.push(series);
self
}
pub fn build(self) -> LineChart {
self.chart
}
}
impl Default for LineChartBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_data_series_creation() {
let series = DataSeries::new("Test Series", Color::blue()).y_data(vec![1.0, 2.0, 3.0]);
assert_eq!(series.name, "Test Series");
assert_eq!(series.color, Color::blue());
assert_eq!(series.data.len(), 3);
assert_eq!(series.data[0], (0.0, 1.0));
assert_eq!(series.data[2], (2.0, 3.0));
}
#[test]
fn test_data_series_ranges() {
let series = DataSeries::new("Test", Color::red()).xy_data(vec![
(0.0, 10.0),
(5.0, 20.0),
(10.0, 5.0),
]);
let (min_x, max_x) = series.x_range();
let (min_y, max_y) = series.y_range();
assert_eq!(min_x, 0.0);
assert_eq!(max_x, 10.0);
assert_eq!(min_y, 5.0);
assert_eq!(max_y, 20.0);
}
#[test]
fn test_line_chart_creation() {
let chart = LineChartBuilder::new()
.title("Test Chart")
.add_simple_series("Series 1", vec![1.0, 2.0, 3.0], Color::blue())
.add_simple_series("Series 2", vec![3.0, 2.0, 1.0], Color::red())
.build();
assert_eq!(chart.title, "Test Chart");
assert_eq!(chart.series.len(), 2);
let (min_y, max_y) = chart.combined_y_range();
assert!(min_y <= 1.0);
assert!(max_y >= 3.0);
}
#[test]
fn test_data_series_line_style() {
let series = DataSeries::new("Test", Color::blue()).line_style(3.0);
assert_eq!(series.line_width, 3.0);
}
#[test]
fn test_data_series_markers() {
let series = DataSeries::new("Test", Color::blue()).markers(false, 8.0);
assert!(!series.show_markers);
assert_eq!(series.marker_size, 8.0);
}
#[test]
fn test_data_series_fill_area() {
let series = DataSeries::new("Test", Color::blue()).fill_area(Some(Color::green()));
assert!(series.fill_area);
assert_eq!(series.fill_color, Some(Color::green()));
let series_no_color = DataSeries::new("Test2", Color::red()).fill_area(None);
assert!(series_no_color.fill_area);
assert!(series_no_color.fill_color.is_none());
}
#[test]
fn test_data_series_empty_ranges() {
let series = DataSeries::new("Empty", Color::black());
let (min_x, max_x) = series.x_range();
let (min_y, max_y) = series.y_range();
assert_eq!((min_x, max_x), (0.0, 1.0));
assert_eq!((min_y, max_y), (0.0, 1.0));
}
#[test]
fn test_line_chart_new() {
let chart = LineChart::new();
assert!(chart.title.is_empty());
assert!(chart.series.is_empty());
assert!(chart.show_grid);
assert_eq!(chart.grid_lines, 5);
}
#[test]
fn test_line_chart_default() {
let chart = LineChart::default();
assert!(chart.title.is_empty());
}
#[test]
fn test_line_chart_combined_x_range_empty() {
let chart = LineChart::new();
let (min_x, max_x) = chart.combined_x_range();
assert_eq!((min_x, max_x), (0.0, 1.0));
}
#[test]
fn test_line_chart_combined_y_range_empty() {
let chart = LineChart::new();
let (min_y, max_y) = chart.combined_y_range();
assert_eq!((min_y, max_y), (0.0, 1.0));
}
#[test]
fn test_line_chart_builder_axis_labels() {
let chart = LineChartBuilder::new()
.axis_labels("X Axis", "Y Axis")
.build();
assert_eq!(chart.x_axis_label, "X Axis");
assert_eq!(chart.y_axis_label, "Y Axis");
}
#[test]
fn test_line_chart_builder_title_font() {
let chart = LineChartBuilder::new()
.title_font(Font::CourierBold, 20.0)
.build();
assert_eq!(chart.title_font, Font::CourierBold);
assert_eq!(chart.title_font_size, 20.0);
}
#[test]
fn test_line_chart_builder_label_font() {
let chart = LineChartBuilder::new()
.label_font(Font::TimesBold, 14.0)
.build();
assert_eq!(chart.label_font, Font::TimesBold);
assert_eq!(chart.label_font_size, 14.0);
}
#[test]
fn test_line_chart_builder_axis_font() {
let chart = LineChartBuilder::new()
.axis_font(Font::Courier, 8.0)
.build();
assert_eq!(chart.axis_font, Font::Courier);
assert_eq!(chart.axis_font_size, 8.0);
}
#[test]
fn test_line_chart_builder_legend_position() {
let chart = LineChartBuilder::new()
.legend_position(LegendPosition::Bottom)
.build();
assert_eq!(chart.legend_position, LegendPosition::Bottom);
}
#[test]
fn test_line_chart_builder_background_color() {
let chart = LineChartBuilder::new()
.background_color(Color::white())
.build();
assert_eq!(chart.background_color, Some(Color::white()));
}
#[test]
fn test_line_chart_builder_grid() {
let chart = LineChartBuilder::new()
.grid(false, Color::gray(0.5), 10)
.build();
assert!(!chart.show_grid);
assert_eq!(chart.grid_color, Color::gray(0.5));
assert_eq!(chart.grid_lines, 10);
}
#[test]
fn test_line_chart_builder_x_range() {
let chart = LineChartBuilder::new().x_range(0.0, 100.0).build();
assert_eq!(chart.x_range, Some((0.0, 100.0)));
let (min_x, max_x) = chart.combined_x_range();
assert_eq!((min_x, max_x), (0.0, 100.0));
}
#[test]
fn test_line_chart_builder_y_range() {
let chart = LineChartBuilder::new().y_range(-10.0, 50.0).build();
assert_eq!(chart.y_range, Some((-10.0, 50.0)));
let (min_y, max_y) = chart.combined_y_range();
assert_eq!((min_y, max_y), (-10.0, 50.0));
}
#[test]
fn test_line_chart_builder_add_series() {
let series = DataSeries::new("Custom", Color::green()).y_data(vec![1.0, 2.0]);
let chart = LineChartBuilder::new().add_series(series).build();
assert_eq!(chart.series.len(), 1);
assert_eq!(chart.series[0].name, "Custom");
}
#[test]
fn test_line_chart_builder_default() {
let builder = LineChartBuilder::default();
let chart = builder.build();
assert!(chart.title.is_empty());
}
#[test]
fn test_data_series_clone() {
let series = DataSeries::new("Test", Color::blue())
.y_data(vec![1.0, 2.0])
.markers(true, 5.0);
let cloned = series.clone();
assert_eq!(series.name, cloned.name);
assert_eq!(series.data, cloned.data);
assert_eq!(series.marker_size, cloned.marker_size);
}
#[test]
fn test_line_chart_clone() {
let chart = LineChartBuilder::new()
.title("Clone Test")
.add_simple_series("S1", vec![1.0], Color::red())
.build();
let cloned = chart.clone();
assert_eq!(chart.title, cloned.title);
assert_eq!(chart.series.len(), cloned.series.len());
}
}