netviz 0.1.0

A simple network traffic monitor and visualizer
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::Color;
use ratatui::symbols::Marker;
use ratatui::text::Span;
use ratatui::widgets::{
    Axis, Block, BorderType, Chart, Dataset, GraphType, Padding, Paragraph, Widget,
};

use crate::define_directional_history;
use crate::history::{DEFAULT_HISTORY_SIZE, Direction, History};
use crate::utils::format_bytes;

define_directional_history!(
    TrafficHistory,
    (f64, f64),
    DEFAULT_HISTORY_SIZE,
    {
        min_value: f64 = f64::MAX,
        max_value: f64 = f64::MIN,
        total: f64 = 0.0,
        samples: u64 = 0,
    },
    {
        pub fn add_point(&mut self, value: (f64, f64)) {
            self.history.add(value);

            let (_, y) = value;

            if y < self.min_value {
                self.min_value = y;
            }

            if y > self.max_value {
                self.max_value = y;
            }

            self.total += y;
            self.samples += 1;
        }

        fn average_value(&self) -> f64 {
            self.total / self.samples as f64
        }
    }
);

impl Widget for &TrafficHistory {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let data: Vec<(f64, f64)> = self
            .history
            .values()
            .iter()
            .map(|(x, y)| (*x, *y))
            .collect();

        let x_bounds = [data[0].0, data[data.len() - 1].0];
        let y_bounds = [0.0, data.iter().map(|(_, y)| *y).fold(f64::MIN, f64::max)];

        let (title, color) = match self.direction {
            Direction::Download => ("Download ↓", Color::Cyan),
            Direction::Upload => ("Upload ↑", Color::Magenta),
        };

        let block = Block::bordered()
            .title(Span::styled(title, color))
            .border_type(BorderType::Rounded)
            .padding(Padding::uniform(1));

        let block_area = block.inner(area);

        block.render(area, buf);

        let block_chunks =
            Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(block_area);

        let values = [
            format!(
                "Current: {:>11}",
                format_bytes(data[data.len() - 1].1, true)
            ),
            format!("Average: {:>11}", format_bytes(self.average_value(), true)),
            format!("Min: {:>11}", format_bytes(self.min_value, true)),
            format!("Max: {:>11}", format_bytes(self.max_value, true)),
            format!("Total: {:>11}", format_bytes(self.total, false)),
        ];

        let value_chunks =
            Layout::horizontal([Constraint::Percentage(20); 5]).split(block_chunks[0]);

        for (i, value) in values.iter().enumerate() {
            let paragraph = Paragraph::new(Span::from(value)).centered();

            paragraph.render(value_chunks[i], buf);
        }

        let datasets = vec![
            Dataset::default()
                .data(&data)
                .marker(Marker::Dot)
                .graph_type(GraphType::Line)
                .style(color),
        ];

        let chart = Chart::new(datasets)
            .x_axis(
                Axis::default()
                    .labels(vec![
                        Span::from(format!("{}s", x_bounds[0])),
                        Span::from(format!("{:.1}s", (x_bounds[0] + x_bounds[1]) / 2.0)),
                        Span::from(format!("{}s", x_bounds[1])),
                    ])
                    .bounds(x_bounds),
            )
            .y_axis(
                Axis::default()
                    .labels(vec![
                        Span::from(format!("{:>11}", format_bytes(y_bounds[0], true))),
                        Span::from(format!(
                            "{:>11}",
                            format_bytes((y_bounds[0] + y_bounds[1]) / 2.0, true)
                        )),
                        Span::from(format!("{:>11}", format_bytes(y_bounds[1], true))),
                    ])
                    .bounds(y_bounds),
            );

        chart.render(block_chunks[1], buf);
    }
}