Skip to main content

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//! ![Braille Rainbow](https://vhs.charm.sh/vhs-1sx9Ht6NzU6e28Cl51jJVv.gif)
5//! ![Solid Plasma](https://vhs.charm.sh/vhs-7pWuLtZpzrz1OVD04cMt1a.gif)
6//! ![Quadrant Magma](https://vhs.charm.sh/vhs-1rx6XQ9mLiO8qybSBXRGwn.gif)
7//! ![Octant Viridis](https://vhs.charm.sh/vhs-7BevtFvn5S7j8jcAJrxl1F.gif)
8//!
9//! <details><summary>More examples</summary>
10//!
11//! ![Braille Magma](https://vhs.charm.sh/vhs-4RDwcz9DApA90iJYMQXHXd.gif)
12//! ![Braille Viridis](https://vhs.charm.sh/vhs-5ylsZAdKGPiHUYboOpZFZL.gif)
13//! ![Solid Inferno](https://vhs.charm.sh/vhs-4z1gbmJ50KGz2TPej3mnVf.gif)
14//! ![Solid Sinebow](https://vhs.charm.sh/vhs-63aAmMhcfMT8CnWCV20dsn.gif)
15//! ![Quadrant Plasma](https://vhs.charm.sh/vhs-5o8AfNgQZAT1U4hOaLtY7m.gif)
16//! ![Quadrant Sinebow](https://vhs.charm.sh/vhs-1zAyLkSvNGTKL1SGHRyZFD.gif)
17//! ![Octant Inferno](https://vhs.charm.sh/vhs-3bwxZkh1WcSFUkVzpBXWb9.gif)
18//! ![Octant Rainbow](https://vhs.charm.sh/vhs-6eDjdEbRK4xWNtVpHuTkIh.gif)
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}