666ghj/MiroFish · error · StarHistoryError

chart coordinates must be ordered

Error message

chart coordinates must be ordered

What it means

Raised in `_monotone_x_path` (scripts/star_history.py:788) when a chart point's x is strictly smaller than the previous point's x. Monotone-X interpolation requires non-decreasing x; equal x values are tolerated (the later point replaces the earlier one), but a regression means the input sequence zig-zags and the cubic tangents would be undefined.

Source

Thrown at scripts/star_history.py:788

    if value > 0:
        return 1
    return 0


def _monotone_x_path(points: Sequence[tuple[float, float]]) -> str:
    """Return a D3 curveMonotoneX-equivalent SVG path.

    D3 uses Steffen monotonic interpolation: interior tangents are limited so a
    smooth cubic cannot overshoot a monotonic run. This small implementation
    keeps the Star History curve shape without adding a JavaScript dependency.
    """

    normalized: list[tuple[float, float]] = []
    for x, y in points:
        if not math.isfinite(x) or not math.isfinite(y):
            raise StarHistoryError("chart coordinates must be finite")
        if normalized and x < normalized[-1][0]:
            raise StarHistoryError("chart coordinates must be ordered")
        if normalized and x == normalized[-1][0]:
            normalized[-1] = (x, y)
        else:
            normalized.append((x, y))

    if not normalized:
        return ""

    start_x, start_y = normalized[0]
    commands = [f"M{_format_float(start_x)},{_format_float(start_y)}"]
    if len(normalized) == 1:
        return "".join(commands)
    if len(normalized) == 2:
        end_x, end_y = normalized[1]
        commands.append(f"L{_format_float(end_x)},{_format_float(end_y)}")
        return "".join(commands)

    secants = [

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Sort the points before calling the path builder: `pts = sorted(pts, key=lambda p: p[0])` — duplicate-x entries are then handled by the function itself.
  2. If `render_svg` raises it, inspect the state's `reconstruction.daily` dates and `snapshots[].at` for out-of-order entries and regenerate the state from a clean backfill.
  3. Avoid hand-merging state files from different runs; use the library's update path which keeps snapshots monotonic.
  4. Add a quick assertion when preparing state: `dates = [p['date'] for p in state['reconstruction']['daily']]; assert dates == sorted(dates)`.

Example fix

# before: unsorted points
path = _monotone_x_path(points)

# after: order guaranteed (duplicate x kept, last wins)
points = sorted(points, key=lambda p: p[0])
path = _monotone_x_path(points)
Defensive patterns

Strategy: validation

Validate before calling

def non_decreasing_x(points) -> bool:
    return all(points[i][0] <= points[i + 1][0] for i in range(len(points) - 1))

if not non_decreasing_x(points):
    points = sorted(points, key=lambda p: p[0])
path = _monotone_x_path(points)

Try / catch

try:
    svg = render_svg(state, theme)
except StarHistoryError as exc:
    if "must be ordered" in str(exc):
        raise ValueError("state has out-of-order chart points; regenerate state") from exc
    raise

Prevention

When it happens

Trigger: `render_svg` building points whose x (a monotonic function of the point's datetime) decreases — e.g. state where a reconstruction `daily` entry is dated after a later snapshot, or direct callers passing unsorted tuples. Duplicate x is fine (`x == normalized[-1][0]` replaces the last point); only `x < normalized[-1][0]` raises.

Common situations: State files whose `reconstruction.daily` dates and `snapshots[].at` overlap out of order (usually from hand-merging two state files); forks that reorder `_chart_points`; direct use of the helper with unsorted data.

Related errors


AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14). Data as JSON: /api/errors/c745f1c6e34b13bd. Report an issue: GitHub.