Skip to main content

forme/chart/
pie.rs

1//! Pie/donut chart builder.
2
3use super::*;
4use crate::model::ChartDataPoint;
5
6/// Configuration for pie chart rendering.
7pub struct PieChartConfig {
8    pub donut: bool,
9    pub show_legend: bool,
10    pub title: Option<String>,
11}
12
13/// Build pie chart primitives from data points.
14pub fn build(
15    width: f64,
16    height: f64,
17    data: &[ChartDataPoint],
18    config: &PieChartConfig,
19) -> Vec<ChartPrimitive> {
20    if data.is_empty() {
21        return vec![];
22    }
23
24    let mut primitives = Vec::new();
25
26    let title_offset = if config.title.is_some() {
27        TITLE_HEIGHT
28    } else {
29        0.0
30    };
31
32    // Legend space on the right
33    let legend_width = if config.show_legend { 80.0 } else { 0.0 };
34
35    let available_w = width - legend_width;
36    let available_h = height - title_offset;
37    let radius = (available_w.min(available_h) / 2.0 - LABEL_MARGIN).max(1.0);
38    let cx = available_w / 2.0;
39    let cy = title_offset + available_h / 2.0;
40
41    let total: f64 = data.iter().map(|d| d.value).sum();
42    if total <= 0.0 {
43        return vec![];
44    }
45
46    // Arc sectors
47    let mut start_angle: f64 = -std::f64::consts::FRAC_PI_2; // Start at 12 o'clock
48    for (i, dp) in data.iter().enumerate() {
49        let slice_angle = (dp.value / total) * std::f64::consts::TAU;
50        let end_angle = start_angle + slice_angle;
51        let color = resolve_color(dp.color.as_deref(), i);
52
53        primitives.push(ChartPrimitive::ArcSector {
54            cx,
55            cy,
56            r: radius,
57            start_angle,
58            end_angle,
59            fill: color,
60        });
61
62        start_angle = end_angle;
63    }
64
65    // Donut: white center circle
66    if config.donut {
67        let inner_r = radius * 0.55;
68        primitives.push(ChartPrimitive::Circle {
69            cx,
70            cy,
71            r: inner_r,
72            fill: Color::WHITE,
73        });
74    }
75
76    // Legend
77    if config.show_legend {
78        let legend_x = available_w + LABEL_MARGIN;
79        let legend_y_start = title_offset + LABEL_MARGIN;
80        let swatch_size = 8.0;
81        let line_height = 14.0;
82
83        for (i, dp) in data.iter().enumerate() {
84            let ly = legend_y_start + i as f64 * line_height;
85            let color = resolve_color(dp.color.as_deref(), i);
86
87            primitives.push(ChartPrimitive::Rect {
88                x: legend_x,
89                y: ly,
90                w: swatch_size,
91                h: swatch_size,
92                fill: color,
93            });
94            primitives.push(ChartPrimitive::Label {
95                text: dp.label.clone(),
96                x: legend_x + swatch_size + LABEL_MARGIN,
97                y: ly + swatch_size - 1.0,
98                font_size: AXIS_LABEL_FONT,
99                color: LABEL_COLOR,
100                anchor: TextAnchor::Left,
101            });
102        }
103    }
104
105    // Title
106    if let Some(ref title) = config.title {
107        primitives.push(ChartPrimitive::Label {
108            text: title.clone(),
109            x: width / 2.0,
110            y: TITLE_FONT,
111            font_size: TITLE_FONT,
112            color: Color::BLACK,
113            anchor: TextAnchor::Center,
114        });
115    }
116
117    primitives
118}