aliencharts 0.3.0 → 0.3.2

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