tui_bar_graph/lib.rs
1//! A [Ratatui] widget to render bold, colorful bar graphs. Part of the [tui-widgets] suite by
2//! [Joshka].
3//!
4//! 
5//! 
6//! 
7//! 
8//!
9//! <details><summary>More examples</summary>
10//!
11//! 
12//! 
13//! 
14//! 
15//! 
16//! 
17//! 
18//! 
19//!
20//! </details>
21//!
22//! Uses the [Colorgrad] crate for gradient coloring.
23//!
24//! [![Crate badge]][Crate]
25//! [![Docs Badge]][Docs]
26//! [![Deps Badge]][Dependency Status]
27//! [![License Badge]][License]
28//! [![Coverage Badge]][Coverage]
29//! [![Discord Badge]][Ratatui Discord]
30//!
31//! [GitHub Repository] · [API Docs] · [Examples] · [Changelog] · [Contributing]
32//!
33//! # Installation
34//!
35//! ```shell
36//! cargo add ratatui tui-bar-graph
37//! ```
38//!
39//! # Usage
40//!
41//! Build a `BarGraph` with your data and render it in a widget area.
42//!
43//! ```rust
44//! use tui_bar_graph::{BarGraph, BarStyle, ColorMode};
45//!
46//! # fn render(frame: &mut ratatui::Frame, area: ratatui::layout::Rect) {
47//! let data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
48//! let bar_graph = BarGraph::new(data)
49//! .with_gradient(colorgrad::preset::turbo())
50//! .with_bar_style(BarStyle::Braille)
51//! .with_color_mode(ColorMode::VerticalGradient);
52//! frame.render_widget(bar_graph, area);
53//! # }
54//! ```
55//!
56//! # More widgets
57//!
58//! For the full suite of widgets, see [tui-widgets].
59//!
60//! [Colorgrad]: https://crates.io/crates/colorgrad
61//! [Ratatui]: https://crates.io/crates/ratatui
62//! [Crate]: https://crates.io/crates/tui-bar-graph
63//! [Docs]: https://docs.rs/tui-bar-graph/
64//! [Dependency Status]: https://deps.rs/repo/github/ratatui/tui-widgets
65//! [Coverage]: https://app.codecov.io/gh/ratatui/tui-widgets
66//! [Ratatui Discord]: https://discord.gg/pMCEU9hNEj
67//! [Crate badge]: https://img.shields.io/crates/v/tui-bar-graph.svg?logo=rust&style=flat
68//! [Docs Badge]: https://img.shields.io/docsrs/tui-bar-graph?logo=rust&style=flat
69//! [Deps Badge]: https://deps.rs/repo/github/ratatui/tui-widgets/status.svg?style=flat
70//! [License Badge]: https://img.shields.io/crates/l/tui-bar-graph.svg?style=flat
71//! [License]: https://github.com/ratatui/tui-widgets/blob/main/LICENSE-MIT
72//! [Coverage Badge]:
73//! https://img.shields.io/codecov/c/github/ratatui/tui-widgets?logo=codecov&style=flat
74//! [Discord Badge]: https://img.shields.io/discord/1070692720437383208?logo=discord&style=flat
75//!
76//! [GitHub Repository]: https://github.com/ratatui/tui-widgets
77//! [API Docs]: https://docs.rs/tui-bar-graph/
78//! [Examples]: https://github.com/ratatui/tui-widgets/tree/main/tui-bar-graph/examples
79//! [Changelog]: https://github.com/ratatui/tui-widgets/blob/main/tui-bar-graph/CHANGELOG.md
80//! [Contributing]: https://github.com/ratatui/tui-widgets/blob/main/CONTRIBUTING.md
81//!
82//! [Joshka]: https://github.com/joshka
83//! [tui-widgets]: https://crates.io/crates/tui-widgets
84#![cfg_attr(docsrs, doc = "\n# Feature flags\n")]
85#![cfg_attr(docsrs, doc = document_features::document_features!())]
86
87use colorgrad::Gradient;
88use ratatui_core::buffer::Buffer;
89use ratatui_core::layout::Rect;
90use ratatui_core::style::Color;
91use ratatui_core::widgets::Widget;
92use strum::{Display, EnumString};
93
94const BRAILLE_PATTERNS: [[&str; 5]; 5] = [
95 ["⠀", "⢀", "⢠", "⢰", "⢸"],
96 ["⡀", "⣀", "⣠", "⣰", "⣸"],
97 ["⡄", "⣄", "⣤", "⣴", "⣼"],
98 ["⡆", "⣆", "⣦", "⣶", "⣾"],
99 ["⡇", "⣇", "⣧", "⣷", "⣿"],
100];
101
102const OCTANT_PATTERNS: [[&str; 5]; 5] = [
103 ["⠀", "", "▗", "", "▐"],
104 ["", "▂", "", "", ""],
105 ["▖", "", "▄", "", "▟"],
106 ["", "", "", "▆", ""],
107 ["▌", "", "▙", "", "█"],
108];
109
110#[rustfmt::skip]
111const QUADRANT_PATTERNS: [[&str; 3]; 3]= [
112 [" ", "▗", "▐"],
113 ["▖", "▄", "▟"],
114 ["▌", "▙", "█"],
115];
116
117/// A widget for displaying a bar graph.
118///
119/// The bars can be colored using a gradient, and can be rendered using either solid blocks or
120/// braille characters for a more granular appearance.
121///
122/// # Example
123///
124/// ```rust
125/// use tui_bar_graph::{BarGraph, BarStyle, ColorMode};
126///
127/// # fn render(frame: &mut ratatui::Frame, area: ratatui::layout::Rect) {
128/// let data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
129/// let bar_graph = BarGraph::new(data)
130/// .with_gradient(colorgrad::preset::turbo())
131/// .with_bar_style(BarStyle::Braille)
132/// .with_color_mode(ColorMode::VerticalGradient);
133/// frame.render_widget(bar_graph, area);
134/// # }
135/// ```
136pub struct BarGraph<'g> {
137 /// The data to display as bars.
138 data: Vec<f64>,
139
140 /// The maximum value to display.
141 max: Option<f64>,
142
143 /// The minimum value to display.
144 min: Option<f64>,
145
146 /// A gradient to use for coloring the bars.
147 gradient: Option<Box<dyn Gradient + 'g>>,
148
149 /// The direction of the gradient coloring.
150 color_mode: ColorMode,
151
152 /// The style of bar to render.
153 bar_style: BarStyle,
154}
155
156/// The direction of the gradient coloring.
157///
158/// - `Solid`: Each bar has a single color based on its value.
159/// - `Gradient`: Each bar is gradient-colored from bottom to top.
160#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
161pub enum ColorMode {
162 /// Each bar has a single color based on its value.
163 Solid,
164 /// Each bar is gradient-colored from bottom to top.
165 #[default]
166 VerticalGradient,
167}
168
169/// The style of bar to render.
170///
171/// - `Solid`: Render bars using the full block character '`█`'.
172/// - `Quadrant`: Render bars using quadrant block characters for a more granular representation.
173/// - `Octant`: Render bars using octant block characters for a more granular representation.
174/// - `Braille`: Render bars using braille characters for a more granular representation.
175///
176/// `Octant` and `Braille` offer the same level of granularity, but `Braille` is more widely
177/// supported by fonts.
178#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, EnumString, Display)]
179#[strum(serialize_all = "snake_case")]
180pub enum BarStyle {
181 /// Render bars using braille characters `⡀`, `⢀`, `⠄`, `⠠`, `⠂`, `⠐`, `⠁`, and `⠈` for a more
182 /// granular representation.
183 #[default]
184 Braille,
185 /// Render bars using the full block character '`█`'.
186 Solid,
187 /// Render bars using the quadrant block characters `▖`, `▗`, `▘`, and `▝` for a more granular
188 /// representation.
189 Quadrant,
190 /// Render bars using the octant block characters ``, ``, ``, ``, ``, ``, ``, and ``
191 /// for a more granular representation.
192 ///
193 /// `Octant` uses characters from the [Symbols for Legacy Computing Supplement] block, which
194 /// is rendered correctly by a small but growing number of fonts.
195 ///
196 /// [Symbols for Legacy Computing Supplement]:
197 /// https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing_Supplement
198 Octant,
199}
200
201impl<'g> BarGraph<'g> {
202 /// Creates a new bar graph with the given data.
203 pub fn new(data: Vec<f64>) -> Self {
204 Self {
205 data,
206 max: None,
207 min: None,
208 gradient: None,
209 color_mode: ColorMode::default(),
210 bar_style: BarStyle::default(),
211 }
212 }
213
214 /// Sets the gradient to use for coloring the bars.
215 ///
216 /// See the [colorgrad] crate for information on creating gradients. Note that the default
217 /// domain (range) of the gradient is [0, 1], so you may need to scale your data to fit this
218 /// range, or modify the gradient's domain to fit your data.
219 pub fn with_gradient(mut self, gradient: impl Gradient + 'g) -> Self {
220 self.gradient = Some(gradient.boxed());
221 self
222 }
223
224 /// Sets the maximum value to display.
225 ///
226 /// Values greater than this will be clamped to this value. If `None`, the maximum value is
227 /// calculated from the data.
228 pub fn with_max(mut self, max: impl Into<Option<f64>>) -> Self {
229 self.max = max.into();
230 self
231 }
232
233 /// Sets the minimum value to display.
234 ///
235 /// Values less than this will be clamped to this value. If `None`, the minimum value is
236 /// calculated from the data.
237 pub fn with_min(mut self, min: impl Into<Option<f64>>) -> Self {
238 self.min = min.into();
239 self
240 }
241
242 /// Sets the color mode for the bars.
243 ///
244 /// The default is `ColorMode::VerticalGradient`.
245 ///
246 /// - `Solid`: Each bar has a single color based on its value.
247 /// - `Gradient`: Each bar is gradient-colored from bottom to top.
248 pub const fn with_color_mode(mut self, color: ColorMode) -> Self {
249 self.color_mode = color;
250 self
251 }
252
253 /// Sets the style of the bars.
254 ///
255 /// The default is `BarStyle::Braille`.
256 ///
257 /// - `Solid`: Render bars using the full block character '`█`'.
258 /// - `Quadrant`: Render bars using quadrant block characters for a more granular
259 /// representation.
260 /// - `Octant`: Render bars using octant block characters for a more granular representation.
261 /// - `Braille`: Render bars using braille characters for a more granular representation.
262 ///
263 /// `Octant` and `Braille` offer the same level of granularity, but `Braille` is more widely
264 /// supported by fonts.
265 pub const fn with_bar_style(mut self, style: BarStyle) -> Self {
266 self.bar_style = style;
267 self
268 }
269
270 /// Renders the graph using solid blocks (█).
271 fn render_solid(&self, area: Rect, buf: &mut Buffer, min: f64, max: f64) {
272 let range = max - min;
273 for (&value, column) in self.data.iter().zip(area.columns()) {
274 let normalized = (value - min) / range;
275 let column_height = (normalized * area.height as f64).ceil() as usize;
276 for (i, row) in column.rows().rev().enumerate().take(column_height) {
277 let color = self.color_for(area, min, range, value, i);
278 buf[row].set_symbol("█").set_fg(color);
279 }
280 }
281 }
282
283 /// Renders the graph using braille characters.
284 fn render_braille(&self, area: Rect, buf: &mut Buffer, min: f64, max: f64) {
285 self.render_pattern(area, buf, min, max, 4, &BRAILLE_PATTERNS);
286 }
287
288 /// Renders the graph using octant blocks.
289 fn render_octant(&self, area: Rect, buf: &mut Buffer, min: f64, max: f64) {
290 self.render_pattern(area, buf, min, max, 4, &OCTANT_PATTERNS);
291 }
292
293 /// Renders the graph using quadrant blocks.
294 fn render_quadrant(&self, area: Rect, buf: &mut Buffer, min: f64, max: f64) {
295 self.render_pattern(area, buf, min, max, 2, &QUADRANT_PATTERNS);
296 }
297
298 /// Common rendering logic for pattern-based bar styles.
299 fn render_pattern<const N: usize, const M: usize>(
300 &self,
301 area: Rect,
302 buf: &mut Buffer,
303 min: f64,
304 max: f64,
305 dots_per_row: usize,
306 patterns: &[[&str; N]; M],
307 ) {
308 let range = max - min;
309 let row_count = area.height;
310 let total_dots = row_count as usize * dots_per_row;
311
312 for (chunk, column) in self
313 .data
314 .chunks(2)
315 .zip(area.columns())
316 .take(area.width as usize)
317 {
318 let left_value = chunk[0];
319 let right_value = chunk.get(1).copied().unwrap_or(min);
320
321 let left_normalized = (left_value - min) / range;
322 let right_normalized = (right_value - min) / range;
323
324 let left_total_dots = (left_normalized * total_dots as f64).round() as usize;
325 let right_total_dots = (right_normalized * total_dots as f64).round() as usize;
326
327 let column_height = (left_total_dots.max(right_total_dots) as f64 / dots_per_row as f64)
328 .ceil() as usize;
329
330 for (row_index, row) in column.rows().rev().enumerate().take(column_height) {
331 let value = f64::midpoint(left_value, right_value);
332 let color = self.color_for(area, min, max, value, row_index);
333
334 let dots_below = row_index * dots_per_row;
335 let left_dots = left_total_dots.saturating_sub(dots_below).min(dots_per_row);
336 let right_dots = right_total_dots
337 .saturating_sub(dots_below)
338 .min(dots_per_row);
339
340 let symbol = patterns[left_dots][right_dots];
341 buf[row].set_symbol(symbol).set_fg(color);
342 }
343 }
344 }
345
346 fn color_for(&self, area: Rect, min: f64, max: f64, value: f64, row: usize) -> Color {
347 let color_value = match self.color_mode {
348 ColorMode::Solid => value,
349 ColorMode::VerticalGradient => {
350 (row as f64 / area.height as f64).mul_add(max - min, min)
351 }
352 };
353 self.gradient
354 .as_ref()
355 .map(|gradient| {
356 let color = gradient.at(color_value as f32);
357 let rgba = color.to_rgba8();
358 // TODO this can be changed to .into() in ratatui 0.30
359 Color::Rgb(rgba[0], rgba[1], rgba[2])
360 })
361 .unwrap_or(Color::Reset)
362 }
363}
364
365impl Widget for BarGraph<'_> {
366 fn render(self, area: Rect, buf: &mut Buffer) {
367 // f64 doesn't impl Ord because NaN != NaN, so we use fold instead of iter::max/min
368 let min = self
369 .min
370 .unwrap_or_else(|| self.data.iter().copied().fold(f64::INFINITY, f64::min));
371 let max = self
372 .max
373 .unwrap_or_else(|| self.data.iter().copied().fold(f64::NEG_INFINITY, f64::max));
374 let max = max.max(min + f64::EPSILON); // avoid division by zero if min == max
375 match self.bar_style {
376 BarStyle::Braille => self.render_braille(area, buf, min, max),
377 BarStyle::Solid => self.render_solid(area, buf, min, max),
378 BarStyle::Quadrant => self.render_quadrant(area, buf, min, max),
379 BarStyle::Octant => self.render_octant(area, buf, min, max),
380 }
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn with_gradient() {
390 let data = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
391 // check that we can use either a gradient or a boxed gradient
392 let _graph = BarGraph::new(data.clone()).with_gradient(colorgrad::preset::turbo());
393 let _graph = BarGraph::new(data).with_gradient(colorgrad::preset::turbo().boxed());
394 }
395
396 #[test]
397 fn braille() {
398 let data = (0..=40).map(|i| i as f64 * 0.125).collect();
399 let bar_graph = BarGraph::new(data);
400
401 let mut buf = Buffer::empty(Rect::new(0, 0, 21, 10));
402 bar_graph.render(buf.area, &mut buf);
403
404 assert_eq!(
405 buf,
406 Buffer::with_lines(vec![
407 " ⢀⣴⡇",
408 " ⢀⣴⣿⣿⡇",
409 " ⢀⣴⣿⣿⣿⣿⡇",
410 " ⢀⣴⣿⣿⣿⣿⣿⣿⡇",
411 " ⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⡇",
412 " ⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇",
413 " ⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇",
414 " ⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇",
415 " ⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇",
416 "⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇",
417 ])
418 );
419 }
420
421 #[test]
422 fn solid() {
423 let data = vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0];
424 let bar_graph = BarGraph::new(data).with_bar_style(BarStyle::Solid);
425
426 let mut buf = Buffer::empty(Rect::new(0, 0, 11, 10));
427 bar_graph.render(buf.area, &mut buf);
428
429 assert_eq!(
430 buf,
431 Buffer::with_lines(vec![
432 " █",
433 " ██",
434 " ███",
435 " ████",
436 " █████",
437 " ██████",
438 " ███████",
439 " ████████",
440 " █████████",
441 " ██████████",
442 ])
443 );
444 }
445
446 #[test]
447 fn quadrant() {
448 let data = vec![
449 0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.75,
450 4.0, 4.25, 4.5, 4.75, 5.0,
451 ];
452 let bar_graph = BarGraph::new(data).with_bar_style(BarStyle::Quadrant);
453
454 let mut buf = Buffer::empty(Rect::new(0, 0, 11, 10));
455 bar_graph.render(buf.area, &mut buf);
456
457 assert_eq!(
458 buf,
459 Buffer::with_lines(vec![
460 " ▗▌",
461 " ▗█▌",
462 " ▗██▌",
463 " ▗███▌",
464 " ▗████▌",
465 " ▗█████▌",
466 " ▗██████▌",
467 " ▗███████▌",
468 " ▗████████▌",
469 "▗█████████▌",
470 ])
471 );
472 }
473
474 #[test]
475 fn octant() {
476 let data = (0..=40).map(|i| i as f64 * 0.125).collect();
477 let bar_graph = BarGraph::new(data).with_bar_style(BarStyle::Octant);
478
479 let mut buf = Buffer::empty(Rect::new(0, 0, 21, 10));
480 bar_graph.render(buf.area, &mut buf);
481
482 assert_eq!(
483 buf,
484 Buffer::with_lines(vec![
485 " ▌",
486 " ██▌",
487 " ████▌",
488 " ██████▌",
489 " ████████▌",
490 " ██████████▌",
491 " ████████████▌",
492 " ██████████████▌",
493 " ████████████████▌",
494 "██████████████████▌",
495 ])
496 );
497 }
498}