trace_game/windows/
statistics_window.rs

1use crate::get_track_record;
2use crate::windows::*;
3use crate::State;
4use crate::Window;
5use crate::WindowCommand;
6use crossterm::event::KeyCode;
7use std::collections::HashMap;
8use std::rc::Rc;
9use tui::backend::Backend;
10use tui::layout::Alignment;
11use tui::layout::Constraint;
12use tui::layout::Direction;
13use tui::layout::Layout;
14use tui::symbols;
15use tui::widgets::Axis;
16use tui::widgets::BarChart;
17use tui::widgets::Block;
18use tui::widgets::Chart;
19use tui::widgets::Dataset;
20use tui::widgets::GraphType;
21use tui::Frame;
22
23fn construct_line_chart<B: Backend>(
24    f: &mut Frame<B>, point_series: &[(f64, f64)], wpm_series: &[(f64, f64)],
25    accuracy_series: &[(f64, f64)], raw_data_length: usize,
26) {
27    let datasets = vec![
28        Dataset::default()
29            .name("Points")
30            .marker(symbols::Marker::Braille)
31            .graph_type(GraphType::Line)
32            .style(Style::default().fg(Color::LightCyan))
33            .data(point_series),
34        Dataset::default()
35            .name("WPM")
36            .marker(symbols::Marker::Braille)
37            .graph_type(GraphType::Line)
38            .style(Style::default().fg(Color::LightYellow))
39            .data(wpm_series),
40        Dataset::default()
41            .name("Accuracy")
42            .marker(symbols::Marker::Braille)
43            .graph_type(GraphType::Line)
44            .style(Style::default().fg(Color::LightGreen))
45            .data(&accuracy_series),
46    ];
47
48    let filter = |n: usize| {
49        let potential_step = (0.1 * raw_data_length as f64) as usize;
50        let step = if potential_step == 0 {
51            1
52        } else {
53            potential_step
54        };
55        if n % step == 0 {
56            Some(Span::from(n.to_string()))
57        } else {
58            None
59        }
60    };
61
62    let max_bound = std::cmp::max(1, raw_data_length - 1);
63    let labels = if raw_data_length == 1 {
64        vec![Span::from("0"), Span::from("1")]
65    } else {
66        (0..raw_data_length).filter_map(filter).collect()
67    };
68    let chart = Chart::new(datasets)
69        .block(
70            Block::default()
71                .title("Statistics")
72                .title_alignment(Alignment::Center),
73        )
74        .x_axis(
75            Axis::default()
76                .title(Span::styled(
77                    "# Run",
78                    Style::default()
79                        .fg(Color::LightRed)
80                        .add_modifier(Modifier::BOLD),
81                ))
82                .bounds([0.0, max_bound as f64])
83                .labels(labels),
84        )
85        .y_axis(
86            Axis::default()
87                .title(Span::styled(
88                    "Press [TAB] to change",
89                    Style::default()
90                        .fg(Color::LightRed)
91                        .add_modifier(Modifier::BOLD),
92                ))
93                .bounds([0.0, 150.0])
94                .labels(
95                    (0..151)
96                        .filter_map(|n: u8| {
97                            if n % 10 == 0 {
98                                Some(Span::from(n.to_string()))
99                            } else {
100                                None
101                            }
102                        })
103                        .collect(),
104                ),
105        );
106    f.render_widget(chart, f.size());
107}
108
109fn construct_bar_charts<B: Backend>(
110    f: &mut Frame<B>, point_series: &[(f64, f64)], wpm_series: &[(f64, f64)],
111    accuracy_series: &[(f64, f64)],
112) {
113    let layout = Layout::default()
114        .direction(Direction::Vertical)
115        .constraints(
116            [
117                Constraint::Percentage(30),
118                Constraint::Percentage(30),
119                Constraint::Percentage(30),
120            ]
121            .as_ref(),
122        )
123        .split(f.size());
124
125    let series = [point_series, wpm_series, accuracy_series];
126    let titles = ["Points", "WPM", "Accuracy"];
127    let colors = [Color::LightCyan, Color::LightYellow, Color::LightGreen];
128    let bar_styles = [
129        Style::default().fg(Color::LightCyan),
130        Style::default().fg(Color::LightYellow),
131        Style::default().fg(Color::LightGreen),
132    ];
133    let value_styles = [
134        Style::default().add_modifier(Modifier::BOLD),
135        Style::default().add_modifier(Modifier::BOLD),
136        Style::default().add_modifier(Modifier::BOLD),
137    ];
138    for i in 0..3 {
139        let transformed_series: Vec<(String, u64)> = series[i]
140            .into_iter()
141            .map(|v| (v.0.to_string(), v.1 as u64))
142            .rev()
143            .collect();
144        let result: Vec<(&str, u64)> = transformed_series
145            .iter()
146            .map(|v| (v.0.as_str(), v.1))
147            .collect();
148        let chart = BarChart::default()
149            .block(
150                Block::default()
151                    .title(Span::styled(
152                        titles[i],
153                        Style::default().fg(colors[i]).add_modifier(Modifier::BOLD),
154                    ))
155                    .borders(Borders::BOTTOM),
156            )
157            .bar_width(3)
158            .bar_gap(1)
159            .bar_style(bar_styles[i])
160            .value_style(value_styles[i])
161            .label_style(Style::default().fg(Color::White))
162            .data(&result);
163        f.render_widget(chart, layout[i]);
164    }
165}
166
167fn statistics_window<B: 'static + Backend>(state: Rc<State>) -> Box<dyn Fn(&mut Frame<B>)> {
168    Box::new(move |f: &mut Frame<B>| {
169        let raw_data = get_track_record();
170
171        let point_series: Vec<(f64, f64)> = (0..raw_data.len())
172            .map(|i| (i as f64, raw_data[i].total_points))
173            .collect();
174        let wpm_series: Vec<(f64, f64)> = (0..raw_data.len())
175            .map(|i| (i as f64, raw_data[i].wpm))
176            .collect();
177        let accuracy_series: Vec<(f64, f64)> = (0..raw_data.len())
178            .map(|i| (i as f64, raw_data[i].accuracy * 100.0))
179            .collect();
180
181        if state.show_bar_charts {
182            construct_bar_charts(f, &point_series, &wpm_series, &accuracy_series);
183        } else {
184            construct_line_chart(
185                f,
186                &point_series,
187                &wpm_series,
188                &accuracy_series,
189                raw_data.len(),
190            );
191        }
192    })
193}
194
195pub fn create_statistics_window<B: 'static + Backend>(_: &mut State) -> Option<Window<B>> {
196    Some(Window {
197        ui: statistics_window,
198        commands: HashMap::from([
199            (
200                KeyCode::Esc,
201                WindowCommand {
202                    activator_key: KeyCode::Esc,
203                    action: Box::new(create_main_menu_window),
204                },
205            ),
206            (
207                KeyCode::Tab,
208                WindowCommand {
209                    activator_key: KeyCode::Tab,
210                    action: Box::new(|s: &mut State| {
211                        s.show_bar_charts = !s.show_bar_charts;
212                        create_statistics_window(s)
213                    }),
214                },
215            ),
216        ]),
217    })
218}