Functional Weave
Code in Rust

charts.scale@1.0.0

impl/python/linear_scale.py

1,503 bytes · the Python implementation · view raw

import math
from typing import Sequence, Tuple


def linear_scale(domain: Sequence[float], range: Sequence[float], value: float, clamp: bool) -> float:
    """Where value lands on the range, as a straight-line map from the domain.

    Outside the domain the line carries on, unless clamp stops it at the
    range's ends.
    """
    d0, d1 = pair(domain, "domain")
    r0, r1 = pair(range, "range")
    t = normalise(d0, d1, value, "domain")
    if clamp:
        t = max(0.0, min(1.0, t))
    return round6(interpolate(r0, r1, t))


# The helpers below are shared with invert_linear, which is the same line read
# the other way; they are not part of the group's contract.


def pair(values: Sequence[float], what: str) -> Tuple[float, float]:
    if len(values) != 2:
        raise ValueError(f"{what} must have exactly 2 values, [from, to]; got {len(values)}")
    return float(values[0]), float(values[1])


def normalise(start: float, end: float, value: float, what: str) -> float:
    # Equal ends would divide by zero, which Python raises as a bare
    # ZeroDivisionError that does not say which end is wrong.
    if start == end:
        raise ValueError(f"{what} has no width: both ends are {start}")
    return (value - start) / (end - start)


def interpolate(start: float, end: float, t: float) -> float:
    return start + t * (end - start)


def round6(x: float) -> float:
    """Half up to 6 decimal places, the same way in every language."""
    return math.floor(x * 1e6 + 0.5) / 1e6