Ctrl K
ComunidadGuías…

src/lib/chart-scale.test.ts

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

Archivo original ↗Preguntar a KiBot
import { describe, expect, it } from "vitest";
import {
  CHART_FULL_WIDTH_PX,
  alignedDomain,
  axisTicks,
  countDomain,
  fittedChartWidth,
  labelCapacity,
  labelWidth,
  niceDomain,
  plotLeft,
  stepCount,
} from "@/lib/chart-scale";

// Moved verbatim from `dive-gas.test.ts` along with the functions themselves;
// the examples are still phrased in RMV because that is the series they were
// written against, and they hold for any axis.

describe("niceDomain", () => {
  it("covers a typical RMV spread with a readable step", () => {
    // The real shape of a dive log: a few very relaxed dives, a few hard ones.
    expect(niceDomain([5.03, 12.67, 25.94])).toEqual({
      min: 5,
      max: 30,
      step: 5,
    });
  });

  it("always contains every value", () => {
    const values = [8.2, 19.9, 13.4, 22.1];
    const { min, max } = niceDomain(values);

    expect(min).toBeLessThanOrEqual(Math.min(...values));
    expect(max).toBeGreaterThanOrEqual(Math.max(...values));
  });

  it("does not anchor to zero", () => {
    // Anchoring at 0 would squash the band divers actually live in.
    expect(niceDomain([18, 19, 20]).min).toBeGreaterThan(0);
  });

  it("gives a flat series a band to sit in rather than a zero-height one", () => {
    // A zero-height range makes every scaled y coordinate NaN.
    expect(niceDomain([14, 14, 14])).toEqual({ min: 13, max: 15, step: 1 });
  });

  it("survives an empty series", () => {
    expect(niceDomain([])).toEqual({ min: 0, max: 1, step: 1 });
  });
});

describe("alignedDomain", () => {
  it("covers the values in exactly the steps asked for", () => {
    // A tank's pressures under a 28-30 °C axis of four steps.
    expect(alignedDomain([46.11, 199.06], 4)).toEqual({
      min: 0,
      max: 200,
      step: 50,
    });
  });

  it("goes coarser rather than break the count", () => {
    // 650-2 900 psi in four steps: 500 would end at 2 500, short of the top.
    const domain = alignedDomain([650, 2900], 4);
    expect(domain).toEqual({ min: 0, max: 4000, step: 1000 });
    expect(stepCount(domain)).toBe(4);
  });

  it("gives values that never change whole units around them", () => {
    // A cylinder whose gauge never moved: a ladder starting from a zero range
    // would print 200.0000000001.
    expect(alignedDomain([200, 200], 4)).toEqual({
      min: 198,
      max: 202,
      step: 1,
    });
  });

  it("lands every tick on another axis's height", () => {
    const temperature = niceDomain([21.6, 26.4]);
    const pressure = alignedDomain([30, 210], stepCount(temperature));

    expect(axisTicks(pressure)).toHaveLength(axisTicks(temperature).length);
  });
});

describe("axisTicks", () => {
  it("includes both ends of the domain", () => {
    expect(axisTicks({ min: 5, max: 30, step: 5 })).toEqual([
      5, 10, 15, 20, 25, 30,
    ]);
  });

  it("does not accumulate floating-point drift on fractional steps", () => {
    // Accumulating `+= step` here yields 12.499999999999998 as a tick label.
    expect(axisTicks({ min: 10, max: 15, step: 2.5 })).toEqual([10, 12.5, 15]);
  });
});

describe("countDomain", () => {
  it("anchors at zero, so a bar's height is its quantity", () => {
    // The whole reason this exists alongside `niceDomain`, which would start
    // this axis at 8 and draw twelve dives as three times the block of ten.
    expect(countDomain(12).min).toBe(0);
  });

  it("labels whole dives, never halves", () => {
    // `niceDomain([0, 2])` picks a step of 0.5 and labels the axis 0, 0.5, 1 -
    // half a dive is not a thing anyone can log.
    for (const highest of [1, 2, 3, 5, 7, 12, 40, 137, 1200]) {
      for (const tick of axisTicks(countDomain(highest))) {
        expect(Number.isInteger(tick)).toBe(true);
      }
    }
  });

  it("always contains the tallest bar", () => {
    for (const highest of [1, 3, 9, 12, 41, 250, 999]) {
      expect(countDomain(highest).max).toBeGreaterThanOrEqual(highest);
    }
  });

  it("keeps the axis to a handful of gridlines", () => {
    for (const highest of [1, 4, 17, 63, 480, 5000]) {
      expect(axisTicks(countDomain(highest)).length).toBeLessThanOrEqual(6);
    }
  });

  it("doesn't leave the tallest bar stranded under a rounded-up axis", () => {
    // A real logbook's busiest year. At a target of four gaps this rounded to a
    // step of 100 and an axis of 300, so the tallest bar in the whole chart
    // stopped three quarters of the way up and the top quarter was always empty.
    const domain = countDomain(223);

    expect(domain).toEqual({ min: 0, max: 250, step: 50 });
    expect(223 / domain.max).toBeGreaterThan(0.85);
  });

  it("gives an empty logbook an axis to draw nothing in", () => {
    // Zero-height everywhere, but the gridlines and the baseline still have to
    // land somewhere finite.
    expect(countDomain(0)).toEqual({ min: 0, max: 1, step: 1 });
  });
});

describe("fittedChartWidth", () => {
  it("keeps the design width from the full-width container up", () => {
    expect(fittedChartWidth(720, CHART_FULL_WIDTH_PX)).toBe(720);
    expect(fittedChartWidth(720, 1100)).toBe(720);
  });

  it("narrows in step with a narrower container, so the scale holds", () => {
    // A 375px phone's card leaves the chart 293px.
    const width = fittedChartWidth(720, 293);

    expect(293 / width).toBeCloseTo(CHART_FULL_WIDTH_PX / 720);
  });

  it("uses the design width for a container it has not measured", () => {
    expect(fittedChartWidth(720, null)).toBe(720);
    expect(fittedChartWidth(720, 0)).toBe(720);
  });
});

describe("labelCapacity", () => {
  it("counts whole labels only", () => {
    expect(labelCapacity(664, 33)).toBe(20);
  });

  it("never offers fewer than one", () => {
    expect(labelCapacity(10, 33)).toBe(1);
  });
});

describe("plotLeft", () => {
  it("makes room for the widest label, not the last one", () => {
    expect(plotLeft([8, 10, 12, 14])).toBe(plotLeft([10]));
    expect(plotLeft([0, 5])).toBeLessThan(plotLeft([0, 50]));
  });

  it("counts a decimal point as narrower than a digit", () => {
    expect(plotLeft([12.5])).toBeGreaterThan(plotLeft([12]));
    expect(plotLeft([12.5])).toBeLessThan(plotLeft([1250]));
  });
});

describe("labelWidth", () => {
  it("scales with the type size", () => {
    expect(labelWidth("200%", 10)).toBeCloseTo((labelWidth("200%") * 10) / 11);
  });

  it("is never narrower than Inter draws it", () => {
    // Measured in the browser at `fontSize={11}`.
    expect(labelWidth("40")).toBeGreaterThanOrEqual(14.04);
    expect(labelWidth("1.6 bar")).toBeGreaterThanOrEqual(34.43);
    expect(labelWidth("200%")).toBeGreaterThanOrEqual(31.39);
  });
});