feat(barchart): enable barchart groups (#288)

* feat(barchart): allow to add a group of bars

Example: to show the revenue of different companies:
┌────────────────────────┐
│             ████       │
│             ████       │
│      ████   ████       │
│ ▄▄▄▄ ████   ████ ████  │
│ ████ ████   ████ ████  │
│ ████ ████   ████ ████  │
│ █50█ █60█   █90█ █55█  │
│    Mars       April    │
└────────────────────────┘
new structs are introduced: Group and Bar.
the data function is modified to accept "impl Into<Group<'a>>".

a new function "group_gap" is introduced to set the gap between each group

unit test changed to allow the label to be in the center

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>

* feat(barchart)!: center labels by default

The bar labels are currently printed string from the left side of
bar. This commit centers the labels under the bar.

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>

---------

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
This commit is contained in:
Hichem 2023-07-13 12:27:08 +02:00 committed by GitHub
parent e66d5cdee0
commit ae8ed8867d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1074 additions and 501 deletions

View file

@ -11,10 +11,20 @@ use crossterm::{
};
use ratatui::prelude::*;
struct Company<'a> {
revenue: [u64; 4],
label: &'a str,
bar_style: Style,
}
struct App<'a> {
data: Vec<(&'a str, u64)>,
months: [&'a str; 4],
companies: [Company<'a>; 3],
}
const TOTAL_REVENUE: &str = "Total Revenue";
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
@ -44,6 +54,24 @@ impl<'a> App<'a> {
("B23", 3),
("B24", 5),
],
companies: [
Company {
label: "Comp.A",
revenue: [9, 12, 5, 8],
bar_style: Style::default().fg(Color::Green),
},
Company {
label: "Comp.B",
revenue: [1, 2, 3, 4],
bar_style: Style::default().fg(Color::Yellow),
},
Company {
label: "Comp.C",
revenue: [10, 10, 9, 4],
bar_style: Style::default().fg(Color::White),
},
],
months: ["Mars", "Apr", "May", "Jun"],
}
}
@ -112,8 +140,16 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.constraints(
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]
.as_ref(),
)
.split(f.size());
let barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.data(&app.data)
@ -122,35 +158,92 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
f.render_widget(barchart, chunks[0]);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
let barchart = BarChart::default()
.block(Block::default().title("Data2").borders(Borders::ALL))
.data(&app.data)
.bar_width(5)
.bar_gap(3)
.bar_style(Style::default().fg(Color::Green))
.value_style(
Style::default()
.bg(Color::Green)
.add_modifier(Modifier::BOLD),
);
f.render_widget(barchart, chunks[0]);
let barchart = BarChart::default()
.block(Block::default().title("Data3").borders(Borders::ALL))
.data(&app.data)
.bar_style(Style::default().fg(Color::Red))
.bar_width(7)
.bar_gap(0)
.value_style(Style::default().bg(Color::Red))
.label_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::ITALIC),
);
f.render_widget(barchart, chunks[1]);
draw_bar_with_group_labels(f, app, chunks[1], false);
draw_bar_with_group_labels(f, app, chunks[2], true);
}
fn draw_bar_with_group_labels<B>(f: &mut Frame<B>, app: &App, area: Rect, bar_labels: bool)
where
B: Backend,
{
let groups: Vec<BarGroup> = app
.months
.iter()
.enumerate()
.map(|(i, &month)| {
let bars: Vec<Bar> = app
.companies
.iter()
.map(|c| {
let mut bar = Bar::default()
.value(c.revenue[i])
.style(c.bar_style)
.value_style(
Style::default()
.bg(c.bar_style.fg.unwrap())
.fg(Color::Black),
);
if bar_labels {
bar = bar.label(c.label.into());
}
bar
})
.collect();
BarGroup::default().label(month.into()).bars(&bars)
})
.collect();
let mut barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.bar_width(7)
.group_gap(3);
for group in groups {
barchart = barchart.data(group)
}
f.render_widget(barchart, area);
const LEGEND_HEIGHT: u16 = 6;
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
let legend_area = Rect {
height: LEGEND_HEIGHT,
width: TOTAL_REVENUE.len() as u16 + 2,
y: area.y,
x: area.x,
};
draw_legend(f, legend_area);
}
}
fn draw_legend<B>(f: &mut Frame<B>, area: Rect)
where
B: Backend,
{
let text = vec![
Line::from(Span::styled(
TOTAL_REVENUE,
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::White),
)),
Line::from(Span::styled(
"- Company A",
Style::default().fg(Color::Green),
)),
Line::from(Span::styled(
"- Company B",
Style::default().fg(Color::Yellow),
)),
Line::from(vec![Span::styled(
"- Company C",
Style::default().fg(Color::White),
)]),
];
let block = Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White));
let paragraph = Paragraph::new(text).block(block);
f.render_widget(paragraph, area);
}

