1use super::*;
4use crate::model::ChartDataPoint;
5
6pub 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
15pub 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 let title_offset = if config.title.is_some() {
30 TITLE_HEIGHT
31 } else {
32 0.0
33 };
34
35 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 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 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 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 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 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 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 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 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}