diff --git a/src/plot.rs b/src/plot.rs
index b750cb4..01b7f67 100644
--- a/src/plot.rs
+++ b/src/plot.rs
@@ -1,8 +1,9 @@
//! 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.
+//! Style: continuous staircase (level `─`, vertical `│` joins — no corner
+//! glyphs; they masked peaks), y-axis labels on the left, time axis on the
+//! bottom (oldest left → 0s right), like nvtop's plot panes.
use ratatui::{
Frame,
@@ -58,25 +59,35 @@ fn levels(samples: &[f64], max: f64, rows: usize) -> Vec<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 {
+/// nvtop draw: y-axis labels overlay the left edge (e.g. 100/75/50/25/0),
+/// the staircase occupies the full area, time labels sit on the bottom row
+/// (rightmost = "0s", leftward = elapsed seconds of the window).
+pub fn render(
+ f: &mut Frame,
+ area: Rect,
+ ring: &Ring,
+ window_ticks: usize,
+ poll_ms: u64,
+ max: f64,
+ color: Color,
+ marker_row: Option<u16>,
+) {
+ if area.width < 8 || area.height < 3 || max <= 0.0 {
return;
}
let rows = area.height as usize;
let cols = area.width as usize;
+
+ // One sample per column, newest at the right; when the window holds
+ // more samples than columns, stride-skip (take every Nth) so the whole
+ // window is visible. Striding preserves steps better than averaging.
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()
+ let stride = samples.len().div_ceil(cols);
+ let shown: Vec<f64> = if stride > 1 {
+ samples.iter().rev().step_by(stride).rev().cloned().collect()
} else {
samples
};
@@ -92,16 +103,13 @@ pub fn render(f: &mut Frame, area: Rect, ring: &Ring, window_ticks: usize, max:
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 {
- // Flat-step style (no rounded corners — they masked peaks):
- // both stepped rows get level dashes, verticals between.
- let (top, bot) = if prev_l < l { (prev_l, l) } else { (l, prev_l) };
- set(top, c, '─', st);
- set(bot, c, '─', st);
- for r in top + 1..bot {
+ set(l, c, '─', st);
+ if c > 0 {
+ // Vertical join between the previous level and this one so a
+ // step is continuous (staircase, nvtop-style).
+ let prev = lvls[c - 1];
+ let (top, bot) = if prev < l { (prev, l) } else { (l, prev) };
+ for r in top..=bot.min(top + rows) {
set(r, c, '│', st);
}
}
@@ -116,6 +124,38 @@ pub fn render(f: &mut Frame, area: Rect, ring: &Ring, window_ticks: usize, max:
}
}
+ // ---- axis labels (overlay, drawn last so they win blanks) ----
+ let axis_st = Style::default().fg(Color::DarkGray);
+ // y-axis: max and half-max at top/left and mid-left.
+ let y_labels = [
+ (0usize, format!("{:.0}", max)),
+ (rows / 2, format!("{:.0}", max / 2.0)),
+ ];
+ for (r, label) in y_labels {
+ if r < rows {
+ for (i, ch) in label.chars().take(4).enumerate() {
+ if buf[r][i].0 == ' ' {
+ buf[r][i] = (ch, axis_st);
+ }
+ }
+ }
+ }
+ // x-axis on the bottom row: window length in seconds at the left,
+ // "0s" at the right (nvtop: seconds count down to now).
+ let total_s = (window_ticks as u64 * poll_ms / 1000).max(1);
+ let t_label = format!("{}s", total_s);
+ for (i, ch) in t_label.chars().enumerate() {
+ if buf[rows - 1][i].0 == ' ' {
+ buf[rows - 1][i] = (ch, axis_st);
+ }
+ }
+ for (i, ch) in "0s".chars().enumerate() {
+ let c = cols - 2 + i;
+ if c < cols && buf[rows - 1][c].0 == ' ' {
+ buf[rows - 1][c] = (ch, axis_st);
+ }
+ }
+
let lines: Vec<Line> = buf
.into_iter()
.map(|row| {
@@ -151,4 +191,4 @@ mod tests {
assert_eq!(got2, vec![5.0, 6.0]);
assert_eq!(r.max_window(2), 6.0);
}
-}
+}
\ No newline at end of file
diff --git a/src/ui.rs b/src/ui.rs
index 88e8d52..71e68df 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -500,7 +500,7 @@ pub fn draw(f: &mut Frame, app: &App) {
Span::styled(text, Style::default().add_modifier(Modifier::BOLD)),
]);
f.render_widget(ratatui::text::Text::from(caption), Rect { height: 1, ..rect });
- plot::render(f, Rect { y: rect.y + 1, height: graph_rows, ..rect }, ring, ticks, max, color, marker);
+ plot::render(f, Rect { y: rect.y + 1, height: graph_rows, ..rect }, ring, ticks, app.cfg.poll_ms, max, color, marker);
x += thirds;
}
} else {
Plot: stride sampling (whole window visible, staircase preserved), y/x axis labels nvtop-style; fix 2/3 height math collisions
2ce9e553166493cf55b45c7c3402057bc2f0b73b
josie <administrator@josie-c.com> · 2026-08-30T13:38 ·
browse files at this commit
parents:
a0b993f