From ac0c627c90e525c9563fca0e41b8d84a2b5701d2 Mon Sep 17 00:00:00 2001 From: "Ben Fekih, Hichem" Date: Mon, 25 Sep 2023 22:02:10 +0200 Subject: [PATCH] feat(barchart): allow to render charts smaller than 3 lines Add an internal structure `LabelInfo`, which stores the reserved height for the labels (0, 1 or 2) and also the state of the labels, whether they will be shown or not. 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 fixes ratatui-org#513 Signed-off-by: Ben Fekih, Hichem --- src/widgets/barchart.rs | 527 ++++++++++++++++++++---------------- src/widgets/barchart/bar.rs | 26 +- 2 files changed, 311 insertions(+), 242 deletions(-) diff --git a/src/widgets/barchart.rs b/src/widgets/barchart.rs index 19408054a1..6f67ff102e 100644 --- a/src/widgets/barchart.rs +++ b/src/widgets/barchart.rs @@ -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> = self + let group_ticks: Vec> = 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]) { // 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,20 @@ 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; + if d > 8 { + d -= 8; } else { - *d = 0; + d = 0; } - bar_x += self.bar_gap + self.bar_width; } - bar_x += self.group_gap; + bar_x += self.bar_gap + self.bar_width; } + bar_x += self.group_gap; } } @@ -489,36 +528,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], + ) { // 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 +577,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.area() == 0 || self.data.is_empty() || self.bar_width == 0 { return; } @@ -558,12 +593,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 +637,7 @@ mod tests { buffer, Buffer::with_lines(vec![ " █ ", - "█ █ ", + "1 2 ", "f b ", ]) ); @@ -629,7 +659,7 @@ mod tests { Buffer::with_lines(vec![ "╔Block════════╗", "║ █ ║", - "║█ █ ║", + "║1 2 ║", "║f b ║", "╚═════════════╝", ]) @@ -657,7 +687,7 @@ mod tests { buffer, Buffer::with_lines(vec![ " █ █ ", - "█ █ █ ", + "1 2 █ ", "f b b ", ]) ); @@ -672,7 +702,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 +739,7 @@ mod tests { buffer, Buffer::with_lines(vec![ " █ ", - "█ █ ", + "1 2 ", "f b ", ]) ); @@ -726,7 +756,7 @@ mod tests { buffer, Buffer::with_lines(vec![ " █ ", - " ▄ █ ", + " ▄ 3 ", "f b b ", ]) ); @@ -753,7 +783,7 @@ mod tests { buffer, Buffer::with_lines(vec![ " ", - " ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ", + " ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8 ", "a b c d e f g h i ", ]) ); @@ -786,7 +816,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 +833,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 +842,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 +865,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 +1047,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 +1062,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 +1108,193 @@ 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 tow_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), + ]); + + let chart = BarChart::default().data(group); + + let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3)); + chart.render(Rect::new(0, 1, buffer.area.width, 2), &mut buffer); + + assert_buffer_eq!( + buffer, + Buffer::with_lines(vec![ + " ", + " ▁ ▂ ▃ ▄ ▅ ▆ 7", + " Group ", + ]) + ); + } } diff --git a/src/widgets/barchart/bar.rs b/src/widgets/barchart/bar.rs index 7d2d6e91a5..00d5d70ca5 100644 --- a/src/widgets/barchart/bar.rs +++ b/src/widgets/barchart/bar.rs @@ -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, ); }