[go: up one dir, main page]

chart/
chart.rs

1//! # [Ratatui] Chart example
2//!
3//! The latest version of this example is available in the [examples] folder in the repository.
4//!
5//! Please note that the examples are designed to be run against the `main` branch of the Github
6//! repository. This means that you may not be able to compile with the latest release version on
7//! crates.io, or the one that you have installed locally.
8//!
9//! See the [examples readme] for more information on finding examples that match the version of the
10//! library you are using.
11//!
12//! [Ratatui]: https://github.com/ratatui/ratatui
13//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
14//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
15
16use std::time::{Duration, Instant};
17
18use color_eyre::Result;
19use ratatui::{
20    crossterm::event::{self, Event, KeyCode},
21    layout::{Constraint, Layout, Rect},
22    style::{Color, Modifier, Style, Stylize},
23    symbols::{self, Marker},
24    text::{Line, Span},
25    widgets::{Axis, Block, Chart, Dataset, GraphType, LegendPosition},
26    DefaultTerminal, Frame,
27};
28
29fn main() -> Result<()> {
30    color_eyre::install()?;
31    let terminal = ratatui::init();
32    let app_result = App::new().run(terminal);
33    ratatui::restore();
34    app_result
35}
36
37struct App {
38    signal1: SinSignal,
39    data1: Vec<(f64, f64)>,
40    signal2: SinSignal,
41    data2: Vec<(f64, f64)>,
42    window: [f64; 2],
43}
44
45#[derive(Clone)]
46struct SinSignal {
47    x: f64,
48    interval: f64,
49    period: f64,
50    scale: f64,
51}
52
53impl SinSignal {
54    const fn new(interval: f64, period: f64, scale: f64) -> Self {
55        Self {
56            x: 0.0,
57            interval,
58            period,
59            scale,
60        }
61    }
62}
63
64impl Iterator for SinSignal {
65    type Item = (f64, f64);
66    fn next(&mut self) -> Option<Self::Item> {
67        let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
68        self.x += self.interval;
69        Some(point)
70    }
71}
72
73impl App {
74    fn new() -> Self {
75        let mut signal1 = SinSignal::new(0.2, 3.0, 18.0);
76        let mut signal2 = SinSignal::new(0.1, 2.0, 10.0);
77        let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
78        let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
79        Self {
80            signal1,
81            data1,
82            signal2,
83            data2,
84            window: [0.0, 20.0],
85        }
86    }
87
88    fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
89        let tick_rate = Duration::from_millis(250);
90        let mut last_tick = Instant::now();
91        loop {
92            terminal.draw(|frame| self.draw(frame))?;
93
94            let timeout = tick_rate.saturating_sub(last_tick.elapsed());
95            if event::poll(timeout)? {
96                if let Event::Key(key) = event::read()? {
97                    if key.code == KeyCode::Char('q') {
98                        return Ok(());
99                    }
100                }
101            }
102            if last_tick.elapsed() >= tick_rate {
103                self.on_tick();
104                last_tick = Instant::now();
105            }
106        }
107    }
108
109    fn on_tick(&mut self) {
110        self.data1.drain(0..5);
111        self.data1.extend(self.signal1.by_ref().take(5));
112
113        self.data2.drain(0..10);
114        self.data2.extend(self.signal2.by_ref().take(10));
115
116        self.window[0] += 1.0;
117        self.window[1] += 1.0;
118    }
119
120    fn draw(&self, frame: &mut Frame) {
121        let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(frame.area());
122        let [animated_chart, bar_chart] =
123            Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top);
124        let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom);
125
126        self.render_animated_chart(frame, animated_chart);
127        render_barchart(frame, bar_chart);
128        render_line_chart(frame, line_chart);
129        render_scatter(frame, scatter);
130    }
131
132    fn render_animated_chart(&self, frame: &mut Frame, area: Rect) {
133        let x_labels = vec![
134            Span::styled(
135                format!("{}", self.window[0]),
136                Style::default().add_modifier(Modifier::BOLD),
137            ),
138            Span::raw(format!("{}", (self.window[0] + self.window[1]) / 2.0)),
139            Span::styled(
140                format!("{}", self.window[1]),
141                Style::default().add_modifier(Modifier::BOLD),
142            ),
143        ];
144        let datasets = vec![
145            Dataset::default()
146                .name("data2")
147                .marker(symbols::Marker::Dot)
148                .style(Style::default().fg(Color::Cyan))
149                .data(&self.data1),
150            Dataset::default()
151                .name("data3")
152                .marker(symbols::Marker::Braille)
153                .style(Style::default().fg(Color::Yellow))
154                .data(&self.data2),
155        ];
156
157        let chart = Chart::new(datasets)
158            .block(Block::bordered())
159            .x_axis(
160                Axis::default()
161                    .title("X Axis")
162                    .style(Style::default().fg(Color::Gray))
163                    .labels(x_labels)
164                    .bounds(self.window),
165            )
166            .y_axis(
167                Axis::default()
168                    .title("Y Axis")
169                    .style(Style::default().fg(Color::Gray))
170                    .labels(["-20".bold(), "0".into(), "20".bold()])
171                    .bounds([-20.0, 20.0]),
172            );
173
174        frame.render_widget(chart, area);
175    }
176}
177
178fn render_barchart(frame: &mut Frame, bar_chart: Rect) {
179    let dataset = Dataset::default()
180        .marker(symbols::Marker::HalfBlock)
181        .style(Style::new().fg(Color::Blue))
182        .graph_type(GraphType::Bar)
183        // a bell curve
184        .data(&[
185            (0., 0.4),
186            (10., 2.9),
187            (20., 13.5),
188            (30., 41.1),
189            (40., 80.1),
190            (50., 100.0),
191            (60., 80.1),
192            (70., 41.1),
193            (80., 13.5),
194            (90., 2.9),
195            (100., 0.4),
196        ]);
197
198    let chart = Chart::new(vec![dataset])
199        .block(Block::bordered().title_top(Line::from("Bar chart").cyan().bold().centered()))
200        .x_axis(
201            Axis::default()
202                .style(Style::default().gray())
203                .bounds([0.0, 100.0])
204                .labels(["0".bold(), "50".into(), "100.0".bold()]),
205        )
206        .y_axis(
207            Axis::default()
208                .style(Style::default().gray())
209                .bounds([0.0, 100.0])
210                .labels(["0".bold(), "50".into(), "100.0".bold()]),
211        )
212        .hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
213
214    frame.render_widget(chart, bar_chart);
215}
216
217fn render_line_chart(frame: &mut Frame, area: Rect) {
218    let datasets = vec![Dataset::default()
219        .name("Line from only 2 points".italic())
220        .marker(symbols::Marker::Braille)
221        .style(Style::default().fg(Color::Yellow))
222        .graph_type(GraphType::Line)
223        .data(&[(1., 1.), (4., 4.)])];
224
225    let chart = Chart::new(datasets)
226        .block(Block::bordered().title(Line::from("Line chart").cyan().bold().centered()))
227        .x_axis(
228            Axis::default()
229                .title("X Axis")
230                .style(Style::default().gray())
231                .bounds([0.0, 5.0])
232                .labels(["0".bold(), "2.5".into(), "5.0".bold()]),
233        )
234        .y_axis(
235            Axis::default()
236                .title("Y Axis")
237                .style(Style::default().gray())
238                .bounds([0.0, 5.0])
239                .labels(["0".bold(), "2.5".into(), "5.0".bold()]),
240        )
241        .legend_position(Some(LegendPosition::TopLeft))
242        .hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
243
244    frame.render_widget(chart, area);
245}
246
247fn render_scatter(frame: &mut Frame, area: Rect) {
248    let datasets = vec![
249        Dataset::default()
250            .name("Heavy")
251            .marker(Marker::Dot)
252            .graph_type(GraphType::Scatter)
253            .style(Style::new().yellow())
254            .data(&HEAVY_PAYLOAD_DATA),
255        Dataset::default()
256            .name("Medium".underlined())
257            .marker(Marker::Braille)
258            .graph_type(GraphType::Scatter)
259            .style(Style::new().magenta())
260            .data(&MEDIUM_PAYLOAD_DATA),
261        Dataset::default()
262            .name("Small")
263            .marker(Marker::Dot)
264            .graph_type(GraphType::Scatter)
265            .style(Style::new().cyan())
266            .data(&SMALL_PAYLOAD_DATA),
267    ];
268
269    let chart = Chart::new(datasets)
270        .block(Block::bordered().title(Line::from("Scatter chart").cyan().bold().centered()))
271        .x_axis(
272            Axis::default()
273                .title("Year")
274                .bounds([1960., 2020.])
275                .style(Style::default().fg(Color::Gray))
276                .labels(["1960", "1990", "2020"]),
277        )
278        .y_axis(
279            Axis::default()
280                .title("Cost")
281                .bounds([0., 75000.])
282                .style(Style::default().fg(Color::Gray))
283                .labels(["0", "37 500", "75 000"]),
284        )
285        .hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
286
287    frame.render_widget(chart, area);
288}
289
290// Data from https://ourworldindata.org/space-exploration-satellites
291const HEAVY_PAYLOAD_DATA: [(f64, f64); 9] = [
292    (1965., 8200.),
293    (1967., 5400.),
294    (1981., 65400.),
295    (1989., 30800.),
296    (1997., 10200.),
297    (2004., 11600.),
298    (2014., 4500.),
299    (2016., 7900.),
300    (2018., 1500.),
301];
302
303const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
304    (1963., 29500.),
305    (1964., 30600.),
306    (1965., 177_900.),
307    (1965., 21000.),
308    (1966., 17900.),
309    (1966., 8400.),
310    (1975., 17500.),
311    (1982., 8300.),
312    (1985., 5100.),
313    (1988., 18300.),
314    (1990., 38800.),
315    (1990., 9900.),
316    (1991., 18700.),
317    (1992., 9100.),
318    (1994., 10500.),
319    (1994., 8500.),
320    (1994., 8700.),
321    (1997., 6200.),
322    (1999., 18000.),
323    (1999., 7600.),
324    (1999., 8900.),
325    (1999., 9600.),
326    (2000., 16000.),
327    (2001., 10000.),
328    (2002., 10400.),
329    (2002., 8100.),
330    (2010., 2600.),
331    (2013., 13600.),
332    (2017., 8000.),
333];
334
335const SMALL_PAYLOAD_DATA: [(f64, f64); 23] = [
336    (1961., 118_500.),
337    (1962., 14900.),
338    (1975., 21400.),
339    (1980., 32800.),
340    (1988., 31100.),
341    (1990., 41100.),
342    (1993., 23600.),
343    (1994., 20600.),
344    (1994., 34600.),
345    (1996., 50600.),
346    (1997., 19200.),
347    (1997., 45800.),
348    (1998., 19100.),
349    (2000., 73100.),
350    (2003., 11200.),
351    (2008., 12600.),
352    (2010., 30500.),
353    (2012., 20000.),
354    (2013., 10600.),
355    (2013., 34500.),
356    (2015., 10600.),
357    (2018., 23100.),
358    (2019., 17300.),
359];