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