use ratatui::{
prelude::*,
widgets::{Axis, Chart, Dataset, GraphType},
Frame,
};
use super::Theme;
use crate::network::SpeedSample;
pub struct GraphWidget<'a> {
samples: &'a [SpeedSample],
max_scale: f64,
theme: Option<&'a Theme>,
}
impl<'a> GraphWidget<'a> {
pub fn new(samples: &'a [SpeedSample], max_scale: f64) -> Self {
Self {
samples,
max_scale,
theme: None,
}
}
pub fn with_theme(mut self, theme: &'a Theme) -> Self {
self.theme = Some(theme);
self
}
pub fn render(self, frame: &mut Frame, area: Rect) {
let default_theme = Theme::default();
let theme = self.theme.unwrap_or(&default_theme);
if self.samples.is_empty() {
let placeholder = ratatui::widgets::Paragraph::new("No data")
.style(Style::default().fg(theme.muted))
.alignment(Alignment::Center);
frame.render_widget(placeholder, area);
return;
}
let data: Vec<(f64, f64)> = self
.samples
.iter()
.map(|s| (s.timestamp_ms as f64 / 1000.0, s.speed_mbps))
.collect();
let max_time = data.iter().map(|(t, _)| *t).fold(0.0_f64, f64::max);
let min_time = data.iter().map(|(t, _)| *t).fold(f64::INFINITY, f64::min);
let time_range = if max_time - min_time < 1.0 {
(0.0, 15.0) } else {
(min_time, max_time)
};
let datasets = vec![Dataset::default()
.name("Speed")
.marker(symbols::Marker::Braille)
.graph_type(GraphType::Line)
.style(Style::default().fg(theme.primary))
.data(&data)];
let x_labels = vec![
Span::styled(
format!("{:.0}s", time_range.0),
Style::default().fg(theme.muted),
),
Span::styled(
format!("{:.0}s", (time_range.0 + time_range.1) / 2.0),
Style::default().fg(theme.muted),
),
Span::styled(
format!("{:.0}s", time_range.1),
Style::default().fg(theme.muted),
),
];
let y_labels = vec![
Span::styled("0", Style::default().fg(theme.muted)),
Span::styled(
format!("{:.0}", self.max_scale / 2.0),
Style::default().fg(theme.muted),
),
Span::styled(
format!("{:.0}", self.max_scale),
Style::default().fg(theme.muted),
),
];
let chart = Chart::new(datasets)
.style(Style::default().bg(theme.background))
.x_axis(
Axis::default()
.style(Style::default().fg(theme.border))
.bounds([time_range.0, time_range.1])
.labels(x_labels),
)
.y_axis(
Axis::default()
.style(Style::default().fg(theme.border))
.bounds([0.0, self.max_scale])
.labels(y_labels),
);
frame.render_widget(chart, area);
}
}
#[allow(dead_code)]
pub struct SampleBuffer {
samples: Vec<SpeedSample>,
max_samples: usize,
}
#[allow(dead_code)]
impl SampleBuffer {
pub fn new(max_samples: usize) -> Self {
Self {
samples: Vec::with_capacity(max_samples),
max_samples,
}
}
pub fn push(&mut self, sample: SpeedSample) {
if self.samples.len() >= self.max_samples {
self.samples.remove(0);
}
self.samples.push(sample);
}
pub fn clear(&mut self) {
self.samples.clear();
}
pub fn samples(&self) -> &[SpeedSample] {
&self.samples
}
pub fn last(&self) -> Option<&SpeedSample> {
self.samples.last()
}
pub fn average(&self) -> f64 {
if self.samples.is_empty() {
return 0.0;
}
self.samples.iter().map(|s| s.speed_mbps).sum::<f64>() / self.samples.len() as f64
}
pub fn peak(&self) -> f64 {
self.samples
.iter()
.map(|s| s.speed_mbps)
.fold(0.0_f64, f64::max)
}
}
impl Default for SampleBuffer {
fn default() -> Self {
Self::new(100)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sample_buffer() {
let mut buffer = SampleBuffer::new(5);
for i in 0..10 {
buffer.push(SpeedSample {
timestamp_ms: i * 1000,
speed_mbps: i as f64 * 10.0,
});
}
assert_eq!(buffer.samples().len(), 5);
assert_eq!(buffer.samples()[0].timestamp_ms, 5000);
}
#[test]
fn test_sample_buffer_stats() {
let mut buffer = SampleBuffer::new(10);
buffer.push(SpeedSample {
timestamp_ms: 0,
speed_mbps: 100.0,
});
buffer.push(SpeedSample {
timestamp_ms: 1000,
speed_mbps: 200.0,
});
buffer.push(SpeedSample {
timestamp_ms: 2000,
speed_mbps: 150.0,
});
assert_eq!(buffer.average(), 150.0);
assert_eq!(buffer.peak(), 200.0);
}
}