View file

@ -213,9 +213,9 @@ pub mod prelude {
widgets::{
block::{Block, Position as BlockTitlePosition, Title as BlockTitle},
canvas::{Canvas, Circle, Line as CanvasLine, Map, MapResolution, Rectangle},
Axis, BarChart, BorderType, Borders, Cell, Chart, Clear, Dataset, Gauge, GraphType,
LineGauge, List, ListItem, ListState, Padding, Paragraph, RenderDirection, Row,
ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState, Sparkline,
Axis, Bar, BarChart, BarGroup, BorderType, Borders, Cell, Chart, Clear, Dataset, Gauge,
GraphType, LineGauge, List, ListItem, ListState, Padding, Paragraph, RenderDirection,
Row, ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState, Sparkline,
StatefulWidget, Table, TableState, Tabs, Widget, Wrap,
},
};

View file

@ -1,463 +0,0 @@
use std::cmp::min;
use unicode_width::UnicodeWidthStr;
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols,
widgets::{Block, Widget},
};
/// Display multiple bars in a single widgets
///
/// # Examples
///
/// ```
/// # use ratatui::widgets::{Block, Borders, BarChart};
/// # use ratatui::style::{Style, Color, Modifier};
/// BarChart::default()
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
/// .bar_width(3)
/// .bar_gap(1)
/// .bar_style(Style::default().fg(Color::Yellow).bg(Color::Red))
/// .value_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
/// .label_style(Style::default().fg(Color::White))
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
/// .max(4);
/// ```
#[derive(Debug, Clone)]
pub struct BarChart<'a> {
/// Block to wrap the widget in
block: Option<Block<'a>>,
/// The width of each bar
bar_width: u16,
/// The gap between each bar
bar_gap: u16,
/// Set of symbols used to display the data
bar_set: symbols::bar::Set,
/// Style of the bars
bar_style: Style,
/// Style of the values printed at the bottom of each bar
value_style: Style,
/// Style of the labels printed under each bar
label_style: Style,
/// Style for the widget
style: Style,
/// Slice of (label, value) pair to plot on the chart
data: &'a [(&'a str, u64)],
/// Value necessary for a bar to reach the maximum height (if no value is specified,
/// the maximum value in the data is taken as reference)
max: Option<u64>,
/// Values to display on the bar (computed when the data is passed to the widget)
values: Vec<String>,
}
impl<'a> Default for BarChart<'a> {
fn default() -> BarChart<'a> {
BarChart {
block: None,
max: None,
data: &[],
values: Vec::new(),
bar_style: Style::default(),
bar_width: 1,
bar_gap: 1,
bar_set: symbols::bar::NINE_LEVELS,
value_style: Style::default(),
label_style: Style::default(),
style: Style::default(),
}
}
}
impl<'a> BarChart<'a> {
pub fn data(mut self, data: &'a [(&'a str, u64)]) -> BarChart<'a> {
self.data = data;
self.values = Vec::with_capacity(self.data.len());
for &(_, v) in self.data {
self.values.push(format!("{v}"));
}
self
}
pub fn block(mut self, block: Block<'a>) -> BarChart<'a> {
self.block = Some(block);
self
}
pub fn max(mut self, max: u64) -> BarChart<'a> {
self.max = Some(max);
self
}
pub fn bar_style(mut self, style: Style) -> BarChart<'a> {
self.bar_style = style;
self
}
pub fn bar_width(mut self, width: u16) -> BarChart<'a> {
self.bar_width = width;
self
}
pub fn bar_gap(mut self, gap: u16) -> BarChart<'a> {
self.bar_gap = gap;
self
}
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> BarChart<'a> {
self.bar_set = bar_set;
self
}
pub fn value_style(mut self, style: Style) -> BarChart<'a> {
self.value_style = style;
self
}
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
self.label_style = style;
self
}
pub fn style(mut self, style: Style) -> BarChart<'a> {
self.style = style;
self
}
}
impl<'a> Widget for BarChart<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let chart_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
if chart_area.height < 2 {
return;
}
let max = self
.max
.unwrap_or_else(|| self.data.iter().map(|t| t.1).max().unwrap_or_default());
let max_index = min(
(chart_area.width / (self.bar_width + self.bar_gap)) as usize,
self.data.len(),
);
let mut data = self
.data
.iter()
.take(max_index)
.map(|&(l, v)| {
(
l,
v * u64::from(chart_area.height - 1) * 8 / std::cmp::max(max, 1),
)
})
.collect::<Vec<(&str, u64)>>();
for j in (0..chart_area.height - 1).rev() {
for (i, d) in data.iter_mut().enumerate() {
let symbol = match d.1 {
0 => self.bar_set.empty,
1 => self.bar_set.one_eighth,
2 => self.bar_set.one_quarter,
3 => self.bar_set.three_eighths,
4 => self.bar_set.half,
5 => self.bar_set.five_eighths,
6 => self.bar_set.three_quarters,
7 => self.bar_set.seven_eighths,
_ => self.bar_set.full,
};
for x in 0..self.bar_width {
buf.get_mut(
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap) + x,
chart_area.top() + j,
)
.set_symbol(symbol)
.set_style(self.bar_style);
}
if d.1 > 8 {
d.1 -= 8;
} else {
d.1 = 0;
}
}
}
for (i, &(label, value)) in self.data.iter().take(max_index).enumerate() {
if value != 0 {
let value_label = &self.values[i];
let width = value_label.width() as u16;
if width < self.bar_width {
buf.set_string(
chart_area.left()
+ i as u16 * (self.bar_width + self.bar_gap)
+ (self.bar_width - width) / 2,
chart_area.bottom() - 2,
value_label,
self.value_style,
);
}
}
buf.set_stringn(
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap),
chart_area.bottom() - 1,
label,
self.bar_width as usize,
self.label_style,
);
}
}
}
#[cfg(test)]
mod tests {
use itertools::iproduct;
use super::*;
use crate::{
assert_buffer_eq,
style::Color,
widgets::{BorderType, Borders},
};
#[test]
fn default() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let widget = BarChart::default();
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "; 3]));
}
#[test]
fn data() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default().data(&[("foo", 1), ("bar", 2)]);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
])
);
}
#[test]
fn block() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 5));
let block = Block::default()
.title("Block")
.border_type(BorderType::Double)
.borders(Borders::ALL);
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.block(block);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"╔Block════════╗",
"║ █ ║",
"║█ █ ║",
"║f b ║",
"╚═════════════╝",
])
);
}
#[test]
fn max() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let without_max = BarChart::default().data(&[("foo", 1), ("bar", 2), ("baz", 100)]);
without_max.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"",
"",
"f b b ",
])
);
let with_max = BarChart::default()
.data(&[("foo", 1), ("bar", 2), ("baz", 100)])
.max(2);
with_max.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" █ █ ",
"█ █ █ ",
"f b b ",
])
);
}
#[test]
fn bar_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_style(Style::default().fg(Color::Red));
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
]);
for (x, y) in iproduct!([0, 2], [0, 1]) {
expected.get_mut(x, y).set_fg(Color::Red);
}
assert_buffer_eq!(buffer, expected);
}
#[test]
fn bar_width() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_width(3);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ███ ",
"█1█ █2█ ",
"foo bar ",
])
);
}
#[test]
fn bar_gap() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_gap(2);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
])
);
}
#[test]
fn bar_set() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 0), ("bar", 1), ("baz", 3)])
.bar_set(symbols::bar::THREE_LEVELS);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"",
" ▄ █ ",
"f b b ",
])
);
}
#[test]
fn bar_set_nine_levels() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 18, 3));
let widget = BarChart::default()
.data(&[
("a", 0),
("b", 1),
("c", 2),
("d", 3),
("e", 4),
("f", 5),
("g", 6),
("h", 7),
("i", 8),
])
.bar_set(symbols::bar::NINE_LEVELS);
widget.render(Rect::new(0, 1, 18, 2), &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ",
"a b c d e f g h i ",
])
);
}
#[test]
fn value_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_width(3)
.value_style(Style::default().fg(Color::Red));
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
" ███ ",
"█1█ █2█ ",
"foo bar ",
]);
expected.get_mut(1, 1).set_fg(Color::Red);
expected.get_mut(5, 1).set_fg(Color::Red);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn label_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.label_style(Style::default().fg(Color::Red));
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
]);
expected.get_mut(0, 2).set_fg(Color::Red);
expected.get_mut(2, 2).set_fg(Color::Red);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.style(Style::default().fg(Color::Red));
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
]);
for (x, y) in iproduct!(0..15, 0..3) {
expected.get_mut(x, y).set_fg(Color::Red);
}
assert_buffer_eq!(buffer, expected);
}
#[test]
fn does_not_render_less_than_two_rows() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
let widget = BarChart::default().data(&[("foo", 1), ("bar", 2)]);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::empty(Rect::new(0, 0, 15, 1)));
}
}

View file

@ -0,0 +1,92 @@
use crate::{buffer::Buffer, style::Style, text::Line};
/// represent a bar to be shown by the Barchart
///
/// # Examples
/// the following example creates a bar with the label "Bar 1", a value "10",
/// red background and a white value foreground
///
/// ```
/// # use ratatui::prelude::*;
/// Bar::default()
/// .label("Bar 1".into())
/// .value(10)
/// .style(Style::default().fg(Color::Red))
/// .value_style(Style::default().bg(Color::Red).fg(Color::White));
/// ```
#[derive(Debug, Clone, Default)]
pub struct Bar<'a> {
/// Value to display on the bar (computed when the data is passed to the widget)
pub(super) value: u64,
/// optional label to be printed under the bar
pub(super) label: Option<Line<'a>>,
/// style for the bar
pub(super) style: Style,
/// style of the value printed at the bottom of the bar.
pub(super) value_style: Style,
}
impl<'a> Bar<'a> {
pub fn value(mut self, value: u64) -> Bar<'a> {
self.value = value;
self
}
pub fn label(mut self, label: Line<'a>) -> Bar<'a> {
self.label = Some(label);
self
}
pub fn style(mut self, style: Style) -> Bar<'a> {
self.style = style;
self
}
pub fn value_style(mut self, style: Style) -> Bar<'a> {
self.value_style = style;
self
}
/// render the bar's value
pub(super) fn render_value(
&self,
buf: &mut Buffer,
max_width: u16,
x: u16,
y: u16,
default_style: Style,
) {
if self.value != 0 {
let value_label = format!("{}", self.value);
let width = value_label.len() as u16;
if width < max_width {
buf.set_string(
x + (max_width.saturating_sub(value_label.len() as u16) >> 1),
y,
value_label,
self.value_style.patch(default_style),
);
}
}
}
/// render the bar's label
pub(super) fn render_label(
self,
buf: &mut Buffer,
max_width: u16,
x: u16,
y: u16,
default_style: Style,
) {
if let Some(mut label) = self.label {
label.patch_style(default_style);
buf.set_line(
x + (max_width.saturating_sub(label.width() as u16) >> 1),
y,
&label,
max_width,
);
}
}
}

View file

@ -0,0 +1,63 @@
use super::Bar;
use crate::text::Line;
/// represent a group of bars to be shown by the Barchart
///
/// # Examples
/// ```
/// # use ratatui::prelude::*;
/// BarGroup::default()
/// .label("Group 1".into())
/// .bars(&[Bar::default().value(200), Bar::default().value(150)]);
/// ```
#[derive(Debug, Clone, Default)]
pub struct BarGroup<'a> {
/// label of the group. It will be printed centered under this group of bars
pub(super) label: Option<Line<'a>>,
/// list of bars to be shown
pub(super) bars: Vec<Bar<'a>>,
}
impl<'a> BarGroup<'a> {
/// Set the group label
pub fn label(mut self, label: Line<'a>) -> BarGroup<'a> {
self.label = Some(label);
self
}
/// Set the bars of the group to be shown
pub fn bars(mut self, bars: &[Bar<'a>]) -> BarGroup<'a> {
self.bars = bars.to_vec();
self
}
/// return the maximum bar value of this group
pub(super) fn max(&self) -> Option<u64> {
self.bars.iter().max_by_key(|v| v.value).map(|v| v.value)
}
}
impl<'a> From<&[(&'a str, u64)]> for BarGroup<'a> {
fn from(value: &[(&'a str, u64)]) -> BarGroup<'a> {
BarGroup {
label: None,
bars: value
.iter()
.map(|&(text, v)| Bar::default().value(v).label(text.into()))
.collect(),
}
}
}
impl<'a, const N: usize> From<&[(&'a str, u64); N]> for BarGroup<'a> {
fn from(value: &[(&'a str, u64); N]) -> BarGroup<'a> {
Self::from(value.as_ref())
}
}
impl<'a> From<&Vec<(&'a str, u64)>> for BarGroup<'a> {
fn from(value: &Vec<(&'a str, u64)>) -> BarGroup<'a> {
let array: &[(&str, u64)] = value;
Self::from(array)
}
}

724
src/widgets/barchart/mod.rs Normal file
View file

@ -0,0 +1,724 @@
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols,
widgets::{Block, Widget},
};
mod bar;
mod bar_group;
pub use bar::Bar;
pub use bar_group::BarGroup;
/// Display multiple bars in a single widgets
///
/// # Examples
/// The following example creates a BarChart with two groups of bars.
/// The first group is added by an array slice (&[(&str, u64)]).
/// The second group is added by a slice of Groups (&[Group]).
/// ```
/// # use ratatui::prelude::*;
/// BarChart::default()
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
/// .bar_width(3)
/// .bar_gap(1)
/// .group_gap(3)
/// .bar_style(Style::default().fg(Color::Yellow).bg(Color::Red))
/// .value_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
/// .label_style(Style::default().fg(Color::White))
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]))
/// .max(4);
/// ```
#[derive(Debug, Clone)]
pub struct BarChart<'a> {
/// Block to wrap the widget in
block: Option<Block<'a>>,
/// The width of each bar
bar_width: u16,
/// The gap between each bar
bar_gap: u16,
/// The gap between each group
group_gap: u16,
/// Set of symbols used to display the data
bar_set: symbols::bar::Set,
/// Style of the bars
bar_style: Style,
/// Style of the values printed at the bottom of each bar
value_style: Style,
/// Style of the labels printed under each bar
label_style: Style,
/// Style for the widget
style: Style,
/// vector of groups containing bars
data: Vec<BarGroup<'a>>,
/// Value necessary for a bar to reach the maximum height (if no value is specified,
/// the maximum value in the data is taken as reference)
max: Option<u64>,
}
impl<'a> Default for BarChart<'a> {
fn default() -> BarChart<'a> {
BarChart {
block: None,
max: None,
data: Vec::new(),
bar_style: Style::default(),
bar_width: 1,
bar_gap: 1,
value_style: Style::default(),
label_style: Style::default(),
group_gap: 0,
bar_set: symbols::bar::NINE_LEVELS,
style: Style::default(),
}
}
}
impl<'a> BarChart<'a> {
/// Add group of bars to the BarChart
/// # Examples
/// The following example creates a BarChart with two groups of bars.
/// The first group is added by an array slice (&[(&str, u64)]).
/// The second group is added by a BarGroup instance.
/// ```
/// # use ratatui::prelude::*;
///
/// BarChart::default()
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]));
/// ```
pub fn data(mut self, data: impl Into<BarGroup<'a>>) -> BarChart<'a> {
self.data.push(data.into());
self
}
pub fn block(mut self, block: Block<'a>) -> BarChart<'a> {
self.block = Some(block);
self
}
pub fn max(mut self, max: u64) -> BarChart<'a> {
self.max = Some(max);
self
}
pub fn bar_style(mut self, style: Style) -> BarChart<'a> {
self.bar_style = style;
self
}
pub fn bar_width(mut self, width: u16) -> BarChart<'a> {
self.bar_width = width;
self
}
pub fn bar_gap(mut self, gap: u16) -> BarChart<'a> {
self.bar_gap = gap;
self
}
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> BarChart<'a> {
self.bar_set = bar_set;
self
}
pub fn value_style(mut self, style: Style) -> BarChart<'a> {
self.value_style = style;
self
}
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
self.label_style = style;
self
}
pub fn group_gap(mut self, gap: u16) -> BarChart<'a> {
self.group_gap = gap;
self
}
pub fn style(mut self, style: Style) -> BarChart<'a> {
self.style = style;
self
}
}
impl<'a> BarChart<'a> {
/// Check the bars, which fits inside the available space and removes
/// the bars and the groups, which are outside of the available space.
fn remove_invisible_groups_and_bars(&mut self, mut width: u16) {
for group_index in 0..self.data.len() {
let n_bars = self.data[group_index].bars.len() as u16;
let group_width = n_bars * self.bar_width + n_bars.saturating_sub(1) * self.bar_gap;
if width > group_width {
width = width.saturating_sub(group_width + self.group_gap + self.bar_gap);
} else {
let max_bars = (width + self.bar_gap) / (self.bar_width + self.bar_gap);
if max_bars == 0 {
self.data.truncate(group_index);
} else {
self.data[group_index].bars.truncate(max_bars as usize);
self.data.truncate(group_index + 1);
}
break;
}
}
}
/// Get the number of lines needed for the labels.
///
/// The number of lines depends on whether we need to print the bar labels and/or the group
/// labels.
/// - If there are no labels, return 0.
/// - If there are only bar labels, return 1.
/// - If there are only group labels, return 1.
/// - If there are both bar and group labels, return 2.
fn label_height(&self) -> u16 {
let has_group_labels = self.data.iter().any(|e| e.label.is_some());
let has_data_labels = self
.data
.iter()
.any(|e| e.bars.iter().any(|e| e.label.is_some()));
// convert true to 1 and false to 0 and add the two values
u16::from(has_group_labels) + u16::from(has_data_labels)
}
/// renders the block if there is one and updates the area to the inner area
fn render_block(&mut self, area: &mut Rect, buf: &mut Buffer) {
if let Some(block) = self.block.take() {
let inner_area = block.inner(*area);
block.render(*area, buf);
*area = inner_area
}
}
fn render_bars(&self, buf: &mut Buffer, bars_area: Rect, max: u64) {
// convert the bar values to ratatui::symbols::bar::Set
let mut groups: Vec<Vec<u64>> = self
.data
.iter()
.map(|group| {
group
.bars
.iter()
.map(|bar| bar.value * u64::from(bars_area.height) * 8 / max)
.collect()
})
.collect();
// print all visible bars (without labels and values)
for j in (0..bars_area.height).rev() {
let mut bar_x = bars_area.left();
for (group_data, group) in groups.iter_mut().zip(&self.data) {
for (d, bar) in group_data.iter_mut().zip(&group.bars) {
let symbol = match d {
0 => self.bar_set.empty,
1 => self.bar_set.one_eighth,
2 => self.bar_set.one_quarter,
3 => self.bar_set.three_eighths,
4 => self.bar_set.half,
5 => self.bar_set.five_eighths,
6 => self.bar_set.three_quarters,
7 => self.bar_set.seven_eighths,
_ => self.bar_set.full,
};
let bar_style = bar.style.patch(self.bar_style);
for x in 0..self.bar_width {
buf.get_mut(bar_x + x, bars_area.top() + j)
.set_symbol(symbol)
.set_style(bar_style);
}
if *d > 8 {
*d -= 8;
} else {
*d = 0;
}
bar_x += self.bar_gap + self.bar_width;
}
bar_x += self.group_gap;
}
}
}
/// get the maximum data value. the returned value is always greater equal 1
fn maximum_data_value(&self) -> u64 {
self.max
.unwrap_or_else(|| {
self.data
.iter()
.map(|group| group.max().unwrap_or_default())
.max()
.unwrap_or_default()
})
.max(1u64)
}
fn render_labels_and_values(self, area: Rect, buf: &mut Buffer, label_height: u16) {
// print labels and values in one go
let mut bar_x = area.left();
let y_value_offset = area.bottom() - label_height - 1;
for group in self.data.into_iter() {
// print group labels under the bars or the previous labels
if let Some(mut label) = group.label {
label.patch_style(self.label_style);
let label_max_width = group.bars.len() as u16 * self.bar_width
+ (group.bars.len() as u16 - 1) * self.bar_gap;
buf.set_line(
bar_x + (label_max_width.saturating_sub(label.width() as u16) >> 1),
area.bottom() - 1,
&label,
label_max_width,
);
}
// print the bar values and numbers
for bar in group.bars.into_iter() {
bar.render_value(buf, self.bar_width, bar_x, y_value_offset, self.value_style);
bar.render_label(
buf,
self.bar_width,
bar_x,
y_value_offset + 1,
self.label_style,
);
bar_x += self.bar_gap + self.bar_width;
}
bar_x += self.group_gap;
}
}
}
impl<'a> Widget for BarChart<'a> {
fn render(mut self, mut area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
self.render_block(&mut area, buf);
if self.data.is_empty() {
return;
}
let label_height = self.label_height();
if area.height <= label_height {
return;
}
let max = self.maximum_data_value();
// remove invisible groups and bars, since we don't need to print them
self.remove_invisible_groups_and_bars(area.width);
let bars_area = Rect {
height: area.height - label_height,
..area
};
self.render_bars(buf, bars_area, max);
self.render_labels_and_values(area, buf, label_height);
}
}
#[cfg(test)]
mod tests {
use itertools::iproduct;
use super::*;
use crate::{
assert_buffer_eq,
style::Color,
widgets::{BorderType, Borders},
};
#[test]
fn default() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let widget = BarChart::default();
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "; 3]));
}
#[test]
fn data() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default().data(&[("foo", 1), ("bar", 2)]);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
])
);
}
#[test]
fn block() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 5));
let block = Block::default()
.title("Block")
.border_type(BorderType::Double)
.borders(Borders::ALL);
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.block(block);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"╔Block════════╗",
"║ █ ║",
"║█ █ ║",
"║f b ║",
"╚═════════════╝",
])
);
}
#[test]
fn max() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let without_max = BarChart::default().data(&[("foo", 1), ("bar", 2), ("baz", 100)]);
without_max.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"",
"",
"f b b ",
])
);
let with_max = BarChart::default()
.data(&[("foo", 1), ("bar", 2), ("baz", 100)])
.max(2);
with_max.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" █ █ ",
"█ █ █ ",
"f b b ",
])
);
}
#[test]
fn bar_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_style(Style::default().fg(Color::Red));
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
]);
for (x, y) in iproduct!([0, 2], [0, 1]) {
expected.get_mut(x, y).set_fg(Color::Red);
}
assert_buffer_eq!(buffer, expected);
}
#[test]
fn bar_width() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_width(3);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ███ ",
"█1█ █2█ ",
"foo bar ",
])
);
}
#[test]
fn bar_gap() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_gap(2);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
])
);
}
#[test]
fn bar_set() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 0), ("bar", 1), ("baz", 3)])
.bar_set(symbols::bar::THREE_LEVELS);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"",
" ▄ █ ",
"f b b ",
])
);
}
#[test]
fn bar_set_nine_levels() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 18, 3));
let widget = BarChart::default()
.data(&[
("a", 0),
("b", 1),
("c", 2),
("d", 3),
("e", 4),
("f", 5),
("g", 6),
("h", 7),
("i", 8),
])
.bar_set(symbols::bar::NINE_LEVELS);
widget.render(Rect::new(0, 1, 18, 2), &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ",
"a b c d e f g h i ",
])
);
}
#[test]
fn value_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_width(3)
.value_style(Style::default().fg(Color::Red));
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
" ███ ",
"█1█ █2█ ",
"foo bar ",
]);
expected.get_mut(1, 1).set_fg(Color::Red);
expected.get_mut(5, 1).set_fg(Color::Red);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn label_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.label_style(Style::default().fg(Color::Red));
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
]);
expected.get_mut(0, 2).set_fg(Color::Red);
expected.get_mut(2, 2).set_fg(Color::Red);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.style(Style::default().fg(Color::Red));
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
]);
for (x, y) in iproduct!(0..15, 0..3) {
expected.get_mut(x, y).set_fg(Color::Red);
}
assert_buffer_eq!(buffer, expected);
}
#[test]
fn does_not_render_less_than_two_rows() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
let widget = BarChart::default().data(&[("foo", 1), ("bar", 2)]);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::empty(Rect::new(0, 0, 15, 1)));
}
fn create_test_barchart<'a>() -> BarChart<'a> {
BarChart::default()
.group_gap(2)
.data(BarGroup::default().label("G1".into()).bars(&[
Bar::default().value(2),
Bar::default().value(1),
Bar::default().value(2),
]))
.data(BarGroup::default().label("G2".into()).bars(&[
Bar::default().value(1),
Bar::default().value(2),
Bar::default().value(1),
]))
.data(BarGroup::default().label("G3".into()).bars(&[
Bar::default().value(1),
Bar::default().value(2),
Bar::default().value(1),
]))
}
#[test]
fn test_invisible_groups_and_bars_full() {
let chart = create_test_barchart();
// Check that the BarChart is shown in full
{
let mut c = chart.clone();
c.remove_invisible_groups_and_bars(21);
assert_eq!(c.data.len(), 3);
assert_eq!(c.data[2].bars.len(), 3);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 21, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
"█ █ █ █ ",
"█ █ █ █ █ █ █ █ █",
" G1 G2 G3 ",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_invisible_groups_and_bars_missing_last_2_bars() {
// Last 2 bars of G3 should be out of screen. (screen width is 17)
let chart = create_test_barchart();
{
let mut w = chart.clone();
w.remove_invisible_groups_and_bars(17);
assert_eq!(w.data.len(), 3);
assert_eq!(w.data[2].bars.len(), 1);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
"█ █ █ ",
"█ █ █ █ █ █ █",
" G1 G2 G",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_invisible_groups_and_bars_missing_last_group() {
// G3 should be out of screen. (screen width is 16)
let chart = create_test_barchart();
{
let mut w = chart.clone();
w.remove_invisible_groups_and_bars(16);
assert_eq!(w.data.len(), 2);
assert_eq!(w.data[1].bars.len(), 3);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 16, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
"█ █ █ ",
"█ █ █ █ █ █ ",
" G1 G2 ",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_invisible_groups_and_bars_show_only_1_bar() {
let chart = create_test_barchart();
{
let mut w = chart.clone();
w.remove_invisible_groups_and_bars(1);
assert_eq!(w.data.len(), 1);
assert_eq!(w.data[0].bars.len(), 1);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "", "G"]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_invisible_groups_and_bars_all_bars_outside_visible_area() {
let chart = create_test_barchart();
{
let mut w = chart.clone();
w.remove_invisible_groups_and_bars(0);
assert_eq!(w.data.len(), 0);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 3));
// Check if the render method panics
chart.render(buffer.area, &mut buffer);
}
#[test]
fn test_label_height() {
{
let barchart = BarChart::default().data(
BarGroup::default()
.label("Group Label".into())
.bars(&[Bar::default().value(2).label("Bar Label".into())]),
);
assert_eq!(barchart.label_height(), 2);
}
{
let barchart = BarChart::default().data(
BarGroup::default()
.label("Group Label".into())
.bars(&[Bar::default().value(2)]),
);
assert_eq!(barchart.label_height(), 1);
}
{
let barchart = BarChart::default().data(
BarGroup::default().bars(&[Bar::default().value(2).label("Bar Label".into())]),
);
assert_eq!(barchart.label_height(), 1);
}
{
let barchart =
BarChart::default().data(BarGroup::default().bars(&[Bar::default().value(2)]));
assert_eq!(barchart.label_height(), 0);
}
}
}

View file

@ -37,7 +37,7 @@ use std::fmt::{self, Debug};
use bitflags::bitflags;
pub use self::{
barchart::BarChart,
barchart::{Bar, BarChart, BarGroup},
block::{Block, BorderType, Padding},
chart::{Axis, Chart, Dataset, GraphType},
clear::Clear,

View file

@ -1,7 +1,8 @@
use ratatui::{
backend::TestBackend,
buffer::Buffer,
widgets::{BarChart, Block, Borders},
style::{Color, Style},
widgets::{Bar, BarChart, BarGroup, Block, Borders},
Terminal,
};
@ -36,7 +37,70 @@ fn widgets_barchart_not_full_below_max_value() {
"│ █████████████████████│",
"│ █████████████████████│",
"│ ██50█████99█████100██│",
"empty half almost full ",
" empty half almost full │",
"└────────────────────────────┘",
]));
}
#[test]
fn widgets_barchart_group() {
const TERMINAL_HEIGHT: u16 = 11u16;
let test_case = |expected| {
let backend = TestBackend::new(35, TERMINAL_HEIGHT);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let barchart = BarChart::default()
.block(Block::default().borders(Borders::ALL))
.data(
BarGroup::default().label("Mar".into()).bars(&[
Bar::default()
.value(10)
.label("C1".into())
.style(Style::default().fg(Color::Red))
.value_style(Style::default().fg(Color::Blue)),
Bar::default()
.value(20)
.style(Style::default().fg(Color::Green)),
]),
)
.data(&vec![("C1", 50u64), ("C2", 40u64)])
.data(&[("C1", 60u64), ("C2", 90u64)])
.data(&[("xx", 10u64), ("xx", 10u64)])
.group_gap(2)
.bar_width(4)
.bar_gap(1);
f.render_widget(barchart, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
let mut expected = Buffer::with_lines(vec![
"┌─────────────────────────────────┐",
"│ ████│",
"│ ████│",
"│ ▅▅▅▅ ████│",
"│ ▇▇▇▇ ████ ████│",
"│ ████ ████ ████ ████│",
"│ ▄▄▄▄ ████ ████ ████ ████│",
"│▆10▆ █20█ █50█ █40█ █60█ █90█│",
"│ C1 C1 C2 C1 C2 │",
"│ Mar │",
"└─────────────────────────────────┘",
]);
for y in 1..(TERMINAL_HEIGHT - 3) {
for x in 1..5 {
expected.get_mut(x, y).set_fg(Color::Red);
expected.get_mut(x + 5, y).set_fg(Color::Green);
}
}
expected.get_mut(2, 7).set_fg(Color::Blue);
expected.get_mut(3, 7).set_fg(Color::Blue);
test_case(expected);
}