milestone 30: overview as a dashboard, explicit health contract, period aggregations

This commit is contained in:
2026-08-22 16:45:15 +02:00
parent 0e83477d80
commit 623667e475
89 changed files with 7222 additions and 4239 deletions
@@ -0,0 +1,93 @@
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);
});
});