Functional Weave
Code in TypeScript

charts.axis@1.0.0

impl/python/axis_from_ticks.py

2,791 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

from .charts_axis_types import Axis, AxisOrient, AxisTick, Line
from .math_round_float import round_float  ← from math.round-float ^1.0.0 · built alongside by fune

# d3-axis's default gap between the end of a tick mark and its label.
_LABEL_PADDING = 3


def axis_pixel(value: float) -> float:
    """A pixel coordinate rounded to 2 places, the precision every axis output uses."""
    return round_float(value, 2)


def axis_line(x1: float, y1: float, x2: float, y2: float) -> Line:
    return Line(x1=axis_pixel(x1), y1=axis_pixel(y1), x2=axis_pixel(x2), y2=axis_pixel(y2))


def check_orient(orient: str) -> None:
    if orient not in ("bottom", "top", "left", "right"):
        raise ValueError('orient must be bottom, top, left or right, received "%s"' % (orient,))


def axis_from_ticks(
    positions: Sequence[float],
    labels: Sequence[str],
    range: Sequence[float],  # noqa: A002 - the manifest names it range
    orient: AxisOrient,
    position: float,
    tick_size: float,
) -> Axis:
    """Axis geometry from tick positions and labels, for any scale (d3-axis layout)."""
    check_orient(orient)
    if len(positions) != len(labels):
        raise ValueError(
            "positions and labels must be the same length, received %d and %d" % (len(positions), len(labels))
        )
    if len(range) != 2:
        raise ValueError("range must have exactly 2 values, [from, to]; got %d" % len(range))
    horizontal = orient in ("bottom", "top")
    sign = 1 if orient in ("bottom", "right") else -1
    mark_end = position + sign * tick_size
    label_at = position + sign * (tick_size + _LABEL_PADDING)
    ticks = []
    for p, label in zip(positions, labels):
        if horizontal:
            ticks.append(
                AxisTick(
                    position=axis_pixel(p),
                    label=label,
                    tick=axis_line(p, position, p, mark_end),
                    label_x=axis_pixel(p),
                    label_y=axis_pixel(label_at),
                    anchor="middle",
                    baseline="hanging" if orient == "bottom" else "alphabetic",
                )
            )
        else:
            ticks.append(
                AxisTick(
                    position=axis_pixel(p),
                    label=label,
                    tick=axis_line(position, p, mark_end, p),
                    label_x=axis_pixel(label_at),
                    label_y=axis_pixel(p),
                    anchor="end" if orient == "left" else "start",
                    baseline="middle",
                )
            )
    if horizontal:
        line = axis_line(range[0], position, range[1], position)
    else:
        line = axis_line(position, range[0], position, range[1])
    return Axis(orient=orient, position=axis_pixel(position), line=line, ticks=ticks)