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); }); });