import type { Bucket } from "@/lib/types"; import { MARGIN, isEmptyTimeseries, layoutTimeseries, niceTicks } from "./chartLayout"; function bucket(ts: number, queries: number, blocked = 0, cached = 0): Bucket { return { ts, queries, blocked, cached }; } describe("niceTicks", () => { test("zero max yields a single zero tick", () => { expect(niceTicks(0)).toEqual([0]); }); test("picks a 1/2/5 step and extends past max", () => { expect(niceTicks(7)).toEqual([0, 2, 4, 6, 8]); expect(niceTicks(100)).toEqual([0, 50, 100]); expect(niceTicks(1234)).toEqual([0, 500, 1000, 1500]); }); }); describe("isEmptyTimeseries", () => { test("true for no buckets and for all-zero buckets", () => { expect(isEmptyTimeseries([])).toBe(true); expect(isEmptyTimeseries([bucket(0, 0), bucket(60, 0)])).toBe(true); }); test("false when any bucket has queries", () => { expect(isEmptyTimeseries([bucket(0, 0), bucket(60, 3)])).toBe(false); }); }); describe("layoutTimeseries", () => { test("segment heights are proportional and stack to the queries total", () => { const layout = layoutTimeseries([bucket(0, 100, 40, 10), bucket(60, 50, 0, 0)], 480, 240); const plotHeight = 240 - MARGIN.top - MARGIN.bottom; const baseline = MARGIN.top + plotHeight; const [first, second] = layout.bars; expect(layout.scaleMax).toBe(100); expect(first.other).toBe(50); expect(first.segments.blocked.height).toBeCloseTo(plotHeight * 0.4); expect(first.segments.cached.height).toBeCloseTo(plotHeight * 0.1); expect(first.segments.other.height).toBeCloseTo(plotHeight * 0.5); expect(first.segments.blocked.y + first.segments.blocked.height).toBeCloseTo(baseline); expect(first.segments.cached.y + first.segments.cached.height).toBeCloseTo(first.segments.blocked.y); expect(first.segments.other.y + first.segments.other.height).toBeCloseTo(first.segments.cached.y); expect(first.segments.other.y).toBeCloseTo(MARGIN.top); expect(second.segments.other.height).toBeCloseTo(plotHeight * 0.5); }); test("clamps other at zero when blocked + cached exceed queries", () => { const layout = layoutTimeseries([bucket(0, 10, 8, 5)], 480, 240); expect(layout.bars[0].other).toBe(0); expect(layout.bars[0].segments.other.height).toBe(0); }); test("zero data still lays out zero-height bars on a unit scale", () => { const layout = layoutTimeseries([bucket(0, 0), bucket(60, 0)], 480, 240); expect(layout.scaleMax).toBe(1); expect(layout.bars).toHaveLength(2); for (const bar of layout.bars) { expect(bar.segments.blocked.height).toBe(0); expect(bar.segments.cached.height).toBe(0); expect(bar.segments.other.height).toBe(0); } expect(layout.yTicks).toEqual([{ value: 0, y: MARGIN.top + (240 - MARGIN.top - MARGIN.bottom) }]); }); test("single bucket fills the plot width minus the gap", () => { const layout = layoutTimeseries([bucket(0, 5, 1, 1)], 480, 240); const plotWidth = 480 - MARGIN.left - MARGIN.right; const bar = layout.bars[0]; expect(bar.slot.width).toBeCloseTo(plotWidth); expect(bar.segments.blocked.width).toBeCloseTo(plotWidth - 2); expect(bar.segments.blocked.x).toBeCloseTo(MARGIN.left + 1); expect(layout.xTicks).toEqual([{ ts: 0, x: MARGIN.left + plotWidth / 2 }]); }); test("x ticks thin out when buckets outnumber the label budget", () => { const buckets = Array.from({ length: 168 }, (_, i) => bucket(i * 3600, i)); const layout = layoutTimeseries(buckets, 800, 240); expect(layout.xTicks.length).toBeLessThan(buckets.length / 10); expect(layout.xTicks[0].ts).toBe(0); const xs = layout.xTicks.map((tick) => tick.x); expect([...xs].sort((a, b) => a - b)).toEqual(xs); }); test("empty bucket list yields no bars and no x ticks", () => { const layout = layoutTimeseries([], 480, 240); expect(layout.bars).toEqual([]); expect(layout.xTicks).toEqual([]); expect(layout.scaleMax).toBe(1); }); });