from typing import Sequence from .charts_shape_line_path import point_runs, svg_pair 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