josie / alder-tools

//! 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 is sized for the configured
/// graph window at the current poll period (min 512); plots clip to the
/// visible width anyway.
pub struct Ring {
    data: Vec<f64>,
    start: usize,
    len: usize,
}

impl Ring {
    pub fn new(capacity: usize) -> Ring {
        Ring {
            data: vec![0.0; capacity],
            start: 0,
            len: 0,
        }
    }

    pub fn push(&mut self, v: f64) {
        let cap = self.data.len();
        let end = (self.start + self.len) % cap;
        self.data[end] = v;
        if self.len < cap {
            self.len += 1;
        } else {
            self.start = (self.start + 1) % cap;
        }
    }

    pub fn samples_window(&self, window_ticks: usize) -> impl Iterator<Item = f64> + '_ {
        let skip = self.len.saturating_sub(window_ticks);
        (skip..self.len).map(move |i| self.data[(self.start + i) % self.data.len()])
    }

    pub fn max_window(&self, window_ticks: usize) -> f64 {
        self.samples_window(window_ticks).fold(f64::MIN, f64::max)
    }
}

fn levels(samples: &[f64], max: f64, rows: usize) -> Vec<usize> {
    // 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, window_ticks: usize, max: f64, color: Color, marker_row: Option<u16>) {
    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<f64> = ring.samples_window(window_ticks).collect();
    if samples.is_empty() {
        return;
    }
    // Bucket the window across the plot width (mean per column): the whole
    // graph_secs window maps to `cols` columns, and per-tick jitter within
    // a bucket averages out instead of drawing corner squiggles.
    let per_col = samples.len() / cols;
    let shown: Vec<f64> = if per_col >= 2 {
        samples
            .chunks(samples.len().div_ceil(cols))
            .map(|b| b.iter().sum::<f64>() / b.len() as f64)
            .collect()
    } else {
        samples
    };
    let skip = shown.len().saturating_sub(cols);
    let lvls = levels(&shown[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<Line> = buf
        .into_iter()
        .map(|row| {
            Line::from(
                row.into_iter()
                    .map(|(ch, sty)| Span::styled(ch.to_string(), sty))
                    .collect::<Vec<_>>(),
            )
        })
        .collect();
    f.render_widget(ratatui::text::Text::from(lines), area);
}