ml-time-graph 1.0.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/index.js ADDED
@@ -0,0 +1,3432 @@
1
+ // src/core/layout.ts
2
+ var Layout = class _Layout {
3
+ _config;
4
+ constructor(config) {
5
+ this._config = config;
6
+ }
7
+ /** Calculate layout dimensions */
8
+ compute() {
9
+ const { width, height, margin } = this._config;
10
+ return {
11
+ totalWidth: width,
12
+ totalHeight: height,
13
+ chartWidth: width - margin.left - margin.right,
14
+ chartHeight: height - margin.top - margin.bottom,
15
+ chartX: margin.left,
16
+ chartY: margin.top,
17
+ margin
18
+ };
19
+ }
20
+ /** Default layout for standard charts */
21
+ static default(width = 800, height = 400) {
22
+ return new _Layout({
23
+ width,
24
+ height,
25
+ margin: { top: 20, right: 20, bottom: 40, left: 60 }
26
+ });
27
+ }
28
+ };
29
+
30
+ // src/core/slug.ts
31
+ function slugify(s) {
32
+ if (!s) return "unnamed";
33
+ const slug = s.toString().normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
34
+ return slug || "unnamed";
35
+ }
36
+
37
+ // src/core/scale.ts
38
+ var LinearScale = class {
39
+ #domain;
40
+ #range;
41
+ constructor(config) {
42
+ this.#domain = [...config.domain];
43
+ this.#range = [...config.range];
44
+ }
45
+ map(value) {
46
+ const v = Number(value);
47
+ const [d0, d1] = this.#domain;
48
+ const [r0, r1] = this.#range;
49
+ if (d1 === d0) return r0;
50
+ return r0 + (v - d0) / (d1 - d0) * (r1 - r0);
51
+ }
52
+ invert(pixel) {
53
+ const [d0, d1] = this.#domain;
54
+ const [r0, r1] = this.#range;
55
+ if (r1 === r0) return d0;
56
+ return d0 + (pixel - r0) / (r1 - r0) * (d1 - d0);
57
+ }
58
+ domain() {
59
+ return [...this.#domain];
60
+ }
61
+ range() {
62
+ return [...this.#range];
63
+ }
64
+ };
65
+ var TIME_INTERVALS = [
66
+ { label: "second", ms: 1e3 },
67
+ { label: "2_seconds", ms: 2e3 },
68
+ { label: "5_seconds", ms: 5e3 },
69
+ { label: "10_seconds", ms: 1e4 },
70
+ { label: "30_seconds", ms: 3e4 },
71
+ { label: "minute", ms: 6e4 },
72
+ { label: "5_minutes", ms: 3e5 },
73
+ { label: "15_minutes", ms: 9e5 },
74
+ { label: "30_minutes", ms: 18e5 },
75
+ { label: "hour", ms: 36e5 },
76
+ { label: "3_hours", ms: 108e5 },
77
+ { label: "6_hours", ms: 216e5 },
78
+ { label: "day", ms: 864e5 },
79
+ { label: "week", ms: 6048e5 },
80
+ { label: "month", ms: 2592e6 },
81
+ { label: "3_months", ms: 7776e6 },
82
+ { label: "6_months", ms: 15552e6 },
83
+ { label: "year", ms: 31536e6 },
84
+ { label: "2_years", ms: 63072e6 },
85
+ { label: "5_years", ms: 15768e7 }
86
+ ];
87
+ var TimeScale = class {
88
+ #linear;
89
+ #locale;
90
+ constructor(config) {
91
+ this.#linear = new LinearScale({
92
+ domain: config.domain,
93
+ range: config.range
94
+ });
95
+ this.#locale = config.locale || (typeof navigator !== "undefined" ? navigator.language : "en-US");
96
+ }
97
+ map(value) {
98
+ return this.#linear.map(Number(value));
99
+ }
100
+ invert(pixel) {
101
+ return this.#linear.invert(pixel);
102
+ }
103
+ domain() {
104
+ return this.#linear.domain();
105
+ }
106
+ range() {
107
+ return this.#linear.range();
108
+ }
109
+ get locale() {
110
+ return this.#locale;
111
+ }
112
+ /**
113
+ * Pick the "nicest" time interval that yields roughly `targetTicks` ticks
114
+ * across the visible range. Clamps to minTicks / maxTicks bounds.
115
+ */
116
+ tickInterval(targetTicks, minTicks = 3, maxTicks = 12) {
117
+ const [d0, d1] = this.#linear.domain();
118
+ const totalMs = d1 - d0;
119
+ if (totalMs <= 0) return { interval: TIME_INTERVALS[0].ms };
120
+ const ideal = totalMs / targetTicks;
121
+ let picked = TIME_INTERVALS[0].ms;
122
+ for (const t of TIME_INTERVALS) {
123
+ if (t.ms >= ideal) {
124
+ picked = t.ms;
125
+ break;
126
+ }
127
+ }
128
+ let candidate = picked;
129
+ let count = Math.round(totalMs / candidate);
130
+ while (count > maxTicks && candidate < TIME_INTERVALS[TIME_INTERVALS.length - 1].ms) {
131
+ const idx = TIME_INTERVALS.findIndex((t) => t.ms === candidate);
132
+ candidate = TIME_INTERVALS[Math.min(idx + 1, TIME_INTERVALS.length - 1)].ms;
133
+ count = Math.round(totalMs / candidate);
134
+ }
135
+ while (count < minTicks && candidate > TIME_INTERVALS[0].ms) {
136
+ const idx = TIME_INTERVALS.findIndex((t) => t.ms === candidate);
137
+ candidate = TIME_INTERVALS[Math.max(idx - 1, 0)].ms;
138
+ count = Math.round(totalMs / candidate);
139
+ }
140
+ return { interval: candidate };
141
+ }
142
+ /**
143
+ * Generate tick positions (timestamps) across the domain.
144
+ */
145
+ ticks(opts) {
146
+ const minT = opts?.minTicks ?? 5;
147
+ const maxT = opts?.maxTicks ?? 12;
148
+ const { interval } = this.tickInterval(
149
+ (minT + maxT) / 2,
150
+ minT,
151
+ maxT
152
+ );
153
+ const [d0, d1] = this.#linear.domain();
154
+ const result = [];
155
+ const start = Math.ceil(d0 / interval) * interval;
156
+ for (let t = start; t <= d1; t += interval) {
157
+ result.push(t);
158
+ }
159
+ return result;
160
+ }
161
+ /** Format a timestamp using Intl.DateTimeFormat */
162
+ format(timestamp, formatOpts) {
163
+ return new Intl.DateTimeFormat(this.#locale, formatOpts).format(
164
+ new Date(timestamp)
165
+ );
166
+ }
167
+ };
168
+
169
+ // src/theme/defaults.ts
170
+ var theme = {
171
+ // ── Series ──
172
+ /** Default stroke color for series lines */
173
+ stroke: "#4285f4",
174
+ /** Default line width in pixels */
175
+ strokeWidth: 2,
176
+ /** Default point marker size (radius / half-width) */
177
+ pointSize: 4,
178
+ /** Max data points before point markers are suppressed */
179
+ pointThreshold: 100,
180
+ /** Default max time gap (ms) before the line breaks; 0 = off */
181
+ gapThreshold: 0,
182
+ /** Default series type */
183
+ fill: "none",
184
+ hatch: null,
185
+ // ── Aggregated series ──
186
+ /** Default band fill color */
187
+ bandFill: "#4285f4",
188
+ /** Default band opacity (when countOpacity is disabled) */
189
+ bandOpacity: 0.6,
190
+ /** Default avg line color for bands */
191
+ bandAvgLine: "#e53e3e",
192
+ /** Default min color for minmaxavg series */
193
+ minColor: "#3b82f6",
194
+ /** Default max color for minmaxavg series */
195
+ maxColor: "#ef4444",
196
+ /** Default avg color for minmaxavg series */
197
+ avgColor: "#64748b",
198
+ /** Area fill alpha suffix (hex) for zoned areas — default 20% opacity */
199
+ areaFillAlpha: "4285f433",
200
+ // ── Axis ──
201
+ /** Default axis baseline color */
202
+ axisColor: "#ccc",
203
+ /** Default tick mark color */
204
+ tickColor: "#ddd",
205
+ /** Default axis label text color */
206
+ textColor: "#777",
207
+ /** Default axis text size (axis labels, tick labels) */
208
+ textSize: 11,
209
+ /** Axis label (rotated title next to axis) fill color */
210
+ axisLabelColor: "#444",
211
+ /** Axis label font size */
212
+ axisLabelSize: 12,
213
+ // ── Grid ──
214
+ /** Default grid line stroke */
215
+ gridStroke: "#e2e8f0",
216
+ /** Default grid line stroke width */
217
+ gridStrokeWidth: 1,
218
+ /** Default grid opacity */
219
+ gridOpacity: 1,
220
+ // ── Legend ──
221
+ /** Legend swatch stroke */
222
+ legendStroke: "#ccc",
223
+ /** Legend text fill */
224
+ legendText: "#333",
225
+ /** Legend font size */
226
+ legendFont: 11,
227
+ // ── Annotations ──
228
+ /** Default annotation color */
229
+ annotationColor: "#334155",
230
+ /** Default annotation line width */
231
+ annotationWidth: 1.5,
232
+ /** Default annotation arrow head size */
233
+ annotationHead: 9,
234
+ /** Default annotation point radius */
235
+ annotationRadius: 4,
236
+ /** Default annotation text font size */
237
+ annotationFontSize: 11,
238
+ // ── Thresholds ──
239
+ /** Default threshold line color */
240
+ thresholdColor: "#666",
241
+ /** Default threshold line style */
242
+ thresholdLine: "dashed",
243
+ /** Default threshold fill opacity */
244
+ thresholdFillOpacity: 0.12,
245
+ /** Default threshold label font size */
246
+ thresholdFontSize: 10,
247
+ // ── Highlights ──
248
+ /** Default highlight fill color */
249
+ highlightColor: "#fbbf24",
250
+ /** Default highlight box opacity */
251
+ highlightOpacity: 0.2,
252
+ /** Default highlight label text color */
253
+ highlightLabelColor: "#92400e",
254
+ // ── Markers ──
255
+ /** Default marker color */
256
+ markerColor: "#f59e0b",
257
+ /** Default marker point size (cross / circle radius) */
258
+ markerSize: 5,
259
+ // ── Gaps ──
260
+ /** Gap region background fill */
261
+ gapFill: "#fff",
262
+ /** Gap border stroke */
263
+ gapStroke: "#ccc",
264
+ /** Gap border stroke width */
265
+ gapStrokeWidth: 1,
266
+ /** Gap label text color */
267
+ gapFontColor: "#999",
268
+ /** Gap label font size */
269
+ gapFontSize: 10,
270
+ gapFillOpacity: 0.15,
271
+ // ── Tooltip ──
272
+ /** Tooltip box background */
273
+ tooltipBg: "#fff",
274
+ /** Tooltip border */
275
+ tooltipBorder: "#cbd5e1",
276
+ /** Tooltip text color */
277
+ tooltipText: "#1e293b",
278
+ /** Tooltip value label color */
279
+ tooltipValue: "#3b82f6",
280
+ /** Tooltip crosshair stroke */
281
+ tooltipCrosshair: "#94a3b8",
282
+ /** Tooltip snap radius in pixels */
283
+ tooltipSnapRadius: 20,
284
+ // ── Statistics ──
285
+ /** Stats overlay line color */
286
+ statsLineColor: "#94a3b8",
287
+ /** Stats label color */
288
+ statsLabelColor: "#64748b",
289
+ // ── Minimap ──
290
+ /** Minimap overview line stroke */
291
+ minimapStroke: "#94a3b8",
292
+ /** Minimap background */
293
+ minimapBg: "#f8f9fa",
294
+ /** Minimap brush (viewport) fill */
295
+ minimapBrush: "#3b82f644",
296
+ // ── Palette ──
297
+ /** Default colour palette for enum categories and multi-series. */
298
+ palette: [
299
+ "#4285f4",
300
+ "#ea4335",
301
+ "#22c55e",
302
+ "#fbbc05",
303
+ "#9334ea",
304
+ "#12b5e5",
305
+ "#fb923c",
306
+ "#6366f1"
307
+ ]
308
+ };
309
+
310
+ // src/patterns/hatch.ts
311
+ function getHatch(id, variant = "classic-diagonal", fillcolor = "rgba(200, 220, 255, 0.3)", linecolor = "#4D88FF", strokewidth = 2) {
312
+ if (variant === "none") {
313
+ return `
314
+ <pattern id="${id}" width="10" height="10" patternUnits="userSpaceOnUse">
315
+ <rect width="10" height="10" fill="${fillcolor}" />
316
+ </pattern>
317
+ `.trim();
318
+ }
319
+ let width = 12;
320
+ let height = 12;
321
+ let transform = "rotate(0)";
322
+ let patternContent = "";
323
+ switch (variant) {
324
+ case "classic-diagonal":
325
+ width = 12;
326
+ height = 12;
327
+ transform = "rotate(45)";
328
+ patternContent = `<line x1="0" y1="0" x2="0" y2="${height}" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linecap="square" />`;
329
+ break;
330
+ case "dense-steep":
331
+ width = 6;
332
+ height = 6;
333
+ transform = "rotate(30)";
334
+ patternContent = `<line x1="0" y1="0" x2="0" y2="${height}" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linecap="square" />`;
335
+ break;
336
+ case "crosshatch":
337
+ width = 14;
338
+ height = 14;
339
+ transform = "rotate(45)";
340
+ patternContent = `
341
+ <line x1="0" y1="0" x2="0" y2="${height}" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linecap="square" />
342
+ <line x1="0" y1="0" x2="${width}" y2="0" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linecap="square" />
343
+ `;
344
+ break;
345
+ case "dots":
346
+ width = 12;
347
+ height = 12;
348
+ patternContent = `<circle cx="${width / 2}" cy="${height / 2}" r="${strokewidth * 1.2}" fill="${linecolor}" />`;
349
+ break;
350
+ case "waves":
351
+ width = 16;
352
+ height = 16;
353
+ patternContent = `
354
+ <path d="M 0 ${height / 2} Q ${width / 4} 0, ${width / 2} ${height / 2} T ${width} ${height / 2}"
355
+ fill="none" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linecap="round" />
356
+ `;
357
+ break;
358
+ case "dashed":
359
+ width = 12;
360
+ height = 12;
361
+ transform = "rotate(45)";
362
+ patternContent = `<line x1="0" y1="0" x2="0" y2="${height}" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-dasharray="3,3" />`;
363
+ break;
364
+ case "herringbone":
365
+ width = 16;
366
+ height = 16;
367
+ patternContent = `
368
+ <path d="M 0 0 L ${width / 2} ${height / 2} L 0 ${height} M ${width} 0 L ${width / 2} ${height / 2} L ${width} ${height}"
369
+ fill="none" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linejoin="round" stroke-linecap="round" />
370
+ `;
371
+ break;
372
+ case "brick":
373
+ width = 20;
374
+ height = 20;
375
+ patternContent = `
376
+ <path d="M 0 ${height / 2} L ${width} ${height / 2} M 0 ${height} L ${width} ${height} M ${width / 2} 0 L ${width / 2} ${height / 2} M 0 ${height / 2} L 0 ${height}"
377
+ fill="none" stroke="${linecolor}" stroke-width="${strokewidth}" />
378
+ `;
379
+ break;
380
+ case "double-stripe":
381
+ width = 16;
382
+ height = 16;
383
+ transform = "rotate(45)";
384
+ patternContent = `
385
+ <line x1="0" y1="0" x2="0" y2="${height}" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linecap="square" />
386
+ <line x1="${width / 2}" y1="0" x2="${width / 2}" y2="${height}" stroke="${linecolor}" stroke-width="${strokewidth / 2}" stroke-linecap="square" />
387
+ `;
388
+ break;
389
+ case "honeycomb":
390
+ width = 18;
391
+ height = 32;
392
+ patternContent = `
393
+ <path d="M 0 0 L ${width / 2} 5 L ${width} 0 M 0 16 L ${width / 2} 11 L ${width} 16 M 0 16 L 0 32 M ${width / 2} 5 L ${width / 2} 11 M ${width} 16 L ${width} 32 M 0 32 L ${width / 2} 27 L ${width} 32 M ${width / 2} 27 L ${width / 2} 32"
394
+ fill="none" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linejoin="round" stroke-linecap="round" />
395
+ `;
396
+ break;
397
+ }
398
+ const bgRect = `<rect width="${width}" height="${height}" fill="${fillcolor}" />`;
399
+ return `
400
+ <pattern id="${id}" width="${width}" height="${height}" patternTransform="${transform}" patternUnits="userSpaceOnUse">
401
+ ${bgRect}
402
+ ${patternContent}
403
+ </pattern>
404
+ `.trim();
405
+ }
406
+
407
+ // src/patterns/line.ts
408
+ function getLineStyle(variant, strokeWidth = 2) {
409
+ const sw = strokeWidth;
410
+ switch (variant) {
411
+ case "dotted":
412
+ return { strokeDasharray: `0, ${sw * 2}`, strokeLinecap: "round" };
413
+ case "sparse-dots":
414
+ return { strokeDasharray: `0, ${sw * 4}`, strokeLinecap: "round" };
415
+ case "dashed":
416
+ return { strokeDasharray: `${sw * 3}, ${sw * 2}`, strokeLinecap: "butt" };
417
+ case "long-dash":
418
+ return { strokeDasharray: `${sw * 6}, ${sw * 3}`, strokeLinecap: "butt" };
419
+ case "dense-dash":
420
+ return {
421
+ strokeDasharray: `${sw * 1.5}, ${sw * 1.5}`,
422
+ strokeLinecap: "butt"
423
+ };
424
+ case "dash-dot":
425
+ return {
426
+ strokeDasharray: `${sw * 4}, ${sw * 2}, 0, ${sw * 2}`,
427
+ strokeLinecap: "round"
428
+ };
429
+ case "dash-dot-dot":
430
+ return {
431
+ strokeDasharray: `${sw * 5}, ${sw * 2}, 0, ${sw * 2}, 0, ${sw * 2}`,
432
+ strokeLinecap: "round"
433
+ };
434
+ case "loose-dash":
435
+ return { strokeDasharray: `${sw * 3}, ${sw * 4}`, strokeLinecap: "butt" };
436
+ case "solid":
437
+ default:
438
+ return { strokeDasharray: "none", strokeLinecap: "butt" };
439
+ }
440
+ }
441
+
442
+ // src/axis/time_axis.ts
443
+ var DEFAULT_COLORS = {
444
+ axisColor: theme.axisColor,
445
+ tickColor: theme.tickColor,
446
+ textColor: theme.textColor,
447
+ textSize: theme.textSize
448
+ };
449
+ var TimeAxis = class {
450
+ #scale;
451
+ #config;
452
+ constructor(config) {
453
+ this.#scale = new TimeScale({
454
+ domain: config.domain,
455
+ range: config.xRange,
456
+ locale: config.locale
457
+ });
458
+ this.#config = config;
459
+ }
460
+ get scale() {
461
+ return this.#scale;
462
+ }
463
+ get axisColor() {
464
+ return (this.#config.colors ?? DEFAULT_COLORS).axisColor;
465
+ }
466
+ get tickColor() {
467
+ return (this.#config.colors ?? DEFAULT_COLORS).tickColor;
468
+ }
469
+ get textColor() {
470
+ return (this.#config.colors ?? DEFAULT_COLORS).textColor;
471
+ }
472
+ get textSize() {
473
+ return (this.#config.colors ?? DEFAULT_COLORS).textSize;
474
+ }
475
+ /**
476
+ * Generate properly spaced, formatted ticks for the time axis.
477
+ * Applies anti-overlap: if ticks are too close, every-other is skipped.
478
+ */
479
+ generateTicks() {
480
+ const minTicks = this.#config.minTicks ?? 5;
481
+ const maxTicks = this.#config.maxTicks ?? 12;
482
+ const timestamps = this.#scale.ticks({ minTicks, maxTicks });
483
+ const ticks = timestamps.map((time) => ({
484
+ time,
485
+ x: this.#scale.map(time),
486
+ label: this.tickLabel(time)
487
+ }));
488
+ return this.antiOverlap(ticks);
489
+ }
490
+ /** Pick the right date format based on the tick interval. */
491
+ tickLabel(time) {
492
+ if (this.#config.format) return this.#config.format(new Date(time));
493
+ const minTicks = this.#config.minTicks ?? 5;
494
+ const maxTicks = this.#config.maxTicks ?? 12;
495
+ const { interval } = this.#scale.tickInterval(
496
+ (minTicks + maxTicks) / 2,
497
+ minTicks,
498
+ maxTicks
499
+ );
500
+ const opts = {};
501
+ if (interval < 6e4) {
502
+ opts.hour = "2-digit";
503
+ opts.minute = "2-digit";
504
+ opts.second = "2-digit";
505
+ } else if (interval < 36e5) {
506
+ opts.hour = "2-digit";
507
+ opts.minute = "2-digit";
508
+ } else if (interval < 864e5) {
509
+ opts.hour = "2-digit";
510
+ opts.minute = "2-digit";
511
+ } else if (interval < 31536e6) {
512
+ opts.day = "numeric";
513
+ opts.month = "short";
514
+ if (interval >= 2592e6) {
515
+ opts.day = void 0;
516
+ opts.month = "long";
517
+ }
518
+ } else {
519
+ opts.year = "numeric";
520
+ if (interval < 2 * 31536e6) opts.month = "short";
521
+ }
522
+ return this.#scale.format(time, opts);
523
+ }
524
+ /** Remove ticks that would overlap (minimum 60px spacing). */
525
+ antiOverlap(ticks) {
526
+ if (ticks.length <= 1) return ticks;
527
+ const minGap = 60;
528
+ const result = [ticks[0]];
529
+ for (let i = 1; i < ticks.length; i++) {
530
+ const lastX = result[result.length - 1].x;
531
+ if (Math.abs(ticks[i].x - lastX) >= minGap) {
532
+ result.push(ticks[i]);
533
+ }
534
+ }
535
+ return result;
536
+ }
537
+ /** Render axis baseline + tick marks as draw commands. */
538
+ render() {
539
+ const ticks = this.generateTicks();
540
+ const colors = this.#config.colors ?? DEFAULT_COLORS;
541
+ const y = this.#config.y ?? 0;
542
+ const commands = [];
543
+ const [x0] = this.#scale.range();
544
+ commands.push({
545
+ type: "line",
546
+ x1: x0,
547
+ y1: y,
548
+ x2: ticks[ticks.length - 1]?.x ?? x0,
549
+ y2: y,
550
+ stroke: colors.axisColor,
551
+ strokeWidth: colors.axisWidth
552
+ });
553
+ for (const tick of ticks) {
554
+ commands.push({
555
+ type: "line",
556
+ x1: tick.x,
557
+ y1: y,
558
+ x2: tick.x,
559
+ y2: y + 6,
560
+ stroke: colors.tickColor,
561
+ strokeWidth: colors.axisWidth
562
+ });
563
+ commands.push({
564
+ type: "text",
565
+ content: tick.label,
566
+ x: tick.x,
567
+ y: y + colors.textSize + 6,
568
+ anchor: "middle",
569
+ fontSize: colors.textSize,
570
+ fill: colors.textColor
571
+ });
572
+ }
573
+ return commands;
574
+ }
575
+ };
576
+
577
+ // src/axis/value_axis.ts
578
+ var DEFAULT_COLORS2 = {
579
+ axisColor: "#ccc",
580
+ tickColor: "#ddd",
581
+ textColor: "#777",
582
+ textSize: 12
583
+ };
584
+ function defaultFormat(value) {
585
+ if (Math.abs(value) >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
586
+ if (Math.abs(value) >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
587
+ if (Number.isInteger(value)) return String(value);
588
+ return value.toFixed(1);
589
+ }
590
+ var ValueAxis = class {
591
+ #scale;
592
+ #config;
593
+ constructor(config) {
594
+ this.#scale = new LinearScale({ domain: config.domain, range: config.range });
595
+ this.#config = config;
596
+ }
597
+ get scale() {
598
+ return this.#scale;
599
+ }
600
+ get axisColor() {
601
+ return (this.#config.colors ?? DEFAULT_COLORS2).axisColor;
602
+ }
603
+ get tickColor() {
604
+ return (this.#config.colors ?? DEFAULT_COLORS2).tickColor;
605
+ }
606
+ get textColor() {
607
+ return (this.#config.colors ?? DEFAULT_COLORS2).textColor;
608
+ }
609
+ get textSize() {
610
+ return (this.#config.colors ?? DEFAULT_COLORS2).textSize;
611
+ }
612
+ /** Generate nicely-spaced tick values. */
613
+ generateTicks() {
614
+ const format = this.#config.format ?? defaultFormat;
615
+ const numTicks = 6;
616
+ const [d0, d1] = this.#scale.domain();
617
+ const range = d1 - d0;
618
+ if (range === 0) {
619
+ return [{ value: d0, position: this.#scale.map(d0), label: format(d0) }];
620
+ }
621
+ const rough = range / numTicks;
622
+ const magnitude = Math.pow(10, Math.floor(Math.log10(rough)));
623
+ const residual = rough / magnitude;
624
+ let step;
625
+ if (residual <= 1.5) step = magnitude;
626
+ else if (residual <= 3) step = 2 * magnitude;
627
+ else if (residual <= 7) step = 5 * magnitude;
628
+ else step = 10 * magnitude;
629
+ const ticks = [];
630
+ const start = Math.ceil(d0 / step) * step;
631
+ for (let v = start; v <= d1; v += step) {
632
+ ticks.push({ value: v, position: this.#scale.map(v), label: format(v) });
633
+ }
634
+ return ticks;
635
+ }
636
+ /** Render axis as draw commands. */
637
+ render() {
638
+ const ticks = this.generateTicks();
639
+ const colors = this.#config.colors ?? DEFAULT_COLORS2;
640
+ const x = this.#config.x ?? 0;
641
+ const orientation = this.#config.orientation ?? "vertical";
642
+ const position = this.#config.position ?? "left";
643
+ const commands = [];
644
+ if (orientation === "vertical") {
645
+ const [r0, r1] = this.#scale.range();
646
+ commands.push({
647
+ type: "line",
648
+ x1: x,
649
+ y1: r0,
650
+ x2: x,
651
+ y2: r1,
652
+ stroke: colors.axisColor,
653
+ strokeWidth: colors.axisWidth
654
+ });
655
+ for (const tick of ticks) {
656
+ if (position === "left") {
657
+ commands.push({
658
+ type: "line",
659
+ x1: x - 4,
660
+ y1: tick.position,
661
+ x2: x,
662
+ y2: tick.position,
663
+ stroke: colors.tickColor,
664
+ strokeWidth: colors.axisWidth
665
+ });
666
+ commands.push({
667
+ type: "text",
668
+ content: tick.label,
669
+ x: x - 8,
670
+ y: tick.position + 4,
671
+ anchor: "end",
672
+ fontSize: 11,
673
+ fill: colors.textColor
674
+ });
675
+ } else {
676
+ commands.push({
677
+ type: "line",
678
+ x1: x,
679
+ y1: tick.position,
680
+ x2: x + 4,
681
+ y2: tick.position,
682
+ stroke: colors.tickColor,
683
+ strokeWidth: colors.axisWidth
684
+ });
685
+ commands.push({
686
+ type: "text",
687
+ content: tick.label,
688
+ x: x + 8,
689
+ y: tick.position + 4,
690
+ anchor: "start",
691
+ fontSize: 11,
692
+ fill: colors.textColor
693
+ });
694
+ }
695
+ }
696
+ } else {
697
+ const [r0, r1] = this.#scale.range();
698
+ commands.push({
699
+ type: "line",
700
+ x1: r0,
701
+ y1: x,
702
+ x2: r1,
703
+ y2: x,
704
+ stroke: colors.axisColor,
705
+ strokeWidth: colors.axisWidth
706
+ });
707
+ for (const tick of ticks) {
708
+ commands.push({
709
+ type: "line",
710
+ x1: tick.position,
711
+ y1: x,
712
+ x2: tick.position,
713
+ y2: x + 6,
714
+ stroke: colors.tickColor,
715
+ strokeWidth: colors.axisWidth
716
+ });
717
+ commands.push({
718
+ type: "text",
719
+ content: tick.label,
720
+ x: tick.position,
721
+ y: x + 18,
722
+ anchor: "middle",
723
+ fontSize: colors.textSize,
724
+ fill: colors.textColor
725
+ });
726
+ }
727
+ }
728
+ return commands;
729
+ }
730
+ };
731
+
732
+ // src/analyze/processor.ts
733
+ var SeriesProcessor = class {
734
+ /**
735
+ * Standard interpolation for scalar DataPoints.
736
+ */
737
+ static interpolateDataPoint(p1, p2, t) {
738
+ return {
739
+ time: p1.time + t * (p2.time - p1.time),
740
+ value: (p1.value ?? 0) + t * ((p2.value ?? 0) - (p1.value ?? 0))
741
+ };
742
+ }
743
+ /**
744
+ * Standard interpolation for AggregatedPoints (interpolates min, max, avg and count).
745
+ */
746
+ static interpolateAggregatedPoint(p1, p2, t) {
747
+ const lerp = (v1, v2) => v1 !== null && v2 !== null ? v1 + t * (v2 - v1) : null;
748
+ return {
749
+ time: p1.time + t * (p2.time - p1.time),
750
+ min: lerp(p1.min, p2.min),
751
+ max: lerp(p1.max, p2.max),
752
+ avg: lerp(p1.avg, p2.avg),
753
+ count: Math.round(p1.count + t * (p2.count - p1.count))
754
+ };
755
+ }
756
+ /**
757
+ * Splits a data array into contiguous runs based on null values or time jumps.
758
+ *
759
+ * @param data The raw data points.
760
+ * @param isNull A predicate to identify "gap" points (e.g. value === null).
761
+ * @param gapThreshold Max time distance between points before a new run starts.
762
+ */
763
+ static getRuns(data, isNull, gapThreshold = 0) {
764
+ const sorted = [...data].sort((a, b) => a.time - b.time);
765
+ const runs = [];
766
+ let current = [];
767
+ let prev = null;
768
+ for (const p of sorted) {
769
+ const isPointNull = isNull(p);
770
+ const isJump = gapThreshold > 0 && prev && p.time - prev.time > gapThreshold;
771
+ if (isPointNull || isJump) {
772
+ if (current.length) {
773
+ runs.push(current);
774
+ current = [];
775
+ }
776
+ }
777
+ if (!isPointNull) {
778
+ current.push(p);
779
+ }
780
+ prev = p;
781
+ }
782
+ if (current.length) {
783
+ runs.push(current);
784
+ }
785
+ return runs;
786
+ }
787
+ /**
788
+ * Splits a contiguous run into sub-segments at the given boundary values.
789
+ * Inserts interpolated points at every boundary crossing so segments
790
+ * meet exactly at the boundary.
791
+ *
792
+ * @param run A gap-free array of points.
793
+ * @param boundaries Values at which to split the run.
794
+ * @param getValue Function to extract the numeric value used for splitting.
795
+ * @param interpolate Function to create an interpolated point between p1 and p2 at factor t [0..1].
796
+ */
797
+ static splitByBoundaries(run, boundaries, getValue, interpolate) {
798
+ if (run.length === 0) return [];
799
+ if (boundaries.length === 0) {
800
+ return [{ data: run, zoneIndex: 0 }];
801
+ }
802
+ const bs = [...boundaries].sort((a, b) => a - b);
803
+ const out = [];
804
+ const getZone = (v) => {
805
+ let idx = 0;
806
+ for (let i = 0; i < bs.length; i++) {
807
+ if (v >= bs[i]) idx = i + 1;
808
+ else break;
809
+ }
810
+ return idx;
811
+ };
812
+ let currentSeg = [run[0]];
813
+ for (let i = 1; i < run.length; i++) {
814
+ const p1 = run[i - 1];
815
+ const p2 = run[i];
816
+ const v1 = getValue(p1);
817
+ const v2 = getValue(p2);
818
+ let crossed;
819
+ if (v2 > v1) {
820
+ crossed = bs.filter((b) => b > v1 && b <= v2);
821
+ } else if (v2 < v1) {
822
+ crossed = bs.filter((b) => b >= v2 && b < v1).reverse();
823
+ } else {
824
+ crossed = [];
825
+ }
826
+ for (const b of crossed) {
827
+ const t = (b - v1) / (v2 - v1);
828
+ const pInt = interpolate(p1, p2, t);
829
+ currentSeg.push(pInt);
830
+ out.push({ data: currentSeg, zoneIndex: getZone((v1 + b) / 2) });
831
+ currentSeg = [pInt];
832
+ }
833
+ currentSeg.push(p2);
834
+ }
835
+ if (currentSeg.length > 0) {
836
+ const vStart = getValue(currentSeg[0]);
837
+ const vEnd = getValue(currentSeg[currentSeg.length - 1]);
838
+ out.push({ data: currentSeg, zoneIndex: getZone((vStart + vEnd) / 2) });
839
+ }
840
+ return out;
841
+ }
842
+ /**
843
+ * Splits a contiguous run into two groups: those below and those at/above a threshold.
844
+ * Internally uses splitByBoundaries to ensure exact intersection points.
845
+ */
846
+ static splitByThreshold(run, threshold, getValue, interpolate) {
847
+ const segments = this.splitByBoundaries(run, [threshold], getValue, interpolate);
848
+ const result = { above: [], below: [] };
849
+ for (const seg of segments) {
850
+ if (seg.zoneIndex === 0) result.below.push(seg.data);
851
+ else result.above.push(seg.data);
852
+ }
853
+ return result;
854
+ }
855
+ };
856
+
857
+ // src/renderer/series_renderer.ts
858
+ function resolveStyle(style) {
859
+ return {
860
+ id: style.id,
861
+ line: {
862
+ stroke: style.stroke ?? theme.stroke,
863
+ strokeWidth: style.strokeWidth ?? theme.strokeWidth,
864
+ smoothing: style.smoothing ?? false,
865
+ dashed: style.dashed ?? false
866
+ },
867
+ fill: style.fill ?? theme.areaFillAlpha,
868
+ markers: {
869
+ type: style.pointStyle ?? "none",
870
+ size: style.pointSize ?? theme.pointSize,
871
+ stroke: style.stroke ?? theme.stroke,
872
+ fill: "#ffffff"
873
+ },
874
+ shadow: {
875
+ color: style.shadowColor ?? "transparent",
876
+ blur: style.shadowBlur ?? 0,
877
+ offsetX: style.shadowOffsetX ?? 0,
878
+ offsetY: style.shadowOffsetY ?? 0
879
+ }
880
+ };
881
+ }
882
+ function renderLine(segments, ctx, style) {
883
+ const commands = [];
884
+ const totalPoints = segments.reduce((sum, seg) => sum + seg.data.length, 0);
885
+ const s = resolveStyle(style);
886
+ for (let si = 0; si < segments.length; si++) {
887
+ const seg = segments[si];
888
+ if (seg.data.length >= 2) {
889
+ commands.push({
890
+ type: "path",
891
+ id: style.id ? `${style.id}-line-${si}` : void 0,
892
+ points: seg.data.map((p) => ({
893
+ x: ctx.timeScale.map(p.time),
894
+ y: ctx.valueScale.map(p.value)
895
+ })),
896
+ stroke: seg.color ?? s.line.stroke,
897
+ strokeWidth: s.line.strokeWidth,
898
+ smoothing: s.line.smoothing,
899
+ dashed: s.line.dashed,
900
+ shadowColor: s.shadow.color,
901
+ shadowBlur: s.shadow.blur,
902
+ shadowOffsetX: s.shadow.offsetX,
903
+ shadowOffsetY: s.shadow.offsetY,
904
+ fill: "none"
905
+ });
906
+ } else if (seg.data.length === 1 && totalPoints === 1) {
907
+ commands.push({
908
+ type: "circle",
909
+ cx: ctx.timeScale.map(seg.data[0].time),
910
+ cy: ctx.valueScale.map(seg.data[0].value),
911
+ r: Math.max(s.markers.size, s.line.strokeWidth),
912
+ fill: seg.color ?? s.line.stroke,
913
+ shadowColor: s.shadow.color,
914
+ shadowBlur: s.shadow.blur
915
+ });
916
+ }
917
+ }
918
+ return commands;
919
+ }
920
+ function renderZonedArea(run, ctx, options, style) {
921
+ if (run.length < 2) return [];
922
+ const segments = SeriesProcessor.splitByBoundaries(
923
+ run,
924
+ options.boundaries,
925
+ options.getValue,
926
+ options.interpolate
927
+ );
928
+ const commands = [];
929
+ for (let si = 0; si < segments.length; si++) {
930
+ const seg = segments[si];
931
+ if (seg.data.length < 2) continue;
932
+ const color = options.getColor(seg.zoneIndex);
933
+ if (!color) continue;
934
+ const ptsLow = seg.data.map((p) => ({
935
+ x: ctx.timeScale.map(p.time),
936
+ y: ctx.valueScale.map(options.yLow(p))
937
+ }));
938
+ const ptsHigh = seg.data.map((p) => ({
939
+ x: ctx.timeScale.map(p.time),
940
+ y: ctx.valueScale.map(options.yHigh(p))
941
+ })).reverse();
942
+ commands.push({
943
+ type: "path",
944
+ id: style?.id ? `${style.id}-fill-${si}` : void 0,
945
+ points: [...ptsLow, ...ptsHigh],
946
+ fill: color,
947
+ hatch: options.getHatch?.(seg.zoneIndex),
948
+ stroke: "none"
949
+ });
950
+ }
951
+ return commands;
952
+ }
953
+ function renderMarkers(points, ctx, style, getColor) {
954
+ const s = resolveStyle(style);
955
+ if (!s.markers.type || s.markers.type === "none") return [];
956
+ const commands = [];
957
+ for (let mi = 0; mi < points.length; mi++) {
958
+ const p = points[mi];
959
+ const x = ctx.timeScale.map(p.time);
960
+ const y = ctx.valueScale.map(p.value);
961
+ const pointColor = getColor(p);
962
+ const stroke = style.pointStroke ?? pointColor;
963
+ const fill = style.pointFill ?? pointColor;
964
+ const strokeWidth = style.pointStrokeWidth ?? 1.5;
965
+ const id = style.id ? `${style.id}-marker-${mi}` : void 0;
966
+ drawMarker(commands, id, s.markers.type, x, y, s.markers.size, stroke, fill, strokeWidth);
967
+ }
968
+ return commands;
969
+ }
970
+ function drawMarker(commands, id, shape, x, y, size, stroke, fill, sw) {
971
+ switch (shape) {
972
+ case "circle":
973
+ commands.push({ type: "circle", cx: x, cy: y, r: size, fill, stroke, strokeWidth: sw, id });
974
+ break;
975
+ case "square":
976
+ commands.push({ type: "rect", x: x - size, y: y - size, w: size * 2, h: size * 2, fill, stroke, strokeWidth: sw, id });
977
+ break;
978
+ case "cross":
979
+ commands.push({ type: "line", x1: x - size, y1: y - size, x2: x + size, y2: y + size, stroke, strokeWidth: sw, id });
980
+ commands.push({ type: "line", x1: x - size, y1: y + size, x2: x + size, y2: y - size, stroke, strokeWidth: sw, id });
981
+ break;
982
+ case "diamond":
983
+ commands.push({ type: "path", points: [{ x, y: y - size }, { x: x + size, y }, { x, y: y + size }, { x: x - size, y }], fill, stroke, strokeWidth: sw, id });
984
+ break;
985
+ case "triangle":
986
+ commands.push({ type: "path", points: [{ x, y: y - size }, { x: x + size, y: y + size }, { x: x - size, y: y + size }], fill, stroke, strokeWidth: sw, id });
987
+ break;
988
+ case "star": {
989
+ const pts = [];
990
+ for (let i = 0; i < 10; i++) {
991
+ const r = i % 2 === 0 ? size : size * 0.5;
992
+ const a = Math.PI / 2 * 3 + i * Math.PI / 5;
993
+ pts.push({ x: x + r * Math.cos(a), y: y + r * Math.sin(a) });
994
+ }
995
+ commands.push({ type: "path", points: pts, fill, stroke, strokeWidth: sw, id });
996
+ break;
997
+ }
998
+ case "arrow":
999
+ commands.push({ type: "path", points: [{ x: x - size, y: y + size }, { x, y: y - size }, { x: x + size, y: y + size }], stroke, strokeWidth: sw, fill: "none", id });
1000
+ break;
1001
+ default:
1002
+ commands.push({ type: "circle", cx: x, cy: y, r: size, fill, stroke, strokeWidth: sw, id });
1003
+ }
1004
+ }
1005
+
1006
+ // src/series/series.ts
1007
+ var Series = class _Series {
1008
+ /** SVG id prefix for elements. Generated ids: `<id>-slot-<index>`, `<id>-avg-<index>`. */
1009
+ #id;
1010
+ #timeScale;
1011
+ static uidcnt = 0;
1012
+ #data;
1013
+ constructor(config, data = []) {
1014
+ this.#id = config.id ?? "id" + Date.now + ++_Series.uidcnt;
1015
+ this.#timeScale = config.timeScale;
1016
+ this.#data = data;
1017
+ }
1018
+ get id() {
1019
+ return this.#id;
1020
+ }
1021
+ get timeScale() {
1022
+ return this.#timeScale;
1023
+ }
1024
+ get data() {
1025
+ return this.#data;
1026
+ }
1027
+ };
1028
+
1029
+ // src/series/minmaxavg_series.ts
1030
+ var MinMaxAvgSeries = class extends Series {
1031
+ #config;
1032
+ constructor(config) {
1033
+ super(config, config.data);
1034
+ this.#config = config;
1035
+ }
1036
+ render() {
1037
+ const c = this.#config;
1038
+ const minColor = c.minColor ?? theme.minColor;
1039
+ const maxColor = c.maxColor ?? theme.maxColor;
1040
+ const avgColor = c.avgColor ?? theme.avgColor;
1041
+ const avgDashed = c.avgDashed ?? true;
1042
+ const smoothing = c.smoothing ?? false;
1043
+ const strokeWidth = c.strokeWidth ?? theme.strokeWidth;
1044
+ const runs = SeriesProcessor.getRuns(
1045
+ this.data,
1046
+ (p) => p.min === null || p.max === null || p.avg === null
1047
+ );
1048
+ if (runs.length === 0) return [];
1049
+ const ctx = { timeScale: this.timeScale, valueScale: c.valueScale };
1050
+ const commands = [];
1051
+ for (const run of runs) {
1052
+ if (run.length < 2) continue;
1053
+ if (c.fillToMax) {
1054
+ commands.push(
1055
+ ...renderZonedArea(
1056
+ run,
1057
+ ctx,
1058
+ {
1059
+ boundaries: [],
1060
+ yLow: (p) => p.avg,
1061
+ yHigh: (p) => p.max,
1062
+ getValue: (p) => p.avg,
1063
+ interpolate: SeriesProcessor.interpolateAggregatedPoint,
1064
+ getColor: () => c.fillToMax,
1065
+ getHatch: () => c.fillToMaxHatch
1066
+ },
1067
+ { id: this.id ? `${this.id}-fillToMax` : void 0 }
1068
+ )
1069
+ );
1070
+ }
1071
+ if (c.fillToMin) {
1072
+ commands.push(
1073
+ ...renderZonedArea(
1074
+ run,
1075
+ ctx,
1076
+ {
1077
+ boundaries: [],
1078
+ yLow: (p) => p.avg,
1079
+ yHigh: (p) => p.min,
1080
+ getValue: (p) => p.avg,
1081
+ interpolate: SeriesProcessor.interpolateAggregatedPoint,
1082
+ getColor: () => c.fillToMin,
1083
+ getHatch: () => c.fillToMinHatch
1084
+ },
1085
+ { id: this.id ? `${this.id}-fillToMin` : void 0 }
1086
+ )
1087
+ );
1088
+ }
1089
+ commands.push(
1090
+ ...renderLine(
1091
+ [{ data: run.map((p) => ({ time: p.time, value: p.max })) }],
1092
+ ctx,
1093
+ { stroke: maxColor, strokeWidth, smoothing, id: this.id ? `${this.id}-max` : void 0 }
1094
+ ),
1095
+ ...renderLine(
1096
+ [{ data: run.map((p) => ({ time: p.time, value: p.min })) }],
1097
+ ctx,
1098
+ { stroke: minColor, strokeWidth, smoothing, id: this.id ? `${this.id}-min` : void 0 }
1099
+ ),
1100
+ ...renderLine(
1101
+ [{ data: run.map((p) => ({ time: p.time, value: p.avg })) }],
1102
+ ctx,
1103
+ { stroke: avgColor, strokeWidth, smoothing, dashed: avgDashed, id: this.id ? `${this.id}-avg` : void 0 }
1104
+ )
1105
+ );
1106
+ }
1107
+ return commands;
1108
+ }
1109
+ };
1110
+
1111
+ // src/series/band_series.ts
1112
+ var BandSeries = class extends Series {
1113
+ #config;
1114
+ constructor(config) {
1115
+ super(config, config.data);
1116
+ this.#config = config;
1117
+ }
1118
+ /** Calculate opacity from count (normalized 0.2-1.0) */
1119
+ opacity(count) {
1120
+ if (!(this.#config.countOpacity ?? false)) return 0.6;
1121
+ const maxCount = Math.max(...this.data.map((d) => d.count));
1122
+ if (maxCount === 0) return 0.2;
1123
+ return 0.2 + 0.8 * count / maxCount;
1124
+ }
1125
+ /** Render bands as draw commands */
1126
+ render() {
1127
+ const c = this.#config;
1128
+ const fill = c.fill ?? theme.bandFill;
1129
+ const hatch = c.hatch;
1130
+ const avgLine = c.avgLine ?? false;
1131
+ const avgLineColor = c.avgLineColor ?? theme.bandAvgLine;
1132
+ const bandWidth = c.bandWidth ?? 10;
1133
+ const commands = [];
1134
+ for (const dp of this.data) {
1135
+ if (dp.min === null || dp.max === null) continue;
1136
+ const x = this.timeScale.map(dp.time);
1137
+ const yMin = c.valueScale.map(dp.max);
1138
+ const yMax = c.valueScale.map(dp.min);
1139
+ const w = bandWidth;
1140
+ const idx = this.data.indexOf(dp);
1141
+ commands.push({
1142
+ type: "rect",
1143
+ x: x - w / 2,
1144
+ y: yMin,
1145
+ w,
1146
+ h: yMax - yMin,
1147
+ fill,
1148
+ hatch,
1149
+ opacity: this.opacity(dp.count),
1150
+ id: this.id ? `${this.id}-slot-${idx}` : void 0
1151
+ });
1152
+ if (avgLine && dp.avg !== null) {
1153
+ const yAvg = c.valueScale.map(dp.avg);
1154
+ commands.push({
1155
+ type: "line",
1156
+ x1: x - w / 2,
1157
+ y1: yAvg,
1158
+ x2: x + w / 2,
1159
+ y2: yAvg,
1160
+ stroke: avgLineColor,
1161
+ strokeWidth: 1,
1162
+ id: this.id ? `${this.id}-avg-${idx}` : void 0
1163
+ });
1164
+ }
1165
+ }
1166
+ return commands;
1167
+ }
1168
+ };
1169
+
1170
+ // src/series/threshold_renderer.ts
1171
+ function dashFor(line) {
1172
+ if (line === "dotted") return { dash: "dotted" };
1173
+ if (line === "dashed") return { dash: "dashed" };
1174
+ return {};
1175
+ }
1176
+ function renderThresholds(config) {
1177
+ const { thresholds, valueScale, xRange } = config;
1178
+ const [x0, x1] = xRange;
1179
+ const [r0, r1] = valueScale.range();
1180
+ const top = Math.min(r0, r1);
1181
+ const bottom = Math.max(r0, r1);
1182
+ const commands = [];
1183
+ for (const t of thresholds) {
1184
+ const color = t.color ?? theme.thresholdColor;
1185
+ const y = valueScale.map(t.value);
1186
+ if (t.fill === "above") {
1187
+ commands.push({
1188
+ type: "rect",
1189
+ x: x0,
1190
+ y: top,
1191
+ w: x1 - x0,
1192
+ h: Math.max(0, y - top),
1193
+ fill: color,
1194
+ hatch: t.fillHatch,
1195
+ opacity: t.fillOpacity ?? 0.12,
1196
+ id: t.id ? `${t.id}-fill` : void 0
1197
+ });
1198
+ } else if (t.fill === "below") {
1199
+ commands.push({
1200
+ type: "rect",
1201
+ x: x0,
1202
+ y,
1203
+ w: x1 - x0,
1204
+ h: Math.max(0, bottom - y),
1205
+ fill: color,
1206
+ hatch: t.fillHatch,
1207
+ opacity: t.fillOpacity ?? 0.12,
1208
+ id: t.id ? `${t.id}-fill` : void 0
1209
+ });
1210
+ }
1211
+ const line = t.line ?? theme.thresholdLine;
1212
+ if (line !== "none") {
1213
+ const dash = dashFor(line);
1214
+ const lineCmd = {
1215
+ type: "line",
1216
+ x1: x0,
1217
+ y1: y,
1218
+ x2: x1,
1219
+ y2: y,
1220
+ stroke: color,
1221
+ strokeWidth: 1,
1222
+ ...dash,
1223
+ id: t.id ? `${t.id}-line` : void 0
1224
+ };
1225
+ if (t.shadowColor) {
1226
+ lineCmd.shadowColor = t.shadowColor;
1227
+ lineCmd.shadowBlur = t.shadowBlur ?? 4;
1228
+ lineCmd.shadowOffsetX = t.shadowOffsetX ?? 0;
1229
+ lineCmd.shadowOffsetY = t.shadowOffsetY ?? 2;
1230
+ }
1231
+ commands.push(lineCmd);
1232
+ }
1233
+ if (t.label !== false) {
1234
+ const labelObj = t.label && typeof t.label === "object" ? t.label : void 0;
1235
+ const text = typeof t.label === "string" ? t.label : labelObj?.text ?? t.name;
1236
+ const position = labelObj?.position ?? "right";
1237
+ commands.push({
1238
+ ...thresholdLabel(text, position, x0, x1, y, color, labelObj),
1239
+ id: t.id ? `${t.id}-label` : void 0
1240
+ });
1241
+ }
1242
+ }
1243
+ return commands;
1244
+ }
1245
+ function thresholdLabel(text, pos, x0, x1, y, color, labelObj) {
1246
+ const mid = (x0 + x1) / 2;
1247
+ const base = {
1248
+ type: "text",
1249
+ content: text,
1250
+ fontSize: theme.thresholdFontSize,
1251
+ fill: color
1252
+ };
1253
+ const extras = labelObj ? {
1254
+ ...labelObj.rotate !== void 0 && { rotate: labelObj.rotate },
1255
+ ...labelObj.textBaseline !== void 0 && {
1256
+ textBaseline: labelObj.textBaseline
1257
+ }
1258
+ } : {};
1259
+ switch (pos) {
1260
+ case "left":
1261
+ return { ...base, ...extras, x: x0 + 4, y: y - 4, anchor: "start" };
1262
+ case "above":
1263
+ return { ...base, ...extras, x: mid, y: y - 6, anchor: "middle" };
1264
+ case "below":
1265
+ return { ...base, ...extras, x: mid, y: y + 14, anchor: "middle" };
1266
+ case "center":
1267
+ return { ...base, ...extras, x: mid, y: y - 4, anchor: "middle" };
1268
+ case "right":
1269
+ default:
1270
+ return { ...base, ...extras, x: x1 - 4, y: y - 4, anchor: "end" };
1271
+ }
1272
+ }
1273
+
1274
+ // src/series/gap_renderer.ts
1275
+ function renderGaps(config) {
1276
+ const {
1277
+ gaps,
1278
+ timeScale,
1279
+ yRange,
1280
+ fill = theme.gapFill,
1281
+ hatch,
1282
+ fillOpacity = theme.gapFillOpacity,
1283
+ stroke = theme.gapStroke,
1284
+ strokeWidth = theme.gapStrokeWidth,
1285
+ dashed = true,
1286
+ fontSize = theme.gapFontSize,
1287
+ fontFill = theme.gapFontColor,
1288
+ labelBaseline: defaultBaseline = "middle",
1289
+ labelRotate: defaultRotate
1290
+ } = config;
1291
+ const [y0, y1] = yRange;
1292
+ const commands = [];
1293
+ for (const gap of gaps) {
1294
+ const x1 = timeScale.map(gap.startTime);
1295
+ const x2 = timeScale.map(gap.endTime);
1296
+ const gapFill = gap.fill ?? fill;
1297
+ const gapHatch = gap.hatch ?? hatch;
1298
+ const gapOpacity = gap.fillOpacity ?? fillOpacity;
1299
+ const gapLabel = gap.label ?? "";
1300
+ const gapRotate = gap.rotate ?? defaultRotate;
1301
+ const baseline = gap.labelBaseline ?? defaultBaseline;
1302
+ if (gap.style === "dashed_border" || !gap.style) {
1303
+ commands.push({
1304
+ type: "rect",
1305
+ x: x1,
1306
+ y: y0,
1307
+ w: x2 - x1,
1308
+ h: y1 - y0,
1309
+ fill: gapFill,
1310
+ hatch: gapHatch,
1311
+ opacity: gapOpacity,
1312
+ stroke,
1313
+ strokeWidth,
1314
+ dashed
1315
+ });
1316
+ } else if (gap.style === "empty") {
1317
+ commands.push({
1318
+ type: "rect",
1319
+ x: x1,
1320
+ y: y0,
1321
+ w: x2 - x1,
1322
+ h: y1 - y0,
1323
+ fill: gapFill,
1324
+ hatch: gapHatch,
1325
+ opacity: gapOpacity
1326
+ });
1327
+ }
1328
+ if (gapLabel) {
1329
+ const labelY = gapLabelY(y0, y1, baseline);
1330
+ const svgBaseline = baseline === "above" ? "top" : baseline === "below" ? "bottom" : "middle";
1331
+ commands.push({
1332
+ type: "text",
1333
+ content: gapLabel,
1334
+ x: (x1 + x2) / 2,
1335
+ y: labelY,
1336
+ anchor: "middle",
1337
+ fontSize,
1338
+ fill: fontFill,
1339
+ textBaseline: svgBaseline,
1340
+ rotate: gapRotate
1341
+ });
1342
+ }
1343
+ }
1344
+ return commands;
1345
+ }
1346
+ function gapLabelY(y0, y1, baseline) {
1347
+ switch (baseline) {
1348
+ case "above":
1349
+ return y0 - 12;
1350
+ case "below":
1351
+ return y1 + 4;
1352
+ case "middle":
1353
+ default:
1354
+ return (y0 + y1) / 2;
1355
+ }
1356
+ }
1357
+
1358
+ // src/series/annotation_band.ts
1359
+ var AnnotationBandSeries = class {
1360
+ #config;
1361
+ constructor(config, xRange, y, height) {
1362
+ this.#config = { ...config, xRange, y, height };
1363
+ }
1364
+ /** Render the band as colored rects with labels, optionally with a time axis. */
1365
+ render() {
1366
+ const { items, timeScale, background, hatch: bandHatch, showAxis, xRange, y, height } = this.#config;
1367
+ const commands = [];
1368
+ if (background) {
1369
+ commands.push({
1370
+ type: "rect",
1371
+ x: xRange[0],
1372
+ y,
1373
+ w: xRange[1] - xRange[0],
1374
+ h: height,
1375
+ fill: background,
1376
+ opacity: 0.04,
1377
+ stroke: "#ddd",
1378
+ strokeWidth: 0.25
1379
+ });
1380
+ }
1381
+ for (const item of items) {
1382
+ const x1 = timeScale.map(item.startTime);
1383
+ const x2 = timeScale.map(item.endTime);
1384
+ if (x2 - x1 < 1) continue;
1385
+ commands.push({
1386
+ type: "rect",
1387
+ x: x1,
1388
+ y,
1389
+ w: x2 - x1,
1390
+ h: height,
1391
+ hatch: item.hatch ?? bandHatch,
1392
+ fill: item.fill ?? "#6b728044",
1393
+ stroke: item.stroke,
1394
+ strokeWidth: item.strokeWidth ?? 0
1395
+ });
1396
+ if (item.label) {
1397
+ commands.push({
1398
+ type: "text",
1399
+ content: item.label,
1400
+ x: (x1 + x2) / 2,
1401
+ y: this.#labelY(item.labelBaseline),
1402
+ anchor: "middle",
1403
+ fontSize: item.labelFontSize ?? 10,
1404
+ fill: item.labelFill ?? "#333",
1405
+ textBaseline: item.labelBaseline ?? "middle"
1406
+ });
1407
+ }
1408
+ }
1409
+ if (showAxis) {
1410
+ const timeAxis = new TimeAxis({
1411
+ domain: timeScale.domain(),
1412
+ xRange,
1413
+ y: y + height + 4
1414
+ });
1415
+ commands.push({
1416
+ type: "group",
1417
+ cssClass: "annotation-band-axis",
1418
+ commands: timeAxis.render()
1419
+ });
1420
+ }
1421
+ return commands;
1422
+ }
1423
+ #labelY(baseline) {
1424
+ const { y, height } = this.#config;
1425
+ switch (baseline) {
1426
+ case "top":
1427
+ return y + 1;
1428
+ case "bottom":
1429
+ return y + height - 1;
1430
+ default:
1431
+ return y + height / 2;
1432
+ }
1433
+ }
1434
+ };
1435
+
1436
+ // src/annotation/highlight.ts
1437
+ function renderHighlights(config) {
1438
+ const { highlights, timeScale, yRange, height } = config;
1439
+ const [y0, y1] = yRange;
1440
+ const commands = [];
1441
+ for (const h of highlights) {
1442
+ const x1 = timeScale.map(h.startTime);
1443
+ const x2 = timeScale.map(h.endTime);
1444
+ commands.push({
1445
+ type: "rect",
1446
+ x: x1,
1447
+ y: y0,
1448
+ w: x2 - x1,
1449
+ h: y1 - y0,
1450
+ fill: h.color ?? theme.highlightColor,
1451
+ opacity: h.opacity ?? theme.highlightOpacity
1452
+ });
1453
+ if (h.label) {
1454
+ commands.push({
1455
+ type: "text",
1456
+ content: h.label,
1457
+ x: (x1 + x2) / 2,
1458
+ y: highlightLabelY(h.labelPosition ?? "top", y0, y1, height),
1459
+ anchor: "middle",
1460
+ fontSize: theme.annotationFontSize,
1461
+ fill: h.color ?? theme.highlightLabelColor
1462
+ });
1463
+ }
1464
+ }
1465
+ return commands;
1466
+ }
1467
+ function highlightLabelY(pos, y0, y1, height) {
1468
+ switch (pos) {
1469
+ case "above":
1470
+ return y0 - 5;
1471
+ case "below":
1472
+ return height !== void 0 ? height - 5 : y1 + 14;
1473
+ case "center":
1474
+ return (y0 + y1) / 2 + 4;
1475
+ case "bottom":
1476
+ return y1 - 6;
1477
+ case "top":
1478
+ default:
1479
+ return y0 + 14;
1480
+ }
1481
+ }
1482
+
1483
+ // src/annotation/marker.ts
1484
+ function renderMarkers2(config) {
1485
+ const { markers, timeScale, valueScale, yRange = [0, 300] } = config;
1486
+ const [yTop, yBottom] = yRange;
1487
+ const commands = [];
1488
+ for (const marker of markers) {
1489
+ const x = timeScale.map(marker.time);
1490
+ const color = marker.color ?? theme.markerColor;
1491
+ const pointStyle = marker.pointStyle ?? (marker.value !== void 0 ? "circle" : "none");
1492
+ const lineStyle = marker.lineStyle ?? "full";
1493
+ if (marker.value !== void 0) {
1494
+ const y = valueScale.map(marker.value);
1495
+ if (lineStyle === "to-value") {
1496
+ commands.push({ type: "line", x1: x, y1: yBottom, x2: x, y2: y, stroke: color, strokeWidth: 1, dashed: true });
1497
+ } else if (lineStyle === "to-top") {
1498
+ commands.push({ type: "line", x1: x, y1: yTop, x2: x, y2: y, stroke: color, strokeWidth: 1, dashed: true });
1499
+ } else if (lineStyle === "full") {
1500
+ commands.push({ type: "line", x1: x, y1: yTop, x2: x, y2: yBottom, stroke: color, strokeWidth: 1 });
1501
+ }
1502
+ if (pointStyle !== "none") {
1503
+ drawMarkerPoint(commands, x, y, color, pointStyle);
1504
+ }
1505
+ if (marker.label) {
1506
+ const labelY = lineStyle === "to-value" ? y - 10 : yTop - 6;
1507
+ commands.push({ type: "text", content: marker.label, x, y: labelY, anchor: "middle", fontSize: 11, fill: color });
1508
+ }
1509
+ } else {
1510
+ commands.push({ type: "line", x1: x, y1: yTop, x2: x, y2: yBottom, stroke: color, strokeWidth: 1 });
1511
+ if (marker.label) {
1512
+ commands.push({ type: "text", content: marker.label, x, y: yTop - 6, anchor: "middle", fontSize: 11, fill: color });
1513
+ }
1514
+ }
1515
+ }
1516
+ return commands;
1517
+ }
1518
+ function drawMarkerPoint(commands, x, y, color, style) {
1519
+ const s = theme.markerSize;
1520
+ switch (style) {
1521
+ case "circle":
1522
+ commands.push({ type: "circle", cx: x, cy: y, r: s, fill: color });
1523
+ break;
1524
+ case "square":
1525
+ commands.push({ type: "rect", x: x - s, y: y - s, w: s * 2, h: s * 2, fill: color });
1526
+ break;
1527
+ case "cross":
1528
+ commands.push({ type: "line", x1: x - s, y1: y - s, x2: x + s, y2: y + s, stroke: color, strokeWidth: 2 });
1529
+ commands.push({ type: "line", x1: x - s, y1: y + s, x2: x + s, y2: y - s, stroke: color, strokeWidth: 2 });
1530
+ break;
1531
+ case "arrow":
1532
+ commands.push({
1533
+ type: "path",
1534
+ points: [{ x: x - s, y: y + s }, { x, y: y - s }, { x: x + s, y: y + s }],
1535
+ stroke: color,
1536
+ strokeWidth: 2,
1537
+ fill: "none"
1538
+ });
1539
+ break;
1540
+ case "diamond":
1541
+ commands.push({
1542
+ type: "path",
1543
+ points: [{ x, y: y - s }, { x: x + s, y }, { x, y: y + s }, { x: x - s, y }],
1544
+ fill: color,
1545
+ stroke: "none"
1546
+ });
1547
+ break;
1548
+ case "triangle":
1549
+ commands.push({
1550
+ type: "path",
1551
+ points: [{ x, y: y - s }, { x: x + s, y: y + s }, { x: x - s, y: y + s }],
1552
+ fill: color,
1553
+ stroke: "none"
1554
+ });
1555
+ break;
1556
+ case "star": {
1557
+ const pts = [];
1558
+ const innerRadius = s * 0.4;
1559
+ for (let i = 0; i < 10; i++) {
1560
+ const r = i % 2 === 0 ? s : innerRadius;
1561
+ const angle = Math.PI / 2 * 3 + i * Math.PI / 5;
1562
+ pts.push({ x: x + r * Math.cos(angle), y: y + r * Math.sin(angle) });
1563
+ }
1564
+ commands.push({ type: "path", points: pts, fill: color, stroke: "none" });
1565
+ break;
1566
+ }
1567
+ case "plus":
1568
+ commands.push({ type: "line", x1: x - s, y1: y, x2: x + s, y2: y, stroke: color, strokeWidth: 2 });
1569
+ commands.push({ type: "line", x1: x, y1: y - s, x2: x, y2: y + s, stroke: color, strokeWidth: 2 });
1570
+ break;
1571
+ case "triangle-down":
1572
+ commands.push({
1573
+ type: "path",
1574
+ points: [{ x, y: y + s }, { x: x + s, y: y - s }, { x: x - s, y: y - s }],
1575
+ fill: color,
1576
+ stroke: "none"
1577
+ });
1578
+ break;
1579
+ case "hexagon": {
1580
+ const pts = [];
1581
+ for (let i = 0; i < 6; i++) {
1582
+ const angle = i * (Math.PI / 3);
1583
+ pts.push({ x: x + s * Math.cos(angle), y: y + s * Math.sin(angle) });
1584
+ }
1585
+ commands.push({ type: "path", points: pts, fill: color, stroke: "none" });
1586
+ break;
1587
+ }
1588
+ case "hourglass":
1589
+ commands.push({
1590
+ type: "path",
1591
+ points: [{ x: x - s, y: y - s }, { x: x + s, y: y - s }, { x: x - s, y: y + s }, { x: x + s, y: y + s }],
1592
+ fill: color,
1593
+ stroke: "none"
1594
+ });
1595
+ break;
1596
+ case "line-horizontal":
1597
+ commands.push({ type: "line", x1: x - s, y1: y, x2: x + s, y2: y, stroke: color, strokeWidth: 2 });
1598
+ break;
1599
+ }
1600
+ }
1601
+
1602
+ // src/annotation/annotation_renderer.ts
1603
+ function renderAnnotations(config) {
1604
+ const { annotations, timeScale, valueScales } = config;
1605
+ const cmds = [];
1606
+ const project = (ref) => {
1607
+ const scale = valueScales.get(ref.axis ?? 0) ?? valueScales.values().next().value;
1608
+ return { x: timeScale.map(ref.time), y: scale ? scale.map(ref.value) : 0 };
1609
+ };
1610
+ for (const a of annotations) {
1611
+ switch (a.type) {
1612
+ case "line": {
1613
+ const p1 = project(a.from);
1614
+ const p2 = project(a.to);
1615
+ cmds.push({ type: "line", x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, stroke: a.color ?? theme.annotationColor, strokeWidth: a.width ?? theme.annotationWidth, dash: a.dash });
1616
+ break;
1617
+ }
1618
+ case "arrow": {
1619
+ const p1 = project(a.from);
1620
+ const p2 = project(a.to);
1621
+ const color = a.color ?? theme.annotationColor;
1622
+ const h = a.headSize ?? theme.annotationHead;
1623
+ cmds.push({ type: "line", x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, stroke: color, strokeWidth: a.width ?? theme.annotationWidth });
1624
+ const len = Math.hypot(p2.x - p1.x, p2.y - p1.y) || 1;
1625
+ const ux = (p2.x - p1.x) / len;
1626
+ const uy = (p2.y - p1.y) / len;
1627
+ const baseX = p2.x - ux * h;
1628
+ const baseY = p2.y - uy * h;
1629
+ cmds.push({
1630
+ type: "path",
1631
+ points: [
1632
+ { x: p2.x, y: p2.y },
1633
+ { x: baseX - uy * h * 0.5, y: baseY + ux * h * 0.5 },
1634
+ { x: baseX + uy * h * 0.5, y: baseY - ux * h * 0.5 }
1635
+ ],
1636
+ fill: color,
1637
+ stroke: "none"
1638
+ });
1639
+ break;
1640
+ }
1641
+ case "rect": {
1642
+ const p1 = project(a.from);
1643
+ const p2 = project(a.to);
1644
+ cmds.push({ type: "rect", x: Math.min(p1.x, p2.x), y: Math.min(p1.y, p2.y), w: Math.abs(p2.x - p1.x), h: Math.abs(p2.y - p1.y), fill: a.fill ?? "none", stroke: a.stroke, opacity: a.opacity });
1645
+ break;
1646
+ }
1647
+ case "point": {
1648
+ const p = project(a.at);
1649
+ const color = a.color ?? "#334155";
1650
+ const r = a.radius ?? theme.annotationRadius;
1651
+ const shape = a.shape ?? "circle";
1652
+ if (shape === "circle") {
1653
+ cmds.push({ type: "circle", cx: p.x, cy: p.y, r, fill: color });
1654
+ } else if (shape === "square") {
1655
+ cmds.push({ type: "rect", x: p.x - r, y: p.y - r, w: r * 2, h: r * 2, fill: color });
1656
+ } else {
1657
+ cmds.push({ type: "line", x1: p.x - r, y1: p.y - r, x2: p.x + r, y2: p.y + r, stroke: color, strokeWidth: 1.5 });
1658
+ cmds.push({ type: "line", x1: p.x - r, y1: p.y + r, x2: p.x + r, y2: p.y - r, stroke: color, strokeWidth: 1.5 });
1659
+ }
1660
+ break;
1661
+ }
1662
+ case "label": {
1663
+ const p = project(a.at);
1664
+ cmds.push({ type: "text", content: a.text, x: p.x + (a.dx ?? 0), y: p.y + (a.dy ?? 0), anchor: a.anchor ?? "middle", fontSize: theme.annotationFontSize, fill: a.color ?? theme.annotationColor, rotate: a.rotate });
1665
+ break;
1666
+ }
1667
+ }
1668
+ }
1669
+ return cmds;
1670
+ }
1671
+
1672
+ // src/renderer/legend_renderer.ts
1673
+ var SWATCH = 12;
1674
+ var GAP = 8;
1675
+ var ITEM_H = 20;
1676
+ var FONT = 11;
1677
+ var H_GAP = 18;
1678
+ var labelWidth = (s) => s.length * FONT * 0.6;
1679
+ function measureLegend(items, orientation = "vertical") {
1680
+ if (orientation === "horizontal") {
1681
+ let w = 0;
1682
+ for (const it of items) w += SWATCH + GAP + labelWidth(it.name) + H_GAP;
1683
+ return { width: Math.max(0, w - H_GAP), height: ITEM_H };
1684
+ }
1685
+ let maxLabel = 0;
1686
+ for (const it of items) maxLabel = Math.max(maxLabel, labelWidth(it.name));
1687
+ return { width: SWATCH + GAP + maxLabel, height: items.length * ITEM_H };
1688
+ }
1689
+ function renderLegend(config) {
1690
+ const { items, x, y, orientation = "vertical" } = config;
1691
+ const commands = [];
1692
+ let cursorX = x;
1693
+ items.forEach((item, i) => {
1694
+ const sx = orientation === "horizontal" ? cursorX : x;
1695
+ const sy = orientation === "horizontal" ? y : y + i * ITEM_H;
1696
+ commands.push({
1697
+ type: "rect",
1698
+ x: sx,
1699
+ y: sy,
1700
+ w: SWATCH,
1701
+ h: SWATCH,
1702
+ fill: item.color,
1703
+ stroke: theme.legendStroke,
1704
+ strokeWidth: 1
1705
+ });
1706
+ commands.push({
1707
+ type: "text",
1708
+ content: item.name,
1709
+ x: sx + SWATCH + GAP,
1710
+ y: sy + SWATCH - 2,
1711
+ fontSize: theme.legendFont,
1712
+ fill: theme.legendText
1713
+ });
1714
+ if (orientation === "horizontal") cursorX += SWATCH + GAP + labelWidth(item.name) + H_GAP;
1715
+ });
1716
+ return { type: "group", cssClass: "chart-legend", commands };
1717
+ }
1718
+
1719
+ // src/renderer/grid_renderer.ts
1720
+ function renderGrid(config) {
1721
+ const {
1722
+ xTicks,
1723
+ yTicks,
1724
+ xRange,
1725
+ yRange,
1726
+ stroke = theme.gridStroke,
1727
+ strokeWidth = theme.gridStrokeWidth,
1728
+ dashed = false,
1729
+ opacity = theme.gridOpacity
1730
+ } = config;
1731
+ const commands = [];
1732
+ if (yTicks) {
1733
+ for (const y of yTicks) {
1734
+ commands.push({ type: "line", x1: xRange[0], y1: y, x2: xRange[1], y2: y, stroke, strokeWidth, dashed, opacity });
1735
+ }
1736
+ }
1737
+ if (xTicks) {
1738
+ for (const x of xTicks) {
1739
+ commands.push({ type: "line", x1: x, y1: yRange[0], x2: x, y2: yRange[1], stroke, strokeWidth, dashed, opacity });
1740
+ }
1741
+ }
1742
+ return commands;
1743
+ }
1744
+
1745
+ // src/analyze/aggregator.ts
1746
+ function detectGaps(data, minGapMs = 6e4) {
1747
+ if (data.length < 2) return [];
1748
+ const sorted = [...data].sort((a, b) => a.time - b.time);
1749
+ const gaps = [];
1750
+ for (let i = 1; i < sorted.length; i++) {
1751
+ const gap = sorted[i].time - sorted[i - 1].time;
1752
+ if (gap > minGapMs) {
1753
+ gaps.push({
1754
+ startTime: sorted[i - 1].time,
1755
+ endTime: sorted[i].time
1756
+ });
1757
+ }
1758
+ }
1759
+ return gaps;
1760
+ }
1761
+
1762
+ // src/series/fill_spec_renderer.ts
1763
+ function projectBound(bound, defaultBound, ctx) {
1764
+ const b = bound ?? defaultBound;
1765
+ if (b === "series") return null;
1766
+ if (b === "chartTop") return ctx.chartTop;
1767
+ if (b === "chartBottom") return ctx.chartBottom;
1768
+ if (typeof b === "object" && "threshold" in b) {
1769
+ const t = ctx.thresholds.get(b.threshold);
1770
+ if (!t) {
1771
+ throw new Error(
1772
+ `fillSpec region references unknown threshold '${b.threshold}'`
1773
+ );
1774
+ }
1775
+ return ctx.valueScale.map(t.value);
1776
+ }
1777
+ if (typeof b === "object" && "value" in b) {
1778
+ return ctx.valueScale.map(b.value);
1779
+ }
1780
+ return null;
1781
+ }
1782
+ function unpackFill(fill) {
1783
+ if (typeof fill === "string") return { color: fill };
1784
+ return { color: fill.color, hatch: fill.hatch };
1785
+ }
1786
+ function renderRegion(region, ctx, regionIndex) {
1787
+ const fromY = projectBound(region.from, "chartBottom", ctx);
1788
+ const toY = projectBound(region.to, "series", ctx);
1789
+ const { color, hatch } = unpackFill(region.fill);
1790
+ const baseAttrs = {
1791
+ type: "path",
1792
+ fill: color,
1793
+ hatch,
1794
+ stroke: "none",
1795
+ ...ctx.idPrefix && { id: `${ctx.idPrefix}-fill-r${regionIndex}` }
1796
+ };
1797
+ if (fromY !== null && toY !== null) {
1798
+ const range = ctx.timeScale.range();
1799
+ const xMin = range[0];
1800
+ const xMax = range[1];
1801
+ const yTop = Math.min(fromY, toY);
1802
+ const yBot = Math.max(fromY, toY);
1803
+ return [
1804
+ {
1805
+ ...baseAttrs,
1806
+ points: [
1807
+ { x: xMin, y: yTop },
1808
+ { x: xMax, y: yTop },
1809
+ { x: xMax, y: yBot },
1810
+ { x: xMin, y: yBot }
1811
+ ]
1812
+ }
1813
+ ];
1814
+ }
1815
+ const fixedY = fromY ?? toY;
1816
+ const cmds = [];
1817
+ let fixedValue = recoverFixedValue(region, ctx);
1818
+ if (fixedValue === null) {
1819
+ const d = ctx.valueScale.domain();
1820
+ fixedValue = (fromY ?? toY) === ctx.chartBottom ? d[0] : d[1];
1821
+ }
1822
+ const outerValue = recoverBoundValue(region.outer, ctx);
1823
+ for (const run of ctx.runs) {
1824
+ if (run.length < 2) continue;
1825
+ const split = SeriesProcessor.splitByThreshold(
1826
+ run,
1827
+ fixedValue,
1828
+ (p) => p.value ?? fixedValue,
1829
+ SeriesProcessor.interpolateDataPoint
1830
+ );
1831
+ let segments = region.side === "above" ? split.above : region.side === "below" ? split.below : [...split.above, ...split.below];
1832
+ if (outerValue !== null && region.side) {
1833
+ const keep = region.side === "above" ? "below" : "above";
1834
+ segments = segments.flatMap((seg) => {
1835
+ if (seg.length < 2) return [];
1836
+ const sub = SeriesProcessor.splitByThreshold(
1837
+ seg,
1838
+ outerValue,
1839
+ (p) => p.value ?? outerValue,
1840
+ SeriesProcessor.interpolateDataPoint
1841
+ );
1842
+ return keep === "above" ? sub.above : sub.below;
1843
+ });
1844
+ }
1845
+ for (const seg of segments) {
1846
+ if (seg.length < 2) continue;
1847
+ const ptsCurve = seg.map((p) => ({
1848
+ x: ctx.timeScale.map(p.time),
1849
+ y: ctx.valueScale.map(p.value)
1850
+ }));
1851
+ const ptsBase = [...ptsCurve].reverse().map((p) => ({ x: p.x, y: fixedY }));
1852
+ cmds.push({
1853
+ ...baseAttrs,
1854
+ smoothing: ctx.smoothing,
1855
+ points: [...ptsCurve, ...ptsBase]
1856
+ });
1857
+ }
1858
+ }
1859
+ return cmds;
1860
+ }
1861
+ function recoverFixedValue(region, ctx) {
1862
+ const fixed = region.from === "series" ? region.to : region.from;
1863
+ return recoverBoundValue(fixed, ctx);
1864
+ }
1865
+ function recoverBoundValue(bound, ctx) {
1866
+ if (bound === void 0 || bound === "series") return null;
1867
+ if (bound === "chartTop" || bound === "chartBottom") return null;
1868
+ if (typeof bound === "object" && "threshold" in bound) {
1869
+ return ctx.thresholds.get(bound.threshold)?.value ?? null;
1870
+ }
1871
+ if (typeof bound === "object" && "value" in bound) return bound.value;
1872
+ return null;
1873
+ }
1874
+ function renderFillSpec(spec, ctx) {
1875
+ if (typeof spec === "string" || !("regions" in spec)) {
1876
+ return renderRegion({ fill: spec }, ctx, 0);
1877
+ }
1878
+ const out = [];
1879
+ spec.regions.forEach((region, i) => {
1880
+ out.push(...renderRegion(region, ctx, i));
1881
+ });
1882
+ return out;
1883
+ }
1884
+
1885
+ // src/analyze/mkt.ts
1886
+ var R = 8.314;
1887
+ var C_TO_K = 273.15;
1888
+ var DEFAULT_ACTIVATION_ENERGY = 83144;
1889
+ function mkt(samples, activationEnergy = DEFAULT_ACTIVATION_ENERGY) {
1890
+ const dhR = activationEnergy / R;
1891
+ let sumExp = 0;
1892
+ let count = 0;
1893
+ for (const v of samples) {
1894
+ if (v === null) continue;
1895
+ sumExp += Math.exp(-dhR / (v + C_TO_K));
1896
+ count++;
1897
+ }
1898
+ if (count === 0) return null;
1899
+ const meanExp = sumExp / count;
1900
+ const mktKelvin = dhR / -Math.log(meanExp);
1901
+ return mktKelvin - C_TO_K;
1902
+ }
1903
+ function rollingMkt(data, windowMs, activationEnergy = DEFAULT_ACTIVATION_ENERGY) {
1904
+ const out = new Array(data.length);
1905
+ let left = 0;
1906
+ for (let i = 0; i < data.length; i++) {
1907
+ const t = data[i].time;
1908
+ const wStart = t - windowMs;
1909
+ while (left < i && data[left].time < wStart) left++;
1910
+ const slice = data.slice(left, i + 1).map((p) => p.value);
1911
+ const value = mkt(slice, activationEnergy);
1912
+ out[i] = { time: t, value, synthetic: true };
1913
+ }
1914
+ return out;
1915
+ }
1916
+
1917
+ // src/analyze/std_dev.ts
1918
+ function stdDev(samples) {
1919
+ let sum = 0;
1920
+ let count = 0;
1921
+ for (const v of samples) {
1922
+ if (v === null) continue;
1923
+ sum += v;
1924
+ count++;
1925
+ }
1926
+ if (count === 0) return null;
1927
+ const mean = sum / count;
1928
+ let sqSum = 0;
1929
+ for (const v of samples) {
1930
+ if (v === null) continue;
1931
+ const d = v - mean;
1932
+ sqSum += d * d;
1933
+ }
1934
+ return Math.sqrt(sqSum / count);
1935
+ }
1936
+ function sampleStdDev(samples) {
1937
+ let sum = 0;
1938
+ let count = 0;
1939
+ for (const v of samples) {
1940
+ if (v === null) continue;
1941
+ sum += v;
1942
+ count++;
1943
+ }
1944
+ if (count < 2) return null;
1945
+ const mean = sum / count;
1946
+ let sqSum = 0;
1947
+ for (const v of samples) {
1948
+ if (v === null) continue;
1949
+ const d = v - mean;
1950
+ sqSum += d * d;
1951
+ }
1952
+ return Math.sqrt(sqSum / (count - 1));
1953
+ }
1954
+ function rollingStdDev(data, windowMs, sample = false) {
1955
+ const out = new Array(data.length);
1956
+ let left = 0;
1957
+ const compute = sample ? sampleStdDev : stdDev;
1958
+ for (let i = 0; i < data.length; i++) {
1959
+ const t = data[i].time;
1960
+ const wStart = t - windowMs;
1961
+ while (left < i && data[left].time < wStart) left++;
1962
+ const slice = data.slice(left, i + 1).map((p) => p.value);
1963
+ const value = compute(slice);
1964
+ out[i] = { time: t, value, synthetic: true };
1965
+ }
1966
+ return out;
1967
+ }
1968
+
1969
+ // src/series/overlay_renderer.ts
1970
+ function rollingMean(data, windowMs) {
1971
+ const out = new Array(data.length);
1972
+ let left = 0;
1973
+ for (let i = 0; i < data.length; i++) {
1974
+ const t = data[i].time;
1975
+ const wStart = t - windowMs;
1976
+ while (left < i && data[left].time < wStart) left++;
1977
+ let sum = 0;
1978
+ let count = 0;
1979
+ for (let j = left; j <= i; j++) {
1980
+ const v = data[j].value;
1981
+ if (v === null) continue;
1982
+ sum += v;
1983
+ count++;
1984
+ }
1985
+ out[i] = {
1986
+ time: t,
1987
+ value: count === 0 ? null : sum / count,
1988
+ synthetic: true
1989
+ };
1990
+ }
1991
+ return out;
1992
+ }
1993
+ function lineStyleFromOverlay(overlay) {
1994
+ const line = overlay.style?.line;
1995
+ const ls = line && !Array.isArray(line) ? line : void 0;
1996
+ return {
1997
+ color: ls?.color ?? theme.stroke,
1998
+ width: ls?.width ?? theme.strokeWidth,
1999
+ dash: ls?.style,
2000
+ smoothing: ls?.smoothing ?? false
2001
+ };
2002
+ }
2003
+ function emitLineFromPoints(points, ctx, overlay, suffix) {
2004
+ const valid = points.filter((p) => p.value !== null);
2005
+ if (valid.length < 2) return [];
2006
+ const pts = valid.map((p) => ({
2007
+ x: ctx.timeScale.map(p.time),
2008
+ y: ctx.valueScale.map(p.value)
2009
+ }));
2010
+ const ls = lineStyleFromOverlay(overlay);
2011
+ const id = ctx.idPrefix ? `${ctx.idPrefix}-overlay-${suffix}` : `overlay-${suffix}`;
2012
+ return [
2013
+ {
2014
+ type: "path",
2015
+ id,
2016
+ points: pts,
2017
+ stroke: ls.color,
2018
+ strokeWidth: ls.width,
2019
+ smoothing: ls.smoothing,
2020
+ dash: ls.dash,
2021
+ fill: "none"
2022
+ }
2023
+ ];
2024
+ }
2025
+ function renderOverlay(overlay, ctx) {
2026
+ switch (overlay.kind) {
2027
+ case "movingAverage": {
2028
+ if (overlay.type === "exponential") ;
2029
+ const computed = rollingMean(ctx.data, overlay.window);
2030
+ return emitLineFromPoints(computed, ctx, overlay, "movingAvg");
2031
+ }
2032
+ case "movingMkt": {
2033
+ const computed = rollingMkt(
2034
+ ctx.data,
2035
+ overlay.window,
2036
+ overlay.activationEnergy
2037
+ );
2038
+ return emitLineFromPoints(computed, ctx, overlay, "movingMkt");
2039
+ }
2040
+ case "limits": {
2041
+ const cmds = [];
2042
+ const range = ctx.timeScale.range();
2043
+ const x1 = range[0];
2044
+ const x2 = range[1];
2045
+ const ls = lineStyleFromOverlay(overlay);
2046
+ const stroke = ls.color;
2047
+ const strokeWidth = ls.width;
2048
+ const dash = ls.dash ?? "dashed";
2049
+ const pre = ctx.idPrefix ? `${ctx.idPrefix}-` : "";
2050
+ if (overlay.high !== void 0) {
2051
+ cmds.push({
2052
+ type: "line",
2053
+ id: `${pre}overlay-limit-high`,
2054
+ x1,
2055
+ y1: ctx.valueScale.map(overlay.high),
2056
+ x2,
2057
+ y2: ctx.valueScale.map(overlay.high),
2058
+ stroke,
2059
+ strokeWidth,
2060
+ dash
2061
+ });
2062
+ }
2063
+ if (overlay.low !== void 0) {
2064
+ cmds.push({
2065
+ type: "line",
2066
+ id: `${pre}overlay-limit-low`,
2067
+ x1,
2068
+ y1: ctx.valueScale.map(overlay.low),
2069
+ x2,
2070
+ y2: ctx.valueScale.map(overlay.low),
2071
+ stroke,
2072
+ strokeWidth,
2073
+ dash
2074
+ });
2075
+ }
2076
+ return cmds;
2077
+ }
2078
+ case "stdDevBand": {
2079
+ const mult = overlay.multiplier ?? 1;
2080
+ const mean = rollingMean(ctx.data, overlay.window);
2081
+ const std = rollingStdDev(ctx.data, overlay.window);
2082
+ const upper = [];
2083
+ const lower = [];
2084
+ for (let i = 0; i < mean.length; i++) {
2085
+ const m = mean[i].value;
2086
+ const s = std[i].value;
2087
+ if (m === null || s === null) continue;
2088
+ const x = ctx.timeScale.map(mean[i].time);
2089
+ upper.push({ x, y: ctx.valueScale.map(m + mult * s) });
2090
+ lower.push({ x, y: ctx.valueScale.map(m - mult * s) });
2091
+ }
2092
+ if (upper.length < 2) return [];
2093
+ const pre = ctx.idPrefix ? `${ctx.idPrefix}-` : "";
2094
+ const cmds = [];
2095
+ const fillSpec = typeof overlay.style?.fill === "string" ? overlay.style.fill : overlay.style?.fill && !("regions" in overlay.style.fill) ? overlay.style.fill : void 0;
2096
+ const bandFill = fillSpec ?? "#94a3b833";
2097
+ const { color, hatch } = unpackFill2(bandFill);
2098
+ cmds.push({
2099
+ type: "path",
2100
+ id: `${pre}overlay-stdDevBand`,
2101
+ points: [...upper, ...[...lower].reverse()],
2102
+ fill: color,
2103
+ hatch,
2104
+ stroke: "none"
2105
+ });
2106
+ const line = overlay.style?.line;
2107
+ const ls = line && !Array.isArray(line) ? line : void 0;
2108
+ if (ls) {
2109
+ const centerPts = mean.filter((p) => p.value !== null).map((p) => ({ x: ctx.timeScale.map(p.time), y: ctx.valueScale.map(p.value) }));
2110
+ if (centerPts.length >= 2) {
2111
+ cmds.push({
2112
+ type: "path",
2113
+ id: `${pre}overlay-stdDevBand-mean`,
2114
+ points: centerPts,
2115
+ stroke: ls.color ?? theme.stroke,
2116
+ strokeWidth: ls.width ?? 1.5,
2117
+ smoothing: ls.smoothing,
2118
+ dash: ls.style,
2119
+ fill: "none"
2120
+ });
2121
+ }
2122
+ }
2123
+ return cmds;
2124
+ }
2125
+ }
2126
+ }
2127
+ function unpackFill2(fill) {
2128
+ if (typeof fill === "string") return { color: fill };
2129
+ return { color: fill.color, hatch: fill.hatch };
2130
+ }
2131
+
2132
+ // src/MLTimeGraph.ts
2133
+ function normalizeGaps(input) {
2134
+ if (!input) return { gaps: [], autoDetect: false, minGapMs: 6e4 };
2135
+ if (Array.isArray(input)) {
2136
+ return { gaps: input, autoDetect: false, minGapMs: 6e4 };
2137
+ }
2138
+ const regions = input.regions ?? [];
2139
+ const def = input.style;
2140
+ const gaps = regions.map((r) => {
2141
+ const merged = { ...def, ...r.style };
2142
+ const fillSpec = merged.fill;
2143
+ let fill;
2144
+ let hatch;
2145
+ if (typeof fillSpec === "string") fill = fillSpec;
2146
+ else if (fillSpec) {
2147
+ fill = fillSpec.color;
2148
+ hatch = fillSpec.hatch;
2149
+ }
2150
+ return {
2151
+ startTime: r.startTime,
2152
+ endTime: r.endTime,
2153
+ label: r.label,
2154
+ fill,
2155
+ hatch,
2156
+ fillOpacity: merged.opacity,
2157
+ labelBaseline: merged.label?.baseline,
2158
+ rotate: merged.label?.rotate,
2159
+ // 'filled' and 'bridge_line' are new display values not in legacy
2160
+ // DrawGapType_t. 'bridge_line' is per-series-only (chart-level gaps
2161
+ // have no series data to interpolate against); both map to undefined
2162
+ // so the renderer falls back to default rendering.
2163
+ style: merged.display === "filled" || merged.display === "bridge_line" ? void 0 : merged.display
2164
+ };
2165
+ });
2166
+ return {
2167
+ gaps,
2168
+ autoDetect: input.autoDetect ?? false,
2169
+ minGapMs: input.minGapMs ?? 6e4
2170
+ };
2171
+ }
2172
+ function isAggregated(s) {
2173
+ return "showAs" in s && !!s.showAs;
2174
+ }
2175
+ var MLTimeGraph = class {
2176
+ _layout;
2177
+ _renderer;
2178
+ _locale;
2179
+ _legend;
2180
+ _markers;
2181
+ _thresholds;
2182
+ _highlights;
2183
+ _gaps;
2184
+ _gapsAutoDetect;
2185
+ _gapsMinGapMs;
2186
+ _annotations;
2187
+ _annotationBands;
2188
+ _disabledAnnotations = /* @__PURE__ */ new Set();
2189
+ _annotationSeq = 0;
2190
+ _axes;
2191
+ _series;
2192
+ _annotationBandHeight = 0;
2193
+ _timeScale;
2194
+ _valueScales = /* @__PURE__ */ new Map();
2195
+ /** @param options Chart configuration; every field is optional and has a sensible default. */
2196
+ constructor(options = {}) {
2197
+ this._layout = new Layout({
2198
+ width: options.width ?? 800,
2199
+ height: options.height ?? 400,
2200
+ margin: options.margin ?? { top: 20, right: 20, bottom: 40, left: 60 }
2201
+ }).compute();
2202
+ this._renderer = options.renderer;
2203
+ this._locale = options.locale;
2204
+ this._legend = options.legend;
2205
+ this._markers = options.markers ?? [];
2206
+ this._thresholds = options.thresholds ?? [];
2207
+ this._highlights = options.highlights ?? [];
2208
+ const normalizedGaps = normalizeGaps(options.gaps);
2209
+ this._gaps = normalizedGaps.gaps;
2210
+ this._gapsAutoDetect = normalizedGaps.autoDetect;
2211
+ this._gapsMinGapMs = normalizedGaps.minGapMs;
2212
+ this._annotations = options.annotations ?? [];
2213
+ this._annotationBands = options.annotationBands ?? [];
2214
+ this._axes = options.axes;
2215
+ this._series = [];
2216
+ if (options.series) {
2217
+ this.setData(options.series);
2218
+ }
2219
+ }
2220
+ getWidth() {
2221
+ return this._layout.totalWidth;
2222
+ }
2223
+ getHeight() {
2224
+ return this._layout.totalHeight + this._annotationBandTotalHeight();
2225
+ }
2226
+ /** Read-only view of the parsed series array (post-`setData`). */
2227
+ get series() {
2228
+ return this._series;
2229
+ }
2230
+ _annotationBandTotalHeight() {
2231
+ let total = 0;
2232
+ for (const band of this._annotationBands) {
2233
+ const bandHeight = band.height ?? 12;
2234
+ const bandSpacing = band.spacing ?? 0;
2235
+ if (band.showAxis ?? false) {
2236
+ total += 26 + bandSpacing;
2237
+ }
2238
+ total += bandHeight + bandSpacing;
2239
+ }
2240
+ return total;
2241
+ }
2242
+ /** Set chart data (raw or aggregated series with a non-empty `data` array). */
2243
+ setData(series) {
2244
+ this._series = series.filter((s) => Array.isArray(s.data));
2245
+ }
2246
+ /** Add a free-form annotation. Returns its id. */
2247
+ addAnnotation(annotation) {
2248
+ const id = annotation.id ?? `anno-${++this._annotationSeq}`;
2249
+ this._annotations.push({ ...annotation, id });
2250
+ return id;
2251
+ }
2252
+ /** Remove an annotation by id. */
2253
+ removeAnnotation(id) {
2254
+ const before = this._annotations.length;
2255
+ this._annotations = this._annotations.filter((a) => a.id !== id);
2256
+ this._disabledAnnotations.delete(id);
2257
+ return this._annotations.length < before;
2258
+ }
2259
+ /** Replace all annotations. */
2260
+ setAnnotations(annotations) {
2261
+ this._annotations = [...annotations];
2262
+ this._disabledAnnotations.clear();
2263
+ }
2264
+ /** Remove all annotations. */
2265
+ clearAnnotations() {
2266
+ this._annotations = [];
2267
+ this._disabledAnnotations.clear();
2268
+ }
2269
+ /** Current annotations (read-only snapshot). */
2270
+ getAnnotations() {
2271
+ return this._annotations;
2272
+ }
2273
+ /** Hide an annotation by id. */
2274
+ disableAnnotation(id) {
2275
+ this._disabledAnnotations.add(id);
2276
+ }
2277
+ /** Re-show a previously disabled annotation. */
2278
+ enableAnnotation(id) {
2279
+ this._disabledAnnotations.delete(id);
2280
+ }
2281
+ _axisIndexOf(s) {
2282
+ return s.yAxisIndex ?? 0;
2283
+ }
2284
+ _timesOf(s) {
2285
+ return s.data.map((p) => p.time);
2286
+ }
2287
+ _valuesOf(s) {
2288
+ if (isAggregated(s)) {
2289
+ const out = [];
2290
+ for (const p of s.data) {
2291
+ if (p.min !== null) out.push(p.min);
2292
+ if (p.max !== null) out.push(p.max);
2293
+ }
2294
+ return out;
2295
+ }
2296
+ return s.data.map((p) => p.value).filter((v) => v !== null);
2297
+ }
2298
+ /**
2299
+ * Compute the chart's renderer-agnostic draw commands.
2300
+ */
2301
+ renderCommands() {
2302
+ if (this._series.length === 0) return [];
2303
+ const legendItems = this._legendItems();
2304
+ const legendShow = (this._legend?.show ?? false) && legendItems.length > 0;
2305
+ const legendPos = this._legend?.position ?? "inside-right";
2306
+ const legendOrient = this._legend?.orientation ?? "vertical";
2307
+ const legendSize = legendShow ? measureLegend(legendItems, legendOrient) : { width: 0};
2308
+ let layout = this._layout;
2309
+ if (legendShow && (legendPos === "outside-right" || legendPos === "outside-left")) {
2310
+ const reserve = legendSize.width + 16;
2311
+ const m = { ...this._layout.margin };
2312
+ if (legendPos === "outside-right") m.right += reserve;
2313
+ else m.left += reserve;
2314
+ layout = new Layout({ width: this._layout.totalWidth, height: this._layout.totalHeight, margin: m }).compute();
2315
+ }
2316
+ const { chartX, chartY, chartWidth, chartHeight } = layout;
2317
+ const xRange = [chartX, chartX + chartWidth];
2318
+ const yRange = [chartY, chartY + chartHeight];
2319
+ const commands = [];
2320
+ for (const s of this._series) {
2321
+ s.data.sort((a, b) => a.time - b.time);
2322
+ }
2323
+ const seriesByIndex = /* @__PURE__ */ new Map();
2324
+ let tMin = Infinity;
2325
+ let tMax = -Infinity;
2326
+ let hasTime = false;
2327
+ for (const s of this._series) {
2328
+ const idx = this._axisIndexOf(s);
2329
+ const bucket = seriesByIndex.get(idx);
2330
+ if (bucket) bucket.push(s);
2331
+ else seriesByIndex.set(idx, [s]);
2332
+ for (const t of this._timesOf(s)) {
2333
+ if (t < tMin) tMin = t;
2334
+ if (t > tMax) tMax = t;
2335
+ hasTime = true;
2336
+ }
2337
+ }
2338
+ if (!hasTime) return [];
2339
+ const xDomainCfg = this._axes?.x?.domain;
2340
+ const timeDomain = xDomainCfg && xDomainCfg !== "auto" ? xDomainCfg : [tMin, tMax];
2341
+ this._timeScale = new TimeScale({ domain: timeDomain, range: xRange, locale: this._locale });
2342
+ const buildAxisColors = (a) => ({
2343
+ axisColor: a?.color ?? theme.axisColor,
2344
+ tickColor: a?.color ?? theme.tickColor,
2345
+ textColor: theme.textColor,
2346
+ textSize: theme.textSize,
2347
+ axisWidth: a?.width
2348
+ });
2349
+ const timeAxis = new TimeAxis({
2350
+ domain: timeDomain,
2351
+ xRange,
2352
+ y: chartY + chartHeight,
2353
+ locale: this._locale,
2354
+ // Phase 3.13 — wire `axes.x.format` + `axes.x.ticks.major`
2355
+ format: this._axes?.x?.format,
2356
+ maxTicks: this._axes?.x?.ticks?.major,
2357
+ colors: buildAxisColors(this._axes?.x?.axis)
2358
+ });
2359
+ this._valueScales.clear();
2360
+ const indices = Array.from(seriesByIndex.keys()).sort((a, b) => a - b);
2361
+ let primaryValueAxis;
2362
+ for (const idx of indices) {
2363
+ const group = seriesByIndex.get(idx);
2364
+ let vMin = Infinity;
2365
+ let vMax = -Infinity;
2366
+ let hasValue = false;
2367
+ for (const s of group) {
2368
+ for (const v of this._valuesOf(s)) {
2369
+ if (v < vMin) vMin = v;
2370
+ if (v > vMax) vMax = v;
2371
+ hasValue = true;
2372
+ }
2373
+ }
2374
+ if (!hasValue) continue;
2375
+ const axisCfg = idx === 0 ? this._axes?.left : this._axes?.right;
2376
+ const cfgDomain = axisCfg?.domain;
2377
+ const domain = cfgDomain && cfgDomain !== "auto" ? cfgDomain : [vMin, vMax];
2378
+ this._valueScales.set(idx, new LinearScale({ domain, range: [chartY + chartHeight, chartY] }));
2379
+ const vAxis = new ValueAxis({
2380
+ domain,
2381
+ range: [chartY + chartHeight, chartY],
2382
+ x: idx === 0 ? chartX : chartX + chartWidth,
2383
+ position: idx === 0 ? "left" : "right",
2384
+ format: axisCfg?.format,
2385
+ ticks: axisCfg?.ticks?.major,
2386
+ colors: buildAxisColors(axisCfg?.axis)
2387
+ });
2388
+ if (idx === 0) primaryValueAxis = vAxis;
2389
+ commands.push({
2390
+ type: "group",
2391
+ cssClass: `value-axis ${idx === 0 ? "left" : "right"}`,
2392
+ commands: vAxis.render()
2393
+ });
2394
+ }
2395
+ const primaryScale = this._valueScales.get(0) ?? this._valueScales.get(indices[0]);
2396
+ commands.push({ type: "group", cssClass: "time-axis", commands: timeAxis.render() });
2397
+ const xGrid = this._axes?.x?.grid?.major;
2398
+ const leftGrid = this._axes?.left?.grid?.major;
2399
+ if (xGrid !== void 0 || leftGrid !== void 0) {
2400
+ const xEnabled = xGrid !== false;
2401
+ const yEnabled = leftGrid !== false && !!primaryValueAxis;
2402
+ const xTicks = xEnabled ? timeAxis.generateTicks().map((t) => t.x) : void 0;
2403
+ const yTicks = yEnabled ? primaryValueAxis.generateTicks().map((t) => t.position) : void 0;
2404
+ if (xTicks || yTicks) {
2405
+ const chosen = (xGrid && typeof xGrid === "object" ? xGrid : void 0) ?? (leftGrid && typeof leftGrid === "object" ? leftGrid : void 0);
2406
+ commands.push({
2407
+ type: "group",
2408
+ cssClass: "chart-grid",
2409
+ commands: renderGrid({
2410
+ xTicks,
2411
+ yTicks,
2412
+ xRange,
2413
+ yRange,
2414
+ stroke: chosen?.color,
2415
+ opacity: chosen?.opacity,
2416
+ dashed: chosen?.style === "dashed"
2417
+ })
2418
+ });
2419
+ }
2420
+ }
2421
+ if (this._highlights.length > 0) {
2422
+ commands.push({
2423
+ type: "group",
2424
+ cssClass: "highlights",
2425
+ commands: renderHighlights({ highlights: this._highlights, timeScale: this._timeScale, yRange, height: layout.totalHeight })
2426
+ });
2427
+ }
2428
+ if (this._thresholds.length > 0 && primaryScale) {
2429
+ const thresholdGroups = this._thresholds.map((t) => {
2430
+ const slug = t.id ?? slugify(t.name);
2431
+ return {
2432
+ type: "group",
2433
+ cssClass: `threshold threshold--${slug}`,
2434
+ id: `threshold-${slug}`,
2435
+ commands: renderThresholds({ thresholds: [t], valueScale: primaryScale, xRange })
2436
+ };
2437
+ });
2438
+ commands.push({
2439
+ type: "group",
2440
+ cssClass: "thresholds",
2441
+ commands: thresholdGroups,
2442
+ clipRect: { x: chartX, y: chartY, w: chartWidth, h: chartHeight }
2443
+ });
2444
+ }
2445
+ let gapsToRender = this._gaps;
2446
+ if (this._gapsAutoDetect) {
2447
+ const auto = [];
2448
+ for (const s of this._series) {
2449
+ if (isAggregated(s)) continue;
2450
+ auto.push(...detectGaps(s.data, this._gapsMinGapMs));
2451
+ }
2452
+ if (auto.length > 0) gapsToRender = [...this._gaps, ...auto];
2453
+ }
2454
+ if (gapsToRender.length > 0) {
2455
+ commands.push({
2456
+ type: "group",
2457
+ cssClass: "gaps",
2458
+ commands: renderGaps({ gaps: gapsToRender, timeScale: this._timeScale, yRange })
2459
+ });
2460
+ }
2461
+ const thresholdByName = new Map(this._thresholds.map((t) => [t.name, t]));
2462
+ for (const series of this._series) {
2463
+ if (series.data.length === 0) continue;
2464
+ const scale = this._valueScales.get(this._axisIndexOf(series));
2465
+ if (!scale) continue;
2466
+ const slug = series.id ?? slugify(series.name);
2467
+ commands.push({
2468
+ type: "group",
2469
+ cssClass: `series series--${slug}`,
2470
+ id: `series-${slug}`,
2471
+ commands: this._renderSeries(series, scale, thresholdByName)
2472
+ });
2473
+ }
2474
+ const markersToRender = this._markers.map((m) => ({ ...m }));
2475
+ for (const m of markersToRender) {
2476
+ if (m.value === void 0 && (m.lineStyle === "to-value" || m.lineStyle === "to-top")) {
2477
+ const target = this._series[m.seriesIndex ?? 0];
2478
+ if (target && !isAggregated(target) && target.data.length >= 2) {
2479
+ m.value = this._interpolateValue(m.time, target.data);
2480
+ }
2481
+ }
2482
+ }
2483
+ if (markersToRender.length > 0 && primaryScale) {
2484
+ commands.push({
2485
+ type: "group",
2486
+ cssClass: "markers",
2487
+ commands: renderMarkers2({
2488
+ markers: markersToRender,
2489
+ timeScale: this._timeScale,
2490
+ valueScale: primaryScale,
2491
+ yRange
2492
+ })
2493
+ });
2494
+ }
2495
+ const activeAnnotations = this._annotations.filter((a) => !a.id || !this._disabledAnnotations.has(a.id));
2496
+ if (activeAnnotations.length > 0) {
2497
+ commands.push({
2498
+ type: "group",
2499
+ cssClass: "annotations",
2500
+ commands: renderAnnotations({
2501
+ annotations: activeAnnotations,
2502
+ timeScale: this._timeScale,
2503
+ valueScales: this._valueScales
2504
+ })
2505
+ });
2506
+ }
2507
+ if (this._annotationBands.length > 0) {
2508
+ const bandYStart = chartY + chartHeight + this._layout.margin.bottom;
2509
+ let bandOffset = 0;
2510
+ this._annotationBands.forEach((band) => {
2511
+ const bandHeight = band.height ?? 12;
2512
+ const bandSpacing = band.spacing ?? 0;
2513
+ const bandTop = bandYStart + bandOffset;
2514
+ if (band.showAxis ?? false) {
2515
+ bandOffset += 26 + bandSpacing;
2516
+ }
2517
+ bandOffset += bandHeight + bandSpacing;
2518
+ commands.push({
2519
+ type: "group",
2520
+ cssClass: "annotation-band",
2521
+ commands: new AnnotationBandSeries({
2522
+ name: band.name,
2523
+ showAxis: band.showAxis ?? false,
2524
+ items: band.items,
2525
+ timeScale: this._timeScale,
2526
+ background: band.background,
2527
+ hatch: band.hatch
2528
+ }, [chartX, chartX + chartWidth], bandTop, bandHeight).render()
2529
+ });
2530
+ });
2531
+ }
2532
+ if (legendShow && legendPos !== "separate") {
2533
+ let lx;
2534
+ let ly;
2535
+ if (legendPos === "inside-right") {
2536
+ lx = chartX + chartWidth - legendSize.width - 8;
2537
+ ly = chartY + 8;
2538
+ } else if (legendPos === "inside-left") {
2539
+ lx = chartX + 8;
2540
+ ly = chartY + 8;
2541
+ } else if (legendPos === "outside-right") {
2542
+ lx = chartX + chartWidth + 16;
2543
+ ly = chartY;
2544
+ } else {
2545
+ lx = 8;
2546
+ ly = chartY;
2547
+ }
2548
+ commands.push(renderLegend({ items: legendItems, x: lx, y: ly, orientation: legendOrient }));
2549
+ }
2550
+ const leftLabel = this._axes?.left?.label;
2551
+ const rightLabel = this._axes?.right?.label;
2552
+ const xLabel = this._axes?.x?.label;
2553
+ if (leftLabel || rightLabel || xLabel) {
2554
+ const labelCmds = [];
2555
+ const midY = chartY + chartHeight / 2;
2556
+ const leftStyle = this._axes?.left?.labels;
2557
+ const rightStyle = this._axes?.right?.labels;
2558
+ const xStyle = this._axes?.x?.labels;
2559
+ if (leftLabel) labelCmds.push({
2560
+ type: "text",
2561
+ content: leftLabel,
2562
+ x: 14,
2563
+ y: midY,
2564
+ anchor: "middle",
2565
+ fontSize: leftStyle?.fontSize ?? theme.axisLabelSize,
2566
+ fill: leftStyle?.color ?? theme.axisLabelColor,
2567
+ rotate: -90
2568
+ });
2569
+ if (rightLabel) labelCmds.push({
2570
+ type: "text",
2571
+ content: rightLabel,
2572
+ x: layout.totalWidth - 14,
2573
+ y: midY,
2574
+ anchor: "middle",
2575
+ fontSize: rightStyle?.fontSize ?? theme.axisLabelSize,
2576
+ fill: rightStyle?.color ?? theme.axisLabelColor,
2577
+ rotate: 90
2578
+ });
2579
+ if (xLabel) labelCmds.push({
2580
+ type: "text",
2581
+ content: xLabel,
2582
+ x: chartX + chartWidth / 2,
2583
+ y: layout.totalHeight - 6,
2584
+ anchor: "middle",
2585
+ fontSize: xStyle?.fontSize ?? theme.axisLabelSize,
2586
+ fill: xStyle?.color ?? theme.axisLabelColor
2587
+ });
2588
+ if (labelCmds.length) commands.push({ type: "group", cssClass: "axis-labels", commands: labelCmds });
2589
+ }
2590
+ return commands;
2591
+ }
2592
+ /** Build the draw commands for a single series based on its type. */
2593
+ _renderSeries(series, scale, thresholds) {
2594
+ const timeScale = this._timeScale;
2595
+ const ctx = { timeScale, valueScale: scale };
2596
+ if (isAggregated(series)) {
2597
+ const aggLine = series.style?.line && !Array.isArray(series.style.line) ? series.style.line : void 0;
2598
+ const aggFillStyle = typeof series.style?.fill === "string" ? series.style.fill : void 0;
2599
+ if (series.showAs === "minmaxavg") {
2600
+ return new MinMaxAvgSeries({
2601
+ data: series.data,
2602
+ timeScale,
2603
+ valueScale: scale,
2604
+ minColor: series.minColor,
2605
+ maxColor: series.maxColor,
2606
+ avgColor: series.avgColor,
2607
+ avgDashed: series.avgDashed,
2608
+ fillToMax: series.fillToMax,
2609
+ fillToMaxHatch: series.fillToMaxHatch,
2610
+ fillToMin: series.fillToMin,
2611
+ fillToMinHatch: series.fillToMinHatch,
2612
+ smoothing: aggLine?.smoothing,
2613
+ strokeWidth: aggLine?.width,
2614
+ id: series.id
2615
+ }).render();
2616
+ }
2617
+ return new BandSeries({
2618
+ data: series.data,
2619
+ timeScale,
2620
+ valueScale: scale,
2621
+ fill: aggFillStyle ?? aggLine?.color,
2622
+ avgLine: series.avgLine,
2623
+ countOpacity: series.countOpacity,
2624
+ id: series.id
2625
+ }).render();
2626
+ }
2627
+ const s = series;
2628
+ const lineDefaults = series.style?.line && !Array.isArray(series.style.line) ? series.style.line : void 0;
2629
+ const gapThreshold = lineDefaults?.gapThreshold ?? theme.gapThreshold;
2630
+ const runs = SeriesProcessor.getRuns(series.data, (p) => p.value === null, gapThreshold);
2631
+ const totalPoints = runs.reduce((sum, run) => sum + run.length, 0);
2632
+ if (totalPoints === 0) return [];
2633
+ const cmds = [];
2634
+ const styleMarkers = series.style?.markers;
2635
+ const styleShadow = series.style?.shadow;
2636
+ const style = {
2637
+ stroke: lineDefaults?.color ?? theme.stroke,
2638
+ strokeWidth: lineDefaults?.width ?? theme.strokeWidth,
2639
+ smoothing: lineDefaults?.smoothing,
2640
+ dashed: lineDefaults?.style === "dashed",
2641
+ pointStyle: styleMarkers?.type,
2642
+ pointSize: styleMarkers?.size,
2643
+ pointStroke: styleMarkers?.stroke,
2644
+ pointFill: styleMarkers?.fill,
2645
+ pointStrokeWidth: styleMarkers?.strokeWidth,
2646
+ shadowColor: styleShadow?.color,
2647
+ shadowBlur: styleShadow?.blur,
2648
+ shadowOffsetX: styleShadow?.offsetX,
2649
+ shadowOffsetY: styleShadow?.offsetY,
2650
+ id: series.id
2651
+ };
2652
+ const styleLine = series.style?.line;
2653
+ const lineOverride = styleLine && !Array.isArray(styleLine) ? styleLine : void 0;
2654
+ const lineColorOverride = lineOverride?.color;
2655
+ const lineWidthOverride = lineOverride?.width;
2656
+ const lineDash = lineOverride?.style;
2657
+ const lineSmoothing = lineOverride?.smoothing;
2658
+ const newFillSpec = series.style?.fill;
2659
+ if (newFillSpec !== void 0) {
2660
+ const r = scale.range();
2661
+ const chartTop = Math.min(r[0], r[1]);
2662
+ const chartBottom = Math.max(r[0], r[1]);
2663
+ cmds.push(...renderFillSpec(newFillSpec, {
2664
+ runs,
2665
+ timeScale,
2666
+ valueScale: scale,
2667
+ thresholds,
2668
+ chartTop,
2669
+ chartBottom,
2670
+ smoothing: lineDefaults?.smoothing,
2671
+ idPrefix: series.id
2672
+ }));
2673
+ }
2674
+ const colorThresholdNames = s.colorByThresholds ?? [];
2675
+ const boundaries = colorThresholdNames.map((n) => thresholds.get(n)).filter((t) => !!t).map((t) => t.value).sort((a, b) => a - b);
2676
+ const getZoneColor = (val, names) => {
2677
+ let color = style.stroke;
2678
+ for (const n of names) {
2679
+ const t = thresholds.get(n);
2680
+ if (t && val >= t.value) color = t.color ?? color;
2681
+ }
2682
+ return color;
2683
+ };
2684
+ for (const run of runs) {
2685
+ if (run.length < 2) continue;
2686
+ const segments = SeriesProcessor.splitByBoundaries(run, boundaries, (p) => p.value, SeriesProcessor.interpolateDataPoint);
2687
+ for (let segIdx = 0; segIdx < segments.length; segIdx++) {
2688
+ const seg = segments[segIdx];
2689
+ if (seg.data.length < 2) continue;
2690
+ const midVal = (seg.data[0].value + seg.data[seg.data.length - 1].value) / 2;
2691
+ const seriesId = s.id;
2692
+ const segPts = seg.data.map((p) => ({ x: timeScale.map(p.time), y: scale.map(p.value) }));
2693
+ const segLineId = seriesId ? `${seriesId}-line-${segIdx}` : void 0;
2694
+ if (styleLine === false) ; else if (styleLine && Array.isArray(styleLine)) {
2695
+ styleLine.forEach((ls, li) => {
2696
+ cmds.push({
2697
+ type: "path",
2698
+ id: segLineId ? `${segLineId}-${li}` : void 0,
2699
+ points: segPts,
2700
+ stroke: ls.color ?? getZoneColor(midVal, colorThresholdNames),
2701
+ strokeWidth: ls.width ?? style.strokeWidth,
2702
+ smoothing: ls.smoothing ?? style.smoothing,
2703
+ dash: ls.style,
2704
+ opacity: ls.opacity,
2705
+ fill: "none",
2706
+ shadowColor: style.shadowColor,
2707
+ shadowBlur: style.shadowBlur,
2708
+ shadowOffsetX: style.shadowOffsetX,
2709
+ shadowOffsetY: style.shadowOffsetY
2710
+ });
2711
+ });
2712
+ } else {
2713
+ cmds.push({
2714
+ type: "path",
2715
+ id: segLineId,
2716
+ points: segPts,
2717
+ stroke: lineColorOverride ?? getZoneColor(midVal, colorThresholdNames),
2718
+ strokeWidth: lineWidthOverride ?? style.strokeWidth,
2719
+ smoothing: lineSmoothing ?? style.smoothing,
2720
+ dash: lineDash,
2721
+ fill: "none",
2722
+ shadowColor: style.shadowColor,
2723
+ shadowBlur: style.shadowBlur,
2724
+ shadowOffsetX: style.shadowOffsetX,
2725
+ shadowOffsetY: style.shadowOffsetY
2726
+ });
2727
+ }
2728
+ }
2729
+ if (style.pointStyle && style.pointStyle !== "none" && totalPoints <= (styleMarkers?.threshold ?? theme.pointThreshold)) {
2730
+ cmds.push(...renderMarkers(run, ctx, style, (p) => getZoneColor(p.value, colorThresholdNames)));
2731
+ }
2732
+ }
2733
+ const overlays = series.overlays;
2734
+ if (overlays && overlays.length > 0) {
2735
+ const r = scale.range();
2736
+ const overlayCtx = {
2737
+ data: series.data,
2738
+ timeScale,
2739
+ valueScale: scale,
2740
+ chartTop: Math.min(r[0], r[1]),
2741
+ chartBottom: Math.max(r[0], r[1]),
2742
+ idPrefix: series.id
2743
+ };
2744
+ for (const overlay of overlays) {
2745
+ cmds.push(...renderOverlay(overlay, overlayCtx));
2746
+ }
2747
+ }
2748
+ if (series.style?.gap && runs.length > 1) {
2749
+ const r = scale.range();
2750
+ const yTop = Math.min(r[0], r[1]);
2751
+ const yBot = Math.max(r[0], r[1]);
2752
+ const seriesGap = series.style.gap;
2753
+ const fillSpec = seriesGap.fill;
2754
+ let fill;
2755
+ let hatch;
2756
+ if (typeof fillSpec === "string") fill = fillSpec;
2757
+ else if (fillSpec) {
2758
+ fill = fillSpec.color;
2759
+ hatch = fillSpec.hatch;
2760
+ }
2761
+ const opacity = seriesGap.opacity ?? 0.15;
2762
+ const bridge = seriesGap.bridge;
2763
+ for (let i = 1; i < runs.length; i++) {
2764
+ const prevPt = runs[i - 1][runs[i - 1].length - 1];
2765
+ const nextPt = runs[i][0];
2766
+ const x1 = timeScale.map(prevPt.time);
2767
+ const x2 = timeScale.map(nextPt.time);
2768
+ if (seriesGap.display === "bridge_line") {
2769
+ if (prevPt.value === null || nextPt.value === null) continue;
2770
+ const y1 = scale.map(prevPt.value);
2771
+ const y2 = scale.map(nextPt.value);
2772
+ cmds.push({
2773
+ type: "line",
2774
+ x1,
2775
+ y1,
2776
+ x2,
2777
+ y2,
2778
+ stroke: bridge?.color ?? (typeof series.style?.line === "object" && !Array.isArray(series.style.line) ? series.style.line.color : void 0) ?? theme.stroke,
2779
+ strokeWidth: bridge?.width ?? 1.5,
2780
+ dash: bridge?.style ?? "dotted"
2781
+ });
2782
+ } else if (fill !== void 0) {
2783
+ cmds.push({
2784
+ type: "rect",
2785
+ x: x1,
2786
+ y: yTop,
2787
+ w: x2 - x1,
2788
+ h: yBot - yTop,
2789
+ fill,
2790
+ hatch,
2791
+ opacity,
2792
+ stroke: "none"
2793
+ });
2794
+ } else if (seriesGap.display !== "empty") {
2795
+ cmds.push({
2796
+ type: "rect",
2797
+ x: x1,
2798
+ y: yTop,
2799
+ w: x2 - x1,
2800
+ h: yBot - yTop,
2801
+ stroke: theme.gapStroke,
2802
+ strokeWidth: 1,
2803
+ dashed: true,
2804
+ fill: "none"
2805
+ });
2806
+ }
2807
+ }
2808
+ }
2809
+ return cmds;
2810
+ }
2811
+ _interpolateValue(time, data) {
2812
+ for (let i = 1; i < data.length; i++) {
2813
+ const p1 = data[i - 1];
2814
+ const p2 = data[i];
2815
+ if (p1.value === null || p2.value === null) continue;
2816
+ if (time >= p1.time && time <= p2.time) {
2817
+ const t = (time - p1.time) / (p2.time - p1.time);
2818
+ return p1.value + t * (p2.value - p1.value);
2819
+ }
2820
+ }
2821
+ return void 0;
2822
+ }
2823
+ legendItems() {
2824
+ return this._legendItems();
2825
+ }
2826
+ _legendItems() {
2827
+ return this._series.map((s) => {
2828
+ const line = s.style?.line && !Array.isArray(s.style.line) ? s.style.line : void 0;
2829
+ return { name: s.name, color: line?.color ?? theme.stroke };
2830
+ });
2831
+ }
2832
+ get renderer() {
2833
+ return this._renderer;
2834
+ }
2835
+ get layout() {
2836
+ return this._layout;
2837
+ }
2838
+ get timeScale() {
2839
+ return this._timeScale;
2840
+ }
2841
+ get valueScales() {
2842
+ return this._valueScales;
2843
+ }
2844
+ invertTime(x) {
2845
+ if (!this._timeScale) return 0;
2846
+ return this._timeScale.invert(x);
2847
+ }
2848
+ invertValue(y, axisIndex = 0) {
2849
+ const scale = this._valueScales.get(axisIndex);
2850
+ if (!scale) return 0;
2851
+ return scale.invert(y);
2852
+ }
2853
+ project(time, value, axisIndex = 0) {
2854
+ const x = this._timeScale ? this._timeScale.map(time) : 0;
2855
+ const scale = this._valueScales.get(axisIndex);
2856
+ return { x, y: scale ? scale.map(value) : 0 };
2857
+ }
2858
+ };
2859
+
2860
+ // src/renderer/renderer.ts
2861
+ var Renderer = class {
2862
+ };
2863
+
2864
+ // src/renderer/svg_renderer.ts
2865
+ var FIXED_FRAC = 1e3;
2866
+ var SVGRenderer = class extends Renderer {
2867
+ #width = "100%";
2868
+ #height = "100%";
2869
+ #filters = /* @__PURE__ */ new Map();
2870
+ #clipPaths = /* @__PURE__ */ new Map();
2871
+ #patterns = /* @__PURE__ */ new Map();
2872
+ constructor(options) {
2873
+ super();
2874
+ if (options?.width !== void 0) this.#width = options.width;
2875
+ if (options?.height !== void 0) this.#height = options.height;
2876
+ }
2877
+ render(commands) {
2878
+ this.#filters.clear();
2879
+ this.#clipPaths.clear();
2880
+ this.#patterns.clear();
2881
+ const elements = commands.map((c) => this._toSVG(c)).join("\n ");
2882
+ let defs = "";
2883
+ const allDefs = [];
2884
+ for (const [, filter] of this.#filters) {
2885
+ allDefs.push(filter);
2886
+ }
2887
+ for (const [, clip] of this.#clipPaths) {
2888
+ allDefs.push(clip);
2889
+ }
2890
+ for (const [, pattern] of this.#patterns) {
2891
+ allDefs.push(pattern);
2892
+ }
2893
+ if (allDefs.length > 0) {
2894
+ defs = ` <defs>
2895
+ ${allDefs.join("\n ")}
2896
+ </defs>
2897
+ `;
2898
+ }
2899
+ const w = typeof this.#width === "number" ? `${this.#width}` : this.#width;
2900
+ const h = typeof this.#height === "number" ? `${this.#height}` : this.#height;
2901
+ const vb = typeof this.#width === "number" && typeof this.#height === "number" ? ` viewBox="0 0 ${this.#width} ${this.#height}"` : "";
2902
+ return {
2903
+ type: "svg",
2904
+ content: `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}"${vb}>
2905
+ ${defs} ${elements}
2906
+ </svg>`
2907
+ };
2908
+ }
2909
+ /* ── Intern: Command → SVG-Element ── */
2910
+ _toSVG(cmd) {
2911
+ switch (cmd.type) {
2912
+ case "path":
2913
+ return this._path(cmd);
2914
+ case "line":
2915
+ return this._line(cmd);
2916
+ case "rect":
2917
+ return this._rect(cmd);
2918
+ case "circle":
2919
+ return this._circle(cmd);
2920
+ case "text":
2921
+ return this.#text(cmd);
2922
+ case "gradient":
2923
+ return this._gradient(cmd);
2924
+ case "gap":
2925
+ return this._gap(cmd);
2926
+ case "group":
2927
+ return this._group(cmd);
2928
+ }
2929
+ }
2930
+ /** Build the path `d`: straight segments, or a Catmull-Rom spline when smoothing. */
2931
+ #pathD(pts, smoothing) {
2932
+ if (pts.length === 0) return "";
2933
+ if (!smoothing || pts.length < 3) {
2934
+ return pts.map((p, i) => `${i === 0 ? "M" : "L"}${p.x},${p.y}`).join(" ");
2935
+ }
2936
+ const r = (n) => Math.round(n * 100) / 100;
2937
+ let d = `M${pts[0].x},${pts[0].y}`;
2938
+ for (let i = 0; i < pts.length - 1; i++) {
2939
+ const p0 = pts[i - 1] ?? pts[i];
2940
+ const p1 = pts[i];
2941
+ const p2 = pts[i + 1];
2942
+ const p3 = pts[i + 2] ?? p2;
2943
+ let c1x = r(p1.x + (p2.x - p0.x) / 6);
2944
+ let c1y = r(p1.y + (p2.y - p0.y) / 6);
2945
+ let c2x = r(p2.x - (p3.x - p1.x) / 6);
2946
+ let c2y = r(p2.y - (p3.y - p1.y) / 6);
2947
+ {
2948
+ c1x = (c1x * FIXED_FRAC | 0) / FIXED_FRAC;
2949
+ c1y = (c1y * FIXED_FRAC | 0) / FIXED_FRAC;
2950
+ c2x = (c2x * FIXED_FRAC | 0) / FIXED_FRAC;
2951
+ c2y = (c2y * FIXED_FRAC | 0) / FIXED_FRAC;
2952
+ }
2953
+ d += ` C${c1x},${c1y} ${c2x},${c2y} ${p2.x},${p2.y}`;
2954
+ }
2955
+ return d;
2956
+ }
2957
+ /** Register or retrieve a hatch pattern by variant. Returns the pattern id for use as fill="url(#id)". */
2958
+ _hatchPattern(variant, fillColor) {
2959
+ const id = `hatch-${variant}-${this._hatchIndex++}`;
2960
+ if (this.#patterns.has(id)) return id;
2961
+ const svg = getHatch(id, variant, fillColor ?? "rgba(200,220,255,0.3)");
2962
+ this.#patterns.set(id, svg);
2963
+ return id;
2964
+ }
2965
+ /** Monotonic counter for unique hatch pattern ids. */
2966
+ _hatchIndex = 0;
2967
+ _path(c) {
2968
+ const d = this.#pathD(c.points, c.smoothing);
2969
+ const att = [];
2970
+ if (c.hatch) {
2971
+ const hatchId = this._hatchPattern(c.hatch, c.fill);
2972
+ att.push(`fill="url(#${hatchId})"`);
2973
+ } else {
2974
+ att.push(`fill="${c.fill ? this._esc(c.fill) : "none"}"`);
2975
+ }
2976
+ if (c.stroke) att.push(`stroke="${this._esc(c.stroke)}"`);
2977
+ if (c.strokeWidth) att.push(`stroke-width="${c.strokeWidth}"`);
2978
+ const pdash = this.#dashArray(c.dash, c.dashed, c.strokeWidth);
2979
+ if (pdash) {
2980
+ att.push(`stroke-dasharray="${pdash.strokeDasharray}"`);
2981
+ if (pdash.strokeLinecap) att.push(`stroke-linecap="${pdash.strokeLinecap}"`);
2982
+ }
2983
+ if (c.opacity !== void 0) att.push(`opacity="${c.opacity}"`);
2984
+ if (c.id) att.push(`id="${this._esc(c.id)}"`);
2985
+ const filterId = this.#getShadowFilter(c);
2986
+ if (filterId) att.push(`filter="url(#${filterId})"`);
2987
+ return `<path d="${d}" ${att.join(" ")} />`;
2988
+ }
2989
+ _line(c) {
2990
+ const att = [];
2991
+ if (c.stroke) att.push(`stroke="${this._esc(c.stroke)}"`);
2992
+ if (c.strokeWidth) att.push(`stroke-width="${c.strokeWidth}"`);
2993
+ const ldash = this.#dashArray(c.dash, c.dashed, c.strokeWidth);
2994
+ if (ldash) {
2995
+ att.push(`stroke-dasharray="${ldash.strokeDasharray}"`);
2996
+ if (ldash.strokeLinecap) att.push(`stroke-linecap="${ldash.strokeLinecap}"`);
2997
+ }
2998
+ if (c.opacity !== void 0) att.push(`opacity="${c.opacity}"`);
2999
+ if (c.id) att.push(`id="${this._esc(c.id)}"`);
3000
+ const filterId = this.#getShadowFilter(c);
3001
+ if (filterId) att.push(`filter="url(#${filterId})"`);
3002
+ return `<line x1="${c.x1}" y1="${c.y1}" x2="${c.x2}" y2="${c.y2}" ${att.join(" ")} />`;
3003
+ }
3004
+ _rect(c) {
3005
+ const att = [];
3006
+ if (c.hatch) {
3007
+ const hatchId = this._hatchPattern(c.hatch, c.fill);
3008
+ att.push(`fill="url(#${hatchId})"`);
3009
+ } else if (c.fill !== void 0) {
3010
+ att.push(`fill="${this._esc(c.fill)}"`);
3011
+ }
3012
+ if (c.stroke) att.push(`stroke="${this._esc(c.stroke)}"`);
3013
+ if (c.strokeWidth) att.push(`stroke-width="${c.strokeWidth}"`);
3014
+ if (c.opacity !== void 0) att.push(`opacity="${c.opacity}"`);
3015
+ if (c.dashed) att.push(`stroke-dasharray="4,4"`);
3016
+ if (c.id) att.push(`id="${this._esc(c.id)}"`);
3017
+ const filterId = this.#getShadowFilter(c);
3018
+ if (filterId) att.push(`filter="url(#${filterId})"`);
3019
+ return `<rect x="${c.x}" y="${c.y}" width="${c.w}" height="${c.h}" ${att.join(" ")} />`;
3020
+ }
3021
+ _circle(c) {
3022
+ const att = [];
3023
+ if (c.hatch) {
3024
+ const hatchId = this._hatchPattern(c.hatch, c.fill);
3025
+ att.push(`fill="url(#${hatchId})"`);
3026
+ } else if (c.fill) {
3027
+ att.push(`fill="${this._esc(c.fill)}"`);
3028
+ }
3029
+ if (c.stroke) att.push(`stroke="${this._esc(c.stroke)}"`);
3030
+ if (c.strokeWidth) att.push(`stroke-width="${c.strokeWidth}"`);
3031
+ if (c.id) att.push(`id="${this._esc(c.id)}"`);
3032
+ const filterId = this.#getShadowFilter(c);
3033
+ if (filterId) att.push(`filter="url(#${filterId})"`);
3034
+ return `<circle cx="${c.cx}" cy="${c.cy}" r="${c.r}" ${att.join(" ")} />`;
3035
+ }
3036
+ /**
3037
+ * Produce stroke-dasharray (and optional stroke-linecap) for a line / path.
3038
+ * Preserves the exact legacy output for `'dashed'` / `'dotted'` (and the
3039
+ * legacy `dashed: boolean` shorthand) so existing renders stay pixel-identical;
3040
+ * uses `getLineStyle()` for the newer {@link LineVariant} values so they
3041
+ * scale proportionally with the stroke width.
3042
+ */
3043
+ #dashArray(dash, dashed, strokeWidth) {
3044
+ if (dash === "dashed" || !dash && dashed) {
3045
+ return { strokeDasharray: "4,4" };
3046
+ }
3047
+ if (dash === "dotted") {
3048
+ return { strokeDasharray: "2,4" };
3049
+ }
3050
+ if (!dash || dash === "solid") return null;
3051
+ return getLineStyle(dash, strokeWidth ?? 2);
3052
+ }
3053
+ /** Build SVG `<filter>` definition for drop-shadow effects. Returns the filter id or null. */
3054
+ #getShadowFilter(c) {
3055
+ if (!c.shadowColor) return null;
3056
+ const blur = c.shadowBlur ?? 0;
3057
+ const dx = c.shadowOffsetX ?? 0;
3058
+ const dy = c.shadowOffsetY ?? 0;
3059
+ if (!blur && !dx && !dy) return null;
3060
+ const key = `shadow_${blur}_${dx}_${dy}`;
3061
+ if (this.#filters.has(key)) return key;
3062
+ const filter = `<filter id="${key}" x="-50%" y="-50%" width="200%" height="200%">
3063
+ <feDropShadow dx="${dx}" dy="${dy}" stdDeviation="${blur / 2}" flood-color="${this._esc(c.shadowColor)}" />
3064
+ </filter>`;
3065
+ this.#filters.set(key, filter);
3066
+ return key;
3067
+ }
3068
+ /** Register a plot-area `<clipPath>` by id. */
3069
+ #registerClipPath(id, x, y, w, h) {
3070
+ if (this.#clipPaths.has(id)) return;
3071
+ this.#clipPaths.set(
3072
+ id,
3073
+ `<clipPath id="${id}">
3074
+ <rect x="${x}" y="${y}" width="${w}" height="${h}" />
3075
+ </clipPath>`
3076
+ );
3077
+ }
3078
+ #text(c) {
3079
+ const att = [];
3080
+ if (c.anchor) att.push(`text-anchor="${c.anchor}"`);
3081
+ if (c.fontSize) att.push(`font-size="${c.fontSize}"`);
3082
+ if (c.fontFamily) att.push(`font-family="${this._esc(c.fontFamily)}"`);
3083
+ if (c.fill) att.push(`fill="${this._esc(c.fill)}"`);
3084
+ if (c.rotate) att.push(`transform="rotate(${c.rotate} ${c.x} ${c.y})"`);
3085
+ if (c.textBaseline) att.push(`textBaseline="${c.textBaseline}"`);
3086
+ if (c.id) att.push(`id="${this._esc(c.id)}"`);
3087
+ return `<text x="${c.x}" y="${c.y}" ${att.join(" ")}>${this._esc(c.content)}</text>`;
3088
+ }
3089
+ _gradient(c) {
3090
+ const id = `grad-${Math.random().toString(36).slice(2, 8)}`;
3091
+ const stops = c.stops.map(
3092
+ (s) => ` <stop offset="${s.offset}" stop-color="${this._esc(s.color)}" />`
3093
+ ).join("\n");
3094
+ return `<linearGradient id="${id}" gradientUnits="userSpaceOnUse">
3095
+ ${stops}
3096
+ </linearGradient>
3097
+ <path d="${c.points.map((p) => `${p.x},${p.y}`).join(" ")}" fill="url(#${id})" />`;
3098
+ }
3099
+ _gap(c) {
3100
+ if (c.style === "empty") return "";
3101
+ const att = [
3102
+ `stroke="${this._esc("#999")}"`,
3103
+ `stroke-dasharray="4,4"`,
3104
+ `fill="none"`
3105
+ ];
3106
+ if (c.style === "label" && c.label) {
3107
+ return `<g class="gap">
3108
+ <rect x="${c.x1}" y="${c.y1}" width="${c.x2 - c.x1}" height="${c.y2 - c.y1}" ${att.join(" ")} />
3109
+ <text x="${c.x1}" y="${c.y1 - 4}" fill="#999" font-size="10">${this._esc(c.label)}</text>
3110
+ </g>`;
3111
+ }
3112
+ return `<rect x="${c.x1}" y="${c.y1}" width="${c.x2 - c.x1}" height="${c.y2 - c.y1}" ${att.join(" ")} />`;
3113
+ }
3114
+ _group(c) {
3115
+ if (c.clipRect) {
3116
+ this.#registerClipPath(`clip-plot`, c.clipRect.x, c.clipRect.y, c.clipRect.w, c.clipRect.h);
3117
+ }
3118
+ const inner = c.commands.map((cmd) => ` ${this._toSVG(cmd).replace(/\n {3}/g, "\n ")}`).join("\n");
3119
+ const cls = c.cssClass ? ` class="${this._esc(c.cssClass)}"` : "";
3120
+ const gid = c.id ? ` id="${this._esc(c.id)}"` : "";
3121
+ const clipId = c.plotClipId || (c.clipRect ? `clip-plot` : null);
3122
+ const clip = clipId ? ` clip-path="url(#${this._esc(clipId)})"` : "";
3123
+ return `<g${cls}${gid}${clip}>
3124
+ ${inner}
3125
+ </g>`;
3126
+ }
3127
+ _esc(s) {
3128
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3129
+ }
3130
+ };
3131
+
3132
+ // src/tooltip.ts
3133
+ var SVG_NS = "http://www.w3.org/2000/svg";
3134
+ function seriesColor(s) {
3135
+ const line = s.style?.line;
3136
+ if (line && !Array.isArray(line) && line.color) return line.color;
3137
+ return theme.stroke;
3138
+ }
3139
+ function escapeHtml(s) {
3140
+ return s.replace(
3141
+ /[&<>"']/g,
3142
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]
3143
+ );
3144
+ }
3145
+ function defaultFormat2(samples) {
3146
+ if (samples.length === 0) return "";
3147
+ const time = new Date(samples[0].time).toLocaleString();
3148
+ const rows = samples.map((s) => {
3149
+ const name = escapeHtml(s.series.name);
3150
+ return `<div><span class="mlc-tooltip__name">${name}:</span> ${s.value.toFixed(2)}</div>`;
3151
+ });
3152
+ return `<div class="mlc-tooltip__time">${escapeHtml(time)}</div>${rows.join("")}`;
3153
+ }
3154
+ function findClosest(data, targetTime) {
3155
+ if (data.length === 0) return void 0;
3156
+ let lo = 0;
3157
+ let hi = data.length - 1;
3158
+ while (lo < hi) {
3159
+ const mid = lo + hi >> 1;
3160
+ if (data[mid].time < targetTime) lo = mid + 1;
3161
+ else hi = mid;
3162
+ }
3163
+ const a = data[Math.max(0, lo - 1)];
3164
+ const b = data[lo];
3165
+ return Math.abs(a.time - targetTime) <= Math.abs(b.time - targetTime) ? a : b;
3166
+ }
3167
+ function pointValue(p) {
3168
+ if ("value" in p) return p.value;
3169
+ return p.avg;
3170
+ }
3171
+ function attachTooltip(target, chart, options = {}) {
3172
+ const svg = target.querySelector("svg");
3173
+ if (!svg) {
3174
+ throw new Error("attachTooltip: target has no <svg> child (did you mount the chart first?)");
3175
+ }
3176
+ const format = options.format ?? defaultFormat2;
3177
+ const snapRadius = options.snapRadius ?? Infinity;
3178
+ const className = options.className ?? "mlc-tooltip";
3179
+ const showPicks = options.showPicks ?? true;
3180
+ const pickRadius = options.pickRadius ?? 5;
3181
+ const picksClassName = options.picksClassName ?? "mlc-tooltip-picks";
3182
+ const tooltip = document.createElement("div");
3183
+ tooltip.className = className;
3184
+ tooltip.style.position = "absolute";
3185
+ tooltip.style.pointerEvents = "none";
3186
+ tooltip.style.display = "none";
3187
+ if (getComputedStyle(target).position === "static") {
3188
+ target.style.position = "relative";
3189
+ }
3190
+ target.appendChild(tooltip);
3191
+ let picksGroup = null;
3192
+ if (showPicks) {
3193
+ picksGroup = document.createElementNS(SVG_NS, "g");
3194
+ picksGroup.setAttribute("class", picksClassName);
3195
+ picksGroup.setAttribute("pointer-events", "none");
3196
+ svg.appendChild(picksGroup);
3197
+ }
3198
+ const clearPicks = () => {
3199
+ if (picksGroup) picksGroup.replaceChildren();
3200
+ };
3201
+ const cache = chart.series.map((s, i) => {
3202
+ const points = s.data.map((p) => ({ time: p.time, value: pointValue(p) })).filter((p) => p.value !== null).sort((a, b) => a.time - b.time);
3203
+ return { data: points, series: s, index: i };
3204
+ });
3205
+ const onMove = (e) => {
3206
+ const rect = svg.getBoundingClientRect();
3207
+ const chartWidth = chart.getWidth?.() ?? rect.width;
3208
+ const chartX = (e.clientX - rect.left) * chartWidth / Math.max(1, rect.width);
3209
+ chart.getHeight?.() ?? rect.height;
3210
+ const time = chart.invertTime(chartX);
3211
+ if (!Number.isFinite(time)) {
3212
+ tooltip.style.display = "none";
3213
+ return;
3214
+ }
3215
+ const samples = [];
3216
+ for (const entry of cache) {
3217
+ const closest = findClosest(entry.data, time);
3218
+ if (!closest) continue;
3219
+ const sx = chart.project(closest.time, closest.value, entry.series.yAxisIndex ?? 0).x;
3220
+ const sy = chart.project(closest.time, closest.value, entry.series.yAxisIndex ?? 0).y;
3221
+ const dx = Math.abs(sx - chartX);
3222
+ if (dx > snapRadius) continue;
3223
+ samples.push({
3224
+ seriesIndex: entry.index,
3225
+ series: entry.series,
3226
+ time: closest.time,
3227
+ value: closest.value,
3228
+ x: sx,
3229
+ y: sy
3230
+ });
3231
+ }
3232
+ if (samples.length === 0) {
3233
+ tooltip.style.display = "none";
3234
+ clearPicks();
3235
+ return;
3236
+ }
3237
+ tooltip.innerHTML = format(samples);
3238
+ const targetRect = target.getBoundingClientRect();
3239
+ tooltip.style.left = `${e.clientX - targetRect.left}px`;
3240
+ tooltip.style.top = `${e.clientY - targetRect.top}px`;
3241
+ tooltip.style.display = "";
3242
+ if (picksGroup) {
3243
+ picksGroup.replaceChildren();
3244
+ for (const s of samples) {
3245
+ const c = document.createElementNS(SVG_NS, "circle");
3246
+ c.setAttribute("cx", String(s.x));
3247
+ c.setAttribute("cy", String(s.y));
3248
+ c.setAttribute("r", String(pickRadius));
3249
+ c.setAttribute("fill", "#ffffff");
3250
+ c.setAttribute("stroke", seriesColor(s.series));
3251
+ c.setAttribute("stroke-width", "2");
3252
+ c.setAttribute("class", `${picksClassName}__dot`);
3253
+ picksGroup.appendChild(c);
3254
+ }
3255
+ }
3256
+ };
3257
+ const onLeave = () => {
3258
+ tooltip.style.display = "none";
3259
+ clearPicks();
3260
+ };
3261
+ svg.addEventListener("mousemove", onMove);
3262
+ svg.addEventListener("mouseleave", onLeave);
3263
+ let disposed = false;
3264
+ return () => {
3265
+ if (disposed) return;
3266
+ disposed = true;
3267
+ svg.removeEventListener("mousemove", onMove);
3268
+ svg.removeEventListener("mouseleave", onLeave);
3269
+ tooltip.remove();
3270
+ if (picksGroup) picksGroup.remove();
3271
+ };
3272
+ }
3273
+
3274
+ // src/mount.ts
3275
+ function mount(target, options = {}) {
3276
+ const el = typeof target === "string" ? document.querySelector(target) : target;
3277
+ if (!el) {
3278
+ throw new Error(
3279
+ `mount: target ${typeof target === "string" ? `'${target}'` : ""} not found`
3280
+ );
3281
+ }
3282
+ const { tooltip, ...chartOptions } = options;
3283
+ const width = chartOptions.width ?? el.clientWidth ?? 800;
3284
+ const height = chartOptions.height ?? 350;
3285
+ const chart = new MLTimeGraph({ ...chartOptions, width, height });
3286
+ const { content } = new SVGRenderer().render(chart.renderCommands());
3287
+ el.innerHTML = content;
3288
+ const svg = el.querySelector("svg");
3289
+ if (svg) {
3290
+ svg.setAttribute("viewBox", `0 0 ${width} ${chart.getHeight()}`);
3291
+ svg.setAttribute("width", "100%");
3292
+ svg.setAttribute("height", String(chart.getHeight()));
3293
+ }
3294
+ if (tooltip?.show) {
3295
+ attachTooltip(el, chart, tooltip);
3296
+ }
3297
+ return chart;
3298
+ }
3299
+
3300
+ // src/style/fill_helpers.ts
3301
+ function fillBetweenThresholds(input) {
3302
+ const { thresholds, colors, hatches } = input;
3303
+ if (colors.length !== thresholds.length + 1) {
3304
+ throw new Error(
3305
+ `fillBetweenThresholds: expected ${thresholds.length + 1} colors for ${thresholds.length} thresholds, got ${colors.length}`
3306
+ );
3307
+ }
3308
+ if (hatches !== void 0 && hatches.length !== colors.length) {
3309
+ throw new Error(
3310
+ `fillBetweenThresholds: hatches length (${hatches.length}) must equal colors length (${colors.length})`
3311
+ );
3312
+ }
3313
+ const regions = colors.map((color, i) => {
3314
+ const hatch = hatches?.[i];
3315
+ const fill = hatch ? { color, hatch } : color;
3316
+ if (i === 0) {
3317
+ return { to: { threshold: thresholds[0] }, fill };
3318
+ }
3319
+ if (i === colors.length - 1) {
3320
+ return { from: { threshold: thresholds[thresholds.length - 1] }, fill };
3321
+ }
3322
+ return {
3323
+ from: { threshold: thresholds[i - 1] },
3324
+ to: { threshold: thresholds[i] },
3325
+ fill
3326
+ };
3327
+ });
3328
+ return { regions };
3329
+ }
3330
+
3331
+ // src/data/parser.ts
3332
+ function parseDataPoint(obj) {
3333
+ if (!obj || typeof obj !== "object") {
3334
+ throw new Error("DataPoint: object expected");
3335
+ }
3336
+ const { time, value, annotation } = obj;
3337
+ if (typeof time !== "number" || isNaN(time)) {
3338
+ throw new Error(`DataPoint: invalid time=${time}`);
3339
+ }
3340
+ if (value !== null && (typeof value !== "number" || isNaN(value))) {
3341
+ throw new Error(`DataPoint: invalid value=${value}`);
3342
+ }
3343
+ return {
3344
+ time,
3345
+ value,
3346
+ // null = gap
3347
+ annotation: annotation && typeof annotation === "string" ? annotation : void 0
3348
+ };
3349
+ }
3350
+ function parseSeries(obj) {
3351
+ if (Array.isArray(obj)) return obj.map(parseDataPoint);
3352
+ if (obj && typeof obj === "object") {
3353
+ const json = obj;
3354
+ const name = json.name;
3355
+ const data = json.data;
3356
+ const sensorType = json.sensorType;
3357
+ const enumMap = json.enumMap;
3358
+ const color = json.color;
3359
+ const lineWidth = json.lineWidth;
3360
+ const smoothing = json.smoothing;
3361
+ const seriesType = json.seriesType;
3362
+ if (!name || !name.length) throw new Error("Series: name required");
3363
+ if (!Array.isArray(data)) throw new Error("Series: data must be an array");
3364
+ const styleLine = color !== void 0 || lineWidth !== void 0 || smoothing !== void 0 ? { color, width: lineWidth, smoothing } : void 0;
3365
+ return {
3366
+ name,
3367
+ data: data.map(parseDataPoint),
3368
+ sensorType: sensorType ?? "numeric",
3369
+ enumMap,
3370
+ seriesType,
3371
+ ...styleLine && { style: { line: styleLine } }
3372
+ };
3373
+ }
3374
+ throw new Error("parseSeries: object or DataPoint[] expected");
3375
+ }
3376
+ function parseAggregated(obj) {
3377
+ if (!obj || typeof obj !== "object") throw new Error("parseAggregated: object expected");
3378
+ const json = obj;
3379
+ const name = json.name;
3380
+ const data = json.data;
3381
+ const showAs = json.showAs;
3382
+ const avgLine = json.avgLine;
3383
+ const countOpacity = json.countOpacity;
3384
+ const color = json.color;
3385
+ if (!name || !name.length) throw new Error("AggregatedSeries: name required");
3386
+ if (!Array.isArray(data)) throw new Error("AggregatedSeries: data must be an array");
3387
+ return {
3388
+ name,
3389
+ data: parseAggregatedData(data),
3390
+ showAs,
3391
+ avgLine,
3392
+ countOpacity,
3393
+ ...color !== void 0 && { style: { line: { color }, fill: color } }
3394
+ };
3395
+ }
3396
+ function parseAggregatedData(arr) {
3397
+ const stat = (v) => typeof v === "number" ? v : null;
3398
+ return arr.map((item) => {
3399
+ const p = item;
3400
+ return {
3401
+ time: p.time ?? 0,
3402
+ min: stat(p.min),
3403
+ max: stat(p.max),
3404
+ avg: stat(p.avg),
3405
+ count: typeof p.count === "number" ? p.count : 0
3406
+ };
3407
+ });
3408
+ }
3409
+
3410
+ // src/theme/runtime.ts
3411
+ var currentDefault = { ...theme };
3412
+ function setDefaultTheme(partial) {
3413
+ currentDefault = { ...currentDefault, ...partial };
3414
+ }
3415
+ function getDefaultTheme() {
3416
+ return currentDefault;
3417
+ }
3418
+ function resetDefaultTheme() {
3419
+ currentDefault = { ...theme };
3420
+ }
3421
+ /*!
3422
+ * MLTimeGraph — Copyright (c) 2026 Michael Lechner
3423
+ * MIT with Attribution: free use incl. commercial requires visible credit to
3424
+ * "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
3425
+ */
3426
+ /*!
3427
+ * MLTimeGraph — Copyright (c) 2026 Michael Lechner
3428
+ * MIT with Attribution: free use incl. commercial must have visible credit to
3429
+ * "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
3430
+ */
3431
+
3432
+ export { MLTimeGraph, SVGRenderer, attachTooltip, fillBetweenThresholds, getDefaultTheme, mount, parseAggregated, parseDataPoint, parseSeries, resetDefaultTheme, setDefaultTheme };