@loadstrike/loadstrike-sdk 1.0.30401 → 1.0.31601
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/README.md +6 -0
- package/dist/cjs/iteration-observation-diagnostics.js +513 -0
- package/dist/cjs/iteration-observations.js +195 -22
- package/dist/cjs/local-report-input.js +21 -0
- package/dist/cjs/local.js +55 -10
- package/dist/cjs/report-history.js +421 -0
- package/dist/cjs/reporting-svg.js +116 -0
- package/dist/cjs/reporting.js +436 -140
- package/dist/cjs/runtime.js +313 -85
- package/dist/cjs/sink-retry-policy.js +52 -0
- package/dist/cjs/sinks.js +112 -9
- package/dist/cjs/transports.js +78 -25
- package/dist/esm/iteration-observation-diagnostics.js +508 -0
- package/dist/esm/iteration-observations.js +195 -22
- package/dist/esm/local-report-input.js +17 -0
- package/dist/esm/local.js +55 -10
- package/dist/esm/report-history.js +413 -0
- package/dist/esm/reporting-svg.js +113 -0
- package/dist/esm/reporting.js +436 -140
- package/dist/esm/runtime.js +313 -85
- package/dist/esm/sink-retry-policy.js +44 -0
- package/dist/esm/sinks.js +112 -9
- package/dist/esm/transports.js +78 -25
- package/dist/types/iteration-observation-diagnostics.d.ts +21 -0
- package/dist/types/iteration-observations.d.ts +5 -0
- package/dist/types/local-report-input.d.ts +6 -0
- package/dist/types/report-history.d.ts +124 -0
- package/dist/types/reporting-svg.d.ts +2 -0
- package/dist/types/reporting.d.ts +6 -3
- package/dist/types/runtime.d.ts +25 -0
- package/dist/types/sink-retry-policy.d.ts +9 -0
- package/dist/types/sinks.d.ts +6 -0
- package/dist/types/transports.d.ts +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReportHistoryWorker = exports.ReportHistoryLifecycleCoordinator = exports.ReportHistoryCollector = exports.REPORT_HISTORY_MAX_SCALAR_VALUES = exports.REPORT_HISTORY_MAX_POINTS = void 0;
|
|
4
|
+
exports.sanitizedExceptionClassChain = sanitizedExceptionClassChain;
|
|
5
|
+
exports.deriveScenarioRates = deriveScenarioRates;
|
|
6
|
+
exports.REPORT_HISTORY_MAX_POINTS = 2048;
|
|
7
|
+
exports.REPORT_HISTORY_MAX_SCALAR_VALUES = 262144;
|
|
8
|
+
/**
|
|
9
|
+
* Bounded report-only cumulative telemetry. This type is intentionally not
|
|
10
|
+
* exported from the package entry point and never participates in public run,
|
|
11
|
+
* sink, observation, or cluster payloads.
|
|
12
|
+
*/
|
|
13
|
+
class ReportHistoryCollector {
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.points = [];
|
|
16
|
+
this.terminalCaptured = false;
|
|
17
|
+
const scenarioCount = Math.max(1, Math.trunc(options.scenarioCount));
|
|
18
|
+
const scalarValuesPerPoint = scenarioCount * 16;
|
|
19
|
+
const budgetPointLimit = Math.floor(exports.REPORT_HISTORY_MAX_SCALAR_VALUES / scalarValuesPerPoint);
|
|
20
|
+
if (!Number.isSafeInteger(scalarValuesPerPoint) || budgetPointLimit < 2) {
|
|
21
|
+
this.pointLimit = 0;
|
|
22
|
+
this.reasonCategory = "budget_pressure";
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const requestedLimit = options.maxPoints === undefined
|
|
26
|
+
? exports.REPORT_HISTORY_MAX_POINTS
|
|
27
|
+
: Math.max(2, Math.trunc(options.maxPoints));
|
|
28
|
+
this.pointLimit = Math.min(exports.REPORT_HISTORY_MAX_POINTS, requestedLimit, Math.max(2, budgetPointLimit));
|
|
29
|
+
}
|
|
30
|
+
this.nowNs = options.nowNs ?? (() => process.hrtime.bigint());
|
|
31
|
+
this.onCaptureError = options.onCaptureError;
|
|
32
|
+
}
|
|
33
|
+
get available() {
|
|
34
|
+
return this.reasonCategory === undefined;
|
|
35
|
+
}
|
|
36
|
+
start(startNs = this.nowNs()) {
|
|
37
|
+
if (!this.available || this.startNs !== undefined) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.startNs = startNs;
|
|
41
|
+
}
|
|
42
|
+
capture(snapshotFactory) {
|
|
43
|
+
return this.captureSafely(snapshotFactory, false);
|
|
44
|
+
}
|
|
45
|
+
finalize(snapshotFactory) {
|
|
46
|
+
return this.captureSafely(snapshotFactory, true);
|
|
47
|
+
}
|
|
48
|
+
disable(reasonCategory) {
|
|
49
|
+
this.reasonCategory = reasonCategory;
|
|
50
|
+
this.points = [];
|
|
51
|
+
}
|
|
52
|
+
toProjection() {
|
|
53
|
+
if (this.reasonCategory !== undefined) {
|
|
54
|
+
return {
|
|
55
|
+
status: "unavailable",
|
|
56
|
+
reasonCategory: this.reasonCategory,
|
|
57
|
+
points: []
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
status: "available",
|
|
62
|
+
points: this.points.map(clonePoint)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
captureSafely(snapshotFactory, terminal) {
|
|
66
|
+
if (!this.available || (this.terminalCaptured && !terminal)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
if (this.startNs === undefined) {
|
|
71
|
+
this.start();
|
|
72
|
+
}
|
|
73
|
+
const nowNs = this.nowNs();
|
|
74
|
+
const elapsedNs = nowNs > this.startNs ? nowNs - this.startNs : 0n;
|
|
75
|
+
const elapsedSeconds = Number(elapsedNs) / 1000000000;
|
|
76
|
+
const scenarios = [...snapshotFactory()]
|
|
77
|
+
.map(projectScenario)
|
|
78
|
+
.sort((left, right) => (left.sortIndex ?? 0) - (right.sortIndex ?? 0)
|
|
79
|
+
|| left.scenarioName.localeCompare(right.scenarioName));
|
|
80
|
+
const point = {
|
|
81
|
+
elapsedSeconds: finiteNonNegative(elapsedSeconds) ?? 0,
|
|
82
|
+
terminal,
|
|
83
|
+
scenarios
|
|
84
|
+
};
|
|
85
|
+
if (terminal) {
|
|
86
|
+
this.points = this.points.filter((existing) => existing.elapsedSeconds <= point.elapsedSeconds);
|
|
87
|
+
this.terminalCaptured = true;
|
|
88
|
+
}
|
|
89
|
+
const existingIndex = this.points.findIndex((existing) => existing.elapsedSeconds === point.elapsedSeconds);
|
|
90
|
+
if (existingIndex >= 0) {
|
|
91
|
+
if (terminal || !this.points[existingIndex].terminal) {
|
|
92
|
+
this.points[existingIndex] = point;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (point.elapsedSeconds > 0 || terminal) {
|
|
96
|
+
this.points.push(point);
|
|
97
|
+
this.points.sort((left, right) => left.elapsedSeconds - right.elapsedSeconds);
|
|
98
|
+
}
|
|
99
|
+
this.compact();
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
this.disable("capture_failure");
|
|
104
|
+
try {
|
|
105
|
+
this.onCaptureError?.(error);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Report diagnostics are best-effort and must never affect the run.
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
compact() {
|
|
114
|
+
if (this.pointLimit === 2 && this.points.length > 2) {
|
|
115
|
+
this.points = [this.points[0], this.points[this.points.length - 1]];
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
while (this.points.length > this.pointLimit) {
|
|
119
|
+
const newest = this.points[this.points.length - 1];
|
|
120
|
+
const thinned = [this.points[0]];
|
|
121
|
+
for (let index = 1; index < this.points.length - 1; index += 2) {
|
|
122
|
+
thinned.push(this.points[index]);
|
|
123
|
+
}
|
|
124
|
+
if (thinned[thinned.length - 1] !== newest) {
|
|
125
|
+
thinned.push(newest);
|
|
126
|
+
}
|
|
127
|
+
this.points = thinned;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
exports.ReportHistoryCollector = ReportHistoryCollector;
|
|
132
|
+
/**
|
|
133
|
+
* Coordinates one private history worker across every local scenario. The
|
|
134
|
+
* first measured phase establishes the history clock, and the last scenario
|
|
135
|
+
* leaving execution records the terminal point before scenario cleanup.
|
|
136
|
+
*/
|
|
137
|
+
class ReportHistoryLifecycleCoordinator {
|
|
138
|
+
constructor(worker) {
|
|
139
|
+
this.worker = worker;
|
|
140
|
+
this.measuredLoadStarted = false;
|
|
141
|
+
this.endedScenarios = 0;
|
|
142
|
+
this.completed = false;
|
|
143
|
+
this.terminalBoundaryReleased = false;
|
|
144
|
+
this.scenarioCount = Math.max(1, Math.trunc(worker.scenarioCount ?? 1));
|
|
145
|
+
this.terminalBoundary = new Promise((resolve) => {
|
|
146
|
+
this.releaseTerminalBoundary = resolve;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
scenarioBombingStarted() {
|
|
150
|
+
if (this.completed || this.measuredLoadStarted) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this.measuredLoadStarted = true;
|
|
154
|
+
try {
|
|
155
|
+
this.worker.start();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// The private report-only worker cannot fail the measured run.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async scenarioExecutionEnded() {
|
|
162
|
+
if (this.completed) {
|
|
163
|
+
await this.terminalBoundary;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.endedScenarios += 1;
|
|
167
|
+
if (this.endedScenarios < this.scenarioCount) {
|
|
168
|
+
await this.terminalBoundary;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.completed = true;
|
|
172
|
+
try {
|
|
173
|
+
if (this.measuredLoadStarted) {
|
|
174
|
+
this.worker.stopAndFinalize();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// The private report-only worker cannot fail scenario completion.
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
this.releaseBoundary();
|
|
182
|
+
}
|
|
183
|
+
await this.terminalBoundary;
|
|
184
|
+
}
|
|
185
|
+
stopWithoutFinalizing() {
|
|
186
|
+
if (this.completed) {
|
|
187
|
+
this.releaseBoundary();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.completed = true;
|
|
191
|
+
try {
|
|
192
|
+
if (this.measuredLoadStarted) {
|
|
193
|
+
this.worker.stopWithoutFinalizing();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// The private report-only worker cannot fail run cleanup.
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
this.releaseBoundary();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
releaseBoundary() {
|
|
204
|
+
if (!this.terminalBoundaryReleased) {
|
|
205
|
+
this.terminalBoundaryReleased = true;
|
|
206
|
+
this.releaseTerminalBoundary();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
exports.ReportHistoryLifecycleCoordinator = ReportHistoryLifecycleCoordinator;
|
|
211
|
+
/**
|
|
212
|
+
* Owns the report timer. Every cadence calculation, timeout callback, snapshot,
|
|
213
|
+
* and projection is exception-bounded so report history can never fail a run.
|
|
214
|
+
*/
|
|
215
|
+
class ReportHistoryWorker {
|
|
216
|
+
constructor(options) {
|
|
217
|
+
this.options = options;
|
|
218
|
+
this.cadenceNs = 0n;
|
|
219
|
+
this.nextDeadlineNs = 0n;
|
|
220
|
+
this.running = false;
|
|
221
|
+
this.nowNs = options.nowNs ?? (() => process.hrtime.bigint());
|
|
222
|
+
}
|
|
223
|
+
get started() {
|
|
224
|
+
return this.running;
|
|
225
|
+
}
|
|
226
|
+
start() {
|
|
227
|
+
if (this.running || !this.options.collector.available) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
this.cadenceNs = cadenceNanoseconds(this.options.cadenceSeconds);
|
|
232
|
+
const nowNs = this.nowNs();
|
|
233
|
+
this.options.collector.start(nowNs);
|
|
234
|
+
this.nextDeadlineNs = nowNs + this.cadenceNs;
|
|
235
|
+
this.running = true;
|
|
236
|
+
this.scheduleNext();
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
this.fail(error);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
stopAndFinalize() {
|
|
243
|
+
if (!this.running) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
this.running = false;
|
|
247
|
+
if (this.timer !== undefined) {
|
|
248
|
+
clearTimeout(this.timer);
|
|
249
|
+
this.timer = undefined;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
this.options.collector.finalize(this.options.snapshotFactory);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
this.fail(error);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
stopWithoutFinalizing() {
|
|
259
|
+
this.running = false;
|
|
260
|
+
if (this.timer !== undefined) {
|
|
261
|
+
clearTimeout(this.timer);
|
|
262
|
+
this.timer = undefined;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
scheduleNext() {
|
|
266
|
+
try {
|
|
267
|
+
if (!this.running || !this.options.collector.available) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const remainingNs = this.nextDeadlineNs - this.nowNs();
|
|
271
|
+
const delayMs = safeTimeoutDelayMs(remainingNs);
|
|
272
|
+
this.timer = setTimeout(() => this.onTimer(), delayMs);
|
|
273
|
+
if (typeof this.timer.unref === "function") {
|
|
274
|
+
this.timer.unref();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
this.fail(error);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
onTimer() {
|
|
282
|
+
try {
|
|
283
|
+
this.timer = undefined;
|
|
284
|
+
if (!this.running || !this.options.collector.available) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const nowNs = this.nowNs();
|
|
288
|
+
if (nowNs < this.nextDeadlineNs) {
|
|
289
|
+
this.scheduleNext();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
this.options.collector.capture(this.options.snapshotFactory);
|
|
293
|
+
if (!this.options.collector.available) {
|
|
294
|
+
this.running = false;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
do {
|
|
298
|
+
this.nextDeadlineNs += this.cadenceNs;
|
|
299
|
+
} while (this.nextDeadlineNs <= nowNs);
|
|
300
|
+
this.scheduleNext();
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
this.fail(error);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
fail(error) {
|
|
307
|
+
this.stopWithoutFinalizing();
|
|
308
|
+
this.options.collector.disable("capture_failure");
|
|
309
|
+
try {
|
|
310
|
+
this.options.onFailure?.("capture_failure", sanitizedExceptionClassChain(error));
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// Logging a report-only warning is best-effort.
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
exports.ReportHistoryWorker = ReportHistoryWorker;
|
|
318
|
+
function sanitizedExceptionClassChain(error) {
|
|
319
|
+
const classes = [];
|
|
320
|
+
const seen = new Set();
|
|
321
|
+
let current = error;
|
|
322
|
+
while (current !== undefined && current !== null && classes.length < 8 && !seen.has(current)) {
|
|
323
|
+
seen.add(current);
|
|
324
|
+
const rawName = typeof current === "object"
|
|
325
|
+
? String(current.constructor?.name ?? "Error")
|
|
326
|
+
: typeof current;
|
|
327
|
+
const safeName = rawName.replace(/[^A-Za-z0-9_.$-]/g, "_").slice(0, 64) || "Error";
|
|
328
|
+
classes.push(safeName);
|
|
329
|
+
current = typeof current === "object"
|
|
330
|
+
? current.cause
|
|
331
|
+
: undefined;
|
|
332
|
+
}
|
|
333
|
+
return classes.join(" -> ") || "Error";
|
|
334
|
+
}
|
|
335
|
+
function cadenceNanoseconds(seconds) {
|
|
336
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
337
|
+
throw new RangeError("Invalid report history cadence.");
|
|
338
|
+
}
|
|
339
|
+
const safelyRepresentableSeconds = Math.min(seconds, Number.MAX_SAFE_INTEGER);
|
|
340
|
+
const wholeSeconds = Math.trunc(safelyRepresentableSeconds);
|
|
341
|
+
const fractionalNanoseconds = Math.round(Math.max(0, safelyRepresentableSeconds - wholeSeconds) * 1000000000);
|
|
342
|
+
const cadence = BigInt(wholeSeconds) * 1000000000n + BigInt(fractionalNanoseconds);
|
|
343
|
+
if (cadence <= 0n) {
|
|
344
|
+
throw new RangeError("Report history cadence is below one nanosecond.");
|
|
345
|
+
}
|
|
346
|
+
return cadence;
|
|
347
|
+
}
|
|
348
|
+
function safeTimeoutDelayMs(remainingNs) {
|
|
349
|
+
if (remainingNs <= 0n) {
|
|
350
|
+
return 1;
|
|
351
|
+
}
|
|
352
|
+
const milliseconds = (remainingNs + 999999n) / 1000000n;
|
|
353
|
+
return Number(milliseconds > 2147483647n ? 2147483647n : milliseconds);
|
|
354
|
+
}
|
|
355
|
+
function deriveScenarioRates(points) {
|
|
356
|
+
const scenarioNames = [...new Set(points.flatMap((point) => point.scenarios.map((scenario) => scenario.scenarioName)))];
|
|
357
|
+
return scenarioNames.map((scenarioName) => {
|
|
358
|
+
let previousElapsed = 0;
|
|
359
|
+
let previousCount = 0;
|
|
360
|
+
return {
|
|
361
|
+
scenarioName,
|
|
362
|
+
values: points.map((point) => {
|
|
363
|
+
const scenario = point.scenarios.find((candidate) => candidate.scenarioName === scenarioName);
|
|
364
|
+
const currentCount = scenario?.all?.count
|
|
365
|
+
?? ((scenario?.ok?.count ?? 0) + (scenario?.failed?.count ?? 0));
|
|
366
|
+
const duration = point.elapsedSeconds - previousElapsed;
|
|
367
|
+
const rate = duration > 0
|
|
368
|
+
? Math.max(0, currentCount - previousCount) / duration
|
|
369
|
+
: null;
|
|
370
|
+
previousElapsed = point.elapsedSeconds;
|
|
371
|
+
previousCount = currentCount;
|
|
372
|
+
return rate === null || !Number.isFinite(rate) ? null : rate;
|
|
373
|
+
})
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
function projectScenario(source) {
|
|
378
|
+
return {
|
|
379
|
+
scenarioName: String(source.scenarioName ?? ""),
|
|
380
|
+
sortIndex: finiteNonNegativeInteger(source.sortIndex),
|
|
381
|
+
...(source.all ? { all: projectMeasurement(source.all) } : {}),
|
|
382
|
+
...(source.ok ? { ok: projectMeasurement(source.ok) } : {}),
|
|
383
|
+
...(source.failed ? { failed: projectMeasurement(source.failed) } : {})
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function projectMeasurement(source) {
|
|
387
|
+
return {
|
|
388
|
+
count: finiteNonNegativeInteger(source.count) ?? 0,
|
|
389
|
+
bytes: finiteNonNegative(source.bytes) ?? 0,
|
|
390
|
+
approximate: source.approximate === true,
|
|
391
|
+
...optionalFinite("percent50Ms", source.percent50Ms),
|
|
392
|
+
...optionalFinite("percent75Ms", source.percent75Ms),
|
|
393
|
+
...optionalFinite("percent95Ms", source.percent95Ms),
|
|
394
|
+
...optionalFinite("percent99Ms", source.percent99Ms)
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function optionalFinite(key, value) {
|
|
398
|
+
const normalized = finiteNonNegative(value);
|
|
399
|
+
return normalized === undefined ? {} : { [key]: normalized };
|
|
400
|
+
}
|
|
401
|
+
function finiteNonNegative(value) {
|
|
402
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
403
|
+
? value
|
|
404
|
+
: undefined;
|
|
405
|
+
}
|
|
406
|
+
function finiteNonNegativeInteger(value) {
|
|
407
|
+
const normalized = finiteNonNegative(value);
|
|
408
|
+
return normalized === undefined ? undefined : Math.trunc(normalized);
|
|
409
|
+
}
|
|
410
|
+
function clonePoint(point) {
|
|
411
|
+
return {
|
|
412
|
+
elapsedSeconds: point.elapsedSeconds,
|
|
413
|
+
terminal: point.terminal,
|
|
414
|
+
scenarios: point.scenarios.map((scenario) => ({
|
|
415
|
+
...scenario,
|
|
416
|
+
...(scenario.all ? { all: { ...scenario.all } } : {}),
|
|
417
|
+
...(scenario.ok ? { ok: { ...scenario.ok } } : {}),
|
|
418
|
+
...(scenario.failed ? { failed: { ...scenario.failed } } : {})
|
|
419
|
+
}))
|
|
420
|
+
};
|
|
421
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REPORT_SVG_SCRIPT = exports.REPORT_SVG_CSS = void 0;
|
|
4
|
+
exports.REPORT_SVG_CSS = String.raw `
|
|
5
|
+
.chart-collection{--chart-min:360px}
|
|
6
|
+
.chart-collection[data-grid-size='compact']{--chart-min:260px}
|
|
7
|
+
.chart-collection[data-grid-size='comfortable']{--chart-min:360px}
|
|
8
|
+
.chart-collection[data-grid-size='spacious']{--chart-min:520px}
|
|
9
|
+
.chart-collection .chart-grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--chart-min)),1fr))}
|
|
10
|
+
.chart-collection .correlation-chart-grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,420px),720px));justify-content:center}
|
|
11
|
+
.chart-collection-tools{display:flex;align-items:end;gap:10px;flex-wrap:wrap;margin:0 0 12px}
|
|
12
|
+
.chart-collection-tools label{display:grid;gap:4px;color:var(--muted);font-size:12px}
|
|
13
|
+
.chart-collection-tools input,.chart-collection-tools select{min-height:34px;border:1px solid var(--line);border-radius:7px;background:var(--panel);color:var(--text);padding:5px 9px}
|
|
14
|
+
.chart-empty{display:none;color:var(--muted);padding:12px;border:1px dashed var(--line);border-radius:8px}
|
|
15
|
+
.chart-empty.visible{display:block}
|
|
16
|
+
.chart-card[hidden]{display:none}
|
|
17
|
+
.chart-actions{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:7px}
|
|
18
|
+
.chart-action,.chart-legend button,.chart-modal-close{border:1px solid #53657c;border-radius:6px;background:#17243a;color:#e6edf3;min-height:30px;padding:4px 8px;cursor:pointer}
|
|
19
|
+
.chart-action:disabled{opacity:.42;cursor:not-allowed}
|
|
20
|
+
.chart-action:focus-visible,.chart-legend button:focus-visible,.chart-modal-close:focus-visible,.chart-canvas:focus-visible{outline:3px solid #60a5fa;outline-offset:2px}
|
|
21
|
+
.chart-host{position:relative;width:100%}
|
|
22
|
+
.chart-canvas{width:100%;height:auto;aspect-ratio:720/320;display:block;color:#dbe6f4}
|
|
23
|
+
.correlation-chart-card .chart-canvas{aspect-ratio:720/320}
|
|
24
|
+
.chart-legend{display:flex;gap:7px;flex-wrap:wrap;margin:7px 0 0}
|
|
25
|
+
.chart-legend button{display:inline-flex;align-items:center;gap:6px;font-size:12px}
|
|
26
|
+
.chart-legend button[aria-pressed='false']{opacity:.55;text-decoration:line-through}
|
|
27
|
+
.chart-legend-swatch{width:10px;height:10px;border-radius:999px;background:var(--series-color)}
|
|
28
|
+
.chart-tooltip{position:absolute;z-index:5;pointer-events:none;max-width:min(340px,85%);padding:7px 9px;border:1px solid #64748b;border-radius:7px;background:#020617;color:#f8fafc;font-size:12px;white-space:pre-line;box-shadow:0 8px 18px rgba(0,0,0,.35);transform:translate(10px,-110%)}
|
|
29
|
+
.chart-tooltip[hidden]{display:none}
|
|
30
|
+
.chart-modal{position:fixed;inset:0;z-index:2000;display:none;align-items:center;justify-content:center;background:rgba(2,6,23,.86);padding:24px}
|
|
31
|
+
.chart-modal.open{display:flex}
|
|
32
|
+
.chart-modal-panel{width:min(1280px,96vw);max-height:94vh;overflow:auto;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px}
|
|
33
|
+
.chart-modal-head{display:flex;justify-content:flex-end;margin-bottom:5px}
|
|
34
|
+
.chart-modal .chart-card{max-width:none;margin:0}
|
|
35
|
+
.chart-modal .chart-canvas{width:100%;height:auto;aspect-ratio:720/320}
|
|
36
|
+
body[data-chart-modal-open='true']{overflow:hidden}
|
|
37
|
+
@media print{.tabs-pane,.theme-toggle-report,.chart-actions,.chart-collection-tools,.chart-modal{display:none!important}.tab{display:block!important}.chart-card[hidden]{display:block!important}.report-layout{display:block}}
|
|
38
|
+
@media (max-width:980px){.chart-collection{--chart-min:100%}}
|
|
39
|
+
`;
|
|
40
|
+
exports.REPORT_SVG_SCRIPT = String.raw `
|
|
41
|
+
const btns=[...document.querySelectorAll('.tab-btn')];
|
|
42
|
+
const tabSections=[...document.querySelectorAll('.tab')];
|
|
43
|
+
const tabsPane=document.getElementById('tab-pane');
|
|
44
|
+
const reportThemeKey='loadstrike-report-theme';
|
|
45
|
+
const reportThemeToggle=document.querySelector('[data-report-theme-toggle]');
|
|
46
|
+
const reportLogo=document.querySelector('[data-report-logo]');
|
|
47
|
+
const SVG_NS='http://www.w3.org/2000/svg';
|
|
48
|
+
const SVG_ELEMENTS=new Set(['g','line','rect','path','circle','text','title']);
|
|
49
|
+
const SVG_ATTRIBUTES=new Set(['x','y','x1','y1','x2','y2','width','height','rx','ry','d','cx','cy','r','fill','stroke','stroke-width','stroke-dasharray','text-anchor','font-size','font-weight','opacity','transform','class','aria-hidden']);
|
|
50
|
+
const chartStates=new WeakMap();
|
|
51
|
+
const chartTouchSelections=new WeakMap();
|
|
52
|
+
const chartPrintStates=new Map();
|
|
53
|
+
const linePalette=['#38bdf8','#22c55e','#f59e0b','#a855f7','#f43f5e','#14b8a6','#eab308','#818cf8','#06b6d4','#84cc16'];
|
|
54
|
+
const modal=document.querySelector('[data-chart-fullscreen-overlay]');
|
|
55
|
+
const modalPanel=modal&&modal.querySelector('[data-chart-modal-panel]');
|
|
56
|
+
let expandedCard=null;
|
|
57
|
+
let expandedPlaceholder=null;
|
|
58
|
+
let expandedTrigger=null;
|
|
59
|
+
let printExpandedCard=false;
|
|
60
|
+
let printExpandedFocus=null;
|
|
61
|
+
function safeColor(value,fallback){const text=(value||'').toString();return /^#[0-9a-f]{6}$/i.test(text)?text:fallback;}
|
|
62
|
+
function svgNode(name,attributes,text){if(!SVG_ELEMENTS.has(name))throw new Error('Unsupported SVG element');const node=document.createElementNS(SVG_NS,name);Object.entries(attributes||{}).forEach(([key,value])=>{if(!SVG_ATTRIBUTES.has(key))throw new Error('Unsupported SVG attribute');node.setAttribute(key,String(value));});if(text!==undefined&&text!==null)node.textContent=String(text);return node;}
|
|
63
|
+
function finiteValue(value,unit){if(value===null||value===undefined||value==='')return null;const number=Number(value);if(!Number.isFinite(number)||number<0)return null;if(unit==='%'&&number>100)return null;return number;}
|
|
64
|
+
function fullMetric(value){return Number.isFinite(value)?String(value):'n/a';}
|
|
65
|
+
function axisMetric(value){if(!Number.isFinite(value))return '';const absolute=Math.abs(value);if(absolute>=1000000000)return (value/1000000000).toFixed(1)+'B';if(absolute>=1000000)return (value/1000000).toFixed(1)+'M';if(absolute>=1000)return (value/1000).toFixed(1)+'K';return absolute>=100?value.toFixed(0):String(Number(value.toFixed(2)));}
|
|
66
|
+
function maximumFinite(values,fallback){let maximum=0;for(const value of values){if(Number.isFinite(value)&&value>maximum)maximum=value;}return maximum>0?maximum:(Number.isFinite(fallback)&&fallback>0?fallback:1);}
|
|
67
|
+
function applyReportTheme(theme){const normalized=theme==='dark'?'dark':'light';document.body.setAttribute('data-theme',normalized);if(reportLogo){const lightLogo=reportLogo.dataset.logoLight||reportLogo.getAttribute('src');const darkLogo=reportLogo.dataset.logoDark||reportLogo.getAttribute('src');reportLogo.setAttribute('src',normalized==='dark'?darkLogo:lightLogo);}if(reportThemeToggle){const darkActive=normalized==='dark';const nextLabel=darkActive?'light':'dark';reportThemeToggle.textContent=darkActive?'\u2600':'\u263e';reportThemeToggle.setAttribute('aria-pressed',darkActive?'true':'false');reportThemeToggle.setAttribute('aria-label','Switch to '+nextLabel+' theme');reportThemeToggle.setAttribute('title','Switch to '+nextLabel+' theme');}}
|
|
68
|
+
function show(id){btns.forEach(button=>button.classList.toggle('active',button.dataset.tab===id));tabSections.forEach(tab=>tab.classList.toggle('active',tab.id===id));}
|
|
69
|
+
function readJsonNode(selector){const node=document.querySelector(selector);if(!node)return {labels:[],series:[]};try{return JSON.parse(node.textContent||'{}');}catch{return {labels:[],series:[]};}}
|
|
70
|
+
function rawChart(host){const source=host.dataset.chartSource||'';if(source==='grouped-correlation'){const key=host.dataset.groupedKey||'';return readJsonNode('script[type="application/json"][data-grouped-correlation-chart="'+CSS.escape(key)+'"]');}if(source==='ungrouped-correlation'){const key=host.dataset.ungroupedKey||'';return readJsonNode('script[type="application/json"][data-ungrouped-correlation="'+CSS.escape(key)+'"]');}if(source==='historyLatencyCharts'){const index=Number.parseInt(host.dataset.chartIndex||'-1',10);const item=Array.isArray(reportCharts.historyLatencyCharts)?reportCharts.historyLatencyCharts[index]:null;return item&&item.chart;}return reportCharts[source];}
|
|
71
|
+
function normalizeChart(host){const raw=rawChart(host);const kind=host.dataset.chartKind||'line';const unit=host.dataset.chartUnit||'';if(Array.isArray(raw)){if(kind==='donut'){return {labels:['Total'],series:raw.map((point,index)=>({name:String(point&&point.label||'Series '+(index+1)),color:safeColor(point&&point.color,linePalette[index%linePalette.length]),values:[finiteValue(point&&point.value,unit)]}))};}return {labels:raw.map(point=>String(point&&point.label||'')),series:[{name:host.dataset.seriesLabel||'Value',color:safeColor(raw[0]&&raw[0].color,linePalette[0]),values:raw.map(point=>finiteValue(point&&point.value,unit))}]};}const labels=Array.isArray(raw&&raw.labels)?raw.labels.map(value=>String(value)):[];const series=Array.isArray(raw&&raw.series)?raw.series.map((item,index)=>({name:String(item&&item.name||'Series '+(index+1)),color:safeColor(item&&item.color,linePalette[index%linePalette.length]),values:Array.isArray(item&&item.values)?item.values.map(value=>finiteValue(value,unit)):[]})):[];return {labels,series};}
|
|
72
|
+
function stateFor(host,labelCount){let state=chartStates.get(host);if(!state){state={start:0,end:Math.max(0,labelCount-1),hidden:new Set(),active:0};chartStates.set(host,state);}state.end=Math.min(Math.max(state.start,state.end),Math.max(0,labelCount-1));state.start=Math.min(state.start,state.end);state.active=Math.min(Math.max(state.start,state.active),state.end);return state;}
|
|
73
|
+
function chartSvg(host){return host.querySelector('svg.chart-canvas');}
|
|
74
|
+
function tooltipFor(host){return host.querySelector('[data-chart-tooltip]');}
|
|
75
|
+
function hideTooltip(host){const tooltip=tooltipFor(host);if(tooltip)tooltip.hidden=true;const marker=chartSvg(host)&&chartSvg(host).querySelector('.chart-active-marker');if(marker)marker.setAttribute('opacity','0');}
|
|
76
|
+
function pointIndexFor(host,state,ratio){const domain=Math.max(0,state.end-state.start);if(host.dataset.chartKind==='bar'){const count=domain+1;return state.start+Math.min(count-1,Math.floor(ratio*count));}return state.start+Math.round(ratio*domain);}
|
|
77
|
+
function pointerPlotRatio(svg,clientX){const bounds=svg.getBoundingClientRect();const viewX=(clientX-bounds.left)/Math.max(1,bounds.width)*720;return Math.max(0,Math.min(1,(viewX-58)/644));}
|
|
78
|
+
function showTooltip(host,chart,state,index,clientX,clientY){if(index<state.start||index>state.end)return;const visible=chart.series.filter((series,seriesIndex)=>!state.hidden.has(seriesIndex));const rows=visible.map(series=>{const value=series.values[index];return Number.isFinite(value)?series.name+': '+fullMetric(value)+(host.dataset.chartUnit?' '+host.dataset.chartUnit:''):null;}).filter(Boolean);if(rows.length===0){hideTooltip(host);return;}const tooltip=tooltipFor(host);if(!tooltip)return;tooltip.textContent=[chart.labels[index]||'Point '+(index+1),...rows].join('\n');tooltip.hidden=false;const bounds=host.getBoundingClientRect();const localX=Number.isFinite(clientX)?clientX-bounds.left:bounds.width/2;const localY=Number.isFinite(clientY)?clientY-bounds.top:bounds.height/2;tooltip.style.left=Math.max(0,Math.min(bounds.width-20,localX))+'px';tooltip.style.top=Math.max(20,localY)+'px';const svg=chartSvg(host);const marker=svg&&svg.querySelector('.chart-active-marker');if(marker){const first=visible.map(series=>series.values[index]).find(Number.isFinite);if(Number.isFinite(first)){const domain=state.end-state.start;const count=Math.max(1,domain+1);const x=host.dataset.chartKind==='bar'?58+(index-state.start+.5)*644/count:58+(domain<=0?322:(index-state.start)*644/domain);const values=visible.flatMap(series=>series.values.slice(state.start,state.end+1)).filter(Number.isFinite);const max=maximumFinite(values,1);const y=270-(first/max)*242;marker.setAttribute('cx',String(x));marker.setAttribute('cy',String(y));marker.setAttribute('opacity','1');}}state.active=index;}
|
|
79
|
+
function noData(svg,message){svg.append(svgNode('text',{x:360,y:160,'text-anchor':'middle',fill:'#9fb0c3','font-size':14},message));}
|
|
80
|
+
function drawAxes(svg,max,labels,start,end,unit,categorical){const left=58,right=18,top=28,bottom=50,width=720-left-right,height=320-top-bottom;for(let tick=0;tick<=4;tick++){const y=top+height*tick/4;svg.append(svgNode('line',{x1:left,y1:y,x2:720-right,y2:y,stroke:'#334155','stroke-width':1}));const value=max*(1-tick/4);svg.append(svgNode('text',{x:left-7,y:y+4,'text-anchor':'end',fill:'#b5c2d3','font-size':11},axisMetric(value)));}const count=Math.max(1,end-start+1);const thin=Math.max(1,Math.ceil(count/8));for(let index=start;index<=end;index++){if((index-start)%thin!==0&&index!==end)continue;const x=categorical?left+(index-start+.5)*width/count:(count<=1?left+width/2:left+(index-start)*width/(count-1));svg.append(svgNode('text',{x,y:304,'text-anchor':'middle',fill:'#b5c2d3','font-size':10},String(labels[index]||'').slice(0,24)));}if(unit)svg.append(svgNode('text',{x:10,y:18,fill:'#b5c2d3','font-size':11},unit));return {left,top,width,height};}
|
|
81
|
+
function drawLine(svg,host,chart,state,visible){const values=visible.flatMap(item=>item.series.values.slice(state.start,state.end+1)).filter(Number.isFinite);if(values.length===0){noData(svg,'No data');return;}const max=maximumFinite(values,1);const plot=drawAxes(svg,max,chart.labels,state.start,state.end,host.dataset.chartUnit||'',false);const domain=state.end-state.start;visible.forEach(item=>{let path='',started=false,isolatedGlyphPath='';for(let index=state.start;index<=state.end;index++){const value=item.series.values[index];if(!Number.isFinite(value)){started=false;continue;}const x=plot.left+(domain<=0?plot.width/2:(index-state.start)*plot.width/domain);const y=plot.top+plot.height-(value/max)*plot.height;path+=(started?' L ':' M ')+x.toFixed(2)+' '+y.toFixed(2);started=true;const previousFinite=index>state.start&&Number.isFinite(item.series.values[index-1]);const nextFinite=index<state.end&&Number.isFinite(item.series.values[index+1]);if(!previousFinite&&!nextFinite)isolatedGlyphPath+=' M '+(x-3).toFixed(2)+' '+y.toFixed(2)+' L '+x.toFixed(2)+' '+(y-3).toFixed(2)+' L '+(x+3).toFixed(2)+' '+y.toFixed(2)+' L '+x.toFixed(2)+' '+(y+3).toFixed(2)+' Z';}if(path)svg.append(svgNode('path',{d:path.trim(),fill:'none',stroke:item.series.color,'stroke-width':2.4}));if(isolatedGlyphPath)svg.append(svgNode('path',{d:isolatedGlyphPath.trim(),fill:item.series.color,stroke:'#f8fafc','stroke-width':1.5,class:'chart-isolated-points','aria-hidden':'true'}));});svg.append(svgNode('circle',{cx:0,cy:0,r:5,fill:'#020617',stroke:'#f8fafc','stroke-width':2,opacity:0,class:'chart-active-marker','aria-hidden':'true'}));}
|
|
82
|
+
function drawBars(svg,host,chart,state,visible){const values=visible.flatMap(item=>item.series.values.slice(state.start,state.end+1)).filter(Number.isFinite);if(values.length===0){noData(svg,'No data');return;}const max=maximumFinite(values,1);const plot=drawAxes(svg,max,chart.labels,state.start,state.end,host.dataset.chartUnit||'',true);const count=Math.max(1,state.end-state.start+1);const groupWidth=plot.width/count;const barWidth=Math.max(2,Math.min(42,groupWidth*.72/Math.max(1,visible.length)));for(let index=state.start;index<=state.end;index++){visible.forEach((item,visibleIndex)=>{const value=item.series.values[index];if(!Number.isFinite(value))return;const height=value/max*plot.height;const x=plot.left+(index-state.start)*groupWidth+(groupWidth-visible.length*barWidth)/2+visibleIndex*barWidth;svg.append(svgNode('rect',{x:x.toFixed(2),y:(plot.top+plot.height-height).toFixed(2),width:Math.max(1,barWidth-2).toFixed(2),height:height.toFixed(2),rx:2,fill:item.series.color}));});}svg.append(svgNode('circle',{cx:0,cy:0,r:5,fill:'#020617',stroke:'#f8fafc','stroke-width':2,opacity:0,class:'chart-active-marker','aria-hidden':'true'}));}
|
|
83
|
+
function arcPath(cx,cy,outer,inner,start,end){const large=end-start>Math.PI?1:0;const point=(radius,angle)=>[cx+radius*Math.cos(angle),cy+radius*Math.sin(angle)];const a=point(outer,start),b=point(outer,end),c=point(inner,end),d=point(inner,start);return 'M '+a[0]+' '+a[1]+' A '+outer+' '+outer+' 0 '+large+' 1 '+b[0]+' '+b[1]+' L '+c[0]+' '+c[1]+' A '+inner+' '+inner+' 0 '+large+' 0 '+d[0]+' '+d[1]+' Z';}
|
|
84
|
+
function donutLegendSeriesParticipates(series){return Number.isFinite(series.values[0]);}
|
|
85
|
+
function donutSeriesHasArc(series){return donutLegendSeriesParticipates(series)&&series.values[0]>0;}
|
|
86
|
+
function positiveDonutSlices(items){return items.map(item=>({item,value:item.series.values[0]})).filter(slice=>donutSeriesHasArc(slice.item.series));}
|
|
87
|
+
function donutArcSpan(value,total,sliceCount){return sliceCount===1?Math.PI*2-.00001:value/total*Math.PI*2;}
|
|
88
|
+
function donutLegendLabel(name,value,total){const percent=total>0?Number((value/total*100).toFixed(2)):0;return name+': '+fullMetric(value)+' ('+percent+'%)';}
|
|
89
|
+
function drawDonut(svg,host,chart,state,visible){const slices=positiveDonutSlices(visible);const total=slices.reduce((sum,slice)=>sum+slice.value,0);if(total<=0){noData(svg,'No data');return;}let angle=-Math.PI/2;slices.forEach(slice=>{const delta=donutArcSpan(slice.value,total,slices.length);const next=angle+delta;svg.append(svgNode('path',{d:arcPath(260,158,104,58,angle,next),fill:slice.item.series.color}));angle=next;});svg.append(svgNode('text',{x:260,y:157,'text-anchor':'middle',fill:'#e5eefc','font-size':20,'font-weight':700},fullMetric(total)));svg.append(svgNode('text',{x:260,y:178,'text-anchor':'middle',fill:'#9fb0c3','font-size':11},host.dataset.chartUnit||''));}
|
|
90
|
+
function renderLegend(host,chart,state){const legend=host.querySelector('[data-chart-legend]');if(!legend)return;legend.replaceChildren();const donut=host.dataset.chartKind==='donut';const donutTotal=positiveDonutSlices(chart.series.map((series,index)=>({series,index}))).reduce((sum,slice)=>sum+slice.value,0);const participates=series=>donut?donutLegendSeriesParticipates(series):series.values.some(Number.isFinite);const draws=series=>donut?donutSeriesHasArc(series):participates(series);chart.series.forEach((series,index)=>{if(!participates(series))return;const value=series.values[0];const button=document.createElement('button');button.type='button';button.dataset.seriesIndex=String(index);button.setAttribute('aria-pressed',state.hidden.has(index)?'false':'true');button.setAttribute('aria-label',(state.hidden.has(index)?'Show ':'Hide ')+series.name+' series');const swatch=document.createElement('span');swatch.className='chart-legend-swatch';swatch.style.setProperty('--series-color',series.color);const label=document.createElement('span');label.textContent=donut?donutLegendLabel(series.name,value,donutTotal):series.name;button.append(swatch,label);button.addEventListener('click',()=>{if(state.hidden.has(index)){state.hidden.delete(index);}else{const visibleCount=chart.series.filter((candidate,candidateIndex)=>!state.hidden.has(candidateIndex)&&draws(candidate)).length;if(draws(series)&&visibleCount<=1)return;state.hidden.add(index);}renderHost(host);const replacement=host.querySelector('[data-chart-legend] button[data-series-index="'+index+'"]');if(replacement)replacement.focus();});legend.append(button);});}
|
|
91
|
+
function updateActions(host,state,labelCount,kind){const card=host.closest('.chart-card');if(!card)return;const zoomable=kind!=='donut'&&labelCount>1;const full=state.start===0&&state.end===Math.max(0,labelCount-1);card.querySelectorAll('[data-chart-action]').forEach(button=>{const action=button.dataset.chartAction;if(action==='expand')button.disabled=false;else if(!zoomable)button.disabled=true;else if(action==='zoom-in')button.disabled=state.end-state.start<2;else if(action==='zoom-out'||action==='reset')button.disabled=full;else if(action==='pan-left')button.disabled=state.start===0;else if(action==='pan-right')button.disabled=state.end===labelCount-1;});}
|
|
92
|
+
function renderHost(host){const svg=chartSvg(host);if(!svg)return;const chart=normalizeChart(host);const state=stateFor(host,chart.labels.length);svg.replaceChildren();svg.append(svgNode('title',{},host.dataset.chartTitle||'LoadStrike chart'));const visible=chart.series.map((series,index)=>({series,index})).filter(item=>!state.hidden.has(item.index)&&item.series.values.some(Number.isFinite));const kind=host.dataset.chartKind||'line';if(chart.labels.length===0||visible.length===0){noData(svg,'No data');}else if(kind==='donut'){drawDonut(svg,host,chart,state,visible);}else if(kind==='bar'){drawBars(svg,host,chart,state,visible);}else{drawLine(svg,host,chart,state,visible);}renderLegend(host,chart,state);updateActions(host,state,chart.labels.length,kind);host.dataset.rendered='true';}
|
|
93
|
+
function renderAllCharts(){document.querySelectorAll('[data-loadstrike-chart-engine="svg-v2"]').forEach(renderHost);}
|
|
94
|
+
function changeViewport(host,action){const chart=normalizeChart(host);const state=stateFor(host,chart.labels.length);const length=chart.labels.length;if(length<=1)return;let span=state.end-state.start+1;if(action==='zoom-in'&&span>2){const next=Math.max(2,Math.ceil(span*.7));state.start+=Math.floor((span-next)/2);state.end=state.start+next-1;}else if(action==='zoom-out'){const next=Math.min(length,Math.ceil(span/0.7));state.start=Math.max(0,state.start-Math.floor((next-span)/2));state.end=Math.min(length-1,state.start+next-1);state.start=Math.max(0,state.end-next+1);}else if(action==='pan-left'&&state.start>0){const shift=Math.max(1,Math.floor(span/4));state.start=Math.max(0,state.start-shift);state.end=state.start+span-1;}else if(action==='pan-right'&&state.end<length-1){const shift=Math.max(1,Math.floor(span/4));state.end=Math.min(length-1,state.end+shift);state.start=state.end-span+1;}else if(action==='reset'){state.start=0;state.end=length-1;}state.active=Math.min(Math.max(state.start,state.active),state.end);hideTooltip(host);renderHost(host);}
|
|
95
|
+
function expandCard(card,trigger){if(!modal||!modalPanel||expandedCard)return;expandedCard=card;expandedTrigger=trigger;expandedPlaceholder=document.createComment('loadstrike-chart-placeholder');card.before(expandedPlaceholder);modalPanel.append(card);modal.classList.add('open');modal.setAttribute('aria-hidden','false');document.body.setAttribute('data-chart-modal-open','true');const close=modal.querySelector('[data-chart-modal-close]');if(close)close.focus();requestAnimationFrame(()=>{const host=card.querySelector('[data-loadstrike-chart-engine]');if(host)renderHost(host);});}
|
|
96
|
+
function closeExpanded(){if(!expandedCard||!expandedPlaceholder)return;expandedPlaceholder.replaceWith(expandedCard);modal.classList.remove('open');modal.setAttribute('aria-hidden','true');document.body.removeAttribute('data-chart-modal-open');const host=expandedCard.querySelector('[data-loadstrike-chart-engine]');if(host)requestAnimationFrame(()=>renderHost(host));if(expandedTrigger)expandedTrigger.focus();expandedCard=null;expandedPlaceholder=null;expandedTrigger=null;}
|
|
97
|
+
function prepareExpandedCardForPrint(){if(printExpandedCard||!expandedCard||!expandedPlaceholder||!expandedPlaceholder.parentNode)return;printExpandedFocus=modal&&modal.contains(document.activeElement)?document.activeElement:null;expandedPlaceholder.parentNode.insertBefore(expandedCard,expandedPlaceholder);printExpandedCard=true;}
|
|
98
|
+
function restoreExpandedCardAfterPrint(){if(!printExpandedCard)return null;if(expandedCard&&modalPanel)modalPanel.append(expandedCard);printExpandedCard=false;const focus=printExpandedFocus;printExpandedFocus=null;return focus;}
|
|
99
|
+
document.addEventListener('click',event=>{const actionButton=event.target.closest&&event.target.closest('[data-chart-action]');if(actionButton){const card=actionButton.closest('.chart-card');const host=card&&card.querySelector('[data-loadstrike-chart-engine]');if(!host)return;const action=actionButton.dataset.chartAction;if(action==='expand')expandCard(card,actionButton);else changeViewport(host,action);return;}if(event.target.closest&&event.target.closest('[data-chart-modal-close]'))closeExpanded();});
|
|
100
|
+
document.querySelectorAll('[data-loadstrike-chart-engine="svg-v2"]').forEach(host=>{const svg=chartSvg(host);if(!svg)return;const selectAtPointer=event=>{const chart=normalizeChart(host);const state=stateFor(host,chart.labels.length);const index=pointIndexFor(host,state,pointerPlotRatio(svg,event.clientX));showTooltip(host,chart,state,index,event.clientX,event.clientY);return index;};svg.addEventListener('pointermove',event=>{if(event.pointerType==='touch')return;selectAtPointer(event);});svg.addEventListener('pointerup',event=>{if(event.pointerType!=='touch')return;const chart=normalizeChart(host);const state=stateFor(host,chart.labels.length);const index=pointIndexFor(host,state,pointerPlotRatio(svg,event.clientX));if(chartTouchSelections.get(host)===index){chartTouchSelections.delete(host);hideTooltip(host);}else{chartTouchSelections.set(host,index);showTooltip(host,chart,state,index,event.clientX,event.clientY);}});svg.addEventListener('pointerleave',event=>{if(event.pointerType!=='touch')hideTooltip(host);});svg.addEventListener('keydown',event=>{const chart=normalizeChart(host);const state=stateFor(host,chart.labels.length);if(event.key==='Escape'){hideTooltip(host);return;}if(event.key==='Home')state.active=state.start;else if(event.key==='End')state.active=state.end;else if(event.key==='ArrowLeft')state.active=Math.max(state.start,state.active-1);else if(event.key==='ArrowRight')state.active=Math.min(state.end,state.active+1);else return;event.preventDefault();showTooltip(host,chart,state,state.active);});svg.addEventListener('focus',()=>{const chart=normalizeChart(host);const state=stateFor(host,chart.labels.length);showTooltip(host,chart,state,state.active);});});
|
|
101
|
+
document.addEventListener('pointerdown',event=>{if(event.target.closest&&event.target.closest('[data-loadstrike-chart-engine="svg-v2"]'))return;document.querySelectorAll('[data-loadstrike-chart-engine="svg-v2"]').forEach(host=>{chartTouchSelections.delete(host);hideTooltip(host);});});
|
|
102
|
+
document.querySelectorAll('[data-chart-search]').forEach(input=>input.addEventListener('input',()=>{const collection=input.closest('[data-chart-collection]');if(!collection)return;const query=input.value.trim().toLocaleLowerCase();let visible=0;collection.querySelectorAll('.chart-card').forEach(card=>{const match=!query||(card.dataset.chartTitle||'').toLocaleLowerCase().includes(query);card.hidden=!match;if(match)visible++;});const empty=collection.querySelector('[data-chart-empty]');if(empty)empty.classList.toggle('visible',visible===0);}));
|
|
103
|
+
document.querySelectorAll('[data-chart-grid-size]').forEach(select=>select.addEventListener('change',()=>{const collection=select.closest('[data-chart-collection]');if(collection)collection.dataset.gridSize=select.value;}));
|
|
104
|
+
if(modal){modal.addEventListener('pointerdown',event=>{if(event.target===modal)closeExpanded();});modal.addEventListener('keydown',event=>{if(event.key==='Escape'){event.preventDefault();closeExpanded();return;}if(event.key==='Tab'){const focusable=[...modal.querySelectorAll('button:not(:disabled),[tabindex="0"]')];if(focusable.length===0)return;const first=focusable[0],last=focusable[focusable.length-1];if(event.shiftKey&&document.activeElement===first){event.preventDefault();last.focus();}else if(!event.shiftKey&&document.activeElement===last){event.preventDefault();first.focus();}}});}
|
|
105
|
+
const storedReportTheme=(()=>{try{return localStorage.getItem(reportThemeKey);}catch{return null;}})();
|
|
106
|
+
applyReportTheme(storedReportTheme==='dark'?'dark':'light');
|
|
107
|
+
if(reportThemeToggle)reportThemeToggle.addEventListener('click',()=>{const next=document.body.getAttribute('data-theme')==='dark'?'light':'dark';applyReportTheme(next);try{localStorage.setItem(reportThemeKey,next);}catch{}renderAllCharts();});
|
|
108
|
+
btns.forEach(button=>button.addEventListener('click',()=>{show(button.dataset.tab);requestAnimationFrame(renderAllCharts);}));
|
|
109
|
+
if(btns.length>0)show(btns[0].dataset.tab);
|
|
110
|
+
if(typeof ResizeObserver!=='undefined'){const observer=new ResizeObserver(entries=>entries.forEach(entry=>{if(entry.target.dataset.rendered==='true')renderHost(entry.target);}));document.querySelectorAll('[data-loadstrike-chart-engine="svg-v2"]').forEach(host=>observer.observe(host));}
|
|
111
|
+
window.addEventListener('beforeprint',()=>{prepareExpandedCardForPrint();document.querySelectorAll('[data-loadstrike-chart-engine="svg-v2"]').forEach(host=>{const chart=normalizeChart(host);const state=stateFor(host,chart.labels.length);chartPrintStates.set(host,{start:state.start,end:state.end,active:state.active});state.start=0;state.end=Math.max(0,chart.labels.length-1);renderHost(host);});});
|
|
112
|
+
window.addEventListener('afterprint',()=>{const focus=restoreExpandedCardAfterPrint();chartPrintStates.forEach((saved,host)=>{const state=chartStates.get(host);if(state){state.start=saved.start;state.end=saved.end;state.active=saved.active;renderHost(host);}});chartPrintStates.clear();if(focus&&document.contains(focus)&&typeof focus.focus==='function')focus.focus();});
|
|
113
|
+
function initPanePan(){if(!tabsPane)return;let pointer=null,startY=0,startScroll=0;tabsPane.addEventListener('pointerdown',event=>{if(event.button!==0||event.target.closest('button,a,input,textarea,select,label'))return;pointer=event.pointerId;startY=event.clientY;startScroll=tabsPane.scrollTop;tabsPane.classList.add('panning');tabsPane.setPointerCapture(pointer);});tabsPane.addEventListener('pointermove',event=>{if(pointer===event.pointerId)tabsPane.scrollTop=startScroll-(event.clientY-startY);});const stop=event=>{if(pointer!==event.pointerId)return;pointer=null;tabsPane.classList.remove('panning');};tabsPane.addEventListener('pointerup',stop);tabsPane.addEventListener('pointercancel',stop);}
|
|
114
|
+
renderAllCharts();
|
|
115
|
+
initPanePan();
|
|
116
|
+
`;
|