feat(barchart): render charts smaller than 3 lines (#532)

The bar values are not shown if the value width is equal the bar width
and the bar is height is less than one line

Add an internal structure `LabelInfo` which stores the reserved height
for the labels (0, 1 or 2) and also whether the labels will be shown.

Fixes ratatui-org#513

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
This commit is contained in:
Hichem 2023-09-26 22:54:04 +02:00 committed by GitHub
parent 3bda372847
commit 301366c4fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 327 additions and 244 deletions

View file

@ -283,6 +283,12 @@ impl<'a> BarChart<'a> {
}
}
struct LabelInfo {
group_label_visible: bool,
bar_label_visible: bool,
height: u16,
}
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.
@ -306,23 +312,43 @@ impl<'a> BarChart<'a> {
}
}
/// Get the number of lines needed for the labels.
/// Get label information.
///
/// 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
/// height is the number of lines, which depends on whether we need to print the bar
/// labels and/or the group labels.
/// - If there are no labels, height is 0.
/// - If there are only bar labels, height is 1.
/// - If there are only group labels, height is 1.
/// - If there are both bar and group labels, height is 2.
fn label_info(&self, available_height: u16) -> LabelInfo {
if available_height == 0 {
return LabelInfo {
group_label_visible: false,
bar_label_visible: false,
height: 0,
};
}
let bar_label_visible = 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)
if available_height == 1 && bar_label_visible {
return LabelInfo {
group_label_visible: false,
bar_label_visible: true,
height: 1,
};
}
let group_label_visible = self.data.iter().any(|e| e.label.is_some());
LabelInfo {
group_label_visible,
bar_label_visible,
// convert true to 1 and false to 0 and add the two values
height: u16::from(group_label_visible) + u16::from(bar_label_visible),
}
}
/// renders the block if there is one and updates the area to the inner area
@ -425,9 +451,16 @@ impl<'a> BarChart<'a> {
}
}
fn render_vertical_bars(&self, buf: &mut Buffer, bars_area: Rect, max: u64) {
fn render_vertical(self, buf: &mut Buffer, area: Rect, max: u64) {
let label_info = self.label_info(area.height - 1);
let bars_area = Rect {
height: area.height - label_info.height,
..area
};
// convert the bar values to ratatui::symbols::bar::Set
let mut groups: Vec<Vec<u64>> = self
let group_ticks: Vec<Vec<u64>> = self
.data
.iter()
.map(|group| {
@ -439,11 +472,17 @@ impl<'a> BarChart<'a> {
})
.collect();
self.render_vertical_bars(bars_area, buf, &group_ticks);
self.render_labels_and_values(area, buf, label_info, &group_ticks);
}
fn render_vertical_bars(&self, area: Rect, buf: &mut Buffer, group_ticks: &[Vec<u64>]) {
// 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 mut bar_x = area.left();
for (ticks, group) in group_ticks.iter().zip(&self.data) {
for (d, bar) in ticks.iter().zip(&group.bars) {
let mut d = *d;
for j in (0..area.height).rev() {
let symbol = match d {
0 => self.bar_set.empty,
1 => self.bar_set.one_eighth,
@ -459,20 +498,16 @@ impl<'a> BarChart<'a> {
let bar_style = self.bar_style.patch(bar.style);
for x in 0..self.bar_width {
buf.get_mut(bar_x + x, bars_area.top() + j)
buf.get_mut(bar_x + x, 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;
d = d.saturating_sub(8);
}
bar_x += self.group_gap;
bar_x += self.bar_gap + self.bar_width;
}
bar_x += self.group_gap;
}
}
@ -489,36 +524,42 @@ impl<'a> BarChart<'a> {
.max(1u64)
}
fn render_labels_and_values(self, area: Rect, buf: &mut Buffer, label_height: u16) {
fn render_labels_and_values(
self,
area: Rect,
buf: &mut Buffer,
label_info: LabelInfo,
group_ticks: &[Vec<u64>],
) {
// print labels and values in one go
let mut bar_x = area.left();
let bar_y = area.bottom() - label_height - 1;
for mut group in self.data.into_iter() {
let bar_y = area.bottom() - label_info.height - 1;
for (mut group, ticks) in self.data.into_iter().zip(group_ticks) {
if group.bars.is_empty() {
continue;
}
let bars = std::mem::take(&mut group.bars);
// print group labels under the bars or the previous labels
let label_max_width =
bars.len() as u16 * (self.bar_width + self.bar_gap) - self.bar_gap;
let group_area = Rect {
x: bar_x,
y: area.bottom() - 1,
width: label_max_width,
height: 1,
};
group.render_label(buf, group_area, self.label_style);
if label_info.group_label_visible {
let label_max_width =
bars.len() as u16 * (self.bar_width + self.bar_gap) - self.bar_gap;
let group_area = Rect {
x: bar_x,
y: area.bottom() - 1,
width: label_max_width,
height: 1,
};
group.render_label(buf, group_area, self.label_style);
}
// print the bar values and numbers
for bar in bars.into_iter() {
bar.render_label_and_value(
buf,
self.bar_width,
bar_x,
bar_y,
self.value_style,
self.label_style,
);
for (mut bar, ticks) in bars.into_iter().zip(ticks) {
if label_info.bar_label_visible {
bar.render_label(buf, self.bar_width, bar_x, bar_y + 1, self.label_style);
}
bar.render_value(buf, self.bar_width, bar_x, bar_y, self.value_style, *ticks);
bar_x += self.bar_gap + self.bar_width;
}
@ -532,18 +573,8 @@ impl<'a> Widget for BarChart<'a> {
buf.set_style(area, self.style);
self.render_block(&mut area, buf);
if area.area() == 0 {
return;
}
if self.data.is_empty() {
return;
}
if self.bar_width == 0 {
return;
}
let label_height = self.label_height();
if area.height <= label_height {
if area.is_empty() || self.data.is_empty() || self.bar_width == 0 {
return;
}
@ -558,12 +589,7 @@ impl<'a> Widget for BarChart<'a> {
Direction::Vertical => {
// 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_vertical_bars(buf, bars_area, max);
self.render_labels_and_values(area, buf, label_height);
self.render_vertical(buf, area, max);
}
}
}
@ -607,7 +633,7 @@ mod tests {
buffer,
Buffer::with_lines(vec![
"",
"█ █ ",
"1 2 ",
"f b ",
])
);
@ -629,7 +655,7 @@ mod tests {
Buffer::with_lines(vec![
"╔Block════════╗",
"║ █ ║",
"█ █",
"1 2",
"║f b ║",
"╚═════════════╝",
])
@ -657,7 +683,7 @@ mod tests {
buffer,
Buffer::with_lines(vec![
" █ █ ",
"█ █",
"1 2",
"f b b ",
])
);
@ -672,7 +698,7 @@ mod tests {
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"1 2 ",
"f b ",
]);
for (x, y) in iproduct!([0, 2], [0, 1]) {
@ -709,7 +735,7 @@ mod tests {
buffer,
Buffer::with_lines(vec![
"",
"█ █ ",
"1 2 ",
"f b ",
])
);
@ -726,7 +752,7 @@ mod tests {
buffer,
Buffer::with_lines(vec![
"",
" ",
"3 ",
"f b b ",
])
);
@ -753,7 +779,7 @@ mod tests {
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8 ",
"a b c d e f g h i ",
])
);
@ -786,7 +812,7 @@ mod tests {
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"1 2 ",
"f b ",
]);
expected.get_mut(0, 2).set_fg(Color::Red);
@ -803,7 +829,7 @@ mod tests {
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"1 2 ",
"f b ",
]);
for (x, y) in iproduct!(0..15, 0..3) {
@ -812,166 +838,6 @@ mod tests {
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);
}
}
#[test]
fn can_be_stylized() {
assert_eq!(
@ -995,7 +861,7 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "█ █", "G "]);
let expected = Buffer::with_lines(vec!["", "1 2", "G "]);
assert_buffer_eq!(buffer, expected);
}
@ -1177,7 +1043,7 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "", " G "]);
let expected = Buffer::with_lines(vec!["", "5", " G "]);
assert_buffer_eq!(buffer, expected);
}
@ -1192,7 +1058,7 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "", " G"]);
let expected = Buffer::with_lines(vec!["", "5", " G"]);
assert_buffer_eq!(buffer, expected);
}
@ -1238,4 +1104,211 @@ mod tests {
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::empty(Rect::new(0, 0, 0, 10)));
}
#[test]
fn single_line() {
let mut group: BarGroup = (&[
("a", 0),
("b", 1),
("c", 2),
("d", 3),
("e", 4),
("f", 5),
("g", 6),
("h", 7),
("i", 8),
])
.into();
group = group.label("Group".into());
let chart = BarChart::default()
.data(group)
.bar_set(symbols::bar::NINE_LEVELS);
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 1));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8"]));
}
#[test]
fn two_lines() {
let mut group: BarGroup = (&[
("a", 0),
("b", 1),
("c", 2),
("d", 3),
("e", 4),
("f", 5),
("g", 6),
("h", 7),
("i", 8),
])
.into();
group = group.label("Group".into());
let chart = BarChart::default()
.data(group)
.bar_set(symbols::bar::NINE_LEVELS);
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
chart.render(Rect::new(0, 1, buffer.area.width, 2), &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
"a b c d e f g h i",
])
);
}
#[test]
fn three_lines() {
let mut group: BarGroup = (&[
("a", 0),
("b", 1),
("c", 2),
("d", 3),
("e", 4),
("f", 5),
("g", 6),
("h", 7),
("i", 8),
])
.into();
group = group.label(Line::from("Group").alignment(Alignment::Center));
let chart = BarChart::default()
.data(group)
.bar_set(symbols::bar::NINE_LEVELS);
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
"a b c d e f g h i",
" Group ",
])
);
}
#[test]
fn three_lines_double_width() {
let mut group = BarGroup::from(&[
("a", 0),
("b", 1),
("c", 2),
("d", 3),
("e", 4),
("f", 5),
("g", 6),
("h", 7),
("i", 8),
]);
group = group.label(Line::from("Group").alignment(Alignment::Center));
let chart = BarChart::default()
.data(group)
.bar_width(2)
.bar_set(symbols::bar::NINE_LEVELS);
let mut buffer = Buffer::empty(Rect::new(0, 0, 26, 3));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" 1▁ 2▂ 3▃ 4▄ 5▅ 6▆ 7▇ 8█",
"a b c d e f g h i ",
" Group ",
])
);
}
#[test]
fn four_lines() {
let mut group: BarGroup = (&[
("a", 0),
("b", 1),
("c", 2),
("d", 3),
("e", 4),
("f", 5),
("g", 6),
("h", 7),
("i", 8),
])
.into();
group = group.label(Line::from("Group").alignment(Alignment::Center));
let chart = BarChart::default()
.data(group)
.bar_set(symbols::bar::NINE_LEVELS);
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 4));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▂ ▄ ▆ █",
" ▂ ▄ ▆ 4 5 6 7 8",
"a b c d e f g h i",
" Group ",
])
);
}
#[test]
fn two_lines_without_bar_labels() {
let group = BarGroup::default()
.label(Line::from("Group").alignment(Alignment::Center))
.bars(&[
Bar::default().value(0),
Bar::default().value(1),
Bar::default().value(2),
Bar::default().value(3),
Bar::default().value(4),
Bar::default().value(5),
Bar::default().value(6),
Bar::default().value(7),
Bar::default().value(8),
]);
let chart = BarChart::default().data(group);
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
chart.render(Rect::new(0, 1, buffer.area.width, 2), &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
" Group ",
])
);
}
#[test]
fn one_lines_with_more_bars() {
let bars: Vec<Bar> = (0..30).map(|i| Bar::default().value(i)).collect();
let chart = BarChart::default().data(BarGroup::default().bars(&bars));
let mut buffer = Buffer::empty(Rect::new(0, 0, 59, 1));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▁ ▁ ▁ ▁ ▂ ▂ ▂ ▃ ▃ ▃ ▃ ▄ ▄ ▄ ▄ ▅ ▅ ▅ ▆ ▆ ▆ ▆ ▇ ▇ ▇ █",
])
);
}
}

