aliencharts 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js ADDED
@@ -0,0 +1,2533 @@
1
+ // src/react/ChartGridAdapter.jsx
2
+ import React, {
3
+ forwardRef,
4
+ useImperativeHandle,
5
+ useLayoutEffect,
6
+ useRef
7
+ } from "react";
8
+
9
+ // src/icons.js
10
+ var icons = {
11
+ arrowLineRight: '<line x1="32" y1="128" x2="176" y2="128"/><polyline points="104 56 176 128 104 200"/><line x1="216" y1="40" x2="216" y2="216"/>',
12
+ pushPinSimple: '<path d="M224,176a8,8,0,0,1-8,8H136v56a8,8,0,0,1-16,0V184H40a8,8,0,0,1,0-16h9.29L70.46,48H64a8,8,0,0,1,0-16H192a8,8,0,0,1,0,16h-6.46l21.17,120H216A8,8,0,0,1,224,176Z"/>',
13
+ arrowCounterClockwise: '<polyline points="24 56 24 104 72 104"/><path d="M67.59,192A88,88,0,1,0,65.77,65.77L24,104"/>',
14
+ arrowsIn: '<polyline points="192 104 152 104 152 64"/><line x1="208" y1="48" x2="152" y2="104"/><polyline points="64 152 104 152 104 192"/><line x1="48" y1="208" x2="104" y2="152"/><polyline points="152 192 152 152 192 152"/><line x1="208" y1="208" x2="152" y2="152"/><polyline points="104 64 104 104 64 104"/><line x1="48" y1="48" x2="104" y2="104"/>',
15
+ arrowsOut: '<polyline points="160 48 208 48 208 96"/><line x1="152" y1="104" x2="208" y2="48"/><polyline points="96 208 48 208 48 160"/><line x1="104" y1="152" x2="48" y2="208"/><polyline points="208 160 208 208 160 208"/><line x1="152" y1="152" x2="208" y2="208"/><polyline points="48 96 48 48 96 48"/><line x1="104" y1="104" x2="48" y2="48"/>',
16
+ broom: '<path d="M112,224a95.2,95.2,0,0,1-29-48"/><path d="M192,152c0,31.67,13.31,59,40,72H61A103.65,103.65,0,0,1,32,152c0-28.21,11.23-50.89,29.47-69.64a8,8,0,0,1,8.67-1.81L95.52,90.83a16,16,0,0,0,20.82-9l21-53.11c4.15-10,15.47-15.32,25.63-11.53a20,20,0,0,1,11.51,26.4L153.13,96.69a16,16,0,0,0,8.93,20.76L187,127.29a8,8,0,0,1,5,7.43Z"/><line x1="40.54" y1="112.21" x2="194.26" y2="173.7"/>',
17
+ waveSine: '<path d="M24,128c104-221.7,104,221.7,208,0"/>',
18
+ magnifyingGlassPlus: '<line x1="80" y1="112" x2="144" y2="112"/><circle cx="112" cy="112" r="80"/><line x1="168.57" y1="168.57" x2="224" y2="224"/><line x1="112" y1="80" x2="112" y2="144"/>',
19
+ mapPin: '<circle cx="128" cy="104" r="32"/><path d="M208,104c0,72-80,128-80,128S48,176,48,104a80,80,0,0,1,160,0Z"/>',
20
+ minus: '<line x1="40" y1="128" x2="216" y2="128"/>',
21
+ eye: '<path d="M128,56C48,56,16,128,16,128s32,72,112,72,112-72,112-72S208,56,128,56Z"/><circle cx="128" cy="128" r="32"/>'
22
+ };
23
+ var filledIcons = /* @__PURE__ */ new Set(["pushPinSimple"]);
24
+ var iconSvg = (name, { size = 16, className = "" } = {}) => {
25
+ const body = icons[name];
26
+ if (!body) throw new Error(`Unknown AlienCharts icon: ${name}`);
27
+ const paint = filledIcons.has(name) ? 'fill="currentColor"' : 'fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"';
28
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="${size}" height="${size}" class="${className}" aria-hidden="true" ${paint}>${body}</svg>`;
29
+ };
30
+
31
+ // src/core/drawingUtils.js
32
+ var DEFAULT_DRAWING_COLOR = "#60a5fa";
33
+ var DRAWING_HIT_DISTANCE = 8;
34
+ var DRAWING_HANDLE_HIT_DISTANCE = 10;
35
+ var PIN_HIT_DISTANCE = 14;
36
+ var DRAWING_TOOLS = /* @__PURE__ */ new Set(["trendline", "hline", "vline", "pin"]);
37
+ var getDrawingStyle = (drawing) => ({
38
+ color: drawing?.style?.color || DEFAULT_DRAWING_COLOR,
39
+ lineWidth: Number.isFinite(drawing?.style?.lineWidth) ? drawing.style.lineWidth : 2,
40
+ dashPattern: Array.isArray(drawing?.style?.dashPattern) ? drawing.style.dashPattern : [],
41
+ extendLeft: Boolean(drawing?.style?.extendLeft),
42
+ extendRight: drawing?.style?.extendRight !== false,
43
+ text: typeof drawing?.style?.text === "string" ? drawing.style.text : ""
44
+ });
45
+ var getDrawingsForChart = (drawings, chartId) => Array.isArray(drawings) ? drawings.filter((drawing) => drawing?.chartId === chartId) : [];
46
+ var getDefaultDrawingStyle = (type) => ({
47
+ color: DEFAULT_DRAWING_COLOR,
48
+ lineWidth: 2,
49
+ dashPattern: [],
50
+ extendLeft: false,
51
+ extendRight: type !== "vline",
52
+ text: ""
53
+ });
54
+ var createDrawing = ({
55
+ chartId,
56
+ type,
57
+ start,
58
+ end,
59
+ createDrawingId
60
+ }) => {
61
+ const id = createDrawingId?.({ chartId, type }) || `${chartId}-drawing-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
62
+ return {
63
+ id,
64
+ chartId,
65
+ type,
66
+ start,
67
+ end,
68
+ createdAt: Date.now(),
69
+ style: getDefaultDrawingStyle(type)
70
+ };
71
+ };
72
+ var createDraftDrawing = ({ chartId, type, start, end }) => ({
73
+ id: "__draft__",
74
+ chartId,
75
+ type,
76
+ start,
77
+ end,
78
+ createdAt: Date.now(),
79
+ style: getDefaultDrawingStyle(type)
80
+ });
81
+ var extendRayToPlotBoundary = (origin, through, plot) => {
82
+ const dx = through.x - origin.x;
83
+ const dy = through.y - origin.y;
84
+ if (Math.abs(dx) <= 1e-6 && Math.abs(dy) <= 1e-6) return through;
85
+ const candidates = [];
86
+ if (dx > 1e-6) candidates.push((plot.x + plot.width - origin.x) / dx);
87
+ else if (dx < -1e-6) candidates.push((plot.x - origin.x) / dx);
88
+ if (dy > 1e-6) candidates.push((plot.y + plot.height - origin.y) / dy);
89
+ else if (dy < -1e-6) candidates.push((plot.y - origin.y) / dy);
90
+ const boundaryScale = Math.min(
91
+ ...candidates.filter((scale2) => Number.isFinite(scale2) && scale2 >= 0)
92
+ );
93
+ const scale = Number.isFinite(boundaryScale) ? Math.max(1, boundaryScale) : 1;
94
+ return {
95
+ x: origin.x + dx * scale,
96
+ y: origin.y + dy * scale
97
+ };
98
+ };
99
+ var getDrawingGeometry = ({ drawing, layout, projectPoint }) => {
100
+ if (!drawing?.start || !drawing?.end) return null;
101
+ const start = projectPoint(drawing.start, layout);
102
+ const end = projectPoint(drawing.end, layout);
103
+ const style = getDrawingStyle(drawing);
104
+ let lineStart = start;
105
+ let lineEnd = end;
106
+ if (drawing.type === "pin") {
107
+ return { start, end: start, lineStart: start, lineEnd: start, style };
108
+ }
109
+ if (drawing.type === "hline") {
110
+ const lineEndX = Math.abs(end.x - start.x) > 1e-6 ? end.x : layout.plot.x + layout.plot.width;
111
+ lineStart = {
112
+ x: style.extendRight ? layout.plot.x : start.x,
113
+ y: start.y
114
+ };
115
+ lineEnd = {
116
+ x: style.extendRight ? layout.plot.x + layout.plot.width : lineEndX,
117
+ y: start.y
118
+ };
119
+ } else if (drawing.type === "vline") {
120
+ lineStart = {
121
+ x: start.x,
122
+ y: layout.plot.y
123
+ };
124
+ lineEnd = {
125
+ x: start.x,
126
+ y: layout.plot.y + layout.plot.height
127
+ };
128
+ } else if (drawing.type === "trendline") {
129
+ if (style.extendLeft) lineStart = extendRayToPlotBoundary(end, start, layout.plot);
130
+ if (style.extendRight) lineEnd = extendRayToPlotBoundary(start, end, layout.plot);
131
+ }
132
+ return { start, end, lineStart, lineEnd, style };
133
+ };
134
+ var distanceToSegment = (point, start, end) => {
135
+ const dx = end.x - start.x;
136
+ const dy = end.y - start.y;
137
+ if (dx === 0 && dy === 0) {
138
+ return Math.hypot(point.x - start.x, point.y - start.y);
139
+ }
140
+ const t = Math.max(
141
+ 0,
142
+ Math.min(
143
+ 1,
144
+ ((point.x - start.x) * dx + (point.y - start.y) * dy) / (dx * dx + dy * dy)
145
+ )
146
+ );
147
+ return Math.hypot(point.x - (start.x + t * dx), point.y - (start.y + t * dy));
148
+ };
149
+ var hitTestDrawing = ({ point, drawing, layout, projectPoint }) => {
150
+ const geometry = getDrawingGeometry({
151
+ drawing,
152
+ layout,
153
+ projectPoint
154
+ });
155
+ if (!geometry) return null;
156
+ if (drawing.type !== "pin" && Math.hypot(point.x - geometry.start.x, point.y - geometry.start.y) <= DRAWING_HANDLE_HIT_DISTANCE) {
157
+ return { drawing, endpoint: "start" };
158
+ }
159
+ if (drawing.type === "pin" && Math.hypot(point.x - geometry.start.x, point.y - geometry.start.y) <= PIN_HIT_DISTANCE) {
160
+ return { drawing, endpoint: "move" };
161
+ }
162
+ if (drawing.type === "trendline" && Math.hypot(point.x - geometry.end.x, point.y - geometry.end.y) <= DRAWING_HANDLE_HIT_DISTANCE) {
163
+ return { drawing, endpoint: "end" };
164
+ }
165
+ const distance = distanceToSegment(point, geometry.lineStart, geometry.lineEnd);
166
+ return distance <= DRAWING_HIT_DISTANCE ? { drawing, endpoint: null } : null;
167
+ };
168
+ var updateDrawingById = (drawings, drawingId, updater) => (Array.isArray(drawings) ? drawings : []).map(
169
+ (drawing) => drawing?.id === drawingId ? updater(drawing) : drawing
170
+ );
171
+ var removeDrawingById = (drawings, drawingId) => (Array.isArray(drawings) ? drawings : []).filter(
172
+ (drawing) => drawing?.id !== drawingId
173
+ );
174
+
175
+ // src/core/lodSeries.js
176
+ var DEFAULT_MAX_LEVELS = 18;
177
+ var toTypedArray = (value, Type) => {
178
+ if (value instanceof Type) return value;
179
+ if (ArrayBuffer.isView(value)) return new Type(value);
180
+ if (Array.isArray(value)) return new Type(value);
181
+ return new Type(0);
182
+ };
183
+ var lowerBound = (array, value) => {
184
+ let lo = 0;
185
+ let hi = array.length;
186
+ while (lo < hi) {
187
+ const mid = lo + hi >> 1;
188
+ if (array[mid] < value) lo = mid + 1;
189
+ else hi = mid;
190
+ }
191
+ return lo;
192
+ };
193
+ var upperBound = (array, value) => {
194
+ let lo = 0;
195
+ let hi = array.length;
196
+ while (lo < hi) {
197
+ const mid = lo + hi >> 1;
198
+ if (array[mid] <= value) lo = mid + 1;
199
+ else hi = mid;
200
+ }
201
+ return lo;
202
+ };
203
+ var pushUniqueSorted = (indices, index) => {
204
+ if (index < 0) return;
205
+ if (indices.includes(index)) return;
206
+ indices.push(index);
207
+ };
208
+ var buildLod = (rawX, rawY, bucketSize, startIndex = 0, endIndex = rawX.length) => {
209
+ if (bucketSize <= 1 || rawX.length <= bucketSize) {
210
+ return { x: rawX, y: rawY, bucketSize: 1 };
211
+ }
212
+ const alignedStart = Math.max(0, Math.floor(startIndex / bucketSize) * bucketSize);
213
+ const alignedEnd = Math.min(rawX.length, endIndex);
214
+ const estimated = Math.ceil((alignedEnd - alignedStart) / bucketSize) * 4;
215
+ const x = new Float64Array(estimated);
216
+ const y = new Float32Array(estimated);
217
+ let out = 0;
218
+ for (let start = alignedStart; start < alignedEnd; start += bucketSize) {
219
+ const end = Math.min(alignedEnd, start + bucketSize);
220
+ let minIndex = start;
221
+ let maxIndex = start;
222
+ for (let i = start + 1; i < end; i += 1) {
223
+ if (rawY[i] < rawY[minIndex]) minIndex = i;
224
+ if (rawY[i] > rawY[maxIndex]) maxIndex = i;
225
+ }
226
+ const indices = [];
227
+ pushUniqueSorted(indices, start);
228
+ pushUniqueSorted(indices, minIndex);
229
+ pushUniqueSorted(indices, maxIndex);
230
+ pushUniqueSorted(indices, end - 1);
231
+ indices.sort((a, b) => a - b);
232
+ for (let i = 0; i < indices.length; i += 1) {
233
+ const sourceIndex = indices[i];
234
+ x[out] = rawX[sourceIndex];
235
+ y[out] = rawY[sourceIndex];
236
+ out += 1;
237
+ }
238
+ }
239
+ return {
240
+ x: x.subarray(0, out),
241
+ y: y.subarray(0, out),
242
+ bucketSize
243
+ };
244
+ };
245
+ var concatTyped = (Type, left, right) => {
246
+ if (!left?.length) return right;
247
+ if (!right?.length) return left;
248
+ const out = new Type(left.length + right.length);
249
+ out.set(left, 0);
250
+ out.set(right, left.length);
251
+ return out;
252
+ };
253
+ var LineSeries = class {
254
+ constructor({ id, name, color, x, y, maxLevels = DEFAULT_MAX_LEVELS }) {
255
+ this.id = id;
256
+ this.name = name || id;
257
+ this.color = color || "#38bdf8";
258
+ this.maxLevels = maxLevels;
259
+ const initialX = toTypedArray(x, Float64Array);
260
+ const initialY = toTypedArray(y, Float32Array);
261
+ this._length = Math.min(initialX.length, initialY.length);
262
+ const capacity = Math.max(1, this._length);
263
+ this.rawX = new Float64Array(capacity);
264
+ this.rawY = new Float32Array(capacity);
265
+ this.rawX.set(initialX.subarray(0, this._length));
266
+ this.rawY.set(initialY.subarray(0, this._length));
267
+ this.levels = [];
268
+ this.rebuildLevels();
269
+ }
270
+ get length() {
271
+ return this._length;
272
+ }
273
+ append(xValues, yValues) {
274
+ const nextX = toTypedArray(xValues, Float64Array);
275
+ const nextY = toTypedArray(yValues, Float32Array);
276
+ const appendLength = Math.min(nextX.length, nextY.length);
277
+ if (appendLength === 0) return;
278
+ const currentLength = this.length;
279
+ this.ensureRawCapacity(currentLength + appendLength);
280
+ this.rawX.set(nextX.subarray(0, appendLength), currentLength);
281
+ this.rawY.set(nextY.subarray(0, appendLength), currentLength);
282
+ this._length = currentLength + appendLength;
283
+ this.markLevelsDirty(currentLength);
284
+ }
285
+ rebuildLevels() {
286
+ const rawX = this.rawX.subarray(0, this.length);
287
+ const rawY = this.rawY.subarray(0, this.length);
288
+ this.levels = [{ x: rawX, y: rawY, bucketSize: 1 }];
289
+ let bucketSize = 4;
290
+ while (this.levels.length < this.maxLevels && bucketSize < Math.max(8, rawX.length)) {
291
+ this.levels.push(buildLod(rawX, rawY, bucketSize));
292
+ bucketSize *= 4;
293
+ }
294
+ }
295
+ ensureRawCapacity(requiredLength) {
296
+ if (requiredLength <= this.rawX.length && requiredLength <= this.rawY.length) {
297
+ return;
298
+ }
299
+ const nextCapacity = Math.max(requiredLength, Math.ceil(this.rawX.length * 1.5));
300
+ const rawX = new Float64Array(nextCapacity);
301
+ const rawY = new Float32Array(nextCapacity);
302
+ rawX.set(this.rawX.subarray(0, this.length));
303
+ rawY.set(this.rawY.subarray(0, this.length));
304
+ this.rawX = rawX;
305
+ this.rawY = rawY;
306
+ }
307
+ markLevelsDirty(previousLength) {
308
+ const rawX = this.rawX.subarray(0, this.length);
309
+ const rawY = this.rawY.subarray(0, this.length);
310
+ this.levels[0] = { x: rawX, y: rawY, bucketSize: 1 };
311
+ let bucketSize = this.levels.length > 0 ? this.levels[this.levels.length - 1].bucketSize * 4 : 4;
312
+ while (this.levels.length < this.maxLevels && bucketSize < Math.max(8, rawX.length)) {
313
+ this.levels.push(buildLod(rawX, rawY, bucketSize));
314
+ bucketSize *= 4;
315
+ }
316
+ for (let i = 1; i < this.levels.length; i += 1) {
317
+ const level = this.levels[i];
318
+ const dirtyRawStart = Math.floor(previousLength / level.bucketSize) * level.bucketSize;
319
+ level.dirtyFrom = Math.min(level.dirtyFrom ?? dirtyRawStart, dirtyRawStart);
320
+ }
321
+ }
322
+ ensureLevel(index) {
323
+ const level = this.levels[index];
324
+ if (!level || index === 0 || level.dirtyFrom == null) return level;
325
+ const rawX = this.rawX.subarray(0, this.length);
326
+ const rawY = this.rawY.subarray(0, this.length);
327
+ const dirtyRawStart = Math.floor(level.dirtyFrom / level.bucketSize) * level.bucketSize;
328
+ const cutoffX = rawX[dirtyRawStart] ?? rawX[0] ?? 0;
329
+ const keepCount = lowerBound(level.x, cutoffX);
330
+ const tail = buildLod(rawX, rawY, level.bucketSize, dirtyRawStart, rawX.length);
331
+ this.levels[index] = {
332
+ x: concatTyped(Float64Array, level.x.subarray(0, keepCount), tail.x),
333
+ y: concatTyped(Float32Array, level.y.subarray(0, keepCount), tail.y),
334
+ bucketSize: level.bucketSize
335
+ };
336
+ return this.levels[index];
337
+ }
338
+ selectLevel(xMin, xMax, pixelWidth, targetPointsPerPixel = 2.5) {
339
+ const rawX = this.levels[0].x;
340
+ if (rawX.length === 0) return this.levels[0];
341
+ const from = lowerBound(rawX, xMin);
342
+ const to = upperBound(rawX, xMax);
343
+ const visibleRawPoints = Math.max(0, to - from);
344
+ const targetPoints = Math.max(32, pixelWidth * targetPointsPerPixel);
345
+ const desiredBucket = Math.max(1, visibleRawPoints / targetPoints);
346
+ let selectedIndex = 0;
347
+ for (let i = 1; i < this.levels.length; i += 1) {
348
+ if (this.levels[i].bucketSize <= desiredBucket) {
349
+ selectedIndex = i;
350
+ } else {
351
+ break;
352
+ }
353
+ }
354
+ return this.ensureLevel(selectedIndex);
355
+ }
356
+ getVisiblePoints(xMin, xMax, pixelWidth) {
357
+ const level = this.selectLevel(xMin, xMax, pixelWidth);
358
+ const start = Math.max(0, lowerBound(level.x, xMin) - 1);
359
+ const end = Math.min(level.x.length, upperBound(level.x, xMax) + 1);
360
+ return {
361
+ x: level.x.subarray(start, end),
362
+ y: level.y.subarray(start, end),
363
+ bucketSize: level.bucketSize,
364
+ pointCount: Math.max(0, end - start)
365
+ };
366
+ }
367
+ };
368
+ var createSeries = (options) => new LineSeries(options);
369
+
370
+ // src/core/chartRenderer.js
371
+ var CHART_HEIGHT = 360;
372
+ var RIGHT_AXIS_WIDTH = 58;
373
+ var PLOT_PADDING = {
374
+ left: 38,
375
+ right: RIGHT_AXIS_WIDTH,
376
+ top: 34,
377
+ bottom: 28
378
+ };
379
+ var Y_SCALE_MIN = 0.05;
380
+ var Y_SCALE_MAX = 40;
381
+ var Y_AXIS_TICK_COUNT = 5;
382
+ var X_AXIS_TICK_COUNT = 5;
383
+ var X_SCALE_MIN_SPAN = 10;
384
+ var X_SCALE_MAX_SPAN = 1e9;
385
+ var JUMP_LATEST_RIGHT_PADDING_RATIO = 0.1;
386
+ var INITIAL_GPU_VERTEX_CAPACITY = 4096;
387
+ var AA_VERTEX_FLOAT_STRIDE = 6;
388
+ var AA_LINE_WIDTH_PX = 1.5;
389
+ var AA_EDGE_WIDTH_PX = 1;
390
+ var DASH_LENGTH_PX = 7;
391
+ var DASH_GAP_PX = 5;
392
+ var DEFAULT_CHART_BACKGROUND = "#f5f9ff";
393
+ var EMPTY_OBJECT = {};
394
+ var hexToRgb = (color) => {
395
+ const value = String(color || "#38bdf8").replace("#", "");
396
+ const normalized = value.length === 3 ? value.split("").map((char) => char + char).join("") : value.padEnd(6, "0").slice(0, 6);
397
+ const number = Number.parseInt(normalized, 16);
398
+ return [
399
+ (number >> 16 & 255) / 255,
400
+ (number >> 8 & 255) / 255,
401
+ (number & 255) / 255
402
+ ];
403
+ };
404
+ var getReadableTextColor = (backgroundColor) => {
405
+ const [r, g, b] = hexToRgb(backgroundColor);
406
+ const toLinear = (value) => value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
407
+ const luminance = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
408
+ return luminance > 0.45 ? "#111827" : "#ffffff";
409
+ };
410
+ var getSeriesLabelTextColor = (series) => {
411
+ if (series.__labelTextColorFor !== series.color) {
412
+ series.__labelTextColorFor = series.color;
413
+ series.__labelTextColor = getReadableTextColor(series.color);
414
+ }
415
+ return series.__labelTextColor;
416
+ };
417
+ var createProgram = (gl) => {
418
+ const vertexSource = `#version 300 es
419
+ in vec2 a_xy;
420
+ uniform vec2 u_resolution;
421
+ uniform vec4 u_rect;
422
+ uniform vec2 u_xRange;
423
+ uniform vec2 u_yRange;
424
+
425
+ void main() {
426
+ float xDenom = max(0.000000000001, abs(u_xRange.y - u_xRange.x));
427
+ float yDenom = max(0.000000000001, abs(u_yRange.y - u_yRange.x));
428
+ float nx = (a_xy.x - u_xRange.x) / xDenom;
429
+ float ny = (a_xy.y - u_yRange.x) / yDenom;
430
+ float px = u_rect.x + nx * u_rect.z;
431
+ float py = u_rect.y + (1.0 - ny) * u_rect.w;
432
+ vec2 zeroToOne = vec2(px, py) / u_resolution;
433
+ vec2 clip = zeroToOne * 2.0 - 1.0;
434
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
435
+ }
436
+ `;
437
+ const fragmentSource = `#version 300 es
438
+ precision highp float;
439
+ uniform vec3 u_color;
440
+ out vec4 outColor;
441
+
442
+ void main() {
443
+ outColor = vec4(u_color, 1.0);
444
+ }
445
+ `;
446
+ const compile = (type, source) => {
447
+ const shader = gl.createShader(type);
448
+ gl.shaderSource(shader, source);
449
+ gl.compileShader(shader);
450
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
451
+ throw new Error(gl.getShaderInfoLog(shader) || "Shader compile failed");
452
+ }
453
+ return shader;
454
+ };
455
+ const program = gl.createProgram();
456
+ gl.attachShader(program, compile(gl.VERTEX_SHADER, vertexSource));
457
+ gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fragmentSource));
458
+ gl.linkProgram(program);
459
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
460
+ throw new Error(gl.getProgramInfoLog(program) || "Program link failed");
461
+ }
462
+ return program;
463
+ };
464
+ var createAntialiasProgram = (gl) => {
465
+ const vertexSource = `#version 300 es
466
+ in vec2 a_start;
467
+ in vec2 a_end;
468
+ in float a_side;
469
+ in float a_along;
470
+ uniform vec2 u_resolution;
471
+ uniform vec4 u_rect;
472
+ uniform vec2 u_xRange;
473
+ uniform vec2 u_yRange;
474
+ uniform float u_lineHalfWidth;
475
+ uniform float u_edgeWidth;
476
+ out float v_side;
477
+
478
+ vec2 toPixel(vec2 value) {
479
+ float xDenom = max(0.000000000001, abs(u_xRange.y - u_xRange.x));
480
+ float yDenom = max(0.000000000001, abs(u_yRange.y - u_yRange.x));
481
+ float nx = (value.x - u_xRange.x) / xDenom;
482
+ float ny = (value.y - u_yRange.x) / yDenom;
483
+ return vec2(
484
+ u_rect.x + nx * u_rect.z,
485
+ u_rect.y + (1.0 - ny) * u_rect.w
486
+ );
487
+ }
488
+
489
+ void main() {
490
+ vec2 startPx = toPixel(a_start);
491
+ vec2 endPx = toPixel(a_end);
492
+ vec2 delta = endPx - startPx;
493
+ float segmentLength = max(0.000001, length(delta));
494
+ vec2 direction = delta / segmentLength;
495
+ vec2 normal = vec2(-direction.y, direction.x);
496
+ float expand = u_lineHalfWidth + u_edgeWidth;
497
+ float cap = min(expand, segmentLength * 0.5);
498
+ vec2 px = mix(startPx, endPx, a_along)
499
+ + direction * ((a_along * 2.0 - 1.0) * cap)
500
+ + normal * a_side * expand;
501
+ vec2 zeroToOne = px / u_resolution;
502
+ vec2 clip = zeroToOne * 2.0 - 1.0;
503
+ v_side = a_side;
504
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
505
+ }
506
+ `;
507
+ const fragmentSource = `#version 300 es
508
+ precision highp float;
509
+ uniform vec3 u_color;
510
+ uniform float u_lineHalfWidth;
511
+ uniform float u_edgeWidth;
512
+ in float v_side;
513
+ out vec4 outColor;
514
+
515
+ void main() {
516
+ float expand = u_lineHalfWidth + u_edgeWidth;
517
+ float distanceFromCenter = abs(v_side) * expand;
518
+ float alpha = 1.0 - smoothstep(u_lineHalfWidth, expand, distanceFromCenter);
519
+ outColor = vec4(u_color, alpha);
520
+ }
521
+ `;
522
+ const compile = (type, source) => {
523
+ const shader = gl.createShader(type);
524
+ gl.shaderSource(shader, source);
525
+ gl.compileShader(shader);
526
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
527
+ throw new Error(gl.getShaderInfoLog(shader) || "Shader compile failed");
528
+ }
529
+ return shader;
530
+ };
531
+ const program = gl.createProgram();
532
+ gl.attachShader(program, compile(gl.VERTEX_SHADER, vertexSource));
533
+ gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fragmentSource));
534
+ gl.linkProgram(program);
535
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
536
+ throw new Error(gl.getProgramInfoLog(program) || "Program link failed");
537
+ }
538
+ return program;
539
+ };
540
+ var createSeriesBufferCache = () => /* @__PURE__ */ new Map();
541
+ var getNextVertexCapacity = (currentCapacity, requiredCapacity) => {
542
+ let nextCapacity = Math.max(
543
+ INITIAL_GPU_VERTEX_CAPACITY,
544
+ currentCapacity || 0
545
+ );
546
+ while (nextCapacity < requiredCapacity) {
547
+ nextCapacity *= 2;
548
+ }
549
+ return nextCapacity;
550
+ };
551
+ var getSeriesBuffer = (gl, cache, seriesId, pointCount) => {
552
+ const requiredVertexFloats = pointCount * 2;
553
+ let entry = cache.get(seriesId);
554
+ if (!entry) {
555
+ entry = {
556
+ buffer: gl.createBuffer(),
557
+ gpuCapacity: 0,
558
+ vertices: new Float32Array(
559
+ getNextVertexCapacity(0, requiredVertexFloats)
560
+ )
561
+ };
562
+ cache.set(seriesId, entry);
563
+ }
564
+ if (entry.vertices.length < requiredVertexFloats) {
565
+ entry.vertices = new Float32Array(
566
+ getNextVertexCapacity(entry.vertices.length, requiredVertexFloats)
567
+ );
568
+ }
569
+ if (entry.gpuCapacity < entry.vertices.length) {
570
+ entry.gpuCapacity = entry.vertices.length;
571
+ gl.bindBuffer(gl.ARRAY_BUFFER, entry.buffer);
572
+ gl.bufferData(
573
+ gl.ARRAY_BUFFER,
574
+ entry.gpuCapacity * Float32Array.BYTES_PER_ELEMENT,
575
+ gl.DYNAMIC_DRAW
576
+ );
577
+ }
578
+ return entry;
579
+ };
580
+ var getAntialiasSeriesBuffer = (gl, cache, seriesId, pointCount) => {
581
+ const segmentCount = Math.max(0, pointCount - 1);
582
+ const requiredVertexFloats = segmentCount * 6 * AA_VERTEX_FLOAT_STRIDE;
583
+ let entry = cache.get(seriesId);
584
+ if (!entry) {
585
+ entry = {
586
+ buffer: gl.createBuffer(),
587
+ gpuCapacity: 0,
588
+ vertices: new Float32Array(
589
+ getNextVertexCapacity(0, requiredVertexFloats)
590
+ )
591
+ };
592
+ cache.set(seriesId, entry);
593
+ }
594
+ if (entry.vertices.length < requiredVertexFloats) {
595
+ entry.vertices = new Float32Array(
596
+ getNextVertexCapacity(entry.vertices.length, requiredVertexFloats)
597
+ );
598
+ }
599
+ if (entry.gpuCapacity < entry.vertices.length) {
600
+ entry.gpuCapacity = entry.vertices.length;
601
+ gl.bindBuffer(gl.ARRAY_BUFFER, entry.buffer);
602
+ gl.bufferData(
603
+ gl.ARRAY_BUFFER,
604
+ entry.gpuCapacity * Float32Array.BYTES_PER_ELEMENT,
605
+ gl.DYNAMIC_DRAW
606
+ );
607
+ }
608
+ return entry;
609
+ };
610
+ var deleteSeriesBuffers = (gl, cache) => {
611
+ cache.forEach((entry) => {
612
+ if (entry.buffer) gl.deleteBuffer(entry.buffer);
613
+ });
614
+ cache.clear();
615
+ };
616
+ var fillSeriesPoints = ({ vertices, visiblePoints, pointCount, animatedPoint }) => {
617
+ for (let i = 0; i < pointCount; i += 1) {
618
+ if (animatedPoint && i === animatedPoint.index) {
619
+ vertices[i * 2] = animatedPoint.x;
620
+ vertices[i * 2 + 1] = animatedPoint.y;
621
+ continue;
622
+ }
623
+ vertices[i * 2] = visiblePoints.x[i];
624
+ vertices[i * 2 + 1] = visiblePoints.y[i];
625
+ }
626
+ };
627
+ var projectDataToPixel = ({ x, y, state, yRange, plot }) => ({
628
+ x: plot.x + (x - state.xMin) / (state.xMax - state.xMin) * plot.width,
629
+ y: plot.y + (yRange.maxY - y) / (yRange.maxY - yRange.minY) * plot.height
630
+ });
631
+ var writeLineVertex = (vertices, offset, x, y) => {
632
+ vertices[offset] = x;
633
+ vertices[offset + 1] = y;
634
+ };
635
+ var fillDashedSeriesSegments = ({
636
+ vertices,
637
+ visiblePoints,
638
+ pointCount,
639
+ state,
640
+ yRange,
641
+ plot
642
+ }) => {
643
+ let offset = 0;
644
+ let distance = 0;
645
+ const dashTotal = DASH_LENGTH_PX + DASH_GAP_PX;
646
+ for (let i = 0; i < pointCount - 1; i += 1) {
647
+ const start = {
648
+ x: visiblePoints.x[i],
649
+ y: visiblePoints.y[i]
650
+ };
651
+ const end = {
652
+ x: visiblePoints.x[i + 1],
653
+ y: visiblePoints.y[i + 1]
654
+ };
655
+ const startPx = projectDataToPixel({ ...start, state, yRange, plot });
656
+ const endPx = projectDataToPixel({ ...end, state, yRange, plot });
657
+ const pixelLength = Math.hypot(endPx.x - startPx.x, endPx.y - startPx.y);
658
+ if (!Number.isFinite(pixelLength) || pixelLength <= 0) continue;
659
+ let consumed = 0;
660
+ while (consumed < pixelLength) {
661
+ const phase = distance % dashTotal;
662
+ const boundary = phase < DASH_LENGTH_PX ? DASH_LENGTH_PX - phase : dashTotal - phase;
663
+ const step = Math.min(boundary, pixelLength - consumed);
664
+ if (phase < DASH_LENGTH_PX) {
665
+ if (offset + 4 > vertices.length) {
666
+ return offset / 2;
667
+ }
668
+ const t0 = consumed / pixelLength;
669
+ const t1 = (consumed + step) / pixelLength;
670
+ writeLineVertex(
671
+ vertices,
672
+ offset,
673
+ start.x + (end.x - start.x) * t0,
674
+ start.y + (end.y - start.y) * t0
675
+ );
676
+ offset += 2;
677
+ writeLineVertex(
678
+ vertices,
679
+ offset,
680
+ start.x + (end.x - start.x) * t1,
681
+ start.y + (end.y - start.y) * t1
682
+ );
683
+ offset += 2;
684
+ }
685
+ consumed += step;
686
+ distance += step;
687
+ }
688
+ }
689
+ return offset / 2;
690
+ };
691
+ var getSeriesPoint = (visiblePoints, animatedPoint, index) => {
692
+ if (animatedPoint && index === animatedPoint.index) {
693
+ return { x: animatedPoint.x, y: animatedPoint.y };
694
+ }
695
+ return { x: visiblePoints.x[index], y: visiblePoints.y[index] };
696
+ };
697
+ var getSinglePointSegment = ({ series, state, yRange, plot }) => {
698
+ if (series.length !== 1) return null;
699
+ const x = series.rawX[0];
700
+ const y = series.rawY[0];
701
+ if (x < state.xMin || x > state.xMax || y < yRange.minY || y > yRange.maxY) {
702
+ return null;
703
+ }
704
+ const halfWidth = (state.xMax - state.xMin) / Math.max(1, plot.width) * 3;
705
+ return {
706
+ x: new Float64Array([x - halfWidth, x + halfWidth]),
707
+ y: new Float32Array([y, y]),
708
+ bucketSize: 1,
709
+ pointCount: 2,
710
+ endpoint: { x, y }
711
+ };
712
+ };
713
+ var writeAntialiasVertex = (vertices, offset, start, end, side, along) => {
714
+ vertices[offset] = start.x;
715
+ vertices[offset + 1] = start.y;
716
+ vertices[offset + 2] = end.x;
717
+ vertices[offset + 3] = end.y;
718
+ vertices[offset + 4] = side;
719
+ vertices[offset + 5] = along;
720
+ };
721
+ var fillAntialiasSeriesSegments = ({
722
+ vertices,
723
+ visiblePoints,
724
+ pointCount,
725
+ animatedPoint
726
+ }) => {
727
+ let offset = 0;
728
+ for (let i = 0; i < pointCount - 1; i += 1) {
729
+ const start = getSeriesPoint(visiblePoints, animatedPoint, i);
730
+ const end = getSeriesPoint(visiblePoints, animatedPoint, i + 1);
731
+ writeAntialiasVertex(vertices, offset, start, end, -1, 0);
732
+ offset += AA_VERTEX_FLOAT_STRIDE;
733
+ writeAntialiasVertex(vertices, offset, start, end, 1, 0);
734
+ offset += AA_VERTEX_FLOAT_STRIDE;
735
+ writeAntialiasVertex(vertices, offset, start, end, -1, 1);
736
+ offset += AA_VERTEX_FLOAT_STRIDE;
737
+ writeAntialiasVertex(vertices, offset, start, end, -1, 1);
738
+ offset += AA_VERTEX_FLOAT_STRIDE;
739
+ writeAntialiasVertex(vertices, offset, start, end, 1, 0);
740
+ offset += AA_VERTEX_FLOAT_STRIDE;
741
+ writeAntialiasVertex(vertices, offset, start, end, 1, 1);
742
+ offset += AA_VERTEX_FLOAT_STRIDE;
743
+ }
744
+ return offset / AA_VERTEX_FLOAT_STRIDE;
745
+ };
746
+ var lowerBound2 = (array, value) => {
747
+ let lo = 0;
748
+ let hi = array.length;
749
+ while (lo < hi) {
750
+ const mid = lo + hi >> 1;
751
+ if (array[mid] < value) lo = mid + 1;
752
+ else hi = mid;
753
+ }
754
+ return lo;
755
+ };
756
+ var getNearestPointIndex = (xValues, xValue) => {
757
+ if (!xValues.length) return -1;
758
+ const nextIndex = lowerBound2(xValues, xValue);
759
+ if (nextIndex <= 0) return 0;
760
+ if (nextIndex >= xValues.length) return xValues.length - 1;
761
+ const previousIndex = nextIndex - 1;
762
+ return Math.abs(xValues[nextIndex] - xValue) < Math.abs(xValue - xValues[previousIndex]) ? nextIndex : previousIndex;
763
+ };
764
+ var getChartXBounds = (chart) => {
765
+ let minX = Infinity;
766
+ let maxX = -Infinity;
767
+ chart.series.forEach((series) => {
768
+ if (series.length === 0) return;
769
+ minX = Math.min(minX, series.rawX[0]);
770
+ maxX = Math.max(maxX, series.rawX[series.length - 1]);
771
+ });
772
+ return { minX, maxX };
773
+ };
774
+ var getOrderedSeries = (chart, seriesOrderByChart) => {
775
+ const series = Array.isArray(chart?.series) ? chart.series : [];
776
+ const order = seriesOrderByChart?.[chart?.id];
777
+ if (!Array.isArray(order) || order.length !== series.length) {
778
+ return series;
779
+ }
780
+ const used = /* @__PURE__ */ new Set();
781
+ const ordered = [];
782
+ order.forEach((seriesIndex) => {
783
+ if (!Number.isInteger(seriesIndex)) return;
784
+ if (seriesIndex < 0 || seriesIndex >= series.length) return;
785
+ if (used.has(seriesIndex)) return;
786
+ used.add(seriesIndex);
787
+ ordered.push(series[seriesIndex]);
788
+ });
789
+ if (ordered.length !== series.length) return series;
790
+ return ordered;
791
+ };
792
+ var normalizeMovingAverage = (movingAverage) => {
793
+ if (!movingAverage?.enabled) return null;
794
+ return {
795
+ enabled: true,
796
+ period: Number.isFinite(Number(movingAverage.period)) ? Math.max(1, Math.round(Number(movingAverage.period))) : 21,
797
+ type: movingAverage.type === "sma" ? "sma" : "ema"
798
+ };
799
+ };
800
+ var getMovingAverageCacheKey = (series, movingAverage) => `${series.id}::ma:${movingAverage.type}:${movingAverage.period}`;
801
+ var calculateMovingAverageChunk = ({
802
+ sourceY,
803
+ startIndex,
804
+ period,
805
+ type,
806
+ previousAverage
807
+ }) => {
808
+ const out = new Float32Array(Math.max(0, sourceY.length - startIndex));
809
+ if (out.length === 0) return out;
810
+ if (type === "sma") {
811
+ let sum = 0;
812
+ const firstWindowStart = Math.max(0, startIndex - period + 1);
813
+ for (let i = firstWindowStart; i < startIndex; i += 1) {
814
+ sum += sourceY[i];
815
+ }
816
+ for (let sourceIndex = startIndex; sourceIndex < sourceY.length; sourceIndex += 1) {
817
+ sum += sourceY[sourceIndex];
818
+ const removeIndex = sourceIndex - period;
819
+ if (removeIndex >= firstWindowStart) {
820
+ sum -= sourceY[removeIndex];
821
+ }
822
+ const safePeriod = Math.min(period, sourceIndex + 1);
823
+ out[sourceIndex - startIndex] = sum / safePeriod;
824
+ }
825
+ return out;
826
+ }
827
+ const multiplier = 2 / (period + 1);
828
+ let prev = Number.isFinite(previousAverage) ? previousAverage : sourceY[startIndex];
829
+ for (let sourceIndex = startIndex; sourceIndex < sourceY.length; sourceIndex += 1) {
830
+ prev = sourceY[sourceIndex] * multiplier + prev * (1 - multiplier);
831
+ out[sourceIndex - startIndex] = prev;
832
+ }
833
+ return out;
834
+ };
835
+ var getMovingAverageSeriesForChart = ({
836
+ chart,
837
+ movingAverage,
838
+ cache
839
+ }) => {
840
+ const normalized = normalizeMovingAverage(movingAverage);
841
+ if (!normalized || !cache) return [];
842
+ return chart.series.map((series) => {
843
+ if (series.length === 0) return null;
844
+ const cacheKey = getMovingAverageCacheKey(series, normalized);
845
+ const cached = cache.get(cacheKey);
846
+ if (cached && cached.sourceSeries === series && cached.sourceLength <= series.length) {
847
+ const appendStart = cached.sourceLength;
848
+ if (appendStart < series.length) {
849
+ const appendedY = calculateMovingAverageChunk({
850
+ sourceY: series.rawY.subarray(0, series.length),
851
+ startIndex: appendStart,
852
+ period: normalized.period,
853
+ type: normalized.type,
854
+ previousAverage: appendStart > 0 ? cached.series.rawY[appendStart - 1] : void 0
855
+ });
856
+ cached.series.append(
857
+ series.rawX.subarray(appendStart, series.length),
858
+ appendedY
859
+ );
860
+ cached.sourceLength = series.length;
861
+ }
862
+ cached.series.color = series.color;
863
+ cached.series.name = `${series.name} ${normalized.type.toUpperCase()} ${normalized.period}`;
864
+ return cached.series;
865
+ }
866
+ const y = calculateMovingAverageChunk({
867
+ sourceY: series.rawY.subarray(0, series.length),
868
+ startIndex: 0,
869
+ period: normalized.period,
870
+ type: normalized.type
871
+ });
872
+ const maSeries = createSeries({
873
+ id: cacheKey,
874
+ name: `${series.name} ${normalized.type.toUpperCase()} ${normalized.period}`,
875
+ color: series.color,
876
+ x: series.rawX.subarray(0, series.length),
877
+ y
878
+ });
879
+ cache.set(cacheKey, {
880
+ sourceSeries: series,
881
+ sourceLength: series.length,
882
+ series: maSeries
883
+ });
884
+ return maSeries;
885
+ }).filter(Boolean);
886
+ };
887
+ var getInitialView = (chart, initialVisiblePoints = null) => {
888
+ const { minX, maxX } = getChartXBounds(chart);
889
+ if (!Number.isFinite(minX) || !Number.isFinite(maxX)) {
890
+ return { xMin: 0, xMax: 1 };
891
+ }
892
+ if (minX === maxX) {
893
+ const span2 = Number.isFinite(initialVisiblePoints) && initialVisiblePoints > 0 ? Math.max(10, initialVisiblePoints) : 10;
894
+ const rightPadding2 = span2 * JUMP_LATEST_RIGHT_PADDING_RATIO;
895
+ const nextMax = maxX + rightPadding2;
896
+ return { xMin: nextMax - span2, xMax: nextMax };
897
+ }
898
+ let span = maxX - minX;
899
+ if (Number.isFinite(initialVisiblePoints) && initialVisiblePoints > 0) {
900
+ span = Math.min(span, initialVisiblePoints);
901
+ const rightPadding2 = span * JUMP_LATEST_RIGHT_PADDING_RATIO;
902
+ const nextMax = maxX + rightPadding2;
903
+ return { xMin: nextMax - span, xMax: nextMax };
904
+ }
905
+ const rightPadding = span * JUMP_LATEST_RIGHT_PADDING_RATIO;
906
+ return { xMin: minX, xMax: maxX + rightPadding };
907
+ };
908
+ var getYRange = (chart, xMin, xMax, width) => {
909
+ let minY = Infinity;
910
+ let maxY = -Infinity;
911
+ let renderedPoints = 0;
912
+ let bucketSize = 1;
913
+ chart.series.forEach((series) => {
914
+ const visible = series.getVisiblePoints(xMin, xMax, width);
915
+ renderedPoints += visible.pointCount;
916
+ bucketSize = Math.max(bucketSize, visible.bucketSize);
917
+ for (let i = 0; i < visible.y.length; i += 1) {
918
+ const value = visible.y[i];
919
+ if (value < minY) minY = value;
920
+ if (value > maxY) maxY = value;
921
+ }
922
+ });
923
+ if (!Number.isFinite(minY) || !Number.isFinite(maxY)) {
924
+ return { minY: 0, maxY: 1, renderedPoints, bucketSize };
925
+ }
926
+ const fixedMinY = Number(chart.yRange?.min);
927
+ const fixedMaxY = Number(chart.yRange?.max);
928
+ if (Number.isFinite(fixedMinY) && Number.isFinite(fixedMaxY) && fixedMinY < fixedMaxY) {
929
+ return { minY: fixedMinY, maxY: fixedMaxY, renderedPoints, bucketSize };
930
+ }
931
+ if (minY === maxY) {
932
+ minY -= 1;
933
+ maxY += 1;
934
+ }
935
+ const pad = (maxY - minY) * 0.08;
936
+ return { minY: minY - pad, maxY: maxY + pad, renderedPoints, bucketSize };
937
+ };
938
+ var applyYScale = (yRange, scale = 1, centerOffset = 0) => {
939
+ const clampedScale = Math.min(Y_SCALE_MAX, Math.max(Y_SCALE_MIN, scale));
940
+ const center = (yRange.minY + yRange.maxY) / 2 + centerOffset;
941
+ const halfRange = (yRange.maxY - yRange.minY) * clampedScale / 2;
942
+ return {
943
+ ...yRange,
944
+ minY: center - halfRange,
945
+ maxY: center + halfRange
946
+ };
947
+ };
948
+ var trimFormattedNumber = (value) => value.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "");
949
+ var formatNumber = (value) => {
950
+ if (!Number.isFinite(value)) return "";
951
+ const abs = Math.abs(value);
952
+ if (abs !== 0 && abs < 1e-4) return value.toExponential(2);
953
+ if (abs < 1) return trimFormattedNumber(value.toPrecision(5));
954
+ if (abs >= 1e5) return value.toFixed(0);
955
+ if (abs >= 1e3) return trimFormattedNumber(value.toFixed(1));
956
+ if (abs >= 10) return trimFormattedNumber(value.toFixed(2));
957
+ return trimFormattedNumber(value.toFixed(4));
958
+ };
959
+ var formatCompactNumber = (value) => {
960
+ if (!Number.isFinite(value)) return "";
961
+ const abs = Math.abs(value);
962
+ if (abs >= 1e9) return `${(value / 1e9).toFixed(abs >= 1e10 ? 0 : 1)}B`;
963
+ if (abs >= 1e6) return `${(value / 1e6).toFixed(abs >= 1e7 ? 0 : 1)}M`;
964
+ if (abs >= 1e3) return `${(value / 1e3).toFixed(abs >= 1e4 ? 0 : 1)}k`;
965
+ return formatNumber(value);
966
+ };
967
+ var normalizeRect = (start, end) => {
968
+ if (!start || !end) return null;
969
+ const left = Math.min(start.x, end.x);
970
+ const top = Math.min(start.y, end.y);
971
+ return {
972
+ left,
973
+ top,
974
+ width: Math.abs(end.x - start.x),
975
+ height: Math.abs(end.y - start.y)
976
+ };
977
+ };
978
+ var clampPointToPlot = (point, plot) => ({
979
+ x: Math.min(plot.x + plot.width, Math.max(plot.x, point.x)),
980
+ y: Math.min(plot.y + plot.height, Math.max(plot.y, point.y))
981
+ });
982
+ var applyRectangleZoom = ({
983
+ chart,
984
+ plot,
985
+ start,
986
+ end,
987
+ initialVisiblePoints,
988
+ viewStateRef,
989
+ yScaleRef,
990
+ yCenterOffsetRef,
991
+ yManualScaleRef
992
+ }) => {
993
+ const clampedStart = clampPointToPlot(start, plot);
994
+ const clampedEnd = clampPointToPlot(end, plot);
995
+ const rect = normalizeRect(clampedStart, clampedEnd);
996
+ if (!rect || rect.width < 6 || rect.height < 6) return false;
997
+ const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
998
+ const currentYRange = applyYScale(
999
+ getYRange(chart, state.xMin, state.xMax, plot.width),
1000
+ yScaleRef.current.get(chart.id) ?? 1,
1001
+ yCenterOffsetRef.current.get(chart.id) ?? 0
1002
+ );
1003
+ const leftRatio = (rect.left - plot.x) / plot.width;
1004
+ const rightRatio = (rect.left + rect.width - plot.x) / plot.width;
1005
+ const topRatio = (rect.top - plot.y) / plot.height;
1006
+ const bottomRatio = (rect.top + rect.height - plot.y) / plot.height;
1007
+ const nextMin = state.xMin + leftRatio * (state.xMax - state.xMin);
1008
+ const nextMax = state.xMin + rightRatio * (state.xMax - state.xMin);
1009
+ if (nextMax - nextMin < X_SCALE_MIN_SPAN) return false;
1010
+ const selectedMaxY = currentYRange.maxY - topRatio * (currentYRange.maxY - currentYRange.minY);
1011
+ const selectedMinY = currentYRange.maxY - bottomRatio * (currentYRange.maxY - currentYRange.minY);
1012
+ const selectedSpan = selectedMaxY - selectedMinY;
1013
+ if (!Number.isFinite(selectedSpan) || selectedSpan <= 0) return false;
1014
+ viewStateRef.current.set(chart.id, { xMin: nextMin, xMax: nextMax });
1015
+ const baseYRange = getYRange(chart, nextMin, nextMax, plot.width);
1016
+ const baseSpan = baseYRange.maxY - baseYRange.minY;
1017
+ if (Number.isFinite(baseSpan) && baseSpan > 0) {
1018
+ const selectedCenter = (selectedMinY + selectedMaxY) / 2;
1019
+ const baseCenter = (baseYRange.minY + baseYRange.maxY) / 2;
1020
+ const nextScale = Math.min(
1021
+ Y_SCALE_MAX,
1022
+ Math.max(Y_SCALE_MIN, selectedSpan / baseSpan)
1023
+ );
1024
+ yScaleRef.current.set(chart.id, nextScale);
1025
+ yCenterOffsetRef.current.set(chart.id, selectedCenter - baseCenter);
1026
+ yManualScaleRef.current.add(chart.id);
1027
+ }
1028
+ return true;
1029
+ };
1030
+ var getChartState = (chart, initialVisiblePoints, viewStateRef) => viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
1031
+ var getScaledYRangeForLayout = ({
1032
+ chart,
1033
+ state,
1034
+ plot,
1035
+ yScaleRef,
1036
+ yCenterOffsetRef
1037
+ }) => applyYScale(
1038
+ getYRange(chart, state.xMin, state.xMax, plot.width),
1039
+ yScaleRef.current.get(chart.id) ?? 1,
1040
+ yCenterOffsetRef.current.get(chart.id) ?? 0
1041
+ );
1042
+ var screenPointToDataPoint = ({
1043
+ point,
1044
+ chart,
1045
+ plot,
1046
+ initialVisiblePoints,
1047
+ viewStateRef,
1048
+ yScaleRef,
1049
+ yCenterOffsetRef
1050
+ }) => {
1051
+ const state = getChartState(chart, initialVisiblePoints, viewStateRef);
1052
+ const yRange = getScaledYRangeForLayout({
1053
+ chart,
1054
+ state,
1055
+ plot,
1056
+ yScaleRef,
1057
+ yCenterOffsetRef
1058
+ });
1059
+ const xRatio = (point.x - plot.x) / plot.width;
1060
+ const yRatio = (point.y - plot.y) / plot.height;
1061
+ return {
1062
+ x: state.xMin + xRatio * (state.xMax - state.xMin),
1063
+ y: yRange.maxY - yRatio * (yRange.maxY - yRange.minY)
1064
+ };
1065
+ };
1066
+ var dataPointToScreenPoint = ({ point, state, yRange, plot }) => {
1067
+ const xRatio = (point.x - state.xMin) / (state.xMax - state.xMin);
1068
+ const yRatio = (yRange.maxY - point.y) / (yRange.maxY - yRange.minY);
1069
+ return {
1070
+ x: plot.x + xRatio * plot.width,
1071
+ y: plot.y + yRatio * plot.height
1072
+ };
1073
+ };
1074
+ var createAxisOverlay = ({
1075
+ chart,
1076
+ plot,
1077
+ state,
1078
+ yRange,
1079
+ seriesEndpoints,
1080
+ seriesOrderByChart = {}
1081
+ }) => {
1082
+ const { maxX } = getChartXBounds(chart);
1083
+ const ticks = Array.from({ length: Y_AXIS_TICK_COUNT }, (_, index) => {
1084
+ const ratio = Y_AXIS_TICK_COUNT === 1 ? 0 : index / (Y_AXIS_TICK_COUNT - 1);
1085
+ const value = yRange.maxY - ratio * (yRange.maxY - yRange.minY);
1086
+ return {
1087
+ id: `${chart.id}-tick-${index}`,
1088
+ value,
1089
+ top: ratio * plot.height
1090
+ };
1091
+ });
1092
+ const xTicks = Array.from({ length: X_AXIS_TICK_COUNT }, (_, index) => {
1093
+ const ratio = X_AXIS_TICK_COUNT === 1 ? 0 : index / (X_AXIS_TICK_COUNT - 1);
1094
+ return {
1095
+ id: `${chart.id}-x-tick-${index}`,
1096
+ value: state.xMin + ratio * (state.xMax - state.xMin),
1097
+ left: 18 + ratio * Math.max(1, plot.width - 36)
1098
+ };
1099
+ });
1100
+ const latestValues = getOrderedSeries(chart, seriesOrderByChart).map((series) => {
1101
+ if (series.length === 0) return null;
1102
+ const value = series.rawY[series.length - 1];
1103
+ const x = series.rawX[series.length - 1];
1104
+ const endpoint = seriesEndpoints.get(series.id) || { x, y: value };
1105
+ const ratio = (yRange.maxY - endpoint.y) / (yRange.maxY - yRange.minY);
1106
+ const xRatio = (endpoint.x - state.xMin) / (state.xMax - state.xMin);
1107
+ return {
1108
+ id: series.id,
1109
+ color: series.color,
1110
+ textColor: getSeriesLabelTextColor(series),
1111
+ value: endpoint.y,
1112
+ rawValue: value,
1113
+ x,
1114
+ left: xRatio * plot.width,
1115
+ top: ratio * plot.height
1116
+ };
1117
+ }).filter(
1118
+ (value) => value && Number.isFinite(value.top) && value.top >= -10 && value.top <= plot.height + 10
1119
+ );
1120
+ return {
1121
+ ticks,
1122
+ xTicks,
1123
+ latestValues,
1124
+ plotWidth: plot.width,
1125
+ showJumpLatest: Number.isFinite(maxX) && (maxX < state.xMin || maxX > state.xMax)
1126
+ };
1127
+ };
1128
+ var drawChartLayouts = ({
1129
+ canvas,
1130
+ width,
1131
+ height,
1132
+ gl,
1133
+ program,
1134
+ antialiasProgram,
1135
+ antialiasLines = false,
1136
+ layouts,
1137
+ viewStateRef,
1138
+ yScaleRef,
1139
+ yCenterOffsetRef,
1140
+ seriesBufferCache,
1141
+ antialiasSeriesBufferCache,
1142
+ initialVisiblePoints,
1143
+ getAppendAnimatedPoint,
1144
+ movingAverageByChart = EMPTY_OBJECT,
1145
+ movingAverageCache,
1146
+ seriesOrderByChart = EMPTY_OBJECT
1147
+ }) => {
1148
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
1149
+ const pixelWidth = Math.max(1, Math.floor(width * dpr));
1150
+ const pixelHeight = Math.max(1, Math.floor(height * dpr));
1151
+ if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
1152
+ canvas.width = pixelWidth;
1153
+ canvas.height = pixelHeight;
1154
+ canvas.style.width = `${width}px`;
1155
+ canvas.style.height = `${height}px`;
1156
+ }
1157
+ gl.viewport(0, 0, pixelWidth, pixelHeight);
1158
+ gl.clearColor(0, 0, 0, 0);
1159
+ gl.clear(gl.COLOR_BUFFER_BIT);
1160
+ const activeAntialiasLines = antialiasLines && antialiasProgram;
1161
+ const activeProgram = activeAntialiasLines ? antialiasProgram : program;
1162
+ gl.useProgram(activeProgram);
1163
+ const uResolution = gl.getUniformLocation(activeProgram, "u_resolution");
1164
+ const uRect = gl.getUniformLocation(activeProgram, "u_rect");
1165
+ const uXRange = gl.getUniformLocation(activeProgram, "u_xRange");
1166
+ const uYRange = gl.getUniformLocation(activeProgram, "u_yRange");
1167
+ const uColor = gl.getUniformLocation(activeProgram, "u_color");
1168
+ let aXy = -1;
1169
+ let aStart = -1;
1170
+ let aEnd = -1;
1171
+ let aSide = -1;
1172
+ let aAlong = -1;
1173
+ let uLineHalfWidth = null;
1174
+ let uEdgeWidth = null;
1175
+ if (activeAntialiasLines) {
1176
+ aStart = gl.getAttribLocation(activeProgram, "a_start");
1177
+ aEnd = gl.getAttribLocation(activeProgram, "a_end");
1178
+ aSide = gl.getAttribLocation(activeProgram, "a_side");
1179
+ aAlong = gl.getAttribLocation(activeProgram, "a_along");
1180
+ uLineHalfWidth = gl.getUniformLocation(activeProgram, "u_lineHalfWidth");
1181
+ uEdgeWidth = gl.getUniformLocation(activeProgram, "u_edgeWidth");
1182
+ gl.enableVertexAttribArray(aStart);
1183
+ gl.enableVertexAttribArray(aEnd);
1184
+ gl.enableVertexAttribArray(aSide);
1185
+ gl.enableVertexAttribArray(aAlong);
1186
+ gl.uniform1f(uLineHalfWidth, AA_LINE_WIDTH_PX * dpr / 2);
1187
+ gl.uniform1f(uEdgeWidth, AA_EDGE_WIDTH_PX * dpr);
1188
+ gl.enable(gl.BLEND);
1189
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
1190
+ } else {
1191
+ aXy = gl.getAttribLocation(activeProgram, "a_xy");
1192
+ gl.enableVertexAttribArray(aXy);
1193
+ gl.disable(gl.BLEND);
1194
+ }
1195
+ gl.uniform2f(uResolution, pixelWidth, pixelHeight);
1196
+ const now = performance.now();
1197
+ const nextAxisOverlays = {};
1198
+ layouts.forEach(({ chart, plot, visible }) => {
1199
+ if (!visible) return;
1200
+ if (activeAntialiasLines) {
1201
+ gl.useProgram(activeProgram);
1202
+ gl.uniform2f(uResolution, pixelWidth, pixelHeight);
1203
+ }
1204
+ const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
1205
+ const scaledPlot = {
1206
+ x: plot.x * dpr,
1207
+ y: plot.y * dpr,
1208
+ width: plot.width * dpr,
1209
+ height: plot.height * dpr
1210
+ };
1211
+ const yRange = applyYScale(
1212
+ getYRange(chart, state.xMin, state.xMax, plot.width),
1213
+ yScaleRef.current.get(chart.id) ?? 1,
1214
+ yCenterOffsetRef.current.get(chart.id) ?? 0
1215
+ );
1216
+ const seriesEndpoints = /* @__PURE__ */ new Map();
1217
+ const chartMovingAverage = movingAverageByChart?.[chart.id];
1218
+ const hideBaseSeries = Boolean(chartMovingAverage?.enabled) && Boolean(chartMovingAverage?.hideBase);
1219
+ gl.enable(gl.SCISSOR_TEST);
1220
+ gl.scissor(
1221
+ Math.floor(scaledPlot.x),
1222
+ Math.floor(pixelHeight - scaledPlot.y - scaledPlot.height),
1223
+ Math.ceil(scaledPlot.width),
1224
+ Math.ceil(scaledPlot.height)
1225
+ );
1226
+ gl.uniform4f(
1227
+ uRect,
1228
+ scaledPlot.x,
1229
+ scaledPlot.y,
1230
+ scaledPlot.width,
1231
+ scaledPlot.height
1232
+ );
1233
+ gl.uniform2f(uXRange, state.xMin, state.xMax);
1234
+ gl.uniform2f(uYRange, yRange.minY, yRange.maxY);
1235
+ if (!hideBaseSeries) getOrderedSeries(chart, seriesOrderByChart).forEach((series) => {
1236
+ let visiblePoints = series.getVisiblePoints(
1237
+ state.xMin,
1238
+ state.xMax,
1239
+ plot.width
1240
+ );
1241
+ let singlePointEndpoint = null;
1242
+ if (visiblePoints.pointCount < 2) {
1243
+ const singlePointSegment = getSinglePointSegment({
1244
+ series,
1245
+ state,
1246
+ yRange,
1247
+ plot
1248
+ });
1249
+ if (!singlePointSegment) return;
1250
+ visiblePoints = singlePointSegment;
1251
+ singlePointEndpoint = singlePointSegment.endpoint;
1252
+ }
1253
+ const animatedPoint = singlePointEndpoint ? null : getAppendAnimatedPoint({
1254
+ seriesId: series.id,
1255
+ visiblePoints,
1256
+ now
1257
+ });
1258
+ const pointCount = animatedPoint?.pointCount ?? visiblePoints.pointCount;
1259
+ if (animatedPoint) {
1260
+ seriesEndpoints.set(series.id, {
1261
+ x: animatedPoint.x,
1262
+ y: animatedPoint.y
1263
+ });
1264
+ } else if (singlePointEndpoint) {
1265
+ seriesEndpoints.set(series.id, singlePointEndpoint);
1266
+ }
1267
+ const [r, g, b] = hexToRgb(series.color);
1268
+ gl.uniform3f(uColor, r, g, b);
1269
+ if (activeAntialiasLines) {
1270
+ const bufferEntry2 = getAntialiasSeriesBuffer(
1271
+ gl,
1272
+ antialiasSeriesBufferCache,
1273
+ series.id,
1274
+ pointCount
1275
+ );
1276
+ const drawVertexCount = fillAntialiasSeriesSegments({
1277
+ vertices: bufferEntry2.vertices,
1278
+ visiblePoints,
1279
+ pointCount,
1280
+ animatedPoint
1281
+ });
1282
+ const stride = AA_VERTEX_FLOAT_STRIDE * Float32Array.BYTES_PER_ELEMENT;
1283
+ gl.bindBuffer(gl.ARRAY_BUFFER, bufferEntry2.buffer);
1284
+ gl.vertexAttribPointer(aStart, 2, gl.FLOAT, false, stride, 0);
1285
+ gl.vertexAttribPointer(
1286
+ aEnd,
1287
+ 2,
1288
+ gl.FLOAT,
1289
+ false,
1290
+ stride,
1291
+ 2 * Float32Array.BYTES_PER_ELEMENT
1292
+ );
1293
+ gl.vertexAttribPointer(
1294
+ aSide,
1295
+ 1,
1296
+ gl.FLOAT,
1297
+ false,
1298
+ stride,
1299
+ 4 * Float32Array.BYTES_PER_ELEMENT
1300
+ );
1301
+ gl.vertexAttribPointer(
1302
+ aAlong,
1303
+ 1,
1304
+ gl.FLOAT,
1305
+ false,
1306
+ stride,
1307
+ 5 * Float32Array.BYTES_PER_ELEMENT
1308
+ );
1309
+ gl.bufferSubData(
1310
+ gl.ARRAY_BUFFER,
1311
+ 0,
1312
+ bufferEntry2.vertices,
1313
+ 0,
1314
+ drawVertexCount * AA_VERTEX_FLOAT_STRIDE
1315
+ );
1316
+ gl.drawArrays(gl.TRIANGLES, 0, drawVertexCount);
1317
+ return;
1318
+ }
1319
+ const bufferEntry = getSeriesBuffer(
1320
+ gl,
1321
+ seriesBufferCache,
1322
+ series.id,
1323
+ pointCount
1324
+ );
1325
+ fillSeriesPoints({
1326
+ vertices: bufferEntry.vertices,
1327
+ visiblePoints,
1328
+ pointCount,
1329
+ animatedPoint
1330
+ });
1331
+ gl.bindBuffer(gl.ARRAY_BUFFER, bufferEntry.buffer);
1332
+ gl.vertexAttribPointer(aXy, 2, gl.FLOAT, false, 0, 0);
1333
+ gl.bufferSubData(
1334
+ gl.ARRAY_BUFFER,
1335
+ 0,
1336
+ bufferEntry.vertices,
1337
+ 0,
1338
+ pointCount * 2
1339
+ );
1340
+ gl.drawArrays(gl.LINE_STRIP, 0, pointCount);
1341
+ });
1342
+ const movingAverageSeries = getMovingAverageSeriesForChart({
1343
+ chart,
1344
+ movingAverage: chartMovingAverage,
1345
+ cache: movingAverageCache
1346
+ });
1347
+ const orderedMovingAverageSeries = getOrderedSeries(
1348
+ { ...chart, series: movingAverageSeries },
1349
+ seriesOrderByChart
1350
+ );
1351
+ let maAXy = aXy;
1352
+ let maUColor = uColor;
1353
+ if (movingAverageSeries.length > 0 && activeAntialiasLines) {
1354
+ gl.useProgram(program);
1355
+ const maUResolution = gl.getUniformLocation(program, "u_resolution");
1356
+ const maURect = gl.getUniformLocation(program, "u_rect");
1357
+ const maUXRange = gl.getUniformLocation(program, "u_xRange");
1358
+ const maUYRange = gl.getUniformLocation(program, "u_yRange");
1359
+ maUColor = gl.getUniformLocation(program, "u_color");
1360
+ maAXy = gl.getAttribLocation(program, "a_xy");
1361
+ gl.enableVertexAttribArray(maAXy);
1362
+ gl.uniform2f(maUResolution, pixelWidth, pixelHeight);
1363
+ gl.uniform4f(
1364
+ maURect,
1365
+ scaledPlot.x,
1366
+ scaledPlot.y,
1367
+ scaledPlot.width,
1368
+ scaledPlot.height
1369
+ );
1370
+ gl.uniform2f(maUXRange, state.xMin, state.xMax);
1371
+ gl.uniform2f(maUYRange, yRange.minY, yRange.maxY);
1372
+ }
1373
+ orderedMovingAverageSeries.forEach((series) => {
1374
+ const visiblePoints = series.getVisiblePoints(
1375
+ state.xMin,
1376
+ state.xMax,
1377
+ plot.width
1378
+ );
1379
+ if (visiblePoints.pointCount < 2) return;
1380
+ const pointCount = visiblePoints.pointCount;
1381
+ const [r, g, b] = hexToRgb(series.color);
1382
+ gl.uniform3f(maUColor, r, g, b);
1383
+ const bufferEntry = getSeriesBuffer(
1384
+ gl,
1385
+ seriesBufferCache,
1386
+ series.id,
1387
+ pointCount * 8
1388
+ );
1389
+ const drawVertexCount = fillDashedSeriesSegments({
1390
+ vertices: bufferEntry.vertices,
1391
+ visiblePoints,
1392
+ pointCount,
1393
+ state,
1394
+ yRange,
1395
+ plot
1396
+ });
1397
+ if (drawVertexCount < 2) return;
1398
+ gl.bindBuffer(gl.ARRAY_BUFFER, bufferEntry.buffer);
1399
+ gl.vertexAttribPointer(maAXy, 2, gl.FLOAT, false, 0, 0);
1400
+ gl.bufferSubData(
1401
+ gl.ARRAY_BUFFER,
1402
+ 0,
1403
+ bufferEntry.vertices,
1404
+ 0,
1405
+ drawVertexCount * 2
1406
+ );
1407
+ gl.drawArrays(gl.LINES, 0, drawVertexCount);
1408
+ });
1409
+ nextAxisOverlays[chart.id] = createAxisOverlay({
1410
+ chart,
1411
+ plot,
1412
+ state,
1413
+ yRange,
1414
+ seriesEndpoints,
1415
+ seriesOrderByChart
1416
+ });
1417
+ gl.disable(gl.SCISSOR_TEST);
1418
+ });
1419
+ if (activeAntialiasLines) {
1420
+ gl.disable(gl.BLEND);
1421
+ }
1422
+ return nextAxisOverlays;
1423
+ };
1424
+
1425
+ // src/vanilla/createChartGrid.js
1426
+ var BASE_ROOT_CLASSES = ["aliencharts-root", "relative", "h-full", "overflow-y-auto"];
1427
+ var EMPTY_OBJECT2 = Object.freeze({});
1428
+ var DEFAULT_OPTIONS = Object.freeze({
1429
+ charts: [],
1430
+ columns: 2,
1431
+ initialVisiblePoints: null,
1432
+ backgroundColor: DEFAULT_CHART_BACKGROUND,
1433
+ antialiasLines: false,
1434
+ gridLines: false,
1435
+ showToolbar: true,
1436
+ showLatestValueLine: true,
1437
+ showTooltips: true,
1438
+ followLatest: false,
1439
+ followVisibleLatest: true,
1440
+ xAxisLabel: "STEP",
1441
+ disableDrawings: false,
1442
+ seriesOrderByChart: EMPTY_OBJECT2,
1443
+ topMarkers: [],
1444
+ formatXTick: formatCompactNumber,
1445
+ formatXValue: formatNumber,
1446
+ formatYValue: formatNumber
1447
+ });
1448
+ var APPEND_ANIMATION = Object.freeze({
1449
+ durationMs: 300,
1450
+ maxBucketSize: 64,
1451
+ maxRevealPoints: 100
1452
+ });
1453
+ var escapeHtml = (value) => String(value ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;");
1454
+ var withAlpha = (color, alpha) => {
1455
+ const value = String(color || DEFAULT_CHART_BACKGROUND).trim();
1456
+ const match = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(value);
1457
+ if (!match) return value;
1458
+ return `rgba(${Number.parseInt(match[1], 16)}, ${Number.parseInt(match[2], 16)}, ${Number.parseInt(match[3], 16)}, ${alpha})`;
1459
+ };
1460
+ var getTextColor = (color) => {
1461
+ const value = String(color || "#38bdf8").replace("#", "");
1462
+ const normalized = value.length === 3 ? value.split("").map((part) => part + part).join("") : value.padEnd(6, "0").slice(0, 6);
1463
+ const number = Number.parseInt(normalized, 16);
1464
+ const rgb = [16, 8, 0].map((shift) => (number >> shift & 255) / 255);
1465
+ const linear = rgb.map((part) => part <= 0.03928 ? part / 12.92 : ((part + 0.055) / 1.055) ** 2.4);
1466
+ return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2] > 0.45 ? "#111827" : "#ffffff";
1467
+ };
1468
+ var hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
1469
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
1470
+ var toolbarButton = ({ action, title, icon, hotkey = "", active = false, className = "" }) => `
1471
+ <button type="button" data-action="${action}" title="${escapeHtml(title)}" aria-label="${escapeHtml(title)}"
1472
+ class="relative inline-flex size-6 items-center justify-center rounded-sm text-foreground hover:bg-accent hover:text-accent-foreground ${active ? "bg-primary/15 text-primary ring-1 ring-primary/40" : ""} ${className}">
1473
+ ${icon}
1474
+ ${hotkey ? `<span class="pointer-events-none absolute left-full top-1/2 -translate-y-1/2 text-[10px] font-semibold leading-none text-foreground/50">${hotkey}</span>` : ""}
1475
+ </button>`;
1476
+ var chartCellHtml = (chart, index, backgroundColor) => `
1477
+ <div data-chart-index="${index}" class="relative select-none overflow-hidden rounded-sm" style="height:${CHART_HEIGHT}px;background-color:${escapeHtml(backgroundColor)}">
1478
+ <div class="pointer-events-none absolute inset-x-0 top-0 z-10 flex h-8 items-center px-2 backdrop-blur" data-header>
1479
+ <div class="flex min-w-0 items-center gap-1 rounded-sm px-1 text-sm font-semibold text-foreground" data-title-wrap>
1480
+ ${chart.pinned ? iconSvg("pushPinSimple", { size: 14, className: "shrink-0 text-foreground/80" }) : ""}
1481
+ <span class="truncate">${escapeHtml(chart.title)}</span>
1482
+ </div>
1483
+ </div>
1484
+ <div data-grid-lines class="pointer-events-none absolute text-border/40"></div>
1485
+ <div data-latest-lines></div>
1486
+ <div data-y-axis class="absolute right-0 z-20" style="top:${PLOT_PADDING.top}px;bottom:${PLOT_PADDING.bottom}px;width:${RIGHT_AXIS_WIDTH}px;cursor:ns-resize"></div>
1487
+ <div data-x-axis class="absolute bottom-0 z-20" style="left:${PLOT_PADDING.left}px;right:${RIGHT_AXIS_WIDTH}px;height:${PLOT_PADDING.bottom}px;cursor:ew-resize"></div>
1488
+ <div data-toolbar></div>
1489
+ </div>`;
1490
+ var makeSurface = (canvas, gl) => ({
1491
+ canvas,
1492
+ gl,
1493
+ program: createProgram(gl),
1494
+ antialiasProgram: createAntialiasProgram(gl),
1495
+ seriesBuffers: createSeriesBufferCache(),
1496
+ antialiasBuffers: createSeriesBufferCache(),
1497
+ movingAverageCache: /* @__PURE__ */ new Map()
1498
+ });
1499
+ var destroySurface = (surface) => {
1500
+ if (!surface) return;
1501
+ deleteSeriesBuffers(surface.gl, surface.seriesBuffers);
1502
+ deleteSeriesBuffers(surface.gl, surface.antialiasBuffers);
1503
+ surface.gl.deleteProgram(surface.program);
1504
+ surface.gl.deleteProgram(surface.antialiasProgram);
1505
+ surface.gl.getExtension("WEBGL_lose_context")?.loseContext();
1506
+ };
1507
+ var AppendAnimator = class {
1508
+ constructor(requestRender) {
1509
+ this.requestRender = requestRender;
1510
+ this.latest = /* @__PURE__ */ new Map();
1511
+ this.animations = /* @__PURE__ */ new Map();
1512
+ this.frame = null;
1513
+ }
1514
+ scan(charts) {
1515
+ const now = performance.now();
1516
+ charts.forEach((chart) => chart.series.forEach((series) => {
1517
+ if (!series.length) return;
1518
+ const next = {
1519
+ x: series.rawX[series.length - 1],
1520
+ y: series.rawY[series.length - 1],
1521
+ length: series.length
1522
+ };
1523
+ const previous = this.latest.get(series.id);
1524
+ if (previous && next.x > previous.x) {
1525
+ this.animations.set(series.id, {
1526
+ fromX: previous.x,
1527
+ fromY: previous.y,
1528
+ toX: next.x,
1529
+ toY: next.y,
1530
+ appendedCount: next.length - previous.length,
1531
+ startedAt: now,
1532
+ duration: APPEND_ANIMATION.durationMs
1533
+ });
1534
+ }
1535
+ this.latest.set(series.id, next);
1536
+ }));
1537
+ if (this.animations.size) this.start();
1538
+ }
1539
+ start() {
1540
+ if (this.frame != null) return;
1541
+ const tick = () => {
1542
+ const now = performance.now();
1543
+ this.animations.forEach((animation, id) => {
1544
+ if (now - animation.startedAt >= animation.duration) this.animations.delete(id);
1545
+ });
1546
+ this.requestRender();
1547
+ this.frame = this.animations.size ? requestAnimationFrame(tick) : null;
1548
+ };
1549
+ this.frame = requestAnimationFrame(tick);
1550
+ }
1551
+ getPoint({ seriesId, visiblePoints, now }) {
1552
+ if (visiblePoints.bucketSize > APPEND_ANIMATION.maxBucketSize) return null;
1553
+ const animation = this.animations.get(seriesId);
1554
+ if (!animation || visiblePoints.x[0] > animation.fromX || visiblePoints.x[visiblePoints.pointCount - 1] < animation.toX) return null;
1555
+ let stableCount = 0;
1556
+ while (stableCount < visiblePoints.pointCount && visiblePoints.x[stableCount] <= animation.fromX) stableCount += 1;
1557
+ if (!stableCount) return null;
1558
+ const rawProgress = clamp((now - animation.startedAt) / animation.duration, 0, 1);
1559
+ const progress = 1 - (1 - rawProgress) ** 2;
1560
+ if (animation.appendedCount > APPEND_ANIMATION.maxRevealPoints) {
1561
+ return {
1562
+ index: stableCount,
1563
+ pointCount: stableCount + 1,
1564
+ x: animation.fromX + (animation.toX - animation.fromX) * progress,
1565
+ y: animation.fromY + (animation.toY - animation.fromY) * progress
1566
+ };
1567
+ }
1568
+ const appendedCount = visiblePoints.pointCount - stableCount;
1569
+ const position = progress * appendedCount;
1570
+ const revealed = Math.min(appendedCount, Math.floor(position));
1571
+ const nextIndex = Math.min(visiblePoints.pointCount - 1, stableCount + revealed);
1572
+ const previousIndex = Math.max(stableCount - 1, nextIndex - 1);
1573
+ const partial = position - revealed;
1574
+ const partialPoint = revealed < appendedCount;
1575
+ return {
1576
+ index: nextIndex,
1577
+ pointCount: Math.min(visiblePoints.pointCount, nextIndex + 1),
1578
+ x: partialPoint ? visiblePoints.x[previousIndex] + (visiblePoints.x[nextIndex] - visiblePoints.x[previousIndex]) * partial : visiblePoints.x[nextIndex],
1579
+ y: partialPoint ? visiblePoints.y[previousIndex] + (visiblePoints.y[nextIndex] - visiblePoints.y[previousIndex]) * partial : visiblePoints.y[nextIndex]
1580
+ };
1581
+ }
1582
+ destroy() {
1583
+ if (this.frame != null) cancelAnimationFrame(this.frame);
1584
+ this.frame = null;
1585
+ this.animations.clear();
1586
+ }
1587
+ };
1588
+ var ChartGridController = class {
1589
+ constructor(container, options) {
1590
+ if (!(container instanceof HTMLElement)) throw new TypeError("createChartGrid requires an HTMLElement target");
1591
+ this.container = container;
1592
+ this.options = { ...DEFAULT_OPTIONS, ...options };
1593
+ if (!Array.isArray(this.options.charts)) throw new TypeError("charts must be an array");
1594
+ this.destroyed = false;
1595
+ this.frame = null;
1596
+ this.structureDirty = true;
1597
+ this.focusedChartId = null;
1598
+ this.fullscreenChartId = null;
1599
+ this.rectangleZoomChartId = null;
1600
+ this.rectangleZoomRect = null;
1601
+ this.crosshair = null;
1602
+ this.drag = null;
1603
+ this.draftDrawing = null;
1604
+ this.drawingSession = null;
1605
+ this.drawings = Array.isArray(options.drawings) ? [...options.drawings] : [];
1606
+ this.activeDrawingTool = options.activeDrawingTool ?? null;
1607
+ this.selectedDrawingId = options.selectedDrawingId ?? null;
1608
+ this.movingAverageByChart = { ...options.movingAverageByChart || {} };
1609
+ this.viewStates = /* @__PURE__ */ new Map();
1610
+ this.yScales = /* @__PURE__ */ new Map();
1611
+ this.yOffsets = /* @__PURE__ */ new Map();
1612
+ this.latestX = /* @__PURE__ */ new Map();
1613
+ this.axisOverlays = {};
1614
+ this.layouts = [];
1615
+ this.fullscreenLayouts = [];
1616
+ this.addedClasses = BASE_ROOT_CLASSES.filter((name) => !container.classList.contains(name));
1617
+ BASE_ROOT_CLASSES.forEach((name) => container.classList.add(name));
1618
+ this.canvas = document.createElement("canvas");
1619
+ this.canvas.className = "pointer-events-none absolute left-0 top-0 z-10 block";
1620
+ this.grid = document.createElement("div");
1621
+ this.grid.className = "relative z-0 grid gap-1 pt-1";
1622
+ this.overlays = document.createElement("div");
1623
+ this.overlays.className = "pointer-events-none absolute left-0 top-0 z-20";
1624
+ this.overlays.style.width = "100%";
1625
+ container.append(this.canvas, this.grid, this.overlays);
1626
+ const gl = this.canvas.getContext("webgl2", { antialias: true, alpha: true, depth: false, stencil: false });
1627
+ if (!gl) {
1628
+ this.cleanupDom();
1629
+ throw new Error("AlienCharts requires WebGL2 support");
1630
+ }
1631
+ this.surface = makeSurface(this.canvas, gl);
1632
+ this.fullscreenSurface = null;
1633
+ this.animator = new AppendAnimator(() => this.requestRender());
1634
+ this.animator.scan(this.options.charts);
1635
+ this.bound = {
1636
+ click: (event) => this.handleClick(event),
1637
+ input: (event) => this.handleInput(event),
1638
+ change: (event) => this.handleInput(event),
1639
+ pointerdown: (event) => this.handlePointerDown(event),
1640
+ pointermove: (event) => this.handlePointerMove(event),
1641
+ pointerup: (event) => this.handlePointerUp(event),
1642
+ pointerleave: () => this.clearCrosshair(),
1643
+ contextmenu: (event) => this.handleContextMenu(event),
1644
+ wheel: (event) => this.handleWheel(event),
1645
+ scroll: () => this.requestRender(),
1646
+ keydown: (event) => this.handleKeyDown(event)
1647
+ };
1648
+ Object.entries(this.bound).forEach(([name, handler]) => {
1649
+ const target = name === "keydown" ? window : container;
1650
+ target.addEventListener(name, handler, name === "wheel" ? { passive: false } : void 0);
1651
+ });
1652
+ this.resizeObserver = new ResizeObserver(() => this.requestRender());
1653
+ this.resizeObserver.observe(container);
1654
+ this.initializeChartState();
1655
+ this.requestRender();
1656
+ }
1657
+ assertAlive() {
1658
+ if (this.destroyed) throw new Error("AlienCharts controller has been destroyed");
1659
+ }
1660
+ initializeChartState() {
1661
+ const ids = new Set(this.options.charts.map((chart) => chart.id));
1662
+ [this.viewStates, this.yScales, this.yOffsets, this.latestX].forEach((map) => {
1663
+ [...map.keys()].forEach((id) => {
1664
+ if (!ids.has(id)) map.delete(id);
1665
+ });
1666
+ });
1667
+ this.options.charts.forEach((chart) => {
1668
+ if (!this.viewStates.has(chart.id)) this.viewStates.set(chart.id, getInitialView(chart, this.options.initialVisiblePoints));
1669
+ if (!this.yScales.has(chart.id)) this.yScales.set(chart.id, 1);
1670
+ if (!this.yOffsets.has(chart.id)) this.yOffsets.set(chart.id, 0);
1671
+ if (!this.latestX.has(chart.id)) this.latestX.set(chart.id, getChartXBounds(chart).maxX);
1672
+ });
1673
+ if (this.focusedChartId && !ids.has(this.focusedChartId)) this.focusedChartId = null;
1674
+ if (this.fullscreenChartId && !ids.has(this.fullscreenChartId)) this.closeFullscreen();
1675
+ }
1676
+ requestRender() {
1677
+ if (this.destroyed || this.frame != null) return;
1678
+ this.frame = requestAnimationFrame(() => {
1679
+ this.frame = null;
1680
+ this.render();
1681
+ });
1682
+ }
1683
+ setOptions(next) {
1684
+ this.assertAlive();
1685
+ if (!next || typeof next !== "object") return;
1686
+ const previousCharts = this.options.charts;
1687
+ const previousActiveDrawingTool = this.activeDrawingTool;
1688
+ this.options = { ...this.options, ...next };
1689
+ if (!Array.isArray(this.options.charts)) throw new TypeError("charts must be an array");
1690
+ if (hasOwn(next, "drawings")) this.drawings = Array.isArray(next.drawings) ? [...next.drawings] : [];
1691
+ if (hasOwn(next, "activeDrawingTool")) {
1692
+ this.activeDrawingTool = next.activeDrawingTool ?? null;
1693
+ if (this.activeDrawingTool !== previousActiveDrawingTool) {
1694
+ this.draftDrawing = null;
1695
+ this.drawingSession = null;
1696
+ }
1697
+ }
1698
+ if (hasOwn(next, "selectedDrawingId")) this.selectedDrawingId = next.selectedDrawingId ?? null;
1699
+ if (hasOwn(next, "movingAverageByChart")) this.movingAverageByChart = { ...next.movingAverageByChart || {} };
1700
+ if (previousCharts !== this.options.charts || hasOwn(next, "columns") || hasOwn(next, "backgroundColor") || hasOwn(next, "gridLines")) this.structureDirty = true;
1701
+ this.initializeChartState();
1702
+ this.requestRender();
1703
+ }
1704
+ setCharts(charts) {
1705
+ this.setOptions({ charts });
1706
+ }
1707
+ invalidate() {
1708
+ this.assertAlive();
1709
+ this.followAppendedData();
1710
+ this.animator.scan(this.options.charts);
1711
+ this.requestRender();
1712
+ }
1713
+ followAppendedData() {
1714
+ this.options.charts.forEach((chart) => {
1715
+ const previous = this.latestX.get(chart.id);
1716
+ const next = getChartXBounds(chart).maxX;
1717
+ if (Number.isFinite(previous) && Number.isFinite(next) && next !== previous) {
1718
+ const state = this.viewStates.get(chart.id) || getInitialView(chart, this.options.initialVisiblePoints);
1719
+ const span = state.xMax - state.xMin;
1720
+ const nearEdge = previous >= state.xMax - span * 0.15 && previous >= state.xMin && previous <= state.xMax;
1721
+ if (this.options.followLatest || this.options.followVisibleLatest && nearEdge) {
1722
+ const delta = next - previous;
1723
+ this.viewStates.set(chart.id, { xMin: state.xMin + delta, xMax: state.xMax + delta });
1724
+ }
1725
+ }
1726
+ this.latestX.set(chart.id, next);
1727
+ });
1728
+ }
1729
+ jumpToLatest(chartId) {
1730
+ this.assertAlive();
1731
+ this.options.charts.forEach((chart) => {
1732
+ if (chartId != null && chart.id !== chartId) return;
1733
+ const maxX = getChartXBounds(chart).maxX;
1734
+ if (!Number.isFinite(maxX)) return;
1735
+ const state = this.viewStates.get(chart.id) || getInitialView(chart, this.options.initialVisiblePoints);
1736
+ const span = Math.max(10, state.xMax - state.xMin);
1737
+ const nextMax = maxX + span * 0.1;
1738
+ this.viewStates.set(chart.id, { xMin: nextMax - span, xMax: nextMax });
1739
+ });
1740
+ this.requestRender();
1741
+ }
1742
+ scrollToTop(options = {}) {
1743
+ this.assertAlive();
1744
+ this.container.scrollTo({ ...options, top: 0 });
1745
+ }
1746
+ renderStructure() {
1747
+ this.grid.style.gridTemplateColumns = `repeat(${Math.max(1, Number(this.options.columns) || 1)}, minmax(18rem, 1fr))`;
1748
+ this.grid.innerHTML = this.options.charts.map((chart, index) => chartCellHtml(chart, index, this.options.backgroundColor)).join("");
1749
+ this.chartNodes = [...this.grid.querySelectorAll("[data-chart-index]")];
1750
+ this.chartNodes.forEach((node) => {
1751
+ node.querySelector("[data-header]").style.backgroundColor = withAlpha(this.options.backgroundColor, 0.82);
1752
+ });
1753
+ this.structureDirty = false;
1754
+ }
1755
+ getLayouts(fullscreen = false) {
1756
+ const nodes = fullscreen ? this.fullscreenNode ? [this.fullscreenNode] : [] : this.chartNodes || [];
1757
+ const charts = fullscreen ? this.options.charts.filter((chart) => chart.id === this.fullscreenChartId) : this.options.charts;
1758
+ const host = fullscreen ? this.fullscreenOverlay : this.container;
1759
+ const hostRect = host.getBoundingClientRect();
1760
+ return nodes.map((node, index) => {
1761
+ const rect = node.getBoundingClientRect();
1762
+ const x = rect.left - hostRect.left + host.scrollLeft;
1763
+ const y = rect.top - hostRect.top + host.scrollTop;
1764
+ const chart = charts[index];
1765
+ const layout = {
1766
+ chart,
1767
+ rect: { x, y, width: rect.width, height: rect.height },
1768
+ plot: {
1769
+ x: x + PLOT_PADDING.left,
1770
+ y: y + PLOT_PADDING.top,
1771
+ width: Math.max(1, rect.width - PLOT_PADDING.left - PLOT_PADDING.right),
1772
+ height: Math.max(1, rect.height - PLOT_PADDING.top - PLOT_PADDING.bottom)
1773
+ },
1774
+ visible: fullscreen || rect.bottom >= hostRect.top && rect.top <= hostRect.bottom,
1775
+ node,
1776
+ fullscreen
1777
+ };
1778
+ return layout;
1779
+ });
1780
+ }
1781
+ render() {
1782
+ const focusState = this.captureControlFocus();
1783
+ if (this.structureDirty) this.renderStructure();
1784
+ this.layouts = this.getLayouts(false);
1785
+ this.axisOverlays = this.drawSurface(this.surface, this.layouts, this.container.scrollWidth, this.container.scrollHeight);
1786
+ this.updateCells(this.layouts, this.axisOverlays, false);
1787
+ if (this.fullscreenChartId) {
1788
+ this.fullscreenLayouts = this.getLayouts(true);
1789
+ const axes = this.drawSurface(this.fullscreenSurface, this.fullscreenLayouts, this.fullscreenOverlay.clientWidth, this.fullscreenOverlay.clientHeight);
1790
+ this.updateCells(this.fullscreenLayouts, axes, true);
1791
+ }
1792
+ this.renderOverlays();
1793
+ this.restoreControlFocus(focusState);
1794
+ }
1795
+ captureControlFocus() {
1796
+ const active = document.activeElement;
1797
+ if (!(active instanceof HTMLInputElement)) return null;
1798
+ const selector = active.matches("[data-moving-period]") ? "[data-moving-period]" : active.matches("[data-pin-text]") ? "[data-pin-text]" : active.matches("[data-drawing-color]") ? "[data-drawing-color]" : null;
1799
+ if (!selector) return null;
1800
+ const fullscreen = Boolean(active.closest("[data-aliencharts-fullscreen]"));
1801
+ let selectionStart = null;
1802
+ let selectionEnd = null;
1803
+ if (active.type === "text") {
1804
+ selectionStart = active.selectionStart;
1805
+ selectionEnd = active.selectionEnd;
1806
+ }
1807
+ return { selector, fullscreen, selectionStart, selectionEnd };
1808
+ }
1809
+ restoreControlFocus(state) {
1810
+ if (!state) return;
1811
+ const host = state.fullscreen ? this.fullscreenOverlay : this.container;
1812
+ const input = host?.querySelector(state.selector);
1813
+ if (!(input instanceof HTMLInputElement)) return;
1814
+ input.focus({ preventScroll: true });
1815
+ if (input.type === "text" && state.selectionStart != null) {
1816
+ input.setSelectionRange(state.selectionStart, state.selectionEnd);
1817
+ }
1818
+ }
1819
+ drawSurface(surface, layouts, width, height) {
1820
+ if (!surface) return {};
1821
+ return drawChartLayouts({
1822
+ canvas: surface.canvas,
1823
+ width: Math.max(1, width),
1824
+ height: Math.max(1, height),
1825
+ gl: surface.gl,
1826
+ program: surface.program,
1827
+ antialiasProgram: surface.antialiasProgram,
1828
+ antialiasLines: this.options.antialiasLines,
1829
+ layouts,
1830
+ viewStateRef: { current: this.viewStates },
1831
+ yScaleRef: { current: this.yScales },
1832
+ yCenterOffsetRef: { current: this.yOffsets },
1833
+ seriesBufferCache: surface.seriesBuffers,
1834
+ antialiasSeriesBufferCache: surface.antialiasBuffers,
1835
+ initialVisiblePoints: this.options.initialVisiblePoints,
1836
+ getAppendAnimatedPoint: (payload) => this.animator.getPoint(payload),
1837
+ movingAverageByChart: this.movingAverageByChart,
1838
+ movingAverageCache: surface.movingAverageCache,
1839
+ seriesOrderByChart: this.options.seriesOrderByChart
1840
+ });
1841
+ }
1842
+ updateCells(layouts, axes, fullscreen) {
1843
+ layouts.forEach(({ chart, node }) => {
1844
+ const axis = axes[chart.id];
1845
+ node.querySelector("[data-title-wrap]").classList.toggle("ring-1", this.focusedChartId === chart.id);
1846
+ node.querySelector("[data-title-wrap]").classList.toggle("ring-primary/70", this.focusedChartId === chart.id);
1847
+ const grid = node.querySelector("[data-grid-lines]");
1848
+ const gridOptions = this.options.gridLines === true ? {} : this.options.gridLines || {};
1849
+ grid.style.display = this.options.gridLines ? "block" : "none";
1850
+ Object.assign(grid.style, {
1851
+ left: `${PLOT_PADDING.left}px`,
1852
+ right: `${PLOT_PADDING.right}px`,
1853
+ top: `${PLOT_PADDING.top}px`,
1854
+ bottom: `${PLOT_PADDING.bottom}px`,
1855
+ backgroundImage: "linear-gradient(to right, transparent calc(100% - 1px), currentColor calc(100% - 1px)), linear-gradient(to bottom, transparent calc(100% - 1px), currentColor calc(100% - 1px))",
1856
+ backgroundSize: `${Math.max(8, Number(gridOptions.xSpacing) || 80)}px 100%, 100% ${Math.max(8, Number(gridOptions.ySpacing) || 48)}px`
1857
+ });
1858
+ const yAxis = node.querySelector("[data-y-axis]");
1859
+ yAxis.style.backgroundColor = withAlpha(this.options.backgroundColor, 0.78);
1860
+ yAxis.innerHTML = (axis?.ticks || []).map((tick) => `<div class="pointer-events-none absolute left-1/2 -translate-x-1/2 -translate-y-1/2 text-center text-[11px] font-medium tabular-nums text-foreground/70" style="top:${tick.top}px">${escapeHtml(this.options.formatYValue(tick.value))}</div>`).join("") + (axis?.latestValues || []).map((latest) => `<div class="pointer-events-none absolute left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-sm px-1.5 py-0.5 text-center text-[10px] font-bold tabular-nums shadow-sm" style="top:${latest.top}px;background:${latest.color};color:${getTextColor(latest.color)}">${escapeHtml(this.options.formatYValue(latest.value))}</div>`).join("");
1861
+ const xAxis = node.querySelector("[data-x-axis]");
1862
+ xAxis.style.backgroundColor = withAlpha(this.options.backgroundColor, 0.7);
1863
+ xAxis.innerHTML = (axis?.xTicks || []).map((tick) => `<div class="pointer-events-none absolute top-1/2 -translate-x-1/2 -translate-y-1/2 text-center text-[11px] font-medium tabular-nums text-foreground/70" style="left:${tick.left}px">${escapeHtml(this.options.formatXTick(tick.value))}</div>`).join("");
1864
+ const latestLayer = node.querySelector("[data-latest-lines]");
1865
+ latestLayer.innerHTML = this.options.showLatestValueLine ? (axis?.latestValues || []).filter((item) => item.left >= 0 && item.left <= axis.plotWidth).map((item) => `<div class="pointer-events-none absolute z-10 opacity-70" style="left:${PLOT_PADDING.left + item.left}px;top:${PLOT_PADDING.top + item.top}px;height:1px;width:${Math.max(0, axis.plotWidth - item.left)}px;background-image:repeating-linear-gradient(to right,${item.color} 0 4px,transparent 4px 8px)"></div>`).join("") : "";
1866
+ this.updateToolbar(node, chart, axis, fullscreen);
1867
+ });
1868
+ }
1869
+ updateToolbar(node, chart, axis, fullscreen) {
1870
+ const target = node.querySelector("[data-toolbar]");
1871
+ const focused = fullscreen || this.focusedChartId === chart.id;
1872
+ let html = "";
1873
+ if (axis?.showJumpLatest) html += `<button type="button" data-action="jump-latest" aria-label="Jump to latest" class="absolute z-30 inline-flex size-7 items-center justify-center rounded-sm text-foreground hover:bg-accent" style="right:${RIGHT_AXIS_WIDTH + 6}px;bottom:${PLOT_PADDING.bottom + 6}px;background:${withAlpha(this.options.backgroundColor, 0.7)}">${iconSvg("arrowLineRight")}</button>`;
1874
+ if (focused && this.options.showToolbar) {
1875
+ const moving = Boolean(this.movingAverageByChart[chart.id]?.enabled);
1876
+ const hasDrawings = getDrawingsForChart(this.drawings, chart.id).length > 0;
1877
+ html += `<div class="absolute left-2 top-12 z-30 flex flex-col gap-0.5 rounded-sm bg-background/80 pr-3 text-foreground shadow-sm backdrop-blur">`;
1878
+ html += toolbarButton({ action: "fullscreen", title: fullscreen ? "Exit fullscreen" : "Maximize chart", hotkey: "F", icon: iconSvg(fullscreen ? "arrowsIn" : "arrowsOut") });
1879
+ html += toolbarButton({ action: "reset", title: "Reset chart", hotkey: "R", icon: iconSvg("arrowCounterClockwise") });
1880
+ html += toolbarButton({ action: "rectangle-zoom", title: "Rectangle zoom", hotkey: "Z", active: this.rectangleZoomChartId === chart.id, icon: iconSvg("magnifyingGlassPlus") });
1881
+ if (!this.options.disableDrawings && hasDrawings) html += toolbarButton({ action: "clear-drawings", title: "Clear drawings", className: "text-orange-500", icon: iconSvg("broom") });
1882
+ if (!this.options.disableDrawings) {
1883
+ html += '<div class="my-1 h-px w-full"></div>';
1884
+ html += toolbarButton({ action: "tool-trendline", title: "Draw trendline", hotkey: "T", active: this.activeDrawingTool === "trendline", icon: iconSvg("minus", { className: "-rotate-45" }) });
1885
+ html += toolbarButton({ action: "tool-hline", title: "Draw horizontal line", hotkey: "H", active: this.activeDrawingTool === "hline", icon: iconSvg("minus") });
1886
+ html += toolbarButton({ action: "tool-vline", title: "Draw vertical line", hotkey: "V", active: this.activeDrawingTool === "vline", icon: iconSvg("minus", { className: "rotate-90" }) });
1887
+ html += toolbarButton({ action: "tool-pin", title: "Place pin", hotkey: "P", active: this.activeDrawingTool === "pin", icon: iconSvg("mapPin") });
1888
+ html += toolbarButton({ action: "moving-average", title: "Toggle moving average", hotkey: "M", active: moving, icon: iconSvg("waveSine") });
1889
+ }
1890
+ html += "</div>";
1891
+ if (!this.options.disableDrawings && moving) html += this.movingAverageOptionsHtml(chart);
1892
+ }
1893
+ target.innerHTML = html;
1894
+ }
1895
+ movingAverageOptionsHtml(chart) {
1896
+ const moving = this.movingAverageByChart[chart.id] || { enabled: true, period: 21, type: "ema", hideBase: false };
1897
+ return `<div data-moving-options class="absolute right-16 top-2 z-40 flex items-center gap-1 rounded-sm bg-background/85 p-1 text-xs text-foreground shadow-sm">
1898
+ <button type="button" data-action="toggle-base" title="${moving.hideBase ? "Show" : "Hide"} base series" aria-label="${moving.hideBase ? "Show" : "Hide"} base series" class="inline-flex size-6 items-center justify-center rounded-sm ${moving.hideBase ? "bg-primary/15 text-primary ring-1 ring-primary/40" : ""}">${iconSvg("eye", { size: 14 })}</button>
1899
+ <button type="button" data-action="moving-type" title="Toggle moving average type" class="h-6 rounded-sm px-2 text-[11px] font-semibold uppercase">${moving.type === "sma" ? "sma" : "ema"}</button>
1900
+ <input data-moving-period type="number" min="1" step="1" value="${Math.max(1, Number(moving.period) || 21)}" title="Moving average period" class="h-6 w-14 rounded-sm border border-border/50 px-1 text-xs" />
1901
+ </div>`;
1902
+ }
1903
+ renderOverlays() {
1904
+ const surface = this.fullscreenChartId ? this.fullscreenOverlay : this.container;
1905
+ const overlay = this.fullscreenChartId ? this.fullscreenOverlays : this.overlays;
1906
+ const layouts = this.fullscreenChartId ? this.fullscreenLayouts : this.layouts;
1907
+ if (!overlay) return;
1908
+ overlay.style.height = `${surface.scrollHeight}px`;
1909
+ let html = this.drawingOverlayHtml(layouts);
1910
+ html += this.topMarkersHtml(layouts);
1911
+ html += this.crosshairHtml();
1912
+ html += this.rectangleOverlayHtml();
1913
+ html += this.drawingOptionsHtml(layouts);
1914
+ overlay.innerHTML = html;
1915
+ }
1916
+ drawingOverlayHtml(layouts) {
1917
+ const drawings = this.draftDrawing ? [...this.drawings, this.draftDrawing] : this.drawings;
1918
+ if (!drawings.length) return "";
1919
+ const body = layouts.flatMap((layout) => getDrawingsForChart(drawings, layout.chart.id).map((drawing) => {
1920
+ const geometry = getDrawingGeometry({ drawing, layout, projectPoint: (point) => this.projectPoint(point, layout) });
1921
+ if (!geometry) return "";
1922
+ const selected = drawing.id === this.selectedDrawingId || drawing.id === "__draft__";
1923
+ if (drawing.type === "pin") {
1924
+ const text = escapeHtml(geometry.style.text?.trim() || "");
1925
+ return `<g opacity="${drawing.id === "__draft__" ? 0.72 : 1}"><g transform="translate(${geometry.start.x} ${geometry.start.y})"><path d="M0 -11 C-6 -11 -10 -6 -10 -1 C-10 6 -2 12 0 15 C2 12 10 6 10 -1 C10 -6 6 -11 0 -11 Z" fill="${geometry.style.color}" stroke="var(--background,#fff)" stroke-width="1.5"/><circle cy="-2" r="3" fill="var(--background,#fff)"/></g>${text ? `<text x="${geometry.start.x + 14}" y="${geometry.start.y}" fill="${geometry.style.color}" stroke="var(--background,#fff)" stroke-width="3" paint-order="stroke" font-size="12" font-weight="600">${text}</text>` : ""}${selected ? `<circle cx="${geometry.start.x}" cy="${geometry.start.y}" r="4" fill="${geometry.style.color}" stroke="var(--background,#fff)"/>` : ""}</g>`;
1926
+ }
1927
+ return `<g><line data-drawing-line x1="${geometry.lineStart.x}" y1="${geometry.lineStart.y}" x2="${geometry.lineEnd.x}" y2="${geometry.lineEnd.y}" stroke="${geometry.style.color}" stroke-width="${geometry.style.lineWidth}" stroke-dasharray="${geometry.style.dashPattern.join(" ")}" opacity="${drawing.id === "__draft__" ? 0.72 : 1}"/>${selected ? `<circle data-drawing-anchor="start" cx="${geometry.start.x}" cy="${geometry.start.y}" r="4" fill="${geometry.style.color}" stroke="var(--background,#fff)"/>${drawing.type === "trendline" ? `<circle data-drawing-anchor="end" cx="${geometry.end.x}" cy="${geometry.end.y}" r="4" fill="${geometry.style.color}" stroke="var(--background,#fff)"/>` : ""}` : ""}</g>`;
1928
+ })).join("");
1929
+ return `<svg class="pointer-events-none absolute left-0 top-0 z-20 block overflow-visible" style="width:100%;height:100%">${body}</svg>`;
1930
+ }
1931
+ topMarkersHtml(layouts) {
1932
+ if (!this.options.topMarkers?.length) return "";
1933
+ return layouts.flatMap((layout) => this.options.topMarkers.map((marker, markerIndex) => {
1934
+ const value = Number(marker.x ?? marker.step);
1935
+ const state = this.viewStates.get(layout.chart.id);
1936
+ const ratio = (value - state.xMin) / (state.xMax - state.xMin);
1937
+ if (!Number.isFinite(value) || ratio < 0 || ratio > 1) return "";
1938
+ return `<button type="button" data-marker-index="${markerIndex}" class="pointer-events-auto absolute z-30 -translate-x-1/2 rounded-full" style="left:${layout.plot.x + ratio * layout.plot.width}px;top:${layout.plot.y + 4}px" title="${escapeHtml(marker.title || "")}" aria-label="${escapeHtml(marker.ariaLabel || marker.title || "Chart marker")}"><span class="block size-2.5 rounded-full border border-background shadow-sm" style="background:${marker.color || "#f97316"}"></span></button>`;
1939
+ })).join("");
1940
+ }
1941
+ crosshairHtml() {
1942
+ const crosshair = this.options.showTooltips ? this.crosshair : null;
1943
+ if (!crosshair) return "";
1944
+ return `<div class="pointer-events-none absolute z-20 border-l border-foreground/40" style="left:${crosshair.x}px;top:0;height:100%"></div><div class="pointer-events-none absolute z-20 border-t border-foreground/40" style="top:${crosshair.y}px;left:0;width:100%"></div>
1945
+ ${crosshair.points.map((point) => `<div class="pointer-events-none absolute z-30 size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border border-background" style="left:${point.x}px;top:${point.y}px;background:${point.color}"></div>`).join("")}
1946
+ <div data-crosshair-tooltip class="pointer-events-none absolute z-30 w-[220px] rounded-sm border border-border/70 bg-popover/80 px-2 py-1.5 text-xs text-popover-foreground shadow-sm backdrop-blur-sm" style="left:${crosshair.tooltipX}px;top:${crosshair.tooltipY}px"><div class="mb-1 font-medium">${escapeHtml(this.options.xAxisLabel)}: ${escapeHtml(this.options.formatXValue(crosshair.xValue))}</div>${crosshair.points.map((point) => `<div class="grid grid-cols-[auto_1fr] gap-x-2"><span class="mt-1 size-2 rounded-full" style="background:${point.color}"></span><div class="min-w-0"><div class="truncate text-muted-foreground">${escapeHtml(point.name)}</div><div class="tabular-nums">${escapeHtml(this.options.formatYValue(point.yValue))}</div></div></div>`).join("")}</div>`;
1947
+ }
1948
+ rectangleOverlayHtml() {
1949
+ const rect = this.rectangleZoomRect && normalizeRect(this.rectangleZoomRect.start, this.rectangleZoomRect.end);
1950
+ return rect ? `<div class="pointer-events-none absolute z-40 rounded-sm border border-primary/70 bg-primary/10" style="left:${rect.left}px;top:${rect.top}px;width:${rect.width}px;height:${rect.height}px"></div>` : "";
1951
+ }
1952
+ drawingOptionsHtml(layouts) {
1953
+ const drawing = this.drawings.find((item) => item.id === this.selectedDrawingId);
1954
+ if (!drawing) return "";
1955
+ const layout = layouts.find((item) => item.chart.id === drawing.chartId);
1956
+ if (!layout) return "";
1957
+ const style = drawing.style || {};
1958
+ const canExtend = drawing.type === "trendline" || drawing.type === "hline";
1959
+ const canText = drawing.type === "pin";
1960
+ const left = Math.max(
1961
+ layout.rect.x + 42,
1962
+ layout.rect.x + layout.rect.width - PLOT_PADDING.right - 8
1963
+ );
1964
+ return `<div data-drawing-options class="pointer-events-auto absolute z-40 flex items-center gap-1 rounded-sm bg-background/85 p-1 text-xs text-foreground shadow-sm backdrop-blur" style="left:${left}px;top:${layout.plot.y + 6}px;transform:translateX(-100%)">
1965
+ ${canExtend ? `<button type="button" data-action="extend-drawing" class="h-6 rounded-sm px-2 text-[11px] font-semibold ${style.extendRight ? "bg-primary/15 text-primary ring-1 ring-primary/40" : ""}">Ext</button>` : ""}
1966
+ ${canText ? `<input data-pin-text type="text" value="${escapeHtml(style.text || "")}" placeholder="Text" class="h-6 w-24 rounded-sm border border-border/50 px-1.5 text-xs"/>` : ""}
1967
+ <input data-drawing-color type="color" value="${/^#[0-9a-f]{6}$/i.test(style.color || "") ? style.color : "#60a5fa"}" title="Line color" class="size-6 rounded-sm border border-border/70"/>
1968
+ </div>`;
1969
+ }
1970
+ projectPoint(point, layout) {
1971
+ const state = this.viewStates.get(layout.chart.id);
1972
+ const range = applyYScale(getYRange(layout.chart, state.xMin, state.xMax, layout.plot.width), this.yScales.get(layout.chart.id), this.yOffsets.get(layout.chart.id));
1973
+ return dataPointToScreenPoint({ point, state, yRange: range, plot: layout.plot });
1974
+ }
1975
+ pointForEvent(event, host) {
1976
+ const rect = host.getBoundingClientRect();
1977
+ return { x: event.clientX - rect.left + host.scrollLeft, y: event.clientY - rect.top + host.scrollTop };
1978
+ }
1979
+ eventContext(event) {
1980
+ const cell = event.target.closest?.("[data-chart-index]");
1981
+ if (!cell) return null;
1982
+ const fullscreen = Boolean(cell.closest("[data-aliencharts-fullscreen]"));
1983
+ const layouts = fullscreen ? this.fullscreenLayouts : this.layouts;
1984
+ const index = Number(cell.dataset.chartIndex);
1985
+ const layout = fullscreen ? layouts[0] : layouts[index];
1986
+ const host = fullscreen ? this.fullscreenOverlay : this.container;
1987
+ return layout ? { layout, host, point: this.pointForEvent(event, host), fullscreen } : null;
1988
+ }
1989
+ handleClick(event) {
1990
+ const marker = event.target.closest?.("[data-marker-index]");
1991
+ if (marker) {
1992
+ event.stopPropagation();
1993
+ this.options.onTopMarkerClick?.(this.options.topMarkers[Number(marker.dataset.markerIndex)]);
1994
+ return;
1995
+ }
1996
+ const button = event.target.closest?.("[data-action]");
1997
+ if (!button) return;
1998
+ event.preventDefault();
1999
+ event.stopPropagation();
2000
+ const context = this.eventContext(event);
2001
+ const chartId = context?.layout.chart.id || this.focusedChartId || this.fullscreenChartId;
2002
+ switch (button.dataset.action) {
2003
+ case "fullscreen":
2004
+ this.fullscreenChartId ? this.closeFullscreen() : this.openFullscreen(chartId);
2005
+ break;
2006
+ case "reset":
2007
+ this.resetChart(chartId);
2008
+ break;
2009
+ case "jump-latest":
2010
+ this.jumpToLatest(chartId);
2011
+ break;
2012
+ case "rectangle-zoom":
2013
+ this.rectangleZoomChartId = this.rectangleZoomChartId === chartId ? null : chartId;
2014
+ this.activeDrawingTool = null;
2015
+ this.emit("onActiveDrawingToolChange", null);
2016
+ this.requestRender();
2017
+ break;
2018
+ case "clear-drawings":
2019
+ this.options.onClearDrawingsRequest ? this.emit("onClearDrawingsRequest", chartId) : this.setDrawings(this.drawings.filter((item) => item.chartId !== chartId));
2020
+ break;
2021
+ case "moving-average":
2022
+ this.toggleMovingAverage(chartId);
2023
+ break;
2024
+ case "tool-trendline":
2025
+ this.toggleDrawingTool("trendline");
2026
+ break;
2027
+ case "tool-hline":
2028
+ this.toggleDrawingTool("hline");
2029
+ break;
2030
+ case "tool-vline":
2031
+ this.toggleDrawingTool("vline");
2032
+ break;
2033
+ case "tool-pin":
2034
+ this.toggleDrawingTool("pin");
2035
+ break;
2036
+ case "toggle-base":
2037
+ this.updateMovingAverage(chartId, { hideBase: !this.movingAverageByChart[chartId]?.hideBase });
2038
+ break;
2039
+ case "moving-type":
2040
+ this.updateMovingAverage(chartId, { type: this.movingAverageByChart[chartId]?.type === "sma" ? "ema" : "sma" });
2041
+ break;
2042
+ case "extend-drawing":
2043
+ this.updateSelectedDrawing((drawing) => ({ ...drawing, style: { ...drawing.style, extendRight: drawing.style?.extendRight === false } }));
2044
+ break;
2045
+ default:
2046
+ break;
2047
+ }
2048
+ }
2049
+ handleInput(event) {
2050
+ if (event.target.matches("[data-moving-period]")) {
2051
+ const context = this.eventContext(event);
2052
+ this.updateMovingAverage(context?.layout.chart.id, { period: Math.max(1, Math.round(Number(event.target.value) || 1)) });
2053
+ } else if (event.target.matches("[data-pin-text]")) {
2054
+ this.updateSelectedDrawing((drawing) => ({ ...drawing, style: { ...drawing.style, text: event.target.value } }));
2055
+ } else if (event.target.matches("[data-drawing-color]")) {
2056
+ this.updateSelectedDrawing((drawing) => ({ ...drawing, style: { ...drawing.style, color: event.target.value } }));
2057
+ }
2058
+ }
2059
+ handlePointerDown(event) {
2060
+ if (event.target.closest?.("button,input")) return;
2061
+ if (event.button !== 0) return;
2062
+ const context = this.eventContext(event);
2063
+ if (!context) return;
2064
+ const { layout, point } = context;
2065
+ this.activePointerContext = context;
2066
+ this.focusedChartId = layout.chart.id;
2067
+ const inPlot = point.x >= layout.plot.x && point.x <= layout.plot.x + layout.plot.width && point.y >= layout.plot.y && point.y <= layout.plot.y + layout.plot.height;
2068
+ if (inPlot && this.activeDrawingTool && !this.options.disableDrawings) {
2069
+ const dataPoint = screenPointToDataPoint({ point, chart: layout.chart, plot: layout.plot, initialVisiblePoints: this.options.initialVisiblePoints, viewStateRef: { current: this.viewStates }, yScaleRef: { current: this.yScales }, yCenterOffsetRef: { current: this.yOffsets } });
2070
+ if (["hline", "vline", "pin"].includes(this.activeDrawingTool)) {
2071
+ this.commitDrawing(layout.chart.id, this.activeDrawingTool, dataPoint, dataPoint);
2072
+ } else if (this.drawingSession?.chartId === layout.chart.id) {
2073
+ this.commitDrawing(layout.chart.id, "trendline", this.drawingSession.start, dataPoint);
2074
+ } else {
2075
+ this.drawingSession = { chartId: layout.chart.id, type: "trendline", start: dataPoint };
2076
+ this.draftDrawing = createDraftDrawing({ chartId: layout.chart.id, type: "trendline", start: dataPoint, end: dataPoint });
2077
+ }
2078
+ } else if (inPlot && this.rectangleZoomChartId === layout.chart.id) {
2079
+ this.drag = { type: "rectangle", layout, start: point };
2080
+ this.rectangleZoomRect = { start: point, end: point };
2081
+ } else if (inPlot) {
2082
+ const hit = this.hitDrawing(point, layout);
2083
+ if (hit) {
2084
+ this.selectedDrawingId = hit.drawing.id;
2085
+ this.emit("onSelectedDrawingIdChange", hit.drawing.id);
2086
+ if (hit.endpoint != null || ["hline", "vline", "pin"].includes(hit.drawing.type)) {
2087
+ this.drag = { type: "drawing-edit", layout, drawingId: hit.drawing.id, endpoint: hit.endpoint };
2088
+ }
2089
+ } else {
2090
+ this.selectedDrawingId = null;
2091
+ this.emit("onSelectedDrawingIdChange", null);
2092
+ this.drag = { type: "pan", layout, start: point, state: { ...this.viewStates.get(layout.chart.id) }, yOffset: this.yOffsets.get(layout.chart.id) || 0 };
2093
+ }
2094
+ } else if (point.x >= layout.plot.x + layout.plot.width) {
2095
+ this.drag = { type: "y-scale", layout, start: point, scale: this.yScales.get(layout.chart.id) || 1 };
2096
+ } else if (point.y >= layout.plot.y + layout.plot.height) {
2097
+ this.drag = { type: "x-scale", layout, start: point, state: { ...this.viewStates.get(layout.chart.id) } };
2098
+ }
2099
+ context.host.setPointerCapture?.(event.pointerId);
2100
+ this.pointerCaptureTarget = context.host;
2101
+ this.requestRender();
2102
+ }
2103
+ handlePointerMove(event) {
2104
+ if (event.target.closest?.("button,input") && !this.drag && !this.drawingSession) return;
2105
+ const context = this.eventContext(event) || (this.activePointerContext && (this.drag || this.drawingSession) ? { ...this.activePointerContext, point: this.pointForEvent(event, this.activePointerContext.host) } : null);
2106
+ if (!context) return this.clearCrosshair();
2107
+ const { layout, point } = context;
2108
+ if (this.drawingSession) {
2109
+ if (layout.chart.id !== this.drawingSession.chartId) return;
2110
+ const end = screenPointToDataPoint({ point, chart: layout.chart, plot: layout.plot, initialVisiblePoints: this.options.initialVisiblePoints, viewStateRef: { current: this.viewStates }, yScaleRef: { current: this.yScales }, yCenterOffsetRef: { current: this.yOffsets } });
2111
+ this.draftDrawing = createDraftDrawing({ ...this.drawingSession, end });
2112
+ this.requestRender();
2113
+ return;
2114
+ }
2115
+ if (this.drag) {
2116
+ const { chart, plot } = this.drag.layout;
2117
+ if (this.drag.type === "drawing-edit") {
2118
+ const dataPoint = screenPointToDataPoint({ point, chart, plot, initialVisiblePoints: this.options.initialVisiblePoints, viewStateRef: { current: this.viewStates }, yScaleRef: { current: this.yScales }, yCenterOffsetRef: { current: this.yOffsets } });
2119
+ const next = updateDrawingById(this.drawings, this.drag.drawingId, (drawing) => {
2120
+ if (drawing.type === "hline") {
2121
+ const deltaX = (drawing.end?.x ?? drawing.start.x) - drawing.start.x;
2122
+ return { ...drawing, start: { x: dataPoint.x, y: dataPoint.y }, end: { x: dataPoint.x + deltaX, y: dataPoint.y } };
2123
+ }
2124
+ if (drawing.type === "vline") return { ...drawing, start: { ...drawing.start, x: dataPoint.x }, end: { ...drawing.end, x: dataPoint.x } };
2125
+ if (drawing.type === "pin") return { ...drawing, start: dataPoint, end: dataPoint };
2126
+ if (this.drag.endpoint === "start" || this.drag.endpoint === "end") return { ...drawing, [this.drag.endpoint]: dataPoint };
2127
+ return drawing;
2128
+ });
2129
+ this.drawings = next;
2130
+ this.emit("onDrawingsChange", [...next]);
2131
+ this.requestRender();
2132
+ return;
2133
+ }
2134
+ if (this.drag.type === "rectangle") this.rectangleZoomRect = { start: this.drag.start, end: point };
2135
+ if (this.drag.type === "pan") {
2136
+ const span = this.drag.state.xMax - this.drag.state.xMin;
2137
+ const deltaX = (point.x - this.drag.start.x) / plot.width * span;
2138
+ const baseRange = getYRange(chart, this.drag.state.xMin, this.drag.state.xMax, plot.width);
2139
+ const rangeSpan = (baseRange.maxY - baseRange.minY) * (this.yScales.get(chart.id) || 1);
2140
+ const deltaY = (point.y - this.drag.start.y) / plot.height * rangeSpan;
2141
+ this.viewStates.set(chart.id, { xMin: this.drag.state.xMin - deltaX, xMax: this.drag.state.xMax - deltaX });
2142
+ this.yOffsets.set(chart.id, this.drag.yOffset + deltaY);
2143
+ }
2144
+ if (this.drag.type === "y-scale") this.yScales.set(chart.id, clamp(this.drag.scale * Math.exp((point.y - this.drag.start.y) * 0.01), Y_SCALE_MIN, Y_SCALE_MAX));
2145
+ if (this.drag.type === "x-scale") {
2146
+ const center = (this.drag.state.xMin + this.drag.state.xMax) / 2;
2147
+ const span = clamp((this.drag.state.xMax - this.drag.state.xMin) * Math.exp((point.x - this.drag.start.x) * 0.01), X_SCALE_MIN_SPAN, X_SCALE_MAX_SPAN);
2148
+ this.viewStates.set(chart.id, { xMin: center - span / 2, xMax: center + span / 2 });
2149
+ }
2150
+ if (this.drag.type === "pan") this.updateCrosshair(point, layout);
2151
+ else this.requestRender();
2152
+ return;
2153
+ }
2154
+ this.updateCrosshair(point, layout, context.fullscreen);
2155
+ }
2156
+ handlePointerUp(event) {
2157
+ if (event.target.closest?.("button,input") && !this.drag && !this.drawingSession) return;
2158
+ const context = this.eventContext(event) || (this.activePointerContext ? { ...this.activePointerContext, point: this.pointForEvent(event, this.activePointerContext.host) } : null);
2159
+ if (this.drag?.type === "rectangle" && this.rectangleZoomRect) {
2160
+ const normalized = normalizeRect(this.rectangleZoomRect.start, this.rectangleZoomRect.end);
2161
+ if (normalized?.width >= 4 && normalized?.height >= 4) {
2162
+ const layout = this.drag.layout;
2163
+ applyRectangleZoom({
2164
+ chart: layout.chart,
2165
+ plot: layout.plot,
2166
+ start: this.rectangleZoomRect.start,
2167
+ end: this.rectangleZoomRect.end,
2168
+ initialVisiblePoints: this.options.initialVisiblePoints,
2169
+ viewStateRef: { current: this.viewStates },
2170
+ yScaleRef: { current: this.yScales },
2171
+ yCenterOffsetRef: { current: this.yOffsets },
2172
+ yManualScaleRef: { current: /* @__PURE__ */ new Set() }
2173
+ });
2174
+ }
2175
+ this.rectangleZoomChartId = null;
2176
+ this.rectangleZoomRect = null;
2177
+ }
2178
+ this.drag = null;
2179
+ this.pointerCaptureTarget?.releasePointerCapture?.(event.pointerId);
2180
+ this.pointerCaptureTarget = null;
2181
+ this.activePointerContext = null;
2182
+ this.requestRender();
2183
+ }
2184
+ handleWheel(event) {
2185
+ const context = this.eventContext(event);
2186
+ if (!context) return;
2187
+ const { layout, point } = context;
2188
+ const inPlot = point.x >= layout.plot.x && point.x <= layout.plot.x + layout.plot.width && point.y >= layout.plot.y && point.y <= layout.plot.y + layout.plot.height;
2189
+ if (!inPlot) return;
2190
+ event.preventDefault();
2191
+ const state = this.viewStates.get(layout.chart.id);
2192
+ const ratio = clamp((point.x - layout.plot.x) / layout.plot.width, 0, 1);
2193
+ const anchor = state.xMin + ratio * (state.xMax - state.xMin);
2194
+ const zoom = Math.exp(event.deltaY * 1e-3);
2195
+ const span = clamp((state.xMax - state.xMin) * zoom, X_SCALE_MIN_SPAN, X_SCALE_MAX_SPAN);
2196
+ this.viewStates.set(layout.chart.id, { xMin: anchor - ratio * span, xMax: anchor + (1 - ratio) * span });
2197
+ this.requestRender();
2198
+ }
2199
+ handleContextMenu(event) {
2200
+ const context = this.eventContext(event);
2201
+ if (!context || !this.options.onChartContextMenu) return;
2202
+ event.preventDefault();
2203
+ const { layout, point } = context;
2204
+ const inPlot = point.x >= layout.plot.x && point.x <= layout.plot.x + layout.plot.width && point.y >= layout.plot.y && point.y <= layout.plot.y + layout.plot.height;
2205
+ const data = inPlot ? screenPointToDataPoint({ point, chart: layout.chart, plot: layout.plot, initialVisiblePoints: this.options.initialVisiblePoints, viewStateRef: { current: this.viewStates }, yScaleRef: { current: this.yScales }, yCenterOffsetRef: { current: this.yOffsets } }) : null;
2206
+ this.options.onChartContextMenu({ chart: layout.chart, event, point: data });
2207
+ }
2208
+ updateCrosshair(point, layout) {
2209
+ const inPlot = point.x >= layout.plot.x && point.x <= layout.plot.x + layout.plot.width && point.y >= layout.plot.y && point.y <= layout.plot.y + layout.plot.height;
2210
+ if (!inPlot || !this.options.showTooltips) return this.clearCrosshair();
2211
+ const state = this.viewStates.get(layout.chart.id);
2212
+ const xValue = state.xMin + (point.x - layout.plot.x) / layout.plot.width * (state.xMax - state.xMin);
2213
+ const range = applyYScale(getYRange(layout.chart, state.xMin, state.xMax, layout.plot.width), this.yScales.get(layout.chart.id), this.yOffsets.get(layout.chart.id));
2214
+ const points = layout.chart.series.map((series) => {
2215
+ const visible = series.getVisiblePoints(state.xMin, state.xMax, layout.plot.width);
2216
+ if (!visible.pointCount) return null;
2217
+ const index = getNearestPointIndex(visible.x, xValue);
2218
+ const data = { x: visible.x[index], y: visible.y[index] };
2219
+ const screen = dataPointToScreenPoint({ point: data, state, yRange: range, plot: layout.plot });
2220
+ return { id: series.id, name: series.name, color: series.color, x: screen.x, y: screen.y, xValue: data.x, yValue: data.y };
2221
+ }).filter(Boolean);
2222
+ if (!points.length) return this.clearCrosshair();
2223
+ const nearest = points.reduce((best, item) => Math.abs(item.x - point.x) < Math.abs(best.x - point.x) ? item : best);
2224
+ this.crosshair = {
2225
+ x: point.x,
2226
+ y: point.y,
2227
+ xValue: nearest.xValue,
2228
+ points,
2229
+ tooltipX: point.x + 232 > layout.rect.x + layout.rect.width ? point.x - 232 : point.x + 12,
2230
+ tooltipY: clamp(point.y + 12, layout.rect.y + PLOT_PADDING.top, layout.rect.y + layout.rect.height - 96)
2231
+ };
2232
+ this.requestRender();
2233
+ }
2234
+ clearCrosshair() {
2235
+ if (!this.crosshair) return;
2236
+ this.crosshair = null;
2237
+ this.requestRender();
2238
+ }
2239
+ hitDrawing(point, layout) {
2240
+ for (const drawing of getDrawingsForChart(this.drawings, layout.chart.id)) {
2241
+ const hit = hitTestDrawing({ point, drawing, layout, projectPoint: (data) => this.projectPoint(data, layout) });
2242
+ if (hit) return hit;
2243
+ }
2244
+ return null;
2245
+ }
2246
+ commitDrawing(chartId, type, start, end) {
2247
+ const drawing = createDrawing({ chartId, type, start, end, createDrawingId: this.options.createDrawingId });
2248
+ this.setDrawings([...this.drawings, drawing]);
2249
+ this.selectedDrawingId = drawing.id;
2250
+ this.activeDrawingTool = null;
2251
+ this.draftDrawing = null;
2252
+ this.drawingSession = null;
2253
+ this.emit("onSelectedDrawingIdChange", drawing.id);
2254
+ this.emit("onActiveDrawingToolChange", null);
2255
+ }
2256
+ setDrawings(drawings) {
2257
+ this.drawings = drawings;
2258
+ this.emit("onDrawingsChange", [...drawings]);
2259
+ this.requestRender();
2260
+ }
2261
+ updateSelectedDrawing(updater) {
2262
+ if (!this.selectedDrawingId) return;
2263
+ this.setDrawings(updateDrawingById(this.drawings, this.selectedDrawingId, updater));
2264
+ }
2265
+ toggleDrawingTool(tool) {
2266
+ if (!DRAWING_TOOLS.has(tool) || this.options.disableDrawings) return;
2267
+ this.activeDrawingTool = this.activeDrawingTool === tool ? null : tool;
2268
+ this.rectangleZoomChartId = null;
2269
+ this.draftDrawing = null;
2270
+ this.drawingSession = null;
2271
+ this.emit("onActiveDrawingToolChange", this.activeDrawingTool);
2272
+ this.requestRender();
2273
+ }
2274
+ toggleMovingAverage(chartId) {
2275
+ if (!chartId || this.options.disableDrawings) return;
2276
+ const current = this.movingAverageByChart[chartId] || { period: 21, type: "ema", hideBase: false };
2277
+ this.movingAverageByChart = { ...this.movingAverageByChart, [chartId]: { ...current, enabled: !current.enabled } };
2278
+ this.emit("onMovingAverageToggle", chartId);
2279
+ this.requestRender();
2280
+ }
2281
+ updateMovingAverage(chartId, changes) {
2282
+ if (!chartId) return;
2283
+ const next = { enabled: true, period: 21, type: "ema", hideBase: false, ...this.movingAverageByChart[chartId] || {}, ...changes };
2284
+ this.movingAverageByChart = { ...this.movingAverageByChart, [chartId]: next };
2285
+ this.emit("onMovingAverageChange", chartId, { ...next });
2286
+ this.requestRender();
2287
+ }
2288
+ resetChart(chartId) {
2289
+ const chart = this.options.charts.find((item) => item.id === chartId);
2290
+ if (!chart) return;
2291
+ this.viewStates.set(chartId, getInitialView(chart, this.options.initialVisiblePoints));
2292
+ this.yScales.set(chartId, 1);
2293
+ this.yOffsets.set(chartId, 0);
2294
+ this.requestRender();
2295
+ }
2296
+ openFullscreen(chartId) {
2297
+ const chart = this.options.charts.find((item) => item.id === chartId);
2298
+ if (!chart || this.fullscreenChartId) return;
2299
+ this.fullscreenChartId = chartId;
2300
+ const overlay = document.createElement("div");
2301
+ overlay.dataset.alienchartsFullscreen = "";
2302
+ overlay.className = "aliencharts-root fixed inset-0 z-[60] bg-background p-2";
2303
+ overlay.innerHTML = `<canvas class="pointer-events-none absolute left-0 top-0 z-10 block"></canvas><div data-fullscreen-cell>${chartCellHtml(chart, 0, this.options.backgroundColor)}</div><div data-fullscreen-overlays class="pointer-events-none absolute left-0 top-0 z-20" style="width:100%;height:100%"></div>`;
2304
+ document.body.append(overlay);
2305
+ this.fullscreenOverlay = overlay;
2306
+ this.fullscreenNode = overlay.querySelector("[data-chart-index]");
2307
+ this.fullscreenNode.style.height = "calc(100vh - 16px)";
2308
+ this.fullscreenOverlays = overlay.querySelector("[data-fullscreen-overlays]");
2309
+ const canvas = overlay.querySelector("canvas");
2310
+ const gl = canvas.getContext("webgl2", { antialias: true, alpha: true, depth: false, stencil: false });
2311
+ if (!gl) {
2312
+ overlay.remove();
2313
+ this.fullscreenChartId = null;
2314
+ return;
2315
+ }
2316
+ this.fullscreenSurface = makeSurface(canvas, gl);
2317
+ this.fullscreenListeners = Object.entries(this.bound).filter(([name]) => name !== "keydown" && name !== "scroll");
2318
+ this.fullscreenListeners.forEach(([name, handler]) => overlay.addEventListener(name, handler, name === "wheel" ? { passive: false } : void 0));
2319
+ this.resizeObserver.observe(overlay);
2320
+ this.requestRender();
2321
+ }
2322
+ closeFullscreen() {
2323
+ if (!this.fullscreenChartId) return;
2324
+ if (this.fullscreenOverlay) this.resizeObserver?.unobserve(this.fullscreenOverlay);
2325
+ this.fullscreenListeners?.forEach(([name, handler]) => this.fullscreenOverlay?.removeEventListener(name, handler));
2326
+ this.fullscreenListeners = null;
2327
+ destroySurface(this.fullscreenSurface);
2328
+ this.fullscreenOverlay?.remove();
2329
+ this.fullscreenSurface = null;
2330
+ this.fullscreenOverlay = null;
2331
+ this.fullscreenNode = null;
2332
+ this.fullscreenOverlays = null;
2333
+ this.fullscreenLayouts = [];
2334
+ this.fullscreenChartId = null;
2335
+ this.requestRender();
2336
+ }
2337
+ handleKeyDown(event) {
2338
+ const editable = event.target instanceof HTMLElement && (event.target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(event.target.tagName));
2339
+ if (editable) return;
2340
+ const chartId = this.fullscreenChartId || this.focusedChartId;
2341
+ const key = event.key.toLowerCase();
2342
+ if (["arrowleft", "arrowright", "arrowup", "arrowdown"].includes(key) && !this.fullscreenChartId) {
2343
+ this.moveFocus(key.replace("arrow", ""));
2344
+ event.preventDefault();
2345
+ return;
2346
+ }
2347
+ if (key === "r") {
2348
+ this.resetChart(chartId);
2349
+ event.preventDefault();
2350
+ } else if (key === "z") {
2351
+ this.rectangleZoomChartId = this.rectangleZoomChartId === chartId ? null : chartId;
2352
+ event.preventDefault();
2353
+ this.requestRender();
2354
+ } else if (key === "m") {
2355
+ this.toggleMovingAverage(chartId);
2356
+ event.preventDefault();
2357
+ } else if (["t", "h", "v", "p"].includes(key)) {
2358
+ this.toggleDrawingTool({ t: "trendline", h: "hline", v: "vline", p: "pin" }[key]);
2359
+ event.preventDefault();
2360
+ } else if ((key === "delete" || key === "backspace") && this.selectedDrawingId) {
2361
+ this.setDrawings(removeDrawingById(this.drawings, this.selectedDrawingId));
2362
+ this.selectedDrawingId = null;
2363
+ this.emit("onSelectedDrawingIdChange", null);
2364
+ event.preventDefault();
2365
+ } else if (key === "escape" && (this.activeDrawingTool || this.draftDrawing || this.rectangleZoomChartId)) {
2366
+ this.activeDrawingTool = null;
2367
+ this.draftDrawing = null;
2368
+ this.drawingSession = null;
2369
+ this.rectangleZoomChartId = null;
2370
+ this.rectangleZoomRect = null;
2371
+ this.emit("onActiveDrawingToolChange", null);
2372
+ this.requestRender();
2373
+ event.preventDefault();
2374
+ } else if (key === "f" || key === "escape" && this.fullscreenChartId) {
2375
+ this.fullscreenChartId ? this.closeFullscreen() : this.openFullscreen(chartId);
2376
+ event.preventDefault();
2377
+ }
2378
+ }
2379
+ moveFocus(direction) {
2380
+ const charts = this.options.charts;
2381
+ if (!charts.length) return;
2382
+ let index = Math.max(0, charts.findIndex((chart) => chart.id === this.focusedChartId));
2383
+ const columns = Math.max(1, Number(this.options.columns) || 1);
2384
+ if (direction === "left") index = Math.max(Math.floor(index / columns) * columns, index - 1);
2385
+ if (direction === "right") index = Math.min(charts.length - 1, index + 1);
2386
+ if (direction === "up") index = Math.max(0, index - columns);
2387
+ if (direction === "down") index = Math.min(charts.length - 1, index + columns);
2388
+ this.focusedChartId = charts[index].id;
2389
+ this.chartNodes[index]?.scrollIntoView({ block: "nearest", inline: "nearest" });
2390
+ this.requestRender();
2391
+ }
2392
+ emit(name, ...args) {
2393
+ this.options[name]?.(...args);
2394
+ }
2395
+ cleanupDom() {
2396
+ this.canvas?.remove();
2397
+ this.grid?.remove();
2398
+ this.overlays?.remove();
2399
+ this.addedClasses?.forEach((name) => this.container.classList.remove(name));
2400
+ }
2401
+ destroy() {
2402
+ if (this.destroyed) return;
2403
+ this.closeFullscreen();
2404
+ this.destroyed = true;
2405
+ if (this.frame != null) cancelAnimationFrame(this.frame);
2406
+ this.animator.destroy();
2407
+ this.resizeObserver.disconnect();
2408
+ Object.entries(this.bound).forEach(([name, handler]) => {
2409
+ const target = name === "keydown" ? window : this.container;
2410
+ target.removeEventListener(name, handler);
2411
+ });
2412
+ destroySurface(this.surface);
2413
+ this.cleanupDom();
2414
+ }
2415
+ };
2416
+ var createChartGrid = (container, options = {}) => new ChartGridController(container, options);
2417
+
2418
+ // src/react/ChartGridAdapter.jsx
2419
+ import { jsx } from "react/jsx-runtime";
2420
+ var ChartGrid = forwardRef(function ChartGrid2({
2421
+ className = "",
2422
+ dataRevision = 0,
2423
+ ...options
2424
+ }, ref) {
2425
+ const hostRef = useRef(null);
2426
+ const controllerRef = useRef(null);
2427
+ const previousOptionsRef = useRef(null);
2428
+ const previousRevisionRef = useRef(dataRevision);
2429
+ useLayoutEffect(() => {
2430
+ const controller = createChartGrid(hostRef.current, options);
2431
+ controllerRef.current = controller;
2432
+ previousOptionsRef.current = options;
2433
+ return () => {
2434
+ controller.destroy();
2435
+ controllerRef.current = null;
2436
+ previousOptionsRef.current = null;
2437
+ };
2438
+ }, []);
2439
+ useLayoutEffect(() => {
2440
+ const controller = controllerRef.current;
2441
+ const previous = previousOptionsRef.current;
2442
+ if (!controller || !previous) return;
2443
+ const changed = {};
2444
+ let hasChanges = false;
2445
+ const keys = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(options)]);
2446
+ keys.forEach((key) => {
2447
+ if (Object.is(previous[key], options[key])) return;
2448
+ changed[key] = options[key];
2449
+ hasChanges = true;
2450
+ });
2451
+ if (hasChanges) controller.setOptions(changed);
2452
+ previousOptionsRef.current = options;
2453
+ });
2454
+ useLayoutEffect(() => {
2455
+ if (previousRevisionRef.current !== dataRevision) {
2456
+ previousRevisionRef.current = dataRevision;
2457
+ controllerRef.current?.invalidate();
2458
+ }
2459
+ }, [dataRevision]);
2460
+ useImperativeHandle(ref, () => ({
2461
+ invalidate: () => controllerRef.current?.invalidate(),
2462
+ jumpToLatest: (chartId) => controllerRef.current?.jumpToLatest(chartId),
2463
+ scrollToTop: (scrollOptions) => controllerRef.current?.scrollToTop(scrollOptions)
2464
+ }), []);
2465
+ return /* @__PURE__ */ jsx("div", { ref: hostRef, className });
2466
+ });
2467
+
2468
+ // src/core/mockData.js
2469
+ var palette = [
2470
+ "#38bdf8",
2471
+ "#22c55e",
2472
+ "#f59e0b",
2473
+ "#ef4444",
2474
+ "#a855f7",
2475
+ "#14b8a6"
2476
+ ];
2477
+ var createRandom = (seed) => {
2478
+ let value = seed >>> 0;
2479
+ return () => {
2480
+ value = value * 1664525 + 1013904223 >>> 0;
2481
+ return value / 4294967296;
2482
+ };
2483
+ };
2484
+ var makeSeries = ({ chartIndex, seriesIndex, pointCount }) => {
2485
+ const x = new Float64Array(pointCount);
2486
+ const y = new Float32Array(pointCount);
2487
+ const random = createRandom((chartIndex + 1) * 1009 + seriesIndex * 9176);
2488
+ const baseline = 15 + random() * 45 + seriesIndex * 4;
2489
+ const waveA = 8e-4 + random() * 4e-3;
2490
+ const waveB = 6e-3 + random() * 0.03;
2491
+ const waveC = 5e-5 + random() * 6e-4;
2492
+ const amplitudeA = 0.015 + random() * 0.09;
2493
+ const amplitudeB = 5e-3 + random() * 0.04;
2494
+ const trend = (random() - 0.5) * 4e-3;
2495
+ const noise = 4e-3 + random() * 0.025;
2496
+ const spikeInterval = 180 + Math.floor(random() * 900);
2497
+ const spikeMagnitude = 0.12 + random() * 0.7;
2498
+ let value = baseline;
2499
+ for (let i = 0; i < pointCount; i += 1) {
2500
+ x[i] = i;
2501
+ const spike = (i + chartIndex * 13 + seriesIndex * 31) % spikeInterval === 0 ? spikeMagnitude * (random() > 0.45 ? 1 : -1) : 0;
2502
+ value += Math.sin(i * waveA + chartIndex) * amplitudeA + Math.cos(i * waveB + seriesIndex) * amplitudeB + Math.sin(i * waveC) * amplitudeA * 0.35 + trend + (random() - 0.5) * noise + spike;
2503
+ y[i] = value;
2504
+ }
2505
+ return createSeries({
2506
+ id: `chart-${chartIndex}-series-${seriesIndex}`,
2507
+ name: `Run ${seriesIndex + 1}`,
2508
+ color: palette[(chartIndex + seriesIndex) % palette.length],
2509
+ x,
2510
+ y
2511
+ });
2512
+ };
2513
+ var createMockCharts = ({
2514
+ chartCount = 50,
2515
+ seriesPerChart = 2,
2516
+ pointCount = 5e5
2517
+ } = {}) => {
2518
+ return Array.from({ length: chartCount }, (_, chartIndex) => ({
2519
+ id: `metric-${chartIndex + 1}`,
2520
+ title: `metric_${String(chartIndex + 1).padStart(2, "0")}`,
2521
+ series: Array.from(
2522
+ { length: seriesPerChart },
2523
+ (_2, seriesIndex) => makeSeries({ chartIndex, seriesIndex, pointCount })
2524
+ )
2525
+ }));
2526
+ };
2527
+ export {
2528
+ ChartGrid,
2529
+ LineSeries,
2530
+ createMockCharts,
2531
+ createSeries
2532
+ };
2533
+ //# sourceMappingURL=react.js.map