Functional Weave
Code in Rust

charts.shape@1.0.1

impl/python/monotone_curve.py

2,207 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_shape_line_path import point_runs, svg_pair  ← linePath, another function of this group · built into the same file, even by a slim install
from .charts_shape_types import Point


def _sign(x: float) -> float:
    return -1.0 if x < 0 else 1.0


def _slope3(x0: float, y0: float, x1: float, y1: float, x2: float, y2: float) -> float:
    # Fritsch–Carlson: the tangent at the middle point, limited so the curve
    # cannot overshoot either neighbouring value. d3's slope3.
    h0 = x1 - x0
    h1 = x2 - x1
    s0 = (y1 - y0) / h0
    s1 = (y2 - y1) / h1
    p = (s0 * h1 + s1 * h0) / (h0 + h1)
    t = (_sign(s0) + _sign(s1)) * min(abs(s0), abs(s1), 0.5 * abs(p))
    return t if t != 0 else 0.0


def _slope2(x0: float, y0: float, x1: float, y1: float, t: float) -> float:
    return (3 * (y1 - y0) / (x1 - x0) - t) / 2


def _bezier(x0: float, y0: float, x1: float, y1: float, t0: float, t1: float) -> str:
    dx = (x1 - x0) / 3
    return "C" + svg_pair(x0 + dx, y0 + dx * t0) + "," + svg_pair(x1 - dx, y1 - dx * t1) + "," + svg_pair(x1, y1)


def monotone_curve(points: Sequence[Point]) -> str:
    """A smooth line that stays monotone wherever the data is: d3.curveMonotoneX,
    Hermite segments with Fritsch–Carlson tangents, as cubic Béziers."""
    out = ""
    for run in point_runs(points):
        for i in range(1, len(run)):
            if not run[i][0] > run[i - 1][0]:
                raise ValueError(
                    "monotone curve needs x strictly increasing, but %r follows %r" % (run[i][0], run[i - 1][0])
                )
        out += "M" + svg_pair(run[0][0], run[0][1])
        n = len(run)
        if n == 1:
            out += "Z"
            continue
        if n == 2:
            out += "L" + svg_pair(run[1][0], run[1][1])
            continue
        t0 = 0.0
        for i in range(2, n):
            ax, ay = run[i - 2]
            bx, by = run[i - 1]
            cx, cy = run[i]
            t1 = _slope3(ax, ay, bx, by, cx, cy)
            start = _slope2(ax, ay, bx, by, t1) if i == 2 else t0
            out += _bezier(ax, ay, bx, by, start, t1)
            t0 = t1
        ax, ay = run[n - 2]
        bx, by = run[n - 1]
        out += _bezier(ax, ay, bx, by, t0, _slope2(ax, ay, bx, by, t0))
    return out