Skip to main content

forme/chart/
bar.rs

1//! Bar chart builder.
2
3use super::*;
4use crate::model::ChartDataPoint;
5
6/// Configuration for bar chart rendering.
7pub struct BarChartConfig {
8    pub color: Option<String>,
9    pub show_labels: bool,
10    pub show_values: bool,
11    pub show_grid: bool,
12    pub title: Option<String>,
13}
14
15/// Build bar chart primitives from data points.
16pub fn build(
17    width: f64,
18    height: f64,
19    data: &[ChartDataPoint],
20    config: &BarChartConfig,
21) -> Vec<ChartPrimitive> {
22    if data.is_empty() {
23        return vec![];
24    }
25
26    let mut primitives = Vec::new();
27
28    // Reserve space for title
29    let title_offset = if config.title.is_some() {
30        TITLE_HEIGHT
31    } else {
32        0.0
33    };
34
35    // Plot area
36    let plot_left = Y_AXIS_WIDTH;
37    let plot_top = title_offset;
38    let plot_right = width - LABEL_MARGIN;
39    let plot_bottom = height - X_AXIS_HEIGHT;
40    let plot_width = plot_right - plot_left;
41    let plot_height = plot_bottom - plot_top;
42
43    if plot_width <= 0.0 || plot_height <= 0.0 {
44        return vec![];
45    }
46
47    // Compute Y range
48    let max_value = data.iter().map(|d| d.value).fold(0.0_f64, f64::max);
49    let y_max = nice_number(max_value);
50    let y_ticks = 5;
51
52    // Grid lines
53    if config.show_grid {
54        for i in 0..=y_ticks {
55            let frac = i as f64 / y_ticks as f64;
56            let y = plot_bottom - frac * plot_height;
57            primitives.push(ChartPrimitive::Line {
58                x1: plot_left,
59                y1: y,
60                x2: plot_right,
61                y2: y,
62                stroke: GRID_COLOR,
63                width: 0.5,
64            });
65        }
66    }
67
68    // Y-axis labels
69    for i in 0..=y_ticks {
70        let frac = i as f64 / y_ticks as f64;
71        let y = plot_bottom - frac * plot_height;
72        let value = y_max * frac;
73        let label = format_number(value);
74        primitives.push(ChartPrimitive::Label {
75            text: label,
76            x: plot_left - LABEL_MARGIN,
77            y: y + AXIS_LABEL_FONT * 0.35,
78            font_size: AXIS_LABEL_FONT,
79            color: LABEL_COLOR,
80            anchor: TextAnchor::Right,
81        });
82    }
83
84    // Axes
85    primitives.push(ChartPrimitive::Line {
86        x1: plot_left,
87        y1: plot_top,
88        x2: plot_left,
89        y2: plot_bottom,
90        stroke: AXIS_COLOR,
91        width: 1.0,
92    });
93    primitives.push(ChartPrimitive::Line {
94        x1: plot_left,
95        y1: plot_bottom,
96        x2: plot_right,
97        y2: plot_bottom,
98        stroke: AXIS_COLOR,
99        width: 1.0,
100    });
101
102    // Bars
103    let bar_gap = 4.0;
104    let n = data.len() as f64;
105    let bar_width = (plot_width - bar_gap * (n + 1.0)) / n;
106    let default_color = resolve_color(config.color.as_deref(), 0);
107
108    for (i, dp) in data.iter().enumerate() {
109        let bar_color = dp
110            .color
111            .as_deref()
112            .map(parse_hex_color)
113            .unwrap_or(default_color);
114        let bar_h = if y_max > 0.0 {
115            (dp.value / y_max) * plot_height
116        } else {
117            0.0
118        };
119        let bx = plot_left + bar_gap + i as f64 * (bar_width + bar_gap);
120        let by = plot_bottom - bar_h;
121
122        primitives.push(ChartPrimitive::Rect {
123            x: bx,
124            y: by,
125            w: bar_width,
126            h: bar_h,
127            fill: bar_color,
128        });
129
130        // Value label above bar
131        if config.show_values {
132            let label = format_number(dp.value);
133            primitives.push(ChartPrimitive::Label {
134                text: label,
135                x: bx + bar_width / 2.0,
136                y: by - LABEL_MARGIN,
137                font_size: AXIS_LABEL_FONT,
138                color: LABEL_COLOR,
139                anchor: TextAnchor::Center,
140            });
141        }
142
143        // X-axis label
144        if config.show_labels {
145            primitives.push(ChartPrimitive::Label {
146                text: dp.label.clone(),
147                x: bx + bar_width / 2.0,
148                y: plot_bottom + AXIS_LABEL_FONT + LABEL_MARGIN,
149                font_size: AXIS_LABEL_FONT,
150                color: LABEL_COLOR,
151                anchor: TextAnchor::Center,
152            });
153        }
154    }
155
156    // Title
157    if let Some(ref title) = config.title {
158        primitives.push(ChartPrimitive::Label {
159            text: title.clone(),
160            x: width / 2.0,
161            y: TITLE_FONT,
162            font_size: TITLE_FONT,
163            color: Color::BLACK,
164            anchor: TextAnchor::Center,
165        });
166    }
167
168    primitives
169}