Skip to content

Commit

Permalink
docs(examples): simplify the barchart example (ratatui#1079)
Browse files Browse the repository at this point in the history
The `barchart` example has been split into two examples: `barchart` and
`barchart-grouped`. The `barchart` example now shows a simple barchart
with random data, while the `barchart-grouped` example shows a grouped
barchart with fake revenue data.

This simplifies the examples a bit so they don't cover too much at once.

- Simplify the rendering functions
- Fix several clippy lints that were marked as allowed

---------

Co-authored-by: EdJoPaTo <rfc-conform-git-commit-email@funny-long-domain-label-everyone-hates-as-it-is-too-long.edjopato.de>
  • Loading branch information
joshka and EdJoPaTo committed Oct 14, 2024
1 parent 05919a5 commit 3a6b55c
Show file tree
Hide file tree
Showing 6 changed files with 467 additions and 256 deletions.
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ exclude = [
"*.log",
"tags",
]
autoexamples = true
edition = "2021"
rust-version = "1.74.0"

Expand Down Expand Up @@ -218,6 +217,11 @@ name = "barchart"
required-features = ["crossterm"]
doc-scrape-examples = true

[[example]]
name = "barchart-grouped"
required-features = ["crossterm"]
doc-scrape-examples = true

[[example]]
name = "block"
required-features = ["crossterm"]
Expand Down
20 changes: 20 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ This folder might use unreleased code. View the examples for the latest release
> We don't keep the CHANGELOG updated with unreleased changes, check the git commit history or run
> `git-cliff -u` against a cloned version of this repository.
## Design choices

The examples contain some opinionated choices in order to make it easier for newer rustaceans to
easily be productive in creating applications:

- Each example has an App struct, with methods that implement a main loop, handle events and drawing
the UI.
- We use color_eyre for handling errors and panics. See [How to use color-eyre with Ratatui] on the
website for more information about this.
- Common code is not extracted into a separate file. This makes each example self-contained and easy
to read as a whole.

Not every example has been updated with all these points in mind yet, however over time they will
be. None of the above choices are strictly necessary for Ratatui apps, but these choices make
examples easier to run, maintain and explain. These choices are designed to help newer users fall
into the pit of success when incorporating example code into their own apps. We may also eventually
move some of these design choices into the core of Ratatui to simplify apps.

[How to use color-eyre with Ratatui]: https://ratatui.rs/how-to/develop-apps/color_eyre/

## Demo2

This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
Expand Down
278 changes: 278 additions & 0 deletions examples/barchart-grouped.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
//! # [Ratatui] `BarChart` example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::iter::zip;

use color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event, KeyCode},
prelude::{Color, Constraint, Direction, Frame, Layout, Line, Style},
style::Stylize,
widgets::{Bar, BarChart, BarGroup, Block},
};

use self::terminal::Terminal;

const COMPANY_COUNT: usize = 3;
const PERIOD_COUNT: usize = 4;

struct App {
should_exit: bool,
companies: [Company; COMPANY_COUNT],
revenues: [Revenues; PERIOD_COUNT],
}

struct Revenues {
period: &'static str,
revenues: [u32; COMPANY_COUNT],
}

struct Company {
short_name: &'static str,
name: &'static str,
color: Color,
}

fn main() -> Result<()> {
color_eyre::install()?;
let mut terminal = terminal::init()?;
let app = App::new();
app.run(&mut terminal)?;
terminal::restore()?;
Ok(())
}

impl App {
const fn new() -> Self {
Self {
should_exit: false,
companies: fake_companies(),
revenues: fake_revenues(),
}
}

fn run(mut self, terminal: &mut Terminal) -> Result<()> {
while !self.should_exit {
self.draw(terminal)?;
self.handle_events()?;
}
Ok(())
}

fn draw(&self, terminal: &mut Terminal) -> Result<()> {
terminal.draw(|frame| self.render(frame))?;
Ok(())
}

fn handle_events(&mut self) -> Result<()> {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
self.should_exit = true;
}
}
Ok(())
}

fn render(&self, frame: &mut Frame) {
use Constraint::{Fill, Length, Min};
let [title, top, bottom] = Layout::vertical([Length(1), Fill(1), Min(20)])
.spacing(1)
.areas(frame.area());

frame.render_widget("Grouped Barchart".bold().into_centered_line(), title);
frame.render_widget(self.vertical_revenue_barchart(), top);
frame.render_widget(self.horizontal_revenue_barchart(), bottom);
}

/// Create a vertical revenue bar chart with the data from the `revenues` field.
fn vertical_revenue_barchart(&self) -> BarChart<'_> {
let title = Line::from("Company revenues (Vertical)").centered();
let mut barchart = BarChart::default()
.block(Block::new().title(title))
.bar_gap(0)
.bar_width(6)
.group_gap(2);
for group in self
.revenues
.iter()
.map(|revenue| revenue.to_vertical_bar_group(&self.companies))
{
barchart = barchart.data(group);
}
barchart
}

