josie / alder-tools

Graphs: 1/4-row trace pad in both fixed+auto; axis labels stay raw

- pad() now applies in BOTH fixed and auto mode (was auto-only). Trace
  stays ~1/4 row clear of the box borders in fixed mode too, instead of
  sitting flush against them when a value hits the conf bound
- PlotSpec extended with (axis_min, axis_max, trace_min, trace_max):
  axis labels show the RAW conf/window bounds (e.g. 1.5/0.5, 280/0),
  render maps over the PADDED bounds. User sees their conf values on the
  axis, not 1.536/0.464
- 15 tests, clippy clean; pty-verified 50x100

bbc5b462ab18885f83b7ffbdd28b78b1b2190d40
josie <administrator@josie-c.com> · 2026-09-01T23:21 · browse files at this commit

parents: 4197028

diff --git a/src/ui.rs b/src/ui.rs
index af13d85..9c4a6eb 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -528,10 +528,11 @@ fn render_lines(f: &mut Frame, area: Rect, lines: &[Line<'static>]) {
     }
 }
 
-/// One stacked plot: (label, live text, peak text, ring, scale min/max,
-/// line color, optional marker row, axis display divisor — clock shows
-/// GHz on the axis but stores kHz).
-type PlotSpec<'a> = (&'a str, String, String, &'a Ring, f64, f64, Color, Option<u16>, f64);
+/// One stacked plot: (label, live text, peak text, ring, axis min/max
+/// [RAW, for labels], trace min/max [PADDED, for render], line color,
+/// optional marker row, axis display divisor — clock shows GHz on the
+/// axis but stores kHz).
+type PlotSpec<'a> = (&'a str, String, String, &'a Ring, f64, f64, f64, f64, Color, Option<u16>, f64);
 
 pub fn draw(f: &mut Frame, app: &App) {
     let area = f.area();
@@ -694,6 +695,9 @@ pub fn draw(f: &mut Frame, app: &App) {
         // and one-row headroom at each edge keeps the line ~2 cells clear
         // of the box borders.
         let ring_min = |ring: &Ring| ring.min_window(ring_ticks);
+        // Raw bounds: fixed = conf min/max, auto = window min/max +
+        // SCALE_MARGIN. Used for the axis labels so they show the user's
+        // conf values (not the padded trace bounds).
         let (vc_lo, vc_hi, ck_lo, ck_hi, pw_lo, pw_hi, tp_lo, tp_hi) =
             if app.cfg.graph_scale_fixed {
                 (
@@ -709,42 +713,37 @@ pub fn draw(f: &mut Frame, app: &App) {
                 let tp = (ring_min(&app.temp_graph), ring_max(&app.temp_graph, 0.0));
                 (vc.0, vc.1, ck.0, ck.1, pw.0, pw.1, tp.0, tp.1)
             };
-        // Headroom so the trace fills the interior without kissing the
-        // borders: pad the window span by ~1/4 interior row top and bottom
-        // (plus a small per-metric floor so flat lines don't go degenerate).
+        // Padded bounds for the trace mapping: ~1/4 interior row top and
+        // bottom (plus a small per-metric floor) so the trace stays just
+        // clear of the box borders in both fixed and auto mode. Applied
+        // to a COPY of the raw bounds; the raw bounds stay for axis labels.
         let pad = |lo: f64, hi: f64, floor: f64| -> (f64, f64) {
             let row_w = ((hi - lo) / (graph_rows.max(1) as f64) * 0.25).max(floor);
             ((lo - row_w).max(0.0), hi + row_w)
         };
-        let (vc_lo, vc_hi, ck_lo, ck_hi, pw_lo, pw_hi, tp_lo, tp_hi) =
-            if app.cfg.graph_scale_fixed {
-                (vc_lo, vc_hi, ck_lo, ck_hi, pw_lo, pw_hi, tp_lo, tp_hi)
-            } else {
-                let (a, b) = pad(vc_lo, vc_hi, 0.02);
-                let (c, d) = pad(ck_lo, ck_hi, 50_000.0);
-                let (e, f_) = pad(pw_lo, pw_hi, 1.0);
-                let (g, h) = pad(tp_lo, tp_hi, 1.0);
-                (a, b, c, d, e, f_, g, h)
-            };
+        let (p_vc_lo, p_vc_hi) = pad(vc_lo, vc_hi, 0.02);
+        let (p_ck_lo, p_ck_hi) = pad(ck_lo, ck_hi, 50_000.0);
+        let (p_pw_lo, p_pw_hi) = pad(pw_lo, pw_hi, 1.0);
+        let (p_tp_lo, p_tp_hi) = pad(tp_lo, tp_hi, 1.0);
         // 4 boxed plots stacked top→bottom, sharing one time axis.
         // Cascade: vCore → Peak clock → Pkg power → Pkg temp (user order).
         let plots: [PlotSpec; 4] = [
-            ("vCore", hero_text.clone(), hero_peak, &app.vcore_graph, vc_lo, vc_hi, vc_color,
-                Some(((1.0 - ((app.cfg.vcore_limit - vc_lo) / (vc_hi - vc_lo)).clamp(0.0, 1.0))
+            ("vCore", hero_text.clone(), hero_peak, &app.vcore_graph, vc_lo, vc_hi, p_vc_lo, p_vc_hi, vc_color,
+                Some(((1.0 - ((app.cfg.vcore_limit - p_vc_lo) / (p_vc_hi - p_vc_lo)).clamp(0.0, 1.0))
                     * (graph_rows - 1) as f64).round() as u16), 1.0),
-            ("Clock", clock_text, clock_peak, &app.clock_graph, ck_lo, ck_hi, Color::Green, None, 1_000_000.0),
-            ("Power", fmt_opt_watts(app.package_watts), watts_peak, &app.watts_graph, pw_lo, pw_hi, Color::Blue, None, 1.0),
-            ("Temp", fmt_opt_temp(app.package_temp), temp_peak, &app.temp_graph, tp_lo, tp_hi,
+            ("Clock", clock_text, clock_peak, &app.clock_graph, ck_lo, ck_hi, p_ck_lo, p_ck_hi, Color::Green, None, 1_000_000.0),
+            ("Power", fmt_opt_watts(app.package_watts), watts_peak, &app.watts_graph, pw_lo, pw_hi, p_pw_lo, p_pw_hi, Color::Blue, None, 1.0),
+            ("Temp", fmt_opt_temp(app.package_temp), temp_peak, &app.temp_graph, tp_lo, tp_hi, p_tp_lo, p_tp_hi,
                 app.package_temp.map_or(Color::DarkGray, |t| temp_color(t, app.cfg.temp_warn, app.cfg.temp_crit)), None, 1.0),
         ];
         let mut y = bottom_y;
-        for (label, text, peak, ring, min, max, color, marker, axis_div) in plots {
-            let axis_max = plot::fmt_axis(max / axis_div);
+        for (label, text, peak, ring, axis_min, axis_max, min, max, color, marker, axis_div) in plots {
+            let axis_max = plot::fmt_axis(axis_max / axis_div);
             let axis_min = if axis_div > 1.0 {
                 // GHz axis: always show decimals so a near-zero min isn't "0".
-                format!("{:.1}", min / axis_div)
+                format!("{:.1}", axis_min / axis_div)
             } else {
-                plot::fmt_axis(min / axis_div)
+                plot::fmt_axis(axis_min / axis_div)
             };
             // Box spans the full width — no left gutter. Axis labels
             // overlay the top/bottom BORDER rows, just inside the ┌/└