1use super::chart_builder::LegendPosition;
4use crate::graphics::Color;
5use crate::text::Font;
6
7#[derive(Debug, Clone)]
9pub struct DataSeries {
10 pub name: String,
12 pub data: Vec<(f64, f64)>,
14 pub color: Color,
16 pub line_width: f64,
18 pub show_markers: bool,
20 pub marker_size: f64,
22 pub fill_area: bool,
24 pub fill_color: Option<Color>,
26}
27
28impl DataSeries {
29 pub fn new<S: Into<String>>(name: S, color: Color) -> Self {
31 Self {
32 name: name.into(),
33 data: Vec::new(),
34 color,
35 line_width: 2.0,
36 show_markers: true,
37 marker_size: 4.0,
38 fill_area: false,
39 fill_color: None,
40 }
41 }
42
43 pub fn y_data(mut self, values: Vec<f64>) -> Self {
45 self.data = values
46 .into_iter()
47 .enumerate()
48 .map(|(i, y)| (i as f64, y))
49 .collect();
50 self
51 }
52
53 pub fn xy_data(mut self, data: Vec<(f64, f64)>) -> Self {
55 self.data = data;
56 self
57 }
58
59 pub fn line_style(mut self, width: f64) -> Self {
61 self.line_width = width;
62 self
63 }
64
65 pub fn markers(mut self, show: bool, size: f64) -> Self {
67 self.show_markers = show;
68 self.marker_size = size;
69 self
70 }
71
72 pub fn fill_area(mut self, fill_color: Option<Color>) -> Self {
74 self.fill_area = true;
75 self.fill_color = fill_color;
76 self
77 }
78
79 pub fn x_range(&self) -> (f64, f64) {
81 if self.data.is_empty() {
82 return (0.0, 1.0);
83 }
84
85 let xs: Vec<f64> = self.data.iter().map(|(x, _)| *x).collect();
86 let min_x = xs.iter().fold(f64::INFINITY, |a, &b| a.min(b));
87 let max_x = xs.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
88
89 (min_x, max_x)
90 }
91
92 pub fn y_range(&self) -> (f64, f64) {
94 if self.data.is_empty() {
95 return (0.0, 1.0);
96 }
97
98 let ys: Vec<f64> = self.data.iter().map(|(_, y)| *y).collect();
99 let min_y = ys.iter().fold(f64::INFINITY, |a, &b| a.min(b));
100 let max_y = ys.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
101
102 (min_y, max_y)
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct LineChart {
109 pub title: String,
111 pub series: Vec<DataSeries>,
113 pub x_axis_label: String,
115 pub y_axis_label: String,
117 pub title_font: Font,
119 pub title_font_size: f64,
120 pub label_font: Font,
122 pub label_font_size: f64,
123 pub axis_font: Font,
125 pub axis_font_size: f64,
126 pub legend_position: LegendPosition,
128 pub background_color: Option<Color>,
130 pub show_grid: bool,
132 pub grid_color: Color,
134 pub axis_color: Color,
136 pub x_range: Option<(f64, f64)>,
138 pub y_range: Option<(f64, f64)>,
140 pub grid_lines: usize,
142}
143
144impl LineChart {
145 pub fn new() -> Self {
147 Self {
148 title: String::new(),
149 series: Vec::new(),
150 x_axis_label: String::new(),
151 y_axis_label: String::new(),
152 title_font: Font::HelveticaBold,
153 title_font_size: 16.0,
154 label_font: Font::Helvetica,
155 label_font_size: 12.0,
156 axis_font: Font::Helvetica,
157 axis_font_size: 10.0,
158 legend_position: LegendPosition::Right,
159 background_color: None,
160 show_grid: true,
161 grid_color: Color::rgb(0.9, 0.9, 0.9),
162 axis_color: Color::black(),
163 x_range: None,
164 y_range: None,
165 grid_lines: 5,
166 }
167 }
168
169 pub fn combined_x_range(&self) -> (f64, f64) {
171 if let Some(range) = self.x_range {
172 return range;
173 }
174
175 if self.series.is_empty() {
176 return (0.0, 1.0);
177 }
178
179 let mut min_x = f64::INFINITY;
180 let mut max_x = f64::NEG_INFINITY;
181
182 for series in &self.series {
183 let (series_min, series_max) = series.x_range();
184 min_x = min_x.min(series_min);
185 max_x = max_x.max(series_max);
186 }
187
188 let range = max_x - min_x;
190 let padding = range * 0.1;
191 (min_x - padding, max_x + padding)
192 }
193
194 pub fn combined_y_range(&self) -> (f64, f64) {
196 if let Some(range) = self.y_range {
197 return range;
198 }
199
200 if self.series.is_empty() {
201 return (0.0, 1.0);
202 }
203
204 let mut min_y = f64::INFINITY;
205 let mut max_y = f64::NEG_INFINITY;
206
207 for series in &self.series {
208 let (series_min, series_max) = series.y_range();
209 min_y = min_y.min(series_min);
210 max_y = max_y.max(series_max);
211 }
212
213 let range = max_y - min_y;
215 let padding = range * 0.1;
216 (min_y - padding, max_y + padding)
217 }
218}
219
220impl Default for LineChart {
221 fn default() -> Self {
222 Self::new()
223 }
224}
225
226pub struct LineChartBuilder {
228 chart: LineChart,
229}
230
231impl LineChartBuilder {
232 pub fn new() -> Self {
234 Self {
235 chart: LineChart::new(),
236 }
237 }
238
239 pub fn title<S: Into<String>>(mut self, title: S) -> Self {
241 self.chart.title = title.into();
242 self
243 }
244
245 pub fn add_series(mut self, series: DataSeries) -> Self {
247 self.chart.series.push(series);
248 self
249 }
250
251 pub fn axis_labels<S: Into<String>>(mut self, x_label: S, y_label: S) -> Self {
253 self.chart.x_axis_label = x_label.into();
254 self.chart.y_axis_label = y_label.into();
255 self
256 }
257
258 pub fn title_font(mut self, font: Font, size: f64) -> Self {
260 self.chart.title_font = font;
261 self.chart.title_font_size = size;
262 self
263 }
264
265 pub fn label_font(mut self, font: Font, size: f64) -> Self {
267 self.chart.label_font = font;
268 self.chart.label_font_size = size;
269 self
270 }
271
272 pub fn axis_font(mut self, font: Font, size: f64) -> Self {
274 self.chart.axis_font = font;
275 self.chart.axis_font_size = size;
276 self
277 }
278
279 pub fn legend_position(mut self, position: LegendPosition) -> Self {
281 self.chart.legend_position = position;
282 self
283 }
284
285 pub fn background_color(mut self, color: Color) -> Self {
287 self.chart.background_color = Some(color);
288 self
289 }
290
291 pub fn grid(mut self, show: bool, color: Color, lines: usize) -> Self {
293 self.chart.show_grid = show;
294 self.chart.grid_color = color;
295 self.chart.grid_lines = lines;
296 self
297 }
298
299 pub fn x_range(mut self, min: f64, max: f64) -> Self {
301 self.chart.x_range = Some((min, max));
302 self
303 }
304
305 pub fn y_range(mut self, min: f64, max: f64) -> Self {
307 self.chart.y_range = Some((min, max));
308 self
309 }
310
311 pub fn add_simple_series<S: Into<String>>(
313 mut self,
314 name: S,
315 values: Vec<f64>,
316 color: Color,
317 ) -> Self {
318 let series = DataSeries::new(name, color).y_data(values);
319 self.chart.series.push(series);
320 self
321 }
322
323 pub fn build(self) -> LineChart {
325 self.chart
326 }
327}
328
329impl Default for LineChartBuilder {
330 fn default() -> Self {
331 Self::new()
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_data_series_creation() {
341 let series = DataSeries::new("Test Series", Color::blue()).y_data(vec![1.0, 2.0, 3.0]);
342
343 assert_eq!(series.name, "Test Series");
344 assert_eq!(series.color, Color::blue());
345 assert_eq!(series.data.len(), 3);
346 assert_eq!(series.data[0], (0.0, 1.0));
347 assert_eq!(series.data[2], (2.0, 3.0));
348 }
349
350 #[test]
351 fn test_data_series_ranges() {
352 let series = DataSeries::new("Test", Color::red()).xy_data(vec![
353 (0.0, 10.0),
354 (5.0, 20.0),
355 (10.0, 5.0),
356 ]);
357
358 let (min_x, max_x) = series.x_range();
359 let (min_y, max_y) = series.y_range();
360
361 assert_eq!(min_x, 0.0);
362 assert_eq!(max_x, 10.0);
363 assert_eq!(min_y, 5.0);
364 assert_eq!(max_y, 20.0);
365 }
366
367 #[test]
368 fn test_line_chart_creation() {
369 let chart = LineChartBuilder::new()
370 .title("Test Chart")
371 .add_simple_series("Series 1", vec![1.0, 2.0, 3.0], Color::blue())
372 .add_simple_series("Series 2", vec![3.0, 2.0, 1.0], Color::red())
373 .build();
374
375 assert_eq!(chart.title, "Test Chart");
376 assert_eq!(chart.series.len(), 2);
377
378 let (min_y, max_y) = chart.combined_y_range();
379 assert!(min_y <= 1.0);
380 assert!(max_y >= 3.0);
381 }
382
383 #[test]
384 fn test_data_series_line_style() {
385 let series = DataSeries::new("Test", Color::blue()).line_style(3.0);
386 assert_eq!(series.line_width, 3.0);
387 }
388
389 #[test]
390 fn test_data_series_markers() {
391 let series = DataSeries::new("Test", Color::blue()).markers(false, 8.0);
392 assert!(!series.show_markers);
393 assert_eq!(series.marker_size, 8.0);
394 }
395
396 #[test]
397 fn test_data_series_fill_area() {
398 let series = DataSeries::new("Test", Color::blue()).fill_area(Some(Color::green()));
399 assert!(series.fill_area);
400 assert_eq!(series.fill_color, Some(Color::green()));
401
402 let series_no_color = DataSeries::new("Test2", Color::red()).fill_area(None);
403 assert!(series_no_color.fill_area);
404 assert!(series_no_color.fill_color.is_none());
405 }
406
407 #[test]
408 fn test_data_series_empty_ranges() {
409 let series = DataSeries::new("Empty", Color::black());
410 let (min_x, max_x) = series.x_range();
411 let (min_y, max_y) = series.y_range();
412
413 assert_eq!((min_x, max_x), (0.0, 1.0));
414 assert_eq!((min_y, max_y), (0.0, 1.0));
415 }
416
417 #[test]
418 fn test_line_chart_new() {
419 let chart = LineChart::new();
420 assert!(chart.title.is_empty());
421 assert!(chart.series.is_empty());
422 assert!(chart.show_grid);
423 assert_eq!(chart.grid_lines, 5);
424 }
425
426 #[test]
427 fn test_line_chart_default() {
428 let chart = LineChart::default();
429 assert!(chart.title.is_empty());
430 }
431
432 #[test]
433 fn test_line_chart_combined_x_range_empty() {
434 let chart = LineChart::new();
435 let (min_x, max_x) = chart.combined_x_range();
436 assert_eq!((min_x, max_x), (0.0, 1.0));
437 }
438
439 #[test]
440 fn test_line_chart_combined_y_range_empty() {
441 let chart = LineChart::new();
442 let (min_y, max_y) = chart.combined_y_range();
443 assert_eq!((min_y, max_y), (0.0, 1.0));
444 }
445
446 #[test]
447 fn test_line_chart_builder_axis_labels() {
448 let chart = LineChartBuilder::new()
449 .axis_labels("X Axis", "Y Axis")
450 .build();
451
452 assert_eq!(chart.x_axis_label, "X Axis");
453 assert_eq!(chart.y_axis_label, "Y Axis");
454 }
455
456 #[test]
457 fn test_line_chart_builder_title_font() {
458 let chart = LineChartBuilder::new()
459 .title_font(Font::CourierBold, 20.0)
460 .build();
461
462 assert_eq!(chart.title_font, Font::CourierBold);
463 assert_eq!(chart.title_font_size, 20.0);
464 }
465
466 #[test]
467 fn test_line_chart_builder_label_font() {
468 let chart = LineChartBuilder::new()
469 .label_font(Font::TimesBold, 14.0)
470 .build();
471
472 assert_eq!(chart.label_font, Font::TimesBold);
473 assert_eq!(chart.label_font_size, 14.0);
474 }
475
476 #[test]
477 fn test_line_chart_builder_axis_font() {
478 let chart = LineChartBuilder::new()
479 .axis_font(Font::Courier, 8.0)
480 .build();
481
482 assert_eq!(chart.axis_font, Font::Courier);
483 assert_eq!(chart.axis_font_size, 8.0);
484 }
485
486 #[test]
487 fn test_line_chart_builder_legend_position() {
488 let chart = LineChartBuilder::new()
489 .legend_position(LegendPosition::Bottom)
490 .build();
491
492 assert_eq!(chart.legend_position, LegendPosition::Bottom);
493 }
494
495 #[test]
496 fn test_line_chart_builder_background_color() {
497 let chart = LineChartBuilder::new()
498 .background_color(Color::white())
499 .build();
500
501 assert_eq!(chart.background_color, Some(Color::white()));
502 }
503
504 #[test]
505 fn test_line_chart_builder_grid() {
506 let chart = LineChartBuilder::new()
507 .grid(false, Color::gray(0.5), 10)
508 .build();
509
510 assert!(!chart.show_grid);
511 assert_eq!(chart.grid_color, Color::gray(0.5));
512 assert_eq!(chart.grid_lines, 10);
513 }
514
515 #[test]
516 fn test_line_chart_builder_x_range() {
517 let chart = LineChartBuilder::new().x_range(0.0, 100.0).build();
518
519 assert_eq!(chart.x_range, Some((0.0, 100.0)));
520
521 let (min_x, max_x) = chart.combined_x_range();
522 assert_eq!((min_x, max_x), (0.0, 100.0));
523 }
524
525 #[test]
526 fn test_line_chart_builder_y_range() {
527 let chart = LineChartBuilder::new().y_range(-10.0, 50.0).build();
528
529 assert_eq!(chart.y_range, Some((-10.0, 50.0)));
530
531 let (min_y, max_y) = chart.combined_y_range();
532 assert_eq!((min_y, max_y), (-10.0, 50.0));
533 }
534
535 #[test]
536 fn test_line_chart_builder_add_series() {
537 let series = DataSeries::new("Custom", Color::green()).y_data(vec![1.0, 2.0]);
538 let chart = LineChartBuilder::new().add_series(series).build();
539
540 assert_eq!(chart.series.len(), 1);
541 assert_eq!(chart.series[0].name, "Custom");
542 }
543
544 #[test]
545 fn test_line_chart_builder_default() {
546 let builder = LineChartBuilder::default();
547 let chart = builder.build();
548 assert!(chart.title.is_empty());
549 }
550
551 #[test]
552 fn test_data_series_clone() {
553 let series = DataSeries::new("Test", Color::blue())
554 .y_data(vec![1.0, 2.0])
555 .markers(true, 5.0);
556
557 let cloned = series.clone();
558 assert_eq!(series.name, cloned.name);
559 assert_eq!(series.data, cloned.data);
560 assert_eq!(series.marker_size, cloned.marker_size);
561 }
562
563 #[test]
564 fn test_line_chart_clone() {
565 let chart = LineChartBuilder::new()
566 .title("Clone Test")
567 .add_simple_series("S1", vec![1.0], Color::red())
568 .build();
569
570 let cloned = chart.clone();
571 assert_eq!(chart.title, cloned.title);
572 assert_eq!(chart.series.len(), cloned.series.len());
573 }
574}