pr_01m47d24b0e6n91zwymwxg0vpx/apps/sudo/app/lib/chart.test.ts

93 lines4,028 bytesCodeBlame
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { axisDollars, barPath, change, marginOf, monthBars, monthLong, monthShort, niceCeiling, shares, ticks } from "./chart.ts";
5
6const M = 1_000_000;
7const month = (m: string, charged: number, cost: number) => ({ month: m, chargedMicros: charged, costMicros: cost, paidMicros: 0 });
8
9test("niceCeiling rounds up to 1, 2, 2.5 or 5 times a power of ten", () => {
10 assert.equal(niceCeiling(0), 1 * M);
11 assert.equal(niceCeiling(1 * M), 1 * M);
12 assert.equal(niceCeiling(1.2 * M), 2 * M);
13 assert.equal(niceCeiling(2.2 * M), 2.5 * M);
14 assert.equal(niceCeiling(3 * M), 5 * M);
15 assert.equal(niceCeiling(7 * M), 10 * M);
16 assert.equal(niceCeiling(420 * M), 500 * M);
17 assert.equal(niceCeiling(10_001 * M), 20_000 * M);
18});
19
20test("ticks run from zero to the top", () => {
21 assert.deepEqual(ticks(100, 4), [0, 25, 50, 75, 100]);
22});
23
24test("months are named", () => {
25 assert.equal(monthShort("2026-10"), "Oct");
26 assert.equal(monthShort("2026-01"), "Jan");
27 assert.equal(monthShort("soon"), "soon");
28 assert.equal(monthLong("2026-10"), "October 2026");
29 assert.equal(monthLong("2026-13"), "2026-13");
30});
31
32test("margin and change", () => {
33 assert.deepEqual(marginOf(100 * M, 80 * M), { micros: 20 * M, percent: 20 });
34 assert.deepEqual(marginOf(0, 5 * M), { micros: -5 * M, percent: null });
35 assert.equal(change(100, 150), 50);
36 assert.equal(change(0, 150), null);
37 assert.equal(change(200, 100), -50);
38});
39
40test("monthBars scales both series to one axis from zero", () => {
41 const chart = monthBars([month("2026-09", 400 * M, 300 * M), month("2026-10", 100 * M, 0)], { width: 300, height: 120, left: 40, right: 0, top: 0, bottom: 20 });
42 assert.equal(chart.top, 500 * M);
43 assert.deepEqual(chart.plot, { x: 40, y: 0, width: 260, height: 100 });
44 const [sep, oct] = chart.bars;
45 assert.equal(sep.label, "Sep");
46 // 400 of 500 is 80 of the plot's 100.
47 assert.equal(Math.round(sep.charged.height), 80);
48 assert.equal(Math.round(sep.charged.y), 20);
49 assert.equal(Math.round(sep.cost.height), 60);
50 // Bars stand on the baseline.
51 assert.equal(sep.charged.y + sep.charged.height, 100);
52 assert.equal(oct.cost.height, 0);
53 // Charged on the left, cost on the right, 2px apart, inside the slot.
54 assert.equal(Math.round((sep.cost.x - (sep.charged.x + sep.charged.width)) * 100) / 100, 2);
55 assert.ok(sep.charged.x >= sep.slot.x && sep.cost.x + sep.cost.width <= sep.slot.x + sep.slot.width);
56 assert.equal(chart.ticks[0].y, 100);
57 assert.equal(chart.ticks.at(-1)?.y, 0);
58});
59
60test("a tiny month still shows, and an empty chart has an axis", () => {
61 const chart = monthBars([month("2026-09", 1000 * M, 0), month("2026-10", 1, 0)]);
62 assert.ok(chart.bars[1].charged.height >= 1.5);
63 const empty = monthBars([month("2026-10", 0, 0)]);
64 assert.equal(empty.top, 1 * M);
65 assert.equal(empty.bars[0].charged.height, 0);
66 assert.equal(monthBars([]).bars.length, 0);
67});
68
69test("negative figures draw nothing below the baseline", () => {
70 const chart = monthBars([month("2026-10", -5 * M, 2 * M)]);
71 assert.equal(chart.bars[0].charged.height, 0);
72});
73
74test("barPath rounds the top and keeps the foot square", () => {
75 assert.equal(barPath({ x: 0, y: 0, width: 10, height: 20 }), "M0 20 V4 Q0 0 4 0 H6 Q10 0 10 4 V20 Z");
76 assert.equal(barPath({ x: 0, y: 0, width: 10, height: 0 }), "");
77 // Never rounder than the bar is tall.
78 assert.equal(barPath({ x: 0, y: 9, width: 10, height: 1 }), "M0 10 V10 Q0 9 1 9 H9 Q10 9 10 10 V10 Z");
79});
80
81test("axis labels are short", () => {
82 assert.equal(axisDollars(0), "$0");
83 assert.equal(axisDollars(250 * M), "$250");
84 assert.equal(axisDollars(1500 * M), "$1.5k");
85 assert.equal(axisDollars(20_000 * M), "$20k");
86 assert.equal(axisDollars(1_200_000 * M), "$1.2M");
87 assert.equal(axisDollars(0.25 * M), "$0.25");
88});
89
90test("shares are against the largest", () => {
91 assert.deepEqual(shares([50, 100, 0, -5]), [0.5, 1, 0, 0]);
92 assert.deepEqual(shares([0, 0]), [0, 0]);
93});