0064-Tui-图表示例

环境

  • Time 2022-08-16
  • Rust 1.63.0
  • Tui 0.18.0

前言

说明

参考:https://github.com/fdehau/tui-rs/blob/master/examples/chart.rs

目标

使用 tui-rs 显示图表。

常量数据

const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
const DATA2: [(f64, f64); 7] = [
    (0.0, 0.0),
    (10.0, 1.0),
    (20.0, 0.5),
    (30.0, 1.5),
    (40.0, 1.0),
    (50.0, 2.5),
    (60.0, 3.0),
];

定义应用


#[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<Self::Item> {
        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::<Vec<(f64, f64)>>();
        let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
        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 ui<B: Backend>(frame: &mut Frame<B>, app: &mut App) {
    let chunks = Layout::default()
        .direction(layout::Direction::Vertical)
        .constraints([
            Constraint::Ratio(1, 3),
            Constraint::Ratio(1, 3),
            Constraint::Ratio(1, 3),
        ])
        .split(frame.size());

    render_top(frame, chunks[0], app);
    render_mid(frame, chunks[1]);
    render_bot(frame, chunks[2]);
}

render_top

fn render_top<B: Backend>(frame: &mut Frame<B>, area: layout::Rect, app: &App) {
    let x_labels = vec![
        text::Span::styled(
            format!("{}", app.window[0]),
            Style::default().add_modifier(Modifier::BOLD),
        ),
        text::Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)),
        text::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(text::Span::styled(
                    "Chart 1",
                    Style::default()
                        .fg(Color::Cyan)
                        .add_modifier(Modifier::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![
                    text::Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
                    text::Span::raw("0"),
                    text::Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
                ])
                .bounds([-20.0, 20.0]),
        );
    frame.render_widget(chart, area);
}

render_mid

fn render_mid<B: Backend>(frame: &mut Frame<B>, area: layout::Rect) {
    let datasets = vec![Dataset::default()
        .name("data")
        .marker(symbols::Marker::Braille)
        .style(Style::default().fg(Color::Yellow))
        .graph_type(GraphType::Line)
        .data(&DATA)];
    let chart = Chart::new(datasets)
        .block(
            Block::default()
                .title(text::Span::styled(
                    "Chart 2",
                    Style::default()
                        .fg(Color::Cyan)
                        .add_modifier(Modifier::BOLD),
                ))
                .borders(Borders::ALL),
        )
        .x_axis(
            Axis::default()
                .title("X Axis")
                .style(Style::default().fg(Color::Gray))
                .bounds([0.0, 5.0])
                .labels(vec![
                    text::Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
                    text::Span::raw("2.5"),
                    text::Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
                ]),
        )
        .y_axis(
            Axis::default()
                .title("Y Axis")
                .style(Style::default().fg(Color::Gray))
                .bounds([0.0, 5.0])
                .labels(vec![
                    text::Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
                    text::Span::raw("2.5"),
                    text::Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
                ]),
        );
    frame.render_widget(chart, area);
}

render_bot

fn render_bot<B: Backend>(frame: &mut Frame<B>, area: layout::Rect) {
    let datasets = vec![Dataset::default()
        .name("data")
        .marker(symbols::Marker::Braille)
        .style(Style::default().fg(Color::Yellow))
        .graph_type(GraphType::Line)
        .data(&DATA2)];
    let chart = Chart::new(datasets)
        .block(
            Block::default()
                .title(text::Span::styled(
                    "Chart 3",
                    Style::default()
                        .fg(Color::Cyan)
                        .add_modifier(Modifier::BOLD),
                ))
                .borders(Borders::ALL),
        )
        .x_axis(
            Axis::default()
                .title("X Axis")
                .style(Style::default().fg(Color::Gray))
                .bounds([0.0, 50.0])
                .labels(vec![
                    text::Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
                    text::Span::raw("25"),
                    text::Span::styled("50", Style::default().add_modifier(Modifier::BOLD)),
                ]),
        )
        .y_axis(
            Axis::default()
                .title("Y Axis")
                .style(Style::default().fg(Color::Gray))
                .bounds([0.0, 5.0])
                .labels(vec![
                    text::Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
                    text::Span::raw("2.5"),
                    text::Span::styled("5", Style::default().add_modifier(Modifier::BOLD)),
                ]),
        );
    frame.render_widget(chart, area);
}

效果展示

图表

总结

使用 tui-rs 渲染图表。

附录

源码

use anyhow::{Context, Result};
use crossterm::{event, terminal, ExecutableCommand};
use layout::{Constraint, Layout};
use tui::backend::{Backend, CrosstermBackend};
use tui::style::{Color, Modifier, Style};
use tui::{layout, symbols, text, widgets, Frame, Terminal};
use widgets::{Axis, Block, Borders, Chart, Dataset, GraphType};

pub fn main() -> Result<()> {
    terminal::enable_raw_mode()?;
    let mut backend = CrosstermBackend::new(std::io::stdout());
    backend
        .execute(terminal::EnterAlternateScreen)?
        .execute(terminal::Clear(terminal::ClearType::All))?
        .hide_cursor()?;
    let mut terminal = tui::Terminal::new(backend)?;
    run(&mut terminal)?;
    terminal::disable_raw_mode()?;
    terminal
        .backend_mut()
        .execute(terminal::Clear(terminal::ClearType::All))?
        .execute(terminal::LeaveAlternateScreen)?
        .show_cursor()
        .context("重置控制台失败")
}

const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
const DATA2: [(f64, f64); 7] = [
    (0.0, 0.0),
    (10.0, 1.0),
    (20.0, 0.5),
    (30.0, 1.5),
    (40.0, 1.0),
    (50.0, 2.5),
    (60.0, 3.0),
];

#[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<Self::Item> {
        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::<Vec<(f64, f64)>>();
        let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
        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 run<B: Backend>(terminal: &mut Terminal<B>) -> Result<()> {
    let timeout = std::time::Duration::from_millis(500);
    let mut app = App::new();
    loop {
        terminal.draw(|frame| ui(frame, &mut app))?;

        if event::poll(timeout)? {
            if let event::Event::Key(key) = event::read()? {
                use event::KeyCode::{Char, Esc};
                match key.code {
                    Char('q') | Char('Q') | Esc => return Ok(()),
                    _ => {}
                }
            }
        }
        app.on_tick();
    }
}

fn ui<B: Backend>(frame: &mut Frame<B>, app: &mut App) {
    let chunks = Layout::default()
        .direction(layout::Direction::Vertical)
        .constraints([
            Constraint::Ratio(1, 3),
            Constraint::Ratio(1, 3),
            Constraint::Ratio(1, 3),
        ])
        .split(frame.size());

    render_top(frame, chunks[0], app);
    render_mid(frame, chunks[1]);
    render_bot(frame, chunks[2]);
}

fn render_top<B: Backend>(frame: &mut Frame<B>, area: layout::Rect, app: &App) {
    let x_labels = vec![
        text::Span::styled(
            format!("{}", app.window[0]),
            Style::default().add_modifier(Modifier::BOLD),
        ),
        text::Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)),
        text::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(text::Span::styled(
                    "Chart 1",
                    Style::default()
                        .fg(Color::Cyan)
                        .add_modifier(Modifier::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![
                    text::Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
                    text::Span::raw("0"),
                    text::Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
                ])
                .bounds([-20.0, 20.0]),
        );
    frame.render_widget(chart, area);
}

fn render_mid<B: Backend>(frame: &mut Frame<B>, area: layout::Rect) {
    let datasets = vec![Dataset::default()
        .name("data")
        .marker(symbols::Marker::Braille)
        .style(Style::default().fg(Color::Yellow))
        .graph_type(GraphType::Line)
        .data(&DATA)];
    let chart = Chart::new(datasets)
        .block(
            Block::default()
                .title(text::Span::styled(
                    "Chart 2",
                    Style::default()
                        .fg(Color::Cyan)
                        .add_modifier(Modifier::BOLD),
                ))
                .borders(Borders::ALL),
        )
        .x_axis(
            Axis::default()
                .title("X Axis")
                .style(Style::default().fg(Color::Gray))
                .bounds([0.0, 5.0])
                .labels(vec![
                    text::Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
                    text::Span::raw("2.5"),
                    text::Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
                ]),
        )
        .y_axis(
            Axis::default()
                .title("Y Axis")
                .style(Style::default().fg(Color::Gray))
                .bounds([0.0, 5.0])
                .labels(vec![
                    text::Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
                    text::Span::raw("2.5"),
                    text::Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
                ]),
        );
    frame.render_widget(chart, area);
}

fn render_bot<B: Backend>(frame: &mut Frame<B>, area: layout::Rect) {
    let datasets = vec![Dataset::default()
        .name("data")
        .marker(symbols::Marker::Braille)
        .style(Style::default().fg(Color::Yellow))
        .graph_type(GraphType::Line)
        .data(&DATA2)];
    let chart = Chart::new(datasets)
        .block(
            Block::default()
                .title(text::Span::styled(
                    "Chart 3",
                    Style::default()
                        .fg(Color::Cyan)
                        .add_modifier(Modifier::BOLD),
                ))
                .borders(Borders::ALL),
        )
        .x_axis(
            Axis::default()
                .title("X Axis")
                .style(Style::default().fg(Color::Gray))
                .bounds([0.0, 50.0])
                .labels(vec![
                    text::Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
                    text::Span::raw("25"),
                    text::Span::styled("50", Style::default().add_modifier(Modifier::BOLD)),
                ]),
        )
        .y_axis(
            Axis::default()
                .title("Y Axis")
                .style(Style::default().fg(Color::Gray))
                .bounds([0.0, 5.0])
                .labels(vec![
                    text::Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
                    text::Span::raw("2.5"),
                    text::Span::styled("5", Style::default().add_modifier(Modifier::BOLD)),
                ]),
        );
    frame.render_widget(chart, area);
}
posted @   jiangbo4444  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
历史上的今天:
2020-09-30 【JavaScript】展开语法
2020-09-30 【JavaScript】剩余参数
2020-09-30 【JavaScript】默认参数
2020-09-30 【JavaScript】import
2020-09-30 【JavaScript】debugger
2020-09-30 【JavaScript】for await...of
点击右上角即可分享
微信分享提示