diff --git a/src/plot.rs b/src/plot.rs
index 1ce0595..cd85f36 100644
--- a/src/plot.rs
+++ b/src/plot.rs
@@ -59,6 +59,33 @@ fn levels(samples: &[f64], max: f64, rows: usize) -> Vec<usize> {
.collect()
}
+/// Hysteresis: a value hovering on a row boundary makes a plain rounding
+/// map flap between two rows (`─│─│─│` comb). Only move off the previous
+/// row when the continuous level is nearer to the new row by a decisive
+/// margin, so noise rides out and real steps still track.
+fn levels_hysteresis(samples: &[f64], max: f64, rows: usize) -> Vec<usize> {
+ let cont = |v: f64| (1.0 - v / max).clamp(0.0, 1.0) * (rows - 1) as f64;
+ let mut out: Vec<usize> = Vec::with_capacity(samples.len());
+ let mut last: Option<f64> = None; // continuous position of drawn row
+ for &v in samples {
+ let target = cont(v);
+ let row = match last {
+ None => target.round(),
+ Some(prev) => {
+ // Distance from target to the drawn row's continuous pos.
+ if (target - prev).abs() > 0.75 {
+ target.round()
+ } else {
+ prev.round()
+ }
+ }
+ };
+ out.push(row as usize);
+ last = Some(row as f64);
+ }
+ out
+}
+
/// 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).
@@ -103,7 +130,7 @@ pub fn render(
samples
};
let skip = shown.len().saturating_sub(cols);
- let lvls = levels(&shown[skip..], max, rows);
+ let lvls = levels_hysteresis(&shown[skip..], max, rows);
let mut buf = vec![vec![(' ', Style::default()); cols]; rows];
let mut set = |r: usize, c: usize, ch: char, st: Style| {
@@ -189,6 +216,23 @@ mod tests {
assert_eq!(levels(&[5.0], 10.0, 5), [2]);
}
+ #[test]
+ fn hysteresis_kills_boundary_flapping() {
+ // Values hovering right at a row boundary: plain rounding would
+ // alternate; hysteresis must hold the row until a decisive move.
+ let max = 100.0;
+ let rows = 10;
+ // 49.9..50.1 straddles rows-1/2 boundary (cont ~4.5).
+ let vals = [50.4, 49.6, 50.4, 49.6, 50.4, 49.6];
+ let lv = levels_hysteresis(&vals, max, rows);
+ let all_same = lv.iter().all(|&r| r == lv[0]);
+ assert!(all_same, "flapped: {:?}", lv);
+ // A real step still tracks: push decisively past the margin.
+ let vals2 = [50.0, 50.0, 70.0, 70.0];
+ let lv2 = levels_hysteresis(&vals2, max, rows);
+ assert!(lv2[3] < lv2[1], "did not step up on real change: {:?}", lv2);
+ }
+
#[test]
fn ring_window_and_wrap() {
let mut r = Ring::new(4);
Plot: row-mapping hysteresis (decisive-move threshold 0.75 row) — boundary flapping was the dash comb; test included
fdbd6dbe23a715c40c39ea7e6c217326cdf16639
josie <administrator@josie-c.com> · 2026-08-30T13:45 ·
browse files at this commit
parents:
8c2345d