1use pineal_render::{Canvas, Color, Rect};
6
7#[derive(Debug, Clone, Copy)]
9pub struct Bar {
10 pub value: f64,
11 pub color: Color,
12}
13
14impl Bar {
15 pub fn new(value: f64, color: Color) -> Self {
16 Self { value, color }
17 }
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Orientation {
23 Vertical,
25 Horizontal,
27}
28
29#[derive(Debug, Clone, Copy)]
31pub struct BarStyle {
32 pub orientation: Orientation,
33 pub gap_ratio: f32,
36 pub baseline: f64,
39 pub range: Option<(f64, f64)>,
42}
43
44impl Default for BarStyle {
45 fn default() -> Self {
46 Self {
47 orientation: Orientation::Vertical,
48 gap_ratio: 0.18,
49 baseline: 0.0,
50 range: None,
51 }
52 }
53}
54
55impl BarStyle {
56 pub fn vertical() -> Self {
57 Self::default()
58 }
59 pub fn horizontal() -> Self {
60 Self {
61 orientation: Orientation::Horizontal,
62 ..Self::default()
63 }
64 }
65 pub fn with_gap(mut self, gap_ratio: f32) -> Self {
66 self.gap_ratio = gap_ratio.clamp(0.0, 0.95);
67 self
68 }
69 pub fn with_baseline(mut self, baseline: f64) -> Self {
70 self.baseline = baseline;
71 self
72 }
73 pub fn with_range(mut self, lo: f64, hi: f64) -> Self {
74 self.range = Some((lo, hi));
75 self
76 }
77}
78
79struct Axis {
83 area: Rect,
84 vmin: f64,
85 vmax: f64,
86 orientation: Orientation,
87}
88
89impl Axis {
90 fn new(area: Rect, vmin: f64, vmax: f64, orientation: Orientation) -> Self {
91 let (vmin, vmax) = if (vmax - vmin).abs() < f64::EPSILON {
93 (vmin - 0.5, vmax + 0.5)
94 } else {
95 (vmin, vmax)
96 };
97 Self { area, vmin, vmax, orientation }
98 }
99
100 fn cat_span(&self) -> (f32, f32) {
102 match self.orientation {
103 Orientation::Vertical => (self.area.x, self.area.x + self.area.w),
104 Orientation::Horizontal => (self.area.y, self.area.y + self.area.h),
105 }
106 }
107
108 fn value_px(&self, v: f64) -> f32 {
111 let t = ((v - self.vmin) / (self.vmax - self.vmin)) as f32;
112 match self.orientation {
113 Orientation::Vertical => self.area.y + self.area.h * (1.0 - t),
114 Orientation::Horizontal => self.area.x + self.area.w * t,
115 }
116 }
117
118 fn bar_rect(&self, cat_lo: f32, cat_hi: f32, v_from: f64, v_to: f64) -> Rect {
121 let p0 = self.value_px(v_from);
122 let p1 = self.value_px(v_to);
123 let (lo, hi) = (p0.min(p1), p0.max(p1));
124 match self.orientation {
125 Orientation::Vertical => Rect::new(cat_lo, lo, cat_hi - cat_lo, hi - lo),
126 Orientation::Horizontal => Rect::new(lo, cat_lo, hi - lo, cat_hi - cat_lo),
127 }
128 }
129}
130
131fn slot(span0: f32, span1: f32, n: usize, i: usize, gap_ratio: f32) -> (f32, f32) {
134 let total = span1 - span0;
135 let w = total / n as f32;
136 let pad = w * gap_ratio * 0.5;
137 let lo = span0 + w * i as f32 + pad;
138 let hi = span0 + w * (i + 1) as f32 - pad;
139 (lo, hi)
140}
141
142fn auto_range(values: impl Iterator<Item = f64>, baseline: f64) -> (f64, f64) {
143 let mut lo = baseline;
144 let mut hi = baseline;
145 for v in values {
146 if v < lo {
147 lo = v;
148 }
149 if v > hi {
150 hi = v;
151 }
152 }
153 (lo, hi)
154}
155
156pub fn paint_bars(bars: &[Bar], area: Rect, style: &BarStyle, canvas: &mut dyn Canvas) {
159 if bars.is_empty() {
160 return;
161 }
162 let (vmin, vmax) = style
163 .range
164 .unwrap_or_else(|| auto_range(bars.iter().map(|b| b.value), style.baseline));
165 let axis = Axis::new(area, vmin, vmax, style.orientation);
166 let (s0, s1) = axis.cat_span();
167 for (i, bar) in bars.iter().enumerate() {
168 let (lo, hi) = slot(s0, s1, bars.len(), i, style.gap_ratio);
169 let r = axis.bar_rect(lo, hi, style.baseline, bar.value);
170 if r.w > 0.0 && r.h > 0.0 {
171 canvas.fill_rect(r, bar.color);
172 }
173 }
174}
175
176pub fn paint_grouped(series: &[&[Bar]], area: Rect, style: &BarStyle, canvas: &mut dyn Canvas) {
181 let n_series = series.len();
182 if n_series == 0 {
183 return;
184 }
185 let n_cats = series.iter().map(|s| s.len()).max().unwrap_or(0);
186 if n_cats == 0 {
187 return;
188 }
189 let all = series.iter().flat_map(|s| s.iter().map(|b| b.value));
190 let (vmin, vmax) = style.range.unwrap_or_else(|| auto_range(all, style.baseline));
191 let axis = Axis::new(area, vmin, vmax, style.orientation);
192 let (s0, s1) = axis.cat_span();
193 for cat in 0..n_cats {
194 let (clo, chi) = slot(s0, s1, n_cats, cat, 0.0);
197 for (k, serie) in series.iter().enumerate() {
198 let Some(bar) = serie.get(cat) else { continue };
199 let (lo, hi) = slot(clo, chi, n_series, k, style.gap_ratio);
200 let r = axis.bar_rect(lo, hi, style.baseline, bar.value);
201 if r.w > 0.0 && r.h > 0.0 {
202 canvas.fill_rect(r, bar.color);
203 }
204 }
205 }
206}
207
208pub fn paint_stacked(stacks: &[&[Bar]], area: Rect, style: &BarStyle, canvas: &mut dyn Canvas) {
212 if stacks.is_empty() {
213 return;
214 }
215 let mut lo = style.baseline;
217 let mut hi = style.baseline;
218 for stack in stacks {
219 let mut acc = style.baseline;
220 for seg in stack.iter() {
221 acc += seg.value;
222 lo = lo.min(acc);
223 hi = hi.max(acc);
224 }
225 }
226 let (vmin, vmax) = style.range.unwrap_or((lo, hi));
227 let axis = Axis::new(area, vmin, vmax, style.orientation);
228 let (s0, s1) = axis.cat_span();
229 for (c, stack) in stacks.iter().enumerate() {
230 let (clo, chi) = slot(s0, s1, stacks.len(), c, style.gap_ratio);
231 let mut acc = style.baseline;
232 for seg in stack.iter() {
233 let from = acc;
234 acc += seg.value;
235 let r = axis.bar_rect(clo, chi, from, acc);
236 if r.w > 0.0 && r.h > 0.0 {
237 canvas.fill_rect(r, seg.color);
238 }
239 }
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use pineal_render::{PlanRecorder, RenderCmd};
247
248 fn fill_rects(rec: PlanRecorder) -> Vec<Rect> {
249 rec.into_plan()
250 .cmds
251 .into_iter()
252 .filter_map(|c| match c {
253 RenderCmd::FillRect { rect, .. } => Some(rect),
254 _ => None,
255 })
256 .collect()
257 }
258
259 #[test]
260 fn one_rect_per_bar() {
261 let bars = [
262 Bar::new(3.0, Color::WHITE),
263 Bar::new(5.0, Color::BLACK),
264 Bar::new(1.0, Color::from_hex(0x00ff00)),
265 ];
266 let mut rec = PlanRecorder::new();
267 paint_bars(&bars, Rect::new(0.0, 0.0, 300.0, 200.0), &BarStyle::vertical(), &mut rec);
268 assert_eq!(fill_rects(rec).len(), 3);
269 }
270
271 #[test]
272 fn taller_value_taller_bar() {
273 let bars = [Bar::new(1.0, Color::WHITE), Bar::new(4.0, Color::WHITE)];
274 let mut rec = PlanRecorder::new();
275 paint_bars(&bars, Rect::new(0.0, 0.0, 200.0, 100.0), &BarStyle::vertical(), &mut rec);
276 let rects = fill_rects(rec);
277 assert!(rects[1].h > rects[0].h, "la barra de mayor valor debe ser más alta");
278 }
279
280 #[test]
281 fn negative_grows_below_baseline() {
282 let bars = [Bar::new(2.0, Color::WHITE), Bar::new(-2.0, Color::WHITE)];
285 let style = BarStyle::vertical().with_range(-3.0, 3.0);
286 let mut rec = PlanRecorder::new();
287 paint_bars(&bars, Rect::new(0.0, 0.0, 200.0, 100.0), &style, &mut rec);
288 let rects = fill_rects(rec);
289 assert!(rects[0].y < 50.0, "positivo arriba del baseline");
292 assert!(rects[1].y >= 50.0 - f32::EPSILON, "negativo en/abajo del baseline");
293 }
294
295 #[test]
296 fn horizontal_swaps_axes() {
297 let bars = [Bar::new(1.0, Color::WHITE), Bar::new(4.0, Color::WHITE)];
298 let mut rec = PlanRecorder::new();
299 paint_bars(&bars, Rect::new(0.0, 0.0, 200.0, 100.0), &BarStyle::horizontal(), &mut rec);
300 let rects = fill_rects(rec);
301 assert!(rects[1].w > rects[0].w, "mayor valor = barra más larga (w)");
303 }
304
305 #[test]
306 fn grouped_emits_all_bars() {
307 let a = [Bar::new(1.0, Color::WHITE), Bar::new(2.0, Color::WHITE)];
308 let b = [Bar::new(3.0, Color::BLACK), Bar::new(4.0, Color::BLACK)];
309 let series: [&[Bar]; 2] = [&a, &b];
310 let mut rec = PlanRecorder::new();
311 paint_grouped(&series, Rect::new(0.0, 0.0, 400.0, 200.0), &BarStyle::vertical(), &mut rec);
312 assert_eq!(fill_rects(rec).len(), 4);
313 }
314
315 #[test]
316 fn stacked_segments_dont_overlap() {
317 let s0 = [Bar::new(2.0, Color::WHITE), Bar::new(3.0, Color::BLACK)];
318 let stacks: [&[Bar]; 1] = [&s0];
319 let mut rec = PlanRecorder::new();
320 paint_stacked(&stacks, Rect::new(0.0, 0.0, 100.0, 100.0), &BarStyle::vertical(), &mut rec);
321 let rects = fill_rects(rec);
322 assert_eq!(rects.len(), 2);
323 let bottom0 = rects[0].y + rects[0].h;
327 let bottom1 = rects[1].y + rects[1].h;
328 assert!(bottom1 <= bottom0 + 0.01 && rects[1].y < rects[0].y);
329 }
330}