// "Nice numbers" axis-tick generation (Heckbert's algorithm). Produces evenly
// spaced, human-friendly tick values for a value axis so charts get readable
// gridlines like 0, 50, 100, ... instead of raw data extremes.

///|
/// Return a "nice" number approximately equal to `x` (which must be positive).
/// When `round` is true it rounds to the nearest nice number {1, 2, 5, 10}ยท10^k;
/// otherwise it rounds up to one.
fn nice_num(x : Double, round : Bool) -> Double {
  let exp = @math.floor(@math.log10(x))
  let frac = x / @math.pow(10.0, exp)
  let nice = if round {
    if frac < 1.5 {
      1.0
    } else if frac < 3.0 {
      2.0
    } else if frac < 7.0 {
      5.0
    } else {
      10.0
    }
  } else if frac <= 1.0 {
    1.0
  } else if frac <= 2.0 {
    2.0
  } else if frac <= 5.0 {
    5.0
  } else {
    10.0
  }
  nice * @math.pow(10.0, exp)
}

///|
/// Compute about `target` evenly spaced, nicely rounded ticks covering
/// `[lo, hi]`. Returns the (possibly expanded) axis bounds together with the
/// tick values, e.g. `nice_ticks(0, 240, 5)` -> bounds `0..250`, ticks
/// `[0, 50, 100, 150, 200, 250]`.
fn nice_ticks(
  lo : Double,
  hi : Double,
  target : Int,
) -> (Double, Double, Array[Double]) {
  // Guard a degenerate range so we still produce a sensible axis.
  let hi = if hi <= lo { lo + 1.0 } else { hi }
  let count = if target < 2 { 2 } else { target }
  let span = nice_num(hi - lo, false)
  let step = nice_num(span / (count - 1).to_double(), true)
  let axis_lo = @math.floor(lo / step) * step
  let axis_hi = @math.ceil(hi / step) * step
  // Derive the tick count from an index to avoid float drift in the values.
  let n = ((axis_hi - axis_lo) / step).to_int() + 1
  let ticks : Array[Double] = []
  for i in 0..