diff --git a/examples/histogram.rs b/examples/histogram.rs new file mode 100644 index 00000000..5ba9a920 --- /dev/null +++ b/examples/histogram.rs @@ -0,0 +1,111 @@ +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + layout::{Constraint, Direction, Layout}, + style::{Color, Style}, + widgets::{Block, Borders, Histogram}, + Frame, Terminal, +}; +use std::{ + error::Error, + io, + time::{Duration, Instant}, +}; + +use rand::{rngs::ThreadRng, thread_rng, Rng}; + +struct App { + data: Vec, + size: usize, + rng: ThreadRng, +} + +impl App { + fn new(size: usize) -> App { + let data = vec![0; size]; + App { + data, + rng: thread_rng(), + size, + } + } + + fn on_tick(&mut self) { + for i in 0..self.size { + self.data[i] = self.rng.gen_range(0..100); + } + } +} + +fn main() -> Result<(), Box> { + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // create app and run it + let tick_rate = Duration::from_millis(250); + let app = App::new(100); + let res = run_app(&mut terminal, app, tick_rate); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err) + } + + Ok(()) +} + +fn run_app( + terminal: &mut Terminal, + mut app: App, + tick_rate: Duration, +) -> io::Result<()> { + let mut last_tick = Instant::now(); + loop { + terminal.draw(|f| ui(f, &app))?; + + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + if let KeyCode::Char('q') = key.code { + return Ok(()); + } + } + } + if last_tick.elapsed() >= tick_rate { + app.on_tick(); + last_tick = Instant::now(); + } + } +} + +fn ui(f: &mut Frame, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints([Constraint::Percentage(100)].as_ref()) + .split(f.size()); + let histogram = Histogram::default() + .block(Block::default().title("Data1").borders(Borders::ALL)) + .data(&app.data, 10) + .bar_style(Style::default().fg(Color::Yellow)) + .value_style(Style::default().fg(Color::Black).bg(Color::Yellow)); + f.render_widget(histogram, chunks[0]); +} diff --git a/src/widgets.rs b/src/widgets.rs index 7a5521ef..4cc744d0 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -30,6 +30,7 @@ pub mod canvas; mod chart; mod clear; mod gauge; +mod histogram; mod list; mod paragraph; mod reflow; @@ -45,6 +46,7 @@ pub use self::{ chart::{Axis, Chart, Dataset, GraphType, LegendPosition}, clear::Clear, gauge::{Gauge, LineGauge}, + histogram::Histogram, list::{List, ListDirection, ListItem, ListState}, paragraph::{Paragraph, Wrap}, scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState}, diff --git a/src/widgets/histogram.rs b/src/widgets/histogram.rs new file mode 100644 index 00000000..6a3716b0 --- /dev/null +++ b/src/widgets/histogram.rs @@ -0,0 +1,228 @@ +use crate::{ + buffer::Buffer, + layout::Rect, + style::Style, + symbols, + widgets::{Block, Widget}, +}; +use unicode_width::UnicodeWidthStr; + +/// A bar chart specialized for showing histograms +/// +/// # Examples +/// +/// ``` +/// # use tui::widgets::{Block, Borders, Histogram}; +/// # use tui::style::{Style, Color, Modifier}; +/// Histogram::default() +/// .block(Block::default().title("Histogram").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 Histogram<'a> { + /// Block to wrap the widget in + block: Option>, + /// 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 values to plot on the chart + data: &'a [u64], + /// each bucket keeps a count of the data points that fall into it + /// buckets[0] counts items where 0 <= x < bucket_size + /// buckets[1] counts items where bucket_size <= x < 2*bucket_size + /// etc. + buckets: Vec, + /// 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, + /// Values to display on the bar (computed when the data is passed to the widget) + values: Vec, +} + +impl<'a> Default for Histogram<'a> { + fn default() -> Histogram<'a> { + Histogram { + block: None, + max: None, + data: &[], + values: Vec::new(), + bar_style: Style::default(), + bar_gap: 1, + bar_set: symbols::bar::NINE_LEVELS, + buckets: Vec::new(), + value_style: Default::default(), + label_style: Default::default(), + style: Default::default(), + } + } +} + +impl<'a> Histogram<'a> { + pub fn data(mut self, data: &'a [u64], n_buckets: u64) -> Histogram<'a> { + self.data = data; + + let min = *self.data.iter().min().unwrap(); + let max = *self.data.iter().max().unwrap() + 1; + let bucket_size: u64 = ((max - min) as f64 / n_buckets as f64).ceil() as u64; + self.buckets = vec![0; n_buckets as usize]; + + // initialize buckets + self.values = Vec::with_capacity(n_buckets as usize); + for v in 0..n_buckets { + self.values.push(format!("{}", v * bucket_size)); + } + + // bucketize data + for &x in self.data.iter() { + let idx: usize = ((x - min) / bucket_size) as usize; + self.buckets[idx] += 1; + } + + self.max = Some(*self.buckets.iter().max().unwrap()); + + self + } + + pub fn block(mut self, block: Block<'a>) -> Histogram<'a> { + self.block = Some(block); + self + } + + pub fn max(mut self, max: u64) -> Histogram<'a> { + self.max = Some(max); + self + } + + pub fn bar_style(mut self, style: Style) -> Histogram<'a> { + self.bar_style = style; + self + } + + pub fn bar_gap(mut self, gap: u16) -> Histogram<'a> { + self.bar_gap = gap; + self + } + + pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> Histogram<'a> { + self.bar_set = bar_set; + self + } + + pub fn value_style(mut self, style: Style) -> Histogram<'a> { + self.value_style = style; + self + } + + pub fn label_style(mut self, style: Style) -> Histogram<'a> { + self.label_style = style; + self + } + + pub fn style(mut self, style: Style) -> Histogram<'a> { + self.style = style; + self + } +} + +impl<'a> Widget for Histogram<'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 n_bars = self.buckets.len() as u16; + let bar_width: u16 = (chart_area.width - (n_bars + 1) * self.bar_gap) / n_bars; + + let max = self + .max + .unwrap_or_else(|| self.buckets.iter().copied().max().unwrap_or_default()); + + let mut data = self + .buckets + .iter() + .take(n_bars as usize) + .map(|&v| v * u64::from(chart_area.height - 1) * 8 / std::cmp::max(max, 1)) + .collect::>(); + for j in (0..chart_area.height - 1).rev() { + for (i, d) in data.iter_mut().enumerate() { + 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, + }; + + for x in 0..bar_width { + buf.get_mut( + chart_area.left() + i as u16 * (bar_width + self.bar_gap) + x, + chart_area.top() + j, + ) + .set_symbol(symbol) + .set_style(self.bar_style); + } + + if *d > 8 { + *d -= 8; + } else { + *d = 0; + } + } + } + + for (i, &value) in self.buckets.iter().enumerate() { + let label = &self.values[i]; + if value != 0 { + let value_label = format!("{}", &self.buckets[i]); + let width = value_label.width() as u16; + if width < bar_width { + buf.set_string( + chart_area.left() + + i as u16 * (bar_width + self.bar_gap) + + (bar_width - width) / 2, + chart_area.bottom() - 2, + value_label, + self.value_style, + ); + } + } + buf.set_stringn( + chart_area.left() + i as u16 * (bar_width + self.bar_gap), + chart_area.bottom() - 1, + label, + bar_width as usize, + self.label_style, + ); + } + } +} diff --git a/src/widgets/overlappingbarchart.rs b/src/widgets/overlappingbarchart.rs new file mode 100644 index 00000000..b3aa8f5a --- /dev/null +++ b/src/widgets/overlappingbarchart.rs @@ -0,0 +1,230 @@ +use crate::{ + buffer::Buffer, + layout::Rect, + style::Style, + symbols, + widgets::{Block, Widget}, +}; +use std::cmp::min; +use unicode_width::UnicodeWidthStr; + +/// A series for a stacked bar chart +#[derive(Debug, Clone)] +pub struct BarSeries<'a> { + /// Name of the series + name: Cow<'a, str>, + /// The color to display for this series + bar_style: Style, + /// A reference to the data for this series + data: &'a [u64] +} + +/// Display multiple bars in a single widgets +/// +/// # Examples +/// +/// ``` +/// # use tui::widgets::{Block, Borders, BarChart}; +/// # use tui::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 OverlappingBarChart<'a> { + /// Block to wrap the widget in + block: Option>, + /// 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, + /// Vec of slices of (label, value) pair to plot on the chart + data: Vec<&'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, + /// Values to display on the bar (computed when the data is passed to the widget) + values: Vec, +} + +impl<'a> Default for BarChart<'a> { + fn default() -> BarChart<'a> { + BarChart { + block: None, + max: None, + data: Vec::new(), + values: Vec::new(), + bar_style: Style::default(), + bar_width: 1, + bar_gap: 1, + bar_set: symbols::bar::NINE_LEVELS, + value_style: Default::default(), + label_style: Default::default(), + style: Default::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::>(); + 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, + ); + } + } +}