use pineal_render::{Canvas, Color, Rect};
#[derive(Debug, Clone, Copy)]
pub struct Bar {
pub value: f64,
pub color: Color,
}
impl Bar {
pub fn new(value: f64, color: Color) -> Self {
Self { value, color }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Orientation {
Vertical,
Horizontal,
}
#[derive(Debug, Clone, Copy)]
pub struct BarStyle {
pub orientation: Orientation,
pub gap_ratio: f32,
pub baseline: f64,
pub range: Option<(f64, f64)>,
}
impl Default for BarStyle {
fn default() -> Self {
Self {
orientation: Orientation::Vertical,
gap_ratio: 0.18,
baseline: 0.0,
range: None,
}
}
}
impl BarStyle {
pub fn vertical() -> Self {
Self::default()
}
pub fn horizontal() -> Self {
Self {
orientation: Orientation::Horizontal,
..Self::default()
}
}
pub fn with_gap(mut self, gap_ratio: f32) -> Self {
self.gap_ratio = gap_ratio.clamp(0.0, 0.95);
self
}
pub fn with_baseline(mut self, baseline: f64) -> Self {
self.baseline = baseline;
self
}
pub fn with_range(mut self, lo: f64, hi: f64) -> Self {
self.range = Some((lo, hi));
self
}
}
struct Axis {
area: Rect,
vmin: f64,
vmax: f64,
orientation: Orientation,
}
impl Axis {
fn new(area: Rect, vmin: f64, vmax: f64, orientation: Orientation) -> Self {
let (vmin, vmax) = if (vmax - vmin).abs() < f64::EPSILON {
(vmin - 0.5, vmax + 0.5)
} else {
(vmin, vmax)
};
Self { area, vmin, vmax, orientation }
}
fn cat_span(&self) -> (f32, f32) {
match self.orientation {
Orientation::Vertical => (self.area.x, self.area.x + self.area.w),
Orientation::Horizontal => (self.area.y, self.area.y + self.area.h),
}
}
fn value_px(&self, v: f64) -> f32 {
let t = ((v - self.vmin) / (self.vmax - self.vmin)) as f32;
match self.orientation {
Orientation::Vertical => self.area.y + self.area.h * (1.0 - t),
Orientation::Horizontal => self.area.x + self.area.w * t,
}
}
fn bar_rect(&self, cat_lo: f32, cat_hi: f32, v_from: f64, v_to: f64) -> Rect {
let p0 = self.value_px(v_from);
let p1 = self.value_px(v_to);
let (lo, hi) = (p0.min(p1), p0.max(p1));
match self.orientation {
Orientation::Vertical => Rect::new(cat_lo, lo, cat_hi - cat_lo, hi - lo),
Orientation::Horizontal => Rect::new(lo, cat_lo, hi - lo, cat_hi - cat_lo),
}
}
}
fn slot(span0: f32, span1: f32, n: usize, i: usize, gap_ratio: f32) -> (f32, f32) {
let total = span1 - span0;
let w = total / n as f32;
let pad = w * gap_ratio * 0.5;
let lo = span0 + w * i as f32 + pad;
let hi = span0 + w * (i + 1) as f32 - pad;
(lo, hi)
}
fn auto_range(values: impl Iterator<Item = f64>, baseline: f64) -> (f64, f64) {
let mut lo = baseline;
let mut hi = baseline;
for v in values {
if v < lo {
lo = v;
}
if v > hi {
hi = v;
}
}
(lo, hi)
}
pub fn paint_bars(bars: &[Bar], area: Rect, style: &BarStyle, canvas: &mut dyn Canvas) {
if bars.is_empty() {
return;
}
let (vmin, vmax) = style
.range
.unwrap_or_else(|| auto_range(bars.iter().map(|b| b.value), style.baseline));
let axis = Axis::new(area, vmin, vmax, style.orientation);
let (s0, s1) = axis.cat_span();
for (i, bar) in bars.iter().enumerate() {
let (lo, hi) = slot(s0, s1, bars.len(), i, style.gap_ratio);
let r = axis.bar_rect(lo, hi, style.baseline, bar.value);
if r.w > 0.0 && r.h > 0.0 {
canvas.fill_rect(r, bar.color);
}
}
}
pub fn paint_grouped(series: &[&[Bar]], area: Rect, style: &BarStyle, canvas: &mut dyn Canvas) {
let n_series = series.len();
if n_series == 0 {
return;
}
let n_cats = series.iter().map(|s| s.len()).max().unwrap_or(0);
if n_cats == 0 {
return;
}
let all = series.iter().flat_map(|s| s.iter().map(|b| b.value));
let (vmin, vmax) = style.range.unwrap_or_else(|| auto_range(all, style.baseline));
let axis = Axis::new(area, vmin, vmax, style.orientation);
let (s0, s1) = axis.cat_span();
for cat in 0..n_cats {
let (clo, chi) = slot(s0, s1, n_cats, cat, 0.0);
for (k, serie) in series.iter().enumerate() {
let Some(bar) = serie.get(cat) else { continue };
let (lo, hi) = slot(clo, chi, n_series, k, style.gap_ratio);
let r = axis.bar_rect(lo, hi, style.baseline, bar.value);
if r.w > 0.0 && r.h > 0.0 {
canvas.fill_rect(r, bar.color);
}
}
}
}
pub fn paint_stacked(stacks: &[&[Bar]], area: Rect, style: &BarStyle, canvas: &mut dyn Canvas) {
if stacks.is_empty() {
return;
}
let mut lo = style.baseline;
let mut hi = style.baseline;
for stack in stacks {
let mut acc = style.baseline;
for seg in stack.iter() {
acc += seg.value;
lo = lo.min(acc);
hi = hi.max(acc);
}
}
let (vmin, vmax) = style.range.unwrap_or((lo, hi));
let axis = Axis::new(area, vmin, vmax, style.orientation);
let (s0, s1) = axis.cat_span();
for (c, stack) in stacks.iter().enumerate() {
let (clo, chi) = slot(s0, s1, stacks.len(), c, style.gap_ratio);
let mut acc = style.baseline;
for seg in stack.iter() {
let from = acc;
acc += seg.value;
let r = axis.bar_rect(clo, chi, from, acc);
if r.w > 0.0 && r.h > 0.0 {
canvas.fill_rect(r, seg.color);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pineal_render::{PlanRecorder, RenderCmd};
fn fill_rects(rec: PlanRecorder) -> Vec<Rect> {
rec.into_plan()
.cmds
.into_iter()
.filter_map(|c| match c {
RenderCmd::FillRect { rect, .. } => Some(rect),
_ => None,
})
.collect()
}
#[test]
fn one_rect_per_bar() {
let bars = [
Bar::new(3.0, Color::WHITE),
Bar::new(5.0, Color::BLACK),
Bar::new(1.0, Color::from_hex(0x00ff00)),
];
let mut rec = PlanRecorder::new();
paint_bars(&bars, Rect::new(0.0, 0.0, 300.0, 200.0), &BarStyle::vertical(), &mut rec);
assert_eq!(fill_rects(rec).len(), 3);
}
#[test]
fn taller_value_taller_bar() {
let bars = [Bar::new(1.0, Color::WHITE), Bar::new(4.0, Color::WHITE)];
let mut rec = PlanRecorder::new();
paint_bars(&bars, Rect::new(0.0, 0.0, 200.0, 100.0), &BarStyle::vertical(), &mut rec);
let rects = fill_rects(rec);
assert!(rects[1].h > rects[0].h, "la barra de mayor valor debe ser más alta");
}
#[test]
fn negative_grows_below_baseline() {
let bars = [Bar::new(2.0, Color::WHITE), Bar::new(-2.0, Color::WHITE)];
let style = BarStyle::vertical().with_range(-3.0, 3.0);
let mut rec = PlanRecorder::new();
paint_bars(&bars, Rect::new(0.0, 0.0, 200.0, 100.0), &style, &mut rec);
let rects = fill_rects(rec);
assert!(rects[0].y < 50.0, "positivo arriba del baseline");
assert!(rects[1].y >= 50.0 - f32::EPSILON, "negativo en/abajo del baseline");
}
#[test]
fn horizontal_swaps_axes() {
let bars = [Bar::new(1.0, Color::WHITE), Bar::new(4.0, Color::WHITE)];
let mut rec = PlanRecorder::new();
paint_bars(&bars, Rect::new(0.0, 0.0, 200.0, 100.0), &BarStyle::horizontal(), &mut rec);
let rects = fill_rects(rec);
assert!(rects[1].w > rects[0].w, "mayor valor = barra más larga (w)");
}
#[test]
fn grouped_emits_all_bars() {
let a = [Bar::new(1.0, Color::WHITE), Bar::new(2.0, Color::WHITE)];
let b = [Bar::new(3.0, Color::BLACK), Bar::new(4.0, Color::BLACK)];
let series: [&[Bar]; 2] = [&a, &b];
let mut rec = PlanRecorder::new();
paint_grouped(&series, Rect::new(0.0, 0.0, 400.0, 200.0), &BarStyle::vertical(), &mut rec);
assert_eq!(fill_rects(rec).len(), 4);
}
#[test]
fn stacked_segments_dont_overlap() {
let s0 = [Bar::new(2.0, Color::WHITE), Bar::new(3.0, Color::BLACK)];
let stacks: [&[Bar]; 1] = [&s0];
let mut rec = PlanRecorder::new();
paint_stacked(&stacks, Rect::new(0.0, 0.0, 100.0, 100.0), &BarStyle::vertical(), &mut rec);
let rects = fill_rects(rec);
assert_eq!(rects.len(), 2);
let bottom0 = rects[0].y + rects[0].h;
let bottom1 = rects[1].y + rects[1].h;
assert!(bottom1 <= bottom0 + 0.01 && rects[1].y < rects[0].y);
}
}