mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-10 07:04:17 +00:00
docs(examples): simplify the barchart example (#1079)
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>
This commit is contained in:
parent
a23ecd9b45
commit
fe4eeab676
6 changed files with 476 additions and 265 deletions
|
@ -18,7 +18,6 @@ exclude = [
|
||||||
"*.log",
|
"*.log",
|
||||||
"tags",
|
"tags",
|
||||||
]
|
]
|
||||||
autoexamples = true
|
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.74.0"
|
rust-version = "1.74.0"
|
||||||
|
|
||||||
|
@ -218,6 +217,11 @@ name = "barchart"
|
||||||
required-features = ["crossterm"]
|
required-features = ["crossterm"]
|
||||||
doc-scrape-examples = true
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "barchart-grouped"
|
||||||
|
required-features = ["crossterm"]
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "block"
|
name = "block"
|
||||||
required-features = ["crossterm"]
|
required-features = ["crossterm"]
|
||||||
|
|
|
@ -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
|
> 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.
|
> `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
|
## Demo2
|
||||||
|
|
||||||
This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
|
This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
|
||||||
|
|
278
examples/barchart-grouped.rs
Normal file
278
examples/barchart-grouped.rs
Normal file
|
@ -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);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,290 +13,187 @@
|
||||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{
|
use color_eyre::Result;
|
||||||
error::Error,
|
use rand::{thread_rng, Rng};
|
||||||
io,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
crossterm::event::{self, Event, KeyCode},
|
||||||
crossterm::{
|
layout::{Constraint, Direction, Layout},
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
prelude::{Color, Line, Style, Stylize},
|
||||||
execute,
|
widgets::{Bar, BarChart, BarGroup, Block},
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
|
||||||
style::{Color, Modifier, Style},
|
|
||||||
text::{Line, Span},
|
|
||||||
widgets::{Bar, BarChart, BarGroup, Block, Paragraph},
|
|
||||||
Frame, Terminal,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Company<'a> {
|
use self::terminal::Terminal;
|
||||||
revenue: [u64; 4],
|
|
||||||
label: &'a str,
|
struct App {
|
||||||
bar_style: Style,
|
should_exit: bool,
|
||||||
|
temperatures: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct App<'a> {
|
fn main() -> Result<()> {
|
||||||
data: Vec<(&'a str, u64)>,
|
color_eyre::install()?;
|
||||||
months: [&'a str; 4],
|
let mut terminal = terminal::init()?;
|
||||||
companies: [Company<'a>; 3],
|
|
||||||
}
|
|
||||||
|
|
||||||
const TOTAL_REVENUE: &str = "Total Revenue";
|
|
||||||
|
|
||||||
impl<'a> App<'a> {
|
|
||||||
fn new() -> Self {
|
|
||||||
App {
|
|
||||||
data: vec![
|
|
||||||
("B1", 9),
|
|
||||||
("B2", 12),
|
|
||||||
("B3", 5),
|
|
||||||
("B4", 8),
|
|
||||||
("B5", 2),
|
|
||||||
("B6", 4),
|
|
||||||
("B7", 5),
|
|
||||||
("B8", 9),
|
|
||||||
("B9", 14),
|
|
||||||
("B10", 15),
|
|
||||||
("B11", 1),
|
|
||||||
("B12", 0),
|
|
||||||
("B13", 4),
|
|
||||||
("B14", 6),
|
|
||||||
("B15", 4),
|
|
||||||
("B16", 6),
|
|
||||||
("B17", 4),
|
|
||||||
("B18", 7),
|
|
||||||
("B19", 13),
|
|
||||||
("B20", 8),
|
|
||||||
("B21", 11),
|
|
||||||
("B22", 9),
|
|
||||||
("B23", 3),
|
|
||||||
("B24", 5),
|
|
||||||
],
|
|
||||||
companies: [
|
|
||||||
Company {
|
|
||||||
label: "Comp.A",
|
|
||||||
revenue: [9500, 12500, 5300, 8500],
|
|
||||||
bar_style: Style::default().fg(Color::Green),
|
|
||||||
},
|
|
||||||
Company {
|
|
||||||
label: "Comp.B",
|
|
||||||
revenue: [1500, 2500, 3000, 500],
|
|
||||||
bar_style: Style::default().fg(Color::Yellow),
|
|
||||||
},
|
|
||||||
Company {
|
|
||||||
label: "Comp.C",
|
|
||||||
revenue: [10500, 10600, 9000, 4200],
|
|
||||||
bar_style: Style::default().fg(Color::White),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
months: ["Mars", "Apr", "May", "Jun"],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_tick(&mut self) {
|
|
||||||
let value = self.data.pop().unwrap();
|
|
||||||
self.data.insert(0, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
// 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();
|
let app = App::new();
|
||||||
let res = run_app(&mut terminal, app, tick_rate);
|
app.run(&mut terminal)?;
|
||||||
|
terminal::restore()?;
|
||||||
// restore terminal
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(
|
|
||||||
terminal.backend_mut(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
DisableMouseCapture
|
|
||||||
)?;
|
|
||||||
terminal.show_cursor()?;
|
|
||||||
|
|
||||||
if let Err(err) = res {
|
|
||||||
println!("{err:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_app<B: Backend>(
|
impl App {
|
||||||
terminal: &mut Terminal<B>,
|
fn new() -> Self {
|
||||||
mut app: App,
|
let mut rng = thread_rng();
|
||||||
tick_rate: Duration,
|
let temperatures = (0..24).map(|_| rng.gen_range(50..90)).collect();
|
||||||
) -> io::Result<()> {
|
Self {
|
||||||
let mut last_tick = Instant::now();
|
should_exit: false,
|
||||||
loop {
|
temperatures,
|
||||||
terminal.draw(|f| ui(f, &app))?;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
fn run(mut self, terminal: &mut Terminal) -> Result<()> {
|
||||||
if crossterm::event::poll(timeout)? {
|
while !self.should_exit {
|
||||||
if let Event::Key(key) = event::read()? {
|
self.draw(terminal)?;
|
||||||
if key.code == KeyCode::Char('q') {
|
self.handle_events()?;
|
||||||
return Ok(());
|
}
|
||||||
}
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if last_tick.elapsed() >= tick_rate {
|
Ok(())
|
||||||
app.on_tick();
|
}
|
||||||
last_tick = Instant::now();
|
|
||||||
}
|
fn render(&self, frame: &mut ratatui::Frame) {
|
||||||
|
let [title, vertical, horizontal] = Layout::vertical([
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Fill(1),
|
||||||
|
Constraint::Fill(1),
|
||||||
|
])
|
||||||
|
.spacing(1)
|
||||||
|
.areas(frame.area());
|
||||||
|
frame.render_widget("Barchart".bold().into_centered_line(), title);
|
||||||
|
frame.render_widget(vertical_barchart(&self.temperatures), vertical);
|
||||||
|
frame.render_widget(horizontal_barchart(&self.temperatures), horizontal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(frame: &mut Frame, app: &App) {
|
/// Create a vertical bar chart from the temperatures data.
|
||||||
let vertical = Layout::vertical([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
|
fn vertical_barchart(temperatures: &[u8]) -> BarChart {
|
||||||
let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
let bars: Vec<Bar> = temperatures
|
||||||
let [top, bottom] = vertical.areas(frame.area());
|
|
||||||
let [left, right] = horizontal.areas(bottom);
|
|
||||||
|
|
||||||
let barchart = BarChart::default()
|
|
||||||
.block(Block::bordered().title("Data1"))
|
|
||||||
.data(&app.data)
|
|
||||||
.bar_width(9)
|
|
||||||
.bar_style(Style::default().fg(Color::Yellow))
|
|
||||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
|
|
||||||
|
|
||||||
frame.render_widget(barchart, top);
|
|
||||||
draw_bar_with_group_labels(frame, app, left);
|
|
||||||
draw_horizontal_bars(frame, app, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
|
|
||||||
app.months
|
|
||||||
.iter()
|
.iter()
|
||||||
|
.map(|v| u64::from(*v))
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, &month)| {
|
.map(|(i, value)| {
|
||||||
let bars: Vec<Bar> = app
|
Bar::default()
|
||||||
.companies
|
.value(value)
|
||||||
.iter()
|
.label(Line::from(format!("{i:>02}:00")))
|
||||||
.map(|c| {
|
.text_value(format!("{value:>3}°"))
|
||||||
let mut bar = Bar::default()
|
.style(temperature_style(value))
|
||||||
.value(c.revenue[i])
|
.value_style(temperature_style(value).reversed())
|
||||||
.style(c.bar_style)
|
|
||||||
.value_style(
|
|
||||||
Style::default()
|
|
||||||
.bg(c.bar_style.fg.unwrap())
|
|
||||||
.fg(Color::Black),
|
|
||||||
);
|
|
||||||
|
|
||||||
if combine_values_and_labels {
|
|
||||||
bar = bar.text_value(format!(
|
|
||||||
"{} ({:.1} M)",
|
|
||||||
c.label,
|
|
||||||
(c.revenue[i] as f64) / 1000.
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
bar = bar
|
|
||||||
.text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.))
|
|
||||||
.label(c.label.into());
|
|
||||||
}
|
|
||||||
bar
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
BarGroup::default()
|
|
||||||
.label(Line::from(month).centered())
|
|
||||||
.bars(&bars)
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect();
|
||||||
|
let title = Line::from("Weather (Vertical)").centered();
|
||||||
|
BarChart::default()
|
||||||
|
.data(BarGroup::default().bars(&bars))
|
||||||
|
.block(Block::new().title(title))
|
||||||
|
.bar_width(5)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
/// Create a horizontal bar chart from the temperatures data.
|
||||||
fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
|
fn horizontal_barchart(temperatures: &[u8]) -> BarChart {
|
||||||
const LEGEND_HEIGHT: u16 = 6;
|
let bars: Vec<Bar> = temperatures
|
||||||
|
.iter()
|
||||||
let groups = create_groups(app, false);
|
.map(|v| u64::from(*v))
|
||||||
|
.enumerate()
|
||||||
let mut barchart = BarChart::default()
|
.map(|(i, value)| {
|
||||||
.block(Block::bordered().title("Data1"))
|
let style = temperature_style(value);
|
||||||
.bar_width(7)
|
Bar::default()
|
||||||
.group_gap(3);
|
.value(value)
|
||||||
|
.label(Line::from(format!("{i:>02}:00")))
|
||||||
for group in groups {
|
.text_value(format!("{value:>3}°"))
|
||||||
barchart = barchart.data(group);
|
.style(style)
|
||||||
}
|
.value_style(style.reversed())
|
||||||
|
})
|
||||||
f.render_widget(barchart, area);
|
.collect();
|
||||||
|
let title = Line::from("Weather (Horizontal)").centered();
|
||||||
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
|
BarChart::default()
|
||||||
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
|
.block(Block::new().title(title))
|
||||||
let legend_area = Rect {
|
.data(BarGroup::default().bars(&bars))
|
||||||
height: LEGEND_HEIGHT,
|
|
||||||
width: legend_width,
|
|
||||||
y: area.y,
|
|
||||||
x: area.right() - legend_width,
|
|
||||||
};
|
|
||||||
draw_legend(f, legend_area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
|
||||||
fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
|
|
||||||
const LEGEND_HEIGHT: u16 = 6;
|
|
||||||
|
|
||||||
let groups = create_groups(app, true);
|
|
||||||
|
|
||||||
let mut barchart = BarChart::default()
|
|
||||||
.block(Block::bordered().title("Data1"))
|
|
||||||
.bar_width(1)
|
.bar_width(1)
|
||||||
.group_gap(1)
|
|
||||||
.bar_gap(0)
|
.bar_gap(0)
|
||||||
.direction(Direction::Horizontal);
|
.direction(Direction::Horizontal)
|
||||||
|
|
||||||
for group in groups {
|
|
||||||
barchart = barchart.data(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
f.render_widget(barchart, area);
|
|
||||||
|
|
||||||
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
|
|
||||||
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
|
|
||||||
let legend_area = Rect {
|
|
||||||
height: LEGEND_HEIGHT,
|
|
||||||
width: legend_width,
|
|
||||||
y: area.y,
|
|
||||||
x: area.right() - legend_width,
|
|
||||||
};
|
|
||||||
draw_legend(f, legend_area);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_legend(f: &mut Frame, area: Rect) {
|
/// create a yellow to red value based on the value (50-90)
|
||||||
let text = vec![
|
fn temperature_style(value: u64) -> Style {
|
||||||
Line::from(Span::styled(
|
let green = (255.0 * (1.0 - (value - 50) as f64 / 40.0)) as u8;
|
||||||
TOTAL_REVENUE,
|
let color = Color::Rgb(255, green, 0);
|
||||||
Style::default()
|
Style::new().fg(color)
|
||||||
.add_modifier(Modifier::BOLD)
|
}
|
||||||
.fg(Color::White),
|
|
||||||
)),
|
/// Contains functions common to all examples
|
||||||
Line::from(Span::styled(
|
mod terminal {
|
||||||
"- Company A",
|
use std::{
|
||||||
Style::default().fg(Color::Green),
|
io::{self, stdout, Stdout},
|
||||||
)),
|
panic,
|
||||||
Line::from(Span::styled(
|
};
|
||||||
"- Company B",
|
|
||||||
Style::default().fg(Color::Yellow),
|
use ratatui::{
|
||||||
)),
|
backend::CrosstermBackend,
|
||||||
Line::from(Span::styled(
|
crossterm::{
|
||||||
"- Company C",
|
execute,
|
||||||
Style::default().fg(Color::White),
|
terminal::{
|
||||||
)),
|
disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
|
||||||
];
|
LeaveAlternateScreen,
|
||||||
|
},
|
||||||
let block = Block::bordered().style(Style::default().fg(Color::White));
|
},
|
||||||
let paragraph = Paragraph::new(text).block(block);
|
};
|
||||||
f.render_widget(paragraph, area);
|
|
||||||
|
// 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);
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
12
examples/vhs/barchart-grouped.tape
Normal file
12
examples/vhs/barchart-grouped.tape
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||||
|
# To run this script, install vhs and run `vhs ./examples/barchart.tape`
|
||||||
|
Output "target/barchart-grouped.gif"
|
||||||
|
Set Theme "Aardvark Blue"
|
||||||
|
Set Width 1200
|
||||||
|
Set Height 1000
|
||||||
|
Hide
|
||||||
|
Type "cargo run --example=barchart-grouped"
|
||||||
|
Enter
|
||||||
|
Sleep 1s
|
||||||
|
Show
|
||||||
|
Sleep 1s
|
|
@ -3,10 +3,10 @@
|
||||||
Output "target/barchart.gif"
|
Output "target/barchart.gif"
|
||||||
Set Theme "Aardvark Blue"
|
Set Theme "Aardvark Blue"
|
||||||
Set Width 1200
|
Set Width 1200
|
||||||
Set Height 800
|
Set Height 600
|
||||||
Hide
|
Hide
|
||||||
Type "cargo run --example=barchart"
|
Type "cargo run --example=barchart"
|
||||||
Enter
|
Enter
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
Show
|
Show
|
||||||
Sleep 5s
|
Sleep 1s
|
||||||
|
|
Loading…
Reference in a new issue