use crate::render::Cell;
use crate::style::Color;
use crate::utils::{char_width, truncate_to_width};
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BarOrientation {
#[default]
Horizontal,
Vertical,
}
#[derive(Clone, Debug)]
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
}
}
pub struct BarChart {
bars: Vec<Bar>,
orientation: BarOrientation,
max: Option<f64>,
bar_width: u16,
gap: u16,
show_values: bool,
fg: Color,
label_width: Option<u16>,
props: WidgetProps,
}
impl BarChart {
pub fn new() -> Self {
Self {
bars: Vec::new(),
orientation: BarOrientation::default(),
max: None,
bar_width: 1,
gap: 1,
show_values: true,
fg: Color::CYAN,
label_width: None,
props: WidgetProps::new(),
}
}
pub fn bar(mut self, label: impl Into<String>, value: f64) -> Self {
self.bars.push(Bar::new(label, value));
self
}
pub fn bar_colored(mut self, label: impl Into<String>, value: f64, color: Color) -> Self {
self.bars.push(Bar::new(label, value).color(color));
self
}
pub fn data<I, S>(mut self, data: I) -> Self
where
I: IntoIterator<Item = (S, f64)>,
S: Into<String>,
{
for (label, value) in data {
self.bars.push(Bar::new(label, value));
}
self
}
pub fn orientation(mut self, orientation: BarOrientation) -> Self {
self.orientation = orientation;
self
}
pub fn horizontal(mut self) -> Self {
self.orientation = BarOrientation::Horizontal;
self
}
pub fn vertical(mut self) -> Self {
self.orientation = BarOrientation::Vertical;
self
}
pub fn max(mut self, max: f64) -> Self {
self.max = Some(max);
self
}
pub fn bar_width(mut self, width: u16) -> Self {
self.bar_width = width.max(1);
self
}
pub fn gap(mut self, gap: u16) -> Self {
self.gap = gap;
self
}
pub fn show_values(mut self, show: bool) -> Self {
self.show_values = show;
self
}
pub fn fg(mut self, color: Color) -> Self {
self.fg = color;
self
}
pub fn label_width(mut self, width: u16) -> Self {
self.label_width = Some(width);
self
}
fn calculate_max(&self) -> f64 {
self.max.unwrap_or_else(|| {
self.bars
.iter()
.map(|b| b.value.abs())
.fold(f64::NEG_INFINITY, f64::max)
.max(1.0)
})
}
fn render_horizontal(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width == 0 || area.height == 0 || self.bars.is_empty() {
return;
}
let max_value = self.calculate_max();
let label_width = self.label_width.unwrap_or_else(|| {
self.bars
.iter()
.map(|b| crate::utils::display_width(&b.label) as u16)
.max()
.unwrap_or(0)
.min(area.width / 3)
});
let value_width = if self.show_values { 8 } else { 0 };
let bar_area_width = area.width.saturating_sub(label_width + 2 + value_width);
let mut y = 0u16;
for bar in &self.bars {
if y >= area.height {
break;
}
let bar_length = if max_value > 0.0 {
((bar.value.abs() / max_value) * bar_area_width as f64) as u16
} else {
0
};
let color = bar.color.unwrap_or(self.fg);
for row in 0..self.bar_width {
if y + row >= area.height {
break;
}
if row == 0 {
let label_dw = crate::utils::display_width(&bar.label);
let label: String = if label_dw > label_width as usize {
crate::utils::truncate_to_width(&bar.label, label_width as usize)
.to_string()
} else {
crate::utils::unicode::right_align_to_width(
&bar.label,
label_width as usize,
)
};
let mut dx: u16 = 0;
for ch in label.chars() {
if dx < area.width {
ctx.set(dx, y, Cell::new(ch));
}
dx += char_width(ch) as u16;
}
}
let bar_start = label_width + 1;
for i in 0..bar_length {
if bar_start + i < area.width {
let mut cell = Cell::new('█');
cell.fg = Some(color);
ctx.set(bar_start + i, y + row, cell);
}
}
if row == 0 && self.show_values {
let value_str = format!(" {:.1}", bar.value);
let value_x = bar_start + bar_length;
let mut dx: u16 = 0;
for ch in value_str.chars() {
if value_x + dx < area.width {
ctx.set(value_x + dx, y, Cell::new(ch));
}
dx += char_width(ch) as u16;
}
}
}
y += self.bar_width + self.gap;
}
}
fn render_vertical(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width == 0 || area.height == 0 || self.bars.is_empty() {
return;
}
let max_value = self.calculate_max();
let label_height = 1;
let value_height = if self.show_values { 1 } else { 0 };
let bar_area_height = area.height.saturating_sub(label_height + value_height);
let total_bar_width = self.bar_width + self.gap;
let mut x = 0u16;
for bar in &self.bars {
if x + self.bar_width > area.width {
break;
}
let bar_height = if max_value > 0.0 {
((bar.value / max_value) * bar_area_height as f64) as u16
} else {
0
};
let color = bar.color.unwrap_or(self.fg);
for row in 0..bar_height {
let y = bar_area_height - 1 - row;
for col in 0..self.bar_width {
if x + col < area.width {
let mut cell = Cell::new('█');
cell.fg = Some(color);
ctx.set(x + col, y, cell);
}
}
}
if self.show_values && bar_area_height > 0 {
let value_str = format!("{:.0}", bar.value);
let value_y = bar_area_height - bar_height.saturating_sub(1).min(bar_area_height);
let mut dx: u16 = 0;
for ch in value_str.chars() {
if x + dx < area.width && value_y > 0 {
ctx.set(x + dx, value_y - 1, Cell::new(ch));
}
dx += char_width(ch) as u16;
}
}
if label_height > 0 {
let label_y = area.height - 1;
let label = truncate_to_width(&bar.label, self.bar_width as usize);
let mut dx: u16 = 0;
for ch in label.chars() {
if x + dx < area.width {
ctx.set(x + dx, label_y, Cell::new(ch));
}
dx += char_width(ch) as u16;
}
}
x += total_bar_width;
}
}
}
impl Default for BarChart {
fn default() -> Self {
Self::new()
}
}
impl View for BarChart {
crate::impl_view_meta!("BarChart");
fn render(&self, ctx: &mut RenderContext) {
match self.orientation {
BarOrientation::Horizontal => self.render_horizontal(ctx),
BarOrientation::Vertical => self.render_vertical(ctx),
}
}
}
impl_styled_view!(BarChart);
impl_props_builders!(BarChart);
pub fn barchart() -> BarChart {
BarChart::new()
}