Functional Weave
Code in Python

charts.scale@1.1.0

impl/python/band_scale.py

2,411 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from typing import Sequence, Tuple

from .charts_scale_linear_scale import pair  ← linearScale, another function of this group · built into the same file, even by a slim install
from .charts_scale_types import Band
from .math_round_float import round_float  ← from math.round-float ^1.0.0 · built alongside by fune


def band_scale(
    domain: Sequence[str],
    range: Sequence[float],
    value: str,
    padding_inner: float,
    padding_outer: float,
    align: float,
) -> Band:
    """The band a category occupies, as d3.scaleBand lays it out (without
    rounding to whole pixels). A reversed range reverses the order of the
    bands, not their direction, so start is always the lower coordinate."""
    if not (0 <= padding_inner <= 1):
        raise ValueError(f"paddingInner must be between 0 and 1; got {padding_inner}")
    check_outer(padding_outer, "paddingOuter")
    check_align(align)
    start, width = band_position(domain, range, value, padding_inner, padding_outer, align)
    return Band(start=round_float(start, 6), center=round_float(start + width / 2, 6), width=round_float(width, 6))


# Exported for point_scale, which is a band scale whose bands have no width.


def check_outer(padding: float, what: str) -> None:
    if not (padding >= 0):
        raise ValueError(f"{what} must be 0 or more; got {padding}")


def check_align(align: float) -> None:
    if not (0 <= align <= 1):
        raise ValueError(f"align must be between 0 and 1; got {align}")


def band_position(
    domain: Sequence[str],
    extent: Sequence[float],
    value: str,
    padding_inner: float,
    padding_outer: float,
    align: float,
) -> Tuple[float, float]:
    """The unrounded lower coordinate of value's band, and the band width."""
    r0, r1 = pair(extent, "range")
    n = len(domain)
    if n == 0:
        raise ValueError("domain must not be empty")
    index = -1
    for i in range(n):
        for j in range(i):
            if domain[j] == domain[i]:
                raise ValueError(f'domain has a repeated value: "{domain[i]}"')
        if domain[i] == value:
            index = i
    if index < 0:
        raise ValueError(f'value is not in the domain: "{value}"')
    reverse = r1 < r0
    start = r1 if reverse else r0
    stop = r0 if reverse else r1
    step = (stop - start) / max(1.0, n - float(padding_inner) + float(padding_outer) * 2)
    start += (stop - start - step * (n - float(padding_inner))) * float(align)
    slot = n - 1 - index if reverse else index
    return start + step * slot, step * (1 - float(padding_inner))