adguardian/widgets/
chart.rs1use tui::{
2 style::{Color, Modifier, Style},
3 symbols,
4 text::Span,
5 widgets::{Axis, Block, Borders, Chart, Dataset},
6};
7
8use crate::fetch::fetch_stats::StatsResponse;
9
10pub fn make_history_chart(stats: &StatsResponse) -> Chart<'_> {
11 let datasets = make_history_datasets(stats);
13 let (x_bound, y_bound) = find_bounds(stats);
15 let x_labels = generate_x_labels(stats.dns_queries.len() as i32, 5);
17 let y_labels = generate_y_labels(y_bound as i32, 5);
18 let chart = Chart::new(datasets)
20 .block(
21 Block::default()
22 .title(Span::styled(
23 "History",
24 Style::default().add_modifier(Modifier::BOLD),
25 ))
26 .borders(Borders::ALL),
27 )
28 .x_axis(
29 Axis::default()
30 .title("Time (Days ago)")
31 .bounds([0.0, x_bound])
32 .labels(x_labels),
33 )
34 .y_axis(
35 Axis::default()
36 .title("Query Count")
37 .labels(y_labels)
38 .bounds([0.0, y_bound]),
39 );
40
41 chart
42}
43
44fn make_history_datasets(stats: &StatsResponse) -> Vec<Dataset<'_>> {
46 let dns_queries_dataset = Dataset::default()
47 .name("DNS Queries")
48 .marker(symbols::Marker::Braille)
49 .style(Style::default().fg(Color::Green))
50 .data(&stats.dns_queries_chart);
51
52 let blocked_filtering_dataset = Dataset::default()
53 .name("Blocked Filtering")
54 .marker(symbols::Marker::Braille)
55 .style(Style::default().fg(Color::Red))
56 .data(&stats.blocked_filtering_chart);
57
58 let datasets = vec![dns_queries_dataset, blocked_filtering_dataset];
59
60 datasets
61}
62
63fn find_bounds(stats: &StatsResponse) -> (f64, f64) {
65 let mut max_length = 0;
66 let mut max_value = 0.0;
69
70 for dataset in &[&stats.dns_queries_chart, &stats.blocked_filtering_chart] {
71 let length = dataset.len();
72 if length > max_length {
73 max_length = length;
74 }
75
76 let max_in_dataset = dataset.iter().map(|&(_, y)| y).fold(f64::MIN, f64::max);
77 if max_in_dataset > max_value {
78 max_value = max_in_dataset;
79 }
80 }
81 (max_length as f64, max_value)
82}
83
84fn generate_y_labels(max: i32, count: usize) -> Vec<Span<'static>> {
86 let step = max / (count - 1) as i32;
87
88 (0..count)
89 .map(|x| Span::raw(format!("{}", x * step as usize)))
90 .collect::<Vec<Span<'static>>>()
91}
92
93fn generate_x_labels(max_days: i32, num_labels: i32) -> Vec<Span<'static>> {
95 let step = max_days / (num_labels - 1);
96 (0..num_labels)
97 .map(|i| {
98 let day = (max_days - i * step).to_string();
99 if i == num_labels - 1 {
100 Span::styled("Today", Style::default().add_modifier(Modifier::BOLD))
101 } else {
102 Span::raw(day)
103 }
104 })
105 .collect()
106}
107
108fn convert_to_chart_data(data: Vec<f64>) -> Vec<(f64, f64)> {
110 data
111 .iter()
112 .enumerate()
113 .map(|(i, &v)| (i as f64, v))
114 .collect()
115}
116
117fn interpolate(input: &[f64], points_between: usize) -> Vec<f64> {
119 let Some(&last) = input.last() else {
121 return Vec::new();
122 };
123
124 let mut output = Vec::new();
125 for window in input.windows(2) {
126 let start = window[0];
127 let end = window[1];
128 let step = (end - start) / (points_between as f64 + 1.0);
129
130 output.push(start);
131 for i in 1..=points_between {
132 output.push(start + step * i as f64);
133 }
134 }
135
136 output.push(last);
137 output
138}
139
140pub fn prepare_chart_data(stats: &mut StatsResponse) {
142 let dns_queries = stats
143 .dns_queries
144 .iter()
145 .map(|&v| v as f64)
146 .collect::<Vec<_>>();
147 let interpolated_dns_queries = interpolate(&dns_queries, 3);
148 stats.dns_queries_chart = convert_to_chart_data(interpolated_dns_queries);
149
150 let blocked_filtering: Vec<f64> = stats
151 .blocked_filtering
152 .iter()
153 .zip(&stats.replaced_safebrowsing)
154 .zip(&stats.replaced_parental)
155 .map(|((&b, &s), &p)| (b + s + p) as f64)
156 .collect();
157
158 let interpolated_blocked_filtering = interpolate(&blocked_filtering, 3);
159 let blocked_filtering_chart: Vec<(f64, f64)> =
160 convert_to_chart_data(interpolated_blocked_filtering);
161
162 stats.blocked_filtering_chart = blocked_filtering_chart;
163}