ml-time-graph 1.0.0 → 1.0.5
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/API_DESIGN.md +1211 -0
- package/README.de.md +12 -0
- package/README.md +12 -0
- package/dist/analyze/index.d.ts +272 -46
- package/dist/analyze/index.js +11 -570
- package/dist/index.d.ts +346 -9
- package/dist/index.js +57 -3420
- package/dist/interaction/index.d.ts +1 -1
- package/dist/interaction/index.js +2 -435
- package/dist/internals.d.ts +20 -5
- package/dist/internals.js +40 -2455
- package/dist/{layout-Sc5UkC0r.d.ts → layout-BOYtrsZa.d.ts} +1 -1
- package/dist/{scale-Cbr0KpPz.d.ts → scale-BRE_QhbZ.d.ts} +38 -15
- package/package.json +65 -80
- package/dist/aggregated_subtypes-DZNZyFTX.d.ts +0 -43
- /package/{MKT_AGGREGATE.md → docs/MKT_AGGREGATE.md} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,3432 +1,69 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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}" />
|
|
1
|
+
var nt=class r{_config;constructor(t){this._config=t;}compute(){let{width:t,height:e,margin:n}=this._config;return {totalWidth:t,totalHeight:e,chartWidth:t-n.left-n.right,chartHeight:e-n.top-n.bottom,chartX:n.left,chartY:n.top,margin:n}}static default(t=800,e=400){return new r({width:t,height:e,margin:{top:20,right:20,bottom:40,left:60}})}};function it(r){return r&&r.toString().normalize("NFKD").replace(/[̀-ͯ]/g,"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,50)||"unnamed"}var U=class{#t;#e;constructor(t){this.#t=[...t.domain],this.#e=[...t.range];}map(t){let e=Number(t),[n,s]=this.#t,[i,o]=this.#e;return s===n?i:i+(e-n)/(s-n)*(o-i)}invert(t){let[e,n]=this.#t,[s,i]=this.#e;return i===s?e:e+(t-s)/(i-s)*(n-e)}domain(){return [...this.#t]}range(){return [...this.#e]}},G=[{label:"second",ms:1e3},{label:"2_seconds",ms:2e3},{label:"5_seconds",ms:5e3},{label:"10_seconds",ms:1e4},{label:"30_seconds",ms:3e4},{label:"minute",ms:6e4},{label:"5_minutes",ms:3e5},{label:"15_minutes",ms:9e5},{label:"30_minutes",ms:18e5},{label:"hour",ms:36e5},{label:"3_hours",ms:108e5},{label:"6_hours",ms:216e5},{label:"day",ms:864e5},{label:"week",ms:6048e5},{label:"month",ms:2592e6},{label:"3_months",ms:7776e6},{label:"6_months",ms:15552e6},{label:"year",ms:31536e6},{label:"2_years",ms:63072e6},{label:"5_years",ms:15768e7}],J=class{#t;#e;constructor(t){this.#t=new U({domain:t.domain,range:t.range}),this.#e=t.locale||(typeof navigator<"u"?navigator.language:"en-US");}map(t){return this.#t.map(Number(t))}invert(t){return this.#t.invert(t)}domain(){return this.#t.domain()}range(){return this.#t.range()}get locale(){return this.#e}tickInterval(t,e=3,n=12){let[s,i]=this.#t.domain(),o=i-s;if(o<=0)return {interval:G[0].ms};let a=o/t,l=G[0].ms;for(let u of G)if(u.ms>=a){l=u.ms;break}let h=l,d=Math.round(o/h);for(;d>n&&h<G[G.length-1].ms;){let u=G.findIndex(m=>m.ms===h);h=G[Math.min(u+1,G.length-1)].ms,d=Math.round(o/h);}for(;d<e&&h>G[0].ms;){let u=G.findIndex(m=>m.ms===h);h=G[Math.max(u-1,0)].ms,d=Math.round(o/h);}return {interval:h}}ticks(t){let e=t?.minTicks??5,n=t?.maxTicks??12,{interval:s}=this.tickInterval((e+n)/2,e,n),[i,o]=this.#t.domain(),a=[],l=Math.ceil(i/s)*s;for(let h=l;h<=o;h+=s)a.push(h);return a}format(t,e){return new Intl.DateTimeFormat(this.#e,e).format(new Date(t))}};var c={stroke:"#4285f4",strokeWidth:2,pointSize:4,pointThreshold:100,gapThreshold:0,fill:"none",hatch:null,bandFill:"#4285f4",bandOpacity:.6,bandAvgLine:"#e53e3e",minColor:"#3b82f6",maxColor:"#ef4444",avgColor:"#64748b",areaFillAlpha:"4285f433",axisColor:"#ccc",tickColor:"#ddd",textColor:"#777",textSize:11,axisLabelColor:"#444",axisLabelSize:12,gridStroke:"#e2e8f0",gridStrokeWidth:1,gridOpacity:1,legendStroke:"#ccc",legendText:"#333",legendFont:11,annotationColor:"#334155",annotationWidth:1.5,annotationHead:9,annotationRadius:4,annotationFontSize:11,thresholdColor:"#666",thresholdLine:"dashed",thresholdFillOpacity:.12,thresholdFontSize:10,highlightColor:"#fbbf24",highlightOpacity:.2,highlightLabelColor:"#92400e",markerColor:"#f59e0b",markerSize:5,gapFill:"#fff",gapStroke:"#ccc",gapStrokeWidth:1,gapFontColor:"#999",gapFontSize:10,gapFillOpacity:.15,tooltipBg:"#fff",tooltipBorder:"#cbd5e1",tooltipText:"#1e293b",tooltipValue:"#3b82f6",tooltipCrosshair:"#94a3b8",tooltipSnapRadius:20,statsLineColor:"#94a3b8",statsLabelColor:"#64748b",minimapStroke:"#94a3b8",minimapBg:"#f8f9fa",minimapBrush:"#3b82f644",palette:["#4285f4","#ea4335","#22c55e","#fbbc05","#9334ea","#12b5e5","#fb923c","#6366f1"]};function St(r,t="classic-diagonal",e="rgba(200, 220, 255, 0.3)",n="#4D88FF",s=2){if(t==="none")return `
|
|
2
|
+
<pattern id="${r}" width="10" height="10" patternUnits="userSpaceOnUse">
|
|
3
|
+
<rect width="10" height="10" fill="${e}" />
|
|
316
4
|
</pattern>
|
|
317
|
-
`.trim();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
break;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
break
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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}
|
|
5
|
+
`.trim();let i=12,o=12,a="rotate(0)",l="";switch(t){case "classic-diagonal":i=12,o=12,a="rotate(45)",l=`<line x1="0" y1="0" x2="0" y2="${o}" stroke="${n}" stroke-width="${s}" stroke-linecap="square" />`;break;case "dense-steep":i=6,o=6,a="rotate(30)",l=`<line x1="0" y1="0" x2="0" y2="${o}" stroke="${n}" stroke-width="${s}" stroke-linecap="square" />`;break;case "crosshatch":i=14,o=14,a="rotate(45)",l=`
|
|
6
|
+
<line x1="0" y1="0" x2="0" y2="${o}" stroke="${n}" stroke-width="${s}" stroke-linecap="square" />
|
|
7
|
+
<line x1="0" y1="0" x2="${i}" y2="0" stroke="${n}" stroke-width="${s}" stroke-linecap="square" />
|
|
8
|
+
`;break;case "dots":i=12,o=12,l=`<circle cx="${i/2}" cy="${o/2}" r="${s*1.2}" fill="${n}" />`;break;case "waves":i=16,o=16,l=`
|
|
9
|
+
<path d="M 0 ${o/2} Q ${i/4} 0, ${i/2} ${o/2} T ${i} ${o/2}"
|
|
10
|
+
fill="none" stroke="${n}" stroke-width="${s}" stroke-linecap="round" />
|
|
11
|
+
`;break;case "dashed":i=12,o=12,a="rotate(45)",l=`<line x1="0" y1="0" x2="0" y2="${o}" stroke="${n}" stroke-width="${s}" stroke-dasharray="3,3" />`;break;case "herringbone":i=16,o=16,l=`
|
|
12
|
+
<path d="M 0 0 L ${i/2} ${o/2} L 0 ${o} M ${i} 0 L ${i/2} ${o/2} L ${i} ${o}"
|
|
13
|
+
fill="none" stroke="${n}" stroke-width="${s}" stroke-linejoin="round" stroke-linecap="round" />
|
|
14
|
+
`;break;case "brick":i=20,o=20,l=`
|
|
15
|
+
<path d="M 0 ${o/2} L ${i} ${o/2} M 0 ${o} L ${i} ${o} M ${i/2} 0 L ${i/2} ${o/2} M 0 ${o/2} L 0 ${o}"
|
|
16
|
+
fill="none" stroke="${n}" stroke-width="${s}" />
|
|
17
|
+
`;break;case "double-stripe":i=16,o=16,a="rotate(45)",l=`
|
|
18
|
+
<line x1="0" y1="0" x2="0" y2="${o}" stroke="${n}" stroke-width="${s}" stroke-linecap="square" />
|
|
19
|
+
<line x1="${i/2}" y1="0" x2="${i/2}" y2="${o}" stroke="${n}" stroke-width="${s/2}" stroke-linecap="square" />
|
|
20
|
+
`;break;case "honeycomb":i=18,o=32,l=`
|
|
21
|
+
<path d="M 0 0 L ${i/2} 5 L ${i} 0 M 0 16 L ${i/2} 11 L ${i} 16 M 0 16 L 0 32 M ${i/2} 5 L ${i/2} 11 M ${i} 16 L ${i} 32 M 0 32 L ${i/2} 27 L ${i} 32 M ${i/2} 27 L ${i/2} 32"
|
|
22
|
+
fill="none" stroke="${n}" stroke-width="${s}" stroke-linejoin="round" stroke-linecap="round" />
|
|
23
|
+
`;break}let h=`<rect width="${i}" height="${o}" fill="${e}" />`;return `
|
|
24
|
+
<pattern id="${r}" width="${i}" height="${o}" patternTransform="${a}" patternUnits="userSpaceOnUse">
|
|
25
|
+
${h}
|
|
26
|
+
${l}
|
|
403
27
|
</pattern>
|
|
404
|
-
`.trim();
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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 ")}
|
|
28
|
+
`.trim()}function Ht(r,t=2){let e=t;switch(r){case "dotted":return {strokeDasharray:`0, ${e*2}`,strokeLinecap:"round"};case "sparse-dots":return {strokeDasharray:`0, ${e*4}`,strokeLinecap:"round"};case "dashed":return {strokeDasharray:`${e*3}, ${e*2}`,strokeLinecap:"butt"};case "long-dash":return {strokeDasharray:`${e*6}, ${e*3}`,strokeLinecap:"butt"};case "dense-dash":return {strokeDasharray:`${e*1.5}, ${e*1.5}`,strokeLinecap:"butt"};case "dash-dot":return {strokeDasharray:`${e*4}, ${e*2}, 0, ${e*2}`,strokeLinecap:"round"};case "dash-dot-dot":return {strokeDasharray:`${e*5}, ${e*2}, 0, ${e*2}, 0, ${e*2}`,strokeLinecap:"round"};case "loose-dash":return {strokeDasharray:`${e*3}, ${e*4}`,strokeLinecap:"butt"};default:return {strokeDasharray:"none",strokeLinecap:"butt"}}}var rt={axisColor:c.axisColor,tickColor:c.tickColor,textColor:c.textColor,textSize:c.textSize},tt=class{#t;#e;constructor(t){this.#t=new J({domain:t.domain,range:t.xRange,locale:t.locale}),this.#e=t;}get scale(){return this.#t}get axisColor(){return (this.#e.colors??rt).axisColor}get tickColor(){return (this.#e.colors??rt).tickColor}get textColor(){return (this.#e.colors??rt).textColor}get textSize(){return (this.#e.colors??rt).textSize}generateTicks(){let t=this.#e.minTicks??5,e=this.#e.maxTicks??12,s=this.#t.ticks({minTicks:t,maxTicks:e}).map(i=>({time:i,x:this.#t.map(i),label:this.tickLabel(i)}));return this.antiOverlap(s)}tickLabel(t){if(this.#e.format)return this.#e.format(new Date(t));let e=this.#e.minTicks??5,n=this.#e.maxTicks??12,{interval:s}=this.#t.tickInterval((e+n)/2,e,n),i={};return s<6e4?(i.hour="2-digit",i.minute="2-digit",i.second="2-digit"):s<36e5||s<864e5?(i.hour="2-digit",i.minute="2-digit"):s<31536e6?(i.day="numeric",i.month="short",s>=2592e6&&(i.day=void 0,i.month="long")):(i.year="numeric",s<2*31536e6&&(i.month="short")),this.#t.format(t,i)}antiOverlap(t){if(t.length<=1)return t;let e=60,n=[t[0]];for(let s=1;s<t.length;s++){let i=n[n.length-1].x;Math.abs(t[s].x-i)>=e&&n.push(t[s]);}return n}render(){let t=this.generateTicks(),e=this.#e.colors??rt,n=this.#e.y??0,s=[],[i]=this.#t.range();s.push({type:"line",x1:i,y1:n,x2:t[t.length-1]?.x??i,y2:n,stroke:e.axisColor,strokeWidth:e.axisWidth});for(let o of t)s.push({type:"line",x1:o.x,y1:n,x2:o.x,y2:n+6,stroke:e.tickColor,strokeWidth:e.axisWidth}),s.push({type:"text",content:o.label,x:o.x,y:n+e.textSize+6,anchor:"middle",fontSize:e.textSize,fill:e.textColor});return s}};var st={axisColor:"#ccc",tickColor:"#ddd",textColor:"#777",textSize:12};function ae(r){return Math.abs(r)>=1e6?`${(r/1e6).toFixed(1)}M`:Math.abs(r)>=1e3?`${(r/1e3).toFixed(1)}k`:Number.isInteger(r)?String(r):r.toFixed(1)}var lt=class{#t;#e;constructor(t){this.#t=new U({domain:t.domain,range:t.range}),this.#e=t;}get scale(){return this.#t}get axisColor(){return (this.#e.colors??st).axisColor}get tickColor(){return (this.#e.colors??st).tickColor}get textColor(){return (this.#e.colors??st).textColor}get textSize(){return (this.#e.colors??st).textSize}generateTicks(){let t=this.#e.format??ae,e=6,[n,s]=this.#t.domain(),i=s-n;if(i===0)return [{value:n,position:this.#t.map(n),label:t(n)}];let o=i/e,a=Math.pow(10,Math.floor(Math.log10(o))),l=o/a,h;l<=1.5?h=a:l<=3?h=2*a:l<=7?h=5*a:h=10*a;let d=[],u=Math.ceil(n/h)*h;for(let m=u;m<=s;m+=h)d.push({value:m,position:this.#t.map(m),label:t(m)});return d}render(){let t=this.generateTicks(),e=this.#e.colors??st,n=this.#e.x??0,s=this.#e.orientation??"vertical",i=this.#e.position??"left",o=this.#e.suppressLabelsNear??[],a=this.#e.suppressTolerancePx??8,l=d=>o.some(u=>Math.abs(u-d)<=a),h=[];if(s==="vertical"){let[d,u]=this.#t.range();h.push({type:"line",x1:n,y1:d,x2:n,y2:u,stroke:e.axisColor,strokeWidth:e.axisWidth});for(let m of t){let g=l(m.position);i==="left"?(h.push({type:"line",x1:n-4,y1:m.position,x2:n,y2:m.position,stroke:e.tickColor,strokeWidth:e.axisWidth}),g||h.push({type:"text",content:m.label,x:n-8,y:m.position+4,anchor:"end",fontSize:11,fill:e.textColor})):(h.push({type:"line",x1:n,y1:m.position,x2:n+4,y2:m.position,stroke:e.tickColor,strokeWidth:e.axisWidth}),g||h.push({type:"text",content:m.label,x:n+8,y:m.position+4,anchor:"start",fontSize:11,fill:e.textColor}));}}else {let[d,u]=this.#t.range();h.push({type:"line",x1:d,y1:n,x2:u,y2:n,stroke:e.axisColor,strokeWidth:e.axisWidth});for(let m of t)h.push({type:"line",x1:m.position,y1:n,x2:m.position,y2:n+6,stroke:e.tickColor,strokeWidth:e.axisWidth}),h.push({type:"text",content:m.label,x:m.position,y:n+18,anchor:"middle",fontSize:e.textSize,fill:e.textColor});}return h}};var F=class{static interpolateDataPoint(r,t,e){return {time:r.time+e*(t.time-r.time),value:(r.value??0)+e*((t.value??0)-(r.value??0))}}static interpolateAggregatedPoint(r,t,e){let n=(s,i)=>s!==null&&i!==null?s+e*(i-s):null;return {time:r.time+e*(t.time-r.time),min:n(r.min,t.min),max:n(r.max,t.max),avg:n(r.avg,t.avg),count:Math.round(r.count+e*(t.count-r.count))}}static getRuns(r,t,e=0){let n=[...r].sort((a,l)=>a.time-l.time),s=[],i=[],o=null;for(let a of n){let l=t(a),h=e>0&&o&&a.time-o.time>e;(l||h)&&i.length&&(s.push(i),i=[]),l||i.push(a),o=a;}return i.length&&s.push(i),s}static splitByBoundaries(r,t,e,n){if(r.length===0)return [];if(t.length===0)return [{data:r,zoneIndex:0}];let s=[...t].sort((l,h)=>l-h),i=[],o=l=>{let h=0;for(let d=0;d<s.length&&l>=s[d];d++)h=d+1;return h},a=[r[0]];for(let l=1;l<r.length;l++){let h=r[l-1],d=r[l],u=e(h),m=e(d),g;m>u?g=s.filter(p=>p>u&&p<=m):m<u?g=s.filter(p=>p>=m&&p<u).reverse():g=[];for(let p of g){let b=(p-u)/(m-u),S=n(h,d,b);a.push(S),i.push({data:a,zoneIndex:o((u+p)/2)}),a=[S];}a.push(d);}if(a.length>0){let l=e(a[0]),h=e(a[a.length-1]);i.push({data:a,zoneIndex:o((l+h)/2)});}return i}static splitByThreshold(r,t,e,n){let s=this.splitByBoundaries(r,[t],e,n),i={above:[],below:[]};for(let o of s)o.zoneIndex===0?i.below.push(o.data):i.above.push(o.data);return i}};function Ot(r,t=6e4){if(r.length<2)return [];let e=[...r].sort((s,i)=>s.time-i.time),n=[];for(let s=1;s<e.length;s++)e[s].time-e[s-1].time>t&&n.push({startTime:e[s-1].time,endTime:e[s].time});return n}function le(r,t=83144){let e=t/8.314,n=0,s=0;for(let o of r)o!==null&&(n+=Math.exp(-e/(o+273.15)),s++);if(s===0)return null;let i=n/s;return e/-Math.log(i)-273.15}function jt(r,t,e=83144){let n=new Array(r.length),s=0;for(let i=0;i<r.length;i++){let o=r[i].time,a=o-t;for(;s<i&&r[s].time<a;)s++;let l=r.slice(s,i+1).map(d=>d.value),h=le(l,e);n[i]={time:o,value:h,synthetic:true};}return n}function he(r){let t=0,e=0;for(let i of r)i!==null&&(t+=i,e++);if(e===0)return null;let n=t/e,s=0;for(let i of r){if(i===null)continue;let o=i-n;s+=o*o;}return Math.sqrt(s/e)}function de(r){let t=0,e=0;for(let i of r)i!==null&&(t+=i,e++);if(e<2)return null;let n=t/e,s=0;for(let i of r){if(i===null)continue;let o=i-n;s+=o*o;}return Math.sqrt(s/(e-1))}function It(r,t,e=false){let n=new Array(r.length),s=0,i=e?de:he;for(let o=0;o<r.length;o++){let a=r[o].time,l=a-t;for(;s<o&&r[s].time<l;)s++;let h=r.slice(s,o+1).map(u=>u.value),d=i(h);n[o]={time:a,value:d,synthetic:true};}return n}function Gt(r){return {id:r.id,line:{stroke:r.stroke??c.stroke,strokeWidth:r.strokeWidth??c.strokeWidth,smoothing:r.smoothing??false,dashed:r.dashed??false},fill:r.fill??c.areaFillAlpha,markers:{type:r.pointStyle??"none",size:r.pointSize??c.pointSize,stroke:r.stroke??c.stroke,fill:"#ffffff"},shadow:{color:r.shadowColor??"transparent",blur:r.shadowBlur??0,offsetX:r.shadowOffsetX??0,offsetY:r.shadowOffsetY??0}}}function ht(r,t,e){let n=[],s=r.reduce((o,a)=>o+a.data.length,0),i=Gt(e);for(let o=0;o<r.length;o++){let a=r[o];a.data.length>=2?n.push({type:"path",id:e.id?`${e.id}-line-${o}`:void 0,points:a.data.map(l=>({x:t.timeScale.map(l.time),y:t.valueScale.map(l.value)})),stroke:a.color??i.line.stroke,strokeWidth:i.line.strokeWidth,smoothing:i.line.smoothing,dashed:i.line.dashed,shadowColor:i.shadow.color,shadowBlur:i.shadow.blur,shadowOffsetX:i.shadow.offsetX,shadowOffsetY:i.shadow.offsetY,fill:"none"}):a.data.length===1&&s===1&&n.push({type:"circle",cx:t.timeScale.map(a.data[0].time),cy:t.valueScale.map(a.data[0].value),r:Math.max(i.markers.size,i.line.strokeWidth),fill:a.color??i.line.stroke,shadowColor:i.shadow.color,shadowBlur:i.shadow.blur});}return n}function kt(r,t,e,n){if(r.length<2)return [];let s=F.splitByBoundaries(r,e.boundaries,e.getValue,e.interpolate),i=[];for(let o=0;o<s.length;o++){let a=s[o];if(a.data.length<2)continue;let l=e.getColor(a.zoneIndex);if(!l)continue;let h=a.data.map(u=>({x:t.timeScale.map(u.time),y:t.valueScale.map(e.yLow(u))})),d=a.data.map(u=>({x:t.timeScale.map(u.time),y:t.valueScale.map(e.yHigh(u))})).reverse();i.push({type:"path",id:n?.id?`${n.id}-fill-${o}`:void 0,points:[...h,...d],fill:l,hatch:e.getHatch?.(a.zoneIndex),stroke:"none"});}return i}function Et(r,t,e,n){let s=Gt(e);if(!s.markers.type||s.markers.type==="none")return [];let i=[];for(let o=0;o<r.length;o++){let a=r[o],l=t.timeScale.map(a.time),h=t.valueScale.map(a.value),d=n(a),u=e.pointStroke??d,m=e.pointFill??d,g=e.pointStrokeWidth??1.5,p=e.id?`${e.id}-marker-${o}`:void 0;me(i,p,s.markers.type,l,h,s.markers.size,u,m,g);}return i}function me(r,t,e,n,s,i,o,a,l){switch(e){case "circle":r.push({type:"circle",cx:n,cy:s,r:i,fill:a,stroke:o,strokeWidth:l,id:t});break;case "square":r.push({type:"rect",x:n-i,y:s-i,w:i*2,h:i*2,fill:a,stroke:o,strokeWidth:l,id:t});break;case "cross":r.push({type:"line",x1:n-i,y1:s-i,x2:n+i,y2:s+i,stroke:o,strokeWidth:l,id:t}),r.push({type:"line",x1:n-i,y1:s+i,x2:n+i,y2:s-i,stroke:o,strokeWidth:l,id:t});break;case "diamond":r.push({type:"path",points:[{x:n,y:s-i},{x:n+i,y:s},{x:n,y:s+i},{x:n-i,y:s}],fill:a,stroke:o,strokeWidth:l,id:t});break;case "triangle":r.push({type:"path",points:[{x:n,y:s-i},{x:n+i,y:s+i},{x:n-i,y:s+i}],fill:a,stroke:o,strokeWidth:l,id:t});break;case "star":{let h=[];for(let d=0;d<10;d++){let u=d%2===0?i:i*.5,m=Math.PI/2*3+d*Math.PI/5;h.push({x:n+u*Math.cos(m),y:s+u*Math.sin(m)});}r.push({type:"path",points:h,fill:a,stroke:o,strokeWidth:l,id:t});break}case "arrow":r.push({type:"path",points:[{x:n-i,y:s+i},{x:n,y:s-i},{x:n+i,y:s+i}],stroke:o,strokeWidth:l,fill:"none",id:t});break;default:r.push({type:"circle",cx:n,cy:s,r:i,fill:a,stroke:o,strokeWidth:l,id:t});}}var et=class r{#t;#e;static uidcnt=0;#n;constructor(t,e=[]){this.#t=t.id??"id"+Date.now+ ++r.uidcnt,this.#e=t.timeScale,this.#n=e;}get id(){return this.#t}get timeScale(){return this.#e}get data(){return this.#n}};var dt=class extends et{#t;constructor(t){super(t,t.data),this.#t=t;}render(){let t=this.#t,e=t.minColor??c.minColor,n=t.maxColor??c.maxColor,s=t.avgColor??c.avgColor,i=t.avgDashed??true,o=t.smoothing??false,a=t.strokeWidth??c.strokeWidth,l=F.getRuns(this.data,u=>u.min===null||u.max===null||u.avg===null);if(l.length===0)return [];let h={timeScale:this.timeScale,valueScale:t.valueScale},d=[];for(let u of l)u.length<2||(t.fillToMax&&d.push(...kt(u,h,{boundaries:[],yLow:m=>m.avg,yHigh:m=>m.max,getValue:m=>m.avg,interpolate:F.interpolateAggregatedPoint,getColor:()=>t.fillToMax,getHatch:()=>t.fillToMaxHatch},{id:this.id?`${this.id}-fillToMax`:void 0})),t.fillToMin&&d.push(...kt(u,h,{boundaries:[],yLow:m=>m.avg,yHigh:m=>m.min,getValue:m=>m.avg,interpolate:F.interpolateAggregatedPoint,getColor:()=>t.fillToMin,getHatch:()=>t.fillToMinHatch},{id:this.id?`${this.id}-fillToMin`:void 0})),d.push(...ht([{data:u.map(m=>({time:m.time,value:m.max}))}],h,{stroke:n,strokeWidth:a,smoothing:o,id:this.id?`${this.id}-max`:void 0}),...ht([{data:u.map(m=>({time:m.time,value:m.min}))}],h,{stroke:e,strokeWidth:a,smoothing:o,id:this.id?`${this.id}-min`:void 0}),...ht([{data:u.map(m=>({time:m.time,value:m.avg}))}],h,{stroke:s,strokeWidth:a,smoothing:o,dashed:i,id:this.id?`${this.id}-avg`:void 0})));return d}};var mt=class extends et{#t;constructor(t){super(t,t.data),this.#t=t;}opacity(t){if(!(this.#t.countOpacity??false))return .6;let e=Math.max(...this.data.map(n=>n.count));return e===0?.2:.2+.8*t/e}render(){let t=this.#t,e=t.fill??c.bandFill,n=t.hatch,s=t.avgLine??false,i=t.avgLineColor??c.bandAvgLine,o=t.bandWidth??10,a=[];for(let l of this.data){if(l.min===null||l.max===null)continue;let h=this.timeScale.map(l.time),d=t.valueScale.map(l.max),u=t.valueScale.map(l.min),m=o,g=this.data.indexOf(l);if(a.push({type:"rect",x:h-m/2,y:d,w:m,h:u-d,fill:e,hatch:n,opacity:this.opacity(l.count),id:this.id?`${this.id}-slot-${g}`:void 0}),s&&l.avg!==null){let p=t.valueScale.map(l.avg);a.push({type:"line",x1:h-m/2,y1:p,x2:h+m/2,y2:p,stroke:i,strokeWidth:1,id:this.id?`${this.id}-avg-${g}`:void 0});}}return a}};function ue(r){return r==="dotted"?{dash:"dotted"}:r==="dashed"?{dash:"dashed"}:{}}function Vt(r){let{thresholds:t,valueScale:e,xRange:n}=r,[s,i]=n,[o,a]=e.range(),l=Math.min(o,a),h=Math.max(o,a),d=[],u=[];for(let m of t){let g=m.color??c.thresholdColor,p=e.map(m.value);m.fill==="above"?d.push({type:"rect",x:s,y:l,w:i-s,h:Math.max(0,p-l),fill:g,hatch:m.fillHatch,opacity:m.fillOpacity??.12,id:m.id?`${m.id}-fill`:void 0}):m.fill==="below"&&d.push({type:"rect",x:s,y:p,w:i-s,h:Math.max(0,h-p),fill:g,hatch:m.fillHatch,opacity:m.fillOpacity??.12,id:m.id?`${m.id}-fill`:void 0});let b=m.line??c.thresholdLine;if(b!=="none"){let S=ue(b),k={type:"line",x1:s,y1:p,x2:i,y2:p,stroke:g,strokeWidth:1,...S,id:m.id?`${m.id}-line`:void 0};m.shadowColor&&(k.shadowColor=m.shadowColor,k.shadowBlur=m.shadowBlur??4,k.shadowOffsetX=m.shadowOffsetX??0,k.shadowOffsetY=m.shadowOffsetY??2),d.push(k);}if(m.label!==false){let S=m.label&&typeof m.label=="object"?m.label:void 0,k=typeof m.label=="string"?m.label:S?.text??m.name,w=S?.position??"right";u.push({...ce(k,w,s,i,p,g,S),id:m.id?`${m.id}-label`:void 0});}}return {inside:d,labels:u}}function ce(r,t,e,n,s,i,o){let a=(e+n)/2,l={type:"text",content:r,fontSize:c.thresholdFontSize,fill:i},h=o?{...o.rotate!==void 0&&{rotate:o.rotate},...o.textBaseline!==void 0&&{textBaseline:o.textBaseline}}:{};switch(t){case "left":return {...l,...h,x:e+4,y:s-4,anchor:"start"};case "above":return {...l,...h,x:a,y:s-6,anchor:"middle"};case "below":return {...l,...h,x:a,y:s+14,anchor:"middle"};case "center":return {...l,...h,x:a,y:s-4,anchor:"middle"};case "outside-left":return {...l,...h,x:e-6,y:s+3,anchor:"end",textBaseline:h.textBaseline??"middle"};case "outside-right":return {...l,...h,x:n+6,y:s+3,anchor:"start",textBaseline:h.textBaseline??"middle"};default:return {...l,...h,x:n-4,y:s-4,anchor:"end"}}}function zt(r){let{gaps:t,timeScale:e,yRange:n,fill:s=c.gapFill,hatch:i,fillOpacity:o=c.gapFillOpacity,stroke:a=c.gapStroke,strokeWidth:l=c.gapStrokeWidth,dashed:h=true,fontSize:d=c.gapFontSize,fontFill:u=c.gapFontColor,labelBaseline:m="middle",labelRotate:g}=r,[p,b]=n,S=[];for(let k of t){let w=e.map(k.startTime),O=e.map(k.endTime),L=k.fill??s,H=k.hatch??i,W=k.fillOpacity??o,R=k.label??"",j=k.rotate??g,$=k.labelBaseline??m;if(k.style==="dashed_border"||!k.style?S.push({type:"rect",x:w,y:p,w:O-w,h:b-p,fill:L,hatch:H,opacity:W,stroke:a,strokeWidth:l,dashed:h}):k.style==="empty"&&S.push({type:"rect",x:w,y:p,w:O-w,h:b-p,fill:L,hatch:H,opacity:W}),R){let y=pe(p,b,$),v=$==="above"?"top":$==="below"?"bottom":"middle";S.push({type:"text",content:R,x:(w+O)/2,y,anchor:"middle",fontSize:d,fill:u,textBaseline:v,rotate:j});}}return S}function pe(r,t,e){switch(e){case "above":return r-12;case "below":return t+4;default:return (r+t)/2}}var ut=class{#t;constructor(t,e,n,s){this.#t={...t,xRange:e,y:n,height:s};}render(){let{items:t,timeScale:e,background:n,hatch:s,showAxis:i,xRange:o,y:a,height:l}=this.#t,h=[];n&&h.push({type:"rect",x:o[0],y:a,w:o[1]-o[0],h:l,fill:n,opacity:.04,stroke:"#ddd",strokeWidth:.25});for(let d of t){let u=e.map(d.startTime),m=e.map(d.endTime);m-u<1||(h.push({type:"rect",x:u,y:a,w:m-u,h:l,hatch:d.hatch??s,fill:d.fill??"#6b728044",stroke:d.stroke,strokeWidth:d.strokeWidth??0}),d.label&&h.push({type:"text",content:d.label,x:(u+m)/2,y:this.#e(d.labelBaseline),anchor:"middle",fontSize:d.labelFontSize??10,fill:d.labelFill??"#333",textBaseline:d.labelBaseline??"middle"}));}if(i){let d=new tt({domain:e.domain(),xRange:o,y:a+l+4});h.push({type:"group",cssClass:"annotation-band-axis",commands:d.render()});}return h}#e(t){let{y:e,height:n}=this.#t;switch(t){case "top":return e+1;case "bottom":return e+n-1;default:return e+n/2}}};function Nt(r){let{highlights:t,timeScale:e,yRange:n,height:s}=r,[i,o]=n,a=[];for(let l of t){let h=e.map(l.startTime),d=e.map(l.endTime);a.push({type:"rect",x:h,y:i,w:d-h,h:o-i,fill:l.color??c.highlightColor,opacity:l.opacity??c.highlightOpacity}),l.label&&a.push({type:"text",content:l.label,x:(h+d)/2,y:fe(l.labelPosition??"top",i,o,s),anchor:"middle",fontSize:c.annotationFontSize,fill:l.color??c.highlightLabelColor,rotate:l.rotate});}return a}function fe(r,t,e,n){switch(r){case "above":return t-5;case "below":return n!==void 0?n-5:e+14;case "center":return (t+e)/2+4;case "bottom":return e-6;default:return t+14}}function Yt(r){let{markers:t,timeScale:e,valueScale:n,yRange:s=[0,300]}=r,[i,o]=s,a=[];for(let l of t){let h=e.map(l.time),d=l.color??c.markerColor,u=l.pointStyle??(l.value!==void 0?"circle":"none"),m=l.lineStyle??"full";if(l.value!==void 0){let g=n.map(l.value);if(m==="to-value"?a.push({type:"line",x1:h,y1:o,x2:h,y2:g,stroke:d,strokeWidth:1,dashed:true}):m==="to-top"?a.push({type:"line",x1:h,y1:i,x2:h,y2:g,stroke:d,strokeWidth:1,dashed:true}):m==="full"&&a.push({type:"line",x1:h,y1:i,x2:h,y2:o,stroke:d,strokeWidth:1}),u!=="none"&&ge(a,h,g,d,u),l.label){let p=m==="to-value"?g-10:i-6;a.push({type:"text",content:l.label,x:h,y:p,anchor:"middle",fontSize:11,fill:d});}}else a.push({type:"line",x1:h,y1:i,x2:h,y2:o,stroke:d,strokeWidth:1}),l.label&&a.push({type:"text",content:l.label,x:h,y:i-6,anchor:"middle",fontSize:11,fill:d});}return a}function ge(r,t,e,n,s){let i=c.markerSize;switch(s){case "circle":r.push({type:"circle",cx:t,cy:e,r:i,fill:n});break;case "square":r.push({type:"rect",x:t-i,y:e-i,w:i*2,h:i*2,fill:n});break;case "cross":r.push({type:"line",x1:t-i,y1:e-i,x2:t+i,y2:e+i,stroke:n,strokeWidth:2}),r.push({type:"line",x1:t-i,y1:e+i,x2:t+i,y2:e-i,stroke:n,strokeWidth:2});break;case "arrow":r.push({type:"path",points:[{x:t-i,y:e+i},{x:t,y:e-i},{x:t+i,y:e+i}],stroke:n,strokeWidth:2,fill:"none"});break;case "diamond":r.push({type:"path",points:[{x:t,y:e-i},{x:t+i,y:e},{x:t,y:e+i},{x:t-i,y:e}],fill:n,stroke:"none"});break;case "triangle":r.push({type:"path",points:[{x:t,y:e-i},{x:t+i,y:e+i},{x:t-i,y:e+i}],fill:n,stroke:"none"});break;case "star":{let o=[],a=i*.4;for(let l=0;l<10;l++){let h=l%2===0?i:a,d=Math.PI/2*3+l*Math.PI/5;o.push({x:t+h*Math.cos(d),y:e+h*Math.sin(d)});}r.push({type:"path",points:o,fill:n,stroke:"none"});break}case "plus":r.push({type:"line",x1:t-i,y1:e,x2:t+i,y2:e,stroke:n,strokeWidth:2}),r.push({type:"line",x1:t,y1:e-i,x2:t,y2:e+i,stroke:n,strokeWidth:2});break;case "triangle-down":r.push({type:"path",points:[{x:t,y:e+i},{x:t+i,y:e-i},{x:t-i,y:e-i}],fill:n,stroke:"none"});break;case "hexagon":{let o=[];for(let a=0;a<6;a++){let l=a*(Math.PI/3);o.push({x:t+i*Math.cos(l),y:e+i*Math.sin(l)});}r.push({type:"path",points:o,fill:n,stroke:"none"});break}case "hourglass":r.push({type:"path",points:[{x:t-i,y:e-i},{x:t+i,y:e-i},{x:t-i,y:e+i},{x:t+i,y:e+i}],fill:n,stroke:"none"});break;case "line-horizontal":r.push({type:"line",x1:t-i,y1:e,x2:t+i,y2:e,stroke:n,strokeWidth:2});break}}function Xt(r){let{annotations:t,timeScale:e,valueScales:n}=r,s=[],i=o=>{let a=n.get(o.axis??0)??n.values().next().value;return {x:e.map(o.time),y:a?a.map(o.value):0}};for(let o of t){let a=[],l=h=>a.push(h);switch(o.type){case "line":{let h=i(o.from),d=i(o.to);l({type:"line",x1:h.x,y1:h.y,x2:d.x,y2:d.y,stroke:o.color??c.annotationColor,strokeWidth:o.width??c.annotationWidth,dash:o.dash});break}case "arrow":{let h=i(o.from),d=i(o.to),u=o.color??c.annotationColor,m=o.headSize??c.annotationHead;l({type:"line",x1:h.x,y1:h.y,x2:d.x,y2:d.y,stroke:u,strokeWidth:o.width??c.annotationWidth});let g=Math.hypot(d.x-h.x,d.y-h.y)||1,p=(d.x-h.x)/g,b=(d.y-h.y)/g,S=d.x-p*m,k=d.y-b*m;l({type:"path",points:[{x:d.x,y:d.y},{x:S-b*m*.5,y:k+p*m*.5},{x:S+b*m*.5,y:k-p*m*.5}],fill:u,stroke:"none"});break}case "rect":{let h=i(o.from),d=i(o.to);l({type:"rect",x:Math.min(h.x,d.x),y:Math.min(h.y,d.y),w:Math.abs(d.x-h.x),h:Math.abs(d.y-h.y),fill:o.fill??"none",stroke:o.stroke,opacity:o.opacity});break}case "point":{let h=i(o.at),d=o.color??"#334155",u=o.radius??c.annotationRadius,m=o.shape??"circle";m==="circle"?l({type:"circle",cx:h.x,cy:h.y,r:u,fill:d}):m==="square"?l({type:"rect",x:h.x-u,y:h.y-u,w:u*2,h:u*2,fill:d}):(l({type:"line",x1:h.x-u,y1:h.y-u,x2:h.x+u,y2:h.y+u,stroke:d,strokeWidth:1.5}),l({type:"line",x1:h.x-u,y1:h.y+u,x2:h.x+u,y2:h.y-u,stroke:d,strokeWidth:1.5}));break}case "label":{let h=i(o.at);l({type:"text",content:o.text,x:h.x+(o.dx??0),y:h.y+(o.dy??0),anchor:o.anchor??"middle",fontSize:c.annotationFontSize,fill:o.color??c.annotationColor,rotate:o.rotate});break}}if(o.id&&a.length>0){let h=it(o.id);s.push({type:"group",cssClass:`annotation annotation--${h}`,commands:a});}else s.push(...a);}return s}var K=12,ct=8,_t=20,ye=11,wt=18,vt=r=>r.length*ye*.6;function qt(r,t="vertical"){if(t==="horizontal"){let n=0;for(let s of r)n+=K+ct+vt(s.name)+wt;return {width:Math.max(0,n-wt),height:_t}}let e=0;for(let n of r)e=Math.max(e,vt(n.name));return {width:K+ct+e,height:r.length*_t}}function Ut(r){let{items:t,x:e,y:n,orientation:s="vertical"}=r,i=[],o=e;return t.forEach((a,l)=>{let h=s==="horizontal"?o:e,d=s==="horizontal"?n:n+l*_t;i.push({type:"rect",x:h,y:d,w:K,h:K,fill:a.color,stroke:c.legendStroke,strokeWidth:1}),i.push({type:"text",content:a.name,x:h+K+ct,y:d+K-2,fontSize:c.legendFont,fill:c.legendText}),s==="horizontal"&&(o+=K+ct+vt(a.name)+wt);}),{type:"group",cssClass:"chart-legend",commands:i}}function Kt(r){let{xTicks:t,yTicks:e,xRange:n,yRange:s,stroke:i=c.gridStroke,strokeWidth:o=c.gridStrokeWidth,dashed:a=false,opacity:l=c.gridOpacity}=r,h=[];if(e)for(let d of e)h.push({type:"line",x1:n[0],y1:d,x2:n[1],y2:d,stroke:i,strokeWidth:o,dashed:a,opacity:l});if(t)for(let d of t)h.push({type:"line",x1:d,y1:s[0],x2:d,y2:s[1],stroke:i,strokeWidth:o,dashed:a,opacity:l});return h}function Zt(r,t,e){let n=r??t;if(n==="series")return null;if(n==="chartTop")return e.chartTop;if(n==="chartBottom")return e.chartBottom;if(typeof n=="object"&&"threshold"in n){let s=e.thresholds.get(n.threshold);if(!s)throw new Error(`fillSpec region references unknown threshold '${n.threshold}'`);return e.valueScale.map(s.value)}return typeof n=="object"&&"value"in n?e.valueScale.map(n.value):null}function be(r){return typeof r=="string"?{color:r}:{color:r.color,hatch:r.hatch}}function Qt(r,t,e){let n=Zt(r.from,"chartBottom",t),s=Zt(r.to,"series",t),{color:i,hatch:o}=be(r.fill),a={type:"path",fill:i,hatch:o,stroke:"none",...t.idPrefix&&{id:`${t.idPrefix}-fill-r${e}`}};if(n!==null&&s!==null){let m=t.timeScale.range(),g=m[0],p=m[1],b=Math.min(n,s),S=Math.max(n,s);return [{...a,points:[{x:g,y:b},{x:p,y:b},{x:p,y:S},{x:g,y:S}]}]}let l=n??s,h=[],d=xe(r,t);if(d===null){let m=t.valueScale.domain();d=(n??s)===t.chartBottom?m[0]:m[1];}let u=Jt(r.outer,t);for(let m of t.runs){if(m.length<2)continue;let g=F.splitByThreshold(m,d,b=>b.value??d,F.interpolateDataPoint),p=r.side==="above"?g.above:r.side==="below"?g.below:[...g.above,...g.below];if(u!==null&&r.side){let b=r.side==="above"?"below":"above";p=p.flatMap(S=>{if(S.length<2)return [];let k=F.splitByThreshold(S,u,w=>w.value??u,F.interpolateDataPoint);return b==="above"?k.above:k.below});}for(let b of p){if(b.length<2)continue;let S=b.map(w=>({x:t.timeScale.map(w.time),y:t.valueScale.map(w.value)})),k=[...S].reverse().map(w=>({x:w.x,y:l}));h.push({...a,smoothing:t.smoothing,smoothCount:S.length,points:[...S,...k]});}}return h}function xe(r,t){let e=r.from==="series"?r.to:r.from;return Jt(e,t)}function Jt(r,t){return r===void 0||r==="series"||r==="chartTop"||r==="chartBottom"?null:typeof r=="object"&&"threshold"in r?t.thresholds.get(r.threshold)?.value??null:typeof r=="object"&&"value"in r?r.value:null}function te(r,t){if(typeof r=="string"||!("regions"in r))return Qt({fill:r},t,0);let e=[];return r.regions.forEach((n,s)=>{e.push(...Qt(n,t,s));}),e}function ee(r,t){let e=new Array(r.length),n=0;for(let s=0;s<r.length;s++){let i=r[s].time,o=i-t;for(;n<s&&r[n].time<o;)n++;let a=0,l=0;for(let h=n;h<=s;h++){let d=r[h].value;d!==null&&(a+=d,l++);}e[s]={time:i,value:l===0?null:a/l,synthetic:true};}return e}function ie(r){let t=r.style?.line,e=t&&!Array.isArray(t)?t:void 0;return {color:e?.color??c.stroke,width:e?.width??c.strokeWidth,dash:e?.style,smoothing:e?.smoothing??false}}function ne(r,t,e,n){let s=r.filter(l=>l.value!==null);if(s.length<2)return [];let i=s.map(l=>({x:t.timeScale.map(l.time),y:t.valueScale.map(l.value)})),o=ie(e);return [{type:"path",id:t.idPrefix?`${t.idPrefix}-overlay-${n}`:`overlay-${n}`,points:i,stroke:o.color,strokeWidth:o.width,smoothing:o.smoothing,dash:o.dash,fill:"none"}]}function re(r,t){switch(r.kind){case "movingAverage":{r.type;let e=ee(t.data,r.window);return ne(e,t,r,"movingAvg")}case "movingMkt":{let e=jt(t.data,r.window,r.activationEnergy);return ne(e,t,r,"movingMkt")}case "limits":{let e=[],n=t.timeScale.range(),s=n[0],i=n[1],o=ie(r),a=o.color,l=o.width,h=o.dash??"dashed",d=t.idPrefix?`${t.idPrefix}-`:"";return r.high!==void 0&&e.push({type:"line",id:`${d}overlay-limit-high`,x1:s,y1:t.valueScale.map(r.high),x2:i,y2:t.valueScale.map(r.high),stroke:a,strokeWidth:l,dash:h}),r.low!==void 0&&e.push({type:"line",id:`${d}overlay-limit-low`,x1:s,y1:t.valueScale.map(r.low),x2:i,y2:t.valueScale.map(r.low),stroke:a,strokeWidth:l,dash:h}),e}case "stdDevBand":{let e=r.multiplier??1,n=ee(t.data,r.window),s=It(t.data,r.window),i=[],o=[];for(let b=0;b<n.length;b++){let S=n[b].value,k=s[b].value;if(S===null||k===null)continue;let w=t.timeScale.map(n[b].time);i.push({x:w,y:t.valueScale.map(S+e*k)}),o.push({x:w,y:t.valueScale.map(S-e*k)});}if(i.length<2)return [];let a=t.idPrefix?`${t.idPrefix}-`:"",l=[],d=(typeof r.style?.fill=="string"||r.style?.fill&&!("regions"in r.style.fill)?r.style.fill:void 0)??"#94a3b833",{color:u,hatch:m}=Se(d);l.push({type:"path",id:`${a}overlay-stdDevBand`,points:[...i,...[...o].reverse()],fill:u,hatch:m,stroke:"none"});let g=r.style?.line,p=g&&!Array.isArray(g)?g:void 0;if(p){let b=n.filter(S=>S.value!==null).map(S=>({x:t.timeScale.map(S.time),y:t.valueScale.map(S.value)}));b.length>=2&&l.push({type:"path",id:`${a}overlay-stdDevBand-mean`,points:b,stroke:p.color??c.stroke,strokeWidth:p.width??1.5,smoothing:p.smoothing,dash:p.style,fill:"none"});}return l}}}function Se(r){return typeof r=="string"?{color:r}:{color:r.color,hatch:r.hatch}}function ke(r){if(!r)return {gaps:[],autoDetect:false,minGapMs:6e4};if(Array.isArray(r))return {gaps:r,autoDetect:false,minGapMs:6e4};let t=r.regions??[],e=r.style;return {gaps:t.map(s=>{let i={...e,...s.style},o=i.fill,a,l;return typeof o=="string"?a=o:o&&(a=o.color,l=o.hatch),{startTime:s.startTime,endTime:s.endTime,label:s.label,fill:a,hatch:l,fillOpacity:i.opacity,labelBaseline:i.label?.baseline,rotate:i.label?.rotate,style:i.display==="filled"||i.display==="bridge_line"?void 0:i.display}}),autoDetect:r.autoDetect??false,minGapMs:r.minGapMs??6e4}}function pt(r){return "showAs"in r&&!!r.showAs}var Z=class{_layout;_renderer;_locale;_legend;_markers;_thresholds;_highlights;_gaps;_gapsAutoDetect;_gapsMinGapMs;_annotations;_annotationBands;_disabledAnnotations=new Set;_annotationSeq=0;_axes;_series;_annotationBandHeight=0;_timeScale;_valueScales=new Map;constructor(t={}){this._layout=new nt({width:t.width??800,height:t.height??400,margin:t.margin??{top:20,right:20,bottom:40,left:60}}).compute(),this._renderer=t.renderer,this._locale=t.locale,this._legend=t.legend,this._markers=t.markers??[],this._thresholds=t.thresholds??[],this._highlights=t.highlights??[];let e=ke(t.gaps);this._gaps=e.gaps,this._gapsAutoDetect=e.autoDetect,this._gapsMinGapMs=e.minGapMs,this._annotations=t.annotations??[],this._annotationBands=t.annotationBands??[],this._axes=t.axes,this._series=[],t.series&&this.setData(t.series);}getWidth(){return this._layout.totalWidth}getHeight(){return this._layout.totalHeight+this._annotationBandTotalHeight()}get series(){return this._series}_annotationBandTotalHeight(){let t=0;for(let e of this._annotationBands){let n=e.height??12,s=e.spacing??0;(e.showAxis??false)&&(t+=26+s),t+=n+s;}return t}setData(t){this._series=t.filter(e=>Array.isArray(e.data));}addAnnotation(t){let e=t.id??`anno-${++this._annotationSeq}`;return this._annotations.push({...t,id:e}),e}removeAnnotation(t){let e=this._annotations.length;return this._annotations=this._annotations.filter(n=>n.id!==t),this._disabledAnnotations.delete(t),this._annotations.length<e}setAnnotations(t){this._annotations=[...t],this._disabledAnnotations.clear();}clearAnnotations(){this._annotations=[],this._disabledAnnotations.clear();}getAnnotations(){return this._annotations}disableAnnotation(t){this._disabledAnnotations.add(t);}enableAnnotation(t){this._disabledAnnotations.delete(t);}_axisIndexOf(t){return t.yAxisIndex??0}_timesOf(t){return t.data.map(e=>e.time)}_valuesOf(t){if(pt(t)){let e=[];for(let n of t.data)n.min!==null&&e.push(n.min),n.max!==null&&e.push(n.max);return e}return t.data.map(e=>e.value).filter(e=>e!==null)}renderCommands(){if(this._series.length===0)return [];let t=this._legendItems(),e=(this._legend?.show??false)&&t.length>0,n=this._legend?.position??"inside-right",s=this._legend?.orientation??"vertical",i=e?qt(t,s):{width:0},o=this._layout;if(e&&(n==="outside-right"||n==="outside-left")){let f=i.width+16,x={...this._layout.margin};n==="outside-right"?x.right+=f:x.left+=f,o=new nt({width:this._layout.totalWidth,height:this._layout.totalHeight,margin:x}).compute();}let{chartX:a,chartY:l,chartWidth:h,chartHeight:d}=o,u=[a,a+h],m=[l,l+d],g=[];for(let f of this._series)f.data.sort((x,_)=>x.time-_.time);let p=new Map,b=1/0,S=-1/0,k=false;for(let f of this._series){let x=this._axisIndexOf(f),_=p.get(x);_?_.push(f):p.set(x,[f]);for(let T of this._timesOf(f))T<b&&(b=T),T>S&&(S=T),k=true;}if(!k)return [];let w=this._axes?.x?.domain,O=w&&w!=="auto"?w:[b,S];this._timeScale=new J({domain:O,range:u,locale:this._locale});let L=f=>({axisColor:f?.color??c.axisColor,tickColor:f?.color??c.tickColor,textColor:c.textColor,textSize:c.textSize,axisWidth:f?.width}),H=new tt({domain:O,xRange:u,y:l+d,locale:this._locale,format:this._axes?.x?.format,maxTicks:this._axes?.x?.ticks?.major,colors:L(this._axes?.x?.axis)});this._valueScales.clear();let W=Array.from(p.keys()).sort((f,x)=>f-x),R,j=0,$=0;for(let f of W){let x=p.get(f),_=1/0,T=-1/0,M=false;for(let Y of x)for(let at of this._valuesOf(Y))at<_&&(_=at),at>T&&(T=at),M=true;if(!M)continue;let B;this._axes?.y&&this._axes.y.length>f?B=this._axes.y[f]:f===0?B=this._axes?.left:f===1&&(B=this._axes?.right);let yt=B?.domain,Q=yt&&yt!=="auto"?yt:[_,T],Ft=new U({domain:Q,range:[l+d,l]});this._valueScales.set(f,Ft);let Wt=this._thresholds.filter(Y=>(Y.axisIndex??0)===f&&Y.label!==false&&Y.value>=Math.min(Q[0],Q[1])&&Y.value<=Math.max(Q[0],Q[1])).map(Y=>Ft.map(Y.value)),bt=B?.position??(f===0?"left":"right"),xt;bt==="right"?(xt=a+h+$*50,$++):(xt=a-j*50,j++);let Rt=new lt({domain:Q,range:[l+d,l],x:xt,position:bt,format:B?.format,ticks:B?.ticks?.major,colors:L(B?.axis),suppressLabelsNear:Wt.length?Wt:void 0});f===0&&(R=Rt),g.push({type:"group",cssClass:`value-axis ${bt}`,commands:Rt.render()});}let y=this._valueScales.get(0)??this._valueScales.get(W[0]);g.push({type:"group",cssClass:"time-axis",commands:H.render()});let v=null,C=this._axes?.x?.grid?.major,A=this._axes?.left?.grid?.major;if(C!==void 0||A!==void 0){let f=C!==false,x=A!==false&&!!R,_=f?H.generateTicks().map(M=>M.x):void 0,T=x?R.generateTicks().map(M=>M.position):void 0;if(_||T){let M=(C&&typeof C=="object"?C:void 0)??(A&&typeof A=="object"?A:void 0);v={type:"group",cssClass:"chart-grid",commands:Kt({xTicks:_,yTicks:T,xRange:u,yRange:m,stroke:M?.color,opacity:M?.opacity,dashed:M?.style==="dashed"})};}}if(this._highlights.length>0&&g.push({type:"group",cssClass:"highlights",commands:Nt({highlights:this._highlights,timeScale:this._timeScale,yRange:m,height:o.totalHeight})}),this._thresholds.length>0&&y){let f=[],x=[];for(let _ of this._thresholds){let T=_.id??it(_.name),M=this._valueScales.get(_.axisIndex??0)??y,B=Vt({thresholds:[_],valueScale:M,xRange:u});B.inside.length&&f.push({type:"group",cssClass:`threshold threshold--${T}`,id:`threshold-${T}`,commands:B.inside}),B.labels.length&&x.push({type:"group",cssClass:`threshold-label threshold-label--${T}`,commands:B.labels});}f.length&&g.push({type:"group",cssClass:"thresholds",commands:f,clipRect:{x:a,y:l,w:h,h:d}}),x.length&&g.push({type:"group",cssClass:"threshold-labels",commands:x});}let D=this._gaps;if(this._gapsAutoDetect){let f=[];for(let x of this._series)pt(x)||f.push(...Ot(x.data,this._gapsMinGapMs));f.length>0&&(D=[...this._gaps,...f]);}D.length>0&&g.push({type:"group",cssClass:"gaps",commands:zt({gaps:D,timeScale:this._timeScale,yRange:m})});let E=new Map(this._thresholds.map(f=>[f.name,f]));for(let f of this._series){if(f.data.length===0)continue;let x=this._valueScales.get(this._axisIndexOf(f));if(!x)continue;let _=f.id??it(f.name);g.push({type:"group",cssClass:`series series--${_}`,id:`series-${_}`,commands:this._renderSeries(f,x,E)});}v&&g.push(v);let V=this._markers.map(f=>({...f}));for(let f of V)if(f.value===void 0&&(f.lineStyle==="to-value"||f.lineStyle==="to-top")){let x=this._series[f.seriesIndex??0];x&&!pt(x)&&x.data.length>=2&&(f.value=this._interpolateValue(f.time,x.data));}V.length>0&&y&&g.push({type:"group",cssClass:"markers",commands:Yt({markers:V,timeScale:this._timeScale,valueScale:y,yRange:m})});let N=this._annotations.filter(f=>!f.id||!this._disabledAnnotations.has(f.id));if(N.length>0&&g.push({type:"group",cssClass:"annotations",commands:Xt({annotations:N,timeScale:this._timeScale,valueScales:this._valueScales})}),this._annotationBands.length>0){let f=l+d+this._layout.margin.bottom,x=0;this._annotationBands.forEach(_=>{let T=_.height??12,M=_.spacing??0,B=f+x;(_.showAxis??false)&&(x+=26+M),x+=T+M,g.push({type:"group",cssClass:"annotation-band",commands:new ut({name:_.name,showAxis:_.showAxis??false,items:_.items,timeScale:this._timeScale,background:_.background,hatch:_.hatch},[a,a+h],B,T).render()});});}if(e&&n!=="separate"){let f,x;n==="inside-right"?(f=a+h-i.width-8,x=l+8):n==="inside-left"?(f=a+8,x=l+8):n==="outside-right"?(f=a+h+16,x=l):(f=8,x=l),g.push(Ut({items:t,x:f,y:x,orientation:s}));}let P=this._axes?.left?.label,I=this._axes?.right?.label,q=this._axes?.x?.label;if(P||I||q){let f=[],x=l+d/2,_=this._axes?.left?.labels,T=this._axes?.right?.labels,M=this._axes?.x?.labels;P&&f.push({type:"text",content:P,x:14,y:x,anchor:"middle",fontSize:_?.fontSize??c.axisLabelSize,fill:_?.color??c.axisLabelColor,rotate:-90}),I&&f.push({type:"text",content:I,x:o.totalWidth-14,y:x,anchor:"middle",fontSize:T?.fontSize??c.axisLabelSize,fill:T?.color??c.axisLabelColor,rotate:90}),q&&f.push({type:"text",content:q,x:a+h/2,y:o.totalHeight-6,anchor:"middle",fontSize:M?.fontSize??c.axisLabelSize,fill:M?.color??c.axisLabelColor}),f.length&&g.push({type:"group",cssClass:"axis-labels",commands:f});}return g}_renderSeries(t,e,n){let s=this._timeScale,i={timeScale:s,valueScale:e};if(pt(t)){let y=t.style?.line&&!Array.isArray(t.style.line)?t.style.line:void 0,v=typeof t.style?.fill=="string"?t.style.fill:void 0;return t.showAs==="minmaxavg"?new dt({data:t.data,timeScale:s,valueScale:e,minColor:t.minColor,maxColor:t.maxColor,avgColor:t.avgColor,avgDashed:t.avgDashed,fillToMax:t.fillToMax,fillToMaxHatch:t.fillToMaxHatch,fillToMin:t.fillToMin,fillToMinHatch:t.fillToMinHatch,smoothing:y?.smoothing,strokeWidth:y?.width,id:t.id}).render():new mt({data:t.data,timeScale:s,valueScale:e,fill:v??y?.color,avgLine:t.avgLine,countOpacity:t.countOpacity,id:t.id}).render()}let o=t,a=t.style?.line&&!Array.isArray(t.style.line)?t.style.line:void 0,l=a?.gapThreshold??c.gapThreshold,h=F.getRuns(t.data,y=>y.value===null,l),d=h.reduce((y,v)=>y+v.length,0);if(d===0)return [];let u=[],m=t.style?.markers,g=t.style?.shadow,p={stroke:a?.color??c.stroke,strokeWidth:a?.width??c.strokeWidth,smoothing:a?.smoothing,dashed:a?.style==="dashed",pointStyle:m?.type,pointSize:m?.size,pointStroke:m?.stroke,pointFill:m?.fill,pointStrokeWidth:m?.strokeWidth,shadowColor:g?.color,shadowBlur:g?.blur,shadowOffsetX:g?.offsetX,shadowOffsetY:g?.offsetY,id:t.id},b=t.style?.line,S=b&&!Array.isArray(b)?b:void 0,k=S?.color,w=S?.width,O=S?.style,L=S?.smoothing,H=t.style?.fill;if(H!==void 0){let y=e.range(),v=Math.min(y[0],y[1]),C=Math.max(y[0],y[1]);u.push(...te(H,{runs:h,timeScale:s,valueScale:e,thresholds:n,chartTop:v,chartBottom:C,smoothing:a?.smoothing,idPrefix:t.id}));}let W=o.colorByThresholds??[],R=W.map(y=>n.get(y)).filter(y=>!!y).map(y=>y.value).sort((y,v)=>y-v),j=(y,v)=>{let C=p.stroke;for(let A of v){let D=n.get(A);D&&y>=D.value&&(C=D.color??C);}return C};for(let y of h){if(y.length<2)continue;let v=F.splitByBoundaries(y,R,C=>C.value,F.interpolateDataPoint);for(let C=0;C<v.length;C++){let A=v[C];if(A.data.length<2)continue;let D=(A.data[0].value+A.data[A.data.length-1].value)/2,E=o.id,V=A.data.map(P=>({x:s.map(P.time),y:e.map(P.value)})),N=E?`${E}-line-${C}`:void 0;b===false||(b&&Array.isArray(b)?b.forEach((P,I)=>{u.push({type:"path",id:N?`${N}-${I}`:void 0,points:V,stroke:P.color??j(D,W),strokeWidth:P.width??p.strokeWidth,smoothing:P.smoothing??p.smoothing,dash:P.style,opacity:P.opacity,fill:"none",shadowColor:p.shadowColor,shadowBlur:p.shadowBlur,shadowOffsetX:p.shadowOffsetX,shadowOffsetY:p.shadowOffsetY});}):u.push({type:"path",id:N,points:V,stroke:k??j(D,W),strokeWidth:w??p.strokeWidth,smoothing:L??p.smoothing,dash:O,fill:"none",shadowColor:p.shadowColor,shadowBlur:p.shadowBlur,shadowOffsetX:p.shadowOffsetX,shadowOffsetY:p.shadowOffsetY}));}p.pointStyle&&p.pointStyle!=="none"&&d<=(m?.threshold??c.pointThreshold)&&u.push(...Et(y,i,p,C=>j(C.value,W)));}let $=t.overlays;if($&&$.length>0){let y=e.range(),v={data:t.data,timeScale:s,valueScale:e,chartTop:Math.min(y[0],y[1]),chartBottom:Math.max(y[0],y[1]),idPrefix:t.id};for(let C of $)u.push(...re(C,v));}if(t.style?.gap&&h.length>1){let y=e.range(),v=Math.min(y[0],y[1]),C=Math.max(y[0],y[1]),A=t.style.gap,D=A.fill,E,V;typeof D=="string"?E=D:D&&(E=D.color,V=D.hatch);let N=A.opacity??.15,P=A.bridge;for(let I=1;I<h.length;I++){let q=h[I-1][h[I-1].length-1],f=h[I][0],x=s.map(q.time),_=s.map(f.time);if(A.display==="bridge_line"){if(q.value===null||f.value===null)continue;let T=e.map(q.value),M=e.map(f.value);u.push({type:"line",x1:x,y1:T,x2:_,y2:M,stroke:P?.color??(typeof t.style?.line=="object"&&!Array.isArray(t.style.line)?t.style.line.color:void 0)??c.stroke,strokeWidth:P?.width??1.5,dash:P?.style??"dotted"});}else E!==void 0?u.push({type:"rect",x,y:v,w:_-x,h:C-v,fill:E,hatch:V,opacity:N,stroke:"none"}):A.display!=="empty"&&u.push({type:"rect",x,y:v,w:_-x,h:C-v,stroke:c.gapStroke,strokeWidth:1,dashed:true,fill:"none"});}}return u}_interpolateValue(t,e){for(let n=1;n<e.length;n++){let s=e[n-1],i=e[n];if(!(s.value===null||i.value===null)&&t>=s.time&&t<=i.time){let o=(t-s.time)/(i.time-s.time);return s.value+o*(i.value-s.value)}}}legendItems(){return this._legendItems()}_legendItems(){return this._series.map(t=>{let e=t.style?.line&&!Array.isArray(t.style.line)?t.style.line:void 0;return {name:t.name,color:e?.color??c.stroke}})}get renderer(){return this._renderer}get layout(){return this._layout}get timeScale(){return this._timeScale}get valueScales(){return this._valueScales}invertTime(t){return this._timeScale?this._timeScale.invert(t):0}invertValue(t,e=0){let n=this._valueScales.get(e);return n?n.invert(t):0}project(t,e,n=0){let s=this._timeScale?this._timeScale.map(t):0,i=this._valueScales.get(n);return {x:s,y:i?i.map(e):0}}};var ft=class{};var z=1e3,X=class extends ft{#t="100%";#e="100%";#n=new Map;#i=new Map;#r=new Map;constructor(t){super(),t?.width!==void 0&&(this.#t=t.width),t?.height!==void 0&&(this.#e=t.height);}render(t){this.#n.clear(),this.#i.clear(),this.#r.clear();let e=t.map(l=>this._toSVG(l)).join(`
|
|
29
|
+
`),n="",s=[];for(let[,l]of this.#n)s.push(l);for(let[,l]of this.#i)s.push(l);for(let[,l]of this.#r)s.push(l);s.length>0&&(n=` <defs>
|
|
30
|
+
${s.join(`
|
|
31
|
+
`)}
|
|
2896
32
|
</defs>
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
${
|
|
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}
|
|
33
|
+
`);let i=typeof this.#t=="number"?`${this.#t}`:this.#t,o=typeof this.#e=="number"?`${this.#e}`:this.#e,a=typeof this.#t=="number"&&typeof this.#e=="number"?` viewBox="0 0 ${this.#t} ${this.#e}"`:"";return {type:"svg",content:`<svg xmlns="http://www.w3.org/2000/svg" width="${i}" height="${o}"${a}>
|
|
34
|
+
${n} ${e}
|
|
35
|
+
</svg>`}}_toSVG(t){switch(t.type){case "path":return this._path(t);case "line":return this._line(t);case "rect":return this._rect(t);case "circle":return this._circle(t);case "text":return this.#h(t);case "gradient":return this._gradient(t);case "gap":return this._gap(t);case "group":return this._group(t)}}#a(t,e,n){if(t.length===0)return "";if(!e||t.length<3)return t.map((a,l)=>`${l===0?"M":"L"}${a.x},${a.y}`).join(" ");let s=n!=null&&n<t.length?Math.max(n,2):t.length,i=a=>Math.round(a*100)/100,o=`M${t[0].x},${t[0].y}`;for(let a=0;a<s-1;a++){let l=t[a-1]??t[a],h=t[a],d=t[a+1],u=t[a+2]&&a+2<s?t[a+2]:d,m=i(h.x+(d.x-l.x)/6),g=i(h.y+(d.y-l.y)/6),p=i(d.x-(u.x-h.x)/6),b=i(d.y-(u.y-h.y)/6);(m=(m*z|0)/z,g=(g*z|0)/z,p=(p*z|0)/z,b=(b*z|0)/z),o+=` C${m},${g} ${p},${b} ${d.x},${d.y}`;}for(let a=s;a<t.length;a++)o+=` L${t[a].x},${t[a].y}`;return o}_hatchPattern(t,e){let n=`hatch-${t}-${this._hatchIndex++}`;if(this.#r.has(n))return n;let s=St(n,t,e??"rgba(200,220,255,0.3)");return this.#r.set(n,s),n}_hatchIndex=0;_path(t){let e=this.#a(t.points,t.smoothing,t.smoothCount),n=[];if(t.hatch){let o=this._hatchPattern(t.hatch,t.fill);n.push(`fill="url(#${o})"`);}else n.push(`fill="${t.fill?this._esc(t.fill):"none"}"`);t.stroke&&n.push(`stroke="${this._esc(t.stroke)}"`),t.strokeWidth&&n.push(`stroke-width="${t.strokeWidth}"`);let s=this.#o(t.dash,t.dashed,t.strokeWidth);s&&(n.push(`stroke-dasharray="${s.strokeDasharray}"`),s.strokeLinecap&&n.push(`stroke-linecap="${s.strokeLinecap}"`)),t.opacity!==void 0&&n.push(`opacity="${t.opacity}"`),t.id&&n.push(`id="${this._esc(t.id)}"`);let i=this.#s(t);return i&&n.push(`filter="url(#${i})"`),`<path d="${e}" ${n.join(" ")} />`}_line(t){let e=[];t.stroke&&e.push(`stroke="${this._esc(t.stroke)}"`),t.strokeWidth&&e.push(`stroke-width="${t.strokeWidth}"`);let n=this.#o(t.dash,t.dashed,t.strokeWidth);n&&(e.push(`stroke-dasharray="${n.strokeDasharray}"`),n.strokeLinecap&&e.push(`stroke-linecap="${n.strokeLinecap}"`)),t.opacity!==void 0&&e.push(`opacity="${t.opacity}"`),t.id&&e.push(`id="${this._esc(t.id)}"`);let s=this.#s(t);return s&&e.push(`filter="url(#${s})"`),`<line x1="${t.x1}" y1="${t.y1}" x2="${t.x2}" y2="${t.y2}" ${e.join(" ")} />`}_rect(t){let e=[];if(t.hatch){let s=this._hatchPattern(t.hatch,t.fill);e.push(`fill="url(#${s})"`);}else t.fill!==void 0&&e.push(`fill="${this._esc(t.fill)}"`);t.stroke&&e.push(`stroke="${this._esc(t.stroke)}"`),t.strokeWidth&&e.push(`stroke-width="${t.strokeWidth}"`),t.opacity!==void 0&&e.push(`opacity="${t.opacity}"`),t.dashed&&e.push('stroke-dasharray="4,4"'),t.id&&e.push(`id="${this._esc(t.id)}"`);let n=this.#s(t);return n&&e.push(`filter="url(#${n})"`),`<rect x="${t.x}" y="${t.y}" width="${t.w}" height="${t.h}" ${e.join(" ")} />`}_circle(t){let e=[];if(t.hatch){let s=this._hatchPattern(t.hatch,t.fill);e.push(`fill="url(#${s})"`);}else t.fill&&e.push(`fill="${this._esc(t.fill)}"`);t.stroke&&e.push(`stroke="${this._esc(t.stroke)}"`),t.strokeWidth&&e.push(`stroke-width="${t.strokeWidth}"`),t.id&&e.push(`id="${this._esc(t.id)}"`);let n=this.#s(t);return n&&e.push(`filter="url(#${n})"`),`<circle cx="${t.cx}" cy="${t.cy}" r="${t.r}" ${e.join(" ")} />`}#o(t,e,n){return t==="dashed"||!t&&e?{strokeDasharray:"4,4"}:t==="dotted"?{strokeDasharray:"2,4"}:!t||t==="solid"?null:Ht(t,n??2)}#s(t){if(!t.shadowColor)return null;let e=t.shadowBlur??0,n=t.shadowOffsetX??0,s=t.shadowOffsetY??0;if(!e&&!n&&!s)return null;let i=`shadow_${e}_${n}_${s}`;if(this.#n.has(i))return i;let o=`<filter id="${i}" x="-50%" y="-50%" width="200%" height="200%">
|
|
36
|
+
<feDropShadow dx="${n}" dy="${s}" stdDeviation="${e/2}" flood-color="${this._esc(t.shadowColor)}" />
|
|
37
|
+
</filter>`;return this.#n.set(i,o),i}#l(t,e,n,s,i){this.#i.has(t)||this.#i.set(t,`<clipPath id="${t}">
|
|
38
|
+
<rect x="${e}" y="${n}" width="${s}" height="${i}" />
|
|
39
|
+
</clipPath>`);}#h(t){let e=[];return t.anchor&&e.push(`text-anchor="${t.anchor}"`),t.fontSize&&e.push(`font-size="${t.fontSize}"`),t.fontFamily&&e.push(`font-family="${this._esc(t.fontFamily)}"`),t.fill&&e.push(`fill="${this._esc(t.fill)}"`),t.rotate&&e.push(`transform="rotate(${t.rotate} ${t.x} ${t.y})"`),t.textBaseline&&e.push(`textBaseline="${t.textBaseline}"`),t.id&&e.push(`id="${this._esc(t.id)}"`),`<text x="${t.x}" y="${t.y}" ${e.join(" ")}>${this._esc(t.content)}</text>`}_gradient(t){let e=`grad-${Math.random().toString(36).slice(2,8)}`,n=t.stops.map(s=>` <stop offset="${s.offset}" stop-color="${this._esc(s.color)}" />`).join(`
|
|
40
|
+
`);return `<linearGradient id="${e}" gradientUnits="userSpaceOnUse">
|
|
41
|
+
${n}
|
|
3096
42
|
</linearGradient>
|
|
3097
|
-
<path d="${
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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
|
-
/*!
|
|
43
|
+
<path d="${t.points.map(s=>`${s.x},${s.y}`).join(" ")}" fill="url(#${e})" />`}_gap(t){if(t.style==="empty")return "";let e=[`stroke="${this._esc("#999")}"`,'stroke-dasharray="4,4"','fill="none"'];return t.style==="label"&&t.label?`<g class="gap">
|
|
44
|
+
<rect x="${t.x1}" y="${t.y1}" width="${t.x2-t.x1}" height="${t.y2-t.y1}" ${e.join(" ")} />
|
|
45
|
+
<text x="${t.x1}" y="${t.y1-4}" fill="#999" font-size="10">${this._esc(t.label)}</text>
|
|
46
|
+
</g>`:`<rect x="${t.x1}" y="${t.y1}" width="${t.x2-t.x1}" height="${t.y2-t.y1}" ${e.join(" ")} />`}_group(t){t.clipRect&&this.#l("clip-plot",t.clipRect.x,t.clipRect.y,t.clipRect.w,t.clipRect.h);let e=t.commands.map(a=>` ${this._toSVG(a).replace(/\n {3}/g,`
|
|
47
|
+
`)}`).join(`
|
|
48
|
+
`),n=t.cssClass?` class="${this._esc(t.cssClass)}"`:"",s=t.id?` id="${this._esc(t.id)}"`:"",i=t.plotClipId||(t.clipRect?"clip-plot":null),o=i?` clip-path="url(#${this._esc(i)})"`:"";return `<g${n}${s}${o}>
|
|
49
|
+
${e}
|
|
50
|
+
</g>`}_esc(t){return t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}};var se="http://www.w3.org/2000/svg";function _e(r){let t=r.style?.line;return t&&!Array.isArray(t)&&t.color?t.color:c.stroke}function oe(r){return r.replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t])}function we(r){if(r.length===0)return "";let t=new Date(r[0].time).toLocaleString(),e=r.map(n=>`<div><span class="mlc-tooltip__name">${oe(n.series.name)}:</span> ${n.value.toFixed(2)}</div>`);return `<div class="mlc-tooltip__time">${oe(t)}</div>${e.join("")}`}function ve(r,t){if(r.length===0)return;let e=0,n=r.length-1;for(;e<n;){let o=e+n>>1;r[o].time<t?e=o+1:n=o;}let s=r[Math.max(0,e-1)],i=r[e];return Math.abs(s.time-t)<=Math.abs(i.time-t)?s:i}function Ce(r){return "value"in r?r.value:r.avg}function ot(r,t,e={}){let n=r.querySelector("svg");if(!n)throw new Error("attachTooltip: target has no <svg> child (did you mount the chart first?)");let s=e.format??we,i=e.snapRadius??1/0,o=e.className??"mlc-tooltip",a=e.showPicks??true,l=e.pickRadius??5,h=e.picksClassName??"mlc-tooltip-picks",d=document.createElement("div");d.className=o,d.style.position="absolute",d.style.pointerEvents="none",d.style.display="none",getComputedStyle(r).position==="static"&&(r.style.position="relative"),r.appendChild(d);let u=null;a&&(u=document.createElementNS(se,"g"),u.setAttribute("class",h),u.setAttribute("pointer-events","none"),n.appendChild(u));let m=()=>{u&&u.replaceChildren();},g=t.series.map((k,w)=>({data:k.data.map(L=>({time:L.time,value:Ce(L)})).filter(L=>L.value!==null).sort((L,H)=>L.time-H.time),series:k,index:w})),p=k=>{let w=n.getBoundingClientRect(),O=t.getWidth?.()??w.width,L=(k.clientX-w.left)*O/Math.max(1,w.width);t.getHeight?.()??w.height;let W=t.invertTime(L);if(!Number.isFinite(W)){d.style.display="none";return}let R=[];for(let $ of g){let y=ve($.data,W);if(!y)continue;let v=t.project(y.time,y.value,$.series.yAxisIndex??0).x,C=t.project(y.time,y.value,$.series.yAxisIndex??0).y;Math.abs(v-L)>i||R.push({seriesIndex:$.index,series:$.series,time:y.time,value:y.value,x:v,y:C});}if(R.length===0){d.style.display="none",m();return}d.innerHTML=s(R);let j=r.getBoundingClientRect();if(d.style.left=`${k.clientX-j.left}px`,d.style.top=`${k.clientY-j.top}px`,d.style.display="",u){u.replaceChildren();for(let $ of R){let y=document.createElementNS(se,"circle");y.setAttribute("cx",String($.x)),y.setAttribute("cy",String($.y)),y.setAttribute("r",String(l)),y.setAttribute("fill","#ffffff"),y.setAttribute("stroke",_e($.series)),y.setAttribute("stroke-width","2"),y.setAttribute("class",`${h}__dot`),u.appendChild(y);}}},b=()=>{d.style.display="none",m();};n.addEventListener("mousemove",p),n.addEventListener("mouseleave",b);let S=false;return ()=>{S||(S=true,n.removeEventListener("mousemove",p),n.removeEventListener("mouseleave",b),d.remove(),u&&u.remove());}}function Te(r,t={}){let e=typeof r=="string"?document.querySelector(r):r;if(!e)throw new Error(`mount: target ${typeof r=="string"?`'${r}'`:""} not found`);let{tooltip:n,...s}=t,i=s.width??e.clientWidth??800,o=s.height??350,a=new Z({...s,width:i,height:o}),{content:l}=new X().render(a.renderCommands());e.innerHTML=l;let h=e.querySelector("svg");return h&&(h.setAttribute("viewBox",`0 0 ${i} ${a.getHeight()}`),h.setAttribute("width","100%"),h.setAttribute("height",String(a.getHeight()))),n?.show&&ot(e,a,n),a}var Ct=class{_opts;_series=[];_thresholds=[];_annotations=[];_highlights=[];_markers=[];_bands=[];_tooltip;constructor(t,e){this._opts={},t!==void 0&&(this._opts.width=t),e!==void 0&&(this._opts.height=e);}setWidth(t){return this._opts.width=t,this}setHeight(t){return this._opts.height=t,this}setSize(t,e){return this._opts.width=t,this._opts.height=e,this}setMargin(t,e,n,s){return typeof t=="object"?this._opts.margin=t:this._opts.margin={top:t,right:e??0,bottom:n??0,left:s??0},this}setLocale(t){return this._opts.locale=t,this}setAxes(t){return this._opts.axes=t,this}setXAxis(t){return this._opts.axes={...this._opts.axes,x:{...this._opts.axes?.x,...t}},this}setLeftAxis(t){return this._opts.axes={...this._opts.axes,left:{...this._opts.axes?.left,...t}},this}setRightAxis(t){return this._opts.axes={...this._opts.axes,right:{...this._opts.axes?.right,...t}},this}setYAxis(t,e){this._opts.axes||(this._opts.axes={}),this._opts.axes.y||(this._opts.axes.y=[]);let n=[...this._opts.axes.y];for(;n.length<=t;)n.push({});return n[t]={...n[t],...e},this._opts.axes.y=n,this}setLegend(t,e,n){return typeof t=="object"?this._opts.legend=t:this._opts.legend={show:t,position:e,orientation:n},this}setGaps(t){return this._opts.gaps=t,this}setTooltip(t){return this._tooltip={show:true,...t},this}addSeries(t){return this._series.push(t),this}addSeriesAll(t){return this._series.push(...t),this}addThreshold(t){return this._thresholds.push(t),this}addThresholds(t){return this._thresholds.push(...t),this}addAnnotation(t){return this._annotations.push(t),this}addAnnotations(t){return this._annotations.push(...t),this}addHighlight(t){return this._highlights.push(t),this}addMarker(t){return this._markers.push(t),this}addAnnotationBand(t){return this._bands.push(t),this}clear(t){return (!t||t==="series")&&(this._series=[]),(!t||t==="thresholds")&&(this._thresholds=[]),(!t||t==="annotations")&&(this._annotations=[]),(!t||t==="highlights")&&(this._highlights=[]),(!t||t==="markers")&&(this._markers=[]),(!t||t==="bands")&&(this._bands=[]),this}toOptions(){return {...this._opts,series:this._series.length?this._series:this._opts.series,thresholds:this._thresholds.length?this._thresholds:this._opts.thresholds,annotations:this._annotations.length?this._annotations:this._opts.annotations,highlights:this._highlights.length?this._highlights:this._opts.highlights,markers:this._markers.length?this._markers:this._opts.markers,annotationBands:this._bands.length?this._bands:this._opts.annotationBands}}build(){return new Z(this.toOptions())}getSvg(){let t=this.build(),{content:e}=new X().render(t.renderCommands());return e}mount(t){let e=typeof t=="string"?document.querySelector(t):t;if(!e)throw new Error(`GraphBuilder.mount: target ${typeof t=="string"?`'${t}'`:""} not found`);this._opts.width||(this._opts.width=e.clientWidth||800);let n=this.build(),{content:s}=new X().render(n.renderCommands());e.innerHTML=s;let i=e.querySelector("svg");if(i){let o=this._opts.width??800;i.setAttribute("viewBox",`0 0 ${o} ${n.getHeight()}`),i.setAttribute("width","100%"),i.setAttribute("height",String(n.getHeight()));}return this._tooltip?.show&&ot(e,n,this._tooltip),n}},Tt=class{_series;constructor(t){this._series={name:t,data:[]};}addPoint(t,e){return this._series.data.push({time:t,value:e}),this}addPoints(t){return this._series.data.push(...t),this}addFloats(t,e){let n=Math.min(t.length,e.length);for(let s=0;s<n;s++)this._series.data.push({time:t[s],value:e[s]});return this}addNullPoint(t){return this._series.data.push({time:t,value:null}),this}setStyle(t){return this._series.style=t,this}setLineStyle(t,e,n){return this._series.style||(this._series.style={}),this._series.style.line={color:t,width:e,style:n},this}setSmoothing(t){return this._series.style||(this._series.style={}),!this._series.style.line||typeof this._series.style.line=="boolean"?this._series.style.line={smoothing:t}:Array.isArray(this._series.style.line)?this._series.style.line.length===0?this._series.style.line=[{smoothing:t}]:this._series.style.line[0].smoothing=t:this._series.style.line.smoothing=t,this}setFill(t){return this._series.style||(this._series.style={}),this._series.style.fill=t,this}setID(t){return this._series.id=t,this}setColorByThresholds(t){return this._series.colorByThresholds=t,this}build(){return this._series}},$t=class{_threshold;constructor(t,e){this._threshold={name:t,value:e};}setColor(t){return this._threshold.color=t,this}setLine(t){return this._threshold.line=t,this}setFill(t,e){return this._threshold.fill=t,this._threshold.fillOpacity=e,this}setFillHatch(t){return this._threshold.fillHatch=t,this}setLabel(t,e){return this._threshold.label={text:t,position:e},this}setID(t){return this._threshold.id=t,this}build(){return this._threshold}},At=class{_marker;constructor(t){this._marker={time:t};}setValue(t){return this._marker.value=t,this}setLabel(t){return this._marker.label=t,this}setColor(t){return this._marker.color=t,this}setPointStyle(t){return this._marker.pointStyle=t,this}setLineStyle(t){return t==="none"?delete this._marker.lineStyle:this._marker.lineStyle=t,this}setSeriesIndex(t){return this._marker.seriesIndex=t,this}build(){return this._marker}},Mt=class{_highlight;constructor(t,e){this._highlight={startTime:t,endTime:e};}setLabel(t){return this._highlight.label=t,this}setColor(t){return this._highlight.color=t,this}setOpacity(t){return this._highlight.opacity=t,this}setLabelPosition(t){return this._highlight.labelPosition=t,this}setRotate(t){return this._highlight.rotate=t,this}build(){return this._highlight}},Lt=class{_ann;constructor(t){this._ann={type:t};}setID(t){return this._ann.id=t,this}setTitle(t){return this._ann.title=t,this}setFrom(t,e,n){return this._ann.from={time:t,value:e,axis:n},this}setTo(t,e,n){return this._ann.to={time:t,value:e,axis:n},this}setAt(t,e,n){return this._ann.at={time:t,value:e,axis:n},this}setColor(t){return this._ann.color=t,this}setWidth(t){return this._ann.width=t,this}setDash(t){return this._ann.dash=t,this}setHeadSize(t){return this._ann.headSize=t,this}setFill(t){return this._ann.fill=t,this}setHatch(t){return this._ann.hatch=t,this}setStroke(t){return this._ann.stroke=t,this}setOpacity(t){return this._ann.opacity=t,this}setRadius(t){return this._ann.radius=t,this}setShape(t){return this._ann.shape=t,this}setText(t){return this._ann.text=t,this}setAnchor(t){return this._ann.anchor=t,this}setDx(t){return this._ann.dx=t,this}setDy(t){return this._ann.dy=t,this}setRotate(t){return this._ann.rotate=t,this}build(){return this._ann}},Dt=class{_band;constructor(t){this._band={name:t,items:[]};}setHeight(t){return this._band.height=t,this}setSpacing(t){return this._band.spacing=t,this}setHatch(t){return this._band.hatch=t,this}setShowAxis(t){return this._band.showAxis=t,this}setShowInLegend(t){return this._band.showInLegend=t,this}setBackground(t){return this._band.background=t,this}addItem(t){return this._band.items.push(t),this}build(){return this._band}},Pt=class{_item;constructor(t,e){this._item={startTime:t,endTime:e};}setFill(t){return this._item.fill=t,this}setHatch(t){return this._item.hatch=t,this}setStroke(t){return this._item.stroke=t,this}setStrokeWidth(t){return this._item.strokeWidth=t,this}setLabel(t,e,n,s){return this._item.label=t,e!==void 0&&(this._item.labelFontSize=e),n!==void 0&&(this._item.labelFill=n),s!==void 0&&(this._item.labelBaseline=s),this}build(){return this._item}};function $e(r){let{thresholds:t,colors:e,hatches:n}=r;if(e.length!==t.length+1)throw new Error(`fillBetweenThresholds: expected ${t.length+1} colors for ${t.length} thresholds, got ${e.length}`);if(n!==void 0&&n.length!==e.length)throw new Error(`fillBetweenThresholds: hatches length (${n.length}) must equal colors length (${e.length})`);return {regions:e.map((i,o)=>{let a=n?.[o],l=a?{color:i,hatch:a}:i;return o===0?{to:{threshold:t[0]},fill:l}:o===e.length-1?{from:{threshold:t[t.length-1]},fill:l}:{from:{threshold:t[o-1]},to:{threshold:t[o]},fill:l}})}}function Bt(r){if(!r||typeof r!="object")throw new Error("DataPoint: object expected");let{time:t,value:e,annotation:n}=r;if(typeof t!="number"||isNaN(t))throw new Error(`DataPoint: invalid time=${t}`);if(e!==null&&(typeof e!="number"||isNaN(e)))throw new Error(`DataPoint: invalid value=${e}`);return {time:t,value:e,annotation:n&&typeof n=="string"?n:void 0}}function Ae(r){if(Array.isArray(r))return r.map(Bt);if(r&&typeof r=="object"){let t=r,e=t.name,n=t.data,s=t.sensorType,i=t.enumMap,o=t.color,a=t.lineWidth,l=t.smoothing,h=t.seriesType;if(!e||!e.length)throw new Error("Series: name required");if(!Array.isArray(n))throw new Error("Series: data must be an array");let d=o!==void 0||a!==void 0||l!==void 0?{color:o,width:a,smoothing:l}:void 0;return {name:e,data:n.map(Bt),sensorType:s??"numeric",enumMap:i,seriesType:h,...d&&{style:{line:d}}}}throw new Error("parseSeries: object or DataPoint[] expected")}function Me(r){if(!r||typeof r!="object")throw new Error("parseAggregated: object expected");let t=r,e=t.name,n=t.data,s=t.showAs,i=t.avgLine,o=t.countOpacity,a=t.color;if(!e||!e.length)throw new Error("AggregatedSeries: name required");if(!Array.isArray(n))throw new Error("AggregatedSeries: data must be an array");return {name:e,data:Le(n),showAs:s,avgLine:i,countOpacity:o,...a!==void 0&&{style:{line:{color:a},fill:a}}}}function Le(r){let t=e=>typeof e=="number"?e:null;return r.map(e=>{let n=e;return {time:n.time??0,min:t(n.min),max:t(n.max),avg:t(n.avg),count:typeof n.count=="number"?n.count:0}})}var gt={...c};function De(r){gt={...gt,...r};}function Pe(){return gt}function Be(){gt={...c};}/*!
|
|
3422
51
|
* MLTimeGraph — Copyright (c) 2026 Michael Lechner
|
|
3423
52
|
* MIT with Attribution: free use incl. commercial requires visible credit to
|
|
3424
53
|
* "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
|
|
3425
54
|
*/
|
|
55
|
+
/*!
|
|
56
|
+
* MLTimeGraph — Copyright (c) 2026 Michael Lechner
|
|
57
|
+
* MIT with Attribution: free use incl. commercial requires visible credit to
|
|
58
|
+
* "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
|
|
59
|
+
*/
|
|
60
|
+
/*!
|
|
61
|
+
* ml-time-analyze — Copyright (c) 2026 Michael Lechner
|
|
62
|
+
* MIT with Attribution: free use incl. commercial requires visible credit to
|
|
63
|
+
* "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
|
|
64
|
+
*/
|
|
3426
65
|
/*!
|
|
3427
66
|
* MLTimeGraph — Copyright (c) 2026 Michael Lechner
|
|
3428
67
|
* MIT with Attribution: free use incl. commercial must have visible credit to
|
|
3429
68
|
* "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 };
|
|
69
|
+
*/export{Dt as AnnotationBandBuilder,Pt as AnnotationBandItemBuilder,Lt as AnnotationBuilder,Ct as GraphBuilder,Mt as HighlightBuilder,Z as MLTimeGraph,At as MarkerBuilder,X as SVGRenderer,$t as ThresholdBuilder,Tt as TimeSeriesBuilder,ot as attachTooltip,$e as fillBetweenThresholds,Pe as getDefaultTheme,Te as mount,Me as parseAggregated,Bt as parseDataPoint,Ae as parseSeries,Be as resetDefaultTheme,De as setDefaultTheme};
|