aliencharts 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3303 @@
1
+ // src/icons.js
2
+ var icons = {
3
+ 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"/>',
4
+ 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"/>',
5
+ arrowCounterClockwise: '<polyline points="24 56 24 104 72 104"/><path d="M67.59,192A88,88,0,1,0,65.77,65.77L24,104"/>',
6
+ 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"/>',
7
+ 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"/>',
8
+ 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"/>',
9
+ waveSine: '<path d="M24,128c104-221.7,104,221.7,208,0"/>',
10
+ 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"/>',
11
+ 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"/>',
12
+ minus: '<line x1="40" y1="128" x2="216" y2="128"/>',
13
+ 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"/>'
14
+ };
15
+ var filledIcons = /* @__PURE__ */ new Set(["pushPinSimple"]);
16
+ var iconSvg = (name, { size = 16, className = "" } = {}) => {
17
+ const body = icons[name];
18
+ if (!body) throw new Error(`Unknown AlienCharts icon: ${name}`);
19
+ const paint = filledIcons.has(name) ? 'fill="currentColor"' : 'fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"';
20
+ 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>`;
21
+ };
22
+
23
+ // src/core/drawingUtils.js
24
+ var DEFAULT_DRAWING_COLOR = "#60a5fa";
25
+ var DRAWING_HIT_DISTANCE = 8;
26
+ var DRAWING_HANDLE_HIT_DISTANCE = 10;
27
+ var PIN_HIT_DISTANCE = 14;
28
+ var DRAWING_TOOLS = /* @__PURE__ */ new Set(["trendline", "hline", "vline", "pin"]);
29
+ var getDrawingStyle = (drawing) => ({
30
+ color: drawing?.style?.color || DEFAULT_DRAWING_COLOR,
31
+ lineWidth: Number.isFinite(drawing?.style?.lineWidth) ? drawing.style.lineWidth : 2,
32
+ dashPattern: Array.isArray(drawing?.style?.dashPattern) ? drawing.style.dashPattern : [],
33
+ extendLeft: Boolean(drawing?.style?.extendLeft),
34
+ extendRight: drawing?.style?.extendRight !== false,
35
+ text: typeof drawing?.style?.text === "string" ? drawing.style.text : ""
36
+ });
37
+ var getDrawingsForChart = (drawings, chartId) => Array.isArray(drawings) ? drawings.filter((drawing) => drawing?.chartId === chartId) : [];
38
+ var getDefaultDrawingStyle = (type) => ({
39
+ color: DEFAULT_DRAWING_COLOR,
40
+ lineWidth: 2,
41
+ dashPattern: [],
42
+ extendLeft: false,
43
+ extendRight: type !== "vline",
44
+ text: ""
45
+ });
46
+ var createDrawing = ({
47
+ chartId,
48
+ type,
49
+ start,
50
+ end,
51
+ createDrawingId
52
+ }) => {
53
+ const id = createDrawingId?.({ chartId, type }) || `${chartId}-drawing-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
54
+ return {
55
+ id,
56
+ chartId,
57
+ type,
58
+ start,
59
+ end,
60
+ createdAt: Date.now(),
61
+ style: getDefaultDrawingStyle(type)
62
+ };
63
+ };
64
+ var createDraftDrawing = ({ chartId, type, start, end }) => ({
65
+ id: "__draft__",
66
+ chartId,
67
+ type,
68
+ start,
69
+ end,
70
+ createdAt: Date.now(),
71
+ style: getDefaultDrawingStyle(type)
72
+ });
73
+ var extendRayToPlotBoundary = (origin, through, plot) => {
74
+ const dx = through.x - origin.x;
75
+ const dy = through.y - origin.y;
76
+ if (Math.abs(dx) <= 1e-6 && Math.abs(dy) <= 1e-6) return through;
77
+ const candidates = [];
78
+ if (dx > 1e-6) candidates.push((plot.x + plot.width - origin.x) / dx);
79
+ else if (dx < -1e-6) candidates.push((plot.x - origin.x) / dx);
80
+ if (dy > 1e-6) candidates.push((plot.y + plot.height - origin.y) / dy);
81
+ else if (dy < -1e-6) candidates.push((plot.y - origin.y) / dy);
82
+ const boundaryScale = Math.min(
83
+ ...candidates.filter((scale2) => Number.isFinite(scale2) && scale2 >= 0)
84
+ );
85
+ const scale = Number.isFinite(boundaryScale) ? Math.max(1, boundaryScale) : 1;
86
+ return {
87
+ x: origin.x + dx * scale,
88
+ y: origin.y + dy * scale
89
+ };
90
+ };
91
+ var getDrawingGeometry = ({ drawing, layout, projectPoint }) => {
92
+ if (!drawing?.start || !drawing?.end) return null;
93
+ const start = projectPoint(drawing.start, layout);
94
+ const end = projectPoint(drawing.end, layout);
95
+ const style = getDrawingStyle(drawing);
96
+ let lineStart = start;
97
+ let lineEnd = end;
98
+ if (drawing.type === "pin") {
99
+ return { start, end: start, lineStart: start, lineEnd: start, style };
100
+ }
101
+ if (drawing.type === "hline") {
102
+ const lineEndX = Math.abs(end.x - start.x) > 1e-6 ? end.x : layout.plot.x + layout.plot.width;
103
+ lineStart = {
104
+ x: style.extendRight ? layout.plot.x : start.x,
105
+ y: start.y
106
+ };
107
+ lineEnd = {
108
+ x: style.extendRight ? layout.plot.x + layout.plot.width : lineEndX,
109
+ y: start.y
110
+ };
111
+ } else if (drawing.type === "vline") {
112
+ lineStart = {
113
+ x: start.x,
114
+ y: layout.plot.y
115
+ };
116
+ lineEnd = {
117
+ x: start.x,
118
+ y: layout.plot.y + layout.plot.height
119
+ };
120
+ } else if (drawing.type === "trendline") {
121
+ if (style.extendLeft) lineStart = extendRayToPlotBoundary(end, start, layout.plot);
122
+ if (style.extendRight) lineEnd = extendRayToPlotBoundary(start, end, layout.plot);
123
+ }
124
+ return { start, end, lineStart, lineEnd, style };
125
+ };
126
+ var distanceToSegment = (point, start, end) => {
127
+ const dx = end.x - start.x;
128
+ const dy = end.y - start.y;
129
+ if (dx === 0 && dy === 0) {
130
+ return Math.hypot(point.x - start.x, point.y - start.y);
131
+ }
132
+ const t = Math.max(
133
+ 0,
134
+ Math.min(
135
+ 1,
136
+ ((point.x - start.x) * dx + (point.y - start.y) * dy) / (dx * dx + dy * dy)
137
+ )
138
+ );
139
+ return Math.hypot(point.x - (start.x + t * dx), point.y - (start.y + t * dy));
140
+ };
141
+ var hitTestDrawing = ({ point, drawing, layout, projectPoint }) => {
142
+ const geometry = getDrawingGeometry({
143
+ drawing,
144
+ layout,
145
+ projectPoint
146
+ });
147
+ if (!geometry) return null;
148
+ if (drawing.type !== "pin" && Math.hypot(point.x - geometry.start.x, point.y - geometry.start.y) <= DRAWING_HANDLE_HIT_DISTANCE) {
149
+ return { drawing, endpoint: "start" };
150
+ }
151
+ if (drawing.type === "pin" && Math.hypot(point.x - geometry.start.x, point.y - geometry.start.y) <= PIN_HIT_DISTANCE) {
152
+ return { drawing, endpoint: "move" };
153
+ }
154
+ if (drawing.type === "trendline" && Math.hypot(point.x - geometry.end.x, point.y - geometry.end.y) <= DRAWING_HANDLE_HIT_DISTANCE) {
155
+ return { drawing, endpoint: "end" };
156
+ }
157
+ const distance = distanceToSegment(point, geometry.lineStart, geometry.lineEnd);
158
+ return distance <= DRAWING_HIT_DISTANCE ? { drawing, endpoint: null } : null;
159
+ };
160
+ var updateDrawingById = (drawings, drawingId, updater) => (Array.isArray(drawings) ? drawings : []).map(
161
+ (drawing) => drawing?.id === drawingId ? updater(drawing) : drawing
162
+ );
163
+ var removeDrawingById = (drawings, drawingId) => (Array.isArray(drawings) ? drawings : []).filter(
164
+ (drawing) => drawing?.id !== drawingId
165
+ );
166
+
167
+ // src/core/chartModel.js
168
+ var LINE_CAPABILITIES = Object.freeze({
169
+ appendAnimation: true,
170
+ drawings: true,
171
+ latestValue: true,
172
+ markers: true,
173
+ movingAverage: true
174
+ });
175
+ var BAR_CAPABILITIES = Object.freeze({
176
+ appendAnimation: false,
177
+ drawings: false,
178
+ latestValue: false,
179
+ markers: false,
180
+ movingAverage: false
181
+ });
182
+ var CHART_DEFINITIONS = Object.freeze({
183
+ line: Object.freeze({
184
+ capabilities: LINE_CAPABILITIES,
185
+ defaultOrientation: "vertical",
186
+ orientationFromSeries: false,
187
+ orientations: Object.freeze(["vertical"]),
188
+ rangeIncludesZero: false
189
+ }),
190
+ bar: Object.freeze({
191
+ capabilities: BAR_CAPABILITIES,
192
+ defaultOrientation: "vertical",
193
+ orientationFromSeries: true,
194
+ orientations: Object.freeze(["vertical", "horizontal"]),
195
+ rangeIncludesZero: true
196
+ })
197
+ });
198
+ var getChartCategories = (chart) => {
199
+ if (chart?.categories == null) return [];
200
+ if (!Array.isArray(chart.categories)) {
201
+ throw new TypeError(`Chart "${chart.id}" categories must be an array`);
202
+ }
203
+ const values = /* @__PURE__ */ new Set();
204
+ return chart.categories.map((category, index) => {
205
+ const normalized = typeof category === "string" ? { value: index, label: category } : { value: Number(category?.value), label: category?.label };
206
+ if (!Number.isFinite(normalized.value) || typeof normalized.label !== "string") {
207
+ throw new TypeError(
208
+ `Chart "${chart.id}" category ${index} must be a string or a { value, label } object`
209
+ );
210
+ }
211
+ if (values.has(normalized.value)) {
212
+ throw new TypeError(
213
+ `Chart "${chart.id}" has duplicate category value ${normalized.value}`
214
+ );
215
+ }
216
+ values.add(normalized.value);
217
+ return normalized;
218
+ });
219
+ };
220
+ var getCategoryLabel = (chart, value) => getChartCategories(chart).find((category) => category.value === value)?.label;
221
+ var getSeriesType = (series) => {
222
+ if (series?.type == null) return "line";
223
+ if (Object.prototype.hasOwnProperty.call(CHART_DEFINITIONS, series.type)) {
224
+ return series.type;
225
+ }
226
+ throw new TypeError(`Unknown AlienCharts series type: "${series.type}"`);
227
+ };
228
+ var resolveChartDescriptor = (chart) => {
229
+ const series = Array.isArray(chart?.series) ? chart.series : [];
230
+ const categories = getChartCategories(chart);
231
+ const type = series.length ? getSeriesType(series[0]) : "line";
232
+ const definition = CHART_DEFINITIONS[type];
233
+ const orientation = definition.orientationFromSeries ? series[0]?.orientation || definition.defaultOrientation : definition.defaultOrientation;
234
+ series.forEach((item) => {
235
+ const itemType = getSeriesType(item);
236
+ if (itemType !== type) {
237
+ const mixedTypes = /* @__PURE__ */ new Set([type, itemType]);
238
+ const typeLabel = mixedTypes.has("line") && mixedTypes.has("bar") ? "line and bar" : `${type} and ${itemType}`;
239
+ throw new TypeError(
240
+ `Chart "${chart.id}" cannot mix ${typeLabel} series`
241
+ );
242
+ }
243
+ const itemOrientation = definition.orientationFromSeries ? item.orientation || definition.defaultOrientation : definition.defaultOrientation;
244
+ if (!definition.orientations.includes(itemOrientation)) {
245
+ throw new TypeError(
246
+ `Chart "${chart.id}" has an invalid ${itemType} orientation`
247
+ );
248
+ }
249
+ if (itemOrientation !== orientation) {
250
+ throw new TypeError(
251
+ `Chart "${chart.id}" cannot mix ${itemType} orientations`
252
+ );
253
+ }
254
+ });
255
+ return Object.freeze({
256
+ capabilities: definition.capabilities,
257
+ categorical: categories.length > 0,
258
+ orientation,
259
+ rangeIncludesZero: definition.rangeIncludesZero,
260
+ rendererType: type,
261
+ type
262
+ });
263
+ };
264
+ var resolveChartDescriptors = (charts) => {
265
+ const descriptors = /* @__PURE__ */ new Map();
266
+ charts.forEach((chart) => {
267
+ descriptors.set(chart.id, resolveChartDescriptor(chart));
268
+ });
269
+ return descriptors;
270
+ };
271
+ var getChartDescriptor = (chart, descriptors) => descriptors?.get(chart?.id) || resolveChartDescriptor(chart);
272
+ var getOrderedSeries = (chart, seriesOrderByChart) => {
273
+ const series = Array.isArray(chart?.series) ? chart.series : [];
274
+ const order = seriesOrderByChart?.[chart?.id];
275
+ if (!Array.isArray(order) || order.length !== series.length) {
276
+ return series;
277
+ }
278
+ const used = /* @__PURE__ */ new Set();
279
+ const ordered = [];
280
+ order.forEach((seriesIndex) => {
281
+ if (!Number.isInteger(seriesIndex)) return;
282
+ if (seriesIndex < 0 || seriesIndex >= series.length) return;
283
+ if (used.has(seriesIndex)) return;
284
+ used.add(seriesIndex);
285
+ ordered.push(series[seriesIndex]);
286
+ });
287
+ return ordered.length === series.length ? ordered : series;
288
+ };
289
+ var CHART_TYPES = Object.freeze(Object.keys(CHART_DEFINITIONS));
290
+
291
+ // src/core/coordinateTransform.js
292
+ var safeSpan = (min, max) => Math.max(1e-12, max - min);
293
+ var createCoordinateTransform = ({
294
+ orientation = "vertical",
295
+ categoryRange,
296
+ valueRange,
297
+ plot
298
+ }) => {
299
+ const horizontal = orientation === "horizontal";
300
+ const categorySpan = safeSpan(categoryRange.min, categoryRange.max);
301
+ const valueSpan = safeSpan(valueRange.min, valueRange.max);
302
+ const categoryPixelLength = horizontal ? plot.height : plot.width;
303
+ const valuePixelLength = horizontal ? plot.width : plot.height;
304
+ const categoryRatio = (point) => horizontal ? (point.y - plot.y) / plot.height : (point.x - plot.x) / plot.width;
305
+ const valueRatio = (point) => horizontal ? (point.x - plot.x) / plot.width : 1 - (point.y - plot.y) / plot.height;
306
+ const dataToScreen = (point) => {
307
+ const category = point.category ?? point.x;
308
+ const value = point.value ?? point.y;
309
+ const categoryPosition = (category - categoryRange.min) / categorySpan;
310
+ const valuePosition = (value - valueRange.min) / valueSpan;
311
+ return horizontal ? {
312
+ x: plot.x + valuePosition * plot.width,
313
+ y: plot.y + categoryPosition * plot.height
314
+ } : {
315
+ x: plot.x + categoryPosition * plot.width,
316
+ y: plot.y + (1 - valuePosition) * plot.height
317
+ };
318
+ };
319
+ const screenToData = (point) => ({
320
+ x: categoryRange.min + categoryRatio(point) * categorySpan,
321
+ y: valueRange.min + valueRatio(point) * valueSpan
322
+ });
323
+ const categoryDragDelta = (start, current) => (horizontal ? current.y - start.y : current.x - start.x) / categoryPixelLength * categorySpan;
324
+ const valueOffsetDelta = (start, current) => horizontal ? -((current.x - start.x) / valuePixelLength) * valueSpan : (current.y - start.y) / valuePixelLength * valueSpan;
325
+ const categoryScaleDelta = (start, current) => horizontal ? current.y - start.y : current.x - start.x;
326
+ const valueScaleDelta = (start, current) => horizontal ? current.x - start.x : current.y - start.y;
327
+ const dataBoundsForRect = (rect) => {
328
+ const first = screenToData({ x: rect.left, y: rect.top });
329
+ const second = screenToData({
330
+ x: rect.left + rect.width,
331
+ y: rect.top + rect.height
332
+ });
333
+ return {
334
+ categoryMin: Math.min(first.x, second.x),
335
+ categoryMax: Math.max(first.x, second.x),
336
+ valueMin: Math.min(first.y, second.y),
337
+ valueMax: Math.max(first.y, second.y)
338
+ };
339
+ };
340
+ return Object.freeze({
341
+ categoryDragDelta,
342
+ categoryPixelLength,
343
+ categoryRatio,
344
+ categoryScaleDelta,
345
+ dataBoundsForRect,
346
+ dataToScreen,
347
+ horizontal,
348
+ orientation,
349
+ screenToData,
350
+ valueOffsetDelta,
351
+ valuePixelLength,
352
+ valueRatio,
353
+ valueScaleDelta
354
+ });
355
+ };
356
+
357
+ // src/core/renderers/webglUtils.js
358
+ var INITIAL_GPU_VERTEX_CAPACITY = 4096;
359
+ var createProgram = (gl, vertexSource, fragmentSource) => {
360
+ const compile = (type, source) => {
361
+ const shader = gl.createShader(type);
362
+ gl.shaderSource(shader, source);
363
+ gl.compileShader(shader);
364
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
365
+ throw new Error(gl.getShaderInfoLog(shader) || "Shader compile failed");
366
+ }
367
+ return shader;
368
+ };
369
+ const program = gl.createProgram();
370
+ const vertexShader = compile(gl.VERTEX_SHADER, vertexSource);
371
+ const fragmentShader = compile(gl.FRAGMENT_SHADER, fragmentSource);
372
+ gl.attachShader(program, vertexShader);
373
+ gl.attachShader(program, fragmentShader);
374
+ gl.linkProgram(program);
375
+ gl.deleteShader(vertexShader);
376
+ gl.deleteShader(fragmentShader);
377
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
378
+ throw new Error(gl.getProgramInfoLog(program) || "Program link failed");
379
+ }
380
+ return program;
381
+ };
382
+ var hexToRgb = (color) => {
383
+ const value = String(color || "#38bdf8").replace("#", "");
384
+ const normalized = value.length === 3 ? value.split("").map((char) => char + char).join("") : value.padEnd(6, "0").slice(0, 6);
385
+ const number = Number.parseInt(normalized, 16);
386
+ return [
387
+ (number >> 16 & 255) / 255,
388
+ (number >> 8 & 255) / 255,
389
+ (number & 255) / 255
390
+ ];
391
+ };
392
+ var getNextVertexCapacity = (currentCapacity, requiredCapacity) => {
393
+ let nextCapacity = Math.max(
394
+ INITIAL_GPU_VERTEX_CAPACITY,
395
+ currentCapacity || 0
396
+ );
397
+ while (nextCapacity < requiredCapacity) nextCapacity *= 2;
398
+ return nextCapacity;
399
+ };
400
+ var getDynamicBuffer = (gl, cache, key, requiredFloats) => {
401
+ let entry = cache.get(key);
402
+ if (!entry) {
403
+ entry = {
404
+ buffer: gl.createBuffer(),
405
+ gpuCapacity: 0,
406
+ vertices: new Float32Array(
407
+ getNextVertexCapacity(0, requiredFloats)
408
+ )
409
+ };
410
+ cache.set(key, entry);
411
+ }
412
+ if (entry.vertices.length < requiredFloats) {
413
+ entry.vertices = new Float32Array(
414
+ getNextVertexCapacity(entry.vertices.length, requiredFloats)
415
+ );
416
+ }
417
+ if (entry.gpuCapacity < entry.vertices.length) {
418
+ entry.gpuCapacity = entry.vertices.length;
419
+ gl.bindBuffer(gl.ARRAY_BUFFER, entry.buffer);
420
+ gl.bufferData(
421
+ gl.ARRAY_BUFFER,
422
+ entry.gpuCapacity * Float32Array.BYTES_PER_ELEMENT,
423
+ gl.DYNAMIC_DRAW
424
+ );
425
+ }
426
+ return entry;
427
+ };
428
+ var deleteBufferCache = (gl, cache) => {
429
+ cache.forEach((entry) => {
430
+ if (entry.buffer) gl.deleteBuffer(entry.buffer);
431
+ });
432
+ cache.clear();
433
+ };
434
+
435
+ // src/core/renderers/barRenderer.js
436
+ var BAR_GROUP_WIDTH_RATIO = 0.8;
437
+ var BAR_SLICE_WIDTH_RATIO = 0.9;
438
+ var BAR_VERTEX_FLOAT_STRIDE = 3;
439
+ var VERTEX_SOURCE = `#version 300 es
440
+ in vec2 a_unit;
441
+ in vec3 a_bar;
442
+ uniform vec2 u_resolution;
443
+ uniform vec4 u_rect;
444
+ uniform vec2 u_categoryRange;
445
+ uniform vec2 u_valueRange;
446
+ uniform float u_baseline;
447
+ uniform float u_seriesIndex;
448
+ uniform float u_seriesCount;
449
+ uniform int u_orientation;
450
+
451
+ void main() {
452
+ float groupWidth = a_bar.z * ${BAR_GROUP_WIDTH_RATIO.toFixed(1)};
453
+ float sliceWidth = groupWidth / max(1.0, u_seriesCount);
454
+ float center = a_bar.x - groupWidth * 0.5
455
+ + sliceWidth * (u_seriesIndex + 0.5);
456
+ float halfWidth = sliceWidth * ${(BAR_SLICE_WIDTH_RATIO / 2).toFixed(2)};
457
+ float category = mix(center - halfWidth, center + halfWidth, a_unit.x);
458
+ float value = mix(u_baseline, a_bar.y, a_unit.y);
459
+ float categoryDenom = max(
460
+ 0.000000000001,
461
+ abs(u_categoryRange.y - u_categoryRange.x)
462
+ );
463
+ float valueDenom = max(
464
+ 0.000000000001,
465
+ abs(u_valueRange.y - u_valueRange.x)
466
+ );
467
+ float categoryRatio = (category - u_categoryRange.x) / categoryDenom;
468
+ float valueRatio = (value - u_valueRange.x) / valueDenom;
469
+ vec2 px = u_orientation == 0
470
+ ? vec2(
471
+ u_rect.x + categoryRatio * u_rect.z,
472
+ u_rect.y + (1.0 - valueRatio) * u_rect.w
473
+ )
474
+ : vec2(
475
+ u_rect.x + valueRatio * u_rect.z,
476
+ u_rect.y + categoryRatio * u_rect.w
477
+ );
478
+ vec2 clip = (px / u_resolution) * 2.0 - 1.0;
479
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
480
+ }
481
+ `;
482
+ var FRAGMENT_SOURCE = `#version 300 es
483
+ precision highp float;
484
+ uniform vec3 u_color;
485
+ out vec4 outColor;
486
+
487
+ void main() {
488
+ outColor = vec4(u_color, 1.0);
489
+ }
490
+ `;
491
+ var getBarCategoryInterval = ({
492
+ visiblePoints,
493
+ index,
494
+ categoryMin,
495
+ categoryMax
496
+ }) => {
497
+ const count = visiblePoints.pointCount;
498
+ const category = visiblePoints.x[index];
499
+ const fallbackInterval = (categoryMax - categoryMin) / Math.max(10, count || 1);
500
+ const previousGap = index > 0 ? category - visiblePoints.x[index - 1] : Infinity;
501
+ const nextGap = index + 1 < count ? visiblePoints.x[index + 1] - category : Infinity;
502
+ let interval = Math.min(
503
+ previousGap > 0 ? previousGap : Infinity,
504
+ nextGap > 0 ? nextGap : Infinity
505
+ );
506
+ if (!Number.isFinite(interval)) {
507
+ interval = Number.isFinite(previousGap) && previousGap > 0 ? previousGap : Number.isFinite(nextGap) && nextGap > 0 ? nextGap : fallbackInterval;
508
+ }
509
+ return Math.max(Number.EPSILON, interval || fallbackInterval || 1);
510
+ };
511
+ var getGroupedBarCategory = ({
512
+ visiblePoints,
513
+ pointIndex,
514
+ seriesIndex,
515
+ seriesCount,
516
+ categoryMin,
517
+ categoryMax
518
+ }) => {
519
+ const category = visiblePoints.x[pointIndex];
520
+ const interval = getBarCategoryInterval({
521
+ visiblePoints,
522
+ index: pointIndex,
523
+ categoryMin,
524
+ categoryMax
525
+ });
526
+ const groupWidth = interval * BAR_GROUP_WIDTH_RATIO;
527
+ const sliceWidth = groupWidth / Math.max(1, seriesCount);
528
+ return category - groupWidth / 2 + sliceWidth * (seriesIndex + 0.5);
529
+ };
530
+ var fillBarInstances = ({
531
+ vertices,
532
+ visiblePoints,
533
+ categoryMin,
534
+ categoryMax
535
+ }) => {
536
+ for (let index = 0; index < visiblePoints.pointCount; index += 1) {
537
+ const offset = index * BAR_VERTEX_FLOAT_STRIDE;
538
+ vertices[offset] = visiblePoints.x[index];
539
+ vertices[offset + 1] = visiblePoints.y[index];
540
+ vertices[offset + 2] = getBarCategoryInterval({
541
+ visiblePoints,
542
+ index,
543
+ categoryMin,
544
+ categoryMax
545
+ });
546
+ }
547
+ return visiblePoints.pointCount;
548
+ };
549
+ var createBarRenderer = (gl) => {
550
+ const program = createProgram(gl, VERTEX_SOURCE, FRAGMENT_SOURCE);
551
+ const quadBuffer = gl.createBuffer();
552
+ const buffers = /* @__PURE__ */ new Map();
553
+ gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
554
+ gl.bufferData(
555
+ gl.ARRAY_BUFFER,
556
+ new Float32Array([
557
+ 0,
558
+ 0,
559
+ 1,
560
+ 0,
561
+ 0,
562
+ 1,
563
+ 0,
564
+ 1,
565
+ 1,
566
+ 0,
567
+ 1,
568
+ 1
569
+ ]),
570
+ gl.STATIC_DRAW
571
+ );
572
+ const locations = Object.freeze({
573
+ aBar: gl.getAttribLocation(program, "a_bar"),
574
+ aUnit: gl.getAttribLocation(program, "a_unit"),
575
+ uBaseline: gl.getUniformLocation(program, "u_baseline"),
576
+ uCategoryRange: gl.getUniformLocation(program, "u_categoryRange"),
577
+ uColor: gl.getUniformLocation(program, "u_color"),
578
+ uOrientation: gl.getUniformLocation(program, "u_orientation"),
579
+ uRect: gl.getUniformLocation(program, "u_rect"),
580
+ uResolution: gl.getUniformLocation(program, "u_resolution"),
581
+ uSeriesCount: gl.getUniformLocation(program, "u_seriesCount"),
582
+ uSeriesIndex: gl.getUniformLocation(program, "u_seriesIndex"),
583
+ uValueRange: gl.getUniformLocation(program, "u_valueRange")
584
+ });
585
+ return {
586
+ draw({
587
+ categoryPixelLength,
588
+ chart,
589
+ descriptor,
590
+ pixelHeight,
591
+ pixelWidth,
592
+ scaledPlot,
593
+ seriesOrderByChart,
594
+ state,
595
+ yRange
596
+ }) {
597
+ gl.disable(gl.BLEND);
598
+ gl.useProgram(program);
599
+ gl.enableVertexAttribArray(locations.aUnit);
600
+ gl.enableVertexAttribArray(locations.aBar);
601
+ gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
602
+ gl.vertexAttribPointer(locations.aUnit, 2, gl.FLOAT, false, 0, 0);
603
+ gl.vertexAttribDivisor(locations.aUnit, 0);
604
+ gl.uniform2f(locations.uResolution, pixelWidth, pixelHeight);
605
+ gl.uniform4f(
606
+ locations.uRect,
607
+ scaledPlot.x,
608
+ scaledPlot.y,
609
+ scaledPlot.width,
610
+ scaledPlot.height
611
+ );
612
+ gl.uniform2f(
613
+ locations.uCategoryRange,
614
+ state.xMin,
615
+ state.xMax
616
+ );
617
+ gl.uniform2f(
618
+ locations.uValueRange,
619
+ yRange.minY,
620
+ yRange.maxY
621
+ );
622
+ gl.uniform1f(locations.uBaseline, 0);
623
+ gl.uniform1i(
624
+ locations.uOrientation,
625
+ descriptor.orientation === "horizontal" ? 1 : 0
626
+ );
627
+ const orderedSeries = getOrderedSeries(chart, seriesOrderByChart);
628
+ gl.uniform1f(locations.uSeriesCount, Math.max(1, orderedSeries.length));
629
+ orderedSeries.forEach((series, seriesIndex) => {
630
+ const visiblePoints = series.getVisiblePoints(
631
+ state.xMin,
632
+ state.xMax,
633
+ categoryPixelLength
634
+ );
635
+ if (!visiblePoints.pointCount) return;
636
+ const requiredFloats = visiblePoints.pointCount * BAR_VERTEX_FLOAT_STRIDE;
637
+ const bufferEntry = getDynamicBuffer(
638
+ gl,
639
+ buffers,
640
+ series.id,
641
+ requiredFloats
642
+ );
643
+ const instanceCount = fillBarInstances({
644
+ vertices: bufferEntry.vertices,
645
+ visiblePoints,
646
+ categoryMin: state.xMin,
647
+ categoryMax: state.xMax
648
+ });
649
+ const [r, g, b] = hexToRgb(series.color);
650
+ gl.uniform3f(locations.uColor, r, g, b);
651
+ gl.uniform1f(locations.uSeriesIndex, seriesIndex);
652
+ gl.bindBuffer(gl.ARRAY_BUFFER, bufferEntry.buffer);
653
+ gl.vertexAttribPointer(
654
+ locations.aBar,
655
+ BAR_VERTEX_FLOAT_STRIDE,
656
+ gl.FLOAT,
657
+ false,
658
+ 0,
659
+ 0
660
+ );
661
+ gl.vertexAttribDivisor(locations.aBar, 1);
662
+ gl.bufferSubData(
663
+ gl.ARRAY_BUFFER,
664
+ 0,
665
+ bufferEntry.vertices,
666
+ 0,
667
+ instanceCount * BAR_VERTEX_FLOAT_STRIDE
668
+ );
669
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, instanceCount);
670
+ });
671
+ gl.vertexAttribDivisor(locations.aBar, 0);
672
+ return { seriesEndpoints: /* @__PURE__ */ new Map() };
673
+ },
674
+ getTooltipCategory(payload) {
675
+ return getGroupedBarCategory(payload);
676
+ },
677
+ destroy() {
678
+ deleteBufferCache(gl, buffers);
679
+ gl.deleteBuffer(quadBuffer);
680
+ gl.deleteProgram(program);
681
+ }
682
+ };
683
+ };
684
+
685
+ // src/core/lodSeries.js
686
+ var DEFAULT_MAX_LEVELS = 18;
687
+ var toTypedArray = (value, Type) => {
688
+ if (value instanceof Type) return value;
689
+ if (ArrayBuffer.isView(value)) return new Type(value);
690
+ if (Array.isArray(value)) return new Type(value);
691
+ return new Type(0);
692
+ };
693
+ var lowerBound = (array, value) => {
694
+ let lo = 0;
695
+ let hi = array.length;
696
+ while (lo < hi) {
697
+ const mid = lo + hi >> 1;
698
+ if (array[mid] < value) lo = mid + 1;
699
+ else hi = mid;
700
+ }
701
+ return lo;
702
+ };
703
+ var upperBound = (array, value) => {
704
+ let lo = 0;
705
+ let hi = array.length;
706
+ while (lo < hi) {
707
+ const mid = lo + hi >> 1;
708
+ if (array[mid] <= value) lo = mid + 1;
709
+ else hi = mid;
710
+ }
711
+ return lo;
712
+ };
713
+ var pushUniqueSorted = (indices, index) => {
714
+ if (index < 0) return;
715
+ if (indices.includes(index)) return;
716
+ indices.push(index);
717
+ };
718
+ var buildLod = (rawX, rawY, bucketSize, startIndex = 0, endIndex = rawX.length) => {
719
+ if (bucketSize <= 1 || rawX.length <= bucketSize) {
720
+ return { x: rawX, y: rawY, bucketSize: 1 };
721
+ }
722
+ const alignedStart = Math.max(0, Math.floor(startIndex / bucketSize) * bucketSize);
723
+ const alignedEnd = Math.min(rawX.length, endIndex);
724
+ const estimated = Math.ceil((alignedEnd - alignedStart) / bucketSize) * 4;
725
+ const x = new Float64Array(estimated);
726
+ const y = new Float32Array(estimated);
727
+ let out = 0;
728
+ for (let start = alignedStart; start < alignedEnd; start += bucketSize) {
729
+ const end = Math.min(alignedEnd, start + bucketSize);
730
+ let minIndex = start;
731
+ let maxIndex = start;
732
+ for (let i = start + 1; i < end; i += 1) {
733
+ if (rawY[i] < rawY[minIndex]) minIndex = i;
734
+ if (rawY[i] > rawY[maxIndex]) maxIndex = i;
735
+ }
736
+ const indices = [];
737
+ pushUniqueSorted(indices, start);
738
+ pushUniqueSorted(indices, minIndex);
739
+ pushUniqueSorted(indices, maxIndex);
740
+ pushUniqueSorted(indices, end - 1);
741
+ indices.sort((a, b) => a - b);
742
+ for (let i = 0; i < indices.length; i += 1) {
743
+ const sourceIndex = indices[i];
744
+ x[out] = rawX[sourceIndex];
745
+ y[out] = rawY[sourceIndex];
746
+ out += 1;
747
+ }
748
+ }
749
+ return {
750
+ x: x.subarray(0, out),
751
+ y: y.subarray(0, out),
752
+ bucketSize
753
+ };
754
+ };
755
+ var concatTyped = (Type, left, right) => {
756
+ if (!left?.length) return right;
757
+ if (!right?.length) return left;
758
+ const out = new Type(left.length + right.length);
759
+ out.set(left, 0);
760
+ out.set(right, left.length);
761
+ return out;
762
+ };
763
+ var LodSeries = class {
764
+ constructor({ id, name, color, x, y, maxLevels = DEFAULT_MAX_LEVELS }) {
765
+ this.id = id;
766
+ this.name = name || id;
767
+ this.color = color || "#38bdf8";
768
+ this.maxLevels = maxLevels;
769
+ const initialX = toTypedArray(x, Float64Array);
770
+ const initialY = toTypedArray(y, Float32Array);
771
+ this._length = Math.min(initialX.length, initialY.length);
772
+ const capacity = Math.max(1, this._length);
773
+ this.rawX = new Float64Array(capacity);
774
+ this.rawY = new Float32Array(capacity);
775
+ this.rawX.set(initialX.subarray(0, this._length));
776
+ this.rawY.set(initialY.subarray(0, this._length));
777
+ this.levels = [];
778
+ this.rebuildLevels();
779
+ }
780
+ get length() {
781
+ return this._length;
782
+ }
783
+ append(xValues, yValues) {
784
+ const nextX = toTypedArray(xValues, Float64Array);
785
+ const nextY = toTypedArray(yValues, Float32Array);
786
+ const appendLength = Math.min(nextX.length, nextY.length);
787
+ if (appendLength === 0) return;
788
+ const currentLength = this.length;
789
+ this.ensureRawCapacity(currentLength + appendLength);
790
+ this.rawX.set(nextX.subarray(0, appendLength), currentLength);
791
+ this.rawY.set(nextY.subarray(0, appendLength), currentLength);
792
+ this._length = currentLength + appendLength;
793
+ this.markLevelsDirty(currentLength);
794
+ }
795
+ rebuildLevels() {
796
+ const rawX = this.rawX.subarray(0, this.length);
797
+ const rawY = this.rawY.subarray(0, this.length);
798
+ this.levels = [{ x: rawX, y: rawY, bucketSize: 1 }];
799
+ let bucketSize = 4;
800
+ while (this.levels.length < this.maxLevels && bucketSize < Math.max(8, rawX.length)) {
801
+ this.levels.push(buildLod(rawX, rawY, bucketSize));
802
+ bucketSize *= 4;
803
+ }
804
+ }
805
+ ensureRawCapacity(requiredLength) {
806
+ if (requiredLength <= this.rawX.length && requiredLength <= this.rawY.length) {
807
+ return;
808
+ }
809
+ const nextCapacity = Math.max(requiredLength, Math.ceil(this.rawX.length * 1.5));
810
+ const rawX = new Float64Array(nextCapacity);
811
+ const rawY = new Float32Array(nextCapacity);
812
+ rawX.set(this.rawX.subarray(0, this.length));
813
+ rawY.set(this.rawY.subarray(0, this.length));
814
+ this.rawX = rawX;
815
+ this.rawY = rawY;
816
+ }
817
+ markLevelsDirty(previousLength) {
818
+ const rawX = this.rawX.subarray(0, this.length);
819
+ const rawY = this.rawY.subarray(0, this.length);
820
+ this.levels[0] = { x: rawX, y: rawY, bucketSize: 1 };
821
+ let bucketSize = this.levels.length > 0 ? this.levels[this.levels.length - 1].bucketSize * 4 : 4;
822
+ while (this.levels.length < this.maxLevels && bucketSize < Math.max(8, rawX.length)) {
823
+ this.levels.push(buildLod(rawX, rawY, bucketSize));
824
+ bucketSize *= 4;
825
+ }
826
+ for (let i = 1; i < this.levels.length; i += 1) {
827
+ const level = this.levels[i];
828
+ const dirtyRawStart = Math.floor(previousLength / level.bucketSize) * level.bucketSize;
829
+ level.dirtyFrom = Math.min(level.dirtyFrom ?? dirtyRawStart, dirtyRawStart);
830
+ }
831
+ }
832
+ ensureLevel(index) {
833
+ const level = this.levels[index];
834
+ if (!level || index === 0 || level.dirtyFrom == null) return level;
835
+ const rawX = this.rawX.subarray(0, this.length);
836
+ const rawY = this.rawY.subarray(0, this.length);
837
+ const dirtyRawStart = Math.floor(level.dirtyFrom / level.bucketSize) * level.bucketSize;
838
+ const cutoffX = rawX[dirtyRawStart] ?? rawX[0] ?? 0;
839
+ const keepCount = lowerBound(level.x, cutoffX);
840
+ const tail = buildLod(rawX, rawY, level.bucketSize, dirtyRawStart, rawX.length);
841
+ this.levels[index] = {
842
+ x: concatTyped(Float64Array, level.x.subarray(0, keepCount), tail.x),
843
+ y: concatTyped(Float32Array, level.y.subarray(0, keepCount), tail.y),
844
+ bucketSize: level.bucketSize
845
+ };
846
+ return this.levels[index];
847
+ }
848
+ selectLevel(xMin, xMax, pixelWidth, targetPointsPerPixel = 2.5) {
849
+ const rawX = this.levels[0].x;
850
+ if (rawX.length === 0) return this.levels[0];
851
+ const from = lowerBound(rawX, xMin);
852
+ const to = upperBound(rawX, xMax);
853
+ const visibleRawPoints = Math.max(0, to - from);
854
+ const targetPoints = Math.max(32, pixelWidth * targetPointsPerPixel);
855
+ const desiredBucket = Math.max(1, visibleRawPoints / targetPoints);
856
+ let selectedIndex = 0;
857
+ for (let i = 1; i < this.levels.length; i += 1) {
858
+ if (this.levels[i].bucketSize <= desiredBucket) {
859
+ selectedIndex = i;
860
+ } else {
861
+ break;
862
+ }
863
+ }
864
+ return this.ensureLevel(selectedIndex);
865
+ }
866
+ getVisiblePoints(xMin, xMax, pixelWidth) {
867
+ const level = this.selectLevel(xMin, xMax, pixelWidth);
868
+ const start = Math.max(0, lowerBound(level.x, xMin) - 1);
869
+ const end = Math.min(level.x.length, upperBound(level.x, xMax) + 1);
870
+ return {
871
+ x: level.x.subarray(start, end),
872
+ y: level.y.subarray(start, end),
873
+ bucketSize: level.bucketSize,
874
+ pointCount: Math.max(0, end - start)
875
+ };
876
+ }
877
+ };
878
+ var LineSeries = class extends LodSeries {
879
+ constructor(options) {
880
+ super(options);
881
+ this.type = "line";
882
+ }
883
+ };
884
+ var BarSeries = class extends LodSeries {
885
+ constructor({ orientation = "vertical", ...options }) {
886
+ super(options);
887
+ if (orientation !== "vertical" && orientation !== "horizontal") {
888
+ throw new TypeError(
889
+ 'BarSeries orientation must be "vertical" or "horizontal"'
890
+ );
891
+ }
892
+ this.type = "bar";
893
+ this.orientation = orientation;
894
+ }
895
+ };
896
+ var createLineSeries = (options) => new LineSeries(options);
897
+ var createSeries = createLineSeries;
898
+ var createBarSeries = (options) => new BarSeries(options);
899
+
900
+ // src/core/renderers/lineRenderer.js
901
+ var AA_VERTEX_FLOAT_STRIDE = 6;
902
+ var AA_LINE_WIDTH_PX = 1.5;
903
+ var AA_EDGE_WIDTH_PX = 1;
904
+ var DASH_LENGTH_PX = 7;
905
+ var DASH_GAP_PX = 5;
906
+ var LINE_VERTEX_SOURCE = `#version 300 es
907
+ in vec2 a_xy;
908
+ uniform vec2 u_resolution;
909
+ uniform vec4 u_rect;
910
+ uniform vec2 u_xRange;
911
+ uniform vec2 u_yRange;
912
+
913
+ void main() {
914
+ float xDenom = max(0.000000000001, abs(u_xRange.y - u_xRange.x));
915
+ float yDenom = max(0.000000000001, abs(u_yRange.y - u_yRange.x));
916
+ float nx = (a_xy.x - u_xRange.x) / xDenom;
917
+ float ny = (a_xy.y - u_yRange.x) / yDenom;
918
+ float px = u_rect.x + nx * u_rect.z;
919
+ float py = u_rect.y + (1.0 - ny) * u_rect.w;
920
+ vec2 clip = (vec2(px, py) / u_resolution) * 2.0 - 1.0;
921
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
922
+ }
923
+ `;
924
+ var LINE_FRAGMENT_SOURCE = `#version 300 es
925
+ precision highp float;
926
+ uniform vec3 u_color;
927
+ out vec4 outColor;
928
+
929
+ void main() {
930
+ outColor = vec4(u_color, 1.0);
931
+ }
932
+ `;
933
+ var AA_VERTEX_SOURCE = `#version 300 es
934
+ in vec2 a_start;
935
+ in vec2 a_end;
936
+ in float a_side;
937
+ in float a_along;
938
+ uniform vec2 u_resolution;
939
+ uniform vec4 u_rect;
940
+ uniform vec2 u_xRange;
941
+ uniform vec2 u_yRange;
942
+ uniform float u_lineHalfWidth;
943
+ uniform float u_edgeWidth;
944
+ out float v_side;
945
+
946
+ vec2 toPixel(vec2 value) {
947
+ float xDenom = max(0.000000000001, abs(u_xRange.y - u_xRange.x));
948
+ float yDenom = max(0.000000000001, abs(u_yRange.y - u_yRange.x));
949
+ float nx = (value.x - u_xRange.x) / xDenom;
950
+ float ny = (value.y - u_yRange.x) / yDenom;
951
+ return vec2(
952
+ u_rect.x + nx * u_rect.z,
953
+ u_rect.y + (1.0 - ny) * u_rect.w
954
+ );
955
+ }
956
+
957
+ void main() {
958
+ vec2 startPx = toPixel(a_start);
959
+ vec2 endPx = toPixel(a_end);
960
+ vec2 delta = endPx - startPx;
961
+ float segmentLength = max(0.000001, length(delta));
962
+ vec2 direction = delta / segmentLength;
963
+ vec2 normal = vec2(-direction.y, direction.x);
964
+ float expand = u_lineHalfWidth + u_edgeWidth;
965
+ float cap = min(expand, segmentLength * 0.5);
966
+ vec2 px = mix(startPx, endPx, a_along)
967
+ + direction * ((a_along * 2.0 - 1.0) * cap)
968
+ + normal * a_side * expand;
969
+ vec2 clip = (px / u_resolution) * 2.0 - 1.0;
970
+ v_side = a_side;
971
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
972
+ }
973
+ `;
974
+ var AA_FRAGMENT_SOURCE = `#version 300 es
975
+ precision highp float;
976
+ uniform vec3 u_color;
977
+ uniform float u_lineHalfWidth;
978
+ uniform float u_edgeWidth;
979
+ in float v_side;
980
+ out vec4 outColor;
981
+
982
+ void main() {
983
+ float expand = u_lineHalfWidth + u_edgeWidth;
984
+ float distanceFromCenter = abs(v_side) * expand;
985
+ float alpha = 1.0 - smoothstep(u_lineHalfWidth, expand, distanceFromCenter);
986
+ outColor = vec4(u_color, alpha);
987
+ }
988
+ `;
989
+ var fillSeriesPoints = ({
990
+ vertices,
991
+ visiblePoints,
992
+ pointCount,
993
+ animatedPoint
994
+ }) => {
995
+ for (let index = 0; index < pointCount; index += 1) {
996
+ const source = animatedPoint && index === animatedPoint.index ? animatedPoint : { x: visiblePoints.x[index], y: visiblePoints.y[index] };
997
+ vertices[index * 2] = source.x;
998
+ vertices[index * 2 + 1] = source.y;
999
+ }
1000
+ };
1001
+ var getSinglePointSegment = ({ series, state, yRange, plot }) => {
1002
+ if (series.length !== 1) return null;
1003
+ const x = series.rawX[0];
1004
+ const y = series.rawY[0];
1005
+ if (x < state.xMin || x > state.xMax || y < yRange.minY || y > yRange.maxY) {
1006
+ return null;
1007
+ }
1008
+ const halfWidth = (state.xMax - state.xMin) / Math.max(1, plot.width) * 3;
1009
+ return {
1010
+ x: new Float64Array([x - halfWidth, x + halfWidth]),
1011
+ y: new Float32Array([y, y]),
1012
+ bucketSize: 1,
1013
+ pointCount: 2,
1014
+ endpoint: { x, y }
1015
+ };
1016
+ };
1017
+ var writeAntialiasVertex = (vertices, offset, start, end, side, along) => {
1018
+ vertices[offset] = start.x;
1019
+ vertices[offset + 1] = start.y;
1020
+ vertices[offset + 2] = end.x;
1021
+ vertices[offset + 3] = end.y;
1022
+ vertices[offset + 4] = side;
1023
+ vertices[offset + 5] = along;
1024
+ };
1025
+ var getSeriesPoint = (visiblePoints, animatedPoint, index) => animatedPoint && index === animatedPoint.index ? { x: animatedPoint.x, y: animatedPoint.y } : { x: visiblePoints.x[index], y: visiblePoints.y[index] };
1026
+ var fillAntialiasSeriesSegments = ({
1027
+ vertices,
1028
+ visiblePoints,
1029
+ pointCount,
1030
+ animatedPoint
1031
+ }) => {
1032
+ let offset = 0;
1033
+ for (let index = 0; index < pointCount - 1; index += 1) {
1034
+ const start = getSeriesPoint(visiblePoints, animatedPoint, index);
1035
+ const end = getSeriesPoint(visiblePoints, animatedPoint, index + 1);
1036
+ [
1037
+ [-1, 0],
1038
+ [1, 0],
1039
+ [-1, 1],
1040
+ [-1, 1],
1041
+ [1, 0],
1042
+ [1, 1]
1043
+ ].forEach(([side, along]) => {
1044
+ writeAntialiasVertex(vertices, offset, start, end, side, along);
1045
+ offset += AA_VERTEX_FLOAT_STRIDE;
1046
+ });
1047
+ }
1048
+ return offset / AA_VERTEX_FLOAT_STRIDE;
1049
+ };
1050
+ var projectDataToPixel = ({ x, y, state, yRange, plot }) => ({
1051
+ x: plot.x + (x - state.xMin) / (state.xMax - state.xMin) * plot.width,
1052
+ y: plot.y + (yRange.maxY - y) / (yRange.maxY - yRange.minY) * plot.height
1053
+ });
1054
+ var fillDashedSeriesSegments = ({
1055
+ vertices,
1056
+ visiblePoints,
1057
+ pointCount,
1058
+ state,
1059
+ yRange,
1060
+ plot
1061
+ }) => {
1062
+ let offset = 0;
1063
+ let distance = 0;
1064
+ const dashTotal = DASH_LENGTH_PX + DASH_GAP_PX;
1065
+ for (let index = 0; index < pointCount - 1; index += 1) {
1066
+ const start = { x: visiblePoints.x[index], y: visiblePoints.y[index] };
1067
+ const end = {
1068
+ x: visiblePoints.x[index + 1],
1069
+ y: visiblePoints.y[index + 1]
1070
+ };
1071
+ const startPx = projectDataToPixel({ ...start, state, yRange, plot });
1072
+ const endPx = projectDataToPixel({ ...end, state, yRange, plot });
1073
+ const pixelLength = Math.hypot(endPx.x - startPx.x, endPx.y - startPx.y);
1074
+ if (!Number.isFinite(pixelLength) || pixelLength <= 0) continue;
1075
+ let consumed = 0;
1076
+ while (consumed < pixelLength) {
1077
+ const phase = distance % dashTotal;
1078
+ const boundary = phase < DASH_LENGTH_PX ? DASH_LENGTH_PX - phase : dashTotal - phase;
1079
+ const step = Math.min(boundary, pixelLength - consumed);
1080
+ if (phase < DASH_LENGTH_PX) {
1081
+ if (offset + 4 > vertices.length) return offset / 2;
1082
+ const t0 = consumed / pixelLength;
1083
+ const t1 = (consumed + step) / pixelLength;
1084
+ vertices[offset] = start.x + (end.x - start.x) * t0;
1085
+ vertices[offset + 1] = start.y + (end.y - start.y) * t0;
1086
+ vertices[offset + 2] = start.x + (end.x - start.x) * t1;
1087
+ vertices[offset + 3] = start.y + (end.y - start.y) * t1;
1088
+ offset += 4;
1089
+ }
1090
+ consumed += step;
1091
+ distance += step;
1092
+ }
1093
+ }
1094
+ return offset / 2;
1095
+ };
1096
+ var normalizeMovingAverage = (movingAverage) => {
1097
+ if (!movingAverage?.enabled) return null;
1098
+ return {
1099
+ enabled: true,
1100
+ period: Number.isFinite(Number(movingAverage.period)) ? Math.max(1, Math.round(Number(movingAverage.period))) : 21,
1101
+ type: movingAverage.type === "sma" ? "sma" : "ema"
1102
+ };
1103
+ };
1104
+ var calculateMovingAverageChunk = ({
1105
+ sourceY,
1106
+ startIndex,
1107
+ period,
1108
+ type,
1109
+ previousAverage
1110
+ }) => {
1111
+ const out = new Float32Array(Math.max(0, sourceY.length - startIndex));
1112
+ if (!out.length) return out;
1113
+ if (type === "sma") {
1114
+ let sum = 0;
1115
+ const firstWindowStart = Math.max(0, startIndex - period + 1);
1116
+ for (let index = firstWindowStart; index < startIndex; index += 1) {
1117
+ sum += sourceY[index];
1118
+ }
1119
+ for (let index = startIndex; index < sourceY.length; index += 1) {
1120
+ sum += sourceY[index];
1121
+ const removeIndex = index - period;
1122
+ if (removeIndex >= firstWindowStart) sum -= sourceY[removeIndex];
1123
+ out[index - startIndex] = sum / Math.min(period, index + 1);
1124
+ }
1125
+ return out;
1126
+ }
1127
+ const multiplier = 2 / (period + 1);
1128
+ let previous = Number.isFinite(previousAverage) ? previousAverage : sourceY[startIndex];
1129
+ for (let index = startIndex; index < sourceY.length; index += 1) {
1130
+ previous = sourceY[index] * multiplier + previous * (1 - multiplier);
1131
+ out[index - startIndex] = previous;
1132
+ }
1133
+ return out;
1134
+ };
1135
+ var getMovingAverageSeriesForChart = ({
1136
+ chart,
1137
+ movingAverage,
1138
+ cache
1139
+ }) => {
1140
+ const normalized = normalizeMovingAverage(movingAverage);
1141
+ if (!normalized) return [];
1142
+ return chart.series.map((series) => {
1143
+ if (!series.length) return null;
1144
+ const key = `${series.id}::ma:${normalized.type}:${normalized.period}`;
1145
+ const cached = cache.get(key);
1146
+ if (cached && cached.sourceSeries === series && cached.sourceLength <= series.length) {
1147
+ const appendStart = cached.sourceLength;
1148
+ if (appendStart < series.length) {
1149
+ const appendedY = calculateMovingAverageChunk({
1150
+ sourceY: series.rawY.subarray(0, series.length),
1151
+ startIndex: appendStart,
1152
+ period: normalized.period,
1153
+ type: normalized.type,
1154
+ previousAverage: appendStart > 0 ? cached.series.rawY[appendStart - 1] : void 0
1155
+ });
1156
+ cached.series.append(
1157
+ series.rawX.subarray(appendStart, series.length),
1158
+ appendedY
1159
+ );
1160
+ cached.sourceLength = series.length;
1161
+ }
1162
+ cached.series.color = series.color;
1163
+ cached.series.name = `${series.name} ${normalized.type.toUpperCase()} ${normalized.period}`;
1164
+ return cached.series;
1165
+ }
1166
+ const movingY = calculateMovingAverageChunk({
1167
+ sourceY: series.rawY.subarray(0, series.length),
1168
+ startIndex: 0,
1169
+ period: normalized.period,
1170
+ type: normalized.type
1171
+ });
1172
+ const movingSeries = createLineSeries({
1173
+ id: key,
1174
+ name: `${series.name} ${normalized.type.toUpperCase()} ${normalized.period}`,
1175
+ color: series.color,
1176
+ x: series.rawX.subarray(0, series.length),
1177
+ y: movingY
1178
+ });
1179
+ cache.set(key, {
1180
+ sourceSeries: series,
1181
+ sourceLength: series.length,
1182
+ series: movingSeries
1183
+ });
1184
+ return movingSeries;
1185
+ }).filter(Boolean);
1186
+ };
1187
+ var getLineLocations = (gl, program) => Object.freeze({
1188
+ aXy: gl.getAttribLocation(program, "a_xy"),
1189
+ uColor: gl.getUniformLocation(program, "u_color"),
1190
+ uRect: gl.getUniformLocation(program, "u_rect"),
1191
+ uResolution: gl.getUniformLocation(program, "u_resolution"),
1192
+ uXRange: gl.getUniformLocation(program, "u_xRange"),
1193
+ uYRange: gl.getUniformLocation(program, "u_yRange")
1194
+ });
1195
+ var createLineRenderer = (gl) => {
1196
+ const program = createProgram(
1197
+ gl,
1198
+ LINE_VERTEX_SOURCE,
1199
+ LINE_FRAGMENT_SOURCE
1200
+ );
1201
+ const antialiasProgram = createProgram(
1202
+ gl,
1203
+ AA_VERTEX_SOURCE,
1204
+ AA_FRAGMENT_SOURCE
1205
+ );
1206
+ const locations = getLineLocations(gl, program);
1207
+ const aaLocations = Object.freeze({
1208
+ aAlong: gl.getAttribLocation(antialiasProgram, "a_along"),
1209
+ aEnd: gl.getAttribLocation(antialiasProgram, "a_end"),
1210
+ aSide: gl.getAttribLocation(antialiasProgram, "a_side"),
1211
+ aStart: gl.getAttribLocation(antialiasProgram, "a_start"),
1212
+ uColor: gl.getUniformLocation(antialiasProgram, "u_color"),
1213
+ uEdgeWidth: gl.getUniformLocation(antialiasProgram, "u_edgeWidth"),
1214
+ uLineHalfWidth: gl.getUniformLocation(antialiasProgram, "u_lineHalfWidth"),
1215
+ uRect: gl.getUniformLocation(antialiasProgram, "u_rect"),
1216
+ uResolution: gl.getUniformLocation(antialiasProgram, "u_resolution"),
1217
+ uXRange: gl.getUniformLocation(antialiasProgram, "u_xRange"),
1218
+ uYRange: gl.getUniformLocation(antialiasProgram, "u_yRange")
1219
+ });
1220
+ const buffers = /* @__PURE__ */ new Map();
1221
+ const antialiasBuffers = /* @__PURE__ */ new Map();
1222
+ const movingAverageCache = /* @__PURE__ */ new Map();
1223
+ const setCommonUniforms = (activeLocations, pixelWidth, pixelHeight, scaledPlot, state, yRange) => {
1224
+ gl.uniform2f(activeLocations.uResolution, pixelWidth, pixelHeight);
1225
+ gl.uniform4f(
1226
+ activeLocations.uRect,
1227
+ scaledPlot.x,
1228
+ scaledPlot.y,
1229
+ scaledPlot.width,
1230
+ scaledPlot.height
1231
+ );
1232
+ gl.uniform2f(activeLocations.uXRange, state.xMin, state.xMax);
1233
+ gl.uniform2f(activeLocations.uYRange, yRange.minY, yRange.maxY);
1234
+ };
1235
+ return {
1236
+ draw({
1237
+ antialiasLines,
1238
+ chart,
1239
+ descriptor,
1240
+ dpr,
1241
+ getAppendAnimatedPoint,
1242
+ movingAverage,
1243
+ now,
1244
+ pixelHeight,
1245
+ pixelWidth,
1246
+ plot,
1247
+ scaledPlot,
1248
+ seriesOrderByChart,
1249
+ state,
1250
+ yRange
1251
+ }) {
1252
+ const useAntialias = Boolean(antialiasLines);
1253
+ const activeProgram = useAntialias ? antialiasProgram : program;
1254
+ const activeLocations = useAntialias ? aaLocations : locations;
1255
+ gl.useProgram(activeProgram);
1256
+ setCommonUniforms(
1257
+ activeLocations,
1258
+ pixelWidth,
1259
+ pixelHeight,
1260
+ scaledPlot,
1261
+ state,
1262
+ yRange
1263
+ );
1264
+ if (useAntialias) {
1265
+ gl.enable(gl.BLEND);
1266
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
1267
+ gl.enableVertexAttribArray(aaLocations.aStart);
1268
+ gl.enableVertexAttribArray(aaLocations.aEnd);
1269
+ gl.enableVertexAttribArray(aaLocations.aSide);
1270
+ gl.enableVertexAttribArray(aaLocations.aAlong);
1271
+ gl.uniform1f(
1272
+ aaLocations.uLineHalfWidth,
1273
+ AA_LINE_WIDTH_PX * dpr / 2
1274
+ );
1275
+ gl.uniform1f(aaLocations.uEdgeWidth, AA_EDGE_WIDTH_PX * dpr);
1276
+ } else {
1277
+ gl.disable(gl.BLEND);
1278
+ gl.enableVertexAttribArray(locations.aXy);
1279
+ }
1280
+ const seriesEndpoints = /* @__PURE__ */ new Map();
1281
+ const hideBaseSeries = descriptor.capabilities.movingAverage && Boolean(movingAverage?.enabled) && Boolean(movingAverage?.hideBase);
1282
+ if (!hideBaseSeries) {
1283
+ getOrderedSeries(chart, seriesOrderByChart).forEach((series) => {
1284
+ let visiblePoints = series.getVisiblePoints(
1285
+ state.xMin,
1286
+ state.xMax,
1287
+ plot.width
1288
+ );
1289
+ let singlePointEndpoint = null;
1290
+ if (visiblePoints.pointCount < 2) {
1291
+ const singlePointSegment = getSinglePointSegment({
1292
+ series,
1293
+ state,
1294
+ yRange,
1295
+ plot
1296
+ });
1297
+ if (!singlePointSegment) return;
1298
+ visiblePoints = singlePointSegment;
1299
+ singlePointEndpoint = singlePointSegment.endpoint;
1300
+ }
1301
+ const animatedPoint = singlePointEndpoint ? null : getAppendAnimatedPoint({
1302
+ seriesId: series.id,
1303
+ visiblePoints,
1304
+ now
1305
+ });
1306
+ const pointCount = animatedPoint?.pointCount ?? visiblePoints.pointCount;
1307
+ if (animatedPoint) {
1308
+ seriesEndpoints.set(series.id, {
1309
+ x: animatedPoint.x,
1310
+ y: animatedPoint.y
1311
+ });
1312
+ } else if (singlePointEndpoint) {
1313
+ seriesEndpoints.set(series.id, singlePointEndpoint);
1314
+ }
1315
+ const [r, g, b] = hexToRgb(series.color);
1316
+ gl.uniform3f(activeLocations.uColor, r, g, b);
1317
+ if (useAntialias) {
1318
+ const requiredFloats = Math.max(0, pointCount - 1) * 6 * AA_VERTEX_FLOAT_STRIDE;
1319
+ const entry2 = getDynamicBuffer(
1320
+ gl,
1321
+ antialiasBuffers,
1322
+ series.id,
1323
+ requiredFloats
1324
+ );
1325
+ const drawVertexCount = fillAntialiasSeriesSegments({
1326
+ vertices: entry2.vertices,
1327
+ visiblePoints,
1328
+ pointCount,
1329
+ animatedPoint
1330
+ });
1331
+ const stride = AA_VERTEX_FLOAT_STRIDE * Float32Array.BYTES_PER_ELEMENT;
1332
+ gl.bindBuffer(gl.ARRAY_BUFFER, entry2.buffer);
1333
+ gl.vertexAttribPointer(
1334
+ aaLocations.aStart,
1335
+ 2,
1336
+ gl.FLOAT,
1337
+ false,
1338
+ stride,
1339
+ 0
1340
+ );
1341
+ gl.vertexAttribPointer(
1342
+ aaLocations.aEnd,
1343
+ 2,
1344
+ gl.FLOAT,
1345
+ false,
1346
+ stride,
1347
+ 2 * Float32Array.BYTES_PER_ELEMENT
1348
+ );
1349
+ gl.vertexAttribPointer(
1350
+ aaLocations.aSide,
1351
+ 1,
1352
+ gl.FLOAT,
1353
+ false,
1354
+ stride,
1355
+ 4 * Float32Array.BYTES_PER_ELEMENT
1356
+ );
1357
+ gl.vertexAttribPointer(
1358
+ aaLocations.aAlong,
1359
+ 1,
1360
+ gl.FLOAT,
1361
+ false,
1362
+ stride,
1363
+ 5 * Float32Array.BYTES_PER_ELEMENT
1364
+ );
1365
+ gl.bufferSubData(
1366
+ gl.ARRAY_BUFFER,
1367
+ 0,
1368
+ entry2.vertices,
1369
+ 0,
1370
+ drawVertexCount * AA_VERTEX_FLOAT_STRIDE
1371
+ );
1372
+ gl.drawArrays(gl.TRIANGLES, 0, drawVertexCount);
1373
+ return;
1374
+ }
1375
+ const entry = getDynamicBuffer(
1376
+ gl,
1377
+ buffers,
1378
+ series.id,
1379
+ pointCount * 2
1380
+ );
1381
+ fillSeriesPoints({
1382
+ vertices: entry.vertices,
1383
+ visiblePoints,
1384
+ pointCount,
1385
+ animatedPoint
1386
+ });
1387
+ gl.bindBuffer(gl.ARRAY_BUFFER, entry.buffer);
1388
+ gl.vertexAttribPointer(locations.aXy, 2, gl.FLOAT, false, 0, 0);
1389
+ gl.bufferSubData(
1390
+ gl.ARRAY_BUFFER,
1391
+ 0,
1392
+ entry.vertices,
1393
+ 0,
1394
+ pointCount * 2
1395
+ );
1396
+ gl.drawArrays(gl.LINE_STRIP, 0, pointCount);
1397
+ });
1398
+ }
1399
+ const movingSeries = getMovingAverageSeriesForChart({
1400
+ chart,
1401
+ movingAverage: descriptor.capabilities.movingAverage ? movingAverage : null,
1402
+ cache: movingAverageCache
1403
+ });
1404
+ if (movingSeries.length) {
1405
+ gl.disable(gl.BLEND);
1406
+ gl.useProgram(program);
1407
+ gl.enableVertexAttribArray(locations.aXy);
1408
+ setCommonUniforms(
1409
+ locations,
1410
+ pixelWidth,
1411
+ pixelHeight,
1412
+ scaledPlot,
1413
+ state,
1414
+ yRange
1415
+ );
1416
+ getOrderedSeries(
1417
+ { ...chart, series: movingSeries },
1418
+ seriesOrderByChart
1419
+ ).forEach((series) => {
1420
+ const visiblePoints = series.getVisiblePoints(
1421
+ state.xMin,
1422
+ state.xMax,
1423
+ plot.width
1424
+ );
1425
+ if (visiblePoints.pointCount < 2) return;
1426
+ const entry = getDynamicBuffer(
1427
+ gl,
1428
+ buffers,
1429
+ series.id,
1430
+ visiblePoints.pointCount * 8 * 2
1431
+ );
1432
+ const drawVertexCount = fillDashedSeriesSegments({
1433
+ vertices: entry.vertices,
1434
+ visiblePoints,
1435
+ pointCount: visiblePoints.pointCount,
1436
+ state,
1437
+ yRange,
1438
+ plot
1439
+ });
1440
+ if (drawVertexCount < 2) return;
1441
+ const [r, g, b] = hexToRgb(series.color);
1442
+ gl.uniform3f(locations.uColor, r, g, b);
1443
+ gl.bindBuffer(gl.ARRAY_BUFFER, entry.buffer);
1444
+ gl.vertexAttribPointer(locations.aXy, 2, gl.FLOAT, false, 0, 0);
1445
+ gl.bufferSubData(
1446
+ gl.ARRAY_BUFFER,
1447
+ 0,
1448
+ entry.vertices,
1449
+ 0,
1450
+ drawVertexCount * 2
1451
+ );
1452
+ gl.drawArrays(gl.LINES, 0, drawVertexCount);
1453
+ });
1454
+ }
1455
+ gl.disable(gl.BLEND);
1456
+ return { seriesEndpoints };
1457
+ },
1458
+ destroy() {
1459
+ deleteBufferCache(gl, buffers);
1460
+ deleteBufferCache(gl, antialiasBuffers);
1461
+ movingAverageCache.clear();
1462
+ gl.deleteProgram(program);
1463
+ gl.deleteProgram(antialiasProgram);
1464
+ }
1465
+ };
1466
+ };
1467
+
1468
+ // src/core/renderers/index.js
1469
+ var RENDERER_FACTORIES = Object.freeze({
1470
+ bar: createBarRenderer,
1471
+ line: createLineRenderer
1472
+ });
1473
+ var createRendererRegistry = (gl) => {
1474
+ const renderers = new Map(
1475
+ Object.entries(RENDERER_FACTORIES).map(([type, createRenderer]) => [
1476
+ type,
1477
+ createRenderer(gl)
1478
+ ])
1479
+ );
1480
+ return {
1481
+ draw(type, payload) {
1482
+ const renderer = renderers.get(type);
1483
+ if (!renderer) {
1484
+ throw new TypeError(`No AlienCharts renderer registered for "${type}"`);
1485
+ }
1486
+ return renderer.draw(payload);
1487
+ },
1488
+ getTooltipCategory(type, payload) {
1489
+ const renderer = renderers.get(type);
1490
+ if (!renderer) {
1491
+ throw new TypeError(`No AlienCharts renderer registered for "${type}"`);
1492
+ }
1493
+ return renderer.getTooltipCategory?.(payload) ?? payload.visiblePoints.x[payload.pointIndex];
1494
+ },
1495
+ destroy() {
1496
+ renderers.forEach((renderer) => renderer.destroy());
1497
+ renderers.clear();
1498
+ }
1499
+ };
1500
+ };
1501
+
1502
+ // src/core/chartRenderer.js
1503
+ var CHART_HEIGHT = 360;
1504
+ var RIGHT_AXIS_WIDTH = 58;
1505
+ var PLOT_PADDING = Object.freeze({
1506
+ left: 38,
1507
+ right: RIGHT_AXIS_WIDTH,
1508
+ top: 34,
1509
+ bottom: 28
1510
+ });
1511
+ var HORIZONTAL_PLOT_PADDING = Object.freeze({
1512
+ left: RIGHT_AXIS_WIDTH,
1513
+ right: 20,
1514
+ top: 34,
1515
+ bottom: 28
1516
+ });
1517
+ var CATEGORICAL_PLOT_PADDING = Object.freeze({
1518
+ ...PLOT_PADDING,
1519
+ bottom: 42
1520
+ });
1521
+ var CATEGORICAL_HORIZONTAL_PLOT_PADDING = Object.freeze({
1522
+ ...HORIZONTAL_PLOT_PADDING,
1523
+ left: 128
1524
+ });
1525
+ var Y_SCALE_MIN = 0.05;
1526
+ var Y_SCALE_MAX = 40;
1527
+ var Y_AXIS_TICK_COUNT = 5;
1528
+ var X_AXIS_TICK_COUNT = 5;
1529
+ var X_SCALE_MIN_SPAN = 10;
1530
+ var X_SCALE_MAX_SPAN = 1e9;
1531
+ var JUMP_LATEST_RIGHT_PADDING_RATIO = 0.1;
1532
+ var DEFAULT_CHART_BACKGROUND = "#f5f9ff";
1533
+ var getChartOrientation = (chart, descriptor) => (descriptor || resolveChartDescriptor(chart)).orientation;
1534
+ var getPlotPadding = (chart, descriptor) => {
1535
+ const categorical = getChartCategories(chart).length > 0;
1536
+ if (getChartOrientation(chart, descriptor) === "horizontal") {
1537
+ return categorical ? CATEGORICAL_HORIZONTAL_PLOT_PADDING : HORIZONTAL_PLOT_PADDING;
1538
+ }
1539
+ return categorical ? CATEGORICAL_PLOT_PADDING : PLOT_PADDING;
1540
+ };
1541
+ var getCategoryPixelLength = (chart, plot, descriptor) => getChartOrientation(chart, descriptor) === "horizontal" ? plot.height : plot.width;
1542
+ var getReadableTextColor = (backgroundColor) => {
1543
+ const [r, g, b] = hexToRgb(backgroundColor);
1544
+ const toLinear = (value) => value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
1545
+ const luminance = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
1546
+ return luminance > 0.45 ? "#111827" : "#ffffff";
1547
+ };
1548
+ var getSeriesLabelTextColor = (series) => {
1549
+ if (series.__labelTextColorFor !== series.color) {
1550
+ series.__labelTextColorFor = series.color;
1551
+ series.__labelTextColor = getReadableTextColor(series.color);
1552
+ }
1553
+ return series.__labelTextColor;
1554
+ };
1555
+ var lowerBound2 = (array, value) => {
1556
+ let low = 0;
1557
+ let high = array.length;
1558
+ while (low < high) {
1559
+ const middle = low + high >> 1;
1560
+ if (array[middle] < value) low = middle + 1;
1561
+ else high = middle;
1562
+ }
1563
+ return low;
1564
+ };
1565
+ var getNearestPointIndex = (xValues, xValue) => {
1566
+ if (!xValues.length) return -1;
1567
+ const nextIndex = lowerBound2(xValues, xValue);
1568
+ if (nextIndex <= 0) return 0;
1569
+ if (nextIndex >= xValues.length) return xValues.length - 1;
1570
+ const previousIndex = nextIndex - 1;
1571
+ return Math.abs(xValues[nextIndex] - xValue) < Math.abs(xValue - xValues[previousIndex]) ? nextIndex : previousIndex;
1572
+ };
1573
+ var getChartXBounds = (chart) => {
1574
+ let minX = Infinity;
1575
+ let maxX = -Infinity;
1576
+ chart.series.forEach((series) => {
1577
+ if (!series.length) return;
1578
+ minX = Math.min(minX, series.rawX[0]);
1579
+ maxX = Math.max(maxX, series.rawX[series.length - 1]);
1580
+ });
1581
+ return { minX, maxX };
1582
+ };
1583
+ var getMinimumCategorySpan = (chart) => {
1584
+ const categories = getChartCategories(chart).map((category) => category.value).sort((left, right) => left - right);
1585
+ if (!categories.length) return X_SCALE_MIN_SPAN;
1586
+ let minimumGap = Infinity;
1587
+ for (let index = 1; index < categories.length; index += 1) {
1588
+ minimumGap = Math.min(minimumGap, categories[index] - categories[index - 1]);
1589
+ }
1590
+ return Number.isFinite(minimumGap) ? minimumGap : 1;
1591
+ };
1592
+ var getInitialView = (chart, initialVisiblePoints = null) => {
1593
+ const categories = getChartCategories(chart).map((category) => category.value).sort((left, right) => left - right);
1594
+ if (categories.length) {
1595
+ const requestedCount = Number.isFinite(initialVisiblePoints) && initialVisiblePoints > 0 ? Math.max(1, Math.floor(initialVisiblePoints)) : categories.length;
1596
+ const visible = categories.slice(-requestedCount);
1597
+ const first = visible[0];
1598
+ const last = visible[visible.length - 1];
1599
+ const firstIndex = categories.indexOf(first);
1600
+ const lastIndex = categories.lastIndexOf(last);
1601
+ const leftGap = firstIndex > 0 ? first - categories[firstIndex - 1] : visible[1] - first || 1;
1602
+ const rightGap = lastIndex + 1 < categories.length ? categories[lastIndex + 1] - last : last - visible[visible.length - 2] || leftGap;
1603
+ return {
1604
+ xMin: first - leftGap / 2,
1605
+ xMax: last + rightGap / 2
1606
+ };
1607
+ }
1608
+ const { minX, maxX } = getChartXBounds(chart);
1609
+ if (!Number.isFinite(minX) || !Number.isFinite(maxX)) {
1610
+ return { xMin: 0, xMax: 1 };
1611
+ }
1612
+ if (minX === maxX) {
1613
+ const span2 = Number.isFinite(initialVisiblePoints) && initialVisiblePoints > 0 ? Math.max(10, initialVisiblePoints) : 10;
1614
+ const nextMax = maxX + span2 * JUMP_LATEST_RIGHT_PADDING_RATIO;
1615
+ return { xMin: nextMax - span2, xMax: nextMax };
1616
+ }
1617
+ let span = maxX - minX;
1618
+ if (Number.isFinite(initialVisiblePoints) && initialVisiblePoints > 0) {
1619
+ span = Math.min(span, initialVisiblePoints);
1620
+ const nextMax = maxX + span * JUMP_LATEST_RIGHT_PADDING_RATIO;
1621
+ return { xMin: nextMax - span, xMax: nextMax };
1622
+ }
1623
+ return {
1624
+ xMin: minX,
1625
+ xMax: maxX + span * JUMP_LATEST_RIGHT_PADDING_RATIO
1626
+ };
1627
+ };
1628
+ var getYRange = (chart, xMin, xMax, width, suppliedDescriptor) => {
1629
+ const descriptor = suppliedDescriptor || resolveChartDescriptor(chart);
1630
+ let minY = Infinity;
1631
+ let maxY = -Infinity;
1632
+ let renderedPoints = 0;
1633
+ let bucketSize = 1;
1634
+ chart.series.forEach((series) => {
1635
+ const visible = series.getVisiblePoints(xMin, xMax, width);
1636
+ renderedPoints += visible.pointCount;
1637
+ bucketSize = Math.max(bucketSize, visible.bucketSize);
1638
+ for (let index = 0; index < visible.y.length; index += 1) {
1639
+ minY = Math.min(minY, visible.y[index]);
1640
+ maxY = Math.max(maxY, visible.y[index]);
1641
+ }
1642
+ });
1643
+ const fixedMinY = Number(chart.yRange?.min);
1644
+ const fixedMaxY = Number(chart.yRange?.max);
1645
+ if (Number.isFinite(fixedMinY) && Number.isFinite(fixedMaxY) && fixedMinY < fixedMaxY) {
1646
+ return {
1647
+ minY: fixedMinY,
1648
+ maxY: fixedMaxY,
1649
+ renderedPoints,
1650
+ bucketSize
1651
+ };
1652
+ }
1653
+ if (!Number.isFinite(minY) || !Number.isFinite(maxY)) {
1654
+ return { minY: 0, maxY: 1, renderedPoints, bucketSize };
1655
+ }
1656
+ if (descriptor.rangeIncludesZero) {
1657
+ if (minY >= 0) {
1658
+ return {
1659
+ minY: 0,
1660
+ maxY: maxY === 0 ? 1 : maxY * 1.08,
1661
+ renderedPoints,
1662
+ bucketSize
1663
+ };
1664
+ }
1665
+ if (maxY <= 0) {
1666
+ return {
1667
+ minY: minY === 0 ? -1 : minY * 1.08,
1668
+ maxY: 0,
1669
+ renderedPoints,
1670
+ bucketSize
1671
+ };
1672
+ }
1673
+ }
1674
+ if (minY === maxY) {
1675
+ minY -= 1;
1676
+ maxY += 1;
1677
+ }
1678
+ const padding = (maxY - minY) * 0.08;
1679
+ return {
1680
+ minY: minY - padding,
1681
+ maxY: maxY + padding,
1682
+ renderedPoints,
1683
+ bucketSize
1684
+ };
1685
+ };
1686
+ var applyYScale = (yRange, scale = 1, centerOffset = 0) => {
1687
+ const clampedScale = Math.min(Y_SCALE_MAX, Math.max(Y_SCALE_MIN, scale));
1688
+ const center = (yRange.minY + yRange.maxY) / 2 + centerOffset;
1689
+ const halfRange = (yRange.maxY - yRange.minY) * clampedScale / 2;
1690
+ return {
1691
+ ...yRange,
1692
+ minY: center - halfRange,
1693
+ maxY: center + halfRange
1694
+ };
1695
+ };
1696
+ var trimFormattedNumber = (value) => value.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "");
1697
+ var formatNumber = (value) => {
1698
+ if (!Number.isFinite(value)) return "";
1699
+ const absolute = Math.abs(value);
1700
+ if (absolute !== 0 && absolute < 1e-4) return value.toExponential(2);
1701
+ if (absolute < 1) return trimFormattedNumber(value.toPrecision(5));
1702
+ if (absolute >= 1e5) return value.toFixed(0);
1703
+ if (absolute >= 1e3) return trimFormattedNumber(value.toFixed(1));
1704
+ if (absolute >= 10) return trimFormattedNumber(value.toFixed(2));
1705
+ return trimFormattedNumber(value.toFixed(4));
1706
+ };
1707
+ var formatCompactNumber = (value) => {
1708
+ if (!Number.isFinite(value)) return "";
1709
+ const absolute = Math.abs(value);
1710
+ if (absolute >= 1e9) {
1711
+ return `${(value / 1e9).toFixed(
1712
+ absolute >= 1e10 ? 0 : 1
1713
+ )}B`;
1714
+ }
1715
+ if (absolute >= 1e6) {
1716
+ return `${(value / 1e6).toFixed(
1717
+ absolute >= 1e7 ? 0 : 1
1718
+ )}M`;
1719
+ }
1720
+ if (absolute >= 1e3) {
1721
+ return `${(value / 1e3).toFixed(absolute >= 1e4 ? 0 : 1)}k`;
1722
+ }
1723
+ return formatNumber(value);
1724
+ };
1725
+ var normalizeRect = (start, end) => {
1726
+ if (!start || !end) return null;
1727
+ const left = Math.min(start.x, end.x);
1728
+ const top = Math.min(start.y, end.y);
1729
+ return {
1730
+ left,
1731
+ top,
1732
+ width: Math.abs(end.x - start.x),
1733
+ height: Math.abs(end.y - start.y)
1734
+ };
1735
+ };
1736
+ var clampPointToPlot = (point, plot) => ({
1737
+ x: Math.min(plot.x + plot.width, Math.max(plot.x, point.x)),
1738
+ y: Math.min(plot.y + plot.height, Math.max(plot.y, point.y))
1739
+ });
1740
+ var getScaledYRangeForLayout = ({
1741
+ chart,
1742
+ descriptor,
1743
+ state,
1744
+ plot,
1745
+ yScaleRef,
1746
+ yCenterOffsetRef
1747
+ }) => applyYScale(
1748
+ getYRange(
1749
+ chart,
1750
+ state.xMin,
1751
+ state.xMax,
1752
+ getCategoryPixelLength(chart, plot, descriptor),
1753
+ descriptor
1754
+ ),
1755
+ yScaleRef.current.get(chart.id) ?? 1,
1756
+ yCenterOffsetRef.current.get(chart.id) ?? 0
1757
+ );
1758
+ var applyRectangleZoom = ({
1759
+ chart,
1760
+ descriptor: suppliedDescriptor,
1761
+ plot,
1762
+ start,
1763
+ end,
1764
+ initialVisiblePoints,
1765
+ viewStateRef,
1766
+ yScaleRef,
1767
+ yCenterOffsetRef,
1768
+ yManualScaleRef
1769
+ }) => {
1770
+ const rect = normalizeRect(
1771
+ clampPointToPlot(start, plot),
1772
+ clampPointToPlot(end, plot)
1773
+ );
1774
+ if (!rect || rect.width < 6 || rect.height < 6) return false;
1775
+ const descriptor = suppliedDescriptor || resolveChartDescriptor(chart);
1776
+ const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
1777
+ const currentYRange = getScaledYRangeForLayout({
1778
+ chart,
1779
+ descriptor,
1780
+ state,
1781
+ plot,
1782
+ yScaleRef,
1783
+ yCenterOffsetRef
1784
+ });
1785
+ const transform = createCoordinateTransform({
1786
+ orientation: descriptor.orientation,
1787
+ categoryRange: { min: state.xMin, max: state.xMax },
1788
+ valueRange: {
1789
+ min: currentYRange.minY,
1790
+ max: currentYRange.maxY
1791
+ },
1792
+ plot
1793
+ });
1794
+ const selected = transform.dataBoundsForRect(rect);
1795
+ if (selected.categoryMax - selected.categoryMin < getMinimumCategorySpan(chart)) {
1796
+ return false;
1797
+ }
1798
+ const selectedSpan = selected.valueMax - selected.valueMin;
1799
+ if (!Number.isFinite(selectedSpan) || selectedSpan <= 0) return false;
1800
+ viewStateRef.current.set(chart.id, {
1801
+ xMin: selected.categoryMin,
1802
+ xMax: selected.categoryMax
1803
+ });
1804
+ const baseYRange = getYRange(
1805
+ chart,
1806
+ selected.categoryMin,
1807
+ selected.categoryMax,
1808
+ getCategoryPixelLength(chart, plot, descriptor),
1809
+ descriptor
1810
+ );
1811
+ const baseSpan = baseYRange.maxY - baseYRange.minY;
1812
+ if (Number.isFinite(baseSpan) && baseSpan > 0) {
1813
+ const selectedCenter = (selected.valueMin + selected.valueMax) / 2;
1814
+ const baseCenter = (baseYRange.minY + baseYRange.maxY) / 2;
1815
+ yScaleRef.current.set(
1816
+ chart.id,
1817
+ Math.min(
1818
+ Y_SCALE_MAX,
1819
+ Math.max(Y_SCALE_MIN, selectedSpan / baseSpan)
1820
+ )
1821
+ );
1822
+ yCenterOffsetRef.current.set(chart.id, selectedCenter - baseCenter);
1823
+ yManualScaleRef.current.add(chart.id);
1824
+ }
1825
+ return true;
1826
+ };
1827
+ var screenPointToDataPoint = ({
1828
+ point,
1829
+ chart,
1830
+ descriptor: suppliedDescriptor,
1831
+ plot,
1832
+ initialVisiblePoints,
1833
+ viewStateRef,
1834
+ yScaleRef,
1835
+ yCenterOffsetRef
1836
+ }) => {
1837
+ const descriptor = suppliedDescriptor || resolveChartDescriptor(chart);
1838
+ const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
1839
+ const yRange = getScaledYRangeForLayout({
1840
+ chart,
1841
+ descriptor,
1842
+ state,
1843
+ plot,
1844
+ yScaleRef,
1845
+ yCenterOffsetRef
1846
+ });
1847
+ return createCoordinateTransform({
1848
+ orientation: descriptor.orientation,
1849
+ categoryRange: { min: state.xMin, max: state.xMax },
1850
+ valueRange: { min: yRange.minY, max: yRange.maxY },
1851
+ plot
1852
+ }).screenToData(point);
1853
+ };
1854
+ var createAxisOverlay = ({
1855
+ chart,
1856
+ descriptor,
1857
+ plot,
1858
+ state,
1859
+ yRange,
1860
+ seriesEndpoints,
1861
+ seriesOrderByChart
1862
+ }) => {
1863
+ const horizontal = descriptor.orientation === "horizontal";
1864
+ const transform = createCoordinateTransform({
1865
+ orientation: descriptor.orientation,
1866
+ categoryRange: { min: state.xMin, max: state.xMax },
1867
+ valueRange: { min: yRange.minY, max: yRange.maxY },
1868
+ plot: { x: 0, y: 0, width: plot.width, height: plot.height }
1869
+ });
1870
+ const ticks = Array.from({ length: Y_AXIS_TICK_COUNT }, (_, index) => {
1871
+ const ratio = index / (Y_AXIS_TICK_COUNT - 1);
1872
+ const value = yRange.maxY - ratio * (yRange.maxY - yRange.minY);
1873
+ const screen = transform.dataToScreen({
1874
+ x: state.xMin,
1875
+ y: value
1876
+ });
1877
+ return {
1878
+ id: `${chart.id}-tick-${index}`,
1879
+ value,
1880
+ top: horizontal ? void 0 : screen.y,
1881
+ left: horizontal ? screen.x : void 0
1882
+ };
1883
+ });
1884
+ const categories = getChartCategories(chart);
1885
+ const visibleCategories = categories.filter(
1886
+ (category) => category.value >= state.xMin && category.value <= state.xMax
1887
+ ).sort((left, right) => left.value - right.value);
1888
+ const categoryPixelLength = horizontal ? plot.height : plot.width;
1889
+ const maximumCategoryLabels = Math.max(
1890
+ 1,
1891
+ Math.floor(categoryPixelLength / (horizontal ? 22 : 72))
1892
+ );
1893
+ const categoryLabelStride = Math.max(
1894
+ 1,
1895
+ Math.ceil(visibleCategories.length / maximumCategoryLabels)
1896
+ );
1897
+ const displayedCategories = visibleCategories.filter(
1898
+ (_, index) => index % categoryLabelStride === 0 || index === visibleCategories.length - 1
1899
+ );
1900
+ const xTicks = categories.length ? displayedCategories.map((category, index) => {
1901
+ const screen = transform.dataToScreen({
1902
+ x: category.value,
1903
+ y: yRange.minY
1904
+ });
1905
+ return {
1906
+ id: `${chart.id}-category-${index}-${category.value}`,
1907
+ categorical: true,
1908
+ label: category.label,
1909
+ value: category.value,
1910
+ left: horizontal ? void 0 : screen.x,
1911
+ top: horizontal ? screen.y : void 0
1912
+ };
1913
+ }) : Array.from({ length: X_AXIS_TICK_COUNT }, (_, index) => {
1914
+ const ratio = index / (X_AXIS_TICK_COUNT - 1);
1915
+ return {
1916
+ id: `${chart.id}-x-tick-${index}`,
1917
+ value: state.xMin + ratio * (state.xMax - state.xMin),
1918
+ left: horizontal ? void 0 : 18 + ratio * Math.max(1, plot.width - 36),
1919
+ top: horizontal ? 10 + ratio * Math.max(1, plot.height - 20) : void 0
1920
+ };
1921
+ });
1922
+ const latestValues = descriptor.capabilities.latestValue ? getOrderedSeries(chart, seriesOrderByChart).map((series) => {
1923
+ if (!series.length) return null;
1924
+ const value = series.rawY[series.length - 1];
1925
+ const x = series.rawX[series.length - 1];
1926
+ const endpoint = seriesEndpoints.get(series.id) || { x, y: value };
1927
+ const screen = transform.dataToScreen(endpoint);
1928
+ return {
1929
+ id: series.id,
1930
+ color: series.color,
1931
+ textColor: getSeriesLabelTextColor(series),
1932
+ value: endpoint.y,
1933
+ rawValue: value,
1934
+ x,
1935
+ left: screen.x,
1936
+ top: screen.y
1937
+ };
1938
+ }).filter(
1939
+ (item) => item && Number.isFinite(item.top) && item.top >= -10 && item.top <= plot.height + 10
1940
+ ) : [];
1941
+ const { maxX } = getChartXBounds(chart);
1942
+ return {
1943
+ orientation: descriptor.orientation,
1944
+ ticks,
1945
+ xTicks,
1946
+ latestValues,
1947
+ plotWidth: plot.width,
1948
+ showJumpLatest: Number.isFinite(maxX) && (maxX < state.xMin || maxX > state.xMax)
1949
+ };
1950
+ };
1951
+ var drawChartLayouts = ({
1952
+ canvas,
1953
+ width,
1954
+ height,
1955
+ gl,
1956
+ rendererRegistry,
1957
+ antialiasLines = false,
1958
+ layouts,
1959
+ viewStateRef,
1960
+ yScaleRef,
1961
+ yCenterOffsetRef,
1962
+ initialVisiblePoints,
1963
+ getAppendAnimatedPoint,
1964
+ movingAverageByChart,
1965
+ seriesOrderByChart
1966
+ }) => {
1967
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
1968
+ const pixelWidth = Math.max(1, Math.floor(width * dpr));
1969
+ const pixelHeight = Math.max(1, Math.floor(height * dpr));
1970
+ if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
1971
+ canvas.width = pixelWidth;
1972
+ canvas.height = pixelHeight;
1973
+ canvas.style.width = `${width}px`;
1974
+ canvas.style.height = `${height}px`;
1975
+ }
1976
+ gl.viewport(0, 0, pixelWidth, pixelHeight);
1977
+ gl.clearColor(0, 0, 0, 0);
1978
+ gl.clear(gl.COLOR_BUFFER_BIT);
1979
+ const now = performance.now();
1980
+ const nextAxisOverlays = {};
1981
+ layouts.forEach((layout) => {
1982
+ if (!layout.visible) return;
1983
+ const { chart, plot } = layout;
1984
+ const descriptor = layout.descriptor || getChartDescriptor(chart);
1985
+ const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
1986
+ const scaledPlot = {
1987
+ x: plot.x * dpr,
1988
+ y: plot.y * dpr,
1989
+ width: plot.width * dpr,
1990
+ height: plot.height * dpr
1991
+ };
1992
+ const categoryPixelLength = getCategoryPixelLength(
1993
+ chart,
1994
+ plot,
1995
+ descriptor
1996
+ );
1997
+ const yRange = applyYScale(
1998
+ getYRange(
1999
+ chart,
2000
+ state.xMin,
2001
+ state.xMax,
2002
+ categoryPixelLength,
2003
+ descriptor
2004
+ ),
2005
+ yScaleRef.current.get(chart.id) ?? 1,
2006
+ yCenterOffsetRef.current.get(chart.id) ?? 0
2007
+ );
2008
+ gl.enable(gl.SCISSOR_TEST);
2009
+ gl.scissor(
2010
+ Math.floor(scaledPlot.x),
2011
+ Math.floor(pixelHeight - scaledPlot.y - scaledPlot.height),
2012
+ Math.ceil(scaledPlot.width),
2013
+ Math.ceil(scaledPlot.height)
2014
+ );
2015
+ const result = rendererRegistry.draw(descriptor.rendererType, {
2016
+ antialiasLines,
2017
+ categoryPixelLength,
2018
+ chart,
2019
+ descriptor,
2020
+ dpr,
2021
+ getAppendAnimatedPoint,
2022
+ movingAverage: movingAverageByChart?.[chart.id],
2023
+ now,
2024
+ pixelHeight,
2025
+ pixelWidth,
2026
+ plot,
2027
+ scaledPlot,
2028
+ seriesOrderByChart,
2029
+ state,
2030
+ yRange
2031
+ });
2032
+ nextAxisOverlays[chart.id] = createAxisOverlay({
2033
+ chart,
2034
+ descriptor,
2035
+ plot,
2036
+ state,
2037
+ yRange,
2038
+ seriesEndpoints: result?.seriesEndpoints || /* @__PURE__ */ new Map(),
2039
+ seriesOrderByChart
2040
+ });
2041
+ gl.disable(gl.SCISSOR_TEST);
2042
+ });
2043
+ gl.disable(gl.BLEND);
2044
+ return nextAxisOverlays;
2045
+ };
2046
+
2047
+ // src/vanilla/createChartGrid.js
2048
+ var BASE_ROOT_CLASSES = ["aliencharts-root", "relative", "h-full", "overflow-y-auto"];
2049
+ var EMPTY_OBJECT = Object.freeze({});
2050
+ var controllerIdSequence = 0;
2051
+ var DEFAULT_OPTIONS = Object.freeze({
2052
+ charts: [],
2053
+ columns: 2,
2054
+ initialVisiblePoints: null,
2055
+ backgroundColor: DEFAULT_CHART_BACKGROUND,
2056
+ antialiasLines: false,
2057
+ gridLines: false,
2058
+ showToolbar: true,
2059
+ showLatestValueLine: true,
2060
+ showTooltips: true,
2061
+ followLatest: false,
2062
+ followVisibleLatest: true,
2063
+ xAxisLabel: "STEP",
2064
+ disableDrawings: false,
2065
+ seriesOrderByChart: EMPTY_OBJECT,
2066
+ topMarkers: [],
2067
+ formatXTick: formatCompactNumber,
2068
+ formatXValue: formatNumber,
2069
+ formatYValue: formatNumber
2070
+ });
2071
+ var APPEND_ANIMATION = Object.freeze({
2072
+ durationMs: 300,
2073
+ maxBucketSize: 64,
2074
+ maxRevealPoints: 100
2075
+ });
2076
+ var POINTER_SCALE_SENSITIVITY = 0.01;
2077
+ var WHEEL_SCALE_SENSITIVITY = 1e-3;
2078
+ var scaleValueRange = (scale, delta, sensitivity) => clamp(
2079
+ scale * Math.exp(delta * sensitivity),
2080
+ Y_SCALE_MIN,
2081
+ Y_SCALE_MAX
2082
+ );
2083
+ var escapeHtml = (value) => String(value ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;");
2084
+ var withAlpha = (color, alpha) => {
2085
+ const value = String(color || DEFAULT_CHART_BACKGROUND).trim();
2086
+ const match = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(value);
2087
+ if (!match) return value;
2088
+ return `rgba(${Number.parseInt(match[1], 16)}, ${Number.parseInt(match[2], 16)}, ${Number.parseInt(match[3], 16)}, ${alpha})`;
2089
+ };
2090
+ var getTextColor = (color) => {
2091
+ const value = String(color || "#38bdf8").replace("#", "");
2092
+ const normalized = value.length === 3 ? value.split("").map((part) => part + part).join("") : value.padEnd(6, "0").slice(0, 6);
2093
+ const number = Number.parseInt(normalized, 16);
2094
+ const rgb = [16, 8, 0].map((shift) => (number >> shift & 255) / 255);
2095
+ const linear = rgb.map((part) => part <= 0.03928 ? part / 12.92 : ((part + 0.055) / 1.055) ** 2.4);
2096
+ return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2] > 0.45 ? "#111827" : "#ffffff";
2097
+ };
2098
+ var hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
2099
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
2100
+ var toolbarButton = ({ action, title, icon, hotkey = "", active = false, className = "" }) => `
2101
+ <button type="button" data-action="${action}" title="${escapeHtml(title)}" aria-label="${escapeHtml(title)}"
2102
+ 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}">
2103
+ ${icon}
2104
+ ${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>` : ""}
2105
+ </button>`;
2106
+ var chartCellHtml = (chart, descriptor, index, backgroundColor) => {
2107
+ const padding = getPlotPadding(chart, descriptor);
2108
+ const horizontal = descriptor.orientation === "horizontal";
2109
+ const valueAxisStyle = horizontal ? `left:${padding.left}px;right:${padding.right}px;bottom:0;height:${padding.bottom}px;cursor:ew-resize` : `right:0;top:${padding.top}px;bottom:${padding.bottom}px;width:${padding.right}px;cursor:ns-resize`;
2110
+ const categoryAxisStyle = horizontal ? `left:0;top:${padding.top}px;bottom:${padding.bottom}px;width:${padding.left}px;cursor:ns-resize` : `left:${padding.left}px;right:${padding.right}px;bottom:0;height:${padding.bottom}px;cursor:ew-resize`;
2111
+ return `
2112
+ <div data-chart-index="${index}" class="relative select-none overflow-hidden rounded-sm" style="height:${CHART_HEIGHT}px;background-color:${escapeHtml(backgroundColor)}">
2113
+ <div class="pointer-events-none absolute inset-x-0 top-0 z-10 flex h-8 items-center px-2 backdrop-blur" data-header>
2114
+ <div class="flex min-w-0 items-center gap-1 rounded-sm px-1 text-sm font-semibold text-foreground" data-title-wrap>
2115
+ ${chart.pinned ? iconSvg("pushPinSimple", { size: 14, className: "shrink-0 text-foreground/80" }) : ""}
2116
+ <span class="truncate">${escapeHtml(chart.title)}</span>
2117
+ </div>
2118
+ </div>
2119
+ <div data-grid-lines class="pointer-events-none absolute text-border/40"></div>
2120
+ <div data-latest-lines></div>
2121
+ <div data-y-axis class="absolute z-20" style="${valueAxisStyle}"></div>
2122
+ <div data-x-axis class="absolute z-20" style="${categoryAxisStyle}"></div>
2123
+ <div data-toolbar></div>
2124
+ </div>`;
2125
+ };
2126
+ var makeSurface = (canvas, gl) => ({
2127
+ canvas,
2128
+ gl,
2129
+ renderers: createRendererRegistry(gl)
2130
+ });
2131
+ var destroySurface = (surface) => {
2132
+ if (!surface) return;
2133
+ surface.renderers.destroy();
2134
+ surface.gl.getExtension("WEBGL_lose_context")?.loseContext();
2135
+ };
2136
+ var AppendAnimator = class {
2137
+ constructor(requestRender) {
2138
+ this.requestRender = requestRender;
2139
+ this.latest = /* @__PURE__ */ new Map();
2140
+ this.animations = /* @__PURE__ */ new Map();
2141
+ this.frame = null;
2142
+ }
2143
+ scan(charts, descriptors) {
2144
+ const now = performance.now();
2145
+ charts.forEach((chart) => {
2146
+ const descriptor = descriptors.get(chart.id);
2147
+ if (!descriptor?.capabilities.appendAnimation) return;
2148
+ chart.series.forEach((series) => {
2149
+ if (!series.length) return;
2150
+ const next = {
2151
+ x: series.rawX[series.length - 1],
2152
+ y: series.rawY[series.length - 1],
2153
+ length: series.length
2154
+ };
2155
+ const previous = this.latest.get(series.id);
2156
+ if (previous && next.x > previous.x) {
2157
+ this.animations.set(series.id, {
2158
+ fromX: previous.x,
2159
+ fromY: previous.y,
2160
+ toX: next.x,
2161
+ toY: next.y,
2162
+ appendedCount: next.length - previous.length,
2163
+ startedAt: now,
2164
+ duration: APPEND_ANIMATION.durationMs
2165
+ });
2166
+ }
2167
+ this.latest.set(series.id, next);
2168
+ });
2169
+ });
2170
+ if (this.animations.size) this.start();
2171
+ }
2172
+ start() {
2173
+ if (this.frame != null) return;
2174
+ const tick = () => {
2175
+ const now = performance.now();
2176
+ this.animations.forEach((animation, id) => {
2177
+ if (now - animation.startedAt >= animation.duration) this.animations.delete(id);
2178
+ });
2179
+ this.requestRender();
2180
+ this.frame = this.animations.size ? requestAnimationFrame(tick) : null;
2181
+ };
2182
+ this.frame = requestAnimationFrame(tick);
2183
+ }
2184
+ getPoint({ seriesId, visiblePoints, now }) {
2185
+ if (visiblePoints.bucketSize > APPEND_ANIMATION.maxBucketSize) return null;
2186
+ const animation = this.animations.get(seriesId);
2187
+ if (!animation || visiblePoints.x[0] > animation.fromX || visiblePoints.x[visiblePoints.pointCount - 1] < animation.toX) return null;
2188
+ let stableCount = 0;
2189
+ while (stableCount < visiblePoints.pointCount && visiblePoints.x[stableCount] <= animation.fromX) stableCount += 1;
2190
+ if (!stableCount) return null;
2191
+ const rawProgress = clamp((now - animation.startedAt) / animation.duration, 0, 1);
2192
+ const progress = 1 - (1 - rawProgress) ** 2;
2193
+ if (animation.appendedCount > APPEND_ANIMATION.maxRevealPoints) {
2194
+ return {
2195
+ index: stableCount,
2196
+ pointCount: stableCount + 1,
2197
+ x: animation.fromX + (animation.toX - animation.fromX) * progress,
2198
+ y: animation.fromY + (animation.toY - animation.fromY) * progress
2199
+ };
2200
+ }
2201
+ const appendedCount = visiblePoints.pointCount - stableCount;
2202
+ const position = progress * appendedCount;
2203
+ const revealed = Math.min(appendedCount, Math.floor(position));
2204
+ const nextIndex = Math.min(visiblePoints.pointCount - 1, stableCount + revealed);
2205
+ const previousIndex = Math.max(stableCount - 1, nextIndex - 1);
2206
+ const partial = position - revealed;
2207
+ const partialPoint = revealed < appendedCount;
2208
+ return {
2209
+ index: nextIndex,
2210
+ pointCount: Math.min(visiblePoints.pointCount, nextIndex + 1),
2211
+ x: partialPoint ? visiblePoints.x[previousIndex] + (visiblePoints.x[nextIndex] - visiblePoints.x[previousIndex]) * partial : visiblePoints.x[nextIndex],
2212
+ y: partialPoint ? visiblePoints.y[previousIndex] + (visiblePoints.y[nextIndex] - visiblePoints.y[previousIndex]) * partial : visiblePoints.y[nextIndex]
2213
+ };
2214
+ }
2215
+ destroy() {
2216
+ if (this.frame != null) cancelAnimationFrame(this.frame);
2217
+ this.frame = null;
2218
+ this.animations.clear();
2219
+ }
2220
+ };
2221
+ var ChartGridController = class {
2222
+ constructor(container, options) {
2223
+ if (!(container instanceof HTMLElement)) throw new TypeError("createChartGrid requires an HTMLElement target");
2224
+ this.container = container;
2225
+ this.controllerId = `aliencharts-${controllerIdSequence += 1}`;
2226
+ this.options = { ...DEFAULT_OPTIONS, ...options };
2227
+ if (!Array.isArray(this.options.charts)) throw new TypeError("charts must be an array");
2228
+ this.chartDescriptors = resolveChartDescriptors(this.options.charts);
2229
+ this.destroyed = false;
2230
+ this.frame = null;
2231
+ this.structureDirty = true;
2232
+ this.focusedChartId = null;
2233
+ this.fullscreenChartId = null;
2234
+ this.rectangleZoomChartId = null;
2235
+ this.rectangleZoomRect = null;
2236
+ this.crosshair = null;
2237
+ this.drag = null;
2238
+ this.draftDrawing = null;
2239
+ this.drawingSession = null;
2240
+ this.drawings = Array.isArray(options.drawings) ? [...options.drawings] : [];
2241
+ this.activeDrawingTool = options.activeDrawingTool ?? null;
2242
+ this.selectedDrawingId = options.selectedDrawingId ?? null;
2243
+ this.movingAverageByChart = { ...options.movingAverageByChart || {} };
2244
+ this.viewStates = /* @__PURE__ */ new Map();
2245
+ this.yScales = /* @__PURE__ */ new Map();
2246
+ this.yOffsets = /* @__PURE__ */ new Map();
2247
+ this.latestX = /* @__PURE__ */ new Map();
2248
+ this.axisOverlays = {};
2249
+ this.layouts = [];
2250
+ this.fullscreenLayouts = [];
2251
+ this.addedClasses = BASE_ROOT_CLASSES.filter((name) => !container.classList.contains(name));
2252
+ BASE_ROOT_CLASSES.forEach((name) => container.classList.add(name));
2253
+ this.canvas = document.createElement("canvas");
2254
+ this.canvas.className = "pointer-events-none absolute left-0 top-0 z-10 block";
2255
+ this.grid = document.createElement("div");
2256
+ this.grid.className = "relative z-0 grid gap-1 pt-1";
2257
+ this.overlays = document.createElement("div");
2258
+ this.overlays.className = "pointer-events-none absolute left-0 top-0 z-20";
2259
+ this.overlays.style.width = "100%";
2260
+ container.append(this.canvas, this.grid, this.overlays);
2261
+ const gl = this.canvas.getContext("webgl2", { antialias: true, alpha: true, depth: false, stencil: false });
2262
+ if (!gl) {
2263
+ this.cleanupDom();
2264
+ throw new Error("AlienCharts requires WebGL2 support");
2265
+ }
2266
+ this.surface = makeSurface(this.canvas, gl);
2267
+ this.fullscreenSurface = null;
2268
+ this.animator = new AppendAnimator(() => this.requestRender());
2269
+ this.animator.scan(this.options.charts, this.chartDescriptors);
2270
+ this.bound = {
2271
+ click: (event) => this.handleClick(event),
2272
+ input: (event) => this.handleInput(event),
2273
+ change: (event) => this.handleInput(event),
2274
+ pointerdown: (event) => this.handlePointerDown(event),
2275
+ pointermove: (event) => this.handlePointerMove(event),
2276
+ pointerup: (event) => this.handlePointerUp(event),
2277
+ pointerleave: () => this.clearCrosshair(),
2278
+ contextmenu: (event) => this.handleContextMenu(event),
2279
+ wheel: (event) => this.handleWheel(event),
2280
+ scroll: () => this.requestRender(),
2281
+ keydown: (event) => this.handleKeyDown(event)
2282
+ };
2283
+ Object.entries(this.bound).forEach(([name, handler]) => {
2284
+ const target = name === "keydown" ? window : container;
2285
+ target.addEventListener(name, handler, name === "wheel" ? { passive: false } : void 0);
2286
+ });
2287
+ this.resizeObserver = new ResizeObserver(() => this.requestRender());
2288
+ this.resizeObserver.observe(container);
2289
+ this.initializeChartState();
2290
+ this.requestRender();
2291
+ }
2292
+ assertAlive() {
2293
+ if (this.destroyed) throw new Error("AlienCharts controller has been destroyed");
2294
+ }
2295
+ initializeChartState() {
2296
+ const ids = new Set(this.options.charts.map((chart) => chart.id));
2297
+ [this.viewStates, this.yScales, this.yOffsets, this.latestX].forEach((map) => {
2298
+ [...map.keys()].forEach((id) => {
2299
+ if (!ids.has(id)) map.delete(id);
2300
+ });
2301
+ });
2302
+ this.options.charts.forEach((chart) => {
2303
+ if (!this.viewStates.has(chart.id)) this.viewStates.set(chart.id, getInitialView(chart, this.options.initialVisiblePoints));
2304
+ if (!this.yScales.has(chart.id)) this.yScales.set(chart.id, 1);
2305
+ if (!this.yOffsets.has(chart.id)) this.yOffsets.set(chart.id, 0);
2306
+ if (!this.latestX.has(chart.id)) this.latestX.set(chart.id, getChartXBounds(chart).maxX);
2307
+ });
2308
+ if (this.focusedChartId && !ids.has(this.focusedChartId)) this.focusedChartId = null;
2309
+ if (this.fullscreenChartId && !ids.has(this.fullscreenChartId)) this.closeFullscreen();
2310
+ }
2311
+ requestRender() {
2312
+ if (this.destroyed || this.frame != null) return;
2313
+ this.frame = requestAnimationFrame(() => {
2314
+ this.frame = null;
2315
+ this.render();
2316
+ });
2317
+ }
2318
+ setOptions(next) {
2319
+ this.assertAlive();
2320
+ if (!next || typeof next !== "object") return;
2321
+ const previousCharts = this.options.charts;
2322
+ const previousActiveDrawingTool = this.activeDrawingTool;
2323
+ const nextOptions = { ...this.options, ...next };
2324
+ if (!Array.isArray(nextOptions.charts)) throw new TypeError("charts must be an array");
2325
+ const nextDescriptors = resolveChartDescriptors(nextOptions.charts);
2326
+ const descriptorLayoutChanged = nextOptions.charts.some((chart) => {
2327
+ const previous = this.chartDescriptors.get(chart.id);
2328
+ const current = nextDescriptors.get(chart.id);
2329
+ return previous?.rendererType !== current?.rendererType || previous?.orientation !== current?.orientation || previous?.categorical !== current?.categorical;
2330
+ });
2331
+ this.options = nextOptions;
2332
+ this.chartDescriptors = nextDescriptors;
2333
+ if (hasOwn(next, "drawings")) this.drawings = Array.isArray(next.drawings) ? [...next.drawings] : [];
2334
+ if (hasOwn(next, "activeDrawingTool")) {
2335
+ this.activeDrawingTool = next.activeDrawingTool ?? null;
2336
+ if (this.activeDrawingTool !== previousActiveDrawingTool) {
2337
+ this.draftDrawing = null;
2338
+ this.drawingSession = null;
2339
+ }
2340
+ }
2341
+ if (hasOwn(next, "selectedDrawingId")) this.selectedDrawingId = next.selectedDrawingId ?? null;
2342
+ if (hasOwn(next, "movingAverageByChart")) this.movingAverageByChart = { ...next.movingAverageByChart || {} };
2343
+ if (previousCharts !== this.options.charts || descriptorLayoutChanged || hasOwn(next, "columns") || hasOwn(next, "backgroundColor") || hasOwn(next, "gridLines")) this.structureDirty = true;
2344
+ this.initializeChartState();
2345
+ this.requestRender();
2346
+ }
2347
+ setCharts(charts) {
2348
+ this.setOptions({ charts });
2349
+ }
2350
+ invalidate() {
2351
+ this.assertAlive();
2352
+ this.followAppendedData();
2353
+ this.animator.scan(this.options.charts, this.chartDescriptors);
2354
+ this.requestRender();
2355
+ }
2356
+ followAppendedData() {
2357
+ this.options.charts.forEach((chart) => {
2358
+ const previous = this.latestX.get(chart.id);
2359
+ const next = getChartXBounds(chart).maxX;
2360
+ if (Number.isFinite(previous) && Number.isFinite(next) && next !== previous) {
2361
+ const state = this.viewStates.get(chart.id) || getInitialView(chart, this.options.initialVisiblePoints);
2362
+ const span = state.xMax - state.xMin;
2363
+ const nearEdge = previous >= state.xMax - span * 0.15 && previous >= state.xMin && previous <= state.xMax;
2364
+ if (this.options.followLatest || this.options.followVisibleLatest && nearEdge) {
2365
+ const delta = next - previous;
2366
+ this.viewStates.set(chart.id, { xMin: state.xMin + delta, xMax: state.xMax + delta });
2367
+ }
2368
+ }
2369
+ this.latestX.set(chart.id, next);
2370
+ });
2371
+ }
2372
+ jumpToLatest(chartId) {
2373
+ this.assertAlive();
2374
+ this.options.charts.forEach((chart) => {
2375
+ if (chartId != null && chart.id !== chartId) return;
2376
+ const maxX = getChartXBounds(chart).maxX;
2377
+ if (!Number.isFinite(maxX)) return;
2378
+ const state = this.viewStates.get(chart.id) || getInitialView(chart, this.options.initialVisiblePoints);
2379
+ const span = Math.max(
2380
+ getMinimumCategorySpan(chart),
2381
+ state.xMax - state.xMin
2382
+ );
2383
+ const nextMax = maxX + span * 0.1;
2384
+ this.viewStates.set(chart.id, { xMin: nextMax - span, xMax: nextMax });
2385
+ });
2386
+ this.requestRender();
2387
+ }
2388
+ scrollToTop(options = {}) {
2389
+ this.assertAlive();
2390
+ this.container.scrollTo({ ...options, top: 0 });
2391
+ }
2392
+ renderStructure() {
2393
+ this.grid.style.gridTemplateColumns = `repeat(${Math.max(1, Number(this.options.columns) || 1)}, minmax(18rem, 1fr))`;
2394
+ this.grid.innerHTML = this.options.charts.map((chart, index) => chartCellHtml(
2395
+ chart,
2396
+ this.chartDescriptors.get(chart.id),
2397
+ index,
2398
+ this.options.backgroundColor
2399
+ )).join("");
2400
+ this.chartNodes = [...this.grid.querySelectorAll("[data-chart-index]")];
2401
+ this.chartNodes.forEach((node) => {
2402
+ node.querySelector("[data-header]").style.backgroundColor = withAlpha(this.options.backgroundColor, 0.82);
2403
+ });
2404
+ this.structureDirty = false;
2405
+ }
2406
+ getLayouts(fullscreen = false) {
2407
+ const nodes = fullscreen ? this.fullscreenNode ? [this.fullscreenNode] : [] : this.chartNodes || [];
2408
+ const charts = fullscreen ? this.options.charts.filter((chart) => chart.id === this.fullscreenChartId) : this.options.charts;
2409
+ const host = fullscreen ? this.fullscreenOverlay : this.container;
2410
+ const hostRect = host.getBoundingClientRect();
2411
+ return nodes.map((node, index) => {
2412
+ const rect = node.getBoundingClientRect();
2413
+ const x = rect.left - hostRect.left + host.scrollLeft;
2414
+ const y = rect.top - hostRect.top + host.scrollTop;
2415
+ const chart = charts[index];
2416
+ const descriptor = this.chartDescriptors.get(chart.id);
2417
+ const padding = getPlotPadding(chart, descriptor);
2418
+ const layout = {
2419
+ chart,
2420
+ descriptor,
2421
+ rect: { x, y, width: rect.width, height: rect.height },
2422
+ plot: {
2423
+ x: x + padding.left,
2424
+ y: y + padding.top,
2425
+ width: Math.max(1, rect.width - padding.left - padding.right),
2426
+ height: Math.max(1, rect.height - padding.top - padding.bottom)
2427
+ },
2428
+ visible: fullscreen || rect.bottom >= hostRect.top && rect.top <= hostRect.bottom,
2429
+ node,
2430
+ fullscreen
2431
+ };
2432
+ return layout;
2433
+ });
2434
+ }
2435
+ render() {
2436
+ const focusState = this.captureControlFocus();
2437
+ if (this.structureDirty) this.renderStructure();
2438
+ this.layouts = this.getLayouts(false);
2439
+ this.axisOverlays = this.drawSurface(this.surface, this.layouts, this.container.scrollWidth, this.container.scrollHeight);
2440
+ this.updateCells(this.layouts, this.axisOverlays, false);
2441
+ if (this.fullscreenChartId) {
2442
+ this.fullscreenLayouts = this.getLayouts(true);
2443
+ const axes = this.drawSurface(this.fullscreenSurface, this.fullscreenLayouts, this.fullscreenOverlay.clientWidth, this.fullscreenOverlay.clientHeight);
2444
+ this.updateCells(this.fullscreenLayouts, axes, true);
2445
+ }
2446
+ this.renderOverlays();
2447
+ this.restoreControlFocus(focusState);
2448
+ }
2449
+ captureControlFocus() {
2450
+ const active = document.activeElement;
2451
+ if (!(active instanceof HTMLInputElement)) return null;
2452
+ 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;
2453
+ if (!selector) return null;
2454
+ const fullscreen = Boolean(active.closest("[data-aliencharts-fullscreen]"));
2455
+ let selectionStart = null;
2456
+ let selectionEnd = null;
2457
+ if (active.type === "text") {
2458
+ selectionStart = active.selectionStart;
2459
+ selectionEnd = active.selectionEnd;
2460
+ }
2461
+ return { selector, fullscreen, selectionStart, selectionEnd };
2462
+ }
2463
+ restoreControlFocus(state) {
2464
+ if (!state) return;
2465
+ const host = state.fullscreen ? this.fullscreenOverlay : this.container;
2466
+ const input = host?.querySelector(state.selector);
2467
+ if (!(input instanceof HTMLInputElement)) return;
2468
+ input.focus({ preventScroll: true });
2469
+ if (input.type === "text" && state.selectionStart != null) {
2470
+ input.setSelectionRange(state.selectionStart, state.selectionEnd);
2471
+ }
2472
+ }
2473
+ drawSurface(surface, layouts, width, height) {
2474
+ if (!surface) return {};
2475
+ return drawChartLayouts({
2476
+ canvas: surface.canvas,
2477
+ width: Math.max(1, width),
2478
+ height: Math.max(1, height),
2479
+ gl: surface.gl,
2480
+ rendererRegistry: surface.renderers,
2481
+ antialiasLines: this.options.antialiasLines,
2482
+ layouts,
2483
+ viewStateRef: { current: this.viewStates },
2484
+ yScaleRef: { current: this.yScales },
2485
+ yCenterOffsetRef: { current: this.yOffsets },
2486
+ initialVisiblePoints: this.options.initialVisiblePoints,
2487
+ getAppendAnimatedPoint: (payload) => this.animator.getPoint(payload),
2488
+ movingAverageByChart: this.movingAverageByChart,
2489
+ seriesOrderByChart: this.options.seriesOrderByChart
2490
+ });
2491
+ }
2492
+ updateCells(layouts, axes, fullscreen) {
2493
+ layouts.forEach(({ chart, descriptor, node }) => {
2494
+ const axis = axes[chart.id];
2495
+ const padding = getPlotPadding(chart, descriptor);
2496
+ const horizontal = axis?.orientation === "horizontal";
2497
+ node.querySelector("[data-title-wrap]").classList.toggle("ring-1", this.focusedChartId === chart.id);
2498
+ node.querySelector("[data-title-wrap]").classList.toggle("ring-primary/70", this.focusedChartId === chart.id);
2499
+ const grid = node.querySelector("[data-grid-lines]");
2500
+ const gridOptions = this.options.gridLines === true ? {} : this.options.gridLines || {};
2501
+ grid.style.display = this.options.gridLines ? "block" : "none";
2502
+ Object.assign(grid.style, {
2503
+ left: `${padding.left}px`,
2504
+ right: `${padding.right}px`,
2505
+ top: `${padding.top}px`,
2506
+ bottom: `${padding.bottom}px`,
2507
+ 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))",
2508
+ backgroundSize: `${Math.max(8, Number(gridOptions.xSpacing) || 80)}px 100%, 100% ${Math.max(8, Number(gridOptions.ySpacing) || 48)}px`
2509
+ });
2510
+ const yAxis = node.querySelector("[data-y-axis]");
2511
+ yAxis.style.backgroundColor = withAlpha(this.options.backgroundColor, 0.78);
2512
+ yAxis.innerHTML = (axis?.ticks || []).map((tick) => horizontal ? `<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.formatYValue(tick.value))}</div>` : `<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("");
2513
+ const xAxis = node.querySelector("[data-x-axis]");
2514
+ xAxis.style.backgroundColor = withAlpha(this.options.backgroundColor, 0.7);
2515
+ xAxis.innerHTML = (axis?.xTicks || []).map((tick) => {
2516
+ const label = tick.categorical ? tick.label : this.options.formatXTick(tick.value);
2517
+ if (horizontal && tick.categorical) {
2518
+ return `<div data-category-label title="${escapeHtml(label)}" class="pointer-events-none absolute inset-x-2 -translate-y-1/2 truncate text-right text-[11px] font-medium text-foreground/70" style="top:${tick.top}px">${escapeHtml(label)}</div>`;
2519
+ }
2520
+ if (tick.categorical) {
2521
+ return `<div data-category-label title="${escapeHtml(label)}" class="pointer-events-none absolute top-1/2 w-24 -translate-x-1/2 -translate-y-1/2 truncate text-center text-[11px] font-medium text-foreground/70" style="left:${tick.left}px">${escapeHtml(label)}</div>`;
2522
+ }
2523
+ return horizontal ? `<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(label)}</div>` : `<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(label)}</div>`;
2524
+ }).join("");
2525
+ const latestLayer = node.querySelector("[data-latest-lines]");
2526
+ 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:${padding.left + item.left}px;top:${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("") : "";
2527
+ this.updateToolbar(node, chart, axis, fullscreen);
2528
+ });
2529
+ }
2530
+ updateToolbar(node, chart, axis, fullscreen) {
2531
+ const target = node.querySelector("[data-toolbar]");
2532
+ const focused = fullscreen || this.focusedChartId === chart.id;
2533
+ const descriptor = this.chartDescriptors.get(chart.id);
2534
+ const drawingsAvailable = descriptor.capabilities.drawings;
2535
+ const movingAverageAvailable = descriptor.capabilities.movingAverage;
2536
+ const padding = getPlotPadding(chart, descriptor);
2537
+ let html = "";
2538
+ 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:${padding.right + 6}px;bottom:${padding.bottom + 6}px;background:${withAlpha(this.options.backgroundColor, 0.7)}">${iconSvg("arrowLineRight", { className: axis.orientation === "horizontal" ? "rotate-90" : "" })}</button>`;
2539
+ if (focused && this.options.showToolbar) {
2540
+ const moving = movingAverageAvailable && Boolean(this.movingAverageByChart[chart.id]?.enabled);
2541
+ const hasDrawings = getDrawingsForChart(this.drawings, chart.id).length > 0;
2542
+ 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">`;
2543
+ html += toolbarButton({ action: "fullscreen", title: fullscreen ? "Exit fullscreen" : "Maximize chart", hotkey: "F", icon: iconSvg(fullscreen ? "arrowsIn" : "arrowsOut") });
2544
+ html += toolbarButton({ action: "reset", title: "Reset chart", hotkey: "R", icon: iconSvg("arrowCounterClockwise") });
2545
+ html += toolbarButton({ action: "rectangle-zoom", title: "Rectangle zoom", hotkey: "Z", active: this.rectangleZoomChartId === chart.id, icon: iconSvg("magnifyingGlassPlus") });
2546
+ if (drawingsAvailable && !this.options.disableDrawings && hasDrawings) html += toolbarButton({ action: "clear-drawings", title: "Clear drawings", className: "text-orange-500", icon: iconSvg("broom") });
2547
+ if ((drawingsAvailable || movingAverageAvailable) && !this.options.disableDrawings) {
2548
+ html += '<div class="my-1 h-px w-full"></div>';
2549
+ if (drawingsAvailable) {
2550
+ html += toolbarButton({ action: "tool-trendline", title: "Draw trendline", hotkey: "T", active: this.activeDrawingTool === "trendline", icon: iconSvg("minus", { className: "-rotate-45" }) });
2551
+ html += toolbarButton({ action: "tool-hline", title: "Draw horizontal line", hotkey: "H", active: this.activeDrawingTool === "hline", icon: iconSvg("minus") });
2552
+ html += toolbarButton({ action: "tool-vline", title: "Draw vertical line", hotkey: "V", active: this.activeDrawingTool === "vline", icon: iconSvg("minus", { className: "rotate-90" }) });
2553
+ html += toolbarButton({ action: "tool-pin", title: "Place pin", hotkey: "P", active: this.activeDrawingTool === "pin", icon: iconSvg("mapPin") });
2554
+ }
2555
+ if (movingAverageAvailable) html += toolbarButton({ action: "moving-average", title: "Toggle moving average", hotkey: "M", active: moving, icon: iconSvg("waveSine") });
2556
+ }
2557
+ html += "</div>";
2558
+ if (movingAverageAvailable && !this.options.disableDrawings && moving) html += this.movingAverageOptionsHtml(chart);
2559
+ }
2560
+ target.innerHTML = html;
2561
+ }
2562
+ movingAverageOptionsHtml(chart) {
2563
+ const moving = this.movingAverageByChart[chart.id] || { enabled: true, period: 21, type: "ema", hideBase: false };
2564
+ 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">
2565
+ <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>
2566
+ <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>
2567
+ <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" />
2568
+ </div>`;
2569
+ }
2570
+ renderOverlays() {
2571
+ const surface = this.fullscreenChartId ? this.fullscreenOverlay : this.container;
2572
+ const overlay = this.fullscreenChartId ? this.fullscreenOverlays : this.overlays;
2573
+ const layouts = this.fullscreenChartId ? this.fullscreenLayouts : this.layouts;
2574
+ if (!overlay) return;
2575
+ overlay.style.height = `${surface.scrollHeight}px`;
2576
+ let html = this.drawingOverlayHtml(layouts);
2577
+ html += this.topMarkersHtml(layouts);
2578
+ html += this.crosshairHtml();
2579
+ html += this.rectangleOverlayHtml();
2580
+ html += this.drawingOptionsHtml(layouts);
2581
+ overlay.innerHTML = html;
2582
+ }
2583
+ drawingOverlayHtml(layouts) {
2584
+ const drawings = this.draftDrawing ? [...this.drawings, this.draftDrawing] : this.drawings;
2585
+ if (!drawings.length) return "";
2586
+ const drawableLayouts = layouts.map((layout, index) => ({
2587
+ layout,
2588
+ index,
2589
+ drawings: layout.descriptor.capabilities.drawings ? getDrawingsForChart(drawings, layout.chart.id) : []
2590
+ })).filter((entry) => entry.drawings.length > 0);
2591
+ if (!drawableLayouts.length) return "";
2592
+ const definitions = drawableLayouts.map(({ layout, index }) => {
2593
+ const clipId = `${this.controllerId}-drawing-clip-${layout.fullscreen ? "fullscreen" : "grid"}-${index}`;
2594
+ return `<clipPath id="${clipId}"><rect data-drawing-clip="${escapeHtml(layout.chart.id)}" x="${layout.plot.x}" y="${layout.plot.y}" width="${layout.plot.width}" height="${layout.plot.height}"/></clipPath>`;
2595
+ }).join("");
2596
+ const body = drawableLayouts.map(({ layout, index, drawings: chartDrawings }) => {
2597
+ const clipId = `${this.controllerId}-drawing-clip-${layout.fullscreen ? "fullscreen" : "grid"}-${index}`;
2598
+ const chartBody = chartDrawings.map((drawing) => {
2599
+ const geometry = getDrawingGeometry({ drawing, layout, projectPoint: (point) => this.projectPoint(point, layout) });
2600
+ if (!geometry) return "";
2601
+ const selected = drawing.id === this.selectedDrawingId || drawing.id === "__draft__";
2602
+ if (drawing.type === "pin") {
2603
+ const text = escapeHtml(geometry.style.text?.trim() || "");
2604
+ 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>`;
2605
+ }
2606
+ 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>`;
2607
+ }).join("");
2608
+ return `<g data-drawing-chart="${escapeHtml(layout.chart.id)}" clip-path="url(#${clipId})">${chartBody}</g>`;
2609
+ }).join("");
2610
+ return `<svg class="pointer-events-none absolute left-0 top-0 z-20 block overflow-visible" style="width:100%;height:100%"><defs>${definitions}</defs>${body}</svg>`;
2611
+ }
2612
+ topMarkersHtml(layouts) {
2613
+ if (!this.options.topMarkers?.length) return "";
2614
+ return layouts.flatMap((layout) => !layout.descriptor.capabilities.markers ? [] : this.options.topMarkers.map((marker, markerIndex) => {
2615
+ const value = Number(marker.x ?? marker.step);
2616
+ const state = this.viewStates.get(layout.chart.id);
2617
+ const ratio = (value - state.xMin) / (state.xMax - state.xMin);
2618
+ if (!Number.isFinite(value) || ratio < 0 || ratio > 1) return "";
2619
+ 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>`;
2620
+ })).join("");
2621
+ }
2622
+ crosshairHtml() {
2623
+ const crosshair = this.options.showTooltips ? this.crosshair : null;
2624
+ if (!crosshair) return "";
2625
+ const chart = this.options.charts.find(
2626
+ (item) => item.id === crosshair.chartId
2627
+ );
2628
+ const xAxisLabel = chart?.xAxisLabel ?? this.options.xAxisLabel;
2629
+ 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>
2630
+ ${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("")}
2631
+ <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(xAxisLabel)}: ${escapeHtml(crosshair.categoryLabel ?? 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>`;
2632
+ }
2633
+ rectangleOverlayHtml() {
2634
+ const rect = this.rectangleZoomRect && normalizeRect(this.rectangleZoomRect.start, this.rectangleZoomRect.end);
2635
+ 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>` : "";
2636
+ }
2637
+ drawingOptionsHtml(layouts) {
2638
+ const drawing = this.drawings.find((item) => item.id === this.selectedDrawingId);
2639
+ if (!drawing) return "";
2640
+ const layout = layouts.find((item) => item.chart.id === drawing.chartId);
2641
+ if (!layout || !layout.descriptor.capabilities.drawings) return "";
2642
+ const style = drawing.style || {};
2643
+ const canExtend = drawing.type === "trendline" || drawing.type === "hline";
2644
+ const canText = drawing.type === "pin";
2645
+ const left = Math.max(
2646
+ layout.rect.x + 42,
2647
+ layout.rect.x + layout.rect.width - PLOT_PADDING.right - 8
2648
+ );
2649
+ 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%)">
2650
+ ${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>` : ""}
2651
+ ${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"/>` : ""}
2652
+ <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"/>
2653
+ </div>`;
2654
+ }
2655
+ projectPoint(point, layout) {
2656
+ const state = this.viewStates.get(layout.chart.id);
2657
+ const range = this.getRange(layout, state);
2658
+ return this.getTransform(layout, state, range).dataToScreen(point);
2659
+ }
2660
+ getRange(layout, state = this.viewStates.get(layout.chart.id)) {
2661
+ return applyYScale(
2662
+ getYRange(
2663
+ layout.chart,
2664
+ state.xMin,
2665
+ state.xMax,
2666
+ getCategoryPixelLength(
2667
+ layout.chart,
2668
+ layout.plot,
2669
+ layout.descriptor
2670
+ ),
2671
+ layout.descriptor
2672
+ ),
2673
+ this.yScales.get(layout.chart.id),
2674
+ this.yOffsets.get(layout.chart.id)
2675
+ );
2676
+ }
2677
+ getTransform(layout, state, range) {
2678
+ return createCoordinateTransform({
2679
+ orientation: layout.descriptor.orientation,
2680
+ categoryRange: { min: state.xMin, max: state.xMax },
2681
+ valueRange: { min: range.minY, max: range.maxY },
2682
+ plot: layout.plot
2683
+ });
2684
+ }
2685
+ pointForEvent(event, host) {
2686
+ const rect = host.getBoundingClientRect();
2687
+ return { x: event.clientX - rect.left + host.scrollLeft, y: event.clientY - rect.top + host.scrollTop };
2688
+ }
2689
+ eventContext(event) {
2690
+ const cell = event.target.closest?.("[data-chart-index]");
2691
+ if (!cell) return null;
2692
+ const fullscreen = Boolean(cell.closest("[data-aliencharts-fullscreen]"));
2693
+ const layouts = fullscreen ? this.fullscreenLayouts : this.layouts;
2694
+ const index = Number(cell.dataset.chartIndex);
2695
+ const layout = fullscreen ? layouts[0] : layouts[index];
2696
+ const host = fullscreen ? this.fullscreenOverlay : this.container;
2697
+ return layout ? { layout, host, point: this.pointForEvent(event, host), fullscreen } : null;
2698
+ }
2699
+ handleClick(event) {
2700
+ const marker = event.target.closest?.("[data-marker-index]");
2701
+ if (marker) {
2702
+ event.stopPropagation();
2703
+ this.options.onTopMarkerClick?.(this.options.topMarkers[Number(marker.dataset.markerIndex)]);
2704
+ return;
2705
+ }
2706
+ const button = event.target.closest?.("[data-action]");
2707
+ if (!button) return;
2708
+ event.preventDefault();
2709
+ event.stopPropagation();
2710
+ const context = this.eventContext(event);
2711
+ const chartId = context?.layout.chart.id || this.focusedChartId || this.fullscreenChartId;
2712
+ switch (button.dataset.action) {
2713
+ case "fullscreen":
2714
+ this.fullscreenChartId ? this.closeFullscreen() : this.openFullscreen(chartId);
2715
+ break;
2716
+ case "reset":
2717
+ this.resetChart(chartId);
2718
+ break;
2719
+ case "jump-latest":
2720
+ this.jumpToLatest(chartId);
2721
+ break;
2722
+ case "rectangle-zoom":
2723
+ this.rectangleZoomChartId = this.rectangleZoomChartId === chartId ? null : chartId;
2724
+ this.activeDrawingTool = null;
2725
+ this.emit("onActiveDrawingToolChange", null);
2726
+ this.requestRender();
2727
+ break;
2728
+ case "clear-drawings":
2729
+ this.options.onClearDrawingsRequest ? this.emit("onClearDrawingsRequest", chartId) : this.setDrawings(this.drawings.filter((item) => item.chartId !== chartId));
2730
+ break;
2731
+ case "moving-average":
2732
+ this.toggleMovingAverage(chartId);
2733
+ break;
2734
+ case "tool-trendline":
2735
+ this.toggleDrawingTool("trendline");
2736
+ break;
2737
+ case "tool-hline":
2738
+ this.toggleDrawingTool("hline");
2739
+ break;
2740
+ case "tool-vline":
2741
+ this.toggleDrawingTool("vline");
2742
+ break;
2743
+ case "tool-pin":
2744
+ this.toggleDrawingTool("pin");
2745
+ break;
2746
+ case "toggle-base":
2747
+ this.updateMovingAverage(chartId, { hideBase: !this.movingAverageByChart[chartId]?.hideBase });
2748
+ break;
2749
+ case "moving-type":
2750
+ this.updateMovingAverage(chartId, { type: this.movingAverageByChart[chartId]?.type === "sma" ? "ema" : "sma" });
2751
+ break;
2752
+ case "extend-drawing":
2753
+ this.updateSelectedDrawing((drawing) => ({ ...drawing, style: { ...drawing.style, extendRight: drawing.style?.extendRight === false } }));
2754
+ break;
2755
+ default:
2756
+ break;
2757
+ }
2758
+ }
2759
+ handleInput(event) {
2760
+ if (event.target.matches("[data-moving-period]")) {
2761
+ const context = this.eventContext(event);
2762
+ this.updateMovingAverage(context?.layout.chart.id, { period: Math.max(1, Math.round(Number(event.target.value) || 1)) });
2763
+ } else if (event.target.matches("[data-pin-text]")) {
2764
+ this.updateSelectedDrawing((drawing) => ({ ...drawing, style: { ...drawing.style, text: event.target.value } }));
2765
+ } else if (event.target.matches("[data-drawing-color]")) {
2766
+ this.updateSelectedDrawing((drawing) => ({ ...drawing, style: { ...drawing.style, color: event.target.value } }));
2767
+ }
2768
+ }
2769
+ handlePointerDown(event) {
2770
+ if (event.target.closest?.("button,input")) return;
2771
+ if (event.button !== 0) return;
2772
+ const context = this.eventContext(event);
2773
+ if (!context) return;
2774
+ const { layout, point } = context;
2775
+ this.activePointerContext = context;
2776
+ this.focusedChartId = layout.chart.id;
2777
+ 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;
2778
+ if (inPlot && this.activeDrawingTool && !this.options.disableDrawings && layout.descriptor.capabilities.drawings) {
2779
+ const dataPoint = screenPointToDataPoint({ point, chart: layout.chart, descriptor: layout.descriptor, plot: layout.plot, initialVisiblePoints: this.options.initialVisiblePoints, viewStateRef: { current: this.viewStates }, yScaleRef: { current: this.yScales }, yCenterOffsetRef: { current: this.yOffsets } });
2780
+ if (["hline", "vline", "pin"].includes(this.activeDrawingTool)) {
2781
+ this.commitDrawing(layout.chart.id, this.activeDrawingTool, dataPoint, dataPoint);
2782
+ } else if (this.drawingSession?.chartId === layout.chart.id) {
2783
+ this.commitDrawing(layout.chart.id, "trendline", this.drawingSession.start, dataPoint);
2784
+ } else {
2785
+ this.drawingSession = { chartId: layout.chart.id, type: "trendline", start: dataPoint };
2786
+ this.draftDrawing = createDraftDrawing({ chartId: layout.chart.id, type: "trendline", start: dataPoint, end: dataPoint });
2787
+ }
2788
+ } else if (inPlot && this.rectangleZoomChartId === layout.chart.id) {
2789
+ this.drag = { type: "rectangle", layout, start: point };
2790
+ this.rectangleZoomRect = { start: point, end: point };
2791
+ } else if (inPlot) {
2792
+ const hit = layout.descriptor.capabilities.drawings ? this.hitDrawing(point, layout) : null;
2793
+ if (hit) {
2794
+ this.selectedDrawingId = hit.drawing.id;
2795
+ this.emit("onSelectedDrawingIdChange", hit.drawing.id);
2796
+ if (hit.endpoint != null || ["hline", "vline", "pin"].includes(hit.drawing.type)) {
2797
+ this.drag = { type: "drawing-edit", layout, drawingId: hit.drawing.id, endpoint: hit.endpoint };
2798
+ }
2799
+ } else {
2800
+ this.selectedDrawingId = null;
2801
+ this.emit("onSelectedDrawingIdChange", null);
2802
+ this.drag = { type: "pan", layout, start: point, state: { ...this.viewStates.get(layout.chart.id) }, yOffset: this.yOffsets.get(layout.chart.id) || 0 };
2803
+ }
2804
+ } else if (layout.descriptor.orientation === "vertical" && point.x >= layout.plot.x + layout.plot.width) {
2805
+ this.drag = { type: "y-scale", layout, start: point, scale: this.yScales.get(layout.chart.id) || 1 };
2806
+ } else if (layout.descriptor.orientation === "horizontal" && point.y >= layout.plot.y + layout.plot.height) {
2807
+ this.drag = { type: "y-scale", layout, start: point, scale: this.yScales.get(layout.chart.id) || 1 };
2808
+ } else if (layout.descriptor.orientation === "vertical" && point.y >= layout.plot.y + layout.plot.height) {
2809
+ this.drag = { type: "x-scale", layout, start: point, state: { ...this.viewStates.get(layout.chart.id) } };
2810
+ } else if (layout.descriptor.orientation === "horizontal" && point.x <= layout.plot.x) {
2811
+ this.drag = { type: "x-scale", layout, start: point, state: { ...this.viewStates.get(layout.chart.id) } };
2812
+ }
2813
+ context.host.setPointerCapture?.(event.pointerId);
2814
+ this.pointerCaptureTarget = context.host;
2815
+ this.requestRender();
2816
+ }
2817
+ handlePointerMove(event) {
2818
+ if (event.target.closest?.("button,input") && !this.drag && !this.drawingSession) return;
2819
+ const context = this.eventContext(event) || (this.activePointerContext && (this.drag || this.drawingSession) ? { ...this.activePointerContext, point: this.pointForEvent(event, this.activePointerContext.host) } : null);
2820
+ if (!context) return this.clearCrosshair();
2821
+ const { layout, point } = context;
2822
+ if (this.drawingSession) {
2823
+ if (layout.chart.id !== this.drawingSession.chartId) return;
2824
+ const end = screenPointToDataPoint({ point, chart: layout.chart, descriptor: layout.descriptor, plot: layout.plot, initialVisiblePoints: this.options.initialVisiblePoints, viewStateRef: { current: this.viewStates }, yScaleRef: { current: this.yScales }, yCenterOffsetRef: { current: this.yOffsets } });
2825
+ this.draftDrawing = createDraftDrawing({ ...this.drawingSession, end });
2826
+ this.requestRender();
2827
+ return;
2828
+ }
2829
+ if (this.drag) {
2830
+ const { chart, descriptor, plot } = this.drag.layout;
2831
+ if (this.drag.type === "drawing-edit") {
2832
+ const dataPoint = screenPointToDataPoint({ point, chart, descriptor, plot, initialVisiblePoints: this.options.initialVisiblePoints, viewStateRef: { current: this.viewStates }, yScaleRef: { current: this.yScales }, yCenterOffsetRef: { current: this.yOffsets } });
2833
+ const next = updateDrawingById(this.drawings, this.drag.drawingId, (drawing) => {
2834
+ if (drawing.type === "hline") {
2835
+ const deltaX = (drawing.end?.x ?? drawing.start.x) - drawing.start.x;
2836
+ return { ...drawing, start: { x: dataPoint.x, y: dataPoint.y }, end: { x: dataPoint.x + deltaX, y: dataPoint.y } };
2837
+ }
2838
+ if (drawing.type === "vline") return { ...drawing, start: { ...drawing.start, x: dataPoint.x }, end: { ...drawing.end, x: dataPoint.x } };
2839
+ if (drawing.type === "pin") return { ...drawing, start: dataPoint, end: dataPoint };
2840
+ if (this.drag.endpoint === "start" || this.drag.endpoint === "end") return { ...drawing, [this.drag.endpoint]: dataPoint };
2841
+ return drawing;
2842
+ });
2843
+ this.drawings = next;
2844
+ this.emit("onDrawingsChange", [...next]);
2845
+ this.requestRender();
2846
+ return;
2847
+ }
2848
+ if (this.drag.type === "rectangle") this.rectangleZoomRect = { start: this.drag.start, end: point };
2849
+ if (this.drag.type === "pan") {
2850
+ const span = this.drag.state.xMax - this.drag.state.xMin;
2851
+ const baseRange = getYRange(
2852
+ chart,
2853
+ this.drag.state.xMin,
2854
+ this.drag.state.xMax,
2855
+ getCategoryPixelLength(chart, plot, descriptor),
2856
+ descriptor
2857
+ );
2858
+ const rangeSpan = (baseRange.maxY - baseRange.minY) * (this.yScales.get(chart.id) || 1);
2859
+ const transform = createCoordinateTransform({
2860
+ orientation: descriptor.orientation,
2861
+ categoryRange: {
2862
+ min: this.drag.state.xMin,
2863
+ max: this.drag.state.xMax
2864
+ },
2865
+ valueRange: { min: 0, max: rangeSpan },
2866
+ plot
2867
+ });
2868
+ const categoryDelta = transform.categoryDragDelta(
2869
+ this.drag.start,
2870
+ point
2871
+ );
2872
+ const valueDelta = transform.valueOffsetDelta(
2873
+ this.drag.start,
2874
+ point
2875
+ );
2876
+ this.viewStates.set(chart.id, { xMin: this.drag.state.xMin - categoryDelta, xMax: this.drag.state.xMax - categoryDelta });
2877
+ this.yOffsets.set(chart.id, this.drag.yOffset + valueDelta);
2878
+ }
2879
+ if (this.drag.type === "y-scale") {
2880
+ const transform = createCoordinateTransform({
2881
+ orientation: descriptor.orientation,
2882
+ categoryRange: { min: 0, max: 1 },
2883
+ valueRange: { min: 0, max: 1 },
2884
+ plot
2885
+ });
2886
+ const delta = transform.valueScaleDelta(this.drag.start, point);
2887
+ this.yScales.set(
2888
+ chart.id,
2889
+ scaleValueRange(
2890
+ this.drag.scale,
2891
+ delta,
2892
+ POINTER_SCALE_SENSITIVITY
2893
+ )
2894
+ );
2895
+ }
2896
+ if (this.drag.type === "x-scale") {
2897
+ const center = (this.drag.state.xMin + this.drag.state.xMax) / 2;
2898
+ const transform = createCoordinateTransform({
2899
+ orientation: descriptor.orientation,
2900
+ categoryRange: { min: 0, max: 1 },
2901
+ valueRange: { min: 0, max: 1 },
2902
+ plot
2903
+ });
2904
+ const delta = transform.categoryScaleDelta(this.drag.start, point);
2905
+ const span = clamp(
2906
+ (this.drag.state.xMax - this.drag.state.xMin) * Math.exp(delta * 0.01),
2907
+ getMinimumCategorySpan(chart),
2908
+ X_SCALE_MAX_SPAN
2909
+ );
2910
+ this.viewStates.set(chart.id, { xMin: center - span / 2, xMax: center + span / 2 });
2911
+ }
2912
+ if (this.drag.type === "pan") {
2913
+ this.updateCrosshair(point, layout);
2914
+ this.requestRender();
2915
+ } else {
2916
+ this.requestRender();
2917
+ }
2918
+ return;
2919
+ }
2920
+ this.updateCrosshair(point, layout, context.fullscreen);
2921
+ }
2922
+ handlePointerUp(event) {
2923
+ if (event.target.closest?.("button,input") && !this.drag && !this.drawingSession) return;
2924
+ const context = this.eventContext(event) || (this.activePointerContext ? { ...this.activePointerContext, point: this.pointForEvent(event, this.activePointerContext.host) } : null);
2925
+ if (this.drag?.type === "rectangle" && this.rectangleZoomRect) {
2926
+ const normalized = normalizeRect(this.rectangleZoomRect.start, this.rectangleZoomRect.end);
2927
+ if (normalized?.width >= 4 && normalized?.height >= 4) {
2928
+ const layout = this.drag.layout;
2929
+ applyRectangleZoom({
2930
+ chart: layout.chart,
2931
+ descriptor: layout.descriptor,
2932
+ plot: layout.plot,
2933
+ start: this.rectangleZoomRect.start,
2934
+ end: this.rectangleZoomRect.end,
2935
+ initialVisiblePoints: this.options.initialVisiblePoints,
2936
+ viewStateRef: { current: this.viewStates },
2937
+ yScaleRef: { current: this.yScales },
2938
+ yCenterOffsetRef: { current: this.yOffsets },
2939
+ yManualScaleRef: { current: /* @__PURE__ */ new Set() }
2940
+ });
2941
+ }
2942
+ this.rectangleZoomChartId = null;
2943
+ this.rectangleZoomRect = null;
2944
+ }
2945
+ this.drag = null;
2946
+ this.pointerCaptureTarget?.releasePointerCapture?.(event.pointerId);
2947
+ this.pointerCaptureTarget = null;
2948
+ this.activePointerContext = null;
2949
+ this.requestRender();
2950
+ }
2951
+ handleWheel(event) {
2952
+ const context = this.eventContext(event);
2953
+ if (!context) return;
2954
+ const { layout, point } = context;
2955
+ 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;
2956
+ if (!inPlot) return;
2957
+ event.preventDefault();
2958
+ if (event.shiftKey) {
2959
+ const wheelDelta = event.deltaY || event.deltaX;
2960
+ const currentScale = this.yScales.get(layout.chart.id) || 1;
2961
+ this.yScales.set(
2962
+ layout.chart.id,
2963
+ scaleValueRange(
2964
+ currentScale,
2965
+ wheelDelta,
2966
+ WHEEL_SCALE_SENSITIVITY
2967
+ )
2968
+ );
2969
+ this.requestRender();
2970
+ return;
2971
+ }
2972
+ const state = this.viewStates.get(layout.chart.id);
2973
+ const range = this.getRange(layout, state);
2974
+ const ratio = clamp(
2975
+ this.getTransform(layout, state, range).categoryRatio(point),
2976
+ 0,
2977
+ 1
2978
+ );
2979
+ const anchor = state.xMin + ratio * (state.xMax - state.xMin);
2980
+ const zoom = Math.exp(event.deltaY * 1e-3);
2981
+ const span = clamp(
2982
+ (state.xMax - state.xMin) * zoom,
2983
+ getMinimumCategorySpan(layout.chart),
2984
+ X_SCALE_MAX_SPAN
2985
+ );
2986
+ this.viewStates.set(layout.chart.id, { xMin: anchor - ratio * span, xMax: anchor + (1 - ratio) * span });
2987
+ this.requestRender();
2988
+ }
2989
+ handleContextMenu(event) {
2990
+ const context = this.eventContext(event);
2991
+ if (!context || !this.options.onChartContextMenu) return;
2992
+ event.preventDefault();
2993
+ const { layout, point } = context;
2994
+ 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;
2995
+ const data = inPlot ? screenPointToDataPoint({ point, chart: layout.chart, descriptor: layout.descriptor, plot: layout.plot, initialVisiblePoints: this.options.initialVisiblePoints, viewStateRef: { current: this.viewStates }, yScaleRef: { current: this.yScales }, yCenterOffsetRef: { current: this.yOffsets } }) : null;
2996
+ this.options.onChartContextMenu({ chart: layout.chart, event, point: data });
2997
+ }
2998
+ updateCrosshair(point, layout) {
2999
+ 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;
3000
+ if (!inPlot || !this.options.showTooltips) return this.clearCrosshair();
3001
+ const state = this.viewStates.get(layout.chart.id);
3002
+ const orientation = layout.descriptor.orientation;
3003
+ const range = this.getRange(layout, state);
3004
+ const transform = this.getTransform(layout, state, range);
3005
+ const xValue = transform.screenToData(point).x;
3006
+ const tooltipSeries = getOrderedSeries(
3007
+ layout.chart,
3008
+ this.options.seriesOrderByChart
3009
+ );
3010
+ const points = tooltipSeries.map((series, seriesIndex) => {
3011
+ const visible = series.getVisiblePoints(state.xMin, state.xMax, transform.categoryPixelLength);
3012
+ if (!visible.pointCount) return null;
3013
+ const index = getNearestPointIndex(visible.x, xValue);
3014
+ const category = this.surface.renderers.getTooltipCategory(
3015
+ layout.descriptor.rendererType,
3016
+ {
3017
+ visiblePoints: visible,
3018
+ pointIndex: index,
3019
+ seriesIndex,
3020
+ seriesCount: tooltipSeries.length,
3021
+ categoryMin: state.xMin,
3022
+ categoryMax: state.xMax
3023
+ }
3024
+ );
3025
+ const data = { x: category, y: visible.y[index] };
3026
+ const screen = transform.dataToScreen(data);
3027
+ return { id: series.id, name: series.name, color: series.color, x: screen.x, y: screen.y, xValue: visible.x[index], yValue: data.y };
3028
+ }).filter(Boolean);
3029
+ if (!points.length) return this.clearCrosshair();
3030
+ const nearest = points.reduce((best, item) => {
3031
+ const itemDistance = orientation === "horizontal" ? Math.abs(item.y - point.y) : Math.abs(item.x - point.x);
3032
+ const bestDistance = orientation === "horizontal" ? Math.abs(best.y - point.y) : Math.abs(best.x - point.x);
3033
+ return itemDistance < bestDistance ? item : best;
3034
+ });
3035
+ this.crosshair = {
3036
+ chartId: layout.chart.id,
3037
+ x: point.x,
3038
+ y: point.y,
3039
+ xValue: nearest.xValue,
3040
+ categoryLabel: getCategoryLabel(layout.chart, nearest.xValue),
3041
+ points,
3042
+ tooltipX: point.x + 232 > layout.rect.x + layout.rect.width ? point.x - 232 : point.x + 12,
3043
+ tooltipY: clamp(point.y + 12, layout.plot.y, layout.rect.y + layout.rect.height - 96)
3044
+ };
3045
+ this.requestRender();
3046
+ }
3047
+ clearCrosshair() {
3048
+ if (!this.crosshair) return;
3049
+ this.crosshair = null;
3050
+ this.requestRender();
3051
+ }
3052
+ hitDrawing(point, layout) {
3053
+ for (const drawing of getDrawingsForChart(this.drawings, layout.chart.id)) {
3054
+ const hit = hitTestDrawing({ point, drawing, layout, projectPoint: (data) => this.projectPoint(data, layout) });
3055
+ if (hit) return hit;
3056
+ }
3057
+ return null;
3058
+ }
3059
+ commitDrawing(chartId, type, start, end) {
3060
+ const drawing = createDrawing({ chartId, type, start, end, createDrawingId: this.options.createDrawingId });
3061
+ this.setDrawings([...this.drawings, drawing]);
3062
+ this.selectedDrawingId = drawing.id;
3063
+ this.activeDrawingTool = null;
3064
+ this.draftDrawing = null;
3065
+ this.drawingSession = null;
3066
+ this.emit("onSelectedDrawingIdChange", drawing.id);
3067
+ this.emit("onActiveDrawingToolChange", null);
3068
+ }
3069
+ setDrawings(drawings) {
3070
+ this.drawings = drawings;
3071
+ this.emit("onDrawingsChange", [...drawings]);
3072
+ this.requestRender();
3073
+ }
3074
+ updateSelectedDrawing(updater) {
3075
+ if (!this.selectedDrawingId) return;
3076
+ this.setDrawings(updateDrawingById(this.drawings, this.selectedDrawingId, updater));
3077
+ }
3078
+ toggleDrawingTool(tool) {
3079
+ if (!DRAWING_TOOLS.has(tool) || this.options.disableDrawings) return;
3080
+ this.activeDrawingTool = this.activeDrawingTool === tool ? null : tool;
3081
+ this.rectangleZoomChartId = null;
3082
+ this.draftDrawing = null;
3083
+ this.drawingSession = null;
3084
+ this.emit("onActiveDrawingToolChange", this.activeDrawingTool);
3085
+ this.requestRender();
3086
+ }
3087
+ toggleMovingAverage(chartId) {
3088
+ if (!chartId || this.options.disableDrawings) return;
3089
+ const chart = this.options.charts.find((item) => item.id === chartId);
3090
+ if (!chart || !this.chartDescriptors.get(chartId)?.capabilities.movingAverage) return;
3091
+ const current = this.movingAverageByChart[chartId] || { period: 21, type: "ema", hideBase: false };
3092
+ this.movingAverageByChart = { ...this.movingAverageByChart, [chartId]: { ...current, enabled: !current.enabled } };
3093
+ this.emit("onMovingAverageToggle", chartId);
3094
+ this.requestRender();
3095
+ }
3096
+ updateMovingAverage(chartId, changes) {
3097
+ if (!chartId) return;
3098
+ const next = { enabled: true, period: 21, type: "ema", hideBase: false, ...this.movingAverageByChart[chartId] || {}, ...changes };
3099
+ this.movingAverageByChart = { ...this.movingAverageByChart, [chartId]: next };
3100
+ this.emit("onMovingAverageChange", chartId, { ...next });
3101
+ this.requestRender();
3102
+ }
3103
+ resetChart(chartId) {
3104
+ const chart = this.options.charts.find((item) => item.id === chartId);
3105
+ if (!chart) return;
3106
+ this.viewStates.set(chartId, getInitialView(chart, this.options.initialVisiblePoints));
3107
+ this.yScales.set(chartId, 1);
3108
+ this.yOffsets.set(chartId, 0);
3109
+ this.requestRender();
3110
+ }
3111
+ openFullscreen(chartId) {
3112
+ const chart = this.options.charts.find((item) => item.id === chartId);
3113
+ if (!chart || this.fullscreenChartId) return;
3114
+ this.fullscreenChartId = chartId;
3115
+ const overlay = document.createElement("div");
3116
+ overlay.dataset.alienchartsFullscreen = "";
3117
+ overlay.className = "aliencharts-root fixed inset-0 z-[60] bg-background p-2";
3118
+ overlay.innerHTML = `<canvas class="pointer-events-none absolute left-0 top-0 z-10 block"></canvas><div data-fullscreen-cell>${chartCellHtml(chart, this.chartDescriptors.get(chart.id), 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>`;
3119
+ document.body.append(overlay);
3120
+ this.fullscreenOverlay = overlay;
3121
+ this.fullscreenNode = overlay.querySelector("[data-chart-index]");
3122
+ this.fullscreenNode.style.height = "calc(100vh - 16px)";
3123
+ this.fullscreenOverlays = overlay.querySelector("[data-fullscreen-overlays]");
3124
+ const canvas = overlay.querySelector("canvas");
3125
+ const gl = canvas.getContext("webgl2", { antialias: true, alpha: true, depth: false, stencil: false });
3126
+ if (!gl) {
3127
+ overlay.remove();
3128
+ this.fullscreenChartId = null;
3129
+ return;
3130
+ }
3131
+ this.fullscreenSurface = makeSurface(canvas, gl);
3132
+ this.fullscreenListeners = Object.entries(this.bound).filter(([name]) => name !== "keydown" && name !== "scroll");
3133
+ this.fullscreenListeners.forEach(([name, handler]) => overlay.addEventListener(name, handler, name === "wheel" ? { passive: false } : void 0));
3134
+ this.resizeObserver.observe(overlay);
3135
+ this.requestRender();
3136
+ }
3137
+ closeFullscreen() {
3138
+ if (!this.fullscreenChartId) return;
3139
+ if (this.fullscreenOverlay) this.resizeObserver?.unobserve(this.fullscreenOverlay);
3140
+ this.fullscreenListeners?.forEach(([name, handler]) => this.fullscreenOverlay?.removeEventListener(name, handler));
3141
+ this.fullscreenListeners = null;
3142
+ destroySurface(this.fullscreenSurface);
3143
+ this.fullscreenOverlay?.remove();
3144
+ this.fullscreenSurface = null;
3145
+ this.fullscreenOverlay = null;
3146
+ this.fullscreenNode = null;
3147
+ this.fullscreenOverlays = null;
3148
+ this.fullscreenLayouts = [];
3149
+ this.fullscreenChartId = null;
3150
+ this.requestRender();
3151
+ }
3152
+ handleKeyDown(event) {
3153
+ const editable = event.target instanceof HTMLElement && (event.target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(event.target.tagName));
3154
+ if (editable) return;
3155
+ const chartId = this.fullscreenChartId || this.focusedChartId;
3156
+ const chart = this.options.charts.find((item) => item.id === chartId);
3157
+ const capabilities = chart && this.chartDescriptors.get(chartId)?.capabilities;
3158
+ const key = event.key.toLowerCase();
3159
+ if (["arrowleft", "arrowright", "arrowup", "arrowdown"].includes(key) && !this.fullscreenChartId) {
3160
+ this.moveFocus(key.replace("arrow", ""));
3161
+ event.preventDefault();
3162
+ return;
3163
+ }
3164
+ if (key === "r") {
3165
+ this.resetChart(chartId);
3166
+ event.preventDefault();
3167
+ } else if (key === "z") {
3168
+ this.rectangleZoomChartId = this.rectangleZoomChartId === chartId ? null : chartId;
3169
+ event.preventDefault();
3170
+ this.requestRender();
3171
+ } else if (key === "m" && capabilities?.movingAverage) {
3172
+ this.toggleMovingAverage(chartId);
3173
+ event.preventDefault();
3174
+ } else if (["t", "h", "v", "p"].includes(key) && capabilities?.drawings) {
3175
+ this.toggleDrawingTool({ t: "trendline", h: "hline", v: "vline", p: "pin" }[key]);
3176
+ event.preventDefault();
3177
+ } else if ((key === "delete" || key === "backspace") && this.selectedDrawingId) {
3178
+ this.setDrawings(removeDrawingById(this.drawings, this.selectedDrawingId));
3179
+ this.selectedDrawingId = null;
3180
+ this.emit("onSelectedDrawingIdChange", null);
3181
+ event.preventDefault();
3182
+ } else if (key === "escape" && (this.activeDrawingTool || this.draftDrawing || this.rectangleZoomChartId)) {
3183
+ this.activeDrawingTool = null;
3184
+ this.draftDrawing = null;
3185
+ this.drawingSession = null;
3186
+ this.rectangleZoomChartId = null;
3187
+ this.rectangleZoomRect = null;
3188
+ this.emit("onActiveDrawingToolChange", null);
3189
+ this.requestRender();
3190
+ event.preventDefault();
3191
+ } else if (key === "f" || key === "escape" && this.fullscreenChartId) {
3192
+ this.fullscreenChartId ? this.closeFullscreen() : this.openFullscreen(chartId);
3193
+ event.preventDefault();
3194
+ }
3195
+ }
3196
+ moveFocus(direction) {
3197
+ const charts = this.options.charts;
3198
+ if (!charts.length) return;
3199
+ let index = Math.max(0, charts.findIndex((chart) => chart.id === this.focusedChartId));
3200
+ const columns = Math.max(1, Number(this.options.columns) || 1);
3201
+ if (direction === "left") index = Math.max(Math.floor(index / columns) * columns, index - 1);
3202
+ if (direction === "right") index = Math.min(charts.length - 1, index + 1);
3203
+ if (direction === "up") index = Math.max(0, index - columns);
3204
+ if (direction === "down") index = Math.min(charts.length - 1, index + columns);
3205
+ this.focusedChartId = charts[index].id;
3206
+ this.chartNodes[index]?.scrollIntoView({ block: "nearest", inline: "nearest" });
3207
+ this.requestRender();
3208
+ }
3209
+ emit(name, ...args) {
3210
+ this.options[name]?.(...args);
3211
+ }
3212
+ cleanupDom() {
3213
+ this.canvas?.remove();
3214
+ this.grid?.remove();
3215
+ this.overlays?.remove();
3216
+ this.addedClasses?.forEach((name) => this.container.classList.remove(name));
3217
+ }
3218
+ destroy() {
3219
+ if (this.destroyed) return;
3220
+ this.closeFullscreen();
3221
+ this.destroyed = true;
3222
+ if (this.frame != null) cancelAnimationFrame(this.frame);
3223
+ this.animator.destroy();
3224
+ this.resizeObserver.disconnect();
3225
+ Object.entries(this.bound).forEach(([name, handler]) => {
3226
+ const target = name === "keydown" ? window : this.container;
3227
+ target.removeEventListener(name, handler);
3228
+ });
3229
+ destroySurface(this.surface);
3230
+ this.cleanupDom();
3231
+ }
3232
+ };
3233
+ var createChartGrid = (container, options = {}) => new ChartGridController(container, options);
3234
+
3235
+ // src/core/mockData.js
3236
+ var palette = [
3237
+ "#38bdf8",
3238
+ "#22c55e",
3239
+ "#f59e0b",
3240
+ "#ef4444",
3241
+ "#a855f7",
3242
+ "#14b8a6"
3243
+ ];
3244
+ var createRandom = (seed) => {
3245
+ let value = seed >>> 0;
3246
+ return () => {
3247
+ value = value * 1664525 + 1013904223 >>> 0;
3248
+ return value / 4294967296;
3249
+ };
3250
+ };
3251
+ var makeSeries = ({ chartIndex, seriesIndex, pointCount }) => {
3252
+ const x = new Float64Array(pointCount);
3253
+ const y = new Float32Array(pointCount);
3254
+ const random = createRandom((chartIndex + 1) * 1009 + seriesIndex * 9176);
3255
+ const baseline = 15 + random() * 45 + seriesIndex * 4;
3256
+ const waveA = 8e-4 + random() * 4e-3;
3257
+ const waveB = 6e-3 + random() * 0.03;
3258
+ const waveC = 5e-5 + random() * 6e-4;
3259
+ const amplitudeA = 0.015 + random() * 0.09;
3260
+ const amplitudeB = 5e-3 + random() * 0.04;
3261
+ const trend = (random() - 0.5) * 4e-3;
3262
+ const noise = 4e-3 + random() * 0.025;
3263
+ const spikeInterval = 180 + Math.floor(random() * 900);
3264
+ const spikeMagnitude = 0.12 + random() * 0.7;
3265
+ let value = baseline;
3266
+ for (let i = 0; i < pointCount; i += 1) {
3267
+ x[i] = i;
3268
+ const spike = (i + chartIndex * 13 + seriesIndex * 31) % spikeInterval === 0 ? spikeMagnitude * (random() > 0.45 ? 1 : -1) : 0;
3269
+ 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;
3270
+ y[i] = value;
3271
+ }
3272
+ return createLineSeries({
3273
+ id: `chart-${chartIndex}-series-${seriesIndex}`,
3274
+ name: `Run ${seriesIndex + 1}`,
3275
+ color: palette[(chartIndex + seriesIndex) % palette.length],
3276
+ x,
3277
+ y
3278
+ });
3279
+ };
3280
+ var createMockCharts = ({
3281
+ chartCount = 50,
3282
+ seriesPerChart = 2,
3283
+ pointCount = 5e5
3284
+ } = {}) => {
3285
+ return Array.from({ length: chartCount }, (_, chartIndex) => ({
3286
+ id: `metric-${chartIndex + 1}`,
3287
+ title: `metric_${String(chartIndex + 1).padStart(2, "0")}`,
3288
+ series: Array.from(
3289
+ { length: seriesPerChart },
3290
+ (_2, seriesIndex) => makeSeries({ chartIndex, seriesIndex, pointCount })
3291
+ )
3292
+ }));
3293
+ };
3294
+ export {
3295
+ BarSeries,
3296
+ LineSeries,
3297
+ createBarSeries,
3298
+ createChartGrid,
3299
+ createLineSeries,
3300
+ createMockCharts,
3301
+ createSeries
3302
+ };
3303
+ //# sourceMappingURL=vanilla.js.map