use crate::components::{Box as RnkBox, Line, Span, Text};
use crate::core::{Color, Element, FlexDirection};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BarChartOrientation {
#[default]
Horizontal,
Vertical,
}
#[derive(Debug, Clone)]
pub struct Bar {
pub label: String,
pub value: f64,
pub color: Option<Color>,
}
impl Bar {
pub fn new(label: impl Into<String>, value: f64) -> Self {
Self {
label: label.into(),
value,
color: None,
}
}
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
}
#[derive(Debug, Clone)]
pub struct BarChart {
bars: Vec<Bar>,
orientation: BarChartOrientation,
bar_max_size: u16,
show_values: bool,
show_labels: bool,
default_color: Option<Color>,
bar_char: char,
bar_gap: u16,
key: Option<String>,
}
impl BarChart {
pub fn new() -> Self {
Self {
bars: Vec::new(),
orientation: BarChartOrientation::Horizontal,
bar_max_size: 20,
show_values: true,
show_labels: true,
default_color: None,
bar_char: '█',
bar_gap: 1,
key: None,
}
}
pub fn from_bars<I>(bars: I) -> Self
where
I: IntoIterator<Item = Bar>,
{
Self {
bars: bars.into_iter().collect(),
..Self::new()
}
}
pub fn bars<I>(mut self, bars: I) -> Self
where
I: IntoIterator<Item = Bar>,
{
self.bars = bars.into_iter().collect();
self
}
pub fn bar(mut self, bar: Bar) -> Self {
self.bars.push(bar);
self
}
pub fn orientation(mut self, orientation: BarChartOrientation) -> Self {
self.orientation = orientation;
self
}
pub fn horizontal(mut self) -> Self {
self.orientation = BarChartOrientation::Horizontal;
self
}
pub fn vertical(mut self) -> Self {
self.orientation = BarChartOrientation::Vertical;
self
}
pub fn bar_max_size(mut self, size: u16) -> Self {
self.bar_max_size = size;
self
}
pub fn show_values(mut self, show: bool) -> Self {
self.show_values = show;
self
}
pub fn show_labels(mut self, show: bool) -> Self {
self.show_labels = show;
self
}
pub fn default_color(mut self, color: Color) -> Self {
self.default_color = Some(color);
self
}
pub fn bar_char(mut self, ch: char) -> Self {
self.bar_char = ch;
self
}
pub fn bar_gap(mut self, gap: u16) -> Self {
self.bar_gap = gap;
self
}
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn into_element(self) -> Element {
if self.bars.is_empty() {
return RnkBox::new().into_element();
}
match self.orientation {
BarChartOrientation::Horizontal => self.render_horizontal(),
BarChartOrientation::Vertical => self.render_vertical(),
}
}
fn render_horizontal(self) -> Element {
let max_value = self.bars.iter().map(|b| b.value).fold(0.0f64, f64::max);
let max_label_len = if self.show_labels {
self.bars.iter().map(|b| b.label.len()).max().unwrap_or(0)
} else {
0
};
let mut container = RnkBox::new().flex_direction(FlexDirection::Column);
if let Some(ref key) = self.key {
container = container.key(key.clone());
}
for bar in &self.bars {
let mut spans = Vec::new();
if self.show_labels {
let padded_label = format!("{:>width$} ", bar.label, width = max_label_len);
spans.push(Span::new(padded_label));
}
let bar_len = if max_value > 0.0 {
((bar.value / max_value) * self.bar_max_size as f64).round() as usize
} else {
0
};
let bar_str: String = std::iter::repeat_n(self.bar_char, bar_len).collect();
let mut bar_span = Span::new(bar_str);
if let Some(color) = bar.color.or(self.default_color) {
bar_span = bar_span.color(color);
}
spans.push(bar_span);
if self.show_values {
let value_str = if bar.value == bar.value.trunc() {
format!(" {:.0}", bar.value)
} else {
format!(" {:.1}", bar.value)
};
spans.push(Span::new(value_str).dim());
}
let text = Text::line(Line::from_spans(spans));
container = container.child(text.into_element());
}
container.into_element()
}
fn render_vertical(self) -> Element {
let max_value = self.bars.iter().map(|b| b.value).fold(0.0f64, f64::max);
let height = self.bar_max_size as usize;
let mut container = RnkBox::new().flex_direction(FlexDirection::Column);
if let Some(ref key) = self.key {
container = container.key(key.clone());
}
for row in (0..height).rev() {
let threshold = (row as f64 + 0.5) / height as f64 * max_value;
let mut spans = Vec::new();
for (i, bar) in self.bars.iter().enumerate() {
if i > 0 {
spans.push(Span::new(" ".repeat(self.bar_gap as usize)));
}
if bar.value >= threshold {
let mut bar_span = Span::new(self.bar_char.to_string());
if let Some(color) = bar.color.or(self.default_color) {
bar_span = bar_span.color(color);
}
spans.push(bar_span);
} else {
spans.push(Span::new(" "));
}
}
let text = Text::line(Line::from_spans(spans));
container = container.child(text.into_element());
}
if self.show_labels {
let mut label_spans = Vec::new();
for (i, bar) in self.bars.iter().enumerate() {
if i > 0 {
label_spans.push(Span::new(" ".repeat(self.bar_gap as usize)));
}
let label_char = bar.label.chars().next().unwrap_or(' ');
label_spans.push(Span::new(label_char.to_string()));
}
let label_text = Text::line(Line::from_spans(label_spans));
container = container.child(label_text.into_element());
}
container.into_element()
}
}
impl Default for BarChart {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bar_creation() {
let bar = Bar::new("CPU", 75.0).color(Color::Green);
assert_eq!(bar.label, "CPU");
assert_eq!(bar.value, 75.0);
assert_eq!(bar.color, Some(Color::Green));
}
#[test]
fn test_barchart_creation() {
let chart = BarChart::from_bars(vec![
Bar::new("A", 10.0),
Bar::new("B", 20.0),
Bar::new("C", 30.0),
]);
assert_eq!(chart.bars.len(), 3);
}
#[test]
fn test_barchart_orientation() {
let horizontal = BarChart::new().horizontal();
assert_eq!(horizontal.orientation, BarChartOrientation::Horizontal);
let vertical = BarChart::new().vertical();
assert_eq!(vertical.orientation, BarChartOrientation::Vertical);
}
#[test]
fn test_barchart_empty() {
let chart = BarChart::new();
let _ = chart.into_element();
}
}