trace_game/windows/
statistics_window.rs1use 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}