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