/// Create a horizontal revenue bar chart with the data from the `revenues` field.
fn horizontal_revenue_barchart(&self) -> BarChart<'_> {
let title = Line::from("Company Revenues (Horizontal)").centered();
let mut barchart = BarChart::default()
.block(Block::new().title(title))
.bar_width(1)
.group_gap(2)
.bar_gap(0)
.direction(Direction::Horizontal);
for group in self
.revenues
.iter()
.map(|revenue| revenue.to_horizontal_bar_group(&self.companies))
{
barchart = barchart.data(group);
}
barchart
}
}

/// Generate fake company data
const fn fake_companies() -> [Company; COMPANY_COUNT] {
[
Company::new("BAKE", "Bake my day", Color::LightRed),
Company::new("BITE", "Bits and Bites", Color::Blue),
Company::new("TART", "Tart of the Table", Color::White),
]
}

/// Some fake revenue data
const fn fake_revenues() -> [Revenues; PERIOD_COUNT] {
[
Revenues::new("Jan", [8500, 6500, 7000]),
Revenues::new("Feb", [9000, 7500, 8500]),
Revenues::new("Mar", [9500, 4500, 8200]),
Revenues::new("Apr", [6300, 4000, 5000]),
]
}

impl Revenues {
/// Create a new instance of `Revenues`
const fn new(period: &'static str, revenues: [u32; COMPANY_COUNT]) -> Self {
Self { period, revenues }
}

/// Create a `BarGroup` with vertical bars for each company
fn to_vertical_bar_group<'a>(&self, companies: &'a [Company]) -> BarGroup<'a> {
let bars: Vec<Bar> = zip(companies, self.revenues)
.map(|(company, revenue)| company.vertical_revenue_bar(revenue))
.collect();
BarGroup::default()
.label(Line::from(self.period).centered())
.bars(&bars)
}

/// Create a `BarGroup` with horizontal bars for each company
fn to_horizontal_bar_group<'a>(&'a self, companies: &'a [Company]) -> BarGroup<'a> {
let bars: Vec<Bar> = zip(companies, self.revenues)
.map(|(company, revenue)| company.horizontal_revenue_bar(revenue))
.collect();
BarGroup::default()
.label(Line::from(self.period).centered())
.bars(&bars)
}
}

impl Company {
/// Create a new instance of `Company`
const fn new(short_name: &'static str, name: &'static str, color: Color) -> Self {
Self {
short_name,
name,
color,
}
}

/// Create a vertical revenue bar for the company
///
/// The label is the short name of the company, and will be displayed under the bar
fn vertical_revenue_bar(&self, revenue: u32) -> Bar {
let text_value = format!("{:.1}M", f64::from(revenue) / 1000.);
Bar::default()
.label(self.short_name.into())
.value(u64::from(revenue))
.text_value(text_value)
.style(self.color)
.value_style(Style::new().fg(Color::Black).bg(self.color))
}

/// Create a horizontal revenue bar for the company
///
/// The label is the long name of the company combined with the revenue and will be displayed
/// on the bar
fn horizontal_revenue_bar(&self, revenue: u32) -> Bar {
let text_value = format!("{} ({:.1} M)", self.name, f64::from(revenue) / 1000.);
Bar::default()
.value(u64::from(revenue))
.text_value(text_value)
.style(self.color)
.value_style(Style::new().fg(Color::Black).bg(self.color))
}
}

/// Contains functions common to all examples
mod terminal {
use std::{
io::{self, stdout, Stdout},
panic,
};

use ratatui::{
backend::CrosstermBackend,
crossterm::{
execute,
terminal::{
disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
LeaveAlternateScreen,
},
},
};

// A type alias to simplify the usage of the terminal and make it easier to change the backend
// or choice of writer.
pub type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;

/// Initialize the terminal by enabling raw mode and entering the alternate screen.
///
/// This function should be called before the program starts to ensure that the terminal is in
/// the correct state for the application.
pub fn init() -> io::Result<Terminal> {
install_panic_hook();
enable_raw_mode()?;
execute!(stdout(), EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}

/// Restore the terminal by leaving the alternate screen and disabling raw mode.
///
/// This function should be called before the program exits to ensure that the terminal is
/// restored to its original state.
pub fn restore() -> io::Result<()> {
disable_raw_mode()?;
execute!(
stdout(),
LeaveAlternateScreen,
Clear(ClearType::FromCursorDown),
)
}

/// Install a panic hook that restores the terminal before printing the panic.
///
/// This prevents error messages from being messed up by the terminal state.
fn install_panic_hook() {
let panic_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
let _ = restore();
panic_hook(panic_info);
}));
}
}
Loading

0 comments on commit 3a6b55c

Please sign in to comment.