Skip to content

Commit

Permalink
feat(barchart): Add direction attribute. (horizontal bars support) (#325
Browse files Browse the repository at this point in the history
)

* feat(barchart): Add direction attribute

Enable rendring the bars horizontally. In some cases this allow us to
make more efficient use of the available space.

Signed-off-by: Ben Fekih, Hichem <[email protected]>

* feat(barchart)!: render the group labels depending on the alignment

This is a breaking change, since the alignment by default is set to
Left and the group labels are always rendered in the center.

Signed-off-by: Ben Fekih, Hichem <[email protected]>

---------

Signed-off-by: Ben Fekih, Hichem <[email protected]>
  • Loading branch information
karthago1 authored Aug 24, 2023
1 parent a937500 commit 0dca6a6
Show file tree
Hide file tree
Showing 5 changed files with 441 additions and 60 deletions.
94 changes: 69 additions & 25 deletions examples/barchart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ impl<'a> App<'a> {
},
Company {
label: "Comp.B",
revenue: [1500, 2500, 3000, 4100],
revenue: [1500, 2500, 3000, 500],
bar_style: Style::default().fg(Color::Yellow),
},
Company {
Expand Down Expand Up @@ -140,14 +140,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]
.as_ref(),
)
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
.split(f.size());

let barchart = BarChart::default()
Expand All @@ -158,16 +151,17 @@ 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]);

draw_bar_with_group_labels(f, app, chunks[1], false);
draw_bar_with_group_labels(f, app, chunks[2], true);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);

draw_bar_with_group_labels(f, app, chunks[0]);
draw_horizontal_bars(f, app, chunks[1]);
}

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
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
app.months
.iter()
.enumerate()
.map(|(i, &month)| {
Expand All @@ -182,17 +176,34 @@ where
Style::default()
.bg(c.bar_style.fg.unwrap())
.fg(Color::Black),
)
.text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.));
if bar_labels {
bar = bar.label(c.label.into());
);

if combine_values_and_labels {
bar = bar.text_value(format!(
"{} ({:.1} M)",
c.label,
(c.revenue[i] as f64) / 1000.
));
} else {
bar = bar
.text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.))
.label(c.label.into());
}
bar
})
.collect();
BarGroup::default().label(month.into()).bars(&bars)
BarGroup::default()
.label(Line::from(month).alignment(Alignment::Center))
.bars(&bars)
})
.collect();
.collect()
}

fn draw_bar_with_group_labels<B>(f: &mut Frame<B>, app: &App, area: Rect)
where
B: Backend,
{
let groups = create_groups(app, false);

let mut barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
Expand All @@ -207,11 +218,44 @@ where

const LEGEND_HEIGHT: u16 = 6;
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
let legend_area = Rect {
height: LEGEND_HEIGHT,
width: legend_width,
y: area.y,
x: area.right() - legend_width,
};
draw_legend(f, legend_area);
}
}

fn draw_horizontal_bars<B>(f: &mut Frame<B>, app: &App, area: Rect)
where
B: Backend,
{
let groups = create_groups(app, true);

let mut barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.bar_width(1)
.group_gap(1)
.bar_gap(0)
.direction(Direction::Horizontal);

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_width = TOTAL_REVENUE.len() as u16 + 2;
let legend_area = Rect {
height: LEGEND_HEIGHT,
width: TOTAL_REVENUE.len() as u16 + 2,
width: legend_width,
y: area.y,
x: area.x,
x: area.right() - legend_width,
};
draw_legend(f, legend_area);
}
Expand Down
49 changes: 46 additions & 3 deletions src/widgets/barchart/bar.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{buffer::Buffer, style::Style, text::Line};
use crate::{buffer::Buffer, prelude::Rect, style::Style, text::Line};

/// represent a bar to be shown by the Barchart
///
Expand Down Expand Up @@ -56,6 +56,45 @@ impl<'a> Bar<'a> {
self
}

/// Render the value of the bar. value_text is used if set, otherwise the value is converted to
/// string. The value is rendered using value_style. If the value width is greater than the
/// bar width, then the value is split into 2 parts. the first part is rendered in the bar
/// using value_style. The second part is rendered outside the bar using bar_style
pub(super) fn render_value_with_different_styles(
self,
buf: &mut Buffer,
area: Rect,
bar_length: usize,
default_value_style: Style,
bar_style: Style,
) {
let text = if let Some(text) = self.text_value {
text
} else {
self.value.to_string()
};

if !text.is_empty() {
let style = default_value_style.patch(self.value_style);
// Since the value may be longer than the bar itself, we need to use 2 different styles
// while rendering. Render the first part with the default value style
buf.set_stringn(area.x, area.y, &text, bar_length, style);
// render the second part with the bar_style
if text.len() > bar_length {
let (first, second) = text.split_at(bar_length);

let style = bar_style.patch(self.style);
buf.set_stringn(
area.x + first.len() as u16,
area.y,
second,
area.width as usize - first.len(),
style,
);
}
}
}

pub(super) fn render_label_and_value(
self,
buf: &mut Buffer,
Expand All @@ -79,14 +118,18 @@ impl<'a> Bar<'a> {
x + (max_width.saturating_sub(value_label.len() as u16) >> 1),
y,
value_label,
self.value_style.patch(default_value_style),
default_value_style.patch(self.value_style),
);
}
}

// render the label
if let Some(mut label) = self.label {
label.patch_style(default_label_style);
// patch label styles
for span in &mut label.spans {
span.style = default_label_style.patch(span.style);
}

buf.set_line(
x + (max_width.saturating_sub(label.width() as u16) >> 1),
y + 1,
Expand Down
23 changes: 22 additions & 1 deletion src/widgets/barchart/bar_group.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use super::Bar;
use crate::text::Line;
use crate::{
prelude::{Alignment, Buffer, Rect},
style::Style,
text::Line,
};

/// represent a group of bars to be shown by the Barchart
///
Expand Down Expand Up @@ -35,6 +39,23 @@ impl<'a> BarGroup<'a> {
pub(super) fn max(&self) -> Option<u64> {
self.bars.iter().max_by_key(|v| v.value).map(|v| v.value)
}

pub(super) fn render_label(self, buf: &mut Buffer, area: Rect, default_label_style: Style) {
if let Some(mut label) = self.label {
// patch label styles
for span in &mut label.spans {
span.style = default_label_style.patch(span.style);
}

let x_offset = match label.alignment {
Some(Alignment::Center) => area.width.saturating_sub(label.width() as u16) >> 1,
Some(Alignment::Right) => area.width.saturating_sub(label.width() as u16),
_ => 0,
};

buf.set_line(area.x + x_offset, area.y, &label, area.width);
}
}
}

impl<'a> From<&[(&'a str, u64)]> for BarGroup<'a> {
Expand Down
Loading

0 comments on commit 0dca6a6

Please sign in to comment.