@fuzdev/fuz_ui 0.206.8 → 0.207.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DeclarationDetail.svelte +55 -20
- package/dist/DeclarationDetail.svelte.d.ts +1 -1
- package/dist/DeclarationDetail.svelte.d.ts.map +1 -1
- package/dist/ProjectActivityChart.svelte +137 -0
- package/dist/ProjectActivityChart.svelte.d.ts +30 -0
- package/dist/ProjectActivityChart.svelte.d.ts.map +1 -0
- package/dist/ProjectLinks.svelte +31 -46
- package/dist/ProjectLinks.svelte.d.ts.map +1 -1
- package/dist/Sparkline.svelte +131 -0
- package/dist/Sparkline.svelte.d.ts +42 -0
- package/dist/Sparkline.svelte.d.ts.map +1 -0
- package/dist/TypeJsonView.svelte +90 -0
- package/dist/TypeJsonView.svelte.d.ts +26 -0
- package/dist/TypeJsonView.svelte.d.ts.map +1 -0
- package/dist/TypeLink.svelte +17 -1
- package/dist/TypeLink.svelte.d.ts +10 -0
- package/dist/TypeLink.svelte.d.ts.map +1 -1
- package/dist/declaration.svelte.d.ts +25 -5
- package/dist/declaration.svelte.d.ts.map +1 -1
- package/dist/declaration.svelte.js +31 -5
- package/dist/project_stats.d.ts +121 -0
- package/dist/project_stats.d.ts.map +1 -0
- package/dist/project_stats.js +153 -0
- package/dist/project_stats_data.d.ts +3 -0
- package/dist/project_stats_data.d.ts.map +1 -0
- package/dist/project_stats_data.js +469 -0
- package/dist/project_stats_update.task.d.ts +4 -0
- package/dist/project_stats_update.task.d.ts.map +1 -0
- package/dist/project_stats_update.task.js +50 -0
- package/dist/projects.d.ts +17 -0
- package/dist/projects.d.ts.map +1 -0
- package/dist/projects.js +20 -0
- package/dist/publish.task.d.ts +3 -0
- package/dist/publish.task.d.ts.map +1 -0
- package/dist/publish.task.js +23 -0
- package/dist/sparkline.d.ts +102 -0
- package/dist/sparkline.d.ts.map +1 -0
- package/dist/sparkline.js +63 -0
- package/package.json +2 -2
- package/src/lib/declaration.svelte.ts +37 -7
- package/src/lib/project_stats.ts +220 -0
- package/src/lib/project_stats_data.ts +472 -0
- package/src/lib/project_stats_update.task.ts +53 -0
- package/src/lib/projects.ts +49 -0
- package/src/lib/publish.task.ts +22 -0
- package/src/lib/sparkline.ts +173 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure geometry helpers behind `Sparkline` and `ProjectActivityChart`.
|
|
3
|
+
*
|
|
4
|
+
* Computes polyline points, alpha-fade gradient stops, and the end-dot
|
|
5
|
+
* position for a series of values, so components stay thin and the math is
|
|
6
|
+
* testable and reusable outside Svelte.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Vertical scale for a sparkline's geometry. The nonlinear options compress
|
|
12
|
+
* the top of the range so small values stay legible under a large shared
|
|
13
|
+
* max — increasingly so from `'sqrt'` to smaller power exponents (a number
|
|
14
|
+
* in `(0, 1]`, where `1` is linear and `1 / 3` is cube root) to `'log'`.
|
|
15
|
+
*/
|
|
16
|
+
export type SparklineScale = 'linear' | 'sqrt' | 'log' | number;
|
|
17
|
+
export interface SparklineOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Width of the drawing area in px.
|
|
20
|
+
*/
|
|
21
|
+
width: number;
|
|
22
|
+
/**
|
|
23
|
+
* Height of the drawing area in px.
|
|
24
|
+
*/
|
|
25
|
+
height: number;
|
|
26
|
+
/**
|
|
27
|
+
* The value rendered at full height and full alpha.
|
|
28
|
+
* Pass a shared max across sparklines for comparable scales.
|
|
29
|
+
* Defaults to the local max of the values.
|
|
30
|
+
*/
|
|
31
|
+
max?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Vertical scale for the line's geometry. The alpha fade always tracks
|
|
34
|
+
* the linear ratio regardless of `scale`.
|
|
35
|
+
*
|
|
36
|
+
* @default 'linear'
|
|
37
|
+
*/
|
|
38
|
+
scale?: SparklineScale;
|
|
39
|
+
/**
|
|
40
|
+
* @default 2
|
|
41
|
+
*/
|
|
42
|
+
stroke_width?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Alpha at a value of 0, scaling linearly to 1 at the max.
|
|
45
|
+
*
|
|
46
|
+
* @default 0.4
|
|
47
|
+
*/
|
|
48
|
+
alpha_floor?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Reserve padding for an end dot of radius `stroke_width` at the final point.
|
|
51
|
+
*
|
|
52
|
+
* @default false
|
|
53
|
+
*/
|
|
54
|
+
end_dot?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A gradient stop for the per-point alpha fade.
|
|
58
|
+
*/
|
|
59
|
+
export interface SparklineStop {
|
|
60
|
+
/**
|
|
61
|
+
* Gradient offset in `[0, 1]`.
|
|
62
|
+
*/
|
|
63
|
+
offset: number;
|
|
64
|
+
/**
|
|
65
|
+
* Stop opacity in `[alpha_floor, 1]`.
|
|
66
|
+
*/
|
|
67
|
+
opacity: number;
|
|
68
|
+
}
|
|
69
|
+
export interface SparklineGeometry {
|
|
70
|
+
/**
|
|
71
|
+
* SVG polyline `points` string.
|
|
72
|
+
*/
|
|
73
|
+
points: string;
|
|
74
|
+
/**
|
|
75
|
+
* Alpha-fade gradient stops, thinned to runs' boundaries so long flat
|
|
76
|
+
* stretches (like all-zero history) cost a constant number of stops.
|
|
77
|
+
*/
|
|
78
|
+
stops: Array<SparklineStop>;
|
|
79
|
+
/**
|
|
80
|
+
* X coordinate of the final point.
|
|
81
|
+
*/
|
|
82
|
+
end_x: number;
|
|
83
|
+
/**
|
|
84
|
+
* Y coordinate of the final point.
|
|
85
|
+
*/
|
|
86
|
+
end_y: number;
|
|
87
|
+
/**
|
|
88
|
+
* Alpha of the final point.
|
|
89
|
+
*/
|
|
90
|
+
end_alpha: number;
|
|
91
|
+
/**
|
|
92
|
+
* Radius for an end dot, equal to the stroke width.
|
|
93
|
+
*/
|
|
94
|
+
dot_radius: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Computes the rendering geometry for a sparkline of `values`.
|
|
98
|
+
*
|
|
99
|
+
* Returns `null` when `values` has fewer than 2 points, which cannot form a line.
|
|
100
|
+
*/
|
|
101
|
+
export declare const sparkline_geometry: (values: Array<number>, options: SparklineOptions) => SparklineGeometry | null;
|
|
102
|
+
//# sourceMappingURL=sparkline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sparkline.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/sparkline.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;AAEhE,MAAM,WAAW,gBAAgB;IAChC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IACjC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;IAC5B;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;CACnB;AAID;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC9B,QAAQ,KAAK,CAAC,MAAM,CAAC,EACrB,SAAS,gBAAgB,KACvB,iBAAiB,GAAG,IA8DtB,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure geometry helpers behind `Sparkline` and `ProjectActivityChart`.
|
|
3
|
+
*
|
|
4
|
+
* Computes polyline points, alpha-fade gradient stops, and the end-dot
|
|
5
|
+
* position for a series of values, so components stay thin and the math is
|
|
6
|
+
* testable and reusable outside Svelte.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
const round = (n) => Math.round(n * 100) / 100;
|
|
11
|
+
/**
|
|
12
|
+
* Computes the rendering geometry for a sparkline of `values`.
|
|
13
|
+
*
|
|
14
|
+
* Returns `null` when `values` has fewer than 2 points, which cannot form a line.
|
|
15
|
+
*/
|
|
16
|
+
export const sparkline_geometry = (values, options) => {
|
|
17
|
+
if (values.length < 2)
|
|
18
|
+
return null;
|
|
19
|
+
const { width, height, max, scale = 'linear', stroke_width = 2, alpha_floor = 0.4, end_dot = false } = options;
|
|
20
|
+
const local_max = max ?? Math.max(0, ...values);
|
|
21
|
+
const scale_max = local_max > 0 ? local_max : 1;
|
|
22
|
+
const dot_radius = stroke_width;
|
|
23
|
+
// inset by half the stroke (or the dot radius) so the extremes aren't clipped
|
|
24
|
+
const pad = end_dot ? Math.max(stroke_width / 2, dot_radius) : stroke_width / 2;
|
|
25
|
+
const x_coord = (index) => pad + (index * (width - 2 * pad)) / (values.length - 1);
|
|
26
|
+
const y_coord = (value) => {
|
|
27
|
+
const clamped = Math.min(value, scale_max);
|
|
28
|
+
const ratio = clamped / scale_max;
|
|
29
|
+
const scaled = scale === 'linear'
|
|
30
|
+
? ratio
|
|
31
|
+
: scale === 'sqrt'
|
|
32
|
+
? Math.sqrt(ratio)
|
|
33
|
+
: scale === 'log'
|
|
34
|
+
? Math.log1p(clamped) / Math.log1p(scale_max)
|
|
35
|
+
: ratio ** scale;
|
|
36
|
+
return height - pad - scaled * (height - 2 * pad);
|
|
37
|
+
};
|
|
38
|
+
const alpha = (value) => alpha_floor + (1 - alpha_floor) * (Math.min(value, scale_max) / scale_max);
|
|
39
|
+
const points = values
|
|
40
|
+
.map((value, index) => `${round(x_coord(index))},${round(y_coord(value))}`)
|
|
41
|
+
.join(' ');
|
|
42
|
+
const opacities = values.map((value) => round(alpha(value)));
|
|
43
|
+
const stops = [];
|
|
44
|
+
for (let i = 0; i < values.length; i++) {
|
|
45
|
+
const opacity = opacities[i];
|
|
46
|
+
// keep the first and last stops and each run's boundaries; drop run interiors
|
|
47
|
+
if (i > 0 &&
|
|
48
|
+
i < values.length - 1 &&
|
|
49
|
+
opacity === opacities[i - 1] &&
|
|
50
|
+
opacity === opacities[i + 1]) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
stops.push({ offset: round(x_coord(i) / width), opacity });
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
points,
|
|
57
|
+
stops,
|
|
58
|
+
end_x: round(x_coord(values.length - 1)),
|
|
59
|
+
end_y: round(y_coord(values[values.length - 1])),
|
|
60
|
+
end_alpha: opacities[values.length - 1],
|
|
61
|
+
dot_radius
|
|
62
|
+
};
|
|
63
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.207.0",
|
|
4
4
|
"description": "Svelte UI library",
|
|
5
5
|
"tagline": "friendly user zystem",
|
|
6
6
|
"glyph": "🧶",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"jsdom": "^29.1.1",
|
|
82
82
|
"svelte": "^5.56.4",
|
|
83
83
|
"svelte-check": "^4.7.2",
|
|
84
|
-
"svelte-docinfo": "^0.
|
|
84
|
+
"svelte-docinfo": "^0.6.0",
|
|
85
85
|
"tslib": "^2.8.1",
|
|
86
86
|
"typescript": "^5.9.3",
|
|
87
87
|
"typescript-eslint": "^8.60.0",
|
|
@@ -5,7 +5,8 @@ import type {
|
|
|
5
5
|
ParameterJsonInput,
|
|
6
6
|
ComponentPropJsonInput,
|
|
7
7
|
OverloadJsonInput,
|
|
8
|
-
Reactivity
|
|
8
|
+
Reactivity,
|
|
9
|
+
TypeJson
|
|
9
10
|
} from 'svelte-docinfo/types.js';
|
|
10
11
|
import { generateImport, getDisplayName } from 'svelte-docinfo/declaration-helpers.js';
|
|
11
12
|
import { EMPTY_ARRAY } from '@fuzdev/fuz_util/array.ts';
|
|
@@ -122,6 +123,23 @@ export class Declaration {
|
|
|
122
123
|
return this.declaration_json.typeSignature;
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Structured type tree beside `type_signature`, absent when the flat string
|
|
128
|
+
* is the whole story. Present on `variable` and `type` kinds only.
|
|
129
|
+
*/
|
|
130
|
+
get type_info(): TypeJson | undefined {
|
|
131
|
+
return field<TypeJson>(this.declaration_json, 'typeInfo');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Default value documented via `@default`; the authoritative initializer
|
|
136
|
+
* (when human-readable) is in `type_signature`. Present on `variable` kind
|
|
137
|
+
* only — parameters, props, and members carry their own.
|
|
138
|
+
*/
|
|
139
|
+
get default_value(): string | undefined {
|
|
140
|
+
return field<string>(this.declaration_json, 'defaultValue');
|
|
141
|
+
}
|
|
142
|
+
|
|
125
143
|
get doc_comment(): string | undefined {
|
|
126
144
|
return this.declaration_json.docComment;
|
|
127
145
|
}
|
|
@@ -142,6 +160,14 @@ export class Declaration {
|
|
|
142
160
|
return field<string>(this.declaration_json, 'returnType');
|
|
143
161
|
}
|
|
144
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Structured return type beside `return_type`, absent when the flat string
|
|
165
|
+
* is the whole story. Present on `function` kind only.
|
|
166
|
+
*/
|
|
167
|
+
get return_type_info(): TypeJson | undefined {
|
|
168
|
+
return field<TypeJson>(this.declaration_json, 'returnTypeInfo');
|
|
169
|
+
}
|
|
170
|
+
|
|
145
171
|
get return_description(): string | undefined {
|
|
146
172
|
return field<string>(this.declaration_json, 'returnDescription');
|
|
147
173
|
}
|
|
@@ -150,8 +176,8 @@ export class Declaration {
|
|
|
150
176
|
return this.declaration_json.genericParams ?? EMPTY_ARRAY;
|
|
151
177
|
}
|
|
152
178
|
|
|
153
|
-
get
|
|
154
|
-
return field<
|
|
179
|
+
get extends_types(): Array<string> | undefined {
|
|
180
|
+
return field<Array<string>>(this.declaration_json, 'extends');
|
|
155
181
|
}
|
|
156
182
|
|
|
157
183
|
get implements_types(): Array<string> | undefined {
|
|
@@ -182,11 +208,15 @@ export class Declaration {
|
|
|
182
208
|
}
|
|
183
209
|
|
|
184
210
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
211
|
+
* External types whose contributions are filtered out of props/members —
|
|
212
|
+
* the attribute bags a component forwards, however the author composed
|
|
213
|
+
* them (intersection, heritage clause, bare or indexed-access reference).
|
|
214
|
+
* On `interface`/`class` kinds it records the external reach of the
|
|
215
|
+
* heritage chain, whose contributions members (own-only) never enumerate.
|
|
216
|
+
* Present on `component`, `type`, `interface`, and `class` kinds.
|
|
187
217
|
*/
|
|
188
|
-
get
|
|
189
|
-
return field<Array<string>>(this.declaration_json, '
|
|
218
|
+
get external_types(): Array<string> | undefined {
|
|
219
|
+
return field<Array<string>>(this.declaration_json, 'externalTypes');
|
|
190
220
|
}
|
|
191
221
|
|
|
192
222
|
/**
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types and pure helpers for project commit-activity stats.
|
|
3
|
+
*
|
|
4
|
+
* The committed dataset (`project_stats_data.ts`, written by the
|
|
5
|
+
* `project_stats_update` task) stores daily commit counts per project as the
|
|
6
|
+
* master granularity; coarser scopes like monthly are derived at runtime by
|
|
7
|
+
* rebucketing. All date handling is UTC — `YYYY-MM-DD` strings and "epoch
|
|
8
|
+
* days" (days since the Unix epoch) — so results are deterministic across
|
|
9
|
+
* machines.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const DAY_MS = 86_400_000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Daily commit counts for one project.
|
|
18
|
+
*/
|
|
19
|
+
export interface ProjectCommitStats {
|
|
20
|
+
/**
|
|
21
|
+
* Project name, matching `project_items`.
|
|
22
|
+
*/
|
|
23
|
+
name: string;
|
|
24
|
+
/**
|
|
25
|
+
* UTC date (`YYYY-MM-DD`) of the first day bucket, the day of the first commit.
|
|
26
|
+
*/
|
|
27
|
+
start_date: string;
|
|
28
|
+
/**
|
|
29
|
+
* Commits per UTC day from `start_date` through the snapshot's `end_date`, inclusive.
|
|
30
|
+
*/
|
|
31
|
+
daily_counts: Array<number>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A snapshot of commit stats across projects, sharing a single `end_date`.
|
|
36
|
+
*/
|
|
37
|
+
export interface ProjectStatsSnapshot {
|
|
38
|
+
/**
|
|
39
|
+
* ISO datetime when the snapshot was collected.
|
|
40
|
+
*/
|
|
41
|
+
generated_at: string;
|
|
42
|
+
/**
|
|
43
|
+
* UTC date (`YYYY-MM-DD`) of the final day bucket, shared by every project.
|
|
44
|
+
*/
|
|
45
|
+
end_date: string;
|
|
46
|
+
projects: Array<ProjectCommitStats>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A derived series of bucketed counts for one project.
|
|
51
|
+
*/
|
|
52
|
+
export interface ProjectStatsSeries {
|
|
53
|
+
name: string;
|
|
54
|
+
counts: Array<number>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Converts a UTC `YYYY-MM-DD` date to days since the Unix epoch.
|
|
59
|
+
*/
|
|
60
|
+
export const epoch_day_from_date = (date: string): number => {
|
|
61
|
+
const [year, month, day] = date.split('-').map(Number);
|
|
62
|
+
return Date.UTC(year!, month! - 1, day) / DAY_MS;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Converts days since the Unix epoch to a UTC `YYYY-MM-DD` date.
|
|
67
|
+
*/
|
|
68
|
+
export const date_from_epoch_day = (epoch_day: number): string =>
|
|
69
|
+
new Date(epoch_day * DAY_MS).toISOString().slice(0, 10);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Buckets commit timestamps into daily counts from the earliest commit's UTC
|
|
73
|
+
* day through `end_date`, inclusive.
|
|
74
|
+
*
|
|
75
|
+
* @param timestamps - commit times as Unix epoch seconds
|
|
76
|
+
* @param end_date - UTC date (`YYYY-MM-DD`) of the final bucket; later timestamps clamp into it
|
|
77
|
+
* @returns the start date and daily counts, or `null` if `timestamps` is empty
|
|
78
|
+
*/
|
|
79
|
+
export const daily_counts_from_timestamps = (
|
|
80
|
+
timestamps: Array<number>,
|
|
81
|
+
end_date: string
|
|
82
|
+
): { start_date: string; daily_counts: Array<number> } | null => {
|
|
83
|
+
if (timestamps.length === 0) return null;
|
|
84
|
+
const days = timestamps.map((t) => Math.floor((t * 1000) / DAY_MS));
|
|
85
|
+
const end_day = epoch_day_from_date(end_date);
|
|
86
|
+
let start_day = end_day;
|
|
87
|
+
for (const day of days) {
|
|
88
|
+
if (day < start_day) start_day = day;
|
|
89
|
+
}
|
|
90
|
+
const daily_counts: Array<number> = new Array(end_day - start_day + 1).fill(0);
|
|
91
|
+
for (const day of days) {
|
|
92
|
+
daily_counts[Math.min(day, end_day) - start_day]! += 1;
|
|
93
|
+
}
|
|
94
|
+
return { start_date: date_from_epoch_day(start_day), daily_counts };
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Pads `daily_counts` with leading zeros so the series starts at
|
|
99
|
+
* `padded_start_date` instead of `start_date`.
|
|
100
|
+
*
|
|
101
|
+
* Returns the input array unchanged when `padded_start_date` is not earlier
|
|
102
|
+
* than `start_date`.
|
|
103
|
+
*/
|
|
104
|
+
export const daily_counts_pad_start = (
|
|
105
|
+
daily_counts: Array<number>,
|
|
106
|
+
start_date: string,
|
|
107
|
+
padded_start_date: string
|
|
108
|
+
): Array<number> => {
|
|
109
|
+
const pad = epoch_day_from_date(start_date) - epoch_day_from_date(padded_start_date);
|
|
110
|
+
if (pad <= 0) return daily_counts;
|
|
111
|
+
return (new Array(pad).fill(0) as Array<number>).concat(daily_counts);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Rebuckets daily counts into calendar-month (UTC) counts.
|
|
116
|
+
*
|
|
117
|
+
* The first and last buckets may cover partial months.
|
|
118
|
+
*/
|
|
119
|
+
export const monthly_counts_from_daily = (
|
|
120
|
+
daily_counts: Array<number>,
|
|
121
|
+
start_date: string
|
|
122
|
+
): Array<number> => {
|
|
123
|
+
const start_day = epoch_day_from_date(start_date);
|
|
124
|
+
const counts: Array<number> = [];
|
|
125
|
+
let current_month_key: number | null = null;
|
|
126
|
+
for (let i = 0; i < daily_counts.length; i++) {
|
|
127
|
+
const date = new Date((start_day + i) * DAY_MS);
|
|
128
|
+
const month_key = date.getUTCFullYear() * 12 + date.getUTCMonth();
|
|
129
|
+
if (month_key !== current_month_key) {
|
|
130
|
+
current_month_key = month_key;
|
|
131
|
+
counts.push(0);
|
|
132
|
+
}
|
|
133
|
+
counts[counts.length - 1]! += daily_counts[i]!;
|
|
134
|
+
}
|
|
135
|
+
return counts;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Rebuckets daily counts into 7-day counts, anchored at the end of the series
|
|
140
|
+
* so the newest bucket always covers a full 7 days.
|
|
141
|
+
*
|
|
142
|
+
* The first bucket may cover fewer days when the length isn't a multiple of 7.
|
|
143
|
+
*/
|
|
144
|
+
export const weekly_counts_from_daily = (daily_counts: Array<number>): Array<number> => {
|
|
145
|
+
const counts: Array<number> = [];
|
|
146
|
+
for (let end = daily_counts.length; end > 0; end -= 7) {
|
|
147
|
+
let sum = 0;
|
|
148
|
+
for (let i = Math.max(0, end - 7); i < end; i++) {
|
|
149
|
+
sum += daily_counts[i]!;
|
|
150
|
+
}
|
|
151
|
+
counts.push(sum);
|
|
152
|
+
}
|
|
153
|
+
return counts.reverse();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const to_aligned_series = (
|
|
157
|
+
snapshot: ProjectStatsSnapshot,
|
|
158
|
+
rebucket: (daily_counts: Array<number>, start_date: string) => Array<number>
|
|
159
|
+
): { series: Array<ProjectStatsSeries>; max: number } => {
|
|
160
|
+
if (snapshot.projects.length === 0) return { series: [], max: 0 };
|
|
161
|
+
let shared_start = snapshot.projects[0]!.start_date;
|
|
162
|
+
for (const project of snapshot.projects) {
|
|
163
|
+
if (project.start_date < shared_start) shared_start = project.start_date;
|
|
164
|
+
}
|
|
165
|
+
let max = 0;
|
|
166
|
+
const series = snapshot.projects.map((project) => {
|
|
167
|
+
const daily = daily_counts_pad_start(project.daily_counts, project.start_date, shared_start);
|
|
168
|
+
const counts = rebucket(daily, shared_start);
|
|
169
|
+
for (const count of counts) {
|
|
170
|
+
if (count > max) max = count;
|
|
171
|
+
}
|
|
172
|
+
return { name: project.name, counts };
|
|
173
|
+
});
|
|
174
|
+
return { series, max };
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Derives aligned monthly series for every project in `snapshot`, sharing one
|
|
179
|
+
* time window and one scale.
|
|
180
|
+
*
|
|
181
|
+
* Each series spans from the earliest project's `start_date` through the
|
|
182
|
+
* snapshot's `end_date` (younger projects are zero-padded at the front), so
|
|
183
|
+
* all series have the same length and bucket boundaries. `max` is the largest
|
|
184
|
+
* monthly count across all projects, for rendering every series at the same
|
|
185
|
+
* vertical scale.
|
|
186
|
+
*/
|
|
187
|
+
export const project_stats_to_monthly_series = (
|
|
188
|
+
snapshot: ProjectStatsSnapshot
|
|
189
|
+
): { series: Array<ProjectStatsSeries>; max: number } =>
|
|
190
|
+
to_aligned_series(snapshot, monthly_counts_from_daily);
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Derives aligned weekly series for every project in `snapshot`, sharing one
|
|
194
|
+
* time window and one scale — the weekly sibling of
|
|
195
|
+
* `project_stats_to_monthly_series`.
|
|
196
|
+
*/
|
|
197
|
+
export const project_stats_to_weekly_series = (
|
|
198
|
+
snapshot: ProjectStatsSnapshot
|
|
199
|
+
): { series: Array<ProjectStatsSeries>; max: number } =>
|
|
200
|
+
to_aligned_series(snapshot, weekly_counts_from_daily);
|
|
201
|
+
|
|
202
|
+
const start_date_formatter = new Intl.DateTimeFormat('en-US', {
|
|
203
|
+
month: 'long',
|
|
204
|
+
day: 'numeric',
|
|
205
|
+
year: 'numeric',
|
|
206
|
+
timeZone: 'UTC'
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Formats a one-line human-readable summary of a project's commit stats,
|
|
211
|
+
* e.g. `"gro — 2,386 commits since August 7, 2019"`. Used for accessible
|
|
212
|
+
* labels and tooltips.
|
|
213
|
+
*/
|
|
214
|
+
export const project_stats_label = (project: ProjectCommitStats): string => {
|
|
215
|
+
let total = 0;
|
|
216
|
+
for (const count of project.daily_counts) {
|
|
217
|
+
total += count;
|
|
218
|
+
}
|
|
219
|
+
return `${project.name} — ${total.toLocaleString('en-US')} commits since ${start_date_formatter.format(new Date(project.start_date))}`;
|
|
220
|
+
};
|