1use 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
13pub struct ChartRenderer {
15 pub margin: f64,
17 pub grid_opacity: f64,
19 pub coordinate_system: CoordinateSystem,
21}
22
23impl ChartRenderer {
24 pub fn new() -> Self {
26 Self {
27 margin: 20.0,
28 grid_opacity: 0.3,
29 coordinate_system: CoordinateSystem::PdfStandard,
30 }
31 }
32
33 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 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 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 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 #[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, CoordinateSystem::ScreenSpace => {
83 page_height - y - chart_height
85 }
86 CoordinateSystem::Custom(matrix) => {
87 let point = crate::geometry::Point::new(0.0, y);
89 matrix.transform_point(point).y
90 }
91 }
92 }
93
94 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 (bar_x, bar_y, bar_height)
107 }
108 CoordinateSystem::ScreenSpace => {
109 (bar_x, bar_y, bar_height)
113 }
114 CoordinateSystem::Custom(matrix) => {
115 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 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 (bar_x, bar_y, bar_width, bar_height)
138 }
139 CoordinateSystem::ScreenSpace => {
140 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 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 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 points.to_vec()
174 }
175 CoordinateSystem::ScreenSpace => {
176 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 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 fn transform_label_position(&self, x: f64, y: f64, chart_area: &ChartArea) -> (f64, f64) {
201 match self.coordinate_system {
202 CoordinateSystem::PdfStandard => {
203 (x, y - 15.0)
205 }
206 CoordinateSystem::ScreenSpace => {
207 (x, chart_area.y + chart_area.height + 15.0)
209 }
210 CoordinateSystem::Custom(matrix) => {
211 let point = matrix.transform_point(crate::geometry::Point::new(x, y));
213 (point.x, point.y)
214 }
215 }
216 }
217
218 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 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 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 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 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 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 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 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 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 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 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 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 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 let (x_min, x_max) = chart.combined_x_range();
389 let (y_min, y_max) = chart.combined_y_range();
390
391 if chart.show_grid {
393 self.draw_line_chart_grid(page, &chart_area, chart.grid_lines, chart.grid_color)?;
394 }
395
396 for series in &chart.series {
398 if series.data.len() < 2 {
399 continue; }
401
402 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 let final_points = self.transform_line_points(&chart_points, &chart_area);
417
418 if series.fill_area && final_points.len() >= 2 {
420 self.draw_area_fill(page, &final_points, &chart_area, series)?;
421 }
422
423 self.draw_line_series(page, &final_points, series)?;
425
426 if series.show_markers {
428 self.draw_line_markers(page, &final_points, series)?;
429 }
430 }
431
432 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 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 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 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 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 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 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 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, 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 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 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 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 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 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 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 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; 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(()); }
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 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 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 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 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 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 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 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 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
882struct ChartArea {
884 x: f64,
885 y: f64,
886 width: f64,
887 height: f64,
888}