Skip to main content

oxidize_pdf/charts/
chart_renderer.rs

1//! Chart renderer for converting chart configurations to PDF graphics
2
3use super::bar_chart::{BarChart, BarOrientation};
4use super::chart_builder::Chart;
5use super::line_chart::LineChart;
6use super::pie_chart::PieChart;
7use crate::coordinate_system::CoordinateSystem;
8use crate::error::PdfError;
9use crate::graphics::Color;
10use crate::page::Page;
11use crate::text::metrics::measure_text;
12
13/// Renderer for various chart types
14pub struct ChartRenderer {
15    /// Default margin around charts
16    pub margin: f64,
17    /// Default grid line opacity
18    pub grid_opacity: f64,
19    /// Coordinate system to use for rendering
20    pub coordinate_system: CoordinateSystem,
21}
22
23impl ChartRenderer {
24    /// Create a new chart renderer with PDF standard coordinates (default)
25    pub fn new() -> Self {
26        Self {
27            margin: 20.0,
28            grid_opacity: 0.3,
29            coordinate_system: CoordinateSystem::PdfStandard,
30        }
31    }
32
33    /// Create a new chart renderer with specific coordinate system
34    pub fn with_coordinate_system(coordinate_system: CoordinateSystem) -> Self {
35        Self {
36            margin: 20.0,
37            grid_opacity: 0.3,
38            coordinate_system,
39        }
40    }
41
42    /// Render a generic chart
43    pub fn render_chart(
44        &self,
45        page: &mut Page,
46        chart: &Chart,
47        x: f64,
48        y: f64,
49        width: f64,
50        height: f64,
51    ) -> Result<(), PdfError> {
52        match chart.chart_type {
53            super::chart_builder::ChartType::VerticalBar => {
54                // Convert to BarChart and render
55                let bar_chart = self.convert_to_bar_chart(chart, BarOrientation::Vertical);
56                self.render_bar_chart(page, &bar_chart, x, y, width, height)
57            }
58            super::chart_builder::ChartType::HorizontalBar => {
59                let bar_chart = self.convert_to_bar_chart(chart, BarOrientation::Horizontal);
60                self.render_bar_chart(page, &bar_chart, x, y, width, height)
61            }
62            super::chart_builder::ChartType::Pie => {
63                let pie_chart = self.convert_to_pie_chart(chart);
64                let radius = (width.min(height) / 2.0) - self.margin;
65                self.render_pie_chart(page, &pie_chart, x + width / 2.0, y + height / 2.0, radius)
66            }
67            _ => {
68                // For other types, render as vertical bar for now
69                let bar_chart = self.convert_to_bar_chart(chart, BarOrientation::Vertical);
70                self.render_bar_chart(page, &bar_chart, x, y, width, height)
71            }
72        }
73    }
74
75    // Coordinate transformation methods
76
77    /// Transform Y coordinate based on the active coordinate system
78    #[allow(dead_code)]
79    fn transform_y(&self, y: f64, chart_height: f64, page_height: f64) -> f64 {
80        match self.coordinate_system {
81            CoordinateSystem::PdfStandard => y, // No transformation needed
82            CoordinateSystem::ScreenSpace => {
83                // Convert screen coordinates (origin top-left) to PDF coordinates (origin bottom-left)
84                page_height - y - chart_height
85            }
86            CoordinateSystem::Custom(matrix) => {
87                // Apply custom transformation matrix
88                let point = crate::geometry::Point::new(0.0, y);
89                matrix.transform_point(point).y
90            }
91        }
92    }
93
94    /// Transform bar coordinates for vertical bars based on coordinate system
95    fn transform_vertical_bar(
96        &self,
97        bar_x: f64,
98        bar_y: f64,
99        bar_height: f64,
100        _chart_area_height: f64,
101        _page_height: f64,
102    ) -> (f64, f64, f64) {
103        match self.coordinate_system {
104            CoordinateSystem::PdfStandard => {
105                // PDF coordinates: bars grow upward from base
106                (bar_x, bar_y, bar_height)
107            }
108            CoordinateSystem::ScreenSpace => {
109                // Screen coordinates: bars should be positioned correctly within chart area
110                // In screen coordinates, Y=0 is at the top, so we need to flip the bar position
111                // but keep bars growing upward visually (which is actually downward in screen space)
112                (bar_x, bar_y, bar_height)
113            }
114            CoordinateSystem::Custom(matrix) => {
115                // For custom matrices, apply basic transformation
116                let start_point = matrix.transform_point(crate::geometry::Point::new(bar_x, bar_y));
117                let end_point =
118                    matrix.transform_point(crate::geometry::Point::new(bar_x, bar_y + bar_height));
119                let transformed_height = (end_point.y - start_point.y).abs();
120                (start_point.x, start_point.y, transformed_height)
121            }
122        }
123    }
124
125    /// Transform bar coordinates for horizontal bars based on coordinate system  
126    fn transform_horizontal_bar(
127        &self,
128        bar_x: f64,
129        bar_y: f64,
130        bar_width: f64,
131        bar_height: f64,
132        chart_area: &ChartArea,
133    ) -> (f64, f64, f64, f64) {
134        match self.coordinate_system {
135            CoordinateSystem::PdfStandard => {
136                // PDF coordinates: no transformation needed
137                (bar_x, bar_y, bar_width, bar_height)
138            }
139            CoordinateSystem::ScreenSpace => {
140                // Screen coordinates: Y positions need to be flipped within chart area
141                let screen_bar_y =
142                    chart_area.y + chart_area.height - bar_y - bar_height + chart_area.y;
143                (bar_x, screen_bar_y, bar_width, bar_height)
144            }
145            CoordinateSystem::Custom(matrix) => {
146                // For custom matrices, apply transformation
147                let start_point = matrix.transform_point(crate::geometry::Point::new(bar_x, bar_y));
148                let end_point = matrix.transform_point(crate::geometry::Point::new(
149                    bar_x + bar_width,
150                    bar_y + bar_height,
151                ));
152                let transformed_width = (end_point.x - start_point.x).abs();
153                let transformed_height = (end_point.y - start_point.y).abs();
154                (
155                    start_point.x,
156                    start_point.y,
157                    transformed_width,
158                    transformed_height,
159                )
160            }
161        }
162    }
163
164    /// Transform line chart data points based on coordinate system
165    fn transform_line_points(
166        &self,
167        points: &[(f64, f64)],
168        chart_area: &ChartArea,
169    ) -> Vec<(f64, f64)> {
170        match self.coordinate_system {
171            CoordinateSystem::PdfStandard => {
172                // PDF coordinates: no transformation needed for data points
173                points.to_vec()
174            }
175            CoordinateSystem::ScreenSpace => {
176                // Screen coordinates: flip Y coordinates within chart area
177                points
178                    .iter()
179                    .map(|(x, y)| {
180                        let flipped_y = chart_area.y + chart_area.height - (y - chart_area.y);
181                        (*x, flipped_y)
182                    })
183                    .collect()
184            }
185            CoordinateSystem::Custom(matrix) => {
186                // Apply custom transformation
187                points
188                    .iter()
189                    .map(|(x, y)| {
190                        let transformed =
191                            matrix.transform_point(crate::geometry::Point::new(*x, *y));
192                        (transformed.x, transformed.y)
193                    })
194                    .collect()
195            }
196        }
197    }
198
199    /// Transform text position for labels based on coordinate system
200    fn transform_label_position(&self, x: f64, y: f64, chart_area: &ChartArea) -> (f64, f64) {
201        match self.coordinate_system {
202            CoordinateSystem::PdfStandard => {
203                // Labels go below the chart area (negative offset)
204                (x, y - 15.0)
205            }
206            CoordinateSystem::ScreenSpace => {
207                // Labels go below the chart area (positive offset in screen space)
208                (x, chart_area.y + chart_area.height + 15.0)
209            }
210            CoordinateSystem::Custom(matrix) => {
211                // Apply custom transformation
212                let point = matrix.transform_point(crate::geometry::Point::new(x, y));
213                (point.x, point.y)
214            }
215        }
216    }
217
218    /// Render a bar chart
219    pub fn render_bar_chart(
220        &self,
221        page: &mut Page,
222        chart: &BarChart,
223        x: f64,
224        y: f64,
225        width: f64,
226        height: f64,
227    ) -> Result<(), PdfError> {
228        if chart.data.is_empty() {
229            return Ok(());
230        }
231
232        // Calculate chart area (excluding title and margins)
233        let title_height = if chart.title.is_empty() {
234            0.0
235        } else {
236            chart.title_font_size + 10.0
237        };
238        let chart_area = self.calculate_chart_area(x, y, width, height, title_height);
239
240        // Draw background
241        if let Some(bg_color) = chart.background_color {
242            page.graphics()
243                .save_state()
244                .set_fill_color(bg_color)
245                .rectangle(x, y, width, height)
246                .fill()
247                .restore_state();
248        }
249
250        // Draw title
251        if !chart.title.is_empty() {
252            let title_width = measure_text(&chart.title, &chart.title_font, chart.title_font_size);
253            page.text()
254                .set_font(chart.title_font.clone(), chart.title_font_size)
255                .set_fill_color(Color::black())
256                .at(
257                    x + width / 2.0 - title_width / 2.0,
258                    y + height - title_height / 2.0,
259                )
260                .write(&chart.title)?;
261        }
262
263        match chart.orientation {
264            BarOrientation::Vertical => {
265                self.render_vertical_bars(page, chart, &chart_area)?;
266            }
267            BarOrientation::Horizontal => {
268                self.render_horizontal_bars(page, chart, &chart_area)?;
269            }
270        }
271
272        Ok(())
273    }
274
275    /// Render a pie chart
276    pub fn render_pie_chart(
277        &self,
278        page: &mut Page,
279        chart: &PieChart,
280        center_x: f64,
281        center_y: f64,
282        radius: f64,
283    ) -> Result<(), PdfError> {
284        if chart.segments.is_empty() {
285            return Ok(());
286        }
287
288        let total_value = chart.total_value();
289        if total_value <= 0.0 {
290            return Ok(());
291        }
292
293        let mut current_angle = chart.start_angle;
294
295        // Draw each segment
296        for segment in &chart.segments {
297            let segment_angle = segment.angle_radians(total_value);
298            if segment_angle <= 0.0 {
299                continue;
300            }
301
302            // Calculate center point (with explosion if needed)
303            let (seg_center_x, seg_center_y) = if segment.exploded {
304                let middle_angle = current_angle + segment_angle / 2.0;
305                let explosion_distance = radius * segment.explosion_distance;
306                (
307                    center_x + explosion_distance * middle_angle.cos(),
308                    center_y + explosion_distance * middle_angle.sin(),
309                )
310            } else {
311                (center_x, center_y)
312            };
313
314            // Draw the segment
315            self.draw_pie_segment(
316                page,
317                seg_center_x,
318                seg_center_y,
319                radius,
320                current_angle,
321                current_angle + segment_angle,
322                segment.color,
323            )?;
324
325            // Draw border if enabled
326            if chart.draw_borders {
327                self.draw_pie_segment_border(
328                    page,
329                    seg_center_x,
330                    seg_center_y,
331                    radius,
332                    current_angle,
333                    current_angle + segment_angle,
334                    chart.border_color,
335                    chart.border_width,
336                )?;
337            }
338
339            current_angle += segment_angle;
340        }
341
342        // Draw title if present
343        if !chart.title.is_empty() {
344            let title_width = measure_text(&chart.title, &chart.title_font, chart.title_font_size);
345            page.text()
346                .set_font(chart.title_font.clone(), chart.title_font_size)
347                .set_fill_color(Color::black())
348                .at(center_x - title_width / 2.0, center_y + radius + 30.0)
349                .write(&chart.title)?;
350        }
351
352        Ok(())
353    }
354
355    /// Render a line chart
356    pub fn render_line_chart(
357        &self,
358        page: &mut Page,
359        chart: &LineChart,
360        x: f64,
361        y: f64,
362        width: f64,
363        height: f64,
364    ) -> Result<(), PdfError> {
365        if chart.series.is_empty() {
366            return Ok(());
367        }
368
369        // Calculate chart area
370        let title_height = if chart.title.is_empty() {
371            0.0
372        } else {
373            chart.title_font_size + 10.0
374        };
375        let chart_area = self.calculate_chart_area(x, y, width, height, title_height);
376
377        // Draw background
378        if let Some(bg_color) = chart.background_color {
379            page.graphics()
380                .save_state()
381                .set_fill_color(bg_color)
382                .rectangle(x, y, width, height)
383                .fill()
384                .restore_state();
385        }
386
387        // Get combined ranges
388        let (x_min, x_max) = chart.combined_x_range();
389        let (y_min, y_max) = chart.combined_y_range();
390
391        // Draw grid if enabled
392        if chart.show_grid {
393            self.draw_line_chart_grid(page, &chart_area, chart.grid_lines, chart.grid_color)?;
394        }
395
396        // Draw each series
397        for series in &chart.series {
398            if series.data.len() < 2 {
399                continue; // Need at least 2 points for a line
400            }
401
402            // Convert data points to chart coordinates
403            let chart_points: Vec<(f64, f64)> = series
404                .data
405                .iter()
406                .map(|(data_x, data_y)| {
407                    let chart_x =
408                        chart_area.x + ((data_x - x_min) / (x_max - x_min)) * chart_area.width;
409                    let chart_y =
410                        chart_area.y + ((data_y - y_min) / (y_max - y_min)) * chart_area.height;
411                    (chart_x, chart_y)
412                })
413                .collect();
414
415            // Transform points based on coordinate system
416            let final_points = self.transform_line_points(&chart_points, &chart_area);
417
418            // Draw area fill if enabled
419            if series.fill_area && final_points.len() >= 2 {
420                self.draw_area_fill(page, &final_points, &chart_area, series)?;
421            }
422
423            // Draw the line
424            self.draw_line_series(page, &final_points, series)?;
425
426            // Draw markers if enabled
427            if series.show_markers {
428                self.draw_line_markers(page, &final_points, series)?;
429            }
430        }
431
432        // Draw title
433        if !chart.title.is_empty() {
434            let title_width = measure_text(&chart.title, &chart.title_font, chart.title_font_size);
435            page.text()
436                .set_font(chart.title_font.clone(), chart.title_font_size)
437                .set_fill_color(Color::black())
438                .at(
439                    x + width / 2.0 - title_width / 2.0,
440                    y + height - title_height / 2.0,
441                )
442                .write(&chart.title)?;
443        }
444
445        // Draw axis labels if present
446        if !chart.x_axis_label.is_empty() {
447            let x_label_width =
448                measure_text(&chart.x_axis_label, &chart.axis_font, chart.axis_font_size);
449            page.text()
450                .set_font(chart.axis_font.clone(), chart.axis_font_size)
451                .set_fill_color(Color::black())
452                .at(x + width / 2.0 - x_label_width / 2.0, y - 20.0)
453                .write(&chart.x_axis_label)?;
454        }
455
456        if !chart.y_axis_label.is_empty() {
457            // Position Y axis label inside the chart area to ensure visibility
458            page.text()
459                .set_font(chart.axis_font.clone(), chart.axis_font_size)
460                .set_fill_color(Color::black())
461                .at(x + 10.0, y + height - 20.0)
462                .write(&chart.y_axis_label)?;
463        }
464
465        Ok(())
466    }
467
468    // Helper methods
469
470    fn calculate_chart_area(
471        &self,
472        x: f64,
473        y: f64,
474        width: f64,
475        height: f64,
476        title_height: f64,
477    ) -> ChartArea {
478        ChartArea {
479            x: x + self.margin,
480            y: y + self.margin,
481            width: width - 2.0 * self.margin,
482            height: height - 2.0 * self.margin - title_height,
483        }
484    }
485
486    fn render_vertical_bars(
487        &self,
488        page: &mut Page,
489        chart: &BarChart,
490        area: &ChartArea,
491    ) -> Result<(), PdfError> {
492        let max_value = chart.max_value();
493        if max_value <= 0.0 {
494            return Ok(());
495        }
496
497        let bar_width = chart.calculate_bar_width(area.width);
498        let spacing = bar_width * chart.bar_spacing;
499
500        for (i, data) in chart.data.iter().enumerate() {
501            let bar_height = (data.value / max_value) * area.height;
502            let bar_x = area.x + i as f64 * (bar_width + spacing);
503            let bar_y_original = area.y;
504
505            // Transform bar coordinates based on coordinate system
506            let (final_bar_x, final_bar_y, final_bar_height) = self.transform_vertical_bar(
507                bar_x,
508                bar_y_original,
509                bar_height,
510                area.height,
511                page.height(),
512            );
513
514            let color = chart.color_for_index(i);
515
516            // Draw bar
517            page.graphics()
518                .save_state()
519                .set_fill_color(color)
520                .rectangle(final_bar_x, final_bar_y, bar_width, final_bar_height)
521                .fill()
522                .restore_state();
523
524            // Draw border if specified
525            if let Some(border_color) = chart.bar_border_color {
526                page.graphics()
527                    .save_state()
528                    .set_stroke_color(border_color)
529                    .set_line_width(chart.bar_border_width)
530                    .rectangle(final_bar_x, final_bar_y, bar_width, final_bar_height)
531                    .stroke()
532                    .restore_state();
533            }
534
535            // Draw value if enabled
536            if chart.show_values {
537                let value_text = format!("{:.1}", data.value);
538                let value_y = match self.coordinate_system {
539                    CoordinateSystem::PdfStandard => final_bar_y + final_bar_height + 5.0,
540                    CoordinateSystem::ScreenSpace => final_bar_y - 5.0, // Above bars in screen space
541                    CoordinateSystem::Custom(_) => final_bar_y + final_bar_height + 5.0,
542                };
543
544                let value_width =
545                    measure_text(&value_text, &chart.value_font, chart.value_font_size);
546                page.text()
547                    .set_font(chart.value_font.clone(), chart.value_font_size)
548                    .set_fill_color(Color::black())
549                    .at(final_bar_x + bar_width / 2.0 - value_width / 2.0, value_y)
550                    .write(&value_text)?;
551            }
552
553            // Draw label using coordinate system transformation
554            let (label_x, label_y) =
555                self.transform_label_position(bar_x + bar_width / 2.0, bar_y_original, area);
556
557            let label_width = measure_text(&data.label, &chart.label_font, chart.label_font_size);
558            page.text()
559                .set_font(chart.label_font.clone(), chart.label_font_size)
560                .set_fill_color(Color::black())
561                .at(label_x - label_width / 2.0, label_y)
562                .write(&data.label)?;
563        }
564
565        Ok(())
566    }
567
568    fn render_horizontal_bars(
569        &self,
570        page: &mut Page,
571        chart: &BarChart,
572        area: &ChartArea,
573    ) -> Result<(), PdfError> {
574        let max_value = chart.max_value();
575        if max_value <= 0.0 {
576            return Ok(());
577        }
578
579        let bar_height = area.height / chart.data.len() as f64;
580        let spacing = bar_height * chart.bar_spacing;
581        let actual_bar_height = bar_height - spacing;
582
583        for (i, data) in chart.data.iter().enumerate() {
584            let bar_width = (data.value / max_value) * area.width;
585            let bar_x_original = area.x;
586            let bar_y_original =
587                area.y + area.height - (i as f64 + 1.0) * bar_height + spacing / 2.0;
588
589            // Transform bar coordinates based on coordinate system
590            let (final_bar_x, final_bar_y, final_bar_width, final_bar_height) = self
591                .transform_horizontal_bar(
592                    bar_x_original,
593                    bar_y_original,
594                    bar_width,
595                    actual_bar_height,
596                    area,
597                );
598
599            let color = chart.color_for_index(i);
600
601            // Draw bar
602            page.graphics()
603                .save_state()
604                .set_fill_color(color)
605                .rectangle(final_bar_x, final_bar_y, final_bar_width, final_bar_height)
606                .fill()
607                .restore_state();
608
609            // Draw border if specified
610            if let Some(border_color) = chart.bar_border_color {
611                page.graphics()
612                    .save_state()
613                    .set_stroke_color(border_color)
614                    .set_line_width(chart.bar_border_width)
615                    .rectangle(final_bar_x, final_bar_y, final_bar_width, final_bar_height)
616                    .stroke()
617                    .restore_state();
618            }
619
620            // Draw value if enabled
621            if chart.show_values {
622                let value_text = format!("{:.1}", data.value);
623                let value_x = final_bar_x + final_bar_width + 5.0;
624                let value_y = final_bar_y + final_bar_height / 2.0;
625
626                // Note: For horizontal bars, values are positioned to the right of the bar
627                // No need to center horizontally as they are left-aligned from the edge
628                page.text()
629                    .set_font(chart.value_font.clone(), chart.value_font_size)
630                    .set_fill_color(Color::black())
631                    .at(value_x, value_y)
632                    .write(&value_text)?;
633            }
634
635            // Draw label - for horizontal bars, labels go to the left
636            let label_width = measure_text(&data.label, &chart.label_font, chart.label_font_size);
637            let label_x = final_bar_x - 10.0 - label_width; // Right-align to the left of the bar
638            let label_y = final_bar_y + final_bar_height / 2.0;
639
640            page.text()
641                .set_font(chart.label_font.clone(), chart.label_font_size)
642                .set_fill_color(Color::black())
643                .at(label_x, label_y)
644                .write(&data.label)?;
645        }
646
647        Ok(())
648    }
649
650    #[allow(clippy::too_many_arguments)]
651    fn draw_pie_segment(
652        &self,
653        page: &mut Page,
654        center_x: f64,
655        center_y: f64,
656        radius: f64,
657        start_angle: f64,
658        end_angle: f64,
659        color: Color,
660    ) -> Result<(), PdfError> {
661        if (end_angle - start_angle).abs() < 0.001 {
662            return Ok(()); // Skip very small segments
663        }
664
665        let graphics = page.graphics();
666
667        graphics
668            .save_state()
669            .set_fill_color(color)
670            .move_to(center_x, center_y);
671
672        // Draw arc
673        let start_x = center_x + radius * start_angle.cos();
674        let start_y = center_y + radius * start_angle.sin();
675        graphics.line_to(start_x, start_y);
676
677        // Simple arc approximation using line segments
678        let segments = 20;
679        let angle_step = (end_angle - start_angle) / segments as f64;
680
681        for i in 0..=segments {
682            let angle = start_angle + i as f64 * angle_step;
683            let x = center_x + radius * angle.cos();
684            let y = center_y + radius * angle.sin();
685            graphics.line_to(x, y);
686        }
687
688        graphics.line_to(center_x, center_y).fill().restore_state();
689
690        Ok(())
691    }
692
693    #[allow(clippy::too_many_arguments)]
694    fn draw_pie_segment_border(
695        &self,
696        page: &mut Page,
697        center_x: f64,
698        center_y: f64,
699        radius: f64,
700        start_angle: f64,
701        end_angle: f64,
702        color: Color,
703        width: f64,
704    ) -> Result<(), PdfError> {
705        let graphics = page.graphics();
706
707        graphics
708            .save_state()
709            .set_stroke_color(color)
710            .set_line_width(width);
711
712        // Draw the arc border
713        let segments = 20;
714        let angle_step = (end_angle - start_angle) / segments as f64;
715
716        let start_x = center_x + radius * start_angle.cos();
717        let start_y = center_y + radius * start_angle.sin();
718        graphics.move_to(start_x, start_y);
719
720        for i in 1..=segments {
721            let angle = start_angle + i as f64 * angle_step;
722            let x = center_x + radius * angle.cos();
723            let y = center_y + radius * angle.sin();
724            graphics.line_to(x, y);
725        }
726
727        graphics.stroke().restore_state();
728
729        Ok(())
730    }
731
732    fn draw_line_chart_grid(
733        &self,
734        page: &mut Page,
735        area: &ChartArea,
736        grid_lines: usize,
737        color: Color,
738    ) -> Result<(), PdfError> {
739        let graphics = page.graphics();
740
741        graphics
742            .save_state()
743            .set_stroke_color(color)
744            .set_line_width(0.5);
745
746        // Vertical grid lines
747        for i in 0..=grid_lines {
748            let x = area.x + (i as f64 / grid_lines as f64) * area.width;
749            graphics.move_to(x, area.y).line_to(x, area.y + area.height);
750        }
751
752        // Horizontal grid lines
753        for i in 0..=grid_lines {
754            let y = area.y + (i as f64 / grid_lines as f64) * area.height;
755            graphics.move_to(area.x, y).line_to(area.x + area.width, y);
756        }
757
758        graphics.stroke().restore_state();
759
760        Ok(())
761    }
762
763    fn draw_line_series(
764        &self,
765        page: &mut Page,
766        points: &[(f64, f64)],
767        series: &super::line_chart::DataSeries,
768    ) -> Result<(), PdfError> {
769        if points.len() < 2 {
770            return Ok(());
771        }
772
773        let graphics = page.graphics();
774
775        graphics
776            .save_state()
777            .set_stroke_color(series.color)
778            .set_line_width(series.line_width)
779            .move_to(points[0].0, points[0].1);
780
781        for point in &points[1..] {
782            graphics.line_to(point.0, point.1);
783        }
784
785        graphics.stroke().restore_state();
786
787        Ok(())
788    }
789
790    fn draw_line_markers(
791        &self,
792        page: &mut Page,
793        points: &[(f64, f64)],
794        series: &super::line_chart::DataSeries,
795    ) -> Result<(), PdfError> {
796        let graphics = page.graphics();
797
798        graphics.save_state().set_fill_color(series.color);
799
800        for &(x, y) in points {
801            graphics.circle(x, y, series.marker_size);
802        }
803
804        graphics.fill().restore_state();
805
806        Ok(())
807    }
808
809    fn draw_area_fill(
810        &self,
811        page: &mut Page,
812        points: &[(f64, f64)],
813        area: &ChartArea,
814        series: &super::line_chart::DataSeries,
815    ) -> Result<(), PdfError> {
816        if points.len() < 2 {
817            return Ok(());
818        }
819
820        let fill_color = series.fill_color.unwrap_or({
821            // PDF doesn't support alpha, use a lighter version of the line color
822            series.color
823        });
824
825        let graphics = page.graphics();
826
827        graphics
828            .save_state()
829            .set_fill_color(fill_color)
830            .move_to(points[0].0, area.y);
831
832        for &(x, y) in points {
833            graphics.line_to(x, y);
834        }
835
836        // Safe to unwrap: points.len() >= 2 is guaranteed by check at line 833
837        if let Some(last_point) = points.last() {
838            graphics
839                .line_to(last_point.0, area.y)
840                .fill()
841                .restore_state();
842        }
843
844        Ok(())
845    }
846
847    // Conversion helpers
848    fn convert_to_bar_chart(&self, chart: &Chart, orientation: BarOrientation) -> BarChart {
849        use super::bar_chart::BarChartBuilder;
850
851        let mut builder = BarChartBuilder::new()
852            .title(chart.title.clone())
853            .orientation(orientation)
854            .colors(chart.colors.clone());
855
856        for data in &chart.data {
857            builder = builder.add_data(super::chart_builder::ChartData::new(
858                data.label.clone(),
859                data.value,
860            ));
861        }
862
863        builder.build()
864    }
865
866    fn convert_to_pie_chart(&self, chart: &Chart) -> PieChart {
867        use super::pie_chart::PieChartBuilder;
868
869        PieChartBuilder::new()
870            .title(chart.title.clone())
871            .data(chart.data.clone())
872            .build()
873    }
874}
875
876impl Default for ChartRenderer {
877    fn default() -> Self {
878        Self::new()
879    }
880}
881
882/// Chart area definition
883struct ChartArea {
884    x: f64,
885    y: f64,
886    width: f64,
887    height: f64,
888}