View file

@ -139,16 +139,15 @@ impl<'a> Bar<'a> {
}
}
pub(super) fn render_label_and_value(
pub(super) fn render_value(
self,
buf: &mut Buffer,
max_width: u16,
x: u16,
y: u16,
default_value_style: Style,
default_label_style: Style,
ticks: u64,
) {
// render the value
if self.value != 0 {
let value_label = if let Some(text) = self.text_value {
text
@ -157,7 +156,10 @@ impl<'a> Bar<'a> {
};
let width = value_label.width() as u16;
if width < max_width {
const TICKS_PER_LINE: u64 = 8;
// if we have enough space or the ticks are greater equal than 1 cell (8)
// then print the value
if width < max_width || (width == max_width && ticks >= TICKS_PER_LINE) {
buf.set_string(
x + (max_width.saturating_sub(value_label.len() as u16) >> 1),
y,
@ -166,9 +168,17 @@ impl<'a> Bar<'a> {
);
}
}
}
// render the label
if let Some(mut label) = self.label {
pub(super) fn render_label(
&mut self,
buf: &mut Buffer,
max_width: u16,
x: u16,
y: u16,
default_label_style: Style,
) {
if let Some(label) = &mut self.label {
// patch label styles
for span in &mut label.spans {
span.style = default_label_style.patch(span.style);
@ -176,8 +186,8 @@ impl<'a> Bar<'a> {
buf.set_line(
x + (max_width.saturating_sub(label.width() as u16) >> 1),
y + 1,
&label,
y,
label,
max_width,
);
}