adguardian/widgets/
chart.rs

1
2use tui::{
3    style::{Color, Modifier, Style},
4    widgets::{Axis, Block, Borders, Dataset, Chart},
5    text::{Span},
6    symbols,
7};
8
9use crate::fetch::fetch_stats::{StatsResponse};
10
11
12pub fn make_history_chart(stats: &StatsResponse) -> Chart<'_> {
13    // Convert datasets into vector that can be consumed by chart
14    let datasets = make_history_datasets(stats);
15    // Find uppermost x and y-axis bounds for chart
16    let (x_bound, y_bound) = find_bounds(stats);
17    // Generate incremental labels from data's values, to render on axis
18    let x_labels = generate_x_labels(stats.dns_queries.len() as i32, 5);
19    let y_labels = generate_y_labels(y_bound as i32, 5);
20    // Create chart
21    let chart = Chart::new(datasets)
22        .block(
23            Block::default()
24            .title(Span::styled(
25                "History",
26                Style::default().add_modifier(Modifier::BOLD),
27            ))
28            .borders(Borders::ALL)
29        )
30        .x_axis(
31            Axis::default()
32            .title("Time (Days ago)")
33            .bounds([0.0, x_bound])
34            .labels(x_labels),
35        )
36        .y_axis(Axis::default().title("Query Count").labels(y_labels).bounds([0.0, y_bound]));
37
38    chart
39}
40
41// Returns a dataset that's consumable by the chart widget
42fn make_history_datasets(stats: &StatsResponse) -> Vec<Dataset<'_>> {
43  let dns_queries_dataset = Dataset::default()
44      .name("DNS Queries")
45      .marker(symbols::Marker::Braille)
46      .style(Style::default().fg(Color::Green))
47      .data(&stats.dns_queries_chart);
48
49  let blocked_filtering_dataset = Dataset::default()
50      .name("Blocked Filtering")
51      .marker(symbols::Marker::Braille)
52      .style(Style::default().fg(Color::Red))
53      .data(&stats.blocked_filtering_chart);
54
55  let datasets = vec![dns_queries_dataset, blocked_filtering_dataset];
56
57  datasets
58}
59
60// Determine the uppermost bounds for the x and y axis
61fn find_bounds(stats: &StatsResponse) -> (f64, f64) {
62    let mut max_length = 0;
63    let mut max_value = f64::MIN;
64
65    for dataset in &[&stats.dns_queries_chart, &stats.blocked_filtering_chart] {
66        let length = dataset.len();
67        if length > max_length {
68            max_length = length;
69        }
70
71        let max_in_dataset = dataset
72            .iter()
73            .map(|&(_, y)| y)
74            .fold(f64::MIN, f64::max);
75        if max_in_dataset > max_value {
76            max_value = max_in_dataset;
77        }
78    }
79    (max_length as f64, max_value)
80}
81
82// Generate periodic labels to render on the y-axis (query count)
83fn generate_y_labels(max: i32, count: usize) -> Vec<Span<'static>> {
84  let step = max / (count - 1) as i32;
85  
86  (0..count)
87      .map(|x| Span::raw(format!("{}", x * step as usize)))
88      .collect::<Vec<Span<'static>>>()
89}
90
91// Generate periodic labels to render on the x-axis (days ago)
92fn generate_x_labels(max_days: i32, num_labels: i32) -> Vec<Span<'static>> {
93    let step = max_days / (num_labels - 1);
94    (0..num_labels)
95        .map(|i| {
96            let day = (max_days - i * step).to_string();
97            if i == num_labels - 1 {
98                Span::styled("Today", Style::default().add_modifier(Modifier::BOLD))
99            } else {
100                Span::raw(day)
101            }
102        })
103        .collect()
104}
105
106// Formats vector data into a format that can be consumed by the chart widget
107fn convert_to_chart_data(data: Vec<f64>) -> Vec<(f64, f64)> {
108    data.iter().enumerate().map(|(i, &v)| (i as f64, v)).collect()
109}
110
111// Interpolates data, adding n number of points, to make the chart look smoother
112fn interpolate(input: &Vec<f64>, points_between: usize) -> Vec<f64> {
113    let mut output = Vec::new();
114
115    for window in input.windows(2) {
116        let start = window[0];
117        let end = window[1];
118        let step = (end - start) / (points_between as f64 + 1.0);
119
120        output.push(start);
121        for i in 1..=points_between {
122            output.push(start + step * i as f64);
123        }
124    }
125
126    output.push(*input.last().unwrap());
127    output
128}
129
130// Adds data formatted for the time-series chart to the stats object
131pub fn prepare_chart_data(stats: &mut StatsResponse) {
132    let dns_queries = stats.dns_queries.iter().map(|&v| v as f64).collect::<Vec<_>>();
133    let interpolated_dns_queries = interpolate(&dns_queries, 3);
134    stats.dns_queries_chart = convert_to_chart_data(interpolated_dns_queries);
135    
136    let blocked_filtering: Vec<f64> = stats.blocked_filtering.iter()
137        .zip(&stats.replaced_safebrowsing)
138        .zip(&stats.replaced_parental)
139        .map(|((&b, &s), &p)| (b + s + p) as f64)
140        .collect();
141    
142    let interpolated_blocked_filtering = interpolate(&blocked_filtering, 3);
143    let blocked_filtering_chart: Vec<(f64, f64)> = convert_to_chart_data(interpolated_blocked_filtering);
144    
145    stats.blocked_filtering_chart = blocked_filtering_chart;
146}