Ctrl K
ComunidadGuías…

src/lib/chart-path.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { describe, expect, it } from "vitest";
import {
  barPath,
  buildAreaPath,
  segmentByTimeGap,
  smoothPath,
} from "@/lib/chart-path";

describe("buildAreaPath", () => {
  it("closes the shape along the baseline", () => {
    const path = buildAreaPath(
      [
        { x: 10, y: 20 },
        { x: 20, y: 40 },
        { x: 30, y: 30 },
      ],
      100,
    );

    expect(path).toBe("M10,20 L20,40 L30,30 L30,100 L10,100 Z");
  });

  it("is empty for an empty series, rather than a stray `Z`", () => {
    expect(buildAreaPath([], 100)).toBe("");
  });
});

describe("smoothPath", () => {
  it("is empty for an empty series", () => {
    expect(smoothPath([])).toBe("");
  });

  it("draws nothing but a move for a single point", () => {
    // A stretch of diving containing one dive - the same no-op a one-point
    // `<polyline>` was.
    expect(smoothPath([{ x: 10, y: 20 }])).toBe("M10,20");
  });

  it("keeps two points a straight line", () => {
    // Both tangents equal the secant, so the control points land on the segment
    // itself. Nothing to smooth between two points, and a bowed line between
    // them would be an invention.
    expect(
      smoothPath([
        { x: 0, y: 0 },
        { x: 30, y: 30 },
      ]),
    ).toBe("M0,0 C10,10 20,20 30,30");
  });

  it("starts at the first point and ends at the last", () => {
    const path = smoothPath([
      { x: 0, y: 50 },
      { x: 10, y: 20 },
      { x: 20, y: 35 },
    ]);

    expect(path.startsWith("M0,50")).toBe(true);
    expect(path.endsWith("20,35")).toBe(true);
  });

  it("never overshoots a local extreme", () => {
    // The whole reason this is monotone cubic and not Catmull-Rom. A dip in a
    // line must not be drawn dipping below its lowest value: on a dive profile
    // that would render a depth the diver never reached, below the deepest
    // point of the dive.
    const path = smoothPath([
      { x: 0, y: 100 },
      { x: 10, y: 100 },
      { x: 20, y: 10 },
      { x: 30, y: 100 },
      { x: 40, y: 100 },
    ]);

    const ys = [...path.matchAll(/[,\s]-?[\d.]+,(-?[\d.]+)/g)].map((match) =>
      Number(match[1]),
    );

    expect(Math.min(...ys)).toBeGreaterThanOrEqual(10);
    expect(Math.max(...ys)).toBeLessThanOrEqual(100);
  });

  it("flattens rather than dividing by zero on a repeated x", () => {
    // Two dives logged at the same instant. Improbable, but an infinite slope
    // would put `NaN` in the `d` attribute and blank the whole line.
    const path = smoothPath([
      { x: 0, y: 10 },
      { x: 0, y: 20 },
      { x: 10, y: 30 },
    ]);

    expect(path).not.toMatch(/NaN|Infinity/);
  });

  it("rounds coordinates instead of emitting float noise", () => {
    const path = smoothPath([
      { x: 0, y: 0 },
      { x: 10, y: 1 / 3 },
      { x: 20, y: 1 },
    ]);

    expect(path).not.toMatch(/\d\.\d{3}/);
  });
});

describe("barPath", () => {
  it("rounds the top corners and leaves the bottom square", () => {
    // Up the left side, round the top, down the right, and along the baseline -
    // which stays flat, so the bar sits on the axis rather than floating.
    expect(barPath(10, 60, 20, 40, 3)).toBe(
      "M10,100 L10,63 Q10,60 13,60 L27,60 Q30,60 30,63 L30,100 Z",
    );
  });

  it("never rounds more than the bar can carry", () => {
    // A one-dive bar in a fifty-dive year is a couple of units tall; the
    // full radius on it would bow the sides out into a lens.
    expect(barPath(10, 98, 20, 2, 3)).toBe(
      "M10,100 L10,100 Q10,98 12,98 L28,98 Q30,98 30,100 L30,100 Z",
    );
  });

  it("draws nothing for a month with no diving", () => {
    // An absence, not a sliver: a zero-height bar would still show a 3-unit cap.
    expect(barPath(10, 100, 20, 0, 3)).toBe("");
  });
});

describe("segmentByTimeGap", () => {
  it("returns one run for an unbroken series", () => {
    expect(segmentByTimeGap([0, 10_000, 20_000, 30_000], 30_000)).toEqual([
      [0, 1, 2, 3],
    ]);
  });

  it("breaks where the gap exceeds the threshold", () => {
    // A transmitter that stopped reporting between 20 s and 620 s.
    expect(
      segmentByTimeGap([0, 10_000, 20_000, 620_000, 630_000], 30_000),
    ).toEqual([
      [0, 1, 2],
      [3, 4],
    ]);
  });

  it("does not break on a gap exactly at the threshold", () => {
    expect(segmentByTimeGap([0, 30_000, 60_000], 30_000)).toEqual([[0, 1, 2]]);
  });

  it("handles an empty series", () => {
    expect(segmentByTimeGap([], 30_000)).toEqual([]);
  });

  it("handles a single sample", () => {
    expect(segmentByTimeGap([42_000], 30_000)).toEqual([[0]]);
  });

  it("covers every index exactly once, in order", () => {
    const day = 24 * 60 * 60 * 1000;
    const times = [0, 100 * day, 101 * day, 500 * day];

    expect(segmentByTimeGap(times, 60 * day).flat()).toEqual([0, 1, 2, 3]);
  });
});