use std::{ error::Error, io, time::{Duration, Instant}, }; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::{ prelude::*, widgets::{block::Title, *}, }; #[derive(Clone)] pub struct SinSignal { x: f64, interval: f64, period: f64, scale: f64, } impl SinSignal { pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal { SinSignal { x: 0.0, interval, period, scale, } } } impl Iterator for SinSignal { type Item = (f64, f64); fn next(&mut self) -> Option { let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale); self.x += self.interval; Some(point) } } struct App { signal1: SinSignal, data1: Vec<(f64, f64)>, signal2: SinSignal, data2: Vec<(f64, f64)>, window: [f64; 2], } impl App { fn new() -> App { let mut signal1 = SinSignal::new(0.2, 3.0, 18.0); let mut signal2 = SinSignal::new(0.1, 2.0, 10.0); let data1 = signal1.by_ref().take(200).collect::>(); let data2 = signal2.by_ref().take(200).collect::>(); App { signal1, data1, signal2, data2, window: [0.0, 20.0], } } fn on_tick(&mut self) { for _ in 0..5 { self.data1.remove(0); } self.data1.extend(self.signal1.by_ref().take(5)); for _ in 0..10 { self.data2.remove(0); } self.data2.extend(self.signal2.by_ref().take(10)); self.window[0] += 1.0; self.window[1] += 1.0; } } fn main() -> Result<(), Box> { // 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 res = run_app(&mut terminal, app, tick_rate); // restore terminal disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?; if let Err(err) = res { println!("{err:?}"); } Ok(()) } fn run_app( terminal: &mut Terminal, mut app: App, tick_rate: Duration, ) -> io::Result<()> { let mut last_tick = Instant::now(); loop { terminal.draw(|f| ui(f, &app))?; let timeout = tick_rate.saturating_sub(last_tick.elapsed()); if crossterm::event::poll(timeout)? { if let Event::Key(key) = event::read()? { if let KeyCode::Char('q') = key.code { return Ok(()); } } } if last_tick.elapsed() >= tick_rate { app.on_tick(); last_tick = Instant::now(); } } } fn ui(f: &mut Frame, app: &App) { let size = f.size(); let vertical_chunks = Layout::new( Direction::Vertical, [Constraint::Percentage(40), Constraint::Percentage(60)], ) .split(size); // top chart render_chart1(f, vertical_chunks[0], app); let horizontal_chunks = Layout::new( Direction::Horizontal, [Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)], ) .split(vertical_chunks[1]); // bottom left render_line_chart(f, horizontal_chunks[0]); // bottom right render_scatter(f, horizontal_chunks[1]); } fn render_chart1(f: &mut Frame, area: Rect, app: &App) { let x_labels = vec![ Span::styled( format!("{}", app.window[0]), Style::default().add_modifier(Modifier::BOLD), ), Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)), Span::styled( format!("{}", app.window[1]), Style::default().add_modifier(Modifier::BOLD), ), ]; let datasets = vec![ Dataset::default() .name("data2") .marker(symbols::Marker::Dot) .style(Style::default().fg(Color::Cyan)) .data(&app.data1), Dataset::default() .name("data3") .marker(symbols::Marker::Braille) .style(Style::default().fg(Color::Yellow)) .data(&app.data2), ]; let chart = Chart::new(datasets) .block( Block::default() .title("Chart 1".cyan().bold()) .borders(Borders::ALL), ) .x_axis( Axis::default() .title("X Axis") .style(Style::default().fg(Color::Gray)) .labels(x_labels) .bounds(app.window), ) .y_axis( Axis::default() .title("Y Axis") .style(Style::default().fg(Color::Gray)) .labels(vec!["-20".bold(), "0".into(), "20".bold()]) .bounds([-20.0, 20.0]), ); f.render_widget(chart, area); } fn render_line_chart(f: &mut Frame, area: Rect) { let datasets = vec![Dataset::default() .name("Line from only 2 points") .marker(symbols::Marker::Braille) .style(Style::default().fg(Color::Yellow)) .graph_type(GraphType::Line) .data(&[(1., 1.), (4., 4.)])]; let chart = Chart::new(datasets) .block( Block::default() .title( Title::default() .content("Line chart".cyan().bold()) .alignment(Alignment::Center), ) .borders(Borders::ALL), ) .x_axis( Axis::default() .title("X Axis") .style(Style::default().gray()) .bounds([0.0, 5.0]) .labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]), ) .y_axis( Axis::default() .title("Y Axis") .style(Style::default().gray()) .bounds([0.0, 5.0]) .labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]), ) .legend_position(Some(LegendPosition::TopLeft)) .hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2))); f.render_widget(chart, area) } fn render_scatter(f: &mut Frame, area: Rect) { let datasets = vec![ Dataset::default() .name("Heavy") .marker(Marker::Dot) .graph_type(GraphType::Scatter) .style(Style::new().yellow()) .data(&HEAVY_PAYLOAD_DATA), Dataset::default() .name("Medium") .marker(Marker::Braille) .graph_type(GraphType::Scatter) .style(Style::new().magenta()) .data(&MEDIUM_PAYLOAD_DATA), Dataset::default() .name("Small") .marker(Marker::Dot) .graph_type(GraphType::Scatter) .style(Style::new().cyan()) .data(&SMALL_PAYLOAD_DATA), ]; let chart = Chart::new(datasets) .block( Block::new().borders(Borders::all()).title( Title::default() .content("Scatter chart".cyan().bold()) .alignment(Alignment::Center), ), ) .x_axis( Axis::default() .title("Year") .bounds([1960., 2020.]) .style(Style::default().fg(Color::Gray)) .labels(vec!["1960".into(), "1990".into(), "2020".into()]), ) .y_axis( Axis::default() .title("Cost") .bounds([0., 75000.]) .style(Style::default().fg(Color::Gray)) .labels(vec!["0".into(), "37 500".into(), "75 000".into()]), ) .hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2))); f.render_widget(chart, area); } // Data from https://ourworldindata.org/space-exploration-satellites const HEAVY_PAYLOAD_DATA: [(f64, f64); 9] = [ (1965., 8200.), (1967., 5400.), (1981., 65400.), (1989., 30800.), (1997., 10200.), (2004., 11600.), (2014., 4500.), (2016., 7900.), (2018., 1500.), ]; const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [ (1963., 29500.), (1964., 30600.), (1965., 177900.), (1965., 21000.), (1966., 17900.), (1966., 8400.), (1975., 17500.), (1982., 8300.), (1985., 5100.), (1988., 18300.), (1990., 38800.), (1990., 9900.), (1991., 18700.), (1992., 9100.), (1994., 10500.), (1994., 8500.), (1994., 8700.), (1997., 6200.), (1999., 18000.), (1999., 7600.), (1999., 8900.), (1999., 9600.), (2000., 16000.), (2001., 10000.), (2002., 10400.), (2002., 8100.), (2010., 2600.), (2013., 13600.), (2017., 8000.), ]; const SMALL_PAYLOAD_DATA: [(f64, f64); 23] = [ (1961., 118500.), (1962., 14900.), (1975., 21400.), (1980., 32800.), (1988., 31100.), (1990., 41100.), (1993., 23600.), (1994., 20600.), (1994., 34600.), (1996., 50600.), (1997., 19200.), (1997., 45800.), (1998., 19100.), (2000., 73100.), (2003., 11200.), (2008., 12600.), (2010., 30500.), (2012., 20000.), (2013., 10600.), (2013., 34500.), (2015., 10600.), (2018., 23100.), (2019., 17300.), ];