mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-10 07:04:17 +00:00
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:
parent
e66d5cdee0
commit
ae8ed8867d
8 changed files with 1074 additions and 501 deletions
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
92
src/widgets/barchart/bar.rs
Normal file
92
src/widgets/barchart/bar.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
63
src/widgets/barchart/bar_group.rs
Normal file
63
src/widgets/barchart/bar_group.rs
Normal 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
724
src/widgets/barchart/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue