Skip to main content

oxidize_pdf/charts/
line_chart.rs

1//! Line chart implementation with multiple data series support
2
3use super::chart_builder::LegendPosition;
4use crate::graphics::Color;
5use crate::text::Font;
6
7/// A data series for line charts
8#[derive(Debug, Clone)]
9pub struct DataSeries {
10    /// Series name
11    pub name: String,
12    /// Data points (x, y) pairs
13    pub data: Vec<(f64, f64)>,
14    /// Line color
15    pub color: Color,
16    /// Line width
17    pub line_width: f64,
18    /// Whether to show markers at data points
19    pub show_markers: bool,
20    /// Marker size
21    pub marker_size: f64,
22    /// Whether to fill area under the line
23    pub fill_area: bool,
24    /// Fill color (if different from line color)
25    pub fill_color: Option<Color>,
26}
27
28impl DataSeries {
29    /// Create a new data series
30    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    /// Add data points from y-values (x will be 0, 1, 2, ...)
44    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    /// Add data points from (x, y) pairs
54    pub fn xy_data(mut self, data: Vec<(f64, f64)>) -> Self {
55        self.data = data;
56        self
57    }
58
59    /// Set line style
60    pub fn line_style(mut self, width: f64) -> Self {
61        self.line_width = width;
62        self
63    }
64
65    /// Enable/disable markers
66    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    /// Enable area fill
73    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    /// Get the range of x values
80    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    /// Get the range of y values
93    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/// Line chart configuration
107#[derive(Debug, Clone)]
108pub struct LineChart {
109    /// Chart title
110    pub title: String,
111    /// Data series
112    pub series: Vec<DataSeries>,
113    /// X-axis label
114    pub x_axis_label: String,
115    /// Y-axis label
116    pub y_axis_label: String,
117    /// Title font and size
118    pub title_font: Font,
119    pub title_font_size: f64,
120    /// Label font and size
121    pub label_font: Font,
122    pub label_font_size: f64,
123    /// Axis font and size
124    pub axis_font: Font,
125    pub axis_font_size: f64,
126    /// Legend position
127    pub legend_position: LegendPosition,
128    /// Background color
129    pub background_color: Option<Color>,
130    /// Show grid lines
131    pub show_grid: bool,
132    /// Grid color
133    pub grid_color: Color,
134    /// Axis color
135    pub axis_color: Color,
136    /// X-axis range (None for auto)
137    pub x_range: Option<(f64, f64)>,
138    /// Y-axis range (None for auto)
139    pub y_range: Option<(f64, f64)>,
140    /// Number of grid lines
141    pub grid_lines: usize,
142}
143
144impl LineChart {
145    /// Create a new line chart
146    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    /// Get the combined X range of all series
170    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        // Add some padding
189        let range = max_x - min_x;
190        let padding = range * 0.1;
191        (min_x - padding, max_x + padding)
192    }
193
194    /// Get the combined Y range of all series
195    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        // Add some padding
214        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
226/// Builder for creating line charts
227pub struct LineChartBuilder {
228    chart: LineChart,
229}
230
231impl LineChartBuilder {
232    /// Create a new line chart builder
233    pub fn new() -> Self {
234        Self {
235            chart: LineChart::new(),
236        }
237    }
238
239    /// Set chart title
240    pub fn title<S: Into<String>>(mut self, title: S) -> Self {
241        self.chart.title = title.into();
242        self
243    }
244
245    /// Add a data series
246    pub fn add_series(mut self, series: DataSeries) -> Self {
247        self.chart.series.push(series);
248        self
249    }
250
251    /// Set axis labels
252    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    /// Set fonts
259    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    /// Set label font
266    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    /// Set axis font
273    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    /// Set legend position
280    pub fn legend_position(mut self, position: LegendPosition) -> Self {
281        self.chart.legend_position = position;
282        self
283    }
284
285    /// Set background color
286    pub fn background_color(mut self, color: Color) -> Self {
287        self.chart.background_color = Some(color);
288        self
289    }
290
291    /// Configure grid
292    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    /// Set axis ranges
300    pub fn x_range(mut self, min: f64, max: f64) -> Self {
301        self.chart.x_range = Some((min, max));
302        self
303    }
304
305    /// Set Y axis range
306    pub fn y_range(mut self, min: f64, max: f64) -> Self {
307        self.chart.y_range = Some((min, max));
308        self
309    }
310
311    /// Add a simple series from Y values
312    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    /// Build the final line chart
324    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}