~james/core

core/web/src/lib/components/profile/profileActivityChart.test.ts -rw-r--r-- 4.2 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import { describe, expect, it } from "vitest";
import {
	ACTIVITY_METRICS,
	layoutActivityChart,
	type ActivityMetric
} from "#lib/components/profile/profileActivityChart.ts";

const values = {
	pulls: [10, 20, 30, 40, 50, 60],
	commits: [600, 500, 400, 300, 200, 100],
	repos: [0, 1, 0, 2, 1, 3],
	issues: [5, 5, 5, 5, 5, 5]
} satisfies Record<ActivityMetric, number[]>;

describe("layoutActivityChart", () => {
	it("fills wide containers and preserves the design's column proportions", () => {
		const layout = layoutActivityChart(values, 1976);

		expect(layout.width).toBe(1976);
		expect(layout.columns.at(-1)!.left + layout.columns.at(-1)!.width).toBe(1976);
		expect(layout.columns[0].width).toBe(292);
		expect(layout.columns.at(-1)!.width).toBe(516);
	});

	it("anchors every metric 12px inside its month column", () => {
		const layout = layoutActivityChart(values, 1976);

		for (const [month, column] of layout.columns.entries()) {
			for (const metric of ACTIVITY_METRICS) {
				expect(layout.points[metric][month].x).toBe(column.left + 12);
			}
		}
	});

	it("keeps a scrollable minimum width and omits the first divider", () => {
		const layout = layoutActivityChart(values, 320);
		const design = layoutActivityChart(values, 720);

		expect(layout.columns[0].width).toBeCloseTo(design.columns[0].width * 1.5);
		expect(layout.columns.at(-1)!.width).toBeCloseTo(design.columns.at(-1)!.width);
		expect(layout.columns[0].width).toBeLessThan(layout.columns.at(-1)!.width);
		expect(layout.columns.at(-1)!.left + layout.columns.at(-1)!.width).toBeCloseTo(
			layout.width
		);
		expect(design.width).toBe(720);
		expect(layout.columns[0].divider).toBe(false);
		expect(layout.columns.slice(1).every((column) => column.divider)).toBe(true);
	});

	it("normalizes every metric independently across the shared plot", () => {
		const layout = layoutActivityChart(values, 988);

		expect(layout.points.pulls[0].y).toBeGreaterThan(layout.points.pulls[5].y);
		expect(layout.points.commits[0].y).toBeLessThan(layout.points.commits[5].y);
	});

	it("keeps every value card legible and bounded by the plot", () => {
		const layout = layoutActivityChart(values, 988);

		for (let month = 0; month < 6; month += 1) {
			const stacked = ACTIVITY_METRICS.map(
				(metric) => layout.points[metric][month].y
			).toSorted((a, b) => a - b);
			for (const [index, y] of stacked.slice(1).entries()) {
				expect(y - stacked[index]).toBeGreaterThanOrEqual(30);
			}
		}

		const flat = layout.points.issues.map((point) => point.y);
		expect(Math.max(...flat) - Math.min(...flat)).toBeLessThanOrEqual(
			30 * (ACTIVITY_METRICS.length - 1)
		);

		for (const metric of ACTIVITY_METRICS) {
			for (const point of layout.points[metric]) {
				expect(point.y).toBeGreaterThanOrEqual(20);
				expect(point.y).toBeLessThanOrEqual(196);
			}
		}
	});

	it("uses the same points as the visible value anchors and path endpoints", () => {
		const layout = layoutActivityChart(values, 988);

		for (const metric of ACTIVITY_METRICS) {
			const connectors = layout.segments[metric].slice(1, -1);
			for (const [index, segment] of connectors.entries()) {
				const point = layout.points[metric][index];
				const next = layout.points[metric][index + 1];
				expect(segment.d).toMatch(new RegExp(`^M ${point.x} ${point.y} C`));
				expect(segment.d).toMatch(new RegExp(`${next.x} ${next.y}$`));
			}
		}
	});

	it("separates each column's metrics from top to bottom", () => {
		const layout = layoutActivityChart(values, 988);

		for (let month = 0; month < 6; month += 1) {
			const byY = ACTIVITY_METRICS.toSorted(
				(a, b) => layout.points[a][month].y - layout.points[b][month].y
			);
			for (const [index, metric] of byY.slice(1).entries()) {
				expect(layout.points[metric][month].y).toBeGreaterThan(
					layout.points[byY[index]][month].y
				);
			}
		}
	});

	it("runs each metric from the left edge to the right edge", () => {
		const layout = layoutActivityChart(values, 988);

		for (const metric of ACTIVITY_METRICS) {
			const first = layout.points[metric][0];
			const last = layout.points[metric].at(-1)!;
			expect(layout.segments[metric].at(0)!.d).toBe(`M 0 ${first.y} L ${first.x} ${first.y}`);
			expect(layout.segments[metric].at(-1)!.d).toBe(`M ${last.x} ${last.y} L 988 ${last.y}`);
		}
	});
});