import math from typing import Sequence from .charts_scale_types import NiceDomain def nice_domain(domain: Sequence[float], count: int) -> NiceDomain: """Widen a domain to multiples of a round tick step chosen for about count ticks. Widening can change the best step, so it repeats until the step settles, as d3's nice() does. """ if len(domain) != 2: raise ValueError(f"domain must have exactly 2 values, [from, to]; got {len(domain)}") if isinstance(count, bool) or not isinstance(count, int) or count < 1: raise ValueError(f"count must be a whole number of at least 1, got {count}") reversed_ = domain[1] < domain[0] lo = float(domain[1] if reversed_ else domain[0]) hi = float(domain[0] if reversed_ else domain[1]) if lo == hi: return NiceDomain(domain=[float(domain[0]), float(domain[1])], step=0.0) step = 0.0 for _ in range(10): nxt = _tick_step(lo, hi, count) if nxt == step: break step = nxt if step >= 1: lo = math.floor(lo / step) * step hi = math.ceil(hi / step) * step else: # A fractional step divides badly (0.3 / 0.1 is 2.9999999999999996), # so multiply by its whole-number inverse instead. inverse = math.floor(1 / step + 0.5) lo = math.floor(lo * inverse) / inverse hi = math.ceil(hi * inverse) / inverse ends = [_round6(hi), _round6(lo)] if reversed_ else [_round6(lo), _round6(hi)] return NiceDomain(domain=ends, step=_round6(step)) def _tick_step(lo: float, hi: float, count: int) -> float: raw = (hi - lo) / count # Powers of ten by repeated multiplication rather than log10, whose last # digit is not guaranteed to agree between languages. power = 1.0 while power * 10 <= raw: power *= 10 while power > raw: power /= 10 error = raw / power if error >= math.sqrt(50): factor = 10 elif error >= math.sqrt(10): factor = 5 elif error >= math.sqrt(2): factor = 2 else: factor = 1 return factor * power def _round6(x: float) -> float: return math.floor(x * 1e6 + 0.5) / 1e6