//! nvtop-style line plot, re-implemented for ratatui (pattern studied in //! references/nvtop/src/plot.c — GPL, code never copied). A `Ring` is a //! fixed-capacity sample buffer; the rightmost drawn column is the newest. //! Cell glyphs: level `─`, rises/falls get corner glyphs, multi-row jumps //! get `│` between them. use ratatui::{ Frame, layout::Rect, style::{Color, Style}, text::{Line, Span}, }; /// Ring of samples, oldest first. Capacity 512 ticks ≈ 2 min at the 250 ms /// cadence; plots clip to the visible width anyway. pub struct Ring { data: Vec, start: usize, len: usize, } const RING_CAP: usize = 512; impl Ring { pub fn new() -> Ring { Ring { data: vec![0.0; RING_CAP], start: 0, len: 0, } } pub fn push(&mut self, v: f64) { let end = (self.start + self.len) % RING_CAP; self.data[end] = v; if self.len < RING_CAP { self.len += 1; } else { self.start = (self.start + 1) % RING_CAP; } } pub fn samples(&self) -> impl Iterator + '_ { (0..self.len).map(move |i| self.data[(self.start + i) % RING_CAP]) } } fn levels(samples: &[f64], max: f64, rows: usize) -> Vec { // Row 0 = top of the plot = max value. samples .iter() .map(|&v| (((1.0 - v / max).clamp(0.0, 1.0)) * (rows - 1) as f64).round() as usize) .collect() } pub fn render(f: &mut Frame, area: Rect, ring: &Ring, max: f64, color: Color, marker_row: Option) { if area.width == 0 || area.height == 0 || max <= 0.0 { return; } let rows = area.height as usize; let cols = area.width as usize; let samples: Vec = ring.samples().collect(); let skip = samples.len().saturating_sub(cols); let lvls = levels(&samples[skip..], max, rows); let mut buf = vec![vec![(' ', Style::default()); cols]; rows]; let mut set = |r: usize, c: usize, ch: char, st: Style| { if r < rows && c < cols && buf[r][c].0 == ' ' { buf[r][c] = (ch, st); } }; let st = Style::default().fg(color); for (c, &l) in lvls.iter().enumerate() { let prev_l = if c == 0 { l } else { lvls[c - 1] }; if prev_l == l { set(l, c, '─', st); } else { // Value rise = row number falls (top row is max), so // prev_l > l means the line steps up on screen. let (upper, lower) = if prev_l < l { (l, prev_l) } else { (prev_l, l) }; set(upper, c, '╮', st); set(lower, c, '╰', st); for r in upper + 1..lower { set(r, c, '│', st); } } } if let Some(mr) = marker_row { let mr = (mr as usize).min(rows - 1); for cell in buf[mr].iter_mut().take(cols) { if cell.0 == ' ' { *cell = ('┄', Style::default().fg(Color::Red)); } } } let lines: Vec = buf .into_iter() .map(|row| { Line::from( row.into_iter() .map(|(ch, sty)| Span::styled(ch.to_string(), sty)) .collect::>(), ) }) .collect(); f.render_widget(ratatui::text::Text::from(lines), area); }