use super::bar_chart::{BarChart, BarOrientation};
use super::chart_builder::Chart;
use super::line_chart::LineChart;
use super::pie_chart::PieChart;
use crate::coordinate_system::CoordinateSystem;
use crate::error::PdfError;
use crate::graphics::Color;
use crate::page::Page;
use crate::text::metrics::measure_text;
pub struct ChartRenderer {
pub margin: f64,
pub grid_opacity: f64,
pub coordinate_system: CoordinateSystem,
}
impl ChartRenderer {
pub fn new() -> Self {
Self {
margin: 20.0,
grid_opacity: 0.3,
coordinate_system: CoordinateSystem::PdfStandard,
}
}
pub fn with_coordinate_system(coordinate_system: CoordinateSystem) -> Self {
Self {
margin: 20.0,
grid_opacity: 0.3,
coordinate_system,
}
}
pub fn render_chart(
&self,
page: &mut Page,
chart: &Chart,
x: f64,
y: f64,
width: f64,
height: f64,
) -> Result<(), PdfError> {
match chart.chart_type {
super::chart_builder::ChartType::VerticalBar => {
let bar_chart = self.convert_to_bar_chart(chart, BarOrientation::Vertical);
self.render_bar_chart(page, &bar_chart, x, y, width, height)
}
super::chart_builder::ChartType::HorizontalBar => {
let bar_chart = self.convert_to_bar_chart(chart, BarOrientation::Horizontal);
self.render_bar_chart(page, &bar_chart, x, y, width, height)
}
super::chart_builder::ChartType::Pie => {
let pie_chart = self.convert_to_pie_chart(chart);
let radius = (width.min(height) / 2.0) - self.margin;
self.render_pie_chart(page, &pie_chart, x + width / 2.0, y + height / 2.0, radius)
}
_ => {
let bar_chart = self.convert_to_bar_chart(chart, BarOrientation::Vertical);
self.render_bar_chart(page, &bar_chart, x, y, width, height)
}
}
}
#[allow(dead_code)]
fn transform_y(&self, y: f64, chart_height: f64, page_height: f64) -> f64 {
match self.coordinate_system {
CoordinateSystem::PdfStandard => y, CoordinateSystem::ScreenSpace => {
page_height - y - chart_height
}
CoordinateSystem::Custom(matrix) => {
let point = crate::geometry::Point::new(0.0, y);
matrix.transform_point(point).y
}
}
}
fn transform_vertical_bar(
&self,
bar_x: f64,
bar_y: f64,
bar_height: f64,
_chart_area_height: f64,
_page_height: f64,
) -> (f64, f64, f64) {
match self.coordinate_system {
CoordinateSystem::PdfStandard => {
(bar_x, bar_y, bar_height)
}
CoordinateSystem::ScreenSpace => {
(bar_x, bar_y, bar_height)
}
CoordinateSystem::Custom(matrix) => {
let start_point = matrix.transform_point(crate::geometry::Point::new(bar_x, bar_y));
let end_point =
matrix.transform_point(crate::geometry::Point::new(bar_x, bar_y + bar_height));
let transformed_height = (end_point.y - start_point.y).abs();
(start_point.x, start_point.y, transformed_height)
}
}
}
fn transform_horizontal_bar(
&self,
bar_x: f64,
bar_y: f64,
bar_width: f64,
bar_height: f64,
chart_area: &ChartArea,
) -> (f64, f64, f64, f64) {
match self.coordinate_system {
CoordinateSystem::PdfStandard => {
(bar_x, bar_y, bar_width, bar_height)
}
CoordinateSystem::ScreenSpace => {
let screen_bar_y =
chart_area.y + chart_area.height - bar_y - bar_height + chart_area.y;
(bar_x, screen_bar_y, bar_width, bar_height)
}
CoordinateSystem::Custom(matrix) => {
let start_point = matrix.transform_point(crate::geometry::Point::new(bar_x, bar_y));
let end_point = matrix.transform_point(crate::geometry::Point::new(
bar_x + bar_width,
bar_y + bar_height,
));
let transformed_width = (end_point.x - start_point.x).abs();
let transformed_height = (end_point.y - start_point.y).abs();
(
start_point.x,
start_point.y,
transformed_width,
transformed_height,
)
}
}
}
fn transform_line_points(
&self,
points: &[(f64, f64)],
chart_area: &ChartArea,
) -> Vec<(f64, f64)> {
match self.coordinate_system {
CoordinateSystem::PdfStandard => {
points.to_vec()
}
CoordinateSystem::ScreenSpace => {
points
.iter()
.map(|(x, y)| {
let flipped_y = chart_area.y + chart_area.height - (y - chart_area.y);
(*x, flipped_y)
})
.collect()
}
CoordinateSystem::Custom(matrix) => {
points
.iter()
.map(|(x, y)| {
let transformed =
matrix.transform_point(crate::geometry::Point::new(*x, *y));
(transformed.x, transformed.y)
})
.collect()
}
}
}
fn transform_label_position(&self, x: f64, y: f64, chart_area: &ChartArea) -> (f64, f64) {
match self.coordinate_system {
CoordinateSystem::PdfStandard => {
(x, y - 15.0)
}
CoordinateSystem::ScreenSpace => {
(x, chart_area.y + chart_area.height + 15.0)
}
CoordinateSystem::Custom(matrix) => {
let point = matrix.transform_point(crate::geometry::Point::new(x, y));
(point.x, point.y)
}
}
}
pub fn render_bar_chart(
&self,
page: &mut Page,
chart: &BarChart,
x: f64,
y: f64,
width: f64,
height: f64,
) -> Result<(), PdfError> {
if chart.data.is_empty() {
return Ok(());
}
let title_height = if chart.title.is_empty() {
0.0
} else {
chart.title_font_size + 10.0
};
let chart_area = self.calculate_chart_area(x, y, width, height, title_height);
if let Some(bg_color) = chart.background_color {
page.graphics()
.save_state()
.set_fill_color(bg_color)
.rectangle(x, y, width, height)
.fill()
.restore_state();
}
if !chart.title.is_empty() {
let title_width = measure_text(&chart.title, &chart.title_font, chart.title_font_size);
page.text()
.set_font(chart.title_font.clone(), chart.title_font_size)
.set_fill_color(Color::black())
.at(
x + width / 2.0 - title_width / 2.0,
y + height - title_height / 2.0,
)
.write(&chart.title)?;
}
match chart.orientation {
BarOrientation::Vertical => {
self.render_vertical_bars(page, chart, &chart_area)?;
}
BarOrientation::Horizontal => {
self.render_horizontal_bars(page, chart, &chart_area)?;
}
}
Ok(())
}
pub fn render_pie_chart(
&self,
page: &mut Page,
chart: &PieChart,
center_x: f64,
center_y: f64,
radius: f64,
) -> Result<(), PdfError> {
if chart.segments.is_empty() {
return Ok(());
}
let total_value = chart.total_value();
if total_value <= 0.0 {
return Ok(());
}
let mut current_angle = chart.start_angle;
for segment in &chart.segments {
let segment_angle = segment.angle_radians(total_value);
if segment_angle <= 0.0 {
continue;
}
let (seg_center_x, seg_center_y) = if segment.exploded {
let middle_angle = current_angle + segment_angle / 2.0;
let explosion_distance = radius * segment.explosion_distance;
(
center_x + explosion_distance * middle_angle.cos(),
center_y + explosion_distance * middle_angle.sin(),
)
} else {
(center_x, center_y)
};
self.draw_pie_segment(
page,
seg_center_x,
seg_center_y,
radius,
current_angle,
current_angle + segment_angle,
segment.color,
)?;
if chart.draw_borders {
self.draw_pie_segment_border(
page,
seg_center_x,
seg_center_y,
radius,
current_angle,
current_angle + segment_angle,
chart.border_color,
chart.border_width,
)?;
}
current_angle += segment_angle;
}
if !chart.title.is_empty() {
let title_width = measure_text(&chart.title, &chart.title_font, chart.title_font_size);
page.text()
.set_font(chart.title_font.clone(), chart.title_font_size)
.set_fill_color(Color::black())
.at(center_x - title_width / 2.0, center_y + radius + 30.0)
.write(&chart.title)?;
}
Ok(())
}
pub fn render_line_chart(
&self,
page: &mut Page,
chart: &LineChart,
x: f64,
y: f64,
width: f64,
height: f64,
) -> Result<(), PdfError> {
if chart.series.is_empty() {
return Ok(());
}
let title_height = if chart.title.is_empty() {
0.0
} else {
chart.title_font_size + 10.0
};
let chart_area = self.calculate_chart_area(x, y, width, height, title_height);
if let Some(bg_color) = chart.background_color {
page.graphics()
.save_state()
.set_fill_color(bg_color)
.rectangle(x, y, width, height)
.fill()
.restore_state();
}
let (x_min, x_max) = chart.combined_x_range();
let (y_min, y_max) = chart.combined_y_range();
if chart.show_grid {
self.draw_line_chart_grid(page, &chart_area, chart.grid_lines, chart.grid_color)?;
}
for series in &chart.series {
if series.data.len() < 2 {
continue; }
let chart_points: Vec<(f64, f64)> = series
.data
.iter()
.map(|(data_x, data_y)| {
let chart_x =
chart_area.x + ((data_x - x_min) / (x_max - x_min)) * chart_area.width;
let chart_y =
chart_area.y + ((data_y - y_min) / (y_max - y_min)) * chart_area.height;
(chart_x, chart_y)
})
.collect();
let final_points = self.transform_line_points(&chart_points, &chart_area);
if series.fill_area && final_points.len() >= 2 {
self.draw_area_fill(page, &final_points, &chart_area, series)?;
}
self.draw_line_series(page, &final_points, series)?;
if series.show_markers {
self.draw_line_markers(page, &final_points, series)?;
}
}
if !chart.title.is_empty() {
let title_width = measure_text(&chart.title, &chart.title_font, chart.title_font_size);
page.text()
.set_font(chart.title_font.clone(), chart.title_font_size)
.set_fill_color(Color::black())
.at(
x + width / 2.0 - title_width / 2.0,
y + height - title_height / 2.0,
)
.write(&chart.title)?;
}
if !chart.x_axis_label.is_empty() {
let x_label_width =
measure_text(&chart.x_axis_label, &chart.axis_font, chart.axis_font_size);
page.text()
.set_font(chart.axis_font.clone(), chart.axis_font_size)
.set_fill_color(Color::black())
.at(x + width / 2.0 - x_label_width / 2.0, y - 20.0)
.write(&chart.x_axis_label)?;
}
if !chart.y_axis_label.is_empty() {
page.text()
.set_font(chart.axis_font.clone(), chart.axis_font_size)
.set_fill_color(Color::black())
.at(x + 10.0, y + height - 20.0)
.write(&chart.y_axis_label)?;
}
Ok(())
}
fn calculate_chart_area(
&self,
x: f64,
y: f64,
width: f64,
height: f64,
title_height: f64,
) -> ChartArea {
ChartArea {
x: x + self.margin,
y: y + self.margin,
width: width - 2.0 * self.margin,
height: height - 2.0 * self.margin - title_height,
}
}
fn render_vertical_bars(
&self,
page: &mut Page,
chart: &BarChart,
area: &ChartArea,
) -> Result<(), PdfError> {
let max_value = chart.max_value();
if max_value <= 0.0 {
return Ok(());
}
let bar_width = chart.calculate_bar_width(area.width);
let spacing = bar_width * chart.bar_spacing;
for (i, data) in chart.data.iter().enumerate() {
let bar_height = (data.value / max_value) * area.height;
let bar_x = area.x + i as f64 * (bar_width + spacing);
let bar_y_original = area.y;
let (final_bar_x, final_bar_y, final_bar_height) = self.transform_vertical_bar(
bar_x,
bar_y_original,
bar_height,
area.height,
page.height(),
);
let color = chart.color_for_index(i);
page.graphics()
.save_state()
.set_fill_color(color)
.rectangle(final_bar_x, final_bar_y, bar_width, final_bar_height)
.fill()
.restore_state();
if let Some(border_color) = chart.bar_border_color {
page.graphics()
.save_state()
.set_stroke_color(border_color)
.set_line_width(chart.bar_border_width)
.rectangle(final_bar_x, final_bar_y, bar_width, final_bar_height)
.stroke()
.restore_state();
}
if chart.show_values {
let value_text = format!("{:.1}", data.value);
let value_y = match self.coordinate_system {
CoordinateSystem::PdfStandard => final_bar_y + final_bar_height + 5.0,
CoordinateSystem::ScreenSpace => final_bar_y - 5.0, CoordinateSystem::Custom(_) => final_bar_y + final_bar_height + 5.0,
};
let value_width =
measure_text(&value_text, &chart.value_font, chart.value_font_size);
page.text()
.set_font(chart.value_font.clone(), chart.value_font_size)
.set_fill_color(Color::black())
.at(final_bar_x + bar_width / 2.0 - value_width / 2.0, value_y)
.write(&value_text)?;
}
let (label_x, label_y) =
self.transform_label_position(bar_x + bar_width / 2.0, bar_y_original, area);
let label_width = measure_text(&data.label, &chart.label_font, chart.label_font_size);
page.text()
.set_font(chart.label_font.clone(), chart.label_font_size)
.set_fill_color(Color::black())
.at(label_x - label_width / 2.0, label_y)
.write(&data.label)?;
}
Ok(())
}
fn render_horizontal_bars(
&self,
page: &mut Page,
chart: &BarChart,
area: &ChartArea,
) -> Result<(), PdfError> {
let max_value = chart.max_value();
if max_value <= 0.0 {
return Ok(());
}
let bar_height = area.height / chart.data.len() as f64;
let spacing = bar_height * chart.bar_spacing;
let actual_bar_height = bar_height - spacing;
for (i, data) in chart.data.iter().enumerate() {
let bar_width = (data.value / max_value) * area.width;
let bar_x_original = area.x;
let bar_y_original =
area.y + area.height - (i as f64 + 1.0) * bar_height + spacing / 2.0;
let (final_bar_x, final_bar_y, final_bar_width, final_bar_height) = self
.transform_horizontal_bar(
bar_x_original,
bar_y_original,
bar_width,
actual_bar_height,
area,
);
let color = chart.color_for_index(i);
page.graphics()
.save_state()
.set_fill_color(color)
.rectangle(final_bar_x, final_bar_y, final_bar_width, final_bar_height)
.fill()
.restore_state();
if let Some(border_color) = chart.bar_border_color {
page.graphics()
.save_state()
.set_stroke_color(border_color)
.set_line_width(chart.bar_border_width)
.rectangle(final_bar_x, final_bar_y, final_bar_width, final_bar_height)
.stroke()
.restore_state();
}
if chart.show_values {
let value_text = format!("{:.1}", data.value);
let value_x = final_bar_x + final_bar_width + 5.0;
let value_y = final_bar_y + final_bar_height / 2.0;
page.text()
.set_font(chart.value_font.clone(), chart.value_font_size)
.set_fill_color(Color::black())
.at(value_x, value_y)
.write(&value_text)?;
}
let label_width = measure_text(&data.label, &chart.label_font, chart.label_font_size);
let label_x = final_bar_x - 10.0 - label_width; let label_y = final_bar_y + final_bar_height / 2.0;
page.text()
.set_font(chart.label_font.clone(), chart.label_font_size)
.set_fill_color(Color::black())
.at(label_x, label_y)
.write(&data.label)?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn draw_pie_segment(
&self,
page: &mut Page,
center_x: f64,
center_y: f64,
radius: f64,
start_angle: f64,
end_angle: f64,
color: Color,
) -> Result<(), PdfError> {
if (end_angle - start_angle).abs() < 0.001 {
return Ok(()); }
let graphics = page.graphics();
graphics
.save_state()
.set_fill_color(color)
.move_to(center_x, center_y);
let start_x = center_x + radius * start_angle.cos();
let start_y = center_y + radius * start_angle.sin();
graphics.line_to(start_x, start_y);
let segments = 20;
let angle_step = (end_angle - start_angle) / segments as f64;
for i in 0..=segments {
let angle = start_angle + i as f64 * angle_step;
let x = center_x + radius * angle.cos();
let y = center_y + radius * angle.sin();
graphics.line_to(x, y);
}
graphics.line_to(center_x, center_y).fill().restore_state();
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn draw_pie_segment_border(
&self,
page: &mut Page,
center_x: f64,
center_y: f64,
radius: f64,
start_angle: f64,
end_angle: f64,
color: Color,
width: f64,
) -> Result<(), PdfError> {
let graphics = page.graphics();
graphics
.save_state()
.set_stroke_color(color)
.set_line_width(width);
let segments = 20;
let angle_step = (end_angle - start_angle) / segments as f64;
let start_x = center_x + radius * start_angle.cos();
let start_y = center_y + radius * start_angle.sin();
graphics.move_to(start_x, start_y);
for i in 1..=segments {
let angle = start_angle + i as f64 * angle_step;
let x = center_x + radius * angle.cos();
let y = center_y + radius * angle.sin();
graphics.line_to(x, y);
}
graphics.stroke().restore_state();
Ok(())
}
fn draw_line_chart_grid(
&self,
page: &mut Page,
area: &ChartArea,
grid_lines: usize,
color: Color,
) -> Result<(), PdfError> {
let graphics = page.graphics();
graphics
.save_state()
.set_stroke_color(color)
.set_line_width(0.5);
for i in 0..=grid_lines {
let x = area.x + (i as f64 / grid_lines as f64) * area.width;
graphics.move_to(x, area.y).line_to(x, area.y + area.height);
}
for i in 0..=grid_lines {
let y = area.y + (i as f64 / grid_lines as f64) * area.height;
graphics.move_to(area.x, y).line_to(area.x + area.width, y);
}
graphics.stroke().restore_state();
Ok(())
}
fn draw_line_series(
&self,
page: &mut Page,
points: &[(f64, f64)],
series: &super::line_chart::DataSeries,
) -> Result<(), PdfError> {
if points.len() < 2 {
return Ok(());
}
let graphics = page.graphics();
graphics
.save_state()
.set_stroke_color(series.color)
.set_line_width(series.line_width)
.move_to(points[0].0, points[0].1);
for point in &points[1..] {
graphics.line_to(point.0, point.1);
}
graphics.stroke().restore_state();
Ok(())
}
fn draw_line_markers(
&self,
page: &mut Page,
points: &[(f64, f64)],
series: &super::line_chart::DataSeries,
) -> Result<(), PdfError> {
let graphics = page.graphics();
graphics.save_state().set_fill_color(series.color);
for &(x, y) in points {
graphics.circle(x, y, series.marker_size);
}
graphics.fill().restore_state();
Ok(())
}
fn draw_area_fill(
&self,
page: &mut Page,
points: &[(f64, f64)],
area: &ChartArea,
series: &super::line_chart::DataSeries,
) -> Result<(), PdfError> {
if points.len() < 2 {
return Ok(());
}
let fill_color = series.fill_color.unwrap_or({
series.color
});
let graphics = page.graphics();
graphics
.save_state()
.set_fill_color(fill_color)
.move_to(points[0].0, area.y);
for &(x, y) in points {
graphics.line_to(x, y);
}
if let Some(last_point) = points.last() {
graphics
.line_to(last_point.0, area.y)
.fill()
.restore_state();
}
Ok(())
}
fn convert_to_bar_chart(&self, chart: &Chart, orientation: BarOrientation) -> BarChart {
use super::bar_chart::BarChartBuilder;
let mut builder = BarChartBuilder::new()
.title(chart.title.clone())
.orientation(orientation)
.colors(chart.colors.clone());
for data in &chart.data {
builder = builder.add_data(super::chart_builder::ChartData::new(
data.label.clone(),
data.value,
));
}
builder.build()
}
fn convert_to_pie_chart(&self, chart: &Chart) -> PieChart {
use super::pie_chart::PieChartBuilder;
PieChartBuilder::new()
.title(chart.title.clone())
.data(chart.data.clone())
.build()
}
}
impl Default for ChartRenderer {
fn default() -> Self {
Self::new()
}
}
struct ChartArea {
x: f64,
y: f64,
width: f64,
height: f64,
}