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.
@@ -0,0 +1,2465 @@
1
+ // src/renderer/renderer.ts
2
+ var Renderer = class {
3
+ };
4
+
5
+ // src/analyze/processor.ts
6
+ var SeriesProcessor = class {
7
+ /**
8
+ * Standard interpolation for scalar DataPoints.
9
+ */
10
+ static interpolateDataPoint(p1, p2, t) {
11
+ return {
12
+ time: p1.time + t * (p2.time - p1.time),
13
+ value: (p1.value ?? 0) + t * ((p2.value ?? 0) - (p1.value ?? 0))
14
+ };
15
+ }
16
+ /**
17
+ * Standard interpolation for AggregatedPoints (interpolates min, max, avg and count).
18
+ */
19
+ static interpolateAggregatedPoint(p1, p2, t) {
20
+ const lerp = (v1, v2) => v1 !== null && v2 !== null ? v1 + t * (v2 - v1) : null;
21
+ return {
22
+ time: p1.time + t * (p2.time - p1.time),
23
+ min: lerp(p1.min, p2.min),
24
+ max: lerp(p1.max, p2.max),
25
+ avg: lerp(p1.avg, p2.avg),
26
+ count: Math.round(p1.count + t * (p2.count - p1.count))
27
+ };
28
+ }
29
+ /**
30
+ * Splits a data array into contiguous runs based on null values or time jumps.
31
+ *
32
+ * @param data The raw data points.
33
+ * @param isNull A predicate to identify "gap" points (e.g. value === null).
34
+ * @param gapThreshold Max time distance between points before a new run starts.
35
+ */
36
+ static getRuns(data, isNull, gapThreshold = 0) {
37
+ const sorted = [...data].sort((a, b) => a.time - b.time);
38
+ const runs = [];
39
+ let current = [];
40
+ let prev = null;
41
+ for (const p of sorted) {
42
+ const isPointNull = isNull(p);
43
+ const isJump = gapThreshold > 0 && prev && p.time - prev.time > gapThreshold;
44
+ if (isPointNull || isJump) {
45
+ if (current.length) {
46
+ runs.push(current);
47
+ current = [];
48
+ }
49
+ }
50
+ if (!isPointNull) {
51
+ current.push(p);
52
+ }
53
+ prev = p;
54
+ }
55
+ if (current.length) {
56
+ runs.push(current);
57
+ }
58
+ return runs;
59
+ }
60
+ /**
61
+ * Splits a contiguous run into sub-segments at the given boundary values.
62
+ * Inserts interpolated points at every boundary crossing so segments
63
+ * meet exactly at the boundary.
64
+ *
65
+ * @param run A gap-free array of points.
66
+ * @param boundaries Values at which to split the run.
67
+ * @param getValue Function to extract the numeric value used for splitting.
68
+ * @param interpolate Function to create an interpolated point between p1 and p2 at factor t [0..1].
69
+ */
70
+ static splitByBoundaries(run, boundaries, getValue, interpolate) {
71
+ if (run.length === 0) return [];
72
+ if (boundaries.length === 0) {
73
+ return [{ data: run, zoneIndex: 0 }];
74
+ }
75
+ const bs = [...boundaries].sort((a, b) => a - b);
76
+ const out = [];
77
+ const getZone = (v) => {
78
+ let idx = 0;
79
+ for (let i = 0; i < bs.length; i++) {
80
+ if (v >= bs[i]) idx = i + 1;
81
+ else break;
82
+ }
83
+ return idx;
84
+ };
85
+ let currentSeg = [run[0]];
86
+ for (let i = 1; i < run.length; i++) {
87
+ const p1 = run[i - 1];
88
+ const p2 = run[i];
89
+ const v1 = getValue(p1);
90
+ const v2 = getValue(p2);
91
+ let crossed;
92
+ if (v2 > v1) {
93
+ crossed = bs.filter((b) => b > v1 && b <= v2);
94
+ } else if (v2 < v1) {
95
+ crossed = bs.filter((b) => b >= v2 && b < v1).reverse();
96
+ } else {
97
+ crossed = [];
98
+ }
99
+ for (const b of crossed) {
100
+ const t = (b - v1) / (v2 - v1);
101
+ const pInt = interpolate(p1, p2, t);
102
+ currentSeg.push(pInt);
103
+ out.push({ data: currentSeg, zoneIndex: getZone((v1 + b) / 2) });
104
+ currentSeg = [pInt];
105
+ }
106
+ currentSeg.push(p2);
107
+ }
108
+ if (currentSeg.length > 0) {
109
+ const vStart = getValue(currentSeg[0]);
110
+ const vEnd = getValue(currentSeg[currentSeg.length - 1]);
111
+ out.push({ data: currentSeg, zoneIndex: getZone((vStart + vEnd) / 2) });
112
+ }
113
+ return out;
114
+ }
115
+ /**
116
+ * Splits a contiguous run into two groups: those below and those at/above a threshold.
117
+ * Internally uses splitByBoundaries to ensure exact intersection points.
118
+ */
119
+ static splitByThreshold(run, threshold, getValue, interpolate) {
120
+ const segments = this.splitByBoundaries(run, [threshold], getValue, interpolate);
121
+ const result = { above: [], below: [] };
122
+ for (const seg of segments) {
123
+ if (seg.zoneIndex === 0) result.below.push(seg.data);
124
+ else result.above.push(seg.data);
125
+ }
126
+ return result;
127
+ }
128
+ };
129
+
130
+ // src/theme/defaults.ts
131
+ var theme = {
132
+ // ── Series ──
133
+ /** Default stroke color for series lines */
134
+ stroke: "#4285f4",
135
+ /** Default line width in pixels */
136
+ strokeWidth: 2,
137
+ /** Default point marker size (radius / half-width) */
138
+ pointSize: 4,
139
+ /** Max data points before point markers are suppressed */
140
+ pointThreshold: 100,
141
+ /** Default max time gap (ms) before the line breaks; 0 = off */
142
+ gapThreshold: 0,
143
+ /** Default series type */
144
+ fill: "none",
145
+ hatch: null,
146
+ // ── Aggregated series ──
147
+ /** Default band fill color */
148
+ bandFill: "#4285f4",
149
+ /** Default band opacity (when countOpacity is disabled) */
150
+ bandOpacity: 0.6,
151
+ /** Default avg line color for bands */
152
+ bandAvgLine: "#e53e3e",
153
+ /** Default min color for minmaxavg series */
154
+ minColor: "#3b82f6",
155
+ /** Default max color for minmaxavg series */
156
+ maxColor: "#ef4444",
157
+ /** Default avg color for minmaxavg series */
158
+ avgColor: "#64748b",
159
+ /** Area fill alpha suffix (hex) for zoned areas — default 20% opacity */
160
+ areaFillAlpha: "4285f433",
161
+ // ── Axis ──
162
+ /** Default axis baseline color */
163
+ axisColor: "#ccc",
164
+ /** Default tick mark color */
165
+ tickColor: "#ddd",
166
+ /** Default axis label text color */
167
+ textColor: "#777",
168
+ /** Default axis text size (axis labels, tick labels) */
169
+ textSize: 11,
170
+ /** Axis label (rotated title next to axis) fill color */
171
+ axisLabelColor: "#444",
172
+ /** Axis label font size */
173
+ axisLabelSize: 12,
174
+ // ── Grid ──
175
+ /** Default grid line stroke */
176
+ gridStroke: "#e2e8f0",
177
+ /** Default grid line stroke width */
178
+ gridStrokeWidth: 1,
179
+ /** Default grid opacity */
180
+ gridOpacity: 1,
181
+ // ── Legend ──
182
+ /** Legend swatch stroke */
183
+ legendStroke: "#ccc",
184
+ /** Legend text fill */
185
+ legendText: "#333",
186
+ /** Legend font size */
187
+ legendFont: 11,
188
+ // ── Annotations ──
189
+ /** Default annotation color */
190
+ annotationColor: "#334155",
191
+ /** Default annotation line width */
192
+ annotationWidth: 1.5,
193
+ /** Default annotation arrow head size */
194
+ annotationHead: 9,
195
+ /** Default annotation point radius */
196
+ annotationRadius: 4,
197
+ /** Default annotation text font size */
198
+ annotationFontSize: 11,
199
+ // ── Thresholds ──
200
+ /** Default threshold line color */
201
+ thresholdColor: "#666",
202
+ /** Default threshold line style */
203
+ thresholdLine: "dashed",
204
+ /** Default threshold fill opacity */
205
+ thresholdFillOpacity: 0.12,
206
+ /** Default threshold label font size */
207
+ thresholdFontSize: 10,
208
+ // ── Highlights ──
209
+ /** Default highlight fill color */
210
+ highlightColor: "#fbbf24",
211
+ /** Default highlight box opacity */
212
+ highlightOpacity: 0.2,
213
+ /** Default highlight label text color */
214
+ highlightLabelColor: "#92400e",
215
+ // ── Markers ──
216
+ /** Default marker color */
217
+ markerColor: "#f59e0b",
218
+ /** Default marker point size (cross / circle radius) */
219
+ markerSize: 5,
220
+ // ── Gaps ──
221
+ /** Gap region background fill */
222
+ gapFill: "#fff",
223
+ /** Gap border stroke */
224
+ gapStroke: "#ccc",
225
+ /** Gap border stroke width */
226
+ gapStrokeWidth: 1,
227
+ /** Gap label text color */
228
+ gapFontColor: "#999",
229
+ /** Gap label font size */
230
+ gapFontSize: 10,
231
+ gapFillOpacity: 0.15,
232
+ // ── Tooltip ──
233
+ /** Tooltip box background */
234
+ tooltipBg: "#fff",
235
+ /** Tooltip border */
236
+ tooltipBorder: "#cbd5e1",
237
+ /** Tooltip text color */
238
+ tooltipText: "#1e293b",
239
+ /** Tooltip value label color */
240
+ tooltipValue: "#3b82f6",
241
+ /** Tooltip crosshair stroke */
242
+ tooltipCrosshair: "#94a3b8",
243
+ /** Tooltip snap radius in pixels */
244
+ tooltipSnapRadius: 20,
245
+ // ── Statistics ──
246
+ /** Stats overlay line color */
247
+ statsLineColor: "#94a3b8",
248
+ /** Stats label color */
249
+ statsLabelColor: "#64748b",
250
+ // ── Minimap ──
251
+ /** Minimap overview line stroke */
252
+ minimapStroke: "#94a3b8",
253
+ /** Minimap background */
254
+ minimapBg: "#f8f9fa",
255
+ /** Minimap brush (viewport) fill */
256
+ minimapBrush: "#3b82f644",
257
+ // ── Palette ──
258
+ /** Default colour palette for enum categories and multi-series. */
259
+ palette: [
260
+ "#4285f4",
261
+ "#ea4335",
262
+ "#22c55e",
263
+ "#fbbc05",
264
+ "#9334ea",
265
+ "#12b5e5",
266
+ "#fb923c",
267
+ "#6366f1"
268
+ ]
269
+ };
270
+
271
+ // src/patterns/hatch.ts
272
+ function getHatch(id, variant = "classic-diagonal", fillcolor = "rgba(200, 220, 255, 0.3)", linecolor = "#4D88FF", strokewidth = 2) {
273
+ if (variant === "none") {
274
+ return `
275
+ <pattern id="${id}" width="10" height="10" patternUnits="userSpaceOnUse">
276
+ <rect width="10" height="10" fill="${fillcolor}" />
277
+ </pattern>
278
+ `.trim();
279
+ }
280
+ let width = 12;
281
+ let height = 12;
282
+ let transform = "rotate(0)";
283
+ let patternContent = "";
284
+ switch (variant) {
285
+ case "classic-diagonal":
286
+ width = 12;
287
+ height = 12;
288
+ transform = "rotate(45)";
289
+ patternContent = `<line x1="0" y1="0" x2="0" y2="${height}" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linecap="square" />`;
290
+ break;
291
+ case "dense-steep":
292
+ width = 6;
293
+ height = 6;
294
+ transform = "rotate(30)";
295
+ patternContent = `<line x1="0" y1="0" x2="0" y2="${height}" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linecap="square" />`;
296
+ break;
297
+ case "crosshatch":
298
+ width = 14;
299
+ height = 14;
300
+ transform = "rotate(45)";
301
+ patternContent = `
302
+ <line x1="0" y1="0" x2="0" y2="${height}" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linecap="square" />
303
+ <line x1="0" y1="0" x2="${width}" y2="0" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linecap="square" />
304
+ `;
305
+ break;
306
+ case "dots":
307
+ width = 12;
308
+ height = 12;
309
+ patternContent = `<circle cx="${width / 2}" cy="${height / 2}" r="${strokewidth * 1.2}" fill="${linecolor}" />`;
310
+ break;
311
+ case "waves":
312
+ width = 16;
313
+ height = 16;
314
+ patternContent = `
315
+ <path d="M 0 ${height / 2} Q ${width / 4} 0, ${width / 2} ${height / 2} T ${width} ${height / 2}"
316
+ fill="none" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linecap="round" />
317
+ `;
318
+ break;
319
+ case "dashed":
320
+ width = 12;
321
+ height = 12;
322
+ transform = "rotate(45)";
323
+ patternContent = `<line x1="0" y1="0" x2="0" y2="${height}" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-dasharray="3,3" />`;
324
+ break;
325
+ case "herringbone":
326
+ width = 16;
327
+ height = 16;
328
+ patternContent = `
329
+ <path d="M 0 0 L ${width / 2} ${height / 2} L 0 ${height} M ${width} 0 L ${width / 2} ${height / 2} L ${width} ${height}"
330
+ fill="none" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linejoin="round" stroke-linecap="round" />
331
+ `;
332
+ break;
333
+ case "brick":
334
+ width = 20;
335
+ height = 20;
336
+ patternContent = `
337
+ <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}"
338
+ fill="none" stroke="${linecolor}" stroke-width="${strokewidth}" />
339
+ `;
340
+ break;
341
+ case "double-stripe":
342
+ width = 16;
343
+ height = 16;
344
+ transform = "rotate(45)";
345
+ patternContent = `
346
+ <line x1="0" y1="0" x2="0" y2="${height}" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linecap="square" />
347
+ <line x1="${width / 2}" y1="0" x2="${width / 2}" y2="${height}" stroke="${linecolor}" stroke-width="${strokewidth / 2}" stroke-linecap="square" />
348
+ `;
349
+ break;
350
+ case "honeycomb":
351
+ width = 18;
352
+ height = 32;
353
+ patternContent = `
354
+ <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"
355
+ fill="none" stroke="${linecolor}" stroke-width="${strokewidth}" stroke-linejoin="round" stroke-linecap="round" />
356
+ `;
357
+ break;
358
+ }
359
+ const bgRect = `<rect width="${width}" height="${height}" fill="${fillcolor}" />`;
360
+ return `
361
+ <pattern id="${id}" width="${width}" height="${height}" patternTransform="${transform}" patternUnits="userSpaceOnUse">
362
+ ${bgRect}
363
+ ${patternContent}
364
+ </pattern>
365
+ `.trim();
366
+ }
367
+
368
+ // src/renderer/series_renderer.ts
369
+ function resolveStyle(style) {
370
+ return {
371
+ id: style.id,
372
+ line: {
373
+ stroke: style.stroke ?? theme.stroke,
374
+ strokeWidth: style.strokeWidth ?? theme.strokeWidth,
375
+ smoothing: style.smoothing ?? false,
376
+ dashed: style.dashed ?? false
377
+ },
378
+ fill: style.fill ?? theme.areaFillAlpha,
379
+ markers: {
380
+ type: style.pointStyle ?? "none",
381
+ size: style.pointSize ?? theme.pointSize,
382
+ stroke: style.stroke ?? theme.stroke,
383
+ fill: "#ffffff"
384
+ },
385
+ shadow: {
386
+ color: style.shadowColor ?? "transparent",
387
+ blur: style.shadowBlur ?? 0,
388
+ offsetX: style.shadowOffsetX ?? 0,
389
+ offsetY: style.shadowOffsetY ?? 0
390
+ }
391
+ };
392
+ }
393
+ function renderLine(segments, ctx, style) {
394
+ const commands = [];
395
+ const totalPoints = segments.reduce((sum, seg) => sum + seg.data.length, 0);
396
+ const s = resolveStyle(style);
397
+ for (let si = 0; si < segments.length; si++) {
398
+ const seg = segments[si];
399
+ if (seg.data.length >= 2) {
400
+ commands.push({
401
+ type: "path",
402
+ id: style.id ? `${style.id}-line-${si}` : void 0,
403
+ points: seg.data.map((p) => ({
404
+ x: ctx.timeScale.map(p.time),
405
+ y: ctx.valueScale.map(p.value)
406
+ })),
407
+ stroke: seg.color ?? s.line.stroke,
408
+ strokeWidth: s.line.strokeWidth,
409
+ smoothing: s.line.smoothing,
410
+ dashed: s.line.dashed,
411
+ shadowColor: s.shadow.color,
412
+ shadowBlur: s.shadow.blur,
413
+ shadowOffsetX: s.shadow.offsetX,
414
+ shadowOffsetY: s.shadow.offsetY,
415
+ fill: "none"
416
+ });
417
+ } else if (seg.data.length === 1 && totalPoints === 1) {
418
+ commands.push({
419
+ type: "circle",
420
+ cx: ctx.timeScale.map(seg.data[0].time),
421
+ cy: ctx.valueScale.map(seg.data[0].value),
422
+ r: Math.max(s.markers.size, s.line.strokeWidth),
423
+ fill: seg.color ?? s.line.stroke,
424
+ shadowColor: s.shadow.color,
425
+ shadowBlur: s.shadow.blur
426
+ });
427
+ }
428
+ }
429
+ return commands;
430
+ }
431
+ function renderStep(segments, ctx, style) {
432
+ const commands = [];
433
+ const s = resolveStyle(style);
434
+ for (let si = 0; si < segments.length; si++) {
435
+ const seg = segments[si];
436
+ if (seg.data.length < 2) continue;
437
+ const pts = [];
438
+ for (let i = 0; i < seg.data.length; i++) {
439
+ const px = ctx.timeScale.map(seg.data[i].time);
440
+ const py = ctx.valueScale.map(seg.data[i].value);
441
+ if (i === 0) {
442
+ pts.push({ x: px, y: py });
443
+ } else {
444
+ pts.push({ x: px, y: pts[pts.length - 1].y });
445
+ pts.push({ x: px, y: py });
446
+ }
447
+ }
448
+ commands.push({
449
+ type: "path",
450
+ id: style.id ? `${style.id}-line-${si}` : void 0,
451
+ points: pts,
452
+ stroke: seg.color ?? s.line.stroke,
453
+ strokeWidth: s.line.strokeWidth,
454
+ smoothing: false,
455
+ fill: "none"
456
+ });
457
+ }
458
+ return commands;
459
+ }
460
+ function renderArea(segments, ctx, style, yLow, yHigh) {
461
+ const commands = [];
462
+ const s = resolveStyle(style);
463
+ for (let si = 0; si < segments.length; si++) {
464
+ const seg = segments[si];
465
+ if (seg.data.length < 2) continue;
466
+ const ptsLow = seg.data.map((p) => ({
467
+ x: ctx.timeScale.map(p.time),
468
+ y: ctx.valueScale.map(yLow(p))
469
+ }));
470
+ const ptsHigh = seg.data.map((p) => ({
471
+ x: ctx.timeScale.map(p.time),
472
+ y: ctx.valueScale.map(yHigh(p))
473
+ })).reverse();
474
+ commands.push({
475
+ type: "path",
476
+ id: style.id ? `${style.id}-fill-${si}` : void 0,
477
+ points: [...ptsLow, ...ptsHigh],
478
+ fill: seg.color ?? s.fill,
479
+ hatch: style.hatch,
480
+ stroke: "none"
481
+ });
482
+ }
483
+ return commands;
484
+ }
485
+ function renderZonedArea(run, ctx, options, style) {
486
+ if (run.length < 2) return [];
487
+ const segments = SeriesProcessor.splitByBoundaries(
488
+ run,
489
+ options.boundaries,
490
+ options.getValue,
491
+ options.interpolate
492
+ );
493
+ const commands = [];
494
+ for (let si = 0; si < segments.length; si++) {
495
+ const seg = segments[si];
496
+ if (seg.data.length < 2) continue;
497
+ const color = options.getColor(seg.zoneIndex);
498
+ if (!color) continue;
499
+ const ptsLow = seg.data.map((p) => ({
500
+ x: ctx.timeScale.map(p.time),
501
+ y: ctx.valueScale.map(options.yLow(p))
502
+ }));
503
+ const ptsHigh = seg.data.map((p) => ({
504
+ x: ctx.timeScale.map(p.time),
505
+ y: ctx.valueScale.map(options.yHigh(p))
506
+ })).reverse();
507
+ commands.push({
508
+ type: "path",
509
+ id: style?.id ? `${style.id}-fill-${si}` : void 0,
510
+ points: [...ptsLow, ...ptsHigh],
511
+ fill: color,
512
+ hatch: options.getHatch?.(seg.zoneIndex),
513
+ stroke: "none"
514
+ });
515
+ }
516
+ return commands;
517
+ }
518
+ function renderSplitLine(run, threshold, ctx, styles) {
519
+ const split = SeriesProcessor.splitByThreshold(
520
+ run,
521
+ threshold,
522
+ (p) => p.value,
523
+ SeriesProcessor.interpolateDataPoint
524
+ );
525
+ const commands = [];
526
+ if (styles.below) {
527
+ commands.push(...renderLine(split.below.map((data) => ({ data })), ctx, styles.below));
528
+ }
529
+ if (styles.above) {
530
+ commands.push(...renderLine(split.above.map((data) => ({ data })), ctx, styles.above));
531
+ }
532
+ return commands;
533
+ }
534
+ function renderMarkers(points, ctx, style, getColor) {
535
+ const s = resolveStyle(style);
536
+ if (!s.markers.type || s.markers.type === "none") return [];
537
+ const commands = [];
538
+ for (let mi = 0; mi < points.length; mi++) {
539
+ const p = points[mi];
540
+ const x = ctx.timeScale.map(p.time);
541
+ const y = ctx.valueScale.map(p.value);
542
+ const pointColor = getColor(p);
543
+ const stroke = style.pointStroke ?? pointColor;
544
+ const fill = style.pointFill ?? pointColor;
545
+ const strokeWidth = style.pointStrokeWidth ?? 1.5;
546
+ const id = style.id ? `${style.id}-marker-${mi}` : void 0;
547
+ drawMarker(commands, id, s.markers.type, x, y, s.markers.size, stroke, fill, strokeWidth);
548
+ }
549
+ return commands;
550
+ }
551
+ function drawMarker(commands, id, shape, x, y, size, stroke, fill, sw) {
552
+ switch (shape) {
553
+ case "circle":
554
+ commands.push({ type: "circle", cx: x, cy: y, r: size, fill, stroke, strokeWidth: sw, id });
555
+ break;
556
+ case "square":
557
+ commands.push({ type: "rect", x: x - size, y: y - size, w: size * 2, h: size * 2, fill, stroke, strokeWidth: sw, id });
558
+ break;
559
+ case "cross":
560
+ commands.push({ type: "line", x1: x - size, y1: y - size, x2: x + size, y2: y + size, stroke, strokeWidth: sw, id });
561
+ commands.push({ type: "line", x1: x - size, y1: y + size, x2: x + size, y2: y - size, stroke, strokeWidth: sw, id });
562
+ break;
563
+ case "diamond":
564
+ 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 });
565
+ break;
566
+ case "triangle":
567
+ 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 });
568
+ break;
569
+ case "star": {
570
+ const pts = [];
571
+ for (let i = 0; i < 10; i++) {
572
+ const r = i % 2 === 0 ? size : size * 0.5;
573
+ const a = Math.PI / 2 * 3 + i * Math.PI / 5;
574
+ pts.push({ x: x + r * Math.cos(a), y: y + r * Math.sin(a) });
575
+ }
576
+ commands.push({ type: "path", points: pts, fill, stroke, strokeWidth: sw, id });
577
+ break;
578
+ }
579
+ case "arrow":
580
+ 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 });
581
+ break;
582
+ default:
583
+ commands.push({ type: "circle", cx: x, cy: y, r: size, fill, stroke, strokeWidth: sw, id });
584
+ }
585
+ }
586
+
587
+ // src/renderer/legend_renderer.ts
588
+ var SWATCH = 12;
589
+ var GAP = 8;
590
+ var ITEM_H = 20;
591
+ var FONT = 11;
592
+ var H_GAP = 18;
593
+ var labelWidth = (s) => s.length * FONT * 0.6;
594
+ function measureLegend(items, orientation = "vertical") {
595
+ if (orientation === "horizontal") {
596
+ let w = 0;
597
+ for (const it of items) w += SWATCH + GAP + labelWidth(it.name) + H_GAP;
598
+ return { width: Math.max(0, w - H_GAP), height: ITEM_H };
599
+ }
600
+ let maxLabel = 0;
601
+ for (const it of items) maxLabel = Math.max(maxLabel, labelWidth(it.name));
602
+ return { width: SWATCH + GAP + maxLabel, height: items.length * ITEM_H };
603
+ }
604
+ function renderLegend(config) {
605
+ const { items, x, y, orientation = "vertical" } = config;
606
+ const commands = [];
607
+ let cursorX = x;
608
+ items.forEach((item, i) => {
609
+ const sx = orientation === "horizontal" ? cursorX : x;
610
+ const sy = orientation === "horizontal" ? y : y + i * ITEM_H;
611
+ commands.push({
612
+ type: "rect",
613
+ x: sx,
614
+ y: sy,
615
+ w: SWATCH,
616
+ h: SWATCH,
617
+ fill: item.color,
618
+ stroke: theme.legendStroke,
619
+ strokeWidth: 1
620
+ });
621
+ commands.push({
622
+ type: "text",
623
+ content: item.name,
624
+ x: sx + SWATCH + GAP,
625
+ y: sy + SWATCH - 2,
626
+ fontSize: theme.legendFont,
627
+ fill: theme.legendText
628
+ });
629
+ if (orientation === "horizontal") cursorX += SWATCH + GAP + labelWidth(item.name) + H_GAP;
630
+ });
631
+ return { type: "group", cssClass: "chart-legend", commands };
632
+ }
633
+
634
+ // src/renderer/grid_renderer.ts
635
+ function renderGrid(config) {
636
+ const {
637
+ xTicks,
638
+ yTicks,
639
+ xRange,
640
+ yRange,
641
+ stroke = theme.gridStroke,
642
+ strokeWidth = theme.gridStrokeWidth,
643
+ dashed = false,
644
+ opacity = theme.gridOpacity
645
+ } = config;
646
+ const commands = [];
647
+ if (yTicks) {
648
+ for (const y of yTicks) {
649
+ commands.push({ type: "line", x1: xRange[0], y1: y, x2: xRange[1], y2: y, stroke, strokeWidth, dashed, opacity });
650
+ }
651
+ }
652
+ if (xTicks) {
653
+ for (const x of xTicks) {
654
+ commands.push({ type: "line", x1: x, y1: yRange[0], x2: x, y2: yRange[1], stroke, strokeWidth, dashed, opacity });
655
+ }
656
+ }
657
+ return commands;
658
+ }
659
+
660
+ // src/core/scale.ts
661
+ var LinearScale = class {
662
+ #domain;
663
+ #range;
664
+ constructor(config) {
665
+ this.#domain = [...config.domain];
666
+ this.#range = [...config.range];
667
+ }
668
+ map(value) {
669
+ const v = Number(value);
670
+ const [d0, d1] = this.#domain;
671
+ const [r0, r1] = this.#range;
672
+ if (d1 === d0) return r0;
673
+ return r0 + (v - d0) / (d1 - d0) * (r1 - r0);
674
+ }
675
+ invert(pixel) {
676
+ const [d0, d1] = this.#domain;
677
+ const [r0, r1] = this.#range;
678
+ if (r1 === r0) return d0;
679
+ return d0 + (pixel - r0) / (r1 - r0) * (d1 - d0);
680
+ }
681
+ domain() {
682
+ return [...this.#domain];
683
+ }
684
+ range() {
685
+ return [...this.#range];
686
+ }
687
+ };
688
+ var TIME_INTERVALS = [
689
+ { label: "second", ms: 1e3 },
690
+ { label: "2_seconds", ms: 2e3 },
691
+ { label: "5_seconds", ms: 5e3 },
692
+ { label: "10_seconds", ms: 1e4 },
693
+ { label: "30_seconds", ms: 3e4 },
694
+ { label: "minute", ms: 6e4 },
695
+ { label: "5_minutes", ms: 3e5 },
696
+ { label: "15_minutes", ms: 9e5 },
697
+ { label: "30_minutes", ms: 18e5 },
698
+ { label: "hour", ms: 36e5 },
699
+ { label: "3_hours", ms: 108e5 },
700
+ { label: "6_hours", ms: 216e5 },
701
+ { label: "day", ms: 864e5 },
702
+ { label: "week", ms: 6048e5 },
703
+ { label: "month", ms: 2592e6 },
704
+ { label: "3_months", ms: 7776e6 },
705
+ { label: "6_months", ms: 15552e6 },
706
+ { label: "year", ms: 31536e6 },
707
+ { label: "2_years", ms: 63072e6 },
708
+ { label: "5_years", ms: 15768e7 }
709
+ ];
710
+ var TimeScale = class {
711
+ #linear;
712
+ #locale;
713
+ constructor(config) {
714
+ this.#linear = new LinearScale({
715
+ domain: config.domain,
716
+ range: config.range
717
+ });
718
+ this.#locale = config.locale || (typeof navigator !== "undefined" ? navigator.language : "en-US");
719
+ }
720
+ map(value) {
721
+ return this.#linear.map(Number(value));
722
+ }
723
+ invert(pixel) {
724
+ return this.#linear.invert(pixel);
725
+ }
726
+ domain() {
727
+ return this.#linear.domain();
728
+ }
729
+ range() {
730
+ return this.#linear.range();
731
+ }
732
+ get locale() {
733
+ return this.#locale;
734
+ }
735
+ /**
736
+ * Pick the "nicest" time interval that yields roughly `targetTicks` ticks
737
+ * across the visible range. Clamps to minTicks / maxTicks bounds.
738
+ */
739
+ tickInterval(targetTicks, minTicks = 3, maxTicks = 12) {
740
+ const [d0, d1] = this.#linear.domain();
741
+ const totalMs = d1 - d0;
742
+ if (totalMs <= 0) return { interval: TIME_INTERVALS[0].ms };
743
+ const ideal = totalMs / targetTicks;
744
+ let picked = TIME_INTERVALS[0].ms;
745
+ for (const t of TIME_INTERVALS) {
746
+ if (t.ms >= ideal) {
747
+ picked = t.ms;
748
+ break;
749
+ }
750
+ }
751
+ let candidate = picked;
752
+ let count = Math.round(totalMs / candidate);
753
+ while (count > maxTicks && candidate < TIME_INTERVALS[TIME_INTERVALS.length - 1].ms) {
754
+ const idx = TIME_INTERVALS.findIndex((t) => t.ms === candidate);
755
+ candidate = TIME_INTERVALS[Math.min(idx + 1, TIME_INTERVALS.length - 1)].ms;
756
+ count = Math.round(totalMs / candidate);
757
+ }
758
+ while (count < minTicks && candidate > TIME_INTERVALS[0].ms) {
759
+ const idx = TIME_INTERVALS.findIndex((t) => t.ms === candidate);
760
+ candidate = TIME_INTERVALS[Math.max(idx - 1, 0)].ms;
761
+ count = Math.round(totalMs / candidate);
762
+ }
763
+ return { interval: candidate };
764
+ }
765
+ /**
766
+ * Generate tick positions (timestamps) across the domain.
767
+ */
768
+ ticks(opts) {
769
+ const minT = opts?.minTicks ?? 5;
770
+ const maxT = opts?.maxTicks ?? 12;
771
+ const { interval } = this.tickInterval(
772
+ (minT + maxT) / 2,
773
+ minT,
774
+ maxT
775
+ );
776
+ const [d0, d1] = this.#linear.domain();
777
+ const result = [];
778
+ const start = Math.ceil(d0 / interval) * interval;
779
+ for (let t = start; t <= d1; t += interval) {
780
+ result.push(t);
781
+ }
782
+ return result;
783
+ }
784
+ /** Format a timestamp using Intl.DateTimeFormat */
785
+ format(timestamp, formatOpts) {
786
+ return new Intl.DateTimeFormat(this.#locale, formatOpts).format(
787
+ new Date(timestamp)
788
+ );
789
+ }
790
+ };
791
+ var BandScale = class {
792
+ #domain;
793
+ #range;
794
+ #paddingInner;
795
+ #paddingOuter;
796
+ constructor(config) {
797
+ this.#domain = [...config.domain];
798
+ this.#range = [...config.range];
799
+ this.#paddingInner = config.paddingInner ?? 0.1;
800
+ this.#paddingOuter = config.paddingOuter ?? 0.05;
801
+ }
802
+ /** Get the pixel width of each band (including padding) */
803
+ get step() {
804
+ const [r0, r1] = this.#range;
805
+ const n = this.#domain.length;
806
+ if (n <= 1) return Math.abs(r1 - r0);
807
+ return Math.abs(r1 - r0) * (1 - this.#paddingOuter * 2) / n + Math.abs(r1 - r0) * this.#paddingInner * 2 / n;
808
+ }
809
+ /** Get the pixel width of each band's content area */
810
+ get bandwidth() {
811
+ const [r0, r1] = this.#range;
812
+ const n = this.#domain.length;
813
+ if (n <= 1) return Math.abs(r1 - r0) * (1 - this.#paddingOuter * 2);
814
+ const totalPaddingOuter = this.#paddingOuter * 2 * Math.abs(r1 - r0);
815
+ const usable = Math.abs(r1 - r0) - totalPaddingOuter;
816
+ const step = usable / n;
817
+ return step * (1 - this.#paddingInner);
818
+ }
819
+ map(value) {
820
+ const idx = this.#domain.findIndex((d) => String(d) === String(value));
821
+ if (idx === -1) return this.#range[0];
822
+ const [r0, r1] = this.#range;
823
+ const n = this.#domain.length;
824
+ if (n <= 1) return (r0 + r1) / 2;
825
+ const totalPaddingOuter = this.#paddingOuter * 2 * Math.abs(r1 - r0);
826
+ const usable = Math.abs(r1 - r0) - totalPaddingOuter;
827
+ const direction = r1 >= r0 ? 1 : -1;
828
+ const step = usable / n;
829
+ const start = r0 + this.#paddingOuter * Math.abs(r1 - r0) * direction;
830
+ return start + idx * step;
831
+ }
832
+ invert(pixel) {
833
+ let bestIdx = 0;
834
+ let bestDist = Infinity;
835
+ for (let i = 0; i < this.#domain.length; i++) {
836
+ const pos = this.map(this.#domain[i]);
837
+ const dist = Math.abs(pixel - pos);
838
+ if (dist < bestDist) {
839
+ bestDist = dist;
840
+ bestIdx = i;
841
+ }
842
+ }
843
+ return this.#domain[bestIdx];
844
+ }
845
+ domain() {
846
+ if (this.#domain.length === 0) return ["", ""];
847
+ return [this.#domain[0], this.#domain[this.#domain.length - 1]];
848
+ }
849
+ range() {
850
+ return [...this.#range];
851
+ }
852
+ /** Get all band positions */
853
+ positions() {
854
+ const m = /* @__PURE__ */ new Map();
855
+ for (const d of this.#domain) {
856
+ m.set(d, this.map(d));
857
+ }
858
+ return m;
859
+ }
860
+ };
861
+
862
+ // src/series/threshold_renderer.ts
863
+ function dashFor(line) {
864
+ if (line === "dotted") return { dash: "dotted" };
865
+ if (line === "dashed") return { dash: "dashed" };
866
+ return {};
867
+ }
868
+ function renderThresholds(config) {
869
+ const { thresholds, valueScale, xRange } = config;
870
+ const [x0, x1] = xRange;
871
+ const [r0, r1] = valueScale.range();
872
+ const top = Math.min(r0, r1);
873
+ const bottom = Math.max(r0, r1);
874
+ const commands = [];
875
+ for (const t of thresholds) {
876
+ const color = t.color ?? theme.thresholdColor;
877
+ const y = valueScale.map(t.value);
878
+ if (t.fill === "above") {
879
+ commands.push({
880
+ type: "rect",
881
+ x: x0,
882
+ y: top,
883
+ w: x1 - x0,
884
+ h: Math.max(0, y - top),
885
+ fill: color,
886
+ hatch: t.fillHatch,
887
+ opacity: t.fillOpacity ?? 0.12,
888
+ id: t.id ? `${t.id}-fill` : void 0
889
+ });
890
+ } else if (t.fill === "below") {
891
+ commands.push({
892
+ type: "rect",
893
+ x: x0,
894
+ y,
895
+ w: x1 - x0,
896
+ h: Math.max(0, bottom - y),
897
+ fill: color,
898
+ hatch: t.fillHatch,
899
+ opacity: t.fillOpacity ?? 0.12,
900
+ id: t.id ? `${t.id}-fill` : void 0
901
+ });
902
+ }
903
+ const line = t.line ?? theme.thresholdLine;
904
+ if (line !== "none") {
905
+ const dash = dashFor(line);
906
+ const lineCmd = {
907
+ type: "line",
908
+ x1: x0,
909
+ y1: y,
910
+ x2: x1,
911
+ y2: y,
912
+ stroke: color,
913
+ strokeWidth: 1,
914
+ ...dash,
915
+ id: t.id ? `${t.id}-line` : void 0
916
+ };
917
+ if (t.shadowColor) {
918
+ lineCmd.shadowColor = t.shadowColor;
919
+ lineCmd.shadowBlur = t.shadowBlur ?? 4;
920
+ lineCmd.shadowOffsetX = t.shadowOffsetX ?? 0;
921
+ lineCmd.shadowOffsetY = t.shadowOffsetY ?? 2;
922
+ }
923
+ commands.push(lineCmd);
924
+ }
925
+ if (t.label !== false) {
926
+ const labelObj = t.label && typeof t.label === "object" ? t.label : void 0;
927
+ const text = typeof t.label === "string" ? t.label : labelObj?.text ?? t.name;
928
+ const position = labelObj?.position ?? "right";
929
+ commands.push({
930
+ ...thresholdLabel(text, position, x0, x1, y, color, labelObj),
931
+ id: t.id ? `${t.id}-label` : void 0
932
+ });
933
+ }
934
+ }
935
+ return commands;
936
+ }
937
+ function thresholdLabel(text, pos, x0, x1, y, color, labelObj) {
938
+ const mid = (x0 + x1) / 2;
939
+ const base = {
940
+ type: "text",
941
+ content: text,
942
+ fontSize: theme.thresholdFontSize,
943
+ fill: color
944
+ };
945
+ const extras = labelObj ? {
946
+ ...labelObj.rotate !== void 0 && { rotate: labelObj.rotate },
947
+ ...labelObj.textBaseline !== void 0 && {
948
+ textBaseline: labelObj.textBaseline
949
+ }
950
+ } : {};
951
+ switch (pos) {
952
+ case "left":
953
+ return { ...base, ...extras, x: x0 + 4, y: y - 4, anchor: "start" };
954
+ case "above":
955
+ return { ...base, ...extras, x: mid, y: y - 6, anchor: "middle" };
956
+ case "below":
957
+ return { ...base, ...extras, x: mid, y: y + 14, anchor: "middle" };
958
+ case "center":
959
+ return { ...base, ...extras, x: mid, y: y - 4, anchor: "middle" };
960
+ case "right":
961
+ default:
962
+ return { ...base, ...extras, x: x1 - 4, y: y - 4, anchor: "end" };
963
+ }
964
+ }
965
+
966
+ // src/series/gap_renderer.ts
967
+ function renderGaps(config) {
968
+ const {
969
+ gaps,
970
+ timeScale,
971
+ yRange,
972
+ fill = theme.gapFill,
973
+ hatch,
974
+ fillOpacity = theme.gapFillOpacity ?? 0.15,
975
+ stroke = theme.gapStroke,
976
+ strokeWidth = theme.gapStrokeWidth,
977
+ dashed = true,
978
+ fontSize = theme.gapFontSize,
979
+ fontFill = theme.gapFontColor,
980
+ labelBaseline: defaultBaseline = "middle",
981
+ labelRotate: defaultRotate
982
+ } = config;
983
+ const [y0, y1] = yRange;
984
+ const commands = [];
985
+ for (const gap of gaps) {
986
+ const x1 = timeScale.map(gap.startTime);
987
+ const x2 = timeScale.map(gap.endTime);
988
+ const gapFill = gap.fill ?? fill;
989
+ const gapHatch = gap.hatch ?? hatch;
990
+ const gapOpacity = gap.fillOpacity ?? fillOpacity;
991
+ const gapLabel = gap.label ?? "";
992
+ const gapRotate = gap.rotate ?? defaultRotate;
993
+ const baseline = gap.labelBaseline ?? defaultBaseline;
994
+ if (gap.style === "dashed_border" || !gap.style) {
995
+ commands.push({
996
+ type: "rect",
997
+ x: x1,
998
+ y: y0,
999
+ w: x2 - x1,
1000
+ h: y1 - y0,
1001
+ fill: gapFill,
1002
+ hatch: gapHatch,
1003
+ opacity: gapOpacity,
1004
+ stroke,
1005
+ strokeWidth,
1006
+ dashed
1007
+ });
1008
+ } else if (gap.style === "empty") {
1009
+ commands.push({
1010
+ type: "rect",
1011
+ x: x1,
1012
+ y: y0,
1013
+ w: x2 - x1,
1014
+ h: y1 - y0,
1015
+ fill: gapFill,
1016
+ hatch: gapHatch,
1017
+ opacity: gapOpacity
1018
+ });
1019
+ }
1020
+ if (gapLabel) {
1021
+ const labelY = gapLabelY(y0, y1, baseline);
1022
+ const svgBaseline = baseline === "above" ? "top" : baseline === "below" ? "bottom" : "middle";
1023
+ commands.push({
1024
+ type: "text",
1025
+ content: gapLabel,
1026
+ x: (x1 + x2) / 2,
1027
+ y: labelY,
1028
+ anchor: "middle",
1029
+ fontSize,
1030
+ fill: fontFill,
1031
+ textBaseline: svgBaseline,
1032
+ rotate: gapRotate
1033
+ });
1034
+ }
1035
+ }
1036
+ return commands;
1037
+ }
1038
+ function gapLabelY(y0, y1, baseline) {
1039
+ switch (baseline) {
1040
+ case "above":
1041
+ return y0 - 12;
1042
+ case "below":
1043
+ return y1 + 4;
1044
+ case "middle":
1045
+ default:
1046
+ return (y0 + y1) / 2;
1047
+ }
1048
+ }
1049
+
1050
+ // src/annotation/marker.ts
1051
+ function renderMarkers2(config) {
1052
+ const { markers, timeScale, valueScale, yRange = [0, 300] } = config;
1053
+ const [yTop, yBottom] = yRange;
1054
+ const commands = [];
1055
+ for (const marker of markers) {
1056
+ const x = timeScale.map(marker.time);
1057
+ const color = marker.color ?? theme.markerColor;
1058
+ const pointStyle = marker.pointStyle ?? (marker.value !== void 0 ? "circle" : "none");
1059
+ const lineStyle = marker.lineStyle ?? "full";
1060
+ if (marker.value !== void 0) {
1061
+ const y = valueScale.map(marker.value);
1062
+ if (lineStyle === "to-value") {
1063
+ commands.push({ type: "line", x1: x, y1: yBottom, x2: x, y2: y, stroke: color, strokeWidth: 1, dashed: true });
1064
+ } else if (lineStyle === "to-top") {
1065
+ commands.push({ type: "line", x1: x, y1: yTop, x2: x, y2: y, stroke: color, strokeWidth: 1, dashed: true });
1066
+ } else if (lineStyle === "full") {
1067
+ commands.push({ type: "line", x1: x, y1: yTop, x2: x, y2: yBottom, stroke: color, strokeWidth: 1 });
1068
+ }
1069
+ if (pointStyle !== "none") {
1070
+ drawMarkerPoint(commands, x, y, color, pointStyle);
1071
+ }
1072
+ if (marker.label) {
1073
+ const labelY = lineStyle === "to-value" ? y - 10 : yTop - 6;
1074
+ commands.push({ type: "text", content: marker.label, x, y: labelY, anchor: "middle", fontSize: 11, fill: color });
1075
+ }
1076
+ } else {
1077
+ commands.push({ type: "line", x1: x, y1: yTop, x2: x, y2: yBottom, stroke: color, strokeWidth: 1 });
1078
+ if (marker.label) {
1079
+ commands.push({ type: "text", content: marker.label, x, y: yTop - 6, anchor: "middle", fontSize: 11, fill: color });
1080
+ }
1081
+ }
1082
+ }
1083
+ return commands;
1084
+ }
1085
+ function drawMarkerPoint(commands, x, y, color, style) {
1086
+ const s = theme.markerSize;
1087
+ switch (style) {
1088
+ case "circle":
1089
+ commands.push({ type: "circle", cx: x, cy: y, r: s, fill: color });
1090
+ break;
1091
+ case "square":
1092
+ commands.push({ type: "rect", x: x - s, y: y - s, w: s * 2, h: s * 2, fill: color });
1093
+ break;
1094
+ case "cross":
1095
+ commands.push({ type: "line", x1: x - s, y1: y - s, x2: x + s, y2: y + s, stroke: color, strokeWidth: 2 });
1096
+ commands.push({ type: "line", x1: x - s, y1: y + s, x2: x + s, y2: y - s, stroke: color, strokeWidth: 2 });
1097
+ break;
1098
+ case "arrow":
1099
+ commands.push({
1100
+ type: "path",
1101
+ points: [{ x: x - s, y: y + s }, { x, y: y - s }, { x: x + s, y: y + s }],
1102
+ stroke: color,
1103
+ strokeWidth: 2,
1104
+ fill: "none"
1105
+ });
1106
+ break;
1107
+ case "diamond":
1108
+ commands.push({
1109
+ type: "path",
1110
+ points: [{ x, y: y - s }, { x: x + s, y }, { x, y: y + s }, { x: x - s, y }],
1111
+ fill: color,
1112
+ stroke: "none"
1113
+ });
1114
+ break;
1115
+ case "triangle":
1116
+ commands.push({
1117
+ type: "path",
1118
+ points: [{ x, y: y - s }, { x: x + s, y: y + s }, { x: x - s, y: y + s }],
1119
+ fill: color,
1120
+ stroke: "none"
1121
+ });
1122
+ break;
1123
+ case "star": {
1124
+ const pts = [];
1125
+ const innerRadius = s * 0.4;
1126
+ for (let i = 0; i < 10; i++) {
1127
+ const r = i % 2 === 0 ? s : innerRadius;
1128
+ const angle = Math.PI / 2 * 3 + i * Math.PI / 5;
1129
+ pts.push({ x: x + r * Math.cos(angle), y: y + r * Math.sin(angle) });
1130
+ }
1131
+ commands.push({ type: "path", points: pts, fill: color, stroke: "none" });
1132
+ break;
1133
+ }
1134
+ case "plus":
1135
+ commands.push({ type: "line", x1: x - s, y1: y, x2: x + s, y2: y, stroke: color, strokeWidth: 2 });
1136
+ commands.push({ type: "line", x1: x, y1: y - s, x2: x, y2: y + s, stroke: color, strokeWidth: 2 });
1137
+ break;
1138
+ case "triangle-down":
1139
+ commands.push({
1140
+ type: "path",
1141
+ points: [{ x, y: y + s }, { x: x + s, y: y - s }, { x: x - s, y: y - s }],
1142
+ fill: color,
1143
+ stroke: "none"
1144
+ });
1145
+ break;
1146
+ case "hexagon": {
1147
+ const pts = [];
1148
+ for (let i = 0; i < 6; i++) {
1149
+ const angle = i * (Math.PI / 3);
1150
+ pts.push({ x: x + s * Math.cos(angle), y: y + s * Math.sin(angle) });
1151
+ }
1152
+ commands.push({ type: "path", points: pts, fill: color, stroke: "none" });
1153
+ break;
1154
+ }
1155
+ case "hourglass":
1156
+ commands.push({
1157
+ type: "path",
1158
+ 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 }],
1159
+ fill: color,
1160
+ stroke: "none"
1161
+ });
1162
+ break;
1163
+ case "line-horizontal":
1164
+ commands.push({ type: "line", x1: x - s, y1: y, x2: x + s, y2: y, stroke: color, strokeWidth: 2 });
1165
+ break;
1166
+ }
1167
+ }
1168
+
1169
+ // src/annotation/highlight.ts
1170
+ function renderHighlights(config) {
1171
+ const { highlights, timeScale, yRange, height } = config;
1172
+ const [y0, y1] = yRange;
1173
+ const commands = [];
1174
+ for (const h of highlights) {
1175
+ const x1 = timeScale.map(h.startTime);
1176
+ const x2 = timeScale.map(h.endTime);
1177
+ commands.push({
1178
+ type: "rect",
1179
+ x: x1,
1180
+ y: y0,
1181
+ w: x2 - x1,
1182
+ h: y1 - y0,
1183
+ fill: h.color ?? theme.highlightColor,
1184
+ opacity: h.opacity ?? theme.highlightOpacity
1185
+ });
1186
+ if (h.label) {
1187
+ commands.push({
1188
+ type: "text",
1189
+ content: h.label,
1190
+ x: (x1 + x2) / 2,
1191
+ y: highlightLabelY(h.labelPosition ?? "top", y0, y1, height),
1192
+ anchor: "middle",
1193
+ fontSize: theme.annotationFontSize,
1194
+ fill: h.color ?? theme.highlightLabelColor
1195
+ });
1196
+ }
1197
+ }
1198
+ return commands;
1199
+ }
1200
+ function highlightLabelY(pos, y0, y1, height) {
1201
+ switch (pos) {
1202
+ case "above":
1203
+ return y0 - 5;
1204
+ case "below":
1205
+ return height !== void 0 ? height - 5 : y1 + 14;
1206
+ case "center":
1207
+ return (y0 + y1) / 2 + 4;
1208
+ case "bottom":
1209
+ return y1 - 6;
1210
+ case "top":
1211
+ default:
1212
+ return y0 + 14;
1213
+ }
1214
+ }
1215
+
1216
+ // src/annotation/annotation_renderer.ts
1217
+ function renderAnnotations(config) {
1218
+ const { annotations, timeScale, valueScales } = config;
1219
+ const cmds = [];
1220
+ const project = (ref) => {
1221
+ const scale = valueScales.get(ref.axis ?? 0) ?? valueScales.values().next().value;
1222
+ return { x: timeScale.map(ref.time), y: scale ? scale.map(ref.value) : 0 };
1223
+ };
1224
+ for (const a of annotations) {
1225
+ switch (a.type) {
1226
+ case "line": {
1227
+ const p1 = project(a.from);
1228
+ const p2 = project(a.to);
1229
+ 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 });
1230
+ break;
1231
+ }
1232
+ case "arrow": {
1233
+ const p1 = project(a.from);
1234
+ const p2 = project(a.to);
1235
+ const color = a.color ?? theme.annotationColor;
1236
+ const h = a.headSize ?? theme.annotationHead;
1237
+ cmds.push({ type: "line", x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, stroke: color, strokeWidth: a.width ?? theme.annotationWidth });
1238
+ const len = Math.hypot(p2.x - p1.x, p2.y - p1.y) || 1;
1239
+ const ux = (p2.x - p1.x) / len;
1240
+ const uy = (p2.y - p1.y) / len;
1241
+ const baseX = p2.x - ux * h;
1242
+ const baseY = p2.y - uy * h;
1243
+ cmds.push({
1244
+ type: "path",
1245
+ points: [
1246
+ { x: p2.x, y: p2.y },
1247
+ { x: baseX - uy * h * 0.5, y: baseY + ux * h * 0.5 },
1248
+ { x: baseX + uy * h * 0.5, y: baseY - ux * h * 0.5 }
1249
+ ],
1250
+ fill: color,
1251
+ stroke: "none"
1252
+ });
1253
+ break;
1254
+ }
1255
+ case "rect": {
1256
+ const p1 = project(a.from);
1257
+ const p2 = project(a.to);
1258
+ 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 });
1259
+ break;
1260
+ }
1261
+ case "point": {
1262
+ const p = project(a.at);
1263
+ const color = a.color ?? "#334155";
1264
+ const r = a.radius ?? theme.annotationRadius;
1265
+ const shape = a.shape ?? "circle";
1266
+ if (shape === "circle") {
1267
+ cmds.push({ type: "circle", cx: p.x, cy: p.y, r, fill: color });
1268
+ } else if (shape === "square") {
1269
+ cmds.push({ type: "rect", x: p.x - r, y: p.y - r, w: r * 2, h: r * 2, fill: color });
1270
+ } else {
1271
+ 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 });
1272
+ 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 });
1273
+ }
1274
+ break;
1275
+ }
1276
+ case "label": {
1277
+ const p = project(a.at);
1278
+ 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 });
1279
+ break;
1280
+ }
1281
+ }
1282
+ }
1283
+ return cmds;
1284
+ }
1285
+
1286
+ // src/core/layout.ts
1287
+ var Layout = class _Layout {
1288
+ _config;
1289
+ constructor(config) {
1290
+ this._config = config;
1291
+ }
1292
+ /** Calculate layout dimensions */
1293
+ compute() {
1294
+ const { width, height, margin } = this._config;
1295
+ return {
1296
+ totalWidth: width,
1297
+ totalHeight: height,
1298
+ chartWidth: width - margin.left - margin.right,
1299
+ chartHeight: height - margin.top - margin.bottom,
1300
+ chartX: margin.left,
1301
+ chartY: margin.top,
1302
+ margin
1303
+ };
1304
+ }
1305
+ /** Default layout for standard charts */
1306
+ static default(width = 800, height = 400) {
1307
+ return new _Layout({
1308
+ width,
1309
+ height,
1310
+ margin: { top: 20, right: 20, bottom: 40, left: 60 }
1311
+ });
1312
+ }
1313
+ };
1314
+
1315
+ // src/core/clip.ts
1316
+ var Clip = class {
1317
+ #regions;
1318
+ constructor() {
1319
+ this.#regions = [];
1320
+ }
1321
+ /** Add a clipping region */
1322
+ add(region) {
1323
+ this.#regions.push(region);
1324
+ }
1325
+ /** Clear all clipping regions */
1326
+ clear() {
1327
+ this.#regions = [];
1328
+ }
1329
+ /** Check if a point is within the clipping regions */
1330
+ isInside(x, y) {
1331
+ if (this.#regions.length === 0) return true;
1332
+ return this.#regions.some(
1333
+ (r) => x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height
1334
+ );
1335
+ }
1336
+ /** Generate SVG clipPath element */
1337
+ toSVGClipPath(id = "clip") {
1338
+ if (this.#regions.length === 0) return "";
1339
+ const rects = this.#regions.map((r) => `<rect x="${r.x}" y="${r.y}" width="${r.width}" height="${r.height}" />`).join("\n ");
1340
+ return `<clipPath id="${id}">
1341
+ ${rects}
1342
+ </clipPath>`;
1343
+ }
1344
+ /** Get current clipping regions */
1345
+ get regions() {
1346
+ return [...this.#regions];
1347
+ }
1348
+ };
1349
+
1350
+ // src/axis/time_axis.ts
1351
+ var DEFAULT_COLORS = {
1352
+ axisColor: theme.axisColor,
1353
+ tickColor: theme.tickColor,
1354
+ textColor: theme.textColor,
1355
+ textSize: theme.textSize
1356
+ };
1357
+ var TimeAxis = class {
1358
+ #scale;
1359
+ #config;
1360
+ constructor(config) {
1361
+ this.#scale = new TimeScale({
1362
+ domain: config.domain,
1363
+ range: config.xRange,
1364
+ locale: config.locale
1365
+ });
1366
+ this.#config = config;
1367
+ }
1368
+ get scale() {
1369
+ return this.#scale;
1370
+ }
1371
+ get axisColor() {
1372
+ return (this.#config.colors ?? DEFAULT_COLORS).axisColor;
1373
+ }
1374
+ get tickColor() {
1375
+ return (this.#config.colors ?? DEFAULT_COLORS).tickColor;
1376
+ }
1377
+ get textColor() {
1378
+ return (this.#config.colors ?? DEFAULT_COLORS).textColor;
1379
+ }
1380
+ get textSize() {
1381
+ return (this.#config.colors ?? DEFAULT_COLORS).textSize;
1382
+ }
1383
+ /**
1384
+ * Generate properly spaced, formatted ticks for the time axis.
1385
+ * Applies anti-overlap: if ticks are too close, every-other is skipped.
1386
+ */
1387
+ generateTicks() {
1388
+ const minTicks = this.#config.minTicks ?? 5;
1389
+ const maxTicks = this.#config.maxTicks ?? 12;
1390
+ const timestamps = this.#scale.ticks({ minTicks, maxTicks });
1391
+ const ticks = timestamps.map((time) => ({
1392
+ time,
1393
+ x: this.#scale.map(time),
1394
+ label: this.tickLabel(time)
1395
+ }));
1396
+ return this.antiOverlap(ticks);
1397
+ }
1398
+ /** Pick the right date format based on the tick interval. */
1399
+ tickLabel(time) {
1400
+ if (this.#config.format) return this.#config.format(new Date(time));
1401
+ const minTicks = this.#config.minTicks ?? 5;
1402
+ const maxTicks = this.#config.maxTicks ?? 12;
1403
+ const { interval } = this.#scale.tickInterval(
1404
+ (minTicks + maxTicks) / 2,
1405
+ minTicks,
1406
+ maxTicks
1407
+ );
1408
+ const opts = {};
1409
+ if (interval < 6e4) {
1410
+ opts.hour = "2-digit";
1411
+ opts.minute = "2-digit";
1412
+ opts.second = "2-digit";
1413
+ } else if (interval < 36e5) {
1414
+ opts.hour = "2-digit";
1415
+ opts.minute = "2-digit";
1416
+ } else if (interval < 864e5) {
1417
+ opts.hour = "2-digit";
1418
+ opts.minute = "2-digit";
1419
+ } else if (interval < 31536e6) {
1420
+ opts.day = "numeric";
1421
+ opts.month = "short";
1422
+ if (interval >= 2592e6) {
1423
+ opts.day = void 0;
1424
+ opts.month = "long";
1425
+ }
1426
+ } else {
1427
+ opts.year = "numeric";
1428
+ if (interval < 2 * 31536e6) opts.month = "short";
1429
+ }
1430
+ return this.#scale.format(time, opts);
1431
+ }
1432
+ /** Remove ticks that would overlap (minimum 60px spacing). */
1433
+ antiOverlap(ticks) {
1434
+ if (ticks.length <= 1) return ticks;
1435
+ const minGap = 60;
1436
+ const result = [ticks[0]];
1437
+ for (let i = 1; i < ticks.length; i++) {
1438
+ const lastX = result[result.length - 1].x;
1439
+ if (Math.abs(ticks[i].x - lastX) >= minGap) {
1440
+ result.push(ticks[i]);
1441
+ }
1442
+ }
1443
+ return result;
1444
+ }
1445
+ /** Render axis baseline + tick marks as draw commands. */
1446
+ render() {
1447
+ const ticks = this.generateTicks();
1448
+ const colors = this.#config.colors ?? DEFAULT_COLORS;
1449
+ const y = this.#config.y ?? 0;
1450
+ const commands = [];
1451
+ const [x0] = this.#scale.range();
1452
+ commands.push({
1453
+ type: "line",
1454
+ x1: x0,
1455
+ y1: y,
1456
+ x2: ticks[ticks.length - 1]?.x ?? x0,
1457
+ y2: y,
1458
+ stroke: colors.axisColor,
1459
+ strokeWidth: colors.axisWidth
1460
+ });
1461
+ for (const tick of ticks) {
1462
+ commands.push({
1463
+ type: "line",
1464
+ x1: tick.x,
1465
+ y1: y,
1466
+ x2: tick.x,
1467
+ y2: y + 6,
1468
+ stroke: colors.tickColor,
1469
+ strokeWidth: colors.axisWidth
1470
+ });
1471
+ commands.push({
1472
+ type: "text",
1473
+ content: tick.label,
1474
+ x: tick.x,
1475
+ y: y + colors.textSize + 6,
1476
+ anchor: "middle",
1477
+ fontSize: colors.textSize,
1478
+ fill: colors.textColor
1479
+ });
1480
+ }
1481
+ return commands;
1482
+ }
1483
+ };
1484
+
1485
+ // src/axis/value_axis.ts
1486
+ var DEFAULT_COLORS2 = {
1487
+ axisColor: "#ccc",
1488
+ tickColor: "#ddd",
1489
+ textColor: "#777",
1490
+ textSize: 12
1491
+ };
1492
+ function defaultFormat(value) {
1493
+ if (Math.abs(value) >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
1494
+ if (Math.abs(value) >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
1495
+ if (Number.isInteger(value)) return String(value);
1496
+ return value.toFixed(1);
1497
+ }
1498
+ var ValueAxis = class {
1499
+ #scale;
1500
+ #config;
1501
+ constructor(config) {
1502
+ this.#scale = new LinearScale({ domain: config.domain, range: config.range });
1503
+ this.#config = config;
1504
+ }
1505
+ get scale() {
1506
+ return this.#scale;
1507
+ }
1508
+ get axisColor() {
1509
+ return (this.#config.colors ?? DEFAULT_COLORS2).axisColor;
1510
+ }
1511
+ get tickColor() {
1512
+ return (this.#config.colors ?? DEFAULT_COLORS2).tickColor;
1513
+ }
1514
+ get textColor() {
1515
+ return (this.#config.colors ?? DEFAULT_COLORS2).textColor;
1516
+ }
1517
+ get textSize() {
1518
+ return (this.#config.colors ?? DEFAULT_COLORS2).textSize;
1519
+ }
1520
+ /** Generate nicely-spaced tick values. */
1521
+ generateTicks() {
1522
+ const format = this.#config.format ?? defaultFormat;
1523
+ const numTicks = 6;
1524
+ const [d0, d1] = this.#scale.domain();
1525
+ const range = d1 - d0;
1526
+ if (range === 0) {
1527
+ return [{ value: d0, position: this.#scale.map(d0), label: format(d0) }];
1528
+ }
1529
+ const rough = range / numTicks;
1530
+ const magnitude = Math.pow(10, Math.floor(Math.log10(rough)));
1531
+ const residual = rough / magnitude;
1532
+ let step;
1533
+ if (residual <= 1.5) step = magnitude;
1534
+ else if (residual <= 3) step = 2 * magnitude;
1535
+ else if (residual <= 7) step = 5 * magnitude;
1536
+ else step = 10 * magnitude;
1537
+ const ticks = [];
1538
+ const start = Math.ceil(d0 / step) * step;
1539
+ for (let v = start; v <= d1; v += step) {
1540
+ ticks.push({ value: v, position: this.#scale.map(v), label: format(v) });
1541
+ }
1542
+ return ticks;
1543
+ }
1544
+ /** Render axis as draw commands. */
1545
+ render() {
1546
+ const ticks = this.generateTicks();
1547
+ const colors = this.#config.colors ?? DEFAULT_COLORS2;
1548
+ const x = this.#config.x ?? 0;
1549
+ const orientation = this.#config.orientation ?? "vertical";
1550
+ const position = this.#config.position ?? "left";
1551
+ const commands = [];
1552
+ if (orientation === "vertical") {
1553
+ const [r0, r1] = this.#scale.range();
1554
+ commands.push({
1555
+ type: "line",
1556
+ x1: x,
1557
+ y1: r0,
1558
+ x2: x,
1559
+ y2: r1,
1560
+ stroke: colors.axisColor,
1561
+ strokeWidth: colors.axisWidth
1562
+ });
1563
+ for (const tick of ticks) {
1564
+ if (position === "left") {
1565
+ commands.push({
1566
+ type: "line",
1567
+ x1: x - 4,
1568
+ y1: tick.position,
1569
+ x2: x,
1570
+ y2: tick.position,
1571
+ stroke: colors.tickColor,
1572
+ strokeWidth: colors.axisWidth
1573
+ });
1574
+ commands.push({
1575
+ type: "text",
1576
+ content: tick.label,
1577
+ x: x - 8,
1578
+ y: tick.position + 4,
1579
+ anchor: "end",
1580
+ fontSize: 11,
1581
+ fill: colors.textColor
1582
+ });
1583
+ } else {
1584
+ commands.push({
1585
+ type: "line",
1586
+ x1: x,
1587
+ y1: tick.position,
1588
+ x2: x + 4,
1589
+ y2: tick.position,
1590
+ stroke: colors.tickColor,
1591
+ strokeWidth: colors.axisWidth
1592
+ });
1593
+ commands.push({
1594
+ type: "text",
1595
+ content: tick.label,
1596
+ x: x + 8,
1597
+ y: tick.position + 4,
1598
+ anchor: "start",
1599
+ fontSize: 11,
1600
+ fill: colors.textColor
1601
+ });
1602
+ }
1603
+ }
1604
+ } else {
1605
+ const [r0, r1] = this.#scale.range();
1606
+ commands.push({
1607
+ type: "line",
1608
+ x1: r0,
1609
+ y1: x,
1610
+ x2: r1,
1611
+ y2: x,
1612
+ stroke: colors.axisColor,
1613
+ strokeWidth: colors.axisWidth
1614
+ });
1615
+ for (const tick of ticks) {
1616
+ commands.push({
1617
+ type: "line",
1618
+ x1: tick.position,
1619
+ y1: x,
1620
+ x2: tick.position,
1621
+ y2: x + 6,
1622
+ stroke: colors.tickColor,
1623
+ strokeWidth: colors.axisWidth
1624
+ });
1625
+ commands.push({
1626
+ type: "text",
1627
+ content: tick.label,
1628
+ x: tick.position,
1629
+ y: x + 18,
1630
+ anchor: "middle",
1631
+ fontSize: colors.textSize,
1632
+ fill: colors.textColor
1633
+ });
1634
+ }
1635
+ }
1636
+ return commands;
1637
+ }
1638
+ };
1639
+
1640
+ // src/axis/enum_axis.ts
1641
+ var EnumAxis = class {
1642
+ #scale;
1643
+ /** All enum keys (as strings), sorted numerically — the full category list. */
1644
+ #keys;
1645
+ #config;
1646
+ constructor(config) {
1647
+ const domain = Object.keys(config.enumMap).map(Number).sort((a, b) => a - b).map(String);
1648
+ this.#keys = domain;
1649
+ this.#scale = new BandScale({ domain, range: config.range });
1650
+ this.#config = config;
1651
+ }
1652
+ get scale() {
1653
+ return this.#scale;
1654
+ }
1655
+ /** Get all enum ticks with labels, positions, colors. */
1656
+ generateTicks() {
1657
+ const { enumMap, showLabels = true, autoColor = true, gapValues } = this.#config;
1658
+ const gaps = new Set(gapValues ?? []);
1659
+ return this.#keys.map((key, idx) => {
1660
+ const value = Number(key);
1661
+ const label = showLabels ? enumMap[value] ?? String(value) : String(value);
1662
+ const y = this.#scale.map(value);
1663
+ const color = autoColor ? theme.palette[idx % theme.palette.length] : void 0;
1664
+ return { value, label, y, color, isGap: gaps.has(value) };
1665
+ });
1666
+ }
1667
+ /** Get color for a specific enum value. */
1668
+ colorFor(value) {
1669
+ const idx = this.#keys.indexOf(String(value));
1670
+ if (idx === -1 || !(this.#config.autoColor ?? true)) return void 0;
1671
+ return theme.palette[idx % theme.palette.length];
1672
+ }
1673
+ /** Render enum axis labels as draw commands. */
1674
+ render() {
1675
+ const ticks = this.generateTicks();
1676
+ const x = this.#config.x ?? 0;
1677
+ const commands = [];
1678
+ for (const tick of ticks) {
1679
+ if (tick.isGap) continue;
1680
+ commands.push({
1681
+ type: "text",
1682
+ content: tick.label,
1683
+ x,
1684
+ y: tick.y + 4,
1685
+ anchor: "end",
1686
+ fontSize: 11,
1687
+ fill: tick.color ?? theme.textColor
1688
+ });
1689
+ }
1690
+ return commands;
1691
+ }
1692
+ /** Map an enum value to its pixel position. */
1693
+ map(value) {
1694
+ return this.#scale.map(value);
1695
+ }
1696
+ /** Check if a value should be treated as a gap. */
1697
+ isGap(value) {
1698
+ return new Set(this.#config.gapValues ?? []).has(value);
1699
+ }
1700
+ };
1701
+
1702
+ // src/series/series.ts
1703
+ var Series = class _Series {
1704
+ /** SVG id prefix for elements. Generated ids: `<id>-slot-<index>`, `<id>-avg-<index>`. */
1705
+ #id;
1706
+ #timeScale;
1707
+ static uidcnt = 0;
1708
+ #data;
1709
+ constructor(config, data = []) {
1710
+ this.#id = config.id ?? "id" + Date.now + ++_Series.uidcnt;
1711
+ this.#timeScale = config.timeScale;
1712
+ this.#data = data;
1713
+ }
1714
+ get id() {
1715
+ return this.#id;
1716
+ }
1717
+ get timeScale() {
1718
+ return this.#timeScale;
1719
+ }
1720
+ get data() {
1721
+ return this.#data;
1722
+ }
1723
+ };
1724
+
1725
+ // src/series/line_series.ts
1726
+ var LineSeries = class extends Series {
1727
+ #config;
1728
+ constructor(config) {
1729
+ super(config, config.data);
1730
+ this.#config = config;
1731
+ }
1732
+ /** Convert non-null data points to pixel coordinates (sorted by X). */
1733
+ points() {
1734
+ const valueScale = this.#config.valueScale;
1735
+ return this.data.filter((dp) => dp.value !== null).map((dp) => ({
1736
+ x: this.timeScale.map(dp.time),
1737
+ y: valueScale.map(dp.value),
1738
+ time: dp.time
1739
+ })).sort((a, b) => a.x - b.x);
1740
+ }
1741
+ render() {
1742
+ const c = this.#config;
1743
+ const style = {
1744
+ smoothing: c.smoothing ?? false,
1745
+ stroke: c.stroke ?? theme.stroke,
1746
+ strokeWidth: c.strokeWidth ?? theme.strokeWidth,
1747
+ dashed: c.dashed ?? false,
1748
+ pointStyle: c.pointStyle ?? "none",
1749
+ pointSize: c.pointSize ?? theme.pointSize,
1750
+ shadowColor: c.shadowColor,
1751
+ shadowBlur: c.shadowBlur,
1752
+ shadowOffsetX: c.shadowOffsetX,
1753
+ shadowOffsetY: c.shadowOffsetY
1754
+ };
1755
+ const pointThreshold = c.pointThreshold ?? theme.pointThreshold;
1756
+ const gapThreshold = c.gapThreshold ?? theme.gapThreshold;
1757
+ const id = c.id;
1758
+ const runs = SeriesProcessor.getRuns(
1759
+ this.data,
1760
+ (p) => p.value === null,
1761
+ gapThreshold
1762
+ );
1763
+ const totalPoints = runs.reduce((sum, run) => sum + run.length, 0);
1764
+ if (totalPoints === 0) return [];
1765
+ const ctx = { timeScale: this.timeScale, valueScale: c.valueScale };
1766
+ const segments = runs.map((run) => ({ data: run }));
1767
+ const commands = [];
1768
+ commands.push(
1769
+ ...renderLine(segments, ctx, {
1770
+ ...style,
1771
+ id: id ? `${id}-line` : void 0
1772
+ })
1773
+ );
1774
+ if (style.pointStyle && style.pointStyle !== "none" && totalPoints <= pointThreshold) {
1775
+ for (const run of runs) {
1776
+ commands.push(
1777
+ ...renderMarkers(
1778
+ run,
1779
+ ctx,
1780
+ { ...style, id: id ? `${id}-marker` : void 0 },
1781
+ () => style.stroke
1782
+ )
1783
+ );
1784
+ }
1785
+ }
1786
+ return commands;
1787
+ }
1788
+ };
1789
+
1790
+ // src/series/step_series.ts
1791
+ var StepSeries = class extends Series {
1792
+ #config;
1793
+ constructor(config) {
1794
+ super(config, config.data);
1795
+ this.#config = config;
1796
+ }
1797
+ /**
1798
+ * Convert (non-null) data points to step-style pixel coordinates.
1799
+ * Each data point generates two corners (horizontal then vertical).
1800
+ */
1801
+ points() {
1802
+ const valueScale = this.#config.valueScale;
1803
+ const sorted = [...this.data].filter((p) => p.value !== null).sort((a, b) => a.time - b.time);
1804
+ const pts = [];
1805
+ for (let i = 0; i < sorted.length; i++) {
1806
+ const px = this.timeScale.map(sorted[i].time);
1807
+ const py = valueScale.map(sorted[i].value);
1808
+ if (i === 0) {
1809
+ pts.push({ x: px, y: py });
1810
+ } else {
1811
+ pts.push({ x: px, y: pts[pts.length - 1].y });
1812
+ pts.push({ x: px, y: py });
1813
+ }
1814
+ }
1815
+ return pts;
1816
+ }
1817
+ render() {
1818
+ const runs = SeriesProcessor.getRuns(this.data, (p) => p.value === null);
1819
+ if (runs.length === 0) return [];
1820
+ const ctx = { timeScale: this.timeScale, valueScale: this.#config.valueScale };
1821
+ const segments = runs.map((run) => ({ data: run }));
1822
+ return renderStep(segments, ctx, {
1823
+ stroke: this.#config.stroke ?? theme.stroke,
1824
+ strokeWidth: this.#config.strokeWidth ?? theme.strokeWidth,
1825
+ id: this.id ? `${this.id}-line` : void 0
1826
+ });
1827
+ }
1828
+ };
1829
+
1830
+ // src/series/band_series.ts
1831
+ var BandSeries = class extends Series {
1832
+ #config;
1833
+ constructor(config) {
1834
+ super(config, config.data);
1835
+ this.#config = config;
1836
+ }
1837
+ /** Calculate opacity from count (normalized 0.2-1.0) */
1838
+ opacity(count) {
1839
+ if (!(this.#config.countOpacity ?? false)) return 0.6;
1840
+ const maxCount = Math.max(...this.data.map((d) => d.count));
1841
+ if (maxCount === 0) return 0.2;
1842
+ return 0.2 + 0.8 * count / maxCount;
1843
+ }
1844
+ /** Render bands as draw commands */
1845
+ render() {
1846
+ const c = this.#config;
1847
+ const fill = c.fill ?? theme.bandFill;
1848
+ const hatch = c.hatch;
1849
+ const avgLine = c.avgLine ?? false;
1850
+ const avgLineColor = c.avgLineColor ?? theme.bandAvgLine;
1851
+ const bandWidth = c.bandWidth ?? 10;
1852
+ const commands = [];
1853
+ for (const dp of this.data) {
1854
+ if (dp.min === null || dp.max === null) continue;
1855
+ const x = this.timeScale.map(dp.time);
1856
+ const yMin = c.valueScale.map(dp.max);
1857
+ const yMax = c.valueScale.map(dp.min);
1858
+ const w = bandWidth;
1859
+ const idx = this.data.indexOf(dp);
1860
+ commands.push({
1861
+ type: "rect",
1862
+ x: x - w / 2,
1863
+ y: yMin,
1864
+ w,
1865
+ h: yMax - yMin,
1866
+ fill,
1867
+ hatch,
1868
+ opacity: this.opacity(dp.count),
1869
+ id: this.id ? `${this.id}-slot-${idx}` : void 0
1870
+ });
1871
+ if (avgLine && dp.avg !== null) {
1872
+ const yAvg = c.valueScale.map(dp.avg);
1873
+ commands.push({
1874
+ type: "line",
1875
+ x1: x - w / 2,
1876
+ y1: yAvg,
1877
+ x2: x + w / 2,
1878
+ y2: yAvg,
1879
+ stroke: avgLineColor,
1880
+ strokeWidth: 1,
1881
+ id: this.id ? `${this.id}-avg-${idx}` : void 0
1882
+ });
1883
+ }
1884
+ }
1885
+ return commands;
1886
+ }
1887
+ };
1888
+
1889
+ // src/series/minmaxavg_series.ts
1890
+ var MinMaxAvgSeries = class extends Series {
1891
+ #config;
1892
+ constructor(config) {
1893
+ super(config, config.data);
1894
+ this.#config = config;
1895
+ }
1896
+ render() {
1897
+ const c = this.#config;
1898
+ const minColor = c.minColor ?? theme.minColor;
1899
+ const maxColor = c.maxColor ?? theme.maxColor;
1900
+ const avgColor = c.avgColor ?? theme.avgColor;
1901
+ const avgDashed = c.avgDashed ?? true;
1902
+ const smoothing = c.smoothing ?? false;
1903
+ const strokeWidth = c.strokeWidth ?? theme.strokeWidth;
1904
+ const runs = SeriesProcessor.getRuns(
1905
+ this.data,
1906
+ (p) => p.min === null || p.max === null || p.avg === null
1907
+ );
1908
+ if (runs.length === 0) return [];
1909
+ const ctx = { timeScale: this.timeScale, valueScale: c.valueScale };
1910
+ const commands = [];
1911
+ for (const run of runs) {
1912
+ if (run.length < 2) continue;
1913
+ if (c.fillToMax) {
1914
+ commands.push(
1915
+ ...renderZonedArea(
1916
+ run,
1917
+ ctx,
1918
+ {
1919
+ boundaries: [],
1920
+ yLow: (p) => p.avg,
1921
+ yHigh: (p) => p.max,
1922
+ getValue: (p) => p.avg,
1923
+ interpolate: SeriesProcessor.interpolateAggregatedPoint,
1924
+ getColor: () => c.fillToMax,
1925
+ getHatch: () => c.fillToMaxHatch
1926
+ },
1927
+ { id: this.id ? `${this.id}-fillToMax` : void 0 }
1928
+ )
1929
+ );
1930
+ }
1931
+ if (c.fillToMin) {
1932
+ commands.push(
1933
+ ...renderZonedArea(
1934
+ run,
1935
+ ctx,
1936
+ {
1937
+ boundaries: [],
1938
+ yLow: (p) => p.avg,
1939
+ yHigh: (p) => p.min,
1940
+ getValue: (p) => p.avg,
1941
+ interpolate: SeriesProcessor.interpolateAggregatedPoint,
1942
+ getColor: () => c.fillToMin,
1943
+ getHatch: () => c.fillToMinHatch
1944
+ },
1945
+ { id: this.id ? `${this.id}-fillToMin` : void 0 }
1946
+ )
1947
+ );
1948
+ }
1949
+ commands.push(
1950
+ ...renderLine(
1951
+ [{ data: run.map((p) => ({ time: p.time, value: p.max })) }],
1952
+ ctx,
1953
+ { stroke: maxColor, strokeWidth, smoothing, id: this.id ? `${this.id}-max` : void 0 }
1954
+ ),
1955
+ ...renderLine(
1956
+ [{ data: run.map((p) => ({ time: p.time, value: p.min })) }],
1957
+ ctx,
1958
+ { stroke: minColor, strokeWidth, smoothing, id: this.id ? `${this.id}-min` : void 0 }
1959
+ ),
1960
+ ...renderLine(
1961
+ [{ data: run.map((p) => ({ time: p.time, value: p.avg })) }],
1962
+ ctx,
1963
+ { stroke: avgColor, strokeWidth, smoothing, dashed: avgDashed, id: this.id ? `${this.id}-avg` : void 0 }
1964
+ )
1965
+ );
1966
+ }
1967
+ return commands;
1968
+ }
1969
+ };
1970
+
1971
+ // src/series/zoned_line_series.ts
1972
+ var ZonedLineSeries = class extends Series {
1973
+ #config;
1974
+ #sortedZones;
1975
+ constructor(config) {
1976
+ super(config, config.data);
1977
+ this.#config = config;
1978
+ this.#sortedZones = [...config.zones ?? []].sort((a, b) => a.value - b.value);
1979
+ }
1980
+ /** Colour for a given zone index (0 = base, 1..N = zones). */
1981
+ #colorAtZone(zoneIndex) {
1982
+ if (zoneIndex === 0) return this.#config.baseColor ?? theme.stroke;
1983
+ return this.#sortedZones[zoneIndex - 1].color;
1984
+ }
1985
+ render() {
1986
+ const c = this.#config;
1987
+ const baseColor = c.baseColor ?? theme.stroke;
1988
+ const style = {
1989
+ stroke: baseColor,
1990
+ strokeWidth: c.strokeWidth ?? theme.strokeWidth,
1991
+ smoothing: c.smoothing ?? false,
1992
+ pointStyle: c.pointStyle ?? "none",
1993
+ pointSize: c.pointSize ?? theme.pointSize
1994
+ };
1995
+ const gapThreshold = c.gapThreshold ?? theme.gapThreshold;
1996
+ const pointThreshold = c.pointThreshold ?? theme.pointThreshold;
1997
+ const runs = SeriesProcessor.getRuns(this.data, (p) => p.value === null, gapThreshold);
1998
+ const totalPoints = runs.reduce((sum, run) => sum + run.length, 0);
1999
+ if (totalPoints === 0) return [];
2000
+ const ctx = { timeScale: this.timeScale, valueScale: c.valueScale };
2001
+ const commands = [];
2002
+ for (const run of runs) {
2003
+ if (run.length < 2) continue;
2004
+ if (c.fill) {
2005
+ const fillVal = c.fill.value;
2006
+ const fillSegments = SeriesProcessor.splitByBoundaries(
2007
+ run,
2008
+ [fillVal],
2009
+ (p) => p.value,
2010
+ SeriesProcessor.interpolateDataPoint
2011
+ );
2012
+ const yFill = c.valueScale.map(fillVal);
2013
+ for (let fi = 0; fi < fillSegments.length; fi++) {
2014
+ const seg = fillSegments[fi];
2015
+ const rep = (seg.data[0].value + seg.data[seg.data.length - 1].value) / 2;
2016
+ if (c.fill.side === "above" === rep >= fillVal) {
2017
+ const pts = seg.data.map((p) => ({
2018
+ x: this.timeScale.map(p.time),
2019
+ y: c.valueScale.map(p.value)
2020
+ }));
2021
+ commands.push({
2022
+ type: "path",
2023
+ id: this.id ? `${this.id}-fill-${fi}` : void 0,
2024
+ points: [...pts, { x: pts[pts.length - 1].x, y: yFill }, { x: pts[0].x, y: yFill }],
2025
+ fill: c.fill.color,
2026
+ hatch: c.fill.hatch,
2027
+ stroke: "none"
2028
+ });
2029
+ }
2030
+ }
2031
+ }
2032
+ const boundaries = this.#sortedZones.map((z) => z.value);
2033
+ const segments = SeriesProcessor.splitByBoundaries(
2034
+ run,
2035
+ boundaries,
2036
+ (p) => p.value,
2037
+ SeriesProcessor.interpolateDataPoint
2038
+ );
2039
+ const zonedSegments = segments.map((seg) => ({
2040
+ data: seg.data,
2041
+ color: this.#colorAtZone(seg.zoneIndex)
2042
+ }));
2043
+ commands.push(
2044
+ ...renderLine(zonedSegments, ctx, { ...style, id: this.id ? `${this.id}-line` : void 0 })
2045
+ );
2046
+ if (style.pointStyle && style.pointStyle !== "none" && totalPoints <= pointThreshold) {
2047
+ commands.push(
2048
+ ...renderMarkers(
2049
+ run,
2050
+ ctx,
2051
+ { ...style, id: this.id ? `${this.id}-marker` : void 0 },
2052
+ (p) => {
2053
+ let color = baseColor;
2054
+ for (const z of this.#sortedZones) {
2055
+ if (p.value >= z.value) color = z.color;
2056
+ }
2057
+ return color;
2058
+ }
2059
+ )
2060
+ );
2061
+ }
2062
+ }
2063
+ return commands;
2064
+ }
2065
+ };
2066
+
2067
+ // src/series/annotation_band.ts
2068
+ var AnnotationBandSeries = class {
2069
+ #config;
2070
+ constructor(config, xRange, y, height) {
2071
+ this.#config = { ...config, xRange, y, height };
2072
+ }
2073
+ /** Render the band as colored rects with labels, optionally with a time axis. */
2074
+ render() {
2075
+ const { items, timeScale, background, hatch: bandHatch, showAxis, xRange, y, height } = this.#config;
2076
+ const commands = [];
2077
+ if (background) {
2078
+ commands.push({
2079
+ type: "rect",
2080
+ x: xRange[0],
2081
+ y,
2082
+ w: xRange[1] - xRange[0],
2083
+ h: height,
2084
+ fill: background,
2085
+ opacity: 0.04,
2086
+ stroke: "#ddd",
2087
+ strokeWidth: 0.25
2088
+ });
2089
+ }
2090
+ for (const item of items) {
2091
+ const x1 = timeScale.map(item.startTime);
2092
+ const x2 = timeScale.map(item.endTime);
2093
+ if (x2 - x1 < 1) continue;
2094
+ commands.push({
2095
+ type: "rect",
2096
+ x: x1,
2097
+ y,
2098
+ w: x2 - x1,
2099
+ h: height,
2100
+ hatch: item.hatch ?? bandHatch,
2101
+ fill: item.fill ?? "#6b728044",
2102
+ stroke: item.stroke,
2103
+ strokeWidth: item.strokeWidth ?? 0
2104
+ });
2105
+ if (item.label) {
2106
+ commands.push({
2107
+ type: "text",
2108
+ content: item.label,
2109
+ x: (x1 + x2) / 2,
2110
+ y: this.#labelY(item.labelBaseline),
2111
+ anchor: "middle",
2112
+ fontSize: item.labelFontSize ?? 10,
2113
+ fill: item.labelFill ?? "#333",
2114
+ textBaseline: item.labelBaseline ?? "middle"
2115
+ });
2116
+ }
2117
+ }
2118
+ if (showAxis) {
2119
+ const timeAxis = new TimeAxis({
2120
+ domain: timeScale.domain(),
2121
+ xRange,
2122
+ y: y + height + 4
2123
+ });
2124
+ commands.push({
2125
+ type: "group",
2126
+ cssClass: "annotation-band-axis",
2127
+ commands: timeAxis.render()
2128
+ });
2129
+ }
2130
+ return commands;
2131
+ }
2132
+ #labelY(baseline) {
2133
+ const { y, height } = this.#config;
2134
+ switch (baseline) {
2135
+ case "top":
2136
+ return y + 1;
2137
+ case "bottom":
2138
+ return y + height - 1;
2139
+ default:
2140
+ return y + height / 2;
2141
+ }
2142
+ }
2143
+ };
2144
+
2145
+ // src/analyze/stats.ts
2146
+ var StatsAggregator = class {
2147
+ /** Compute statistics from data points */
2148
+ static compute(data) {
2149
+ const values = data.map((d) => d.value).filter((v) => v !== null).sort((a, b) => a - b);
2150
+ if (values.length === 0) {
2151
+ return { min: NaN, max: NaN, avg: NaN, mean: NaN, median: NaN, stdDev: NaN, count: 0 };
2152
+ }
2153
+ const count = values.length;
2154
+ const sum = values.reduce((a, b) => a + b, 0);
2155
+ const mean = sum / count;
2156
+ const median = count % 2 === 1 ? values[Math.floor(count / 2)] : (values[count / 2 - 1] + values[count / 2]) / 2;
2157
+ const variance = values.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / count;
2158
+ const stdDev = Math.sqrt(variance);
2159
+ return {
2160
+ min: values[0],
2161
+ max: values[count - 1],
2162
+ avg: mean,
2163
+ mean,
2164
+ median,
2165
+ stdDev,
2166
+ count
2167
+ };
2168
+ }
2169
+ /** Compute stats for a specific time range (viewport-scoped) */
2170
+ static computeInRange(data, startTime, endTime) {
2171
+ const filtered = data.filter((d) => d.time >= startTime && d.time <= endTime);
2172
+ return this.compute(filtered);
2173
+ }
2174
+ };
2175
+
2176
+ // src/series/stats_overlay.ts
2177
+ var StatsOverlay = class {
2178
+ #config;
2179
+ constructor(config) {
2180
+ this.#config = config;
2181
+ }
2182
+ /** Render stats as horizontal lines with labels. */
2183
+ render() {
2184
+ const {
2185
+ data,
2186
+ valueScale,
2187
+ xRange,
2188
+ showMin = false,
2189
+ showMax = false,
2190
+ showAvg = true,
2191
+ showMedian = false,
2192
+ labelPosition = "end",
2193
+ lineColor = theme.statsLineColor,
2194
+ labelColor = theme.statsLabelColor
2195
+ } = this.#config;
2196
+ const stats = StatsAggregator.compute(data);
2197
+ const lines = [
2198
+ { name: "min", value: stats.min, enabled: showMin },
2199
+ { name: "max", value: stats.max, enabled: showMax },
2200
+ { name: "avg", value: stats.avg, enabled: showAvg },
2201
+ { name: "median", value: stats.median, enabled: showMedian }
2202
+ ];
2203
+ const commands = [];
2204
+ const [x0, x1] = xRange;
2205
+ for (const stat of lines.filter((s) => s.enabled)) {
2206
+ const y = valueScale.map(stat.value);
2207
+ commands.push({ type: "line", x1: x0, y1: y, x2: x1, y2: y, stroke: lineColor, strokeWidth: 1 });
2208
+ const label = `${stat.name}: ${stat.value.toFixed(1)}`;
2209
+ if (labelPosition === "start" || labelPosition === "both") {
2210
+ commands.push({ type: "text", content: label, x: x0 + 4, y: y - 4, anchor: "start", fontSize: 10, fill: labelColor });
2211
+ }
2212
+ if (labelPosition === "end" || labelPosition === "both") {
2213
+ commands.push({ type: "text", content: label, x: x1 - 4, y: y - 4, anchor: "end", fontSize: 10, fill: labelColor });
2214
+ }
2215
+ if (labelPosition === "center") {
2216
+ commands.push({ type: "text", content: label, x: (x0 + x1) / 2, y: y - 4, anchor: "middle", fontSize: 10, fill: labelColor });
2217
+ }
2218
+ }
2219
+ return commands;
2220
+ }
2221
+ };
2222
+
2223
+ // src/formatter/time_formatter.ts
2224
+ var TimeFormatter = class {
2225
+ #locale;
2226
+ #fallbackLocales;
2227
+ #autoFormat;
2228
+ constructor(opts) {
2229
+ this.#locale = opts?.locale || (typeof navigator !== "undefined" ? navigator.language : "en-US");
2230
+ this.#fallbackLocales = opts?.fallbackLocales ?? ["en-US"];
2231
+ this.#autoFormat = opts?.autoFormat ?? true;
2232
+ }
2233
+ /**
2234
+ * Format a timestamp.
2235
+ * Auto-selects format options based on the time difference if autoFormat is enabled.
2236
+ */
2237
+ format(timestamp, opts, referenceTime) {
2238
+ try {
2239
+ const options = this.#autoFormat && referenceTime ? this.#autoOptions(timestamp, referenceTime, opts) : opts;
2240
+ return new Intl.DateTimeFormat(this.#locale, options).format(
2241
+ new Date(timestamp)
2242
+ );
2243
+ } catch {
2244
+ for (const locale of this.#fallbackLocales) {
2245
+ try {
2246
+ return new Intl.DateTimeFormat(locale, opts).format(
2247
+ new Date(timestamp)
2248
+ );
2249
+ } catch {
2250
+ }
2251
+ }
2252
+ return new Date(timestamp).toISOString();
2253
+ }
2254
+ }
2255
+ /** Format a time range display */
2256
+ formatRange(start, end) {
2257
+ const startStr = this.format(start);
2258
+ const endStr = this.format(end);
2259
+ return `${startStr} \u2014 ${endStr}`;
2260
+ }
2261
+ #autoOptions(timestamp, referenceTime, base) {
2262
+ const diff = Math.abs(timestamp - referenceTime);
2263
+ const day = 864e5;
2264
+ const hour = 36e5;
2265
+ const minute = 6e4;
2266
+ const opts = {};
2267
+ if (diff < minute) {
2268
+ opts.second = "2-digit";
2269
+ opts.minute = "2-digit";
2270
+ opts.hour = "2-digit";
2271
+ } else if (diff < hour) {
2272
+ opts.minute = "2-digit";
2273
+ opts.hour = "2-digit";
2274
+ } else if (diff < day) {
2275
+ opts.hour = "2-digit";
2276
+ opts.minute = "2-digit";
2277
+ } else if (diff < 7 * day) {
2278
+ opts.weekday = "short";
2279
+ opts.day = "numeric";
2280
+ } else if (diff < 365 * day) {
2281
+ opts.month = "short";
2282
+ opts.day = "numeric";
2283
+ } else {
2284
+ opts.month = "short";
2285
+ opts.year = "numeric";
2286
+ }
2287
+ return { ...base, ...opts };
2288
+ }
2289
+ };
2290
+
2291
+ // src/formatter/value_formatter.ts
2292
+ var ValueFormatter = class _ValueFormatter {
2293
+ #unit;
2294
+ #decimals;
2295
+ _compact;
2296
+ #custom;
2297
+ constructor(opts) {
2298
+ this.#unit = opts?.unit ?? "";
2299
+ this.#decimals = opts?.decimals ?? 1;
2300
+ this._compact = opts?.compact ?? false;
2301
+ this.#custom = opts?.custom;
2302
+ }
2303
+ /** Format a single value */
2304
+ format(value) {
2305
+ if (this.#custom) {
2306
+ return this.#custom(value);
2307
+ }
2308
+ let formatted;
2309
+ if (this._compact && Math.abs(value) >= 1e6) {
2310
+ formatted = `${(value / 1e6).toFixed(this.#decimals)}M`;
2311
+ } else if (this._compact && Math.abs(value) >= 1e3) {
2312
+ formatted = `${(value / 1e3).toFixed(this.#decimals)}k`;
2313
+ } else {
2314
+ formatted = value.toFixed(this.#decimals);
2315
+ }
2316
+ return this.#unit ? `${formatted}${this.#unit}` : formatted;
2317
+ }
2318
+ /** Format a range of values */
2319
+ formatRange(min, max) {
2320
+ return `${this.format(min)} \u2014 ${this.format(max)}`;
2321
+ }
2322
+ /** Create a formatter for a specific unit */
2323
+ static unit(unit, decimals = 1) {
2324
+ return new _ValueFormatter({ unit, decimals });
2325
+ }
2326
+ /** Temperature formatter */
2327
+ static temperature(unit = "\xB0C") {
2328
+ return new _ValueFormatter({ unit, decimals: 1 });
2329
+ }
2330
+ /** Percentage formatter */
2331
+ static percentage() {
2332
+ return new _ValueFormatter({ unit: "%", decimals: 0 });
2333
+ }
2334
+ };
2335
+
2336
+ // src/formatter/enum_formatter.ts
2337
+ var EnumFormatter = class _EnumFormatter {
2338
+ #map;
2339
+ #fallback;
2340
+ #showValue;
2341
+ constructor(opts) {
2342
+ this.#map = opts?.map ?? {};
2343
+ this.#fallback = opts?.fallback ?? ((v) => String(v));
2344
+ this.#showValue = opts?.showValue ?? false;
2345
+ }
2346
+ /** Format an enum value */
2347
+ format(value) {
2348
+ const label = this.#map[value] ?? this.#fallback(value);
2349
+ return this.#showValue ? `(${value}) ${label}` : label;
2350
+ }
2351
+ /** Check if value exists in map */
2352
+ has(value) {
2353
+ return value in this.#map;
2354
+ }
2355
+ /** Get all mapped labels */
2356
+ labels() {
2357
+ return Object.values(this.#map);
2358
+ }
2359
+ /** Get all mapped values */
2360
+ values() {
2361
+ return Object.keys(this.#map).map(Number);
2362
+ }
2363
+ /** Create formatter from label config strings */
2364
+ static fromLabels(labels, showValue = false) {
2365
+ const map = {};
2366
+ labels.forEach((label, idx) => {
2367
+ map[idx] = label;
2368
+ });
2369
+ return new _EnumFormatter({ map, showValue });
2370
+ }
2371
+ };
2372
+
2373
+ // src/formatter/index.ts
2374
+ var Formatters = {
2375
+ time: new TimeFormatter(),
2376
+ value: new ValueFormatter(),
2377
+ enum: new EnumFormatter()
2378
+ };
2379
+
2380
+ // src/theme/runtime.ts
2381
+ var currentDefault = { ...theme };
2382
+ function getDefaultTheme() {
2383
+ return currentDefault;
2384
+ }
2385
+
2386
+ // src/style/resolver.ts
2387
+ function mergeLine(line, series) {
2388
+ const theme2 = getDefaultTheme();
2389
+ return {
2390
+ color: line.color ?? theme2.stroke,
2391
+ width: line.width ?? theme2.strokeWidth,
2392
+ style: line.style ?? "solid",
2393
+ smoothing: line.smoothing ?? false,
2394
+ shape: line.shape ?? (series.seriesType === "step" ? "step" : "line"),
2395
+ gapThreshold: line.gapThreshold ?? theme2.gapThreshold,
2396
+ opacity: line.opacity ?? 1
2397
+ };
2398
+ }
2399
+ function resolveLines(series) {
2400
+ const styleLine = series.style?.line;
2401
+ if (styleLine === false) return [];
2402
+ if (styleLine === void 0) return [mergeLine({}, series)];
2403
+ if (Array.isArray(styleLine)) return styleLine.map((l) => mergeLine(l, series));
2404
+ return [mergeLine(styleLine, series)];
2405
+ }
2406
+ function resolveMarkers(series) {
2407
+ const m = series.style?.markers;
2408
+ if (!m?.type || m.type === "none") return void 0;
2409
+ const theme2 = getDefaultTheme();
2410
+ const lineColor = series.style?.line && !Array.isArray(series.style.line) ? series.style.line.color : void 0;
2411
+ const seriesColor = lineColor ?? theme2.stroke;
2412
+ return {
2413
+ type: m.type,
2414
+ size: m.size ?? theme2.pointSize,
2415
+ stroke: m.stroke ?? seriesColor,
2416
+ fill: m.fill ?? "#ffffff",
2417
+ strokeWidth: m.strokeWidth ?? 1,
2418
+ threshold: m.threshold
2419
+ };
2420
+ }
2421
+ function resolveShadow(series) {
2422
+ const s = series.style?.shadow;
2423
+ if (!s?.color || s.color === "transparent" || s.color === "none") return void 0;
2424
+ return {
2425
+ color: s.color,
2426
+ blur: s.blur ?? 4,
2427
+ offsetX: s.offsetX ?? 0,
2428
+ offsetY: s.offsetY ?? 0
2429
+ };
2430
+ }
2431
+ function resolveGap(series) {
2432
+ return series.style?.gap;
2433
+ }
2434
+ function resolveFill(series) {
2435
+ return series.style?.fill;
2436
+ }
2437
+ function resolveSeriesStyle(series) {
2438
+ return {
2439
+ lines: resolveLines(series),
2440
+ fill: resolveFill(series),
2441
+ markers: resolveMarkers(series),
2442
+ shadow: resolveShadow(series),
2443
+ gap: resolveGap(series),
2444
+ id: series.style?.id ?? series.id
2445
+ };
2446
+ }
2447
+
2448
+ // src/core/slug.ts
2449
+ function slugify(s) {
2450
+ if (!s) return "unnamed";
2451
+ const slug = s.toString().normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
2452
+ return slug || "unnamed";
2453
+ }
2454
+ /*!
2455
+ * MLTimeGraph — Copyright (c) 2026 Michael Lechner
2456
+ * MIT with Attribution: free use incl. commercial requires visible credit to
2457
+ * "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
2458
+ */
2459
+ /*!
2460
+ * MLTimeGraph — Copyright (c) 2026 Michael Lechner
2461
+ * MIT with Attribution: free use incl. commercial must have visible credit to
2462
+ * "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
2463
+ */
2464
+
2465
+ export { AnnotationBandSeries, BandScale, BandSeries, Clip, EnumAxis, EnumFormatter, Formatters, Layout, LineSeries, LinearScale, MinMaxAvgSeries, Renderer, StatsOverlay, StepSeries, TIME_INTERVALS, TimeAxis, TimeFormatter, TimeScale, ValueAxis, ValueFormatter, ZonedLineSeries, getHatch, measureLegend, renderAnnotations, renderArea, renderGaps, renderGrid, renderHighlights, renderLegend, renderLine, renderMarkers2 as renderMarkers, renderMarkers as renderSeriesMarkers, renderSplitLine, renderStep, renderThresholds, renderZonedArea, resolveFill, resolveGap, resolveLines, resolveMarkers, resolveSeriesStyle, resolveShadow, slugify, theme };