From 53972cf37c2fd20a2383abb851a2a4ee73a6671a Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:07:43 +0000 Subject: [PATCH] Make chart axis flatness thresholds relative to data magnitude Found while testing crates/model/src/chart.rs:775-794 (axis_scale); same pattern in nice_scale :817 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/model/src/chart.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/crates/model/src/chart.rs b/crates/model/src/chart.rs index f5aeb1ee..29d28fbb 100644 --- a/crates/model/src/chart.rs +++ b/crates/model/src/chart.rs @@ -772,7 +772,7 @@ pub fn axis_scale( // Office's automatic limits ("How chart axis limits are determined", as Jon Peltier // summarises it): 5 % of the data range past the data, and zero when the values don't // sit within the top sixth of their largest magnitude. A single value is measured from zero. - let flat = hi - lo < 1e-12; + let flat = hi - lo <= 1e-12 * lo.abs().max(hi.abs()); let pad = if !headroom { 0.0 } else if flat { @@ -784,12 +784,15 @@ pub fn axis_scale( let auto_hi = if hi <= 0.0 && (flat || hi > lo * 5.0 / 6.0) { 0.0 } else { hi + pad }; let mut lo = fmin.unwrap_or(auto_lo); let mut hi = fmax.unwrap_or(auto_hi); - if hi - lo < 1e-12 { + let mag = lo.abs().max(hi.abs()); + // Relative to the data's magnitude, so tiny values don't count as flat; all-zero falls back to 1. + let unit = if mag == 0.0 { 1.0 } else { mag }; + if hi - lo <= 1e-12 * mag { // A fixed limit on the far side of the data: the automatic end gives way. match (fmin, fmax) { - (None, Some(_)) => lo = if hi > 0.0 { 0.0 } else { hi - hi.abs().max(1.0) }, + (None, Some(_)) => lo = if hi > 0.0 { 0.0 } else { hi - hi.abs().max(unit) }, (Some(_), None) if lo < 0.0 => hi = 0.0, - _ => hi = lo + lo.abs().max(1.0), + _ => hi = lo + unit, } } let n = max_intervals.clamp(1, 50) as f64; @@ -814,7 +817,9 @@ pub fn axis_scale( /// "Nice" axis scale: (min, max, major step) covering [lo, hi]. pub fn nice_scale(lo: f64, hi: f64) -> (f64, f64, f64) { let (lo, hi) = if lo.is_finite() && hi.is_finite() { (lo.min(hi), lo.max(hi)) } else { (0.0, 1.0) }; - let span = if hi - lo < 1e-12 { hi.abs().max(1.0) } else { hi - lo }; + let data_mag = lo.abs().max(hi.abs()); + let span = if hi - lo <= 1e-12 * data_mag { data_mag } else { hi - lo }; + let span = if span == 0.0 { 1.0 } else { span }; let raw = span / 5.0; let mag = 10f64.powf(raw.log10().floor()); let norm = raw / mag; @@ -831,7 +836,7 @@ pub fn nice_scale(lo: f64, hi: f64) -> (f64, f64, f64) { } * mag; let min = (lo / step).floor() * step; let max = (hi / step).ceil() * step; - let max = if (max - min).abs() < 1e-12 { min + step } else { max }; + let max = if (max - min).abs() <= 1e-12 * data_mag { min + step } else { max }; (min, max, step) } @@ -839,6 +844,16 @@ pub fn nice_scale(lo: f64, hi: f64) -> (f64, f64, f64) { mod tests { use super::*; + #[test] + fn tiny_data_keeps_its_own_scale() { + let (min, max, step) = axis_scale(1e-13, 5e-13, None, None, None, 8, true); + assert!(min == 0.0 && (5e-13..1e-11).contains(&max), "{min} {max} {step}"); + assert!(step > 0.0 && step < 1e-11); + let (min, max, step) = nice_scale(1e-13, 5e-13); + assert!(min <= 1e-13 && (5e-13..1e-11).contains(&max) && step < 1e-11, "{min} {max} {step}"); + assert_eq!(axis_scale(0.0, 0.0, None, None, None, 8, true).1, 1.0); + } + #[test] fn log_axes_run_over_whole_powers() { let mut c = Chart::sample(ChartType::Line);