aliencharts 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs DELETED
@@ -1,5005 +0,0 @@
1
- var __create = Object.create;
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getProtoOf = Object.getPrototypeOf;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
- // If the importer is in node compatibility mode or this is not an ESM
21
- // file that has been converted to a CommonJS file using a Babel-
22
- // compatible transform (i.e. "__esModule" has not been set), then set
23
- // "default" to the CommonJS "module.exports" for node compatibility.
24
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
- mod
26
- ));
27
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
-
29
- // src/index.js
30
- var index_exports = {};
31
- __export(index_exports, {
32
- ChartGrid: () => ChartGrid,
33
- createMockCharts: () => createMockCharts,
34
- createSeries: () => createSeries
35
- });
36
- module.exports = __toCommonJS(index_exports);
37
-
38
- // src/react/ChartGrid.jsx
39
- var import_react9 = __toESM(require("react"), 1);
40
- var import_react10 = require("@phosphor-icons/react");
41
-
42
- // src/core/lodSeries.js
43
- var DEFAULT_MAX_LEVELS = 18;
44
- var toTypedArray = (value, Type) => {
45
- if (value instanceof Type) return value;
46
- if (ArrayBuffer.isView(value)) return new Type(value);
47
- if (Array.isArray(value)) return new Type(value);
48
- return new Type(0);
49
- };
50
- var lowerBound = (array, value) => {
51
- let lo = 0;
52
- let hi = array.length;
53
- while (lo < hi) {
54
- const mid = lo + hi >> 1;
55
- if (array[mid] < value) lo = mid + 1;
56
- else hi = mid;
57
- }
58
- return lo;
59
- };
60
- var upperBound = (array, value) => {
61
- let lo = 0;
62
- let hi = array.length;
63
- while (lo < hi) {
64
- const mid = lo + hi >> 1;
65
- if (array[mid] <= value) lo = mid + 1;
66
- else hi = mid;
67
- }
68
- return lo;
69
- };
70
- var pushUniqueSorted = (indices, index) => {
71
- if (index < 0) return;
72
- if (indices.includes(index)) return;
73
- indices.push(index);
74
- };
75
- var buildLod = (rawX, rawY, bucketSize, startIndex = 0, endIndex = rawX.length) => {
76
- if (bucketSize <= 1 || rawX.length <= bucketSize) {
77
- return { x: rawX, y: rawY, bucketSize: 1 };
78
- }
79
- const alignedStart = Math.max(0, Math.floor(startIndex / bucketSize) * bucketSize);
80
- const alignedEnd = Math.min(rawX.length, endIndex);
81
- const estimated = Math.ceil((alignedEnd - alignedStart) / bucketSize) * 4;
82
- const x = new Float64Array(estimated);
83
- const y = new Float32Array(estimated);
84
- let out = 0;
85
- for (let start = alignedStart; start < alignedEnd; start += bucketSize) {
86
- const end = Math.min(alignedEnd, start + bucketSize);
87
- let minIndex = start;
88
- let maxIndex = start;
89
- for (let i = start + 1; i < end; i += 1) {
90
- if (rawY[i] < rawY[minIndex]) minIndex = i;
91
- if (rawY[i] > rawY[maxIndex]) maxIndex = i;
92
- }
93
- const indices = [];
94
- pushUniqueSorted(indices, start);
95
- pushUniqueSorted(indices, minIndex);
96
- pushUniqueSorted(indices, maxIndex);
97
- pushUniqueSorted(indices, end - 1);
98
- indices.sort((a, b) => a - b);
99
- for (let i = 0; i < indices.length; i += 1) {
100
- const sourceIndex = indices[i];
101
- x[out] = rawX[sourceIndex];
102
- y[out] = rawY[sourceIndex];
103
- out += 1;
104
- }
105
- }
106
- return {
107
- x: x.subarray(0, out),
108
- y: y.subarray(0, out),
109
- bucketSize
110
- };
111
- };
112
- var concatTyped = (Type, left, right) => {
113
- if (!left?.length) return right;
114
- if (!right?.length) return left;
115
- const out = new Type(left.length + right.length);
116
- out.set(left, 0);
117
- out.set(right, left.length);
118
- return out;
119
- };
120
- var LineSeries = class {
121
- constructor({ id, name, color, x, y, maxLevels = DEFAULT_MAX_LEVELS }) {
122
- this.id = id;
123
- this.name = name || id;
124
- this.color = color || "#38bdf8";
125
- this.maxLevels = maxLevels;
126
- const initialX = toTypedArray(x, Float64Array);
127
- const initialY = toTypedArray(y, Float32Array);
128
- this._length = Math.min(initialX.length, initialY.length);
129
- const capacity = Math.max(1, this._length);
130
- this.rawX = new Float64Array(capacity);
131
- this.rawY = new Float32Array(capacity);
132
- this.rawX.set(initialX.subarray(0, this._length));
133
- this.rawY.set(initialY.subarray(0, this._length));
134
- this.levels = [];
135
- this.rebuildLevels();
136
- }
137
- get length() {
138
- return this._length;
139
- }
140
- append(xValues, yValues) {
141
- const nextX = toTypedArray(xValues, Float64Array);
142
- const nextY = toTypedArray(yValues, Float32Array);
143
- const appendLength = Math.min(nextX.length, nextY.length);
144
- if (appendLength === 0) return;
145
- const currentLength = this.length;
146
- this.ensureRawCapacity(currentLength + appendLength);
147
- this.rawX.set(nextX.subarray(0, appendLength), currentLength);
148
- this.rawY.set(nextY.subarray(0, appendLength), currentLength);
149
- this._length = currentLength + appendLength;
150
- this.markLevelsDirty(currentLength);
151
- }
152
- rebuildLevels() {
153
- const rawX = this.rawX.subarray(0, this.length);
154
- const rawY = this.rawY.subarray(0, this.length);
155
- this.levels = [{ x: rawX, y: rawY, bucketSize: 1 }];
156
- let bucketSize = 4;
157
- while (this.levels.length < this.maxLevels && bucketSize < Math.max(8, rawX.length)) {
158
- this.levels.push(buildLod(rawX, rawY, bucketSize));
159
- bucketSize *= 4;
160
- }
161
- }
162
- ensureRawCapacity(requiredLength) {
163
- if (requiredLength <= this.rawX.length && requiredLength <= this.rawY.length) {
164
- return;
165
- }
166
- const nextCapacity = Math.max(requiredLength, Math.ceil(this.rawX.length * 1.5));
167
- const rawX = new Float64Array(nextCapacity);
168
- const rawY = new Float32Array(nextCapacity);
169
- rawX.set(this.rawX.subarray(0, this.length));
170
- rawY.set(this.rawY.subarray(0, this.length));
171
- this.rawX = rawX;
172
- this.rawY = rawY;
173
- }
174
- markLevelsDirty(previousLength) {
175
- const rawX = this.rawX.subarray(0, this.length);
176
- const rawY = this.rawY.subarray(0, this.length);
177
- this.levels[0] = { x: rawX, y: rawY, bucketSize: 1 };
178
- let bucketSize = this.levels.length > 0 ? this.levels[this.levels.length - 1].bucketSize * 4 : 4;
179
- while (this.levels.length < this.maxLevels && bucketSize < Math.max(8, rawX.length)) {
180
- this.levels.push(buildLod(rawX, rawY, bucketSize));
181
- bucketSize *= 4;
182
- }
183
- for (let i = 1; i < this.levels.length; i += 1) {
184
- const level = this.levels[i];
185
- const dirtyRawStart = Math.floor(previousLength / level.bucketSize) * level.bucketSize;
186
- level.dirtyFrom = Math.min(level.dirtyFrom ?? dirtyRawStart, dirtyRawStart);
187
- }
188
- }
189
- ensureLevel(index) {
190
- const level = this.levels[index];
191
- if (!level || index === 0 || level.dirtyFrom == null) return level;
192
- const rawX = this.rawX.subarray(0, this.length);
193
- const rawY = this.rawY.subarray(0, this.length);
194
- const dirtyRawStart = Math.floor(level.dirtyFrom / level.bucketSize) * level.bucketSize;
195
- const cutoffX = rawX[dirtyRawStart] ?? rawX[0] ?? 0;
196
- const keepCount = lowerBound(level.x, cutoffX);
197
- const tail = buildLod(rawX, rawY, level.bucketSize, dirtyRawStart, rawX.length);
198
- this.levels[index] = {
199
- x: concatTyped(Float64Array, level.x.subarray(0, keepCount), tail.x),
200
- y: concatTyped(Float32Array, level.y.subarray(0, keepCount), tail.y),
201
- bucketSize: level.bucketSize
202
- };
203
- return this.levels[index];
204
- }
205
- selectLevel(xMin, xMax, pixelWidth, targetPointsPerPixel = 2.5) {
206
- const rawX = this.levels[0].x;
207
- if (rawX.length === 0) return this.levels[0];
208
- const from = lowerBound(rawX, xMin);
209
- const to = upperBound(rawX, xMax);
210
- const visibleRawPoints = Math.max(0, to - from);
211
- const targetPoints = Math.max(32, pixelWidth * targetPointsPerPixel);
212
- const desiredBucket = Math.max(1, visibleRawPoints / targetPoints);
213
- let selectedIndex = 0;
214
- for (let i = 1; i < this.levels.length; i += 1) {
215
- if (this.levels[i].bucketSize <= desiredBucket) {
216
- selectedIndex = i;
217
- } else {
218
- break;
219
- }
220
- }
221
- return this.ensureLevel(selectedIndex);
222
- }
223
- getVisiblePoints(xMin, xMax, pixelWidth) {
224
- const level = this.selectLevel(xMin, xMax, pixelWidth);
225
- const start = Math.max(0, lowerBound(level.x, xMin) - 1);
226
- const end = Math.min(level.x.length, upperBound(level.x, xMax) + 1);
227
- return {
228
- x: level.x.subarray(start, end),
229
- y: level.y.subarray(start, end),
230
- bucketSize: level.bucketSize,
231
- pointCount: Math.max(0, end - start)
232
- };
233
- }
234
- };
235
- var createSeries = (options) => new LineSeries(options);
236
-
237
- // src/core/mockData.js
238
- var palette = [
239
- "#38bdf8",
240
- "#22c55e",
241
- "#f59e0b",
242
- "#ef4444",
243
- "#a855f7",
244
- "#14b8a6"
245
- ];
246
- var createRandom = (seed) => {
247
- let value = seed >>> 0;
248
- return () => {
249
- value = value * 1664525 + 1013904223 >>> 0;
250
- return value / 4294967296;
251
- };
252
- };
253
- var makeSeries = ({ chartIndex, seriesIndex, pointCount }) => {
254
- const x = new Float64Array(pointCount);
255
- const y = new Float32Array(pointCount);
256
- const random = createRandom((chartIndex + 1) * 1009 + seriesIndex * 9176);
257
- const baseline = 15 + random() * 45 + seriesIndex * 4;
258
- const waveA = 8e-4 + random() * 4e-3;
259
- const waveB = 6e-3 + random() * 0.03;
260
- const waveC = 5e-5 + random() * 6e-4;
261
- const amplitudeA = 0.015 + random() * 0.09;
262
- const amplitudeB = 5e-3 + random() * 0.04;
263
- const trend = (random() - 0.5) * 4e-3;
264
- const noise = 4e-3 + random() * 0.025;
265
- const spikeInterval = 180 + Math.floor(random() * 900);
266
- const spikeMagnitude = 0.12 + random() * 0.7;
267
- let value = baseline;
268
- for (let i = 0; i < pointCount; i += 1) {
269
- x[i] = i;
270
- const spike = (i + chartIndex * 13 + seriesIndex * 31) % spikeInterval === 0 ? spikeMagnitude * (random() > 0.45 ? 1 : -1) : 0;
271
- 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;
272
- y[i] = value;
273
- }
274
- return createSeries({
275
- id: `chart-${chartIndex}-series-${seriesIndex}`,
276
- name: `Run ${seriesIndex + 1}`,
277
- color: palette[(chartIndex + seriesIndex) % palette.length],
278
- x,
279
- y
280
- });
281
- };
282
- var createMockCharts = ({
283
- chartCount = 50,
284
- seriesPerChart = 2,
285
- pointCount = 5e5
286
- } = {}) => {
287
- return Array.from({ length: chartCount }, (_, chartIndex) => ({
288
- id: `metric-${chartIndex + 1}`,
289
- title: `metric_${String(chartIndex + 1).padStart(2, "0")}`,
290
- series: Array.from(
291
- { length: seriesPerChart },
292
- (_2, seriesIndex) => makeSeries({ chartIndex, seriesIndex, pointCount })
293
- )
294
- }));
295
- };
296
-
297
- // src/react/ChartToolbar.jsx
298
- var import_react = __toESM(require("react"), 1);
299
- var import_react2 = require("@phosphor-icons/react");
300
- function ToolbarAction({
301
- title,
302
- hotkey,
303
- children,
304
- onClick,
305
- active = false,
306
- className = ""
307
- }) {
308
- return /* @__PURE__ */ import_react.default.createElement(
309
- "button",
310
- {
311
- type: "button",
312
- title,
313
- "aria-label": title,
314
- className: `relative inline-flex size-6 items-center justify-center rounded-sm text-foreground hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 ${className} ${active ? "bg-primary/15 text-primary ring-1 ring-primary/40" : ""}`,
315
- onPointerDown: (event) => {
316
- event.preventDefault();
317
- event.stopPropagation();
318
- },
319
- onClick: (event) => {
320
- event.preventDefault();
321
- event.stopPropagation();
322
- onClick?.();
323
- }
324
- },
325
- children,
326
- hotkey ? /* @__PURE__ */ import_react.default.createElement("span", { className: "pointer-events-none absolute left-full top-1/2 -translate-y-1/2 text-[10px] font-semibold leading-none text-foreground/50" }, hotkey) : null
327
- );
328
- }
329
- function ChartToolbar({
330
- isFullscreen = false,
331
- onFullscreenToggle,
332
- onReset,
333
- onRectangleZoomToggle,
334
- rectangleZoomActive = false,
335
- onMovingAverageToggle,
336
- movingAverageActive = false,
337
- activeDrawingTool = null,
338
- onDrawingToolToggle,
339
- hasDrawings = false,
340
- onClearDrawingsRequest,
341
- disableDrawings = false
342
- }) {
343
- return /* @__PURE__ */ import_react.default.createElement("div", { className: "absolute left-2 top-12 z-30 flex flex-col gap-0.5 rounded-sm bg-background/80 p-0 pr-3 text-foreground shadow-sm backdrop-blur" }, /* @__PURE__ */ import_react.default.createElement(
344
- ToolbarAction,
345
- {
346
- title: isFullscreen ? "Exit fullscreen" : "Maximize chart",
347
- hotkey: "F",
348
- onClick: onFullscreenToggle
349
- },
350
- isFullscreen ? /* @__PURE__ */ import_react.default.createElement(import_react2.ArrowsInIcon, { size: 16, weight: "bold" }) : /* @__PURE__ */ import_react.default.createElement(import_react2.ArrowsOutIcon, { size: 16, weight: "bold" })
351
- ), /* @__PURE__ */ import_react.default.createElement(ToolbarAction, { title: "Reset chart", hotkey: "R", onClick: onReset }, /* @__PURE__ */ import_react.default.createElement(import_react2.ArrowCounterClockwiseIcon, { size: 16, weight: "bold" })), /* @__PURE__ */ import_react.default.createElement(
352
- ToolbarAction,
353
- {
354
- title: "Rectangle zoom",
355
- hotkey: "Z",
356
- onClick: onRectangleZoomToggle,
357
- active: rectangleZoomActive
358
- },
359
- /* @__PURE__ */ import_react.default.createElement(import_react2.MagnifyingGlassPlusIcon, { size: 16, weight: "bold" })
360
- ), !disableDrawings && hasDrawings ? /* @__PURE__ */ import_react.default.createElement(
361
- ToolbarAction,
362
- {
363
- title: "Clear drawings",
364
- className: "text-orange-500 dark:text-orange-300 dark:hover:text-orange-300",
365
- onClick: onClearDrawingsRequest
366
- },
367
- /* @__PURE__ */ import_react.default.createElement(import_react2.BroomIcon, { size: 16, weight: "bold" })
368
- ) : null, !disableDrawings ? /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement("div", { className: "my-1 h-px w-full" }), /* @__PURE__ */ import_react.default.createElement(
369
- ToolbarAction,
370
- {
371
- title: "Draw trendline",
372
- hotkey: "T",
373
- onClick: () => onDrawingToolToggle?.("trendline"),
374
- active: activeDrawingTool === "trendline"
375
- },
376
- /* @__PURE__ */ import_react.default.createElement(import_react2.MinusIcon, { className: "-rotate-45", size: 16, weight: "bold" })
377
- ), /* @__PURE__ */ import_react.default.createElement(
378
- ToolbarAction,
379
- {
380
- title: "Draw horizontal line",
381
- hotkey: "H",
382
- onClick: () => onDrawingToolToggle?.("hline"),
383
- active: activeDrawingTool === "hline"
384
- },
385
- /* @__PURE__ */ import_react.default.createElement(import_react2.MinusIcon, { size: 16, weight: "bold" })
386
- ), /* @__PURE__ */ import_react.default.createElement(
387
- ToolbarAction,
388
- {
389
- title: "Draw vertical line",
390
- hotkey: "V",
391
- onClick: () => onDrawingToolToggle?.("vline"),
392
- active: activeDrawingTool === "vline"
393
- },
394
- /* @__PURE__ */ import_react.default.createElement(import_react2.MinusIcon, { className: "rotate-90", size: 16, weight: "bold" })
395
- ), /* @__PURE__ */ import_react.default.createElement(
396
- ToolbarAction,
397
- {
398
- title: "Place pin",
399
- hotkey: "P",
400
- onClick: () => onDrawingToolToggle?.("pin"),
401
- active: activeDrawingTool === "pin"
402
- },
403
- /* @__PURE__ */ import_react.default.createElement(import_react2.MapPinIcon, { size: 16, weight: "bold" })
404
- )) : null, !disableDrawings ? /* @__PURE__ */ import_react.default.createElement(
405
- ToolbarAction,
406
- {
407
- title: "Toggle moving average",
408
- hotkey: "M",
409
- onClick: onMovingAverageToggle,
410
- active: movingAverageActive
411
- },
412
- /* @__PURE__ */ import_react.default.createElement(import_react2.WaveSineIcon, { size: 16, weight: "bold" })
413
- ) : null);
414
- }
415
-
416
- // src/react/DrawingOptionsToolbar.jsx
417
- var import_react3 = __toESM(require("react"), 1);
418
-
419
- // src/react/drawingUtils.js
420
- var DEFAULT_DRAWING_COLOR = "#60a5fa";
421
- var DRAWING_HIT_DISTANCE = 8;
422
- var DRAWING_HANDLE_RADIUS = 4;
423
- var DRAWING_HANDLE_HIT_DISTANCE = 10;
424
- var PIN_HIT_DISTANCE = 14;
425
- var DRAWING_TOOLS = /* @__PURE__ */ new Set(["trendline", "hline", "vline", "pin"]);
426
- var getDrawingStyle = (drawing) => ({
427
- color: drawing?.style?.color || DEFAULT_DRAWING_COLOR,
428
- lineWidth: Number.isFinite(drawing?.style?.lineWidth) ? drawing.style.lineWidth : 2,
429
- dashPattern: Array.isArray(drawing?.style?.dashPattern) ? drawing.style.dashPattern : [],
430
- extendLeft: Boolean(drawing?.style?.extendLeft),
431
- extendRight: drawing?.style?.extendRight !== false,
432
- text: typeof drawing?.style?.text === "string" ? drawing.style.text : ""
433
- });
434
- var getDrawingsForChart = (drawings, chartId) => Array.isArray(drawings) ? drawings.filter((drawing) => drawing?.chartId === chartId) : [];
435
- var getDefaultDrawingStyle = (type) => ({
436
- color: DEFAULT_DRAWING_COLOR,
437
- lineWidth: 2,
438
- dashPattern: [],
439
- extendLeft: false,
440
- extendRight: type !== "vline",
441
- text: ""
442
- });
443
- var createDrawing = ({
444
- chartId,
445
- type,
446
- start,
447
- end,
448
- createDrawingId
449
- }) => {
450
- const id = createDrawingId?.({ chartId, type }) || `${chartId}-drawing-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
451
- return {
452
- id,
453
- chartId,
454
- type,
455
- start,
456
- end,
457
- createdAt: Date.now(),
458
- style: getDefaultDrawingStyle(type)
459
- };
460
- };
461
- var createDraftDrawing = ({ chartId, type, start, end }) => ({
462
- id: "__draft__",
463
- chartId,
464
- type,
465
- start,
466
- end,
467
- createdAt: Date.now(),
468
- style: getDefaultDrawingStyle(type)
469
- });
470
- var getDrawingGeometry = ({ drawing, layout, projectPoint }) => {
471
- if (!drawing?.start || !drawing?.end) return null;
472
- const start = projectPoint(drawing.start, layout);
473
- const end = projectPoint(drawing.end, layout);
474
- const style = getDrawingStyle(drawing);
475
- let lineStart = start;
476
- let lineEnd = end;
477
- if (drawing.type === "pin") {
478
- return { start, end: start, lineStart: start, lineEnd: start, style };
479
- }
480
- if (drawing.type === "hline") {
481
- const lineEndX = Math.abs(end.x - start.x) > 1e-6 ? end.x : layout.plot.x + layout.plot.width;
482
- lineStart = {
483
- x: style.extendRight ? layout.plot.x : start.x,
484
- y: start.y
485
- };
486
- lineEnd = {
487
- x: style.extendRight ? layout.plot.x + layout.plot.width : lineEndX,
488
- y: start.y
489
- };
490
- } else if (drawing.type === "vline") {
491
- lineStart = {
492
- x: start.x,
493
- y: layout.plot.y
494
- };
495
- lineEnd = {
496
- x: start.x,
497
- y: layout.plot.y + layout.plot.height
498
- };
499
- } else if (drawing.type === "trendline") {
500
- const dx = end.x - start.x;
501
- const dy = end.y - start.y;
502
- if (Math.abs(dx) > 1e-6) {
503
- if (style.extendLeft) {
504
- const leftX = layout.plot.x;
505
- lineStart = {
506
- x: leftX,
507
- y: start.y + dy / dx * (leftX - start.x)
508
- };
509
- }
510
- if (style.extendRight) {
511
- const rightX = layout.plot.x + layout.plot.width;
512
- lineEnd = {
513
- x: rightX,
514
- y: start.y + dy / dx * (rightX - start.x)
515
- };
516
- }
517
- }
518
- }
519
- return { start, end, lineStart, lineEnd, style };
520
- };
521
- var distanceToSegment = (point, start, end) => {
522
- const dx = end.x - start.x;
523
- const dy = end.y - start.y;
524
- if (dx === 0 && dy === 0) {
525
- return Math.hypot(point.x - start.x, point.y - start.y);
526
- }
527
- const t = Math.max(
528
- 0,
529
- Math.min(
530
- 1,
531
- ((point.x - start.x) * dx + (point.y - start.y) * dy) / (dx * dx + dy * dy)
532
- )
533
- );
534
- return Math.hypot(point.x - (start.x + t * dx), point.y - (start.y + t * dy));
535
- };
536
- var hitTestDrawing = ({ point, drawing, layout, projectPoint }) => {
537
- const geometry = getDrawingGeometry({
538
- drawing,
539
- layout,
540
- projectPoint
541
- });
542
- if (!geometry) return null;
543
- if (drawing.type !== "pin" && Math.hypot(point.x - geometry.start.x, point.y - geometry.start.y) <= DRAWING_HANDLE_HIT_DISTANCE) {
544
- return { drawing, endpoint: "start" };
545
- }
546
- if (drawing.type === "pin" && Math.hypot(point.x - geometry.start.x, point.y - geometry.start.y) <= PIN_HIT_DISTANCE) {
547
- return { drawing, endpoint: "move" };
548
- }
549
- if (drawing.type === "trendline" && Math.hypot(point.x - geometry.end.x, point.y - geometry.end.y) <= DRAWING_HANDLE_HIT_DISTANCE) {
550
- return { drawing, endpoint: "end" };
551
- }
552
- const distance = distanceToSegment(point, geometry.lineStart, geometry.lineEnd);
553
- return distance <= DRAWING_HIT_DISTANCE ? { drawing, endpoint: null } : null;
554
- };
555
- var updateDrawingById = (drawings, drawingId, updater) => (Array.isArray(drawings) ? drawings : []).map(
556
- (drawing) => drawing?.id === drawingId ? updater(drawing) : drawing
557
- );
558
- var removeDrawingById = (drawings, drawingId) => (Array.isArray(drawings) ? drawings : []).filter(
559
- (drawing) => drawing?.id !== drawingId
560
- );
561
-
562
- // src/react/DrawingOptionsToolbar.jsx
563
- var isHexColor = (color) => /^#[0-9a-f]{6}$/i.test(String(color || ""));
564
- var stopEvent = (event) => {
565
- event.stopPropagation();
566
- };
567
- var stopAndPreventEvent = (event) => {
568
- event.preventDefault();
569
- event.stopPropagation();
570
- };
571
- function DrawingOptionsToolbar({
572
- drawing,
573
- style,
574
- onToggleExtend,
575
- onColorChange,
576
- onTextChange
577
- }) {
578
- const colorInputRef = (0, import_react3.useRef)(null);
579
- if (!drawing) return null;
580
- const drawingStyle = getDrawingStyle(drawing);
581
- const color = isHexColor(drawingStyle.color) ? drawingStyle.color : DEFAULT_DRAWING_COLOR;
582
- const canExtend = drawing.type === "trendline" || drawing.type === "hline";
583
- const canEditText = drawing.type === "pin";
584
- return /* @__PURE__ */ import_react3.default.createElement(
585
- "div",
586
- {
587
- className: "pointer-events-auto absolute z-40 flex items-center justify-end gap-1 rounded-sm bg-background/85 p-1 text-xs text-foreground shadow-sm backdrop-blur",
588
- style,
589
- onPointerDown: stopEvent,
590
- onClick: (event) => event.stopPropagation()
591
- },
592
- canExtend ? /* @__PURE__ */ import_react3.default.createElement(
593
- "button",
594
- {
595
- type: "button",
596
- title: "Extend line",
597
- "aria-label": "Extend line",
598
- className: `h-6 rounded-sm px-2 text-[11px] font-semibold leading-none hover:bg-accent hover:text-accent-foreground ${drawingStyle.extendRight ? "bg-primary/15 text-primary ring-1 ring-primary/40" : "text-foreground/80"}`,
599
- onPointerDown: stopAndPreventEvent,
600
- onClick: (event) => {
601
- event.preventDefault();
602
- event.stopPropagation();
603
- onToggleExtend?.();
604
- }
605
- },
606
- "Ext"
607
- ) : null,
608
- canEditText ? /* @__PURE__ */ import_react3.default.createElement(
609
- "input",
610
- {
611
- type: "text",
612
- value: drawingStyle.text,
613
- placeholder: "Text",
614
- title: "Pin text",
615
- className: "h-6 w-24 rounded-sm border border-border/50 bg-background/70 px-1.5 text-xs",
616
- onPointerDown: stopEvent,
617
- onClick: stopEvent,
618
- onChange: (event) => onTextChange?.(event.target.value)
619
- }
620
- ) : null,
621
- /* @__PURE__ */ import_react3.default.createElement(
622
- "button",
623
- {
624
- type: "button",
625
- title: "Line color",
626
- "aria-label": "Line color",
627
- className: "size-6 rounded-sm border border-border/70 shadow-sm",
628
- style: { backgroundColor: color },
629
- onPointerDown: stopAndPreventEvent,
630
- onClick: (event) => {
631
- event.preventDefault();
632
- event.stopPropagation();
633
- if (typeof colorInputRef.current?.showPicker === "function") {
634
- colorInputRef.current.showPicker();
635
- return;
636
- }
637
- colorInputRef.current?.click();
638
- }
639
- }
640
- ),
641
- /* @__PURE__ */ import_react3.default.createElement(
642
- "input",
643
- {
644
- ref: colorInputRef,
645
- type: "color",
646
- value: color,
647
- tabIndex: -1,
648
- className: "fixed left-[-1000px] top-[-1000px] size-px opacity-0",
649
- style: { pointerEvents: "none" },
650
- onPointerDown: (event) => event.stopPropagation(),
651
- onChange: (event) => onColorChange?.(event.target.value)
652
- }
653
- )
654
- );
655
- }
656
-
657
- // src/react/DrawingOverlay.jsx
658
- var import_react4 = __toESM(require("react"), 1);
659
- var getClipId = (chartId) => `drawing-clip-${String(chartId).replace(/[^a-zA-Z0-9_-]/g, "_")}`;
660
- function PinDrawing({ drawing, geometry, selected }) {
661
- const text = geometry.style.text.trim();
662
- return /* @__PURE__ */ import_react4.default.createElement("g", { opacity: drawing.id === "__draft__" ? 0.72 : 1 }, /* @__PURE__ */ import_react4.default.createElement("g", { transform: `translate(${geometry.start.x} ${geometry.start.y})` }, /* @__PURE__ */ import_react4.default.createElement(
663
- "path",
664
- {
665
- 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",
666
- fill: geometry.style.color,
667
- stroke: "var(--background, #ffffff)",
668
- strokeWidth: "1.5"
669
- }
670
- ), /* @__PURE__ */ import_react4.default.createElement("circle", { cx: "0", cy: "-2", r: "3", fill: "var(--background, #ffffff)", opacity: "0.9" })), text ? /* @__PURE__ */ import_react4.default.createElement(
671
- "text",
672
- {
673
- x: geometry.start.x + 14,
674
- y: geometry.start.y,
675
- fill: geometry.style.color,
676
- stroke: "var(--background, #ffffff)",
677
- strokeWidth: "3",
678
- paintOrder: "stroke",
679
- fontSize: "12",
680
- fontWeight: "600",
681
- dominantBaseline: "middle"
682
- },
683
- text
684
- ) : null, selected ? /* @__PURE__ */ import_react4.default.createElement(
685
- "circle",
686
- {
687
- cx: geometry.start.x,
688
- cy: geometry.start.y,
689
- r: DRAWING_HANDLE_RADIUS,
690
- fill: geometry.style.color,
691
- stroke: "var(--background, #ffffff)",
692
- strokeWidth: "1.5"
693
- }
694
- ) : null);
695
- }
696
- function DrawingOverlay({
697
- layouts,
698
- drawings,
699
- draftDrawing,
700
- selectedDrawingId,
701
- projectPoint,
702
- height = "100%"
703
- }) {
704
- const allDrawings = draftDrawing ? [...Array.isArray(drawings) ? drawings : [], draftDrawing] : Array.isArray(drawings) ? drawings : [];
705
- if (!allDrawings.length) return null;
706
- return /* @__PURE__ */ import_react4.default.createElement(
707
- "svg",
708
- {
709
- className: "pointer-events-none absolute left-0 top-0 z-20 block overflow-visible",
710
- style: { width: "100%", height }
711
- },
712
- /* @__PURE__ */ import_react4.default.createElement("defs", null, layouts.map((layout) => /* @__PURE__ */ import_react4.default.createElement("clipPath", { key: getClipId(layout.chart.id), id: getClipId(layout.chart.id) }, /* @__PURE__ */ import_react4.default.createElement(
713
- "rect",
714
- {
715
- x: layout.plot.x,
716
- y: layout.plot.y,
717
- width: layout.plot.width,
718
- height: layout.plot.height
719
- }
720
- )))),
721
- layouts.flatMap(
722
- (layout) => getDrawingsForChart(allDrawings, layout.chart.id).map((drawing) => {
723
- const geometry = getDrawingGeometry({
724
- drawing,
725
- layout,
726
- projectPoint
727
- });
728
- if (!geometry) return null;
729
- const selected = drawing.id === selectedDrawingId || drawing.id === "__draft__";
730
- const dashArray = geometry.style.dashPattern.length ? geometry.style.dashPattern.join(" ") : void 0;
731
- if (drawing.type === "pin") {
732
- return /* @__PURE__ */ import_react4.default.createElement("g", { key: drawing.id, clipPath: `url(#${getClipId(layout.chart.id)})` }, /* @__PURE__ */ import_react4.default.createElement(
733
- PinDrawing,
734
- {
735
- drawing,
736
- geometry,
737
- selected
738
- }
739
- ));
740
- }
741
- return /* @__PURE__ */ import_react4.default.createElement("g", { key: drawing.id, clipPath: `url(#${getClipId(layout.chart.id)})` }, /* @__PURE__ */ import_react4.default.createElement(
742
- "line",
743
- {
744
- x1: geometry.lineStart.x,
745
- y1: geometry.lineStart.y,
746
- x2: geometry.lineEnd.x,
747
- y2: geometry.lineEnd.y,
748
- stroke: geometry.style.color,
749
- strokeWidth: geometry.style.lineWidth,
750
- strokeDasharray: dashArray,
751
- opacity: drawing.id === "__draft__" ? 0.72 : 1
752
- }
753
- ), selected ? /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(
754
- "circle",
755
- {
756
- cx: geometry.start.x,
757
- cy: geometry.start.y,
758
- r: DRAWING_HANDLE_RADIUS,
759
- fill: geometry.style.color,
760
- stroke: "var(--background, #ffffff)",
761
- strokeWidth: "1.5"
762
- }
763
- ), drawing.type === "trendline" ? /* @__PURE__ */ import_react4.default.createElement(
764
- "circle",
765
- {
766
- cx: geometry.end.x,
767
- cy: geometry.end.y,
768
- r: DRAWING_HANDLE_RADIUS,
769
- fill: geometry.style.color,
770
- stroke: "var(--background, #ffffff)",
771
- strokeWidth: "1.5"
772
- }
773
- ) : null) : null);
774
- }).filter(Boolean)
775
- )
776
- );
777
- }
778
-
779
- // src/react/MovingAverageOptionsToolbar.jsx
780
- var import_react5 = __toESM(require("react"), 1);
781
- var import_react6 = require("@phosphor-icons/react");
782
- var stopEvent2 = (event) => {
783
- event.stopPropagation();
784
- };
785
- var stopAndPreventEvent2 = (event) => {
786
- event.preventDefault();
787
- event.stopPropagation();
788
- };
789
- function MovingAverageOptionsToolbar({
790
- movingAverage,
791
- style,
792
- onChange
793
- }) {
794
- if (!movingAverage?.enabled) return null;
795
- const period = Number.isFinite(Number(movingAverage.period)) ? Math.max(1, Math.round(Number(movingAverage.period))) : 21;
796
- const type = movingAverage.type === "sma" ? "sma" : "ema";
797
- const hideBase = Boolean(movingAverage.hideBase);
798
- return /* @__PURE__ */ import_react5.default.createElement(
799
- "div",
800
- {
801
- className: "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",
802
- style,
803
- onPointerDown: stopEvent2,
804
- onClick: (event) => event.stopPropagation()
805
- },
806
- /* @__PURE__ */ import_react5.default.createElement(
807
- "button",
808
- {
809
- type: "button",
810
- title: hideBase ? "Show base series" : "Hide base series",
811
- "aria-label": hideBase ? "Show base series" : "Hide base series",
812
- className: `inline-flex size-6 items-center justify-center rounded-sm text-foreground/80 hover:bg-accent hover:text-accent-foreground ${hideBase ? "bg-primary/15 text-primary ring-1 ring-primary/40" : ""}`,
813
- onPointerDown: stopAndPreventEvent2,
814
- onClick: (event) => {
815
- event.preventDefault();
816
- event.stopPropagation();
817
- onChange?.({
818
- ...movingAverage,
819
- hideBase: !hideBase
820
- });
821
- }
822
- },
823
- /* @__PURE__ */ import_react5.default.createElement(import_react6.EyeIcon, { size: 14, weight: "bold" })
824
- ),
825
- /* @__PURE__ */ import_react5.default.createElement(
826
- "button",
827
- {
828
- type: "button",
829
- title: "Toggle moving average type",
830
- "aria-label": "Toggle moving average type",
831
- className: "h-6 rounded-sm px-2 text-[11px] font-semibold uppercase leading-none text-foreground/80 hover:bg-accent hover:text-accent-foreground",
832
- onPointerDown: stopAndPreventEvent2,
833
- onClick: (event) => {
834
- event.preventDefault();
835
- event.stopPropagation();
836
- onChange?.({
837
- ...movingAverage,
838
- type: type === "sma" ? "ema" : "sma"
839
- });
840
- }
841
- },
842
- type
843
- ),
844
- /* @__PURE__ */ import_react5.default.createElement(
845
- "input",
846
- {
847
- type: "number",
848
- min: 1,
849
- step: 1,
850
- value: period,
851
- title: "Moving average period",
852
- className: "h-6 w-14 rounded-sm border border-border/50 bg-background/70 px-1 text-xs tabular-nums",
853
- onPointerDown: stopEvent2,
854
- onClick: stopEvent2,
855
- onChange: (event) => {
856
- const nextPeriod = Math.max(1, Math.round(Number(event.target.value)));
857
- if (!Number.isFinite(nextPeriod)) return;
858
- onChange?.({
859
- ...movingAverage,
860
- period: nextPeriod
861
- });
862
- }
863
- }
864
- )
865
- );
866
- }
867
-
868
- // src/react/useAppendAnimations.js
869
- var import_react7 = require("react");
870
- var APPEND_ANIMATION_SETTINGS = {
871
- enabled: true,
872
- durationMs: 300,
873
- maxBucketSize: 64,
874
- // Zoom threshold
875
- maxRevealPoints: 100
876
- // Larger appends skips in-between points
877
- };
878
- var easeOutQuad = (value) => 1 - (1 - value) * (1 - value);
879
- function useAppendAnimations({
880
- charts,
881
- dataRevision,
882
- requestRender,
883
- settings = APPEND_ANIMATION_SETTINGS
884
- }) {
885
- const latestPointRef = (0, import_react7.useRef)(/* @__PURE__ */ new Map());
886
- const animationsRef = (0, import_react7.useRef)(/* @__PURE__ */ new Map());
887
- const animationRafRef = (0, import_react7.useRef)(null);
888
- const stopAnimationLoop = (0, import_react7.useCallback)(() => {
889
- if (animationRafRef.current != null) {
890
- cancelAnimationFrame(animationRafRef.current);
891
- animationRafRef.current = null;
892
- }
893
- }, []);
894
- const startAnimationLoop = (0, import_react7.useCallback)(() => {
895
- if (!settings.enabled || animationRafRef.current != null) return;
896
- const tick = () => {
897
- const now = performance.now();
898
- let hasActiveAnimation = false;
899
- animationsRef.current.forEach((animation, seriesId) => {
900
- if (now - animation.startedAt >= animation.duration) {
901
- animationsRef.current.delete(seriesId);
902
- } else {
903
- hasActiveAnimation = true;
904
- }
905
- });
906
- requestRender();
907
- animationRafRef.current = hasActiveAnimation ? requestAnimationFrame(tick) : null;
908
- };
909
- animationRafRef.current = requestAnimationFrame(tick);
910
- }, [requestRender, settings.enabled]);
911
- (0, import_react7.useEffect)(() => {
912
- if (!settings.enabled) {
913
- animationsRef.current.clear();
914
- stopAnimationLoop();
915
- }
916
- }, [settings.enabled, stopAnimationLoop]);
917
- (0, import_react7.useLayoutEffect)(() => {
918
- if (!settings.enabled) return;
919
- const now = performance.now();
920
- charts.forEach((chart) => {
921
- chart.series.forEach((series) => {
922
- if (series.length === 0) return;
923
- const next = {
924
- x: series.rawX[series.length - 1],
925
- y: series.rawY[series.length - 1],
926
- length: series.length
927
- };
928
- const previous = latestPointRef.current.get(series.id);
929
- if (previous && Number.isFinite(previous.x) && Number.isFinite(previous.y) && next.x > previous.x) {
930
- animationsRef.current.set(series.id, {
931
- fromX: previous.x,
932
- fromY: previous.y,
933
- toX: next.x,
934
- toY: next.y,
935
- appendedCount: next.length - previous.length,
936
- startedAt: now,
937
- duration: settings.durationMs
938
- });
939
- startAnimationLoop();
940
- }
941
- latestPointRef.current.set(series.id, next);
942
- });
943
- });
944
- }, [
945
- charts,
946
- dataRevision,
947
- settings.durationMs,
948
- settings.enabled,
949
- startAnimationLoop
950
- ]);
951
- (0, import_react7.useEffect)(() => stopAnimationLoop, [stopAnimationLoop]);
952
- const getAppendAnimatedPoint = (0, import_react7.useCallback)(
953
- ({ seriesId, visiblePoints, now }) => {
954
- if (!settings.enabled || visiblePoints.bucketSize > settings.maxBucketSize) {
955
- return null;
956
- }
957
- const animation = animationsRef.current.get(seriesId);
958
- if (!animation || visiblePoints.x[0] > animation.fromX || visiblePoints.x[visiblePoints.pointCount - 1] < animation.toX) {
959
- return null;
960
- }
961
- let stableCount = 0;
962
- while (stableCount < visiblePoints.pointCount && visiblePoints.x[stableCount] <= animation.fromX) {
963
- stableCount += 1;
964
- }
965
- if (stableCount === 0) return null;
966
- const progress = Math.min(
967
- 1,
968
- Math.max(0, (now - animation.startedAt) / animation.duration)
969
- );
970
- const eased = easeOutQuad(progress);
971
- if (animation.appendedCount <= settings.maxRevealPoints) {
972
- const appendedVisibleCount = visiblePoints.pointCount - stableCount;
973
- const revealPosition = eased * appendedVisibleCount;
974
- const revealedPoints = Math.min(
975
- appendedVisibleCount,
976
- Math.floor(revealPosition)
977
- );
978
- const nextIndex = Math.min(
979
- visiblePoints.pointCount - 1,
980
- stableCount + revealedPoints
981
- );
982
- const previousIndex = Math.max(stableCount - 1, nextIndex - 1);
983
- const partial = revealPosition - revealedPoints;
984
- const hasPartial = revealedPoints < appendedVisibleCount;
985
- const x = hasPartial ? visiblePoints.x[previousIndex] + (visiblePoints.x[nextIndex] - visiblePoints.x[previousIndex]) * partial : visiblePoints.x[nextIndex];
986
- const y = hasPartial ? visiblePoints.y[previousIndex] + (visiblePoints.y[nextIndex] - visiblePoints.y[previousIndex]) * partial : visiblePoints.y[nextIndex];
987
- return {
988
- index: nextIndex,
989
- pointCount: Math.min(visiblePoints.pointCount, nextIndex + 1),
990
- x,
991
- y
992
- };
993
- }
994
- return {
995
- index: stableCount,
996
- pointCount: stableCount + 1,
997
- x: animation.fromX + (animation.toX - animation.fromX) * eased,
998
- y: animation.fromY + (animation.toY - animation.fromY) * eased
999
- };
1000
- },
1001
- [settings.enabled, settings.maxBucketSize, settings.maxRevealPoints]
1002
- );
1003
- return { getAppendAnimatedPoint };
1004
- }
1005
-
1006
- // src/react/useDrawingInteractions.js
1007
- var import_react8 = require("react");
1008
- function useDrawingInteractions({
1009
- drawings,
1010
- onDrawingsChange,
1011
- activeDrawingTool,
1012
- onActiveDrawingToolChange,
1013
- selectedDrawingId,
1014
- onSelectedDrawingIdChange,
1015
- createDrawingId,
1016
- focusedChartId = null,
1017
- requireFocusedChart = false,
1018
- onModeChange
1019
- }) {
1020
- const drawingSessionRef = (0, import_react8.useRef)({ chartId: null, startPoint: null });
1021
- const drawingEditRef = (0, import_react8.useRef)(null);
1022
- const [draftDrawing, setDraftDrawing] = (0, import_react8.useState)(null);
1023
- const clearDraft = (0, import_react8.useCallback)(() => {
1024
- setDraftDrawing(null);
1025
- drawingSessionRef.current = { chartId: null, startPoint: null };
1026
- }, []);
1027
- const cancelDrawing = (0, import_react8.useCallback)(() => {
1028
- onActiveDrawingToolChange?.(null);
1029
- onSelectedDrawingIdChange?.(null);
1030
- setDraftDrawing(null);
1031
- drawingSessionRef.current = { chartId: null, startPoint: null };
1032
- drawingEditRef.current = null;
1033
- }, [onActiveDrawingToolChange, onSelectedDrawingIdChange]);
1034
- const toggleDrawingTool = (0, import_react8.useCallback)(
1035
- (tool) => {
1036
- if (!DRAWING_TOOLS.has(tool)) return;
1037
- if (requireFocusedChart && !focusedChartId) return;
1038
- onModeChange?.();
1039
- clearDraft();
1040
- onActiveDrawingToolChange?.(activeDrawingTool === tool ? null : tool);
1041
- },
1042
- [
1043
- activeDrawingTool,
1044
- clearDraft,
1045
- focusedChartId,
1046
- onActiveDrawingToolChange,
1047
- onModeChange,
1048
- requireFocusedChart
1049
- ]
1050
- );
1051
- const commitDrawing = (0, import_react8.useCallback)(
1052
- ({ chartId, type, start, end }) => {
1053
- const drawing = createDrawing({
1054
- chartId,
1055
- type,
1056
- start,
1057
- end,
1058
- createDrawingId
1059
- });
1060
- onDrawingsChange?.([...Array.isArray(drawings) ? drawings : [], drawing]);
1061
- onSelectedDrawingIdChange?.(drawing.id);
1062
- onActiveDrawingToolChange?.(null);
1063
- clearDraft();
1064
- },
1065
- [
1066
- clearDraft,
1067
- createDrawingId,
1068
- drawings,
1069
- onActiveDrawingToolChange,
1070
- onDrawingsChange,
1071
- onSelectedDrawingIdChange
1072
- ]
1073
- );
1074
- const startDraftDrawing = (0, import_react8.useCallback)(
1075
- ({ chartId, type, start, end = start }) => {
1076
- drawingSessionRef.current = { chartId, startPoint: start };
1077
- setDraftDrawing(createDraftDrawing({ chartId, type, start, end }));
1078
- },
1079
- []
1080
- );
1081
- const updateDraftDrawing = (0, import_react8.useCallback)(({ chartId, type, start, end }) => {
1082
- setDraftDrawing(createDraftDrawing({ chartId, type, start, end }));
1083
- }, []);
1084
- const startEditDrawing = (0, import_react8.useCallback)(({ drawingId, endpoint }) => {
1085
- drawingEditRef.current = {
1086
- id: drawingId,
1087
- endpoint: endpoint || "move"
1088
- };
1089
- }, []);
1090
- const finishEditDrawing = (0, import_react8.useCallback)(() => {
1091
- drawingEditRef.current = null;
1092
- }, []);
1093
- const deleteSelectedDrawing = (0, import_react8.useCallback)(() => {
1094
- if (!selectedDrawingId) return false;
1095
- onDrawingsChange?.(removeDrawingById(drawings, selectedDrawingId));
1096
- onSelectedDrawingIdChange?.(null);
1097
- return true;
1098
- }, [
1099
- drawings,
1100
- onDrawingsChange,
1101
- onSelectedDrawingIdChange,
1102
- selectedDrawingId
1103
- ]);
1104
- (0, import_react8.useEffect)(() => {
1105
- if (activeDrawingTool) return;
1106
- clearDraft();
1107
- }, [activeDrawingTool, clearDraft]);
1108
- return {
1109
- draftDrawing,
1110
- drawingSessionRef,
1111
- drawingEditRef,
1112
- clearDraft,
1113
- cancelDrawing,
1114
- toggleDrawingTool,
1115
- commitDrawing,
1116
- startDraftDrawing,
1117
- updateDraftDrawing,
1118
- startEditDrawing,
1119
- finishEditDrawing,
1120
- deleteSelectedDrawing
1121
- };
1122
- }
1123
-
1124
- // src/react/ChartGrid.jsx
1125
- var CHART_HEIGHT = 360;
1126
- var RIGHT_AXIS_WIDTH = 58;
1127
- var PLOT_PADDING = {
1128
- left: 38,
1129
- right: RIGHT_AXIS_WIDTH,
1130
- top: 34,
1131
- bottom: 28
1132
- };
1133
- var FOLLOW_VISIBLE_RIGHT_EDGE_RATIO = 0.15;
1134
- var Y_SCALE_MIN = 0.05;
1135
- var Y_SCALE_MAX = 40;
1136
- var Y_AXIS_TICK_COUNT = 5;
1137
- var X_AXIS_TICK_COUNT = 5;
1138
- var X_SCALE_MIN_SPAN = 10;
1139
- var X_SCALE_MAX_SPAN = 1e9;
1140
- var JUMP_LATEST_RIGHT_PADDING_RATIO = 0.1;
1141
- var INITIAL_GPU_VERTEX_CAPACITY = 4096;
1142
- var AA_VERTEX_FLOAT_STRIDE = 6;
1143
- var AA_LINE_WIDTH_PX = 1.5;
1144
- var AA_EDGE_WIDTH_PX = 1;
1145
- var DASH_LENGTH_PX = 7;
1146
- var DASH_GAP_PX = 5;
1147
- var TOOLTIP_WIDTH = 220;
1148
- var TOOLTIP_OFFSET = 12;
1149
- var DEFAULT_GRID_X_SPACING = 80;
1150
- var DEFAULT_GRID_Y_SPACING = 48;
1151
- var DEFAULT_CHART_BACKGROUND = "#f5f9ff";
1152
- var EMPTY_ARRAY = [];
1153
- var EMPTY_OBJECT = {};
1154
- var withAlpha = (color, alpha) => {
1155
- const value = String(color || DEFAULT_CHART_BACKGROUND).trim();
1156
- if (/^#[0-9a-f]{6}$/i.test(value)) {
1157
- const r = Number.parseInt(value.slice(1, 3), 16);
1158
- const g = Number.parseInt(value.slice(3, 5), 16);
1159
- const b = Number.parseInt(value.slice(5, 7), 16);
1160
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
1161
- }
1162
- if (/^#[0-9a-f]{3}$/i.test(value)) {
1163
- const r = Number.parseInt(value[1] + value[1], 16);
1164
- const g = Number.parseInt(value[2] + value[2], 16);
1165
- const b = Number.parseInt(value[3] + value[3], 16);
1166
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
1167
- }
1168
- return value;
1169
- };
1170
- var hexToRgb = (color) => {
1171
- const value = String(color || "#38bdf8").replace("#", "");
1172
- const normalized = value.length === 3 ? value.split("").map((char) => char + char).join("") : value.padEnd(6, "0").slice(0, 6);
1173
- const number = Number.parseInt(normalized, 16);
1174
- return [
1175
- (number >> 16 & 255) / 255,
1176
- (number >> 8 & 255) / 255,
1177
- (number & 255) / 255
1178
- ];
1179
- };
1180
- var getReadableTextColor = (backgroundColor) => {
1181
- const [r, g, b] = hexToRgb(backgroundColor);
1182
- const toLinear = (value) => value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
1183
- const luminance = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
1184
- return luminance > 0.45 ? "#111827" : "#ffffff";
1185
- };
1186
- var getSeriesLabelTextColor = (series) => {
1187
- if (series.__labelTextColorFor !== series.color) {
1188
- series.__labelTextColorFor = series.color;
1189
- series.__labelTextColor = getReadableTextColor(series.color);
1190
- }
1191
- return series.__labelTextColor;
1192
- };
1193
- var getSelectedDrawingLayout = (layouts, drawings, selectedDrawingId) => {
1194
- if (!selectedDrawingId || !Array.isArray(drawings)) return null;
1195
- const drawing = drawings.find((candidate) => candidate?.id === selectedDrawingId);
1196
- if (!drawing) return null;
1197
- const layout = layouts.find(
1198
- (candidate) => candidate.visible && candidate.chart.id === drawing.chartId
1199
- );
1200
- return layout ? { drawing, layout } : null;
1201
- };
1202
- var getDrawingOptionsToolbarStyle = (layout) => ({
1203
- left: Math.max(
1204
- layout.rect.x + 42,
1205
- layout.rect.x + layout.rect.width - PLOT_PADDING.right - 8
1206
- ),
1207
- top: layout.rect.y + PLOT_PADDING.top + 6,
1208
- transform: "translateX(-100%)"
1209
- });
1210
- var getMovingAverageOptionsToolbarStyle = (layout) => ({
1211
- left: Math.max(
1212
- layout.rect.x + 120,
1213
- layout.rect.x + layout.rect.width - PLOT_PADDING.right - 8
1214
- ),
1215
- top: layout.rect.y + 8,
1216
- transform: "translateX(-100%)"
1217
- });
1218
- var getChartContextMenuPoint = ({
1219
- point,
1220
- chart,
1221
- layout,
1222
- initialVisiblePoints,
1223
- viewStateRef,
1224
- yScaleRef,
1225
- yCenterOffsetRef
1226
- }) => {
1227
- 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;
1228
- if (!inPlot) {
1229
- return {
1230
- x: point.x,
1231
- y: point.y,
1232
- inPlot: false,
1233
- data: null
1234
- };
1235
- }
1236
- return {
1237
- x: point.x,
1238
- y: point.y,
1239
- inPlot: true,
1240
- data: screenPointToDataPoint({
1241
- point,
1242
- chart,
1243
- plot: layout.plot,
1244
- initialVisiblePoints,
1245
- viewStateRef,
1246
- yScaleRef,
1247
- yCenterOffsetRef
1248
- })
1249
- };
1250
- };
1251
- var createProgram = (gl) => {
1252
- const vertexSource = `#version 300 es
1253
- in vec2 a_xy;
1254
- uniform vec2 u_resolution;
1255
- uniform vec4 u_rect;
1256
- uniform vec2 u_xRange;
1257
- uniform vec2 u_yRange;
1258
-
1259
- void main() {
1260
- float xDenom = max(0.000000000001, abs(u_xRange.y - u_xRange.x));
1261
- float yDenom = max(0.000000000001, abs(u_yRange.y - u_yRange.x));
1262
- float nx = (a_xy.x - u_xRange.x) / xDenom;
1263
- float ny = (a_xy.y - u_yRange.x) / yDenom;
1264
- float px = u_rect.x + nx * u_rect.z;
1265
- float py = u_rect.y + (1.0 - ny) * u_rect.w;
1266
- vec2 zeroToOne = vec2(px, py) / u_resolution;
1267
- vec2 clip = zeroToOne * 2.0 - 1.0;
1268
- gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
1269
- }
1270
- `;
1271
- const fragmentSource = `#version 300 es
1272
- precision highp float;
1273
- uniform vec3 u_color;
1274
- out vec4 outColor;
1275
-
1276
- void main() {
1277
- outColor = vec4(u_color, 1.0);
1278
- }
1279
- `;
1280
- const compile = (type, source) => {
1281
- const shader = gl.createShader(type);
1282
- gl.shaderSource(shader, source);
1283
- gl.compileShader(shader);
1284
- if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
1285
- throw new Error(gl.getShaderInfoLog(shader) || "Shader compile failed");
1286
- }
1287
- return shader;
1288
- };
1289
- const program = gl.createProgram();
1290
- gl.attachShader(program, compile(gl.VERTEX_SHADER, vertexSource));
1291
- gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fragmentSource));
1292
- gl.linkProgram(program);
1293
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
1294
- throw new Error(gl.getProgramInfoLog(program) || "Program link failed");
1295
- }
1296
- return program;
1297
- };
1298
- var createAntialiasProgram = (gl) => {
1299
- const vertexSource = `#version 300 es
1300
- in vec2 a_start;
1301
- in vec2 a_end;
1302
- in float a_side;
1303
- in float a_along;
1304
- uniform vec2 u_resolution;
1305
- uniform vec4 u_rect;
1306
- uniform vec2 u_xRange;
1307
- uniform vec2 u_yRange;
1308
- uniform float u_lineHalfWidth;
1309
- uniform float u_edgeWidth;
1310
- out float v_side;
1311
-
1312
- vec2 toPixel(vec2 value) {
1313
- float xDenom = max(0.000000000001, abs(u_xRange.y - u_xRange.x));
1314
- float yDenom = max(0.000000000001, abs(u_yRange.y - u_yRange.x));
1315
- float nx = (value.x - u_xRange.x) / xDenom;
1316
- float ny = (value.y - u_yRange.x) / yDenom;
1317
- return vec2(
1318
- u_rect.x + nx * u_rect.z,
1319
- u_rect.y + (1.0 - ny) * u_rect.w
1320
- );
1321
- }
1322
-
1323
- void main() {
1324
- vec2 startPx = toPixel(a_start);
1325
- vec2 endPx = toPixel(a_end);
1326
- vec2 delta = endPx - startPx;
1327
- float segmentLength = max(0.000001, length(delta));
1328
- vec2 direction = delta / segmentLength;
1329
- vec2 normal = vec2(-direction.y, direction.x);
1330
- float expand = u_lineHalfWidth + u_edgeWidth;
1331
- float cap = min(expand, segmentLength * 0.5);
1332
- vec2 px = mix(startPx, endPx, a_along)
1333
- + direction * ((a_along * 2.0 - 1.0) * cap)
1334
- + normal * a_side * expand;
1335
- vec2 zeroToOne = px / u_resolution;
1336
- vec2 clip = zeroToOne * 2.0 - 1.0;
1337
- v_side = a_side;
1338
- gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
1339
- }
1340
- `;
1341
- const fragmentSource = `#version 300 es
1342
- precision highp float;
1343
- uniform vec3 u_color;
1344
- uniform float u_lineHalfWidth;
1345
- uniform float u_edgeWidth;
1346
- in float v_side;
1347
- out vec4 outColor;
1348
-
1349
- void main() {
1350
- float expand = u_lineHalfWidth + u_edgeWidth;
1351
- float distanceFromCenter = abs(v_side) * expand;
1352
- float alpha = 1.0 - smoothstep(u_lineHalfWidth, expand, distanceFromCenter);
1353
- outColor = vec4(u_color, alpha);
1354
- }
1355
- `;
1356
- const compile = (type, source) => {
1357
- const shader = gl.createShader(type);
1358
- gl.shaderSource(shader, source);
1359
- gl.compileShader(shader);
1360
- if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
1361
- throw new Error(gl.getShaderInfoLog(shader) || "Shader compile failed");
1362
- }
1363
- return shader;
1364
- };
1365
- const program = gl.createProgram();
1366
- gl.attachShader(program, compile(gl.VERTEX_SHADER, vertexSource));
1367
- gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fragmentSource));
1368
- gl.linkProgram(program);
1369
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
1370
- throw new Error(gl.getProgramInfoLog(program) || "Program link failed");
1371
- }
1372
- return program;
1373
- };
1374
- var createSeriesBufferCache = () => /* @__PURE__ */ new Map();
1375
- var getNextVertexCapacity = (currentCapacity, requiredCapacity) => {
1376
- let nextCapacity = Math.max(
1377
- INITIAL_GPU_VERTEX_CAPACITY,
1378
- currentCapacity || 0
1379
- );
1380
- while (nextCapacity < requiredCapacity) {
1381
- nextCapacity *= 2;
1382
- }
1383
- return nextCapacity;
1384
- };
1385
- var getSeriesBuffer = (gl, cache, seriesId, pointCount) => {
1386
- const requiredVertexFloats = pointCount * 2;
1387
- let entry = cache.get(seriesId);
1388
- if (!entry) {
1389
- entry = {
1390
- buffer: gl.createBuffer(),
1391
- gpuCapacity: 0,
1392
- vertices: new Float32Array(
1393
- getNextVertexCapacity(0, requiredVertexFloats)
1394
- )
1395
- };
1396
- cache.set(seriesId, entry);
1397
- }
1398
- if (entry.vertices.length < requiredVertexFloats) {
1399
- entry.vertices = new Float32Array(
1400
- getNextVertexCapacity(entry.vertices.length, requiredVertexFloats)
1401
- );
1402
- }
1403
- if (entry.gpuCapacity < entry.vertices.length) {
1404
- entry.gpuCapacity = entry.vertices.length;
1405
- gl.bindBuffer(gl.ARRAY_BUFFER, entry.buffer);
1406
- gl.bufferData(
1407
- gl.ARRAY_BUFFER,
1408
- entry.gpuCapacity * Float32Array.BYTES_PER_ELEMENT,
1409
- gl.DYNAMIC_DRAW
1410
- );
1411
- }
1412
- return entry;
1413
- };
1414
- var getAntialiasSeriesBuffer = (gl, cache, seriesId, pointCount) => {
1415
- const segmentCount = Math.max(0, pointCount - 1);
1416
- const requiredVertexFloats = segmentCount * 6 * AA_VERTEX_FLOAT_STRIDE;
1417
- let entry = cache.get(seriesId);
1418
- if (!entry) {
1419
- entry = {
1420
- buffer: gl.createBuffer(),
1421
- gpuCapacity: 0,
1422
- vertices: new Float32Array(
1423
- getNextVertexCapacity(0, requiredVertexFloats)
1424
- )
1425
- };
1426
- cache.set(seriesId, entry);
1427
- }
1428
- if (entry.vertices.length < requiredVertexFloats) {
1429
- entry.vertices = new Float32Array(
1430
- getNextVertexCapacity(entry.vertices.length, requiredVertexFloats)
1431
- );
1432
- }
1433
- if (entry.gpuCapacity < entry.vertices.length) {
1434
- entry.gpuCapacity = entry.vertices.length;
1435
- gl.bindBuffer(gl.ARRAY_BUFFER, entry.buffer);
1436
- gl.bufferData(
1437
- gl.ARRAY_BUFFER,
1438
- entry.gpuCapacity * Float32Array.BYTES_PER_ELEMENT,
1439
- gl.DYNAMIC_DRAW
1440
- );
1441
- }
1442
- return entry;
1443
- };
1444
- var deleteSeriesBuffers = (gl, cache) => {
1445
- cache.forEach((entry) => {
1446
- if (entry.buffer) gl.deleteBuffer(entry.buffer);
1447
- });
1448
- cache.clear();
1449
- };
1450
- var fillSeriesPoints = ({ vertices, visiblePoints, pointCount, animatedPoint }) => {
1451
- for (let i = 0; i < pointCount; i += 1) {
1452
- if (animatedPoint && i === animatedPoint.index) {
1453
- vertices[i * 2] = animatedPoint.x;
1454
- vertices[i * 2 + 1] = animatedPoint.y;
1455
- continue;
1456
- }
1457
- vertices[i * 2] = visiblePoints.x[i];
1458
- vertices[i * 2 + 1] = visiblePoints.y[i];
1459
- }
1460
- };
1461
- var projectDataToPixel = ({ x, y, state, yRange, plot }) => ({
1462
- x: plot.x + (x - state.xMin) / (state.xMax - state.xMin) * plot.width,
1463
- y: plot.y + (yRange.maxY - y) / (yRange.maxY - yRange.minY) * plot.height
1464
- });
1465
- var toViewportLayout = (layout, scrollLeft, scrollTop) => ({
1466
- ...layout,
1467
- rect: {
1468
- ...layout.rect,
1469
- x: layout.rect.x - scrollLeft,
1470
- y: layout.rect.y - scrollTop
1471
- },
1472
- plot: {
1473
- ...layout.plot,
1474
- x: layout.plot.x - scrollLeft,
1475
- y: layout.plot.y - scrollTop
1476
- }
1477
- });
1478
- var writeLineVertex = (vertices, offset, x, y) => {
1479
- vertices[offset] = x;
1480
- vertices[offset + 1] = y;
1481
- };
1482
- var fillDashedSeriesSegments = ({
1483
- vertices,
1484
- visiblePoints,
1485
- pointCount,
1486
- state,
1487
- yRange,
1488
- plot
1489
- }) => {
1490
- let offset = 0;
1491
- let distance = 0;
1492
- const dashTotal = DASH_LENGTH_PX + DASH_GAP_PX;
1493
- for (let i = 0; i < pointCount - 1; i += 1) {
1494
- const start = {
1495
- x: visiblePoints.x[i],
1496
- y: visiblePoints.y[i]
1497
- };
1498
- const end = {
1499
- x: visiblePoints.x[i + 1],
1500
- y: visiblePoints.y[i + 1]
1501
- };
1502
- const startPx = projectDataToPixel({ ...start, state, yRange, plot });
1503
- const endPx = projectDataToPixel({ ...end, state, yRange, plot });
1504
- const pixelLength = Math.hypot(endPx.x - startPx.x, endPx.y - startPx.y);
1505
- if (!Number.isFinite(pixelLength) || pixelLength <= 0) continue;
1506
- let consumed = 0;
1507
- while (consumed < pixelLength) {
1508
- const phase = distance % dashTotal;
1509
- const boundary = phase < DASH_LENGTH_PX ? DASH_LENGTH_PX - phase : dashTotal - phase;
1510
- const step = Math.min(boundary, pixelLength - consumed);
1511
- if (phase < DASH_LENGTH_PX) {
1512
- if (offset + 4 > vertices.length) {
1513
- return offset / 2;
1514
- }
1515
- const t0 = consumed / pixelLength;
1516
- const t1 = (consumed + step) / pixelLength;
1517
- writeLineVertex(
1518
- vertices,
1519
- offset,
1520
- start.x + (end.x - start.x) * t0,
1521
- start.y + (end.y - start.y) * t0
1522
- );
1523
- offset += 2;
1524
- writeLineVertex(
1525
- vertices,
1526
- offset,
1527
- start.x + (end.x - start.x) * t1,
1528
- start.y + (end.y - start.y) * t1
1529
- );
1530
- offset += 2;
1531
- }
1532
- consumed += step;
1533
- distance += step;
1534
- }
1535
- }
1536
- return offset / 2;
1537
- };
1538
- var getSeriesPoint = (visiblePoints, animatedPoint, index) => {
1539
- if (animatedPoint && index === animatedPoint.index) {
1540
- return { x: animatedPoint.x, y: animatedPoint.y };
1541
- }
1542
- return { x: visiblePoints.x[index], y: visiblePoints.y[index] };
1543
- };
1544
- var getSinglePointSegment = ({ series, state, yRange, plot }) => {
1545
- if (series.length !== 1) return null;
1546
- const x = series.rawX[0];
1547
- const y = series.rawY[0];
1548
- if (x < state.xMin || x > state.xMax || y < yRange.minY || y > yRange.maxY) {
1549
- return null;
1550
- }
1551
- const halfWidth = (state.xMax - state.xMin) / Math.max(1, plot.width) * 3;
1552
- return {
1553
- x: new Float64Array([x - halfWidth, x + halfWidth]),
1554
- y: new Float32Array([y, y]),
1555
- bucketSize: 1,
1556
- pointCount: 2,
1557
- endpoint: { x, y }
1558
- };
1559
- };
1560
- var writeAntialiasVertex = (vertices, offset, start, end, side, along) => {
1561
- vertices[offset] = start.x;
1562
- vertices[offset + 1] = start.y;
1563
- vertices[offset + 2] = end.x;
1564
- vertices[offset + 3] = end.y;
1565
- vertices[offset + 4] = side;
1566
- vertices[offset + 5] = along;
1567
- };
1568
- var fillAntialiasSeriesSegments = ({
1569
- vertices,
1570
- visiblePoints,
1571
- pointCount,
1572
- animatedPoint
1573
- }) => {
1574
- let offset = 0;
1575
- for (let i = 0; i < pointCount - 1; i += 1) {
1576
- const start = getSeriesPoint(visiblePoints, animatedPoint, i);
1577
- const end = getSeriesPoint(visiblePoints, animatedPoint, i + 1);
1578
- writeAntialiasVertex(vertices, offset, start, end, -1, 0);
1579
- offset += AA_VERTEX_FLOAT_STRIDE;
1580
- writeAntialiasVertex(vertices, offset, start, end, 1, 0);
1581
- offset += AA_VERTEX_FLOAT_STRIDE;
1582
- writeAntialiasVertex(vertices, offset, start, end, -1, 1);
1583
- offset += AA_VERTEX_FLOAT_STRIDE;
1584
- writeAntialiasVertex(vertices, offset, start, end, -1, 1);
1585
- offset += AA_VERTEX_FLOAT_STRIDE;
1586
- writeAntialiasVertex(vertices, offset, start, end, 1, 0);
1587
- offset += AA_VERTEX_FLOAT_STRIDE;
1588
- writeAntialiasVertex(vertices, offset, start, end, 1, 1);
1589
- offset += AA_VERTEX_FLOAT_STRIDE;
1590
- }
1591
- return offset / AA_VERTEX_FLOAT_STRIDE;
1592
- };
1593
- var lowerBound2 = (array, value) => {
1594
- let lo = 0;
1595
- let hi = array.length;
1596
- while (lo < hi) {
1597
- const mid = lo + hi >> 1;
1598
- if (array[mid] < value) lo = mid + 1;
1599
- else hi = mid;
1600
- }
1601
- return lo;
1602
- };
1603
- var getNearestPointIndex = (xValues, xValue) => {
1604
- if (!xValues.length) return -1;
1605
- const nextIndex = lowerBound2(xValues, xValue);
1606
- if (nextIndex <= 0) return 0;
1607
- if (nextIndex >= xValues.length) return xValues.length - 1;
1608
- const previousIndex = nextIndex - 1;
1609
- return Math.abs(xValues[nextIndex] - xValue) < Math.abs(xValue - xValues[previousIndex]) ? nextIndex : previousIndex;
1610
- };
1611
- var getChartXBounds = (chart) => {
1612
- let minX = Infinity;
1613
- let maxX = -Infinity;
1614
- chart.series.forEach((series) => {
1615
- if (series.length === 0) return;
1616
- minX = Math.min(minX, series.rawX[0]);
1617
- maxX = Math.max(maxX, series.rawX[series.length - 1]);
1618
- });
1619
- return { minX, maxX };
1620
- };
1621
- var getOrderedSeries = (chart, seriesOrderByChart) => {
1622
- const series = Array.isArray(chart?.series) ? chart.series : [];
1623
- const order = seriesOrderByChart?.[chart?.id];
1624
- if (!Array.isArray(order) || order.length !== series.length) {
1625
- return series;
1626
- }
1627
- const used = /* @__PURE__ */ new Set();
1628
- const ordered = [];
1629
- order.forEach((seriesIndex) => {
1630
- if (!Number.isInteger(seriesIndex)) return;
1631
- if (seriesIndex < 0 || seriesIndex >= series.length) return;
1632
- if (used.has(seriesIndex)) return;
1633
- used.add(seriesIndex);
1634
- ordered.push(series[seriesIndex]);
1635
- });
1636
- if (ordered.length !== series.length) return series;
1637
- return ordered;
1638
- };
1639
- var normalizeMovingAverage = (movingAverage) => {
1640
- if (!movingAverage?.enabled) return null;
1641
- return {
1642
- enabled: true,
1643
- period: Number.isFinite(Number(movingAverage.period)) ? Math.max(1, Math.round(Number(movingAverage.period))) : 21,
1644
- type: movingAverage.type === "sma" ? "sma" : "ema"
1645
- };
1646
- };
1647
- var getMovingAverageCacheKey = (series, movingAverage) => `${series.id}::ma:${movingAverage.type}:${movingAverage.period}`;
1648
- var calculateMovingAverageChunk = ({
1649
- sourceY,
1650
- startIndex,
1651
- period,
1652
- type,
1653
- previousAverage
1654
- }) => {
1655
- const out = new Float32Array(Math.max(0, sourceY.length - startIndex));
1656
- if (out.length === 0) return out;
1657
- if (type === "sma") {
1658
- let sum = 0;
1659
- const firstWindowStart = Math.max(0, startIndex - period + 1);
1660
- for (let i = firstWindowStart; i < startIndex; i += 1) {
1661
- sum += sourceY[i];
1662
- }
1663
- for (let sourceIndex = startIndex; sourceIndex < sourceY.length; sourceIndex += 1) {
1664
- sum += sourceY[sourceIndex];
1665
- const removeIndex = sourceIndex - period;
1666
- if (removeIndex >= firstWindowStart) {
1667
- sum -= sourceY[removeIndex];
1668
- }
1669
- const safePeriod = Math.min(period, sourceIndex + 1);
1670
- out[sourceIndex - startIndex] = sum / safePeriod;
1671
- }
1672
- return out;
1673
- }
1674
- const multiplier = 2 / (period + 1);
1675
- let prev = Number.isFinite(previousAverage) ? previousAverage : sourceY[startIndex];
1676
- for (let sourceIndex = startIndex; sourceIndex < sourceY.length; sourceIndex += 1) {
1677
- prev = sourceY[sourceIndex] * multiplier + prev * (1 - multiplier);
1678
- out[sourceIndex - startIndex] = prev;
1679
- }
1680
- return out;
1681
- };
1682
- var getMovingAverageSeriesForChart = ({
1683
- chart,
1684
- movingAverage,
1685
- cache
1686
- }) => {
1687
- const normalized = normalizeMovingAverage(movingAverage);
1688
- if (!normalized || !cache) return [];
1689
- return chart.series.map((series) => {
1690
- if (series.length === 0) return null;
1691
- const cacheKey = getMovingAverageCacheKey(series, normalized);
1692
- const cached = cache.get(cacheKey);
1693
- if (cached && cached.sourceSeries === series && cached.sourceLength <= series.length) {
1694
- const appendStart = cached.sourceLength;
1695
- if (appendStart < series.length) {
1696
- const appendedY = calculateMovingAverageChunk({
1697
- sourceY: series.rawY.subarray(0, series.length),
1698
- startIndex: appendStart,
1699
- period: normalized.period,
1700
- type: normalized.type,
1701
- previousAverage: appendStart > 0 ? cached.series.rawY[appendStart - 1] : void 0
1702
- });
1703
- cached.series.append(
1704
- series.rawX.subarray(appendStart, series.length),
1705
- appendedY
1706
- );
1707
- cached.sourceLength = series.length;
1708
- }
1709
- cached.series.color = series.color;
1710
- cached.series.name = `${series.name} ${normalized.type.toUpperCase()} ${normalized.period}`;
1711
- return cached.series;
1712
- }
1713
- const y = calculateMovingAverageChunk({
1714
- sourceY: series.rawY.subarray(0, series.length),
1715
- startIndex: 0,
1716
- period: normalized.period,
1717
- type: normalized.type
1718
- });
1719
- const maSeries = createSeries({
1720
- id: cacheKey,
1721
- name: `${series.name} ${normalized.type.toUpperCase()} ${normalized.period}`,
1722
- color: series.color,
1723
- x: series.rawX.subarray(0, series.length),
1724
- y
1725
- });
1726
- cache.set(cacheKey, {
1727
- sourceSeries: series,
1728
- sourceLength: series.length,
1729
- series: maSeries
1730
- });
1731
- return maSeries;
1732
- }).filter(Boolean);
1733
- };
1734
- var getInitialView = (chart, initialVisiblePoints = null) => {
1735
- const { minX, maxX } = getChartXBounds(chart);
1736
- if (!Number.isFinite(minX) || !Number.isFinite(maxX)) {
1737
- return { xMin: 0, xMax: 1 };
1738
- }
1739
- if (minX === maxX) {
1740
- const span2 = Number.isFinite(initialVisiblePoints) && initialVisiblePoints > 0 ? Math.max(10, initialVisiblePoints) : 10;
1741
- const rightPadding2 = span2 * JUMP_LATEST_RIGHT_PADDING_RATIO;
1742
- const nextMax = maxX + rightPadding2;
1743
- return { xMin: nextMax - span2, xMax: nextMax };
1744
- }
1745
- let span = maxX - minX;
1746
- if (Number.isFinite(initialVisiblePoints) && initialVisiblePoints > 0) {
1747
- span = Math.min(span, initialVisiblePoints);
1748
- const rightPadding2 = span * JUMP_LATEST_RIGHT_PADDING_RATIO;
1749
- const nextMax = maxX + rightPadding2;
1750
- return { xMin: nextMax - span, xMax: nextMax };
1751
- }
1752
- const rightPadding = span * JUMP_LATEST_RIGHT_PADDING_RATIO;
1753
- return { xMin: minX, xMax: maxX + rightPadding };
1754
- };
1755
- var getYRange = (chart, xMin, xMax, width) => {
1756
- let minY = Infinity;
1757
- let maxY = -Infinity;
1758
- let renderedPoints = 0;
1759
- let bucketSize = 1;
1760
- chart.series.forEach((series) => {
1761
- const visible = series.getVisiblePoints(xMin, xMax, width);
1762
- renderedPoints += visible.pointCount;
1763
- bucketSize = Math.max(bucketSize, visible.bucketSize);
1764
- for (let i = 0; i < visible.y.length; i += 1) {
1765
- const value = visible.y[i];
1766
- if (value < minY) minY = value;
1767
- if (value > maxY) maxY = value;
1768
- }
1769
- });
1770
- if (!Number.isFinite(minY) || !Number.isFinite(maxY)) {
1771
- return { minY: 0, maxY: 1, renderedPoints, bucketSize };
1772
- }
1773
- const fixedMinY = Number(chart.yRange?.min);
1774
- const fixedMaxY = Number(chart.yRange?.max);
1775
- if (Number.isFinite(fixedMinY) && Number.isFinite(fixedMaxY) && fixedMinY < fixedMaxY) {
1776
- return { minY: fixedMinY, maxY: fixedMaxY, renderedPoints, bucketSize };
1777
- }
1778
- if (minY === maxY) {
1779
- minY -= 1;
1780
- maxY += 1;
1781
- }
1782
- const pad = (maxY - minY) * 0.08;
1783
- return { minY: minY - pad, maxY: maxY + pad, renderedPoints, bucketSize };
1784
- };
1785
- var applyYScale = (yRange, scale = 1, centerOffset = 0) => {
1786
- const clampedScale = Math.min(Y_SCALE_MAX, Math.max(Y_SCALE_MIN, scale));
1787
- const center = (yRange.minY + yRange.maxY) / 2 + centerOffset;
1788
- const halfRange = (yRange.maxY - yRange.minY) * clampedScale / 2;
1789
- return {
1790
- ...yRange,
1791
- minY: center - halfRange,
1792
- maxY: center + halfRange
1793
- };
1794
- };
1795
- var getLatestYBounds = (chart) => {
1796
- let minY = Infinity;
1797
- let maxY = -Infinity;
1798
- chart.series.forEach((series) => {
1799
- if (series.length === 0) return;
1800
- const value = series.rawY[series.length - 1];
1801
- if (value < minY) minY = value;
1802
- if (value > maxY) maxY = value;
1803
- });
1804
- return { minY, maxY };
1805
- };
1806
- var getPlotWidth = (node) => node ? Math.max(1, node.offsetWidth - PLOT_PADDING.left - PLOT_PADDING.right) : 1;
1807
- var trimFormattedNumber = (value) => value.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "");
1808
- var formatNumber = (value) => {
1809
- if (!Number.isFinite(value)) return "";
1810
- const abs = Math.abs(value);
1811
- if (abs !== 0 && abs < 1e-4) return value.toExponential(2);
1812
- if (abs < 1) return trimFormattedNumber(value.toPrecision(5));
1813
- if (abs >= 1e5) return value.toFixed(0);
1814
- if (abs >= 1e3) return trimFormattedNumber(value.toFixed(1));
1815
- if (abs >= 10) return trimFormattedNumber(value.toFixed(2));
1816
- return trimFormattedNumber(value.toFixed(4));
1817
- };
1818
- var formatCompactNumber = (value) => {
1819
- if (!Number.isFinite(value)) return "";
1820
- const abs = Math.abs(value);
1821
- if (abs >= 1e9) return `${(value / 1e9).toFixed(abs >= 1e10 ? 0 : 1)}B`;
1822
- if (abs >= 1e6) return `${(value / 1e6).toFixed(abs >= 1e7 ? 0 : 1)}M`;
1823
- if (abs >= 1e3) return `${(value / 1e3).toFixed(abs >= 1e4 ? 0 : 1)}k`;
1824
- return formatNumber(value);
1825
- };
1826
- var normalizeRect = (start, end) => {
1827
- if (!start || !end) return null;
1828
- const left = Math.min(start.x, end.x);
1829
- const top = Math.min(start.y, end.y);
1830
- return {
1831
- left,
1832
- top,
1833
- width: Math.abs(end.x - start.x),
1834
- height: Math.abs(end.y - start.y)
1835
- };
1836
- };
1837
- var clampPointToPlot = (point, plot) => ({
1838
- x: Math.min(plot.x + plot.width, Math.max(plot.x, point.x)),
1839
- y: Math.min(plot.y + plot.height, Math.max(plot.y, point.y))
1840
- });
1841
- var applyRectangleZoom = ({
1842
- chart,
1843
- plot,
1844
- start,
1845
- end,
1846
- initialVisiblePoints,
1847
- viewStateRef,
1848
- yScaleRef,
1849
- yCenterOffsetRef,
1850
- yManualScaleRef
1851
- }) => {
1852
- const clampedStart = clampPointToPlot(start, plot);
1853
- const clampedEnd = clampPointToPlot(end, plot);
1854
- const rect = normalizeRect(clampedStart, clampedEnd);
1855
- if (!rect || rect.width < 6 || rect.height < 6) return false;
1856
- const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
1857
- const currentYRange = applyYScale(
1858
- getYRange(chart, state.xMin, state.xMax, plot.width),
1859
- yScaleRef.current.get(chart.id) ?? 1,
1860
- yCenterOffsetRef.current.get(chart.id) ?? 0
1861
- );
1862
- const leftRatio = (rect.left - plot.x) / plot.width;
1863
- const rightRatio = (rect.left + rect.width - plot.x) / plot.width;
1864
- const topRatio = (rect.top - plot.y) / plot.height;
1865
- const bottomRatio = (rect.top + rect.height - plot.y) / plot.height;
1866
- const nextMin = state.xMin + leftRatio * (state.xMax - state.xMin);
1867
- const nextMax = state.xMin + rightRatio * (state.xMax - state.xMin);
1868
- if (nextMax - nextMin < X_SCALE_MIN_SPAN) return false;
1869
- const selectedMaxY = currentYRange.maxY - topRatio * (currentYRange.maxY - currentYRange.minY);
1870
- const selectedMinY = currentYRange.maxY - bottomRatio * (currentYRange.maxY - currentYRange.minY);
1871
- const selectedSpan = selectedMaxY - selectedMinY;
1872
- if (!Number.isFinite(selectedSpan) || selectedSpan <= 0) return false;
1873
- viewStateRef.current.set(chart.id, { xMin: nextMin, xMax: nextMax });
1874
- const baseYRange = getYRange(chart, nextMin, nextMax, plot.width);
1875
- const baseSpan = baseYRange.maxY - baseYRange.minY;
1876
- if (Number.isFinite(baseSpan) && baseSpan > 0) {
1877
- const selectedCenter = (selectedMinY + selectedMaxY) / 2;
1878
- const baseCenter = (baseYRange.minY + baseYRange.maxY) / 2;
1879
- const nextScale = Math.min(
1880
- Y_SCALE_MAX,
1881
- Math.max(Y_SCALE_MIN, selectedSpan / baseSpan)
1882
- );
1883
- yScaleRef.current.set(chart.id, nextScale);
1884
- yCenterOffsetRef.current.set(chart.id, selectedCenter - baseCenter);
1885
- yManualScaleRef.current.add(chart.id);
1886
- }
1887
- return true;
1888
- };
1889
- var getChartState = (chart, initialVisiblePoints, viewStateRef) => viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
1890
- var getScaledYRangeForLayout = ({
1891
- chart,
1892
- state,
1893
- plot,
1894
- yScaleRef,
1895
- yCenterOffsetRef
1896
- }) => applyYScale(
1897
- getYRange(chart, state.xMin, state.xMax, plot.width),
1898
- yScaleRef.current.get(chart.id) ?? 1,
1899
- yCenterOffsetRef.current.get(chart.id) ?? 0
1900
- );
1901
- var screenPointToDataPoint = ({
1902
- point,
1903
- chart,
1904
- plot,
1905
- initialVisiblePoints,
1906
- viewStateRef,
1907
- yScaleRef,
1908
- yCenterOffsetRef
1909
- }) => {
1910
- const state = getChartState(chart, initialVisiblePoints, viewStateRef);
1911
- const yRange = getScaledYRangeForLayout({
1912
- chart,
1913
- state,
1914
- plot,
1915
- yScaleRef,
1916
- yCenterOffsetRef
1917
- });
1918
- const xRatio = (point.x - plot.x) / plot.width;
1919
- const yRatio = (point.y - plot.y) / plot.height;
1920
- return {
1921
- x: state.xMin + xRatio * (state.xMax - state.xMin),
1922
- y: yRange.maxY - yRatio * (yRange.maxY - yRange.minY)
1923
- };
1924
- };
1925
- var dataPointToScreenPoint = ({ point, state, yRange, plot }) => {
1926
- const xRatio = (point.x - state.xMin) / (state.xMax - state.xMin);
1927
- const yRatio = (yRange.maxY - point.y) / (yRange.maxY - yRange.minY);
1928
- return {
1929
- x: plot.x + xRatio * plot.width,
1930
- y: plot.y + yRatio * plot.height
1931
- };
1932
- };
1933
- var projectDataPointToScreenPoint = ({
1934
- point,
1935
- layout,
1936
- initialVisiblePoints,
1937
- viewStateRef,
1938
- yScaleRef,
1939
- yCenterOffsetRef
1940
- }) => {
1941
- const state = getChartState(layout.chart, initialVisiblePoints, viewStateRef);
1942
- const yRange = getScaledYRangeForLayout({
1943
- chart: layout.chart,
1944
- state,
1945
- plot: layout.plot,
1946
- yScaleRef,
1947
- yCenterOffsetRef
1948
- });
1949
- return dataPointToScreenPoint({
1950
- point,
1951
- state,
1952
- yRange,
1953
- plot: layout.plot
1954
- });
1955
- };
1956
- var createAxisOverlay = ({
1957
- chart,
1958
- plot,
1959
- state,
1960
- yRange,
1961
- seriesEndpoints,
1962
- seriesOrderByChart = {}
1963
- }) => {
1964
- const { maxX } = getChartXBounds(chart);
1965
- const ticks = Array.from({ length: Y_AXIS_TICK_COUNT }, (_, index) => {
1966
- const ratio = Y_AXIS_TICK_COUNT === 1 ? 0 : index / (Y_AXIS_TICK_COUNT - 1);
1967
- const value = yRange.maxY - ratio * (yRange.maxY - yRange.minY);
1968
- return {
1969
- id: `${chart.id}-tick-${index}`,
1970
- value,
1971
- top: ratio * plot.height
1972
- };
1973
- });
1974
- const xTicks = Array.from({ length: X_AXIS_TICK_COUNT }, (_, index) => {
1975
- const ratio = X_AXIS_TICK_COUNT === 1 ? 0 : index / (X_AXIS_TICK_COUNT - 1);
1976
- return {
1977
- id: `${chart.id}-x-tick-${index}`,
1978
- value: state.xMin + ratio * (state.xMax - state.xMin),
1979
- left: 18 + ratio * Math.max(1, plot.width - 36)
1980
- };
1981
- });
1982
- const latestValues = getOrderedSeries(chart, seriesOrderByChart).map((series) => {
1983
- if (series.length === 0) return null;
1984
- const value = series.rawY[series.length - 1];
1985
- const x = series.rawX[series.length - 1];
1986
- const endpoint = seriesEndpoints.get(series.id) || { x, y: value };
1987
- const ratio = (yRange.maxY - endpoint.y) / (yRange.maxY - yRange.minY);
1988
- const xRatio = (endpoint.x - state.xMin) / (state.xMax - state.xMin);
1989
- return {
1990
- id: series.id,
1991
- color: series.color,
1992
- textColor: getSeriesLabelTextColor(series),
1993
- value: endpoint.y,
1994
- rawValue: value,
1995
- x,
1996
- left: xRatio * plot.width,
1997
- top: ratio * plot.height
1998
- };
1999
- }).filter(
2000
- (value) => value && Number.isFinite(value.top) && value.top >= -10 && value.top <= plot.height + 10
2001
- );
2002
- return {
2003
- ticks,
2004
- xTicks,
2005
- latestValues,
2006
- plotWidth: plot.width,
2007
- showJumpLatest: Number.isFinite(maxX) && (maxX < state.xMin || maxX > state.xMax)
2008
- };
2009
- };
2010
- function ChartCell({
2011
- chart,
2012
- setRef,
2013
- axisOverlay,
2014
- focused,
2015
- onFocus,
2016
- onFullscreenToggle,
2017
- onReset,
2018
- onJumpLatest,
2019
- onRectangleZoomToggle,
2020
- rectangleZoomActive = false,
2021
- movingAverage = null,
2022
- onMovingAverageToggle,
2023
- activeDrawingTool = null,
2024
- onDrawingToolToggle,
2025
- hasDrawings = false,
2026
- onClearDrawingsRequest,
2027
- disableDrawings = false,
2028
- gridLines = false,
2029
- showToolbar = true,
2030
- showLatestValueLine = true,
2031
- height = CHART_HEIGHT,
2032
- isFullscreen = false,
2033
- backgroundColor = DEFAULT_CHART_BACKGROUND,
2034
- formatXTick = formatCompactNumber,
2035
- formatYValue = formatNumber
2036
- }) {
2037
- const headerBackground = withAlpha(backgroundColor, 0.82);
2038
- const controlBackground = withAlpha(backgroundColor, 0.78);
2039
- const mutedControlBackground = withAlpha(backgroundColor, 0.7);
2040
- const jumpLatestStyle = typeof height === "number" ? {
2041
- right: RIGHT_AXIS_WIDTH + 6,
2042
- top: height - PLOT_PADDING.bottom - 34
2043
- } : {
2044
- right: RIGHT_AXIS_WIDTH + 6,
2045
- bottom: PLOT_PADDING.bottom + 6
2046
- };
2047
- const gridOptions = gridLines === true ? EMPTY_OBJECT : gridLines;
2048
- const gridXSpacing = Math.max(
2049
- 8,
2050
- Number(gridOptions?.xSpacing) || DEFAULT_GRID_X_SPACING
2051
- );
2052
- const gridYSpacing = Math.max(
2053
- 8,
2054
- Number(gridOptions?.ySpacing) || DEFAULT_GRID_Y_SPACING
2055
- );
2056
- return /* @__PURE__ */ import_react9.default.createElement(
2057
- "div",
2058
- {
2059
- ref: setRef,
2060
- className: "relative select-none overflow-hidden rounded-sm",
2061
- style: { height, backgroundColor },
2062
- onPointerDown: () => onFocus(chart.id)
2063
- },
2064
- /* @__PURE__ */ import_react9.default.createElement(
2065
- "div",
2066
- {
2067
- className: "pointer-events-none absolute inset-x-0 top-0 z-10 flex h-8 items-center justify-between px-2 backdrop-blur",
2068
- style: { backgroundColor: headerBackground }
2069
- },
2070
- /* @__PURE__ */ import_react9.default.createElement(
2071
- "div",
2072
- {
2073
- className: `flex min-w-0 items-center gap-1 rounded-sm px-1 text-sm font-semibold text-foreground ${focused ? "ring-1 ring-primary/70" : ""}`
2074
- },
2075
- chart.pinned ? /* @__PURE__ */ import_react9.default.createElement(
2076
- import_react10.PushPinSimpleIcon,
2077
- {
2078
- size: 14,
2079
- weight: "fill",
2080
- className: "shrink-0 text-foreground/80"
2081
- }
2082
- ) : null,
2083
- /* @__PURE__ */ import_react9.default.createElement("span", { className: "truncate" }, chart.title)
2084
- )
2085
- ),
2086
- gridLines ? /* @__PURE__ */ import_react9.default.createElement(
2087
- "div",
2088
- {
2089
- className: "pointer-events-none absolute text-border/40",
2090
- style: {
2091
- left: PLOT_PADDING.left,
2092
- right: PLOT_PADDING.right,
2093
- top: PLOT_PADDING.top,
2094
- bottom: PLOT_PADDING.bottom,
2095
- 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))",
2096
- backgroundSize: `${gridXSpacing}px 100%, 100% ${gridYSpacing}px`
2097
- }
2098
- }
2099
- ) : null,
2100
- axisOverlay?.showJumpLatest ? /* @__PURE__ */ import_react9.default.createElement(
2101
- "button",
2102
- {
2103
- type: "button",
2104
- "aria-label": "Jump to latest",
2105
- className: "absolute z-30 inline-flex size-7 items-center justify-center rounded-sm text-foreground hover:bg-accent hover:text-accent-foreground",
2106
- style: {
2107
- ...jumpLatestStyle,
2108
- backgroundColor: mutedControlBackground
2109
- },
2110
- onPointerDown: (event) => {
2111
- event.preventDefault();
2112
- event.stopPropagation();
2113
- },
2114
- onClick: (event) => {
2115
- event.preventDefault();
2116
- event.stopPropagation();
2117
- onJumpLatest(chart.id);
2118
- }
2119
- },
2120
- /* @__PURE__ */ import_react9.default.createElement(import_react10.ArrowLineRightIcon, { size: 16, weight: "bold" })
2121
- ) : null,
2122
- focused && showToolbar ? /* @__PURE__ */ import_react9.default.createElement(
2123
- ChartToolbar,
2124
- {
2125
- isFullscreen,
2126
- onFullscreenToggle,
2127
- onReset: () => onReset?.(chart.id),
2128
- onRectangleZoomToggle: () => onRectangleZoomToggle?.(chart.id),
2129
- rectangleZoomActive,
2130
- onMovingAverageToggle: () => onMovingAverageToggle?.(chart.id),
2131
- movingAverageActive: Boolean(movingAverage?.enabled),
2132
- activeDrawingTool,
2133
- onDrawingToolToggle,
2134
- hasDrawings,
2135
- onClearDrawingsRequest: () => onClearDrawingsRequest?.(chart.id),
2136
- disableDrawings
2137
- }
2138
- ) : null,
2139
- showLatestValueLine && axisOverlay?.latestValues.map(
2140
- (latest) => latest.left >= 0 && latest.left <= axisOverlay.plotWidth ? /* @__PURE__ */ import_react9.default.createElement(
2141
- "div",
2142
- {
2143
- key: `${latest.id}-connector`,
2144
- className: "pointer-events-none absolute z-10 opacity-70",
2145
- style: {
2146
- left: PLOT_PADDING.left + latest.left,
2147
- top: PLOT_PADDING.top + latest.top,
2148
- height: 1,
2149
- width: Math.max(0, axisOverlay.plotWidth - latest.left),
2150
- backgroundImage: `repeating-linear-gradient(to right, ${latest.color} 0 4px, transparent 4px 8px)`
2151
- }
2152
- }
2153
- ) : null
2154
- ),
2155
- /* @__PURE__ */ import_react9.default.createElement(
2156
- "div",
2157
- {
2158
- className: "absolute right-0 z-20 cursor-ns-resize",
2159
- style: {
2160
- top: PLOT_PADDING.top,
2161
- bottom: PLOT_PADDING.bottom,
2162
- width: RIGHT_AXIS_WIDTH,
2163
- backgroundColor: controlBackground,
2164
- cursor: "ns-resize"
2165
- }
2166
- },
2167
- axisOverlay?.ticks.map((tick) => /* @__PURE__ */ import_react9.default.createElement(
2168
- "div",
2169
- {
2170
- key: tick.id,
2171
- className: "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 dark:text-foreground/80",
2172
- style: { top: tick.top }
2173
- },
2174
- formatYValue(tick.value)
2175
- )),
2176
- axisOverlay?.latestValues.map((latest) => /* @__PURE__ */ import_react9.default.createElement(
2177
- "div",
2178
- {
2179
- key: latest.id,
2180
- className: "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",
2181
- style: {
2182
- top: latest.top,
2183
- backgroundColor: latest.color,
2184
- color: latest.textColor
2185
- }
2186
- },
2187
- formatYValue(latest.value)
2188
- ))
2189
- ),
2190
- /* @__PURE__ */ import_react9.default.createElement(
2191
- "div",
2192
- {
2193
- className: "absolute bottom-0 z-20 cursor-ew-resize",
2194
- style: {
2195
- left: PLOT_PADDING.left,
2196
- right: RIGHT_AXIS_WIDTH,
2197
- height: PLOT_PADDING.bottom,
2198
- backgroundColor: mutedControlBackground,
2199
- cursor: "ew-resize"
2200
- }
2201
- },
2202
- axisOverlay?.xTicks.map((tick) => /* @__PURE__ */ import_react9.default.createElement(
2203
- "div",
2204
- {
2205
- key: tick.id,
2206
- className: "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 dark:text-foreground/80",
2207
- style: { left: tick.left }
2208
- },
2209
- formatXTick(tick.value)
2210
- ))
2211
- )
2212
- );
2213
- }
2214
- function CrosshairOverlay({
2215
- crosshair,
2216
- height = "100%",
2217
- xAxisLabel = "STEP",
2218
- formatXValue = formatNumber,
2219
- formatYValue = formatNumber
2220
- }) {
2221
- if (!crosshair) return null;
2222
- return /* @__PURE__ */ import_react9.default.createElement(import_react9.default.Fragment, null, /* @__PURE__ */ import_react9.default.createElement(
2223
- "div",
2224
- {
2225
- className: "pointer-events-none absolute z-20 border-l border-foreground/40",
2226
- style: {
2227
- left: crosshair.x,
2228
- top: 0,
2229
- height
2230
- }
2231
- }
2232
- ), /* @__PURE__ */ import_react9.default.createElement(
2233
- "div",
2234
- {
2235
- className: "pointer-events-none absolute z-20 border-t border-foreground/40",
2236
- style: { left: 0, top: crosshair.y, width: "100%" }
2237
- }
2238
- ), crosshair.points.map((point) => /* @__PURE__ */ import_react9.default.createElement(
2239
- "div",
2240
- {
2241
- key: point.id,
2242
- className: "pointer-events-none absolute z-30 size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border border-background shadow-sm",
2243
- style: {
2244
- left: point.x,
2245
- top: point.y,
2246
- backgroundColor: point.color
2247
- }
2248
- }
2249
- )), /* @__PURE__ */ import_react9.default.createElement(
2250
- "div",
2251
- {
2252
- className: "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",
2253
- style: {
2254
- left: crosshair.tooltipX,
2255
- top: crosshair.tooltipY
2256
- }
2257
- },
2258
- /* @__PURE__ */ import_react9.default.createElement("div", { className: "mb-1 font-medium" }, xAxisLabel, ": ", formatXValue(crosshair.xValue)),
2259
- crosshair.points.map((point) => /* @__PURE__ */ import_react9.default.createElement(
2260
- "div",
2261
- {
2262
- key: `${point.id}-tooltip`,
2263
- className: "grid grid-cols-[auto_1fr] gap-x-2 gap-y-0.5"
2264
- },
2265
- /* @__PURE__ */ import_react9.default.createElement(
2266
- "span",
2267
- {
2268
- className: "mt-1 size-2 rounded-full",
2269
- style: { backgroundColor: point.color }
2270
- }
2271
- ),
2272
- /* @__PURE__ */ import_react9.default.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ import_react9.default.createElement("div", { className: "truncate text-muted-foreground" }, point.name), /* @__PURE__ */ import_react9.default.createElement("div", { className: "tabular-nums" }, formatYValue(point.yValue)))
2273
- ))
2274
- ));
2275
- }
2276
- function RectangleZoomOverlay({ rect }) {
2277
- if (!rect) return null;
2278
- const normalized = normalizeRect(rect.start, rect.end);
2279
- if (!normalized) return null;
2280
- return /* @__PURE__ */ import_react9.default.createElement(
2281
- "div",
2282
- {
2283
- className: "pointer-events-none absolute z-40 rounded-sm border border-primary/70 bg-primary/10",
2284
- style: {
2285
- left: normalized.left,
2286
- top: normalized.top,
2287
- width: normalized.width,
2288
- height: normalized.height
2289
- }
2290
- }
2291
- );
2292
- }
2293
- function TopMarkersOverlay({
2294
- layouts,
2295
- markers,
2296
- viewStateRef,
2297
- initialVisiblePoints,
2298
- onMarkerClick,
2299
- height = "100%"
2300
- }) {
2301
- const safeMarkers = Array.isArray(markers) ? markers : [];
2302
- if (!safeMarkers.length || !layouts.length) return null;
2303
- const markerItems = layouts.flatMap((layout) => {
2304
- const state = viewStateRef.current.get(layout.chart.id) || getInitialView(layout.chart, initialVisiblePoints);
2305
- const span = state.xMax - state.xMin;
2306
- if (!Number.isFinite(span) || span <= 0) return [];
2307
- return safeMarkers.map((marker) => {
2308
- const xValue = Number(marker?.x ?? marker?.step);
2309
- if (!Number.isFinite(xValue)) return null;
2310
- const ratio = (xValue - state.xMin) / span;
2311
- if (ratio < 0 || ratio > 1) return null;
2312
- return {
2313
- marker,
2314
- key: `${layout.chart.id}-${marker.id ?? xValue}-${xValue}`,
2315
- left: layout.plot.x + ratio * layout.plot.width,
2316
- top: layout.plot.y + 4
2317
- };
2318
- }).filter(Boolean);
2319
- });
2320
- if (!markerItems.length) return null;
2321
- return /* @__PURE__ */ import_react9.default.createElement(
2322
- "div",
2323
- {
2324
- className: "pointer-events-none absolute left-0 top-0 z-30",
2325
- style: { width: "100%", height }
2326
- },
2327
- markerItems.map((item) => /* @__PURE__ */ import_react9.default.createElement(
2328
- "button",
2329
- {
2330
- key: item.key,
2331
- type: "button",
2332
- className: "pointer-events-auto absolute -translate-x-1/2 rounded-full",
2333
- style: { left: item.left, top: item.top },
2334
- title: item.marker.title || "",
2335
- "aria-label": item.marker.ariaLabel || item.marker.title || "Chart marker",
2336
- onPointerDown: (event) => {
2337
- event.stopPropagation();
2338
- },
2339
- onClick: (event) => {
2340
- event.preventDefault();
2341
- event.stopPropagation();
2342
- onMarkerClick?.(item.marker);
2343
- }
2344
- },
2345
- /* @__PURE__ */ import_react9.default.createElement(
2346
- "span",
2347
- {
2348
- className: "block size-2.5 rounded-full border border-background shadow-sm",
2349
- style: { backgroundColor: item.marker.color || "#f97316" }
2350
- }
2351
- )
2352
- ))
2353
- );
2354
- }
2355
- var drawChartLayouts = ({
2356
- canvas,
2357
- width,
2358
- height,
2359
- gl,
2360
- program,
2361
- antialiasProgram,
2362
- antialiasLines = false,
2363
- layouts,
2364
- viewStateRef,
2365
- yScaleRef,
2366
- yCenterOffsetRef,
2367
- seriesBufferCache,
2368
- antialiasSeriesBufferCache,
2369
- initialVisiblePoints,
2370
- getAppendAnimatedPoint,
2371
- movingAverageByChart = EMPTY_OBJECT,
2372
- movingAverageCache,
2373
- seriesOrderByChart = EMPTY_OBJECT
2374
- }) => {
2375
- const dpr = Math.min(2, window.devicePixelRatio || 1);
2376
- const pixelWidth = Math.max(1, Math.floor(width * dpr));
2377
- const pixelHeight = Math.max(1, Math.floor(height * dpr));
2378
- if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
2379
- canvas.width = pixelWidth;
2380
- canvas.height = pixelHeight;
2381
- canvas.style.width = `${width}px`;
2382
- canvas.style.height = `${height}px`;
2383
- }
2384
- gl.viewport(0, 0, pixelWidth, pixelHeight);
2385
- gl.clearColor(0, 0, 0, 0);
2386
- gl.clear(gl.COLOR_BUFFER_BIT);
2387
- const activeAntialiasLines = antialiasLines && antialiasProgram;
2388
- const activeProgram = activeAntialiasLines ? antialiasProgram : program;
2389
- gl.useProgram(activeProgram);
2390
- const uResolution = gl.getUniformLocation(activeProgram, "u_resolution");
2391
- const uRect = gl.getUniformLocation(activeProgram, "u_rect");
2392
- const uXRange = gl.getUniformLocation(activeProgram, "u_xRange");
2393
- const uYRange = gl.getUniformLocation(activeProgram, "u_yRange");
2394
- const uColor = gl.getUniformLocation(activeProgram, "u_color");
2395
- let aXy = -1;
2396
- let aStart = -1;
2397
- let aEnd = -1;
2398
- let aSide = -1;
2399
- let aAlong = -1;
2400
- let uLineHalfWidth = null;
2401
- let uEdgeWidth = null;
2402
- if (activeAntialiasLines) {
2403
- aStart = gl.getAttribLocation(activeProgram, "a_start");
2404
- aEnd = gl.getAttribLocation(activeProgram, "a_end");
2405
- aSide = gl.getAttribLocation(activeProgram, "a_side");
2406
- aAlong = gl.getAttribLocation(activeProgram, "a_along");
2407
- uLineHalfWidth = gl.getUniformLocation(activeProgram, "u_lineHalfWidth");
2408
- uEdgeWidth = gl.getUniformLocation(activeProgram, "u_edgeWidth");
2409
- gl.enableVertexAttribArray(aStart);
2410
- gl.enableVertexAttribArray(aEnd);
2411
- gl.enableVertexAttribArray(aSide);
2412
- gl.enableVertexAttribArray(aAlong);
2413
- gl.uniform1f(uLineHalfWidth, AA_LINE_WIDTH_PX * dpr / 2);
2414
- gl.uniform1f(uEdgeWidth, AA_EDGE_WIDTH_PX * dpr);
2415
- gl.enable(gl.BLEND);
2416
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
2417
- } else {
2418
- aXy = gl.getAttribLocation(activeProgram, "a_xy");
2419
- gl.enableVertexAttribArray(aXy);
2420
- gl.disable(gl.BLEND);
2421
- }
2422
- gl.uniform2f(uResolution, pixelWidth, pixelHeight);
2423
- const now = performance.now();
2424
- const nextAxisOverlays = {};
2425
- layouts.forEach(({ chart, plot, visible }) => {
2426
- if (!visible) return;
2427
- if (activeAntialiasLines) {
2428
- gl.useProgram(activeProgram);
2429
- gl.uniform2f(uResolution, pixelWidth, pixelHeight);
2430
- }
2431
- const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
2432
- const scaledPlot = {
2433
- x: plot.x * dpr,
2434
- y: plot.y * dpr,
2435
- width: plot.width * dpr,
2436
- height: plot.height * dpr
2437
- };
2438
- const yRange = applyYScale(
2439
- getYRange(chart, state.xMin, state.xMax, plot.width),
2440
- yScaleRef.current.get(chart.id) ?? 1,
2441
- yCenterOffsetRef.current.get(chart.id) ?? 0
2442
- );
2443
- const seriesEndpoints = /* @__PURE__ */ new Map();
2444
- const chartMovingAverage = movingAverageByChart?.[chart.id];
2445
- const hideBaseSeries = Boolean(chartMovingAverage?.enabled) && Boolean(chartMovingAverage?.hideBase);
2446
- gl.enable(gl.SCISSOR_TEST);
2447
- gl.scissor(
2448
- Math.floor(scaledPlot.x),
2449
- Math.floor(pixelHeight - scaledPlot.y - scaledPlot.height),
2450
- Math.ceil(scaledPlot.width),
2451
- Math.ceil(scaledPlot.height)
2452
- );
2453
- gl.uniform4f(
2454
- uRect,
2455
- scaledPlot.x,
2456
- scaledPlot.y,
2457
- scaledPlot.width,
2458
- scaledPlot.height
2459
- );
2460
- gl.uniform2f(uXRange, state.xMin, state.xMax);
2461
- gl.uniform2f(uYRange, yRange.minY, yRange.maxY);
2462
- if (!hideBaseSeries) getOrderedSeries(chart, seriesOrderByChart).forEach((series) => {
2463
- let visiblePoints = series.getVisiblePoints(
2464
- state.xMin,
2465
- state.xMax,
2466
- plot.width
2467
- );
2468
- let singlePointEndpoint = null;
2469
- if (visiblePoints.pointCount < 2) {
2470
- const singlePointSegment = getSinglePointSegment({
2471
- series,
2472
- state,
2473
- yRange,
2474
- plot
2475
- });
2476
- if (!singlePointSegment) return;
2477
- visiblePoints = singlePointSegment;
2478
- singlePointEndpoint = singlePointSegment.endpoint;
2479
- }
2480
- const animatedPoint = singlePointEndpoint ? null : getAppendAnimatedPoint({
2481
- seriesId: series.id,
2482
- visiblePoints,
2483
- now
2484
- });
2485
- const pointCount = animatedPoint?.pointCount ?? visiblePoints.pointCount;
2486
- if (animatedPoint) {
2487
- seriesEndpoints.set(series.id, {
2488
- x: animatedPoint.x,
2489
- y: animatedPoint.y
2490
- });
2491
- } else if (singlePointEndpoint) {
2492
- seriesEndpoints.set(series.id, singlePointEndpoint);
2493
- }
2494
- const [r, g, b] = hexToRgb(series.color);
2495
- gl.uniform3f(uColor, r, g, b);
2496
- if (activeAntialiasLines) {
2497
- const bufferEntry2 = getAntialiasSeriesBuffer(
2498
- gl,
2499
- antialiasSeriesBufferCache,
2500
- series.id,
2501
- pointCount
2502
- );
2503
- const drawVertexCount = fillAntialiasSeriesSegments({
2504
- vertices: bufferEntry2.vertices,
2505
- visiblePoints,
2506
- pointCount,
2507
- animatedPoint
2508
- });
2509
- const stride = AA_VERTEX_FLOAT_STRIDE * Float32Array.BYTES_PER_ELEMENT;
2510
- gl.bindBuffer(gl.ARRAY_BUFFER, bufferEntry2.buffer);
2511
- gl.vertexAttribPointer(aStart, 2, gl.FLOAT, false, stride, 0);
2512
- gl.vertexAttribPointer(
2513
- aEnd,
2514
- 2,
2515
- gl.FLOAT,
2516
- false,
2517
- stride,
2518
- 2 * Float32Array.BYTES_PER_ELEMENT
2519
- );
2520
- gl.vertexAttribPointer(
2521
- aSide,
2522
- 1,
2523
- gl.FLOAT,
2524
- false,
2525
- stride,
2526
- 4 * Float32Array.BYTES_PER_ELEMENT
2527
- );
2528
- gl.vertexAttribPointer(
2529
- aAlong,
2530
- 1,
2531
- gl.FLOAT,
2532
- false,
2533
- stride,
2534
- 5 * Float32Array.BYTES_PER_ELEMENT
2535
- );
2536
- gl.bufferSubData(
2537
- gl.ARRAY_BUFFER,
2538
- 0,
2539
- bufferEntry2.vertices,
2540
- 0,
2541
- drawVertexCount * AA_VERTEX_FLOAT_STRIDE
2542
- );
2543
- gl.drawArrays(gl.TRIANGLES, 0, drawVertexCount);
2544
- return;
2545
- }
2546
- const bufferEntry = getSeriesBuffer(
2547
- gl,
2548
- seriesBufferCache,
2549
- series.id,
2550
- pointCount
2551
- );
2552
- fillSeriesPoints({
2553
- vertices: bufferEntry.vertices,
2554
- visiblePoints,
2555
- pointCount,
2556
- animatedPoint
2557
- });
2558
- gl.bindBuffer(gl.ARRAY_BUFFER, bufferEntry.buffer);
2559
- gl.vertexAttribPointer(aXy, 2, gl.FLOAT, false, 0, 0);
2560
- gl.bufferSubData(
2561
- gl.ARRAY_BUFFER,
2562
- 0,
2563
- bufferEntry.vertices,
2564
- 0,
2565
- pointCount * 2
2566
- );
2567
- gl.drawArrays(gl.LINE_STRIP, 0, pointCount);
2568
- });
2569
- const movingAverageSeries = getMovingAverageSeriesForChart({
2570
- chart,
2571
- movingAverage: chartMovingAverage,
2572
- cache: movingAverageCache
2573
- });
2574
- const orderedMovingAverageSeries = getOrderedSeries(
2575
- { ...chart, series: movingAverageSeries },
2576
- seriesOrderByChart
2577
- );
2578
- let maAXy = aXy;
2579
- let maUColor = uColor;
2580
- if (movingAverageSeries.length > 0 && activeAntialiasLines) {
2581
- gl.useProgram(program);
2582
- const maUResolution = gl.getUniformLocation(program, "u_resolution");
2583
- const maURect = gl.getUniformLocation(program, "u_rect");
2584
- const maUXRange = gl.getUniformLocation(program, "u_xRange");
2585
- const maUYRange = gl.getUniformLocation(program, "u_yRange");
2586
- maUColor = gl.getUniformLocation(program, "u_color");
2587
- maAXy = gl.getAttribLocation(program, "a_xy");
2588
- gl.enableVertexAttribArray(maAXy);
2589
- gl.uniform2f(maUResolution, pixelWidth, pixelHeight);
2590
- gl.uniform4f(
2591
- maURect,
2592
- scaledPlot.x,
2593
- scaledPlot.y,
2594
- scaledPlot.width,
2595
- scaledPlot.height
2596
- );
2597
- gl.uniform2f(maUXRange, state.xMin, state.xMax);
2598
- gl.uniform2f(maUYRange, yRange.minY, yRange.maxY);
2599
- }
2600
- orderedMovingAverageSeries.forEach((series) => {
2601
- const visiblePoints = series.getVisiblePoints(
2602
- state.xMin,
2603
- state.xMax,
2604
- plot.width
2605
- );
2606
- if (visiblePoints.pointCount < 2) return;
2607
- const pointCount = visiblePoints.pointCount;
2608
- const [r, g, b] = hexToRgb(series.color);
2609
- gl.uniform3f(maUColor, r, g, b);
2610
- const bufferEntry = getSeriesBuffer(
2611
- gl,
2612
- seriesBufferCache,
2613
- series.id,
2614
- pointCount * 8
2615
- );
2616
- const drawVertexCount = fillDashedSeriesSegments({
2617
- vertices: bufferEntry.vertices,
2618
- visiblePoints,
2619
- pointCount,
2620
- state,
2621
- yRange,
2622
- plot
2623
- });
2624
- if (drawVertexCount < 2) return;
2625
- gl.bindBuffer(gl.ARRAY_BUFFER, bufferEntry.buffer);
2626
- gl.vertexAttribPointer(maAXy, 2, gl.FLOAT, false, 0, 0);
2627
- gl.bufferSubData(
2628
- gl.ARRAY_BUFFER,
2629
- 0,
2630
- bufferEntry.vertices,
2631
- 0,
2632
- drawVertexCount * 2
2633
- );
2634
- gl.drawArrays(gl.LINES, 0, drawVertexCount);
2635
- });
2636
- nextAxisOverlays[chart.id] = createAxisOverlay({
2637
- chart,
2638
- plot,
2639
- state,
2640
- yRange,
2641
- seriesEndpoints,
2642
- seriesOrderByChart
2643
- });
2644
- gl.disable(gl.SCISSOR_TEST);
2645
- });
2646
- if (activeAntialiasLines) {
2647
- gl.disable(gl.BLEND);
2648
- }
2649
- return nextAxisOverlays;
2650
- };
2651
- function ChartFullscreenOverlay({
2652
- chart,
2653
- dataRevision,
2654
- renderRevision,
2655
- initialVisiblePoints,
2656
- backgroundColor,
2657
- antialiasLines = false,
2658
- getAppendAnimatedPoint,
2659
- viewStateRef,
2660
- yScaleRef,
2661
- yCenterOffsetRef,
2662
- yManualScaleRef,
2663
- movingAverageByChart = {},
2664
- movingAverageCache,
2665
- onMovingAverageToggle,
2666
- onMovingAverageChange,
2667
- seriesOrderByChart = {},
2668
- onClose,
2669
- onReset,
2670
- onJumpLatest,
2671
- xAxisLabel = "STEP",
2672
- drawings = EMPTY_ARRAY,
2673
- onDrawingsChange,
2674
- activeDrawingTool = null,
2675
- onActiveDrawingToolChange,
2676
- selectedDrawingId = null,
2677
- onSelectedDrawingIdChange,
2678
- createDrawingId,
2679
- onClearDrawingsRequest,
2680
- onChartContextMenu,
2681
- topMarkers = EMPTY_ARRAY,
2682
- onTopMarkerClick,
2683
- disableDrawings = false,
2684
- gridLines = false,
2685
- showToolbar = true,
2686
- showLatestValueLine = true,
2687
- showTooltips = true,
2688
- formatXTick = formatCompactNumber,
2689
- formatXValue = formatNumber,
2690
- formatYValue = formatNumber
2691
- }) {
2692
- const containerRef = (0, import_react9.useRef)(null);
2693
- const chartRef = (0, import_react9.useRef)(null);
2694
- const canvasRef = (0, import_react9.useRef)(null);
2695
- const glRef = (0, import_react9.useRef)(null);
2696
- const programRef = (0, import_react9.useRef)(null);
2697
- const antialiasProgramRef = (0, import_react9.useRef)(null);
2698
- const seriesBufferCacheRef = (0, import_react9.useRef)(createSeriesBufferCache());
2699
- const antialiasSeriesBufferCacheRef = (0, import_react9.useRef)(createSeriesBufferCache());
2700
- const movingAverageCacheRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
2701
- const dragRef = (0, import_react9.useRef)(null);
2702
- const rectangleZoomDragRef = (0, import_react9.useRef)(null);
2703
- const closeTimeoutRef = (0, import_react9.useRef)(null);
2704
- const [revision, setRevision] = (0, import_react9.useState)(0);
2705
- const [axisOverlay, setAxisOverlay] = (0, import_react9.useState)(null);
2706
- const [crosshair, setCrosshair] = (0, import_react9.useState)(null);
2707
- const [visible, setVisible] = (0, import_react9.useState)(false);
2708
- const [rectangleZoomActive, setRectangleZoomActive] = (0, import_react9.useState)(false);
2709
- const [rectangleZoomRect, setRectangleZoomRect] = (0, import_react9.useState)(null);
2710
- const chartDrawings = disableDrawings ? EMPTY_ARRAY : drawings;
2711
- const chartActiveDrawingTool = disableDrawings ? null : activeDrawingTool;
2712
- const chartSelectedDrawingId = disableDrawings ? null : selectedDrawingId;
2713
- (0, import_react9.useEffect)(() => {
2714
- if (!showTooltips) setCrosshair(null);
2715
- }, [showTooltips]);
2716
- const requestRender = (0, import_react9.useCallback)(() => {
2717
- setRevision((value) => value + 1);
2718
- }, []);
2719
- const closeWithTransition = (0, import_react9.useCallback)(() => {
2720
- setVisible(false);
2721
- closeTimeoutRef.current = window.setTimeout(onClose, 120);
2722
- }, [onClose]);
2723
- const {
2724
- draftDrawing,
2725
- drawingSessionRef,
2726
- drawingEditRef,
2727
- clearDraft,
2728
- cancelDrawing,
2729
- toggleDrawingTool,
2730
- commitDrawing,
2731
- startDraftDrawing,
2732
- updateDraftDrawing,
2733
- startEditDrawing,
2734
- finishEditDrawing,
2735
- deleteSelectedDrawing
2736
- } = useDrawingInteractions({
2737
- drawings: chartDrawings,
2738
- onDrawingsChange: disableDrawings ? void 0 : onDrawingsChange,
2739
- activeDrawingTool: chartActiveDrawingTool,
2740
- onActiveDrawingToolChange,
2741
- selectedDrawingId: chartSelectedDrawingId,
2742
- onSelectedDrawingIdChange,
2743
- createDrawingId,
2744
- onModeChange: () => {
2745
- setRectangleZoomActive(false);
2746
- setRectangleZoomRect(null);
2747
- rectangleZoomDragRef.current = null;
2748
- }
2749
- });
2750
- const toggleRectangleZoom = (0, import_react9.useCallback)(() => {
2751
- setRectangleZoomActive((value) => !value);
2752
- setRectangleZoomRect(null);
2753
- rectangleZoomDragRef.current = null;
2754
- clearDraft();
2755
- onActiveDrawingToolChange?.(null);
2756
- }, [clearDraft, onActiveDrawingToolChange]);
2757
- const toggleSelectedDrawingExtend = (0, import_react9.useCallback)(() => {
2758
- if (disableDrawings || !chartSelectedDrawingId || !onDrawingsChange) return;
2759
- onDrawingsChange(
2760
- updateDrawingById(chartDrawings, chartSelectedDrawingId, (drawing) => ({
2761
- ...drawing,
2762
- style: {
2763
- ...drawing.style,
2764
- extendRight: drawing.style?.extendRight === false
2765
- }
2766
- }))
2767
- );
2768
- }, [chartDrawings, chartSelectedDrawingId, disableDrawings, onDrawingsChange]);
2769
- const updateSelectedDrawingColor = (0, import_react9.useCallback)(
2770
- (color) => {
2771
- if (disableDrawings || !chartSelectedDrawingId || !onDrawingsChange) return;
2772
- onDrawingsChange(
2773
- updateDrawingById(chartDrawings, chartSelectedDrawingId, (drawing) => ({
2774
- ...drawing,
2775
- style: {
2776
- ...drawing.style,
2777
- color
2778
- }
2779
- }))
2780
- );
2781
- },
2782
- [chartDrawings, chartSelectedDrawingId, disableDrawings, onDrawingsChange]
2783
- );
2784
- const updateSelectedDrawingText = (0, import_react9.useCallback)(
2785
- (text) => {
2786
- if (disableDrawings || !chartSelectedDrawingId || !onDrawingsChange) return;
2787
- onDrawingsChange(
2788
- updateDrawingById(chartDrawings, chartSelectedDrawingId, (drawing) => ({
2789
- ...drawing,
2790
- style: {
2791
- ...drawing.style,
2792
- text
2793
- }
2794
- }))
2795
- );
2796
- },
2797
- [chartDrawings, chartSelectedDrawingId, disableDrawings, onDrawingsChange]
2798
- );
2799
- (0, import_react9.useEffect)(() => {
2800
- const frame = requestAnimationFrame(() => setVisible(true));
2801
- return () => {
2802
- cancelAnimationFrame(frame);
2803
- if (closeTimeoutRef.current != null) {
2804
- clearTimeout(closeTimeoutRef.current);
2805
- closeTimeoutRef.current = null;
2806
- }
2807
- };
2808
- }, []);
2809
- const getLayout = (0, import_react9.useCallback)(() => {
2810
- const node = chartRef.current;
2811
- if (!node) return null;
2812
- const rect = {
2813
- x: node.offsetLeft,
2814
- y: node.offsetTop,
2815
- width: node.offsetWidth,
2816
- height: node.offsetHeight
2817
- };
2818
- const plot = {
2819
- x: rect.x + PLOT_PADDING.left,
2820
- y: rect.y + PLOT_PADDING.top,
2821
- width: Math.max(1, rect.width - PLOT_PADDING.left - PLOT_PADDING.right),
2822
- height: Math.max(1, rect.height - PLOT_PADDING.top - PLOT_PADDING.bottom)
2823
- };
2824
- return { chart, rect, plot, visible: true };
2825
- }, [chart]);
2826
- const getLocalPoint = (0, import_react9.useCallback)((event) => {
2827
- const rect = containerRef.current?.getBoundingClientRect();
2828
- if (!rect) return null;
2829
- return {
2830
- x: event.clientX - rect.left,
2831
- y: event.clientY - rect.top
2832
- };
2833
- }, []);
2834
- (0, import_react9.useEffect)(() => {
2835
- const canvas = canvasRef.current;
2836
- if (!canvas) return;
2837
- const gl = canvas.getContext("webgl2", {
2838
- antialias: true,
2839
- alpha: true,
2840
- depth: false,
2841
- stencil: false,
2842
- preserveDrawingBuffer: false
2843
- });
2844
- if (!gl) return;
2845
- glRef.current = gl;
2846
- programRef.current = createProgram(gl);
2847
- antialiasProgramRef.current = createAntialiasProgram(gl);
2848
- requestRender();
2849
- return () => {
2850
- deleteSeriesBuffers(gl, seriesBufferCacheRef.current);
2851
- deleteSeriesBuffers(gl, antialiasSeriesBufferCacheRef.current);
2852
- glRef.current = null;
2853
- programRef.current = null;
2854
- antialiasProgramRef.current = null;
2855
- };
2856
- }, [requestRender]);
2857
- (0, import_react9.useEffect)(() => {
2858
- const container = containerRef.current;
2859
- if (!container) return;
2860
- const observer = new ResizeObserver(requestRender);
2861
- observer.observe(container);
2862
- return () => observer.disconnect();
2863
- }, [requestRender]);
2864
- (0, import_react9.useEffect)(() => {
2865
- const container = containerRef.current;
2866
- const canvas = canvasRef.current;
2867
- const gl = glRef.current;
2868
- const program = programRef.current;
2869
- const antialiasProgram = antialiasProgramRef.current;
2870
- const layout = getLayout();
2871
- if (!container || !canvas || !gl || !program || !layout) return;
2872
- const nextAxisOverlays = drawChartLayouts({
2873
- canvas,
2874
- width: container.clientWidth,
2875
- height: container.clientHeight,
2876
- gl,
2877
- program,
2878
- antialiasProgram,
2879
- antialiasLines,
2880
- layouts: [layout],
2881
- viewStateRef,
2882
- yScaleRef,
2883
- yCenterOffsetRef,
2884
- seriesBufferCache: seriesBufferCacheRef.current,
2885
- antialiasSeriesBufferCache: antialiasSeriesBufferCacheRef.current,
2886
- initialVisiblePoints,
2887
- getAppendAnimatedPoint,
2888
- movingAverageByChart,
2889
- movingAverageCache,
2890
- seriesOrderByChart
2891
- });
2892
- setAxisOverlay(nextAxisOverlays[chart.id] || null);
2893
- }, [
2894
- chart,
2895
- antialiasLines,
2896
- dataRevision,
2897
- getAppendAnimatedPoint,
2898
- getLayout,
2899
- initialVisiblePoints,
2900
- movingAverageByChart,
2901
- movingAverageCache,
2902
- seriesOrderByChart,
2903
- renderRevision,
2904
- revision,
2905
- viewStateRef,
2906
- yCenterOffsetRef,
2907
- yScaleRef
2908
- ]);
2909
- const updateCrosshair = (0, import_react9.useCallback)(
2910
- (event) => {
2911
- const point = getLocalPoint(event);
2912
- const layout = getLayout();
2913
- if (!point || !layout) return;
2914
- if (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) {
2915
- setCrosshair(null);
2916
- return;
2917
- }
2918
- const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
2919
- const xRatio = (point.x - layout.plot.x) / layout.plot.width;
2920
- const xValue = state.xMin + xRatio * (state.xMax - state.xMin);
2921
- const yRange = getYRange(
2922
- chart,
2923
- state.xMin,
2924
- state.xMax,
2925
- layout.plot.width
2926
- );
2927
- const scaledYRange = applyYScale(
2928
- yRange,
2929
- yScaleRef.current.get(chart.id) ?? 1,
2930
- yCenterOffsetRef.current.get(chart.id) ?? 0
2931
- );
2932
- const yRatio = (point.y - layout.plot.y) / layout.plot.height;
2933
- const yValue = scaledYRange.maxY - yRatio * (scaledYRange.maxY - scaledYRange.minY);
2934
- const nearestPoints = getOrderedSeries(chart, seriesOrderByChart).map((series) => {
2935
- const visiblePoints = series.getVisiblePoints(
2936
- state.xMin,
2937
- state.xMax,
2938
- layout.plot.width
2939
- );
2940
- const index = getNearestPointIndex(visiblePoints.x, xValue);
2941
- if (index < 0) return null;
2942
- const pointX = visiblePoints.x[index];
2943
- const pointY = visiblePoints.y[index];
2944
- const pointXRatio = (pointX - state.xMin) / (state.xMax - state.xMin);
2945
- const pointYRatio = (scaledYRange.maxY - pointY) / (scaledYRange.maxY - scaledYRange.minY);
2946
- return {
2947
- id: series.id,
2948
- name: series.name,
2949
- color: series.color,
2950
- xValue: pointX,
2951
- yValue: pointY,
2952
- x: layout.plot.x + pointXRatio * layout.plot.width,
2953
- y: layout.plot.y + pointYRatio * layout.plot.height
2954
- };
2955
- }).filter(Boolean).filter(
2956
- (nearestPoint) => nearestPoint.x >= layout.plot.x - 1 && nearestPoint.x <= layout.plot.x + layout.plot.width + 1 && nearestPoint.y >= layout.plot.y - 1 && nearestPoint.y <= layout.plot.y + layout.plot.height + 1
2957
- );
2958
- const tooltipLeft = point.x + TOOLTIP_WIDTH + TOOLTIP_OFFSET > layout.rect.x + layout.rect.width ? point.x - TOOLTIP_WIDTH - TOOLTIP_OFFSET : point.x + TOOLTIP_OFFSET;
2959
- const tooltipTop = Math.max(
2960
- layout.rect.y + PLOT_PADDING.top,
2961
- Math.min(
2962
- point.y + TOOLTIP_OFFSET,
2963
- layout.rect.y + layout.rect.height - 96
2964
- )
2965
- );
2966
- const primaryPoint = nearestPoints[0];
2967
- setCrosshair({
2968
- chartId: chart.id,
2969
- title: chart.title,
2970
- x: point.x,
2971
- y: point.y,
2972
- xValue: primaryPoint?.xValue ?? xValue,
2973
- yValue: primaryPoint?.yValue ?? yValue,
2974
- points: nearestPoints,
2975
- tooltipX: Math.max(layout.rect.x + 6, tooltipLeft),
2976
- tooltipY: tooltipTop
2977
- });
2978
- },
2979
- [
2980
- chart,
2981
- getLayout,
2982
- getLocalPoint,
2983
- initialVisiblePoints,
2984
- seriesOrderByChart,
2985
- viewStateRef,
2986
- yCenterOffsetRef,
2987
- yScaleRef
2988
- ]
2989
- );
2990
- const handlePointerDown = (0, import_react9.useCallback)(
2991
- (event) => {
2992
- const point = getLocalPoint(event);
2993
- const layout = getLayout();
2994
- if (!point || !layout) return;
2995
- 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;
2996
- if (chartActiveDrawingTool && DRAWING_TOOLS.has(chartActiveDrawingTool)) {
2997
- if (!inPlot || event.button !== 0) return;
2998
- event.preventDefault();
2999
- const dataPoint = screenPointToDataPoint({
3000
- point,
3001
- chart,
3002
- plot: layout.plot,
3003
- initialVisiblePoints,
3004
- viewStateRef,
3005
- yScaleRef,
3006
- yCenterOffsetRef
3007
- });
3008
- if (chartActiveDrawingTool === "hline" || chartActiveDrawingTool === "vline" || chartActiveDrawingTool === "pin") {
3009
- commitDrawing({
3010
- chartId: chart.id,
3011
- type: chartActiveDrawingTool,
3012
- start: dataPoint,
3013
- end: dataPoint
3014
- });
3015
- return;
3016
- }
3017
- if (!drawingSessionRef.current.startPoint) {
3018
- startDraftDrawing({
3019
- chartId: chart.id,
3020
- type: "trendline",
3021
- start: dataPoint,
3022
- end: dataPoint
3023
- });
3024
- return;
3025
- }
3026
- commitDrawing({
3027
- chartId: chart.id,
3028
- type: "trendline",
3029
- start: drawingSessionRef.current.startPoint,
3030
- end: dataPoint
3031
- });
3032
- return;
3033
- }
3034
- if (inPlot && Array.isArray(chartDrawings) && chartDrawings.length > 0) {
3035
- const hitDrawings = getDrawingsForChart(chartDrawings, chart.id);
3036
- for (let i = hitDrawings.length - 1; i >= 0; i -= 1) {
3037
- const hit = hitTestDrawing({
3038
- point,
3039
- drawing: hitDrawings[i],
3040
- layout,
3041
- projectPoint: (dataPoint, targetLayout) => projectDataPointToScreenPoint({
3042
- point: dataPoint,
3043
- layout: targetLayout,
3044
- initialVisiblePoints,
3045
- viewStateRef,
3046
- yScaleRef,
3047
- yCenterOffsetRef
3048
- })
3049
- });
3050
- if (hit) {
3051
- event.preventDefault();
3052
- onSelectedDrawingIdChange?.(hit.drawing.id);
3053
- if (hit.endpoint || hit.drawing.type === "hline" || hit.drawing.type === "vline" || hit.drawing.type === "pin") {
3054
- startEditDrawing({
3055
- drawingId: hit.drawing.id,
3056
- endpoint: hit.endpoint || "move"
3057
- });
3058
- event.currentTarget.setPointerCapture?.(event.pointerId);
3059
- }
3060
- return;
3061
- }
3062
- }
3063
- onSelectedDrawingIdChange?.(null);
3064
- }
3065
- if (rectangleZoomActive) {
3066
- if (!inPlot || event.button !== 0) return;
3067
- event.preventDefault();
3068
- const start = clampPointToPlot(point, layout.plot);
3069
- rectangleZoomDragRef.current = { start, end: start };
3070
- setRectangleZoomRect({ start, end: start });
3071
- event.currentTarget.setPointerCapture?.(event.pointerId);
3072
- return;
3073
- }
3074
- const inYAxis = point.x >= layout.plot.x + layout.plot.width && point.x <= layout.rect.x + layout.rect.width && point.y >= layout.plot.y && point.y <= layout.plot.y + layout.plot.height;
3075
- if (inYAxis) {
3076
- event.preventDefault();
3077
- dragRef.current = {
3078
- type: "y-scale",
3079
- startY: point.y,
3080
- startScale: yScaleRef.current.get(chart.id) ?? 1
3081
- };
3082
- event.currentTarget.setPointerCapture?.(event.pointerId);
3083
- return;
3084
- }
3085
- const inXAxis = point.x >= layout.plot.x && point.x <= layout.plot.x + layout.plot.width && point.y >= layout.plot.y + layout.plot.height && point.y <= layout.rect.y + layout.rect.height;
3086
- if (inXAxis) {
3087
- event.preventDefault();
3088
- const state2 = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
3089
- dragRef.current = {
3090
- type: "x-scale",
3091
- startX: point.x,
3092
- startMin: state2.xMin,
3093
- startMax: state2.xMax
3094
- };
3095
- event.currentTarget.setPointerCapture?.(event.pointerId);
3096
- return;
3097
- }
3098
- if (!inPlot) return;
3099
- const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
3100
- const yRange = getYRange(
3101
- chart,
3102
- state.xMin,
3103
- state.xMax,
3104
- layout.plot.width
3105
- );
3106
- const scaledYRange = applyYScale(
3107
- yRange,
3108
- yScaleRef.current.get(chart.id) ?? 1,
3109
- yCenterOffsetRef.current.get(chart.id) ?? 0
3110
- );
3111
- dragRef.current = {
3112
- type: "x-pan",
3113
- startX: point.x,
3114
- startY: point.y,
3115
- startMin: state.xMin,
3116
- startMax: state.xMax,
3117
- startYOffset: yCenterOffsetRef.current.get(chart.id) ?? 0,
3118
- canPanY: yManualScaleRef.current.has(chart.id),
3119
- yValueSpan: scaledYRange.maxY - scaledYRange.minY,
3120
- width: layout.plot.width,
3121
- height: layout.plot.height
3122
- };
3123
- event.currentTarget.setPointerCapture?.(event.pointerId);
3124
- },
3125
- [
3126
- chart,
3127
- getLayout,
3128
- getLocalPoint,
3129
- initialVisiblePoints,
3130
- chartActiveDrawingTool,
3131
- chartDrawings,
3132
- commitDrawing,
3133
- onSelectedDrawingIdChange,
3134
- rectangleZoomActive,
3135
- viewStateRef,
3136
- yCenterOffsetRef,
3137
- yManualScaleRef,
3138
- yScaleRef
3139
- ]
3140
- );
3141
- const handlePointerMove = (0, import_react9.useCallback)(
3142
- (event) => {
3143
- if (showTooltips) updateCrosshair(event);
3144
- const drawingPoint = getLocalPoint(event);
3145
- const drawingLayout = getLayout();
3146
- if (drawingPoint && drawingLayout) {
3147
- if (drawingEditRef.current) {
3148
- const dataPoint = screenPointToDataPoint({
3149
- point: clampPointToPlot(drawingPoint, drawingLayout.plot),
3150
- chart,
3151
- plot: drawingLayout.plot,
3152
- initialVisiblePoints,
3153
- viewStateRef,
3154
- yScaleRef,
3155
- yCenterOffsetRef
3156
- });
3157
- const edit = drawingEditRef.current;
3158
- const nextDrawings = updateDrawingById(chartDrawings, edit.id, (drawing) => {
3159
- if (drawing.type === "hline") {
3160
- const deltaX = (drawing.end?.x ?? drawing.start.x) - drawing.start.x;
3161
- return {
3162
- ...drawing,
3163
- start: { ...drawing.start, x: dataPoint.x, y: dataPoint.y },
3164
- end: {
3165
- ...drawing.end,
3166
- x: dataPoint.x + deltaX,
3167
- y: dataPoint.y
3168
- }
3169
- };
3170
- }
3171
- if (drawing.type === "vline") {
3172
- return { ...drawing, start: { ...drawing.start, x: dataPoint.x }, end: { ...drawing.end, x: dataPoint.x } };
3173
- }
3174
- if (drawing.type === "pin") {
3175
- return { ...drawing, start: dataPoint, end: dataPoint };
3176
- }
3177
- if (edit.endpoint === "start" || edit.endpoint === "end") {
3178
- return { ...drawing, [edit.endpoint]: dataPoint };
3179
- }
3180
- return drawing;
3181
- });
3182
- onDrawingsChange?.(nextDrawings);
3183
- return;
3184
- }
3185
- if (chartActiveDrawingTool && DRAWING_TOOLS.has(chartActiveDrawingTool)) {
3186
- if (drawingPoint.x < drawingLayout.plot.x || drawingPoint.x > drawingLayout.plot.x + drawingLayout.plot.width || drawingPoint.y < drawingLayout.plot.y || drawingPoint.y > drawingLayout.plot.y + drawingLayout.plot.height) {
3187
- return;
3188
- }
3189
- const dataPoint = screenPointToDataPoint({
3190
- point: drawingPoint,
3191
- chart,
3192
- plot: drawingLayout.plot,
3193
- initialVisiblePoints,
3194
- viewStateRef,
3195
- yScaleRef,
3196
- yCenterOffsetRef
3197
- });
3198
- const startPoint = chartActiveDrawingTool === "trendline" ? drawingSessionRef.current.startPoint : dataPoint;
3199
- if (startPoint || chartActiveDrawingTool !== "trendline") {
3200
- updateDraftDrawing({
3201
- chartId: chart.id,
3202
- type: chartActiveDrawingTool,
3203
- start: startPoint || dataPoint,
3204
- end: dataPoint
3205
- });
3206
- }
3207
- return;
3208
- }
3209
- }
3210
- if (rectangleZoomDragRef.current) {
3211
- const point2 = getLocalPoint(event);
3212
- const layout = getLayout();
3213
- if (!point2 || !layout) return;
3214
- const end = clampPointToPlot(point2, layout.plot);
3215
- rectangleZoomDragRef.current = {
3216
- ...rectangleZoomDragRef.current,
3217
- end
3218
- };
3219
- setRectangleZoomRect({
3220
- start: rectangleZoomDragRef.current.start,
3221
- end
3222
- });
3223
- return;
3224
- }
3225
- const drag = dragRef.current;
3226
- if (!drag) return;
3227
- const point = getLocalPoint(event);
3228
- if (!point) return;
3229
- if (drag.type === "y-scale") {
3230
- yScaleRef.current.set(
3231
- chart.id,
3232
- Math.min(
3233
- Y_SCALE_MAX,
3234
- Math.max(
3235
- Y_SCALE_MIN,
3236
- drag.startScale * Math.exp((point.y - drag.startY) * 0.01)
3237
- )
3238
- )
3239
- );
3240
- yManualScaleRef.current.add(chart.id);
3241
- requestRender();
3242
- return;
3243
- }
3244
- if (drag.type === "x-scale") {
3245
- const startSpan = drag.startMax - drag.startMin;
3246
- const nextSpan = Math.min(
3247
- X_SCALE_MAX_SPAN,
3248
- Math.max(
3249
- X_SCALE_MIN_SPAN,
3250
- startSpan * Math.exp((drag.startX - point.x) * 0.01)
3251
- )
3252
- );
3253
- viewStateRef.current.set(chart.id, {
3254
- xMin: drag.startMax - nextSpan,
3255
- xMax: drag.startMax
3256
- });
3257
- requestRender();
3258
- return;
3259
- }
3260
- const range = drag.startMax - drag.startMin;
3261
- const delta = (point.x - drag.startX) / drag.width * range;
3262
- viewStateRef.current.set(chart.id, {
3263
- xMin: drag.startMin - delta,
3264
- xMax: drag.startMax - delta
3265
- });
3266
- if (drag.canPanY) {
3267
- const yDelta = (point.y - drag.startY) / drag.height * drag.yValueSpan;
3268
- yCenterOffsetRef.current.set(chart.id, drag.startYOffset + yDelta);
3269
- }
3270
- requestRender();
3271
- },
3272
- [
3273
- chart.id,
3274
- chart,
3275
- chartActiveDrawingTool,
3276
- chartDrawings,
3277
- getLayout,
3278
- getLocalPoint,
3279
- initialVisiblePoints,
3280
- onDrawingsChange,
3281
- requestRender,
3282
- showTooltips,
3283
- updateCrosshair,
3284
- viewStateRef,
3285
- yCenterOffsetRef,
3286
- yManualScaleRef,
3287
- yScaleRef
3288
- ]
3289
- );
3290
- const handlePointerUp = (0, import_react9.useCallback)(
3291
- (event) => {
3292
- if (drawingEditRef.current) {
3293
- finishEditDrawing();
3294
- event.currentTarget.releasePointerCapture?.(event.pointerId);
3295
- return;
3296
- }
3297
- if (rectangleZoomDragRef.current) {
3298
- const layout = getLayout();
3299
- const { start, end } = rectangleZoomDragRef.current;
3300
- rectangleZoomDragRef.current = null;
3301
- setRectangleZoomRect(null);
3302
- event.currentTarget.releasePointerCapture?.(event.pointerId);
3303
- if (layout) {
3304
- const applied = applyRectangleZoom({
3305
- chart,
3306
- plot: layout.plot,
3307
- start,
3308
- end,
3309
- initialVisiblePoints,
3310
- viewStateRef,
3311
- yScaleRef,
3312
- yCenterOffsetRef,
3313
- yManualScaleRef
3314
- });
3315
- if (applied) {
3316
- setRectangleZoomActive(false);
3317
- requestRender();
3318
- }
3319
- }
3320
- return;
3321
- }
3322
- dragRef.current = null;
3323
- event.currentTarget.releasePointerCapture?.(event.pointerId);
3324
- },
3325
- [
3326
- chart,
3327
- getLayout,
3328
- initialVisiblePoints,
3329
- requestRender,
3330
- viewStateRef,
3331
- yCenterOffsetRef,
3332
- yManualScaleRef,
3333
- yScaleRef
3334
- ]
3335
- );
3336
- const handleWheel = (0, import_react9.useCallback)(
3337
- (event) => {
3338
- const point = getLocalPoint(event);
3339
- const layout = getLayout();
3340
- if (!point || !layout) return;
3341
- if (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) {
3342
- return;
3343
- }
3344
- event.preventDefault();
3345
- if (event.shiftKey) {
3346
- const currentScale = yScaleRef.current.get(chart.id) ?? 1;
3347
- yScaleRef.current.set(
3348
- chart.id,
3349
- Math.min(
3350
- Y_SCALE_MAX,
3351
- Math.max(
3352
- Y_SCALE_MIN,
3353
- currentScale * Math.exp(event.deltaY * 0.01)
3354
- )
3355
- )
3356
- );
3357
- yManualScaleRef.current.add(chart.id);
3358
- requestRender();
3359
- return;
3360
- }
3361
- const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
3362
- const ratio = (point.x - layout.plot.x) / layout.plot.width;
3363
- const anchor = state.xMin + ratio * (state.xMax - state.xMin);
3364
- const zoom = Math.exp(event.deltaY * 15e-4);
3365
- const nextMin = anchor - (anchor - state.xMin) * zoom;
3366
- const nextMax = anchor + (state.xMax - anchor) * zoom;
3367
- if (nextMax - nextMin < X_SCALE_MIN_SPAN) return;
3368
- viewStateRef.current.set(chart.id, {
3369
- xMin: nextMin,
3370
- xMax: nextMax
3371
- });
3372
- requestRender();
3373
- },
3374
- [
3375
- chart,
3376
- getLayout,
3377
- getLocalPoint,
3378
- initialVisiblePoints,
3379
- requestRender,
3380
- viewStateRef,
3381
- yScaleRef,
3382
- yManualScaleRef
3383
- ]
3384
- );
3385
- (0, import_react9.useEffect)(() => {
3386
- const container = containerRef.current;
3387
- if (!container) return void 0;
3388
- container.addEventListener("wheel", handleWheel, { passive: false });
3389
- return () => container.removeEventListener("wheel", handleWheel);
3390
- }, [handleWheel]);
3391
- const handleContextMenu = (0, import_react9.useCallback)(
3392
- (event) => {
3393
- const point = getLocalPoint(event);
3394
- const layout = getLayout();
3395
- if (!point || !layout) return;
3396
- event.preventDefault();
3397
- onChartContextMenu?.({
3398
- chart,
3399
- event,
3400
- point: getChartContextMenuPoint({
3401
- point,
3402
- chart,
3403
- layout,
3404
- initialVisiblePoints,
3405
- viewStateRef,
3406
- yScaleRef,
3407
- yCenterOffsetRef
3408
- })
3409
- });
3410
- },
3411
- [
3412
- chart,
3413
- getLayout,
3414
- getLocalPoint,
3415
- initialVisiblePoints,
3416
- onChartContextMenu,
3417
- viewStateRef,
3418
- yCenterOffsetRef,
3419
- yScaleRef
3420
- ]
3421
- );
3422
- (0, import_react9.useEffect)(() => {
3423
- const handleKeyDown = (event) => {
3424
- const target = event.target;
3425
- const editable = target instanceof HTMLElement && (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName));
3426
- if (editable) return;
3427
- if (event.key.toLowerCase() === "r") {
3428
- event.preventDefault();
3429
- onReset?.(chart.id);
3430
- setRectangleZoomActive(false);
3431
- setRectangleZoomRect(null);
3432
- requestRender();
3433
- return;
3434
- }
3435
- if (event.key.toLowerCase() === "z") {
3436
- event.preventDefault();
3437
- toggleRectangleZoom();
3438
- return;
3439
- }
3440
- if (!disableDrawings && event.key.toLowerCase() === "m") {
3441
- event.preventDefault();
3442
- onMovingAverageToggle?.(chart.id);
3443
- requestRender();
3444
- return;
3445
- }
3446
- if (!disableDrawings && ["t", "h", "v", "p"].includes(event.key.toLowerCase())) {
3447
- event.preventDefault();
3448
- const toolByKey = { t: "trendline", h: "hline", v: "vline", p: "pin" };
3449
- toggleDrawingTool(toolByKey[event.key.toLowerCase()]);
3450
- return;
3451
- }
3452
- if (event.key === "Delete" || event.key === "Backspace") {
3453
- if (disableDrawings || !chartSelectedDrawingId) return;
3454
- event.preventDefault();
3455
- deleteSelectedDrawing();
3456
- return;
3457
- }
3458
- if (event.key === "Escape" || event.key.toLowerCase() === "f") {
3459
- event.preventDefault();
3460
- if (chartActiveDrawingTool || draftDrawing || drawingEditRef.current) {
3461
- cancelDrawing();
3462
- return;
3463
- }
3464
- if (rectangleZoomActive) {
3465
- setRectangleZoomActive(false);
3466
- setRectangleZoomRect(null);
3467
- rectangleZoomDragRef.current = null;
3468
- return;
3469
- }
3470
- closeWithTransition();
3471
- }
3472
- };
3473
- window.addEventListener("keydown", handleKeyDown);
3474
- return () => window.removeEventListener("keydown", handleKeyDown);
3475
- }, [
3476
- chart.id,
3477
- closeWithTransition,
3478
- chartActiveDrawingTool,
3479
- chartSelectedDrawingId,
3480
- cancelDrawing,
3481
- deleteSelectedDrawing,
3482
- disableDrawings,
3483
- draftDrawing,
3484
- onReset,
3485
- onMovingAverageToggle,
3486
- rectangleZoomActive,
3487
- requestRender,
3488
- toggleRectangleZoom,
3489
- toggleDrawingTool
3490
- ]);
3491
- const fullscreenLayout = getLayout();
3492
- const selectedDrawingLayout = getSelectedDrawingLayout(
3493
- fullscreenLayout ? [fullscreenLayout] : [],
3494
- chartDrawings,
3495
- chartSelectedDrawingId
3496
- );
3497
- return /* @__PURE__ */ import_react9.default.createElement(
3498
- "div",
3499
- {
3500
- className: `fixed inset-0 z-50 bg-background/95 backdrop-blur transition-opacity duration-150 ${visible ? "opacity-100" : "opacity-0"}`
3501
- },
3502
- /* @__PURE__ */ import_react9.default.createElement(
3503
- "div",
3504
- {
3505
- ref: containerRef,
3506
- className: `relative h-full overflow-hidden transition-transform duration-150 ${visible ? "scale-100" : "scale-[0.985]"}`,
3507
- style: {
3508
- cursor: rectangleZoomActive || chartActiveDrawingTool ? "crosshair" : void 0
3509
- },
3510
- onPointerDown: handlePointerDown,
3511
- onPointerMove: handlePointerMove,
3512
- onPointerLeave: () => setCrosshair(null),
3513
- onPointerUp: handlePointerUp,
3514
- onPointerCancel: handlePointerUp,
3515
- onContextMenu: handleContextMenu
3516
- },
3517
- /* @__PURE__ */ import_react9.default.createElement(
3518
- "canvas",
3519
- {
3520
- ref: canvasRef,
3521
- className: "pointer-events-none absolute left-0 top-0 z-10 block",
3522
- style: { width: "100%", height: "100%" }
3523
- }
3524
- ),
3525
- /* @__PURE__ */ import_react9.default.createElement(
3526
- ChartCell,
3527
- {
3528
- chart,
3529
- axisOverlay,
3530
- focused: true,
3531
- height: "100%",
3532
- isFullscreen: true,
3533
- backgroundColor,
3534
- onFocus: () => {
3535
- },
3536
- onFullscreenToggle: closeWithTransition,
3537
- onReset,
3538
- onJumpLatest,
3539
- onRectangleZoomToggle: toggleRectangleZoom,
3540
- rectangleZoomActive,
3541
- movingAverage: disableDrawings ? null : movingAverageByChart?.[chart.id],
3542
- onMovingAverageToggle,
3543
- activeDrawingTool: chartActiveDrawingTool,
3544
- onDrawingToolToggle: toggleDrawingTool,
3545
- hasDrawings: getDrawingsForChart(chartDrawings, chart.id).length > 0,
3546
- onClearDrawingsRequest,
3547
- disableDrawings,
3548
- gridLines,
3549
- showToolbar,
3550
- showLatestValueLine,
3551
- formatXTick,
3552
- formatYValue,
3553
- setRef: (node) => {
3554
- chartRef.current = node;
3555
- }
3556
- }
3557
- ),
3558
- /* @__PURE__ */ import_react9.default.createElement(
3559
- DrawingOverlay,
3560
- {
3561
- layouts: fullscreenLayout ? [fullscreenLayout] : [],
3562
- drawings: chartDrawings,
3563
- draftDrawing: disableDrawings ? null : draftDrawing,
3564
- selectedDrawingId: chartSelectedDrawingId,
3565
- projectPoint: (dataPoint, targetLayout) => projectDataPointToScreenPoint({
3566
- point: dataPoint,
3567
- layout: targetLayout,
3568
- initialVisiblePoints,
3569
- viewStateRef,
3570
- yScaleRef,
3571
- yCenterOffsetRef
3572
- }),
3573
- height: "100%"
3574
- }
3575
- ),
3576
- /* @__PURE__ */ import_react9.default.createElement(
3577
- TopMarkersOverlay,
3578
- {
3579
- layouts: fullscreenLayout ? [fullscreenLayout] : [],
3580
- markers: topMarkers,
3581
- viewStateRef,
3582
- initialVisiblePoints,
3583
- onMarkerClick: (marker) => {
3584
- closeWithTransition();
3585
- requestAnimationFrame(() => {
3586
- onTopMarkerClick?.(marker);
3587
- });
3588
- },
3589
- height: "100%"
3590
- }
3591
- ),
3592
- selectedDrawingLayout ? /* @__PURE__ */ import_react9.default.createElement(
3593
- DrawingOptionsToolbar,
3594
- {
3595
- drawing: selectedDrawingLayout.drawing,
3596
- style: getDrawingOptionsToolbarStyle(selectedDrawingLayout.layout),
3597
- onToggleExtend: toggleSelectedDrawingExtend,
3598
- onColorChange: updateSelectedDrawingColor,
3599
- onTextChange: updateSelectedDrawingText
3600
- }
3601
- ) : null,
3602
- !disableDrawings && fullscreenLayout && movingAverageByChart?.[chart.id]?.enabled ? /* @__PURE__ */ import_react9.default.createElement(
3603
- MovingAverageOptionsToolbar,
3604
- {
3605
- movingAverage: movingAverageByChart[chart.id],
3606
- style: getMovingAverageOptionsToolbarStyle(fullscreenLayout),
3607
- onChange: (nextMovingAverage) => onMovingAverageChange?.(chart.id, nextMovingAverage)
3608
- }
3609
- ) : null,
3610
- /* @__PURE__ */ import_react9.default.createElement(
3611
- CrosshairOverlay,
3612
- {
3613
- crosshair: showTooltips ? crosshair : null,
3614
- height: "100%",
3615
- xAxisLabel,
3616
- formatXValue,
3617
- formatYValue
3618
- }
3619
- ),
3620
- /* @__PURE__ */ import_react9.default.createElement(RectangleZoomOverlay, { rect: rectangleZoomRect })
3621
- )
3622
- );
3623
- }
3624
- function ChartGrid({
3625
- charts,
3626
- columns = 2,
3627
- className = "",
3628
- dataRevision = 0,
3629
- initialVisiblePoints = null,
3630
- backgroundColor = DEFAULT_CHART_BACKGROUND,
3631
- antialiasLines = false,
3632
- gridLines = false,
3633
- showToolbar = true,
3634
- showLatestValueLine = true,
3635
- showTooltips = true,
3636
- followLatest = false,
3637
- followVisibleLatest = true,
3638
- jumpToLatestRevision = 0,
3639
- drawings = EMPTY_ARRAY,
3640
- onDrawingsChange,
3641
- activeDrawingTool = null,
3642
- onActiveDrawingToolChange,
3643
- selectedDrawingId = null,
3644
- onSelectedDrawingIdChange,
3645
- createDrawingId,
3646
- onClearDrawingsRequest,
3647
- onChartContextMenu,
3648
- movingAverageByChart = EMPTY_OBJECT,
3649
- onMovingAverageToggle,
3650
- onMovingAverageChange,
3651
- seriesOrderByChart = EMPTY_OBJECT,
3652
- topMarkers = EMPTY_ARRAY,
3653
- onTopMarkerClick,
3654
- xAxisLabel = "STEP",
3655
- disableDrawings = false,
3656
- formatXTick = formatCompactNumber,
3657
- formatXValue = formatNumber,
3658
- formatYValue = formatNumber
3659
- }) {
3660
- const containerRef = (0, import_react9.useRef)(null);
3661
- const gridRef = (0, import_react9.useRef)(null);
3662
- const canvasRef = (0, import_react9.useRef)(null);
3663
- const glRef = (0, import_react9.useRef)(null);
3664
- const programRef = (0, import_react9.useRef)(null);
3665
- const antialiasProgramRef = (0, import_react9.useRef)(null);
3666
- const seriesBufferCacheRef = (0, import_react9.useRef)(createSeriesBufferCache());
3667
- const antialiasSeriesBufferCacheRef = (0, import_react9.useRef)(createSeriesBufferCache());
3668
- const movingAverageCacheRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
3669
- const chartRefs = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
3670
- const viewStateRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
3671
- const yScaleRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
3672
- const yCenterOffsetRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
3673
- const yManualScaleRef = (0, import_react9.useRef)(/* @__PURE__ */ new Set());
3674
- const latestXRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
3675
- const latestJumpRevisionRef = (0, import_react9.useRef)(jumpToLatestRevision);
3676
- const dragRef = (0, import_react9.useRef)(null);
3677
- const rectangleZoomDragRef = (0, import_react9.useRef)(null);
3678
- const scrollRafRef = (0, import_react9.useRef)(null);
3679
- const [revision, setRevision] = (0, import_react9.useState)(0);
3680
- const [crosshair, setCrosshair] = (0, import_react9.useState)(null);
3681
- const [axisOverlays, setAxisOverlays] = (0, import_react9.useState)({});
3682
- const [focusedChartId, setFocusedChartId] = (0, import_react9.useState)(null);
3683
- const [fullscreenChartId, setFullscreenChartId] = (0, import_react9.useState)(null);
3684
- const [rectangleZoomChartId, setRectangleZoomChartId] = (0, import_react9.useState)(null);
3685
- const [rectangleZoomRect, setRectangleZoomRect] = (0, import_react9.useState)(null);
3686
- const chartDrawings = disableDrawings ? EMPTY_ARRAY : drawings;
3687
- const chartActiveDrawingTool = disableDrawings ? null : activeDrawingTool;
3688
- const chartSelectedDrawingId = disableDrawings ? null : selectedDrawingId;
3689
- (0, import_react9.useEffect)(() => {
3690
- if (!showTooltips) setCrosshair(null);
3691
- }, [showTooltips]);
3692
- const requestRender = (0, import_react9.useCallback)(() => {
3693
- setRevision((value) => value + 1);
3694
- }, []);
3695
- const {
3696
- draftDrawing,
3697
- drawingSessionRef,
3698
- drawingEditRef,
3699
- clearDraft,
3700
- cancelDrawing,
3701
- toggleDrawingTool,
3702
- commitDrawing,
3703
- startDraftDrawing,
3704
- updateDraftDrawing,
3705
- startEditDrawing,
3706
- finishEditDrawing,
3707
- deleteSelectedDrawing
3708
- } = useDrawingInteractions({
3709
- drawings: chartDrawings,
3710
- onDrawingsChange: disableDrawings ? void 0 : onDrawingsChange,
3711
- activeDrawingTool: chartActiveDrawingTool,
3712
- onActiveDrawingToolChange,
3713
- selectedDrawingId: chartSelectedDrawingId,
3714
- onSelectedDrawingIdChange,
3715
- createDrawingId,
3716
- focusedChartId,
3717
- requireFocusedChart: true,
3718
- onModeChange: () => {
3719
- setRectangleZoomChartId(null);
3720
- setRectangleZoomRect(null);
3721
- rectangleZoomDragRef.current = null;
3722
- }
3723
- });
3724
- const closeFullscreen = (0, import_react9.useCallback)(() => {
3725
- setFullscreenChartId(null);
3726
- setCrosshair(null);
3727
- requestRender();
3728
- }, [requestRender]);
3729
- const openFullscreen = (0, import_react9.useCallback)(
3730
- (chartId) => {
3731
- setFocusedChartId(chartId);
3732
- setFullscreenChartId(chartId);
3733
- setCrosshair(null);
3734
- setRectangleZoomChartId(null);
3735
- setRectangleZoomRect(null);
3736
- rectangleZoomDragRef.current = null;
3737
- clearDraft();
3738
- requestRender();
3739
- },
3740
- [clearDraft, requestRender]
3741
- );
3742
- const toggleRectangleZoom = (0, import_react9.useCallback)((chartId) => {
3743
- setFocusedChartId(chartId);
3744
- setRectangleZoomRect(null);
3745
- rectangleZoomDragRef.current = null;
3746
- clearDraft();
3747
- onActiveDrawingToolChange?.(null);
3748
- setRectangleZoomChartId((current) => current === chartId ? null : chartId);
3749
- }, [clearDraft, onActiveDrawingToolChange]);
3750
- const toggleMovingAverage = (0, import_react9.useCallback)(
3751
- (chartId) => {
3752
- onMovingAverageToggle?.(chartId);
3753
- setFocusedChartId(chartId);
3754
- setRectangleZoomChartId(null);
3755
- clearDraft();
3756
- onActiveDrawingToolChange?.(null);
3757
- requestRender();
3758
- },
3759
- [
3760
- clearDraft,
3761
- onActiveDrawingToolChange,
3762
- onMovingAverageToggle,
3763
- requestRender
3764
- ]
3765
- );
3766
- const toggleSelectedDrawingExtend = (0, import_react9.useCallback)(() => {
3767
- if (disableDrawings || !chartSelectedDrawingId || !onDrawingsChange) return;
3768
- onDrawingsChange(
3769
- updateDrawingById(chartDrawings, chartSelectedDrawingId, (drawing) => ({
3770
- ...drawing,
3771
- style: {
3772
- ...drawing.style,
3773
- extendRight: drawing.style?.extendRight === false
3774
- }
3775
- }))
3776
- );
3777
- }, [chartDrawings, chartSelectedDrawingId, disableDrawings, onDrawingsChange]);
3778
- const updateSelectedDrawingColor = (0, import_react9.useCallback)(
3779
- (color) => {
3780
- if (disableDrawings || !chartSelectedDrawingId || !onDrawingsChange) return;
3781
- onDrawingsChange(
3782
- updateDrawingById(chartDrawings, chartSelectedDrawingId, (drawing) => ({
3783
- ...drawing,
3784
- style: {
3785
- ...drawing.style,
3786
- color
3787
- }
3788
- }))
3789
- );
3790
- },
3791
- [chartDrawings, chartSelectedDrawingId, disableDrawings, onDrawingsChange]
3792
- );
3793
- const updateSelectedDrawingText = (0, import_react9.useCallback)(
3794
- (text) => {
3795
- if (disableDrawings || !chartSelectedDrawingId || !onDrawingsChange) return;
3796
- onDrawingsChange(
3797
- updateDrawingById(chartDrawings, chartSelectedDrawingId, (drawing) => ({
3798
- ...drawing,
3799
- style: {
3800
- ...drawing.style,
3801
- text
3802
- }
3803
- }))
3804
- );
3805
- },
3806
- [chartDrawings, chartSelectedDrawingId, disableDrawings, onDrawingsChange]
3807
- );
3808
- const { getAppendAnimatedPoint } = useAppendAnimations({
3809
- charts,
3810
- dataRevision,
3811
- requestRender
3812
- });
3813
- (0, import_react9.useEffect)(() => {
3814
- if (!focusedChartId) return;
3815
- if (charts.some((chart) => chart.id === focusedChartId)) return;
3816
- setFocusedChartId(null);
3817
- }, [charts, focusedChartId]);
3818
- (0, import_react9.useEffect)(() => {
3819
- if (!fullscreenChartId) return;
3820
- if (charts.some((chart) => chart.id === fullscreenChartId)) return;
3821
- setFullscreenChartId(null);
3822
- }, [charts, fullscreenChartId]);
3823
- (0, import_react9.useEffect)(() => {
3824
- const gl = glRef.current;
3825
- if (!gl) return;
3826
- const activeSeriesIds = new Set(
3827
- charts.flatMap((chart) => chart.series.map((series) => series.id))
3828
- );
3829
- charts.forEach((chart) => {
3830
- const movingAverage = normalizeMovingAverage(movingAverageByChart?.[chart.id]);
3831
- if (!movingAverage) return;
3832
- chart.series.forEach((series) => {
3833
- activeSeriesIds.add(getMovingAverageCacheKey(series, movingAverage));
3834
- });
3835
- });
3836
- seriesBufferCacheRef.current.forEach((entry, seriesId) => {
3837
- if (activeSeriesIds.has(seriesId)) return;
3838
- if (entry.buffer) gl.deleteBuffer(entry.buffer);
3839
- seriesBufferCacheRef.current.delete(seriesId);
3840
- });
3841
- antialiasSeriesBufferCacheRef.current.forEach((entry, seriesId) => {
3842
- if (activeSeriesIds.has(seriesId)) return;
3843
- if (entry.buffer) gl.deleteBuffer(entry.buffer);
3844
- antialiasSeriesBufferCacheRef.current.delete(seriesId);
3845
- });
3846
- movingAverageCacheRef.current.forEach((entry, seriesId) => {
3847
- if (activeSeriesIds.has(seriesId)) return;
3848
- movingAverageCacheRef.current.delete(seriesId);
3849
- });
3850
- }, [charts, movingAverageByChart]);
3851
- (0, import_react9.useEffect)(() => {
3852
- charts.forEach((chart) => {
3853
- if (!viewStateRef.current.has(chart.id)) {
3854
- viewStateRef.current.set(
3855
- chart.id,
3856
- getInitialView(chart, initialVisiblePoints)
3857
- );
3858
- }
3859
- if (!yScaleRef.current.has(chart.id)) {
3860
- yScaleRef.current.set(chart.id, 1);
3861
- }
3862
- if (!yCenterOffsetRef.current.has(chart.id)) {
3863
- yCenterOffsetRef.current.set(chart.id, 0);
3864
- }
3865
- if (!latestXRef.current.has(chart.id)) {
3866
- latestXRef.current.set(chart.id, getChartXBounds(chart).maxX);
3867
- }
3868
- });
3869
- }, [charts, initialVisiblePoints]);
3870
- (0, import_react9.useEffect)(() => {
3871
- charts.forEach((chart) => {
3872
- const previousMax = latestXRef.current.get(chart.id);
3873
- const { maxX } = getChartXBounds(chart);
3874
- if (!Number.isFinite(maxX)) return;
3875
- if (!Number.isFinite(previousMax)) {
3876
- latestXRef.current.set(chart.id, maxX);
3877
- return;
3878
- }
3879
- if (maxX !== previousMax) {
3880
- const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
3881
- const span = state.xMax - state.xMin;
3882
- const rightEdgeFollowMin = state.xMax - span * FOLLOW_VISIBLE_RIGHT_EDGE_RATIO;
3883
- const previousMaxVisible = previousMax >= state.xMin && previousMax <= state.xMax;
3884
- const previousMaxNearRightEdge = previousMax >= rightEdgeFollowMin;
3885
- if (followLatest || followVisibleLatest && previousMaxVisible && previousMaxNearRightEdge) {
3886
- const delta = maxX - previousMax;
3887
- const nextState = {
3888
- xMin: state.xMin + delta,
3889
- xMax: state.xMax + delta
3890
- };
3891
- viewStateRef.current.set(chart.id, nextState);
3892
- const latestYBounds = getLatestYBounds(chart);
3893
- if (Number.isFinite(latestYBounds.minY) && Number.isFinite(latestYBounds.maxY)) {
3894
- const baseYRange = getYRange(
3895
- chart,
3896
- nextState.xMin,
3897
- nextState.xMax,
3898
- getPlotWidth(chartRefs.current.get(chart.id))
3899
- );
3900
- const scale = yScaleRef.current.get(chart.id) ?? 1;
3901
- const currentOffset = yCenterOffsetRef.current.get(chart.id) ?? 0;
3902
- const scaledYRange = applyYScale(baseYRange, scale, currentOffset);
3903
- const pad = (scaledYRange.maxY - scaledYRange.minY) * 0.08;
3904
- let nextOffset = currentOffset;
3905
- if (latestYBounds.maxY > scaledYRange.maxY - pad) {
3906
- nextOffset += latestYBounds.maxY - (scaledYRange.maxY - pad);
3907
- } else if (latestYBounds.minY < scaledYRange.minY + pad) {
3908
- nextOffset += latestYBounds.minY - (scaledYRange.minY + pad);
3909
- }
3910
- if (nextOffset !== currentOffset) {
3911
- yCenterOffsetRef.current.set(chart.id, nextOffset);
3912
- }
3913
- }
3914
- }
3915
- }
3916
- latestXRef.current.set(chart.id, maxX);
3917
- });
3918
- }, [
3919
- charts,
3920
- dataRevision,
3921
- followLatest,
3922
- followVisibleLatest,
3923
- initialVisiblePoints
3924
- ]);
3925
- (0, import_react9.useEffect)(() => {
3926
- if (latestJumpRevisionRef.current === jumpToLatestRevision) return;
3927
- latestJumpRevisionRef.current = jumpToLatestRevision;
3928
- charts.forEach((chart) => {
3929
- const { maxX } = getChartXBounds(chart);
3930
- if (!Number.isFinite(maxX)) return;
3931
- const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
3932
- const span = Math.max(10, state.xMax - state.xMin);
3933
- const rightPadding = span * JUMP_LATEST_RIGHT_PADDING_RATIO;
3934
- const nextMax = maxX + rightPadding;
3935
- viewStateRef.current.set(chart.id, {
3936
- xMin: nextMax - span,
3937
- xMax: nextMax
3938
- });
3939
- });
3940
- requestRender();
3941
- }, [charts, initialVisiblePoints, jumpToLatestRevision, requestRender]);
3942
- const getChartLayout = (0, import_react9.useCallback)(() => {
3943
- const container = containerRef.current;
3944
- const grid = gridRef.current;
3945
- if (!container) return [];
3946
- return charts.map((chart) => {
3947
- const node = chartRefs.current.get(chart.id);
3948
- if (!node) return null;
3949
- const rect = {
3950
- x: node.offsetLeft,
3951
- y: node.offsetTop,
3952
- width: node.offsetWidth,
3953
- height: node.offsetHeight
3954
- };
3955
- let parent = node.offsetParent;
3956
- while (parent && parent !== container && parent !== grid) {
3957
- rect.x += parent.offsetLeft;
3958
- rect.y += parent.offsetTop;
3959
- parent = parent.offsetParent;
3960
- }
3961
- const plot = {
3962
- x: rect.x + PLOT_PADDING.left,
3963
- y: rect.y + PLOT_PADDING.top,
3964
- width: Math.max(
3965
- 1,
3966
- rect.width - PLOT_PADDING.left - PLOT_PADDING.right
3967
- ),
3968
- height: Math.max(
3969
- 1,
3970
- rect.height - PLOT_PADDING.top - PLOT_PADDING.bottom
3971
- )
3972
- };
3973
- const visibleTop = container.scrollTop;
3974
- const visibleBottom = visibleTop + container.clientHeight;
3975
- const visible = rect.y + rect.height >= visibleTop && rect.y <= visibleBottom;
3976
- return { chart, rect, plot, visible };
3977
- }).filter(Boolean);
3978
- }, [charts]);
3979
- (0, import_react9.useEffect)(() => {
3980
- const canvas = canvasRef.current;
3981
- if (!canvas) return;
3982
- const gl = canvas.getContext("webgl2", {
3983
- antialias: true,
3984
- alpha: true,
3985
- depth: false,
3986
- stencil: false,
3987
- preserveDrawingBuffer: false
3988
- });
3989
- if (!gl) return;
3990
- glRef.current = gl;
3991
- programRef.current = createProgram(gl);
3992
- antialiasProgramRef.current = createAntialiasProgram(gl);
3993
- requestRender();
3994
- return () => {
3995
- deleteSeriesBuffers(gl, seriesBufferCacheRef.current);
3996
- deleteSeriesBuffers(gl, antialiasSeriesBufferCacheRef.current);
3997
- glRef.current = null;
3998
- programRef.current = null;
3999
- antialiasProgramRef.current = null;
4000
- };
4001
- }, [requestRender]);
4002
- (0, import_react9.useEffect)(() => {
4003
- const container = containerRef.current;
4004
- if (!container) return;
4005
- const observer = new ResizeObserver(requestRender);
4006
- observer.observe(container);
4007
- return () => observer.disconnect();
4008
- }, [requestRender]);
4009
- (0, import_react9.useEffect)(() => {
4010
- const container = containerRef.current;
4011
- if (!container) return void 0;
4012
- const handleScroll = () => {
4013
- setCrosshair(null);
4014
- if (scrollRafRef.current != null) return;
4015
- scrollRafRef.current = requestAnimationFrame(() => {
4016
- scrollRafRef.current = null;
4017
- requestRender();
4018
- });
4019
- };
4020
- container.addEventListener("scroll", handleScroll, { passive: true });
4021
- return () => {
4022
- container.removeEventListener("scroll", handleScroll);
4023
- if (scrollRafRef.current != null) {
4024
- cancelAnimationFrame(scrollRafRef.current);
4025
- scrollRafRef.current = null;
4026
- }
4027
- };
4028
- }, [requestRender]);
4029
- (0, import_react9.useEffect)(() => {
4030
- const container = containerRef.current;
4031
- const canvas = canvasRef.current;
4032
- const gl = glRef.current;
4033
- const program = programRef.current;
4034
- const antialiasProgram = antialiasProgramRef.current;
4035
- const seriesBufferCache = seriesBufferCacheRef.current;
4036
- const antialiasSeriesBufferCache = antialiasSeriesBufferCacheRef.current;
4037
- if (!container || !canvas || !gl || !program) return;
4038
- if (fullscreenChartId) {
4039
- canvas.style.transform = "";
4040
- gl.viewport(0, 0, canvas.width || 1, canvas.height || 1);
4041
- gl.clearColor(0, 0, 0, 0);
4042
- gl.clear(gl.COLOR_BUFFER_BIT);
4043
- setAxisOverlays({});
4044
- return;
4045
- }
4046
- const scrollLeft = container.scrollLeft;
4047
- const scrollTop = container.scrollTop;
4048
- canvas.style.transform = `translate(${scrollLeft}px, ${scrollTop}px)`;
4049
- const nextAxisOverlays = drawChartLayouts({
4050
- canvas,
4051
- width: container.clientWidth,
4052
- height: container.clientHeight,
4053
- gl,
4054
- program,
4055
- antialiasProgram,
4056
- antialiasLines,
4057
- layouts: getChartLayout().map(
4058
- (layout) => toViewportLayout(layout, scrollLeft, scrollTop)
4059
- ),
4060
- viewStateRef,
4061
- yScaleRef,
4062
- yCenterOffsetRef,
4063
- seriesBufferCache,
4064
- antialiasSeriesBufferCache,
4065
- initialVisiblePoints,
4066
- getAppendAnimatedPoint,
4067
- movingAverageByChart,
4068
- movingAverageCache: movingAverageCacheRef.current,
4069
- seriesOrderByChart
4070
- });
4071
- setAxisOverlays(nextAxisOverlays);
4072
- }, [
4073
- charts,
4074
- antialiasLines,
4075
- dataRevision,
4076
- fullscreenChartId,
4077
- getAppendAnimatedPoint,
4078
- getChartLayout,
4079
- initialVisiblePoints,
4080
- movingAverageByChart,
4081
- seriesOrderByChart,
4082
- revision
4083
- ]);
4084
- const findLayoutAt = (0, import_react9.useCallback)(
4085
- (clientX, clientY) => getChartLayout().find(
4086
- ({ plot, visible }) => visible && clientX >= plot.x && clientX <= plot.x + plot.width && clientY >= plot.y && clientY <= plot.y + plot.height
4087
- ),
4088
- [getChartLayout]
4089
- );
4090
- const findYAxisLayoutAt = (0, import_react9.useCallback)(
4091
- (clientX, clientY) => getChartLayout().find(
4092
- ({ rect, plot, visible }) => visible && clientX >= plot.x + plot.width && clientX <= rect.x + rect.width && clientY >= plot.y && clientY <= plot.y + plot.height
4093
- ),
4094
- [getChartLayout]
4095
- );
4096
- const findXAxisLayoutAt = (0, import_react9.useCallback)(
4097
- (clientX, clientY) => getChartLayout().find(
4098
- ({ rect, plot, visible }) => visible && clientX >= plot.x && clientX <= plot.x + plot.width && clientY >= plot.y + plot.height && clientY <= rect.y + rect.height
4099
- ),
4100
- [getChartLayout]
4101
- );
4102
- const getLocalPoint = (0, import_react9.useCallback)((event) => {
4103
- const container = containerRef.current;
4104
- const rect = container?.getBoundingClientRect();
4105
- if (!rect) return null;
4106
- return {
4107
- x: event.clientX - rect.left,
4108
- y: event.clientY - rect.top + container.scrollTop
4109
- };
4110
- }, []);
4111
- const updateCrosshair = (0, import_react9.useCallback)(
4112
- (event) => {
4113
- const point = getLocalPoint(event);
4114
- if (!point) return;
4115
- const layout = findLayoutAt(point.x, point.y);
4116
- if (!layout) {
4117
- setCrosshair(null);
4118
- return;
4119
- }
4120
- const state = viewStateRef.current.get(layout.chart.id) || getInitialView(layout.chart, initialVisiblePoints);
4121
- const xRatio = (point.x - layout.plot.x) / layout.plot.width;
4122
- const xValue = state.xMin + xRatio * (state.xMax - state.xMin);
4123
- const yRange = getYRange(
4124
- layout.chart,
4125
- state.xMin,
4126
- state.xMax,
4127
- layout.plot.width
4128
- );
4129
- const scaledYRange = applyYScale(
4130
- yRange,
4131
- yScaleRef.current.get(layout.chart.id) ?? 1,
4132
- yCenterOffsetRef.current.get(layout.chart.id) ?? 0
4133
- );
4134
- const yRatio = (point.y - layout.plot.y) / layout.plot.height;
4135
- const yValue = scaledYRange.maxY - yRatio * (scaledYRange.maxY - scaledYRange.minY);
4136
- const nearestPoints = getOrderedSeries(layout.chart, seriesOrderByChart).map((series) => {
4137
- const visiblePoints = series.getVisiblePoints(
4138
- state.xMin,
4139
- state.xMax,
4140
- layout.plot.width
4141
- );
4142
- const index = getNearestPointIndex(visiblePoints.x, xValue);
4143
- if (index < 0) return null;
4144
- const pointX = visiblePoints.x[index];
4145
- const pointY = visiblePoints.y[index];
4146
- const pointXRatio = (pointX - state.xMin) / (state.xMax - state.xMin);
4147
- const pointYRatio = (scaledYRange.maxY - pointY) / (scaledYRange.maxY - scaledYRange.minY);
4148
- return {
4149
- id: series.id,
4150
- name: series.name,
4151
- color: series.color,
4152
- xValue: pointX,
4153
- yValue: pointY,
4154
- x: layout.plot.x + pointXRatio * layout.plot.width,
4155
- y: layout.plot.y + pointYRatio * layout.plot.height
4156
- };
4157
- }).filter(Boolean).filter(
4158
- (nearestPoint) => nearestPoint.x >= layout.plot.x - 1 && nearestPoint.x <= layout.plot.x + layout.plot.width + 1 && nearestPoint.y >= layout.plot.y - 1 && nearestPoint.y <= layout.plot.y + layout.plot.height + 1
4159
- );
4160
- const tooltipLeft = point.x + TOOLTIP_WIDTH + TOOLTIP_OFFSET > layout.rect.x + layout.rect.width ? point.x - TOOLTIP_WIDTH - TOOLTIP_OFFSET : point.x + TOOLTIP_OFFSET;
4161
- const tooltipTop = Math.max(
4162
- layout.rect.y + PLOT_PADDING.top,
4163
- Math.min(
4164
- point.y + TOOLTIP_OFFSET,
4165
- layout.rect.y + layout.rect.height - 96
4166
- )
4167
- );
4168
- const primaryPoint = nearestPoints[0];
4169
- setCrosshair({
4170
- chartId: layout.chart.id,
4171
- title: layout.chart.title,
4172
- x: point.x,
4173
- y: point.y,
4174
- xValue: primaryPoint?.xValue ?? xValue,
4175
- yValue: primaryPoint?.yValue ?? yValue,
4176
- points: nearestPoints,
4177
- tooltipX: Math.max(layout.rect.x + 6, tooltipLeft),
4178
- tooltipY: tooltipTop,
4179
- bucketSize: scaledYRange.bucketSize,
4180
- renderedPoints: scaledYRange.renderedPoints
4181
- });
4182
- },
4183
- [findLayoutAt, getLocalPoint, initialVisiblePoints, seriesOrderByChart]
4184
- );
4185
- const jumpChartToLatest = (0, import_react9.useCallback)(
4186
- (chartId) => {
4187
- const chart = charts.find((candidate) => candidate.id === chartId);
4188
- if (!chart) return;
4189
- const { maxX } = getChartXBounds(chart);
4190
- if (!Number.isFinite(maxX)) return;
4191
- const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
4192
- const span = Math.max(X_SCALE_MIN_SPAN, state.xMax - state.xMin);
4193
- const rightPadding = span * JUMP_LATEST_RIGHT_PADDING_RATIO;
4194
- const nextMax = maxX + rightPadding;
4195
- viewStateRef.current.set(chart.id, {
4196
- xMin: nextMax - span,
4197
- xMax: nextMax
4198
- });
4199
- yCenterOffsetRef.current.set(chart.id, 0);
4200
- requestRender();
4201
- },
4202
- [charts, initialVisiblePoints, requestRender]
4203
- );
4204
- const resetChartView = (0, import_react9.useCallback)(
4205
- (chartId) => {
4206
- const chart = charts.find((candidate) => candidate.id === chartId);
4207
- if (!chart) return;
4208
- viewStateRef.current.set(chart.id, getInitialView(chart, initialVisiblePoints));
4209
- yScaleRef.current.set(chart.id, 1);
4210
- yCenterOffsetRef.current.set(chart.id, 0);
4211
- yManualScaleRef.current.delete(chart.id);
4212
- if (rectangleZoomChartId === chart.id) {
4213
- setRectangleZoomChartId(null);
4214
- setRectangleZoomRect(null);
4215
- rectangleZoomDragRef.current = null;
4216
- }
4217
- requestRender();
4218
- },
4219
- [charts, initialVisiblePoints, rectangleZoomChartId, requestRender]
4220
- );
4221
- const handleContextMenu = (0, import_react9.useCallback)(
4222
- (event) => {
4223
- if (fullscreenChartId) return;
4224
- const point = getLocalPoint(event);
4225
- if (!point) return;
4226
- const layout = findLayoutAt(point.x, point.y);
4227
- if (!layout) return;
4228
- event.preventDefault();
4229
- setFocusedChartId(layout.chart.id);
4230
- onChartContextMenu?.({
4231
- chart: layout.chart,
4232
- event,
4233
- point: getChartContextMenuPoint({
4234
- point,
4235
- chart: layout.chart,
4236
- layout,
4237
- initialVisiblePoints,
4238
- viewStateRef,
4239
- yScaleRef,
4240
- yCenterOffsetRef
4241
- })
4242
- });
4243
- },
4244
- [
4245
- findLayoutAt,
4246
- fullscreenChartId,
4247
- getLocalPoint,
4248
- initialVisiblePoints,
4249
- onChartContextMenu
4250
- ]
4251
- );
4252
- const handlePointerDown = (0, import_react9.useCallback)(
4253
- (event) => {
4254
- if (fullscreenChartId) return;
4255
- const point = getLocalPoint(event);
4256
- if (!point) return;
4257
- const drawingLayout = findLayoutAt(point.x, point.y);
4258
- if (drawingLayout && chartActiveDrawingTool && DRAWING_TOOLS.has(chartActiveDrawingTool)) {
4259
- if (event.button !== 0) return;
4260
- event.preventDefault();
4261
- setFocusedChartId(drawingLayout.chart.id);
4262
- const dataPoint = screenPointToDataPoint({
4263
- point,
4264
- chart: drawingLayout.chart,
4265
- plot: drawingLayout.plot,
4266
- initialVisiblePoints,
4267
- viewStateRef,
4268
- yScaleRef,
4269
- yCenterOffsetRef
4270
- });
4271
- if (chartActiveDrawingTool === "hline" || chartActiveDrawingTool === "vline" || chartActiveDrawingTool === "pin") {
4272
- commitDrawing({
4273
- chartId: drawingLayout.chart.id,
4274
- type: chartActiveDrawingTool,
4275
- start: dataPoint,
4276
- end: dataPoint
4277
- });
4278
- return;
4279
- }
4280
- const session = drawingSessionRef.current;
4281
- if (!session.startPoint || session.chartId !== drawingLayout.chart.id) {
4282
- startDraftDrawing({
4283
- chartId: drawingLayout.chart.id,
4284
- type: "trendline",
4285
- start: dataPoint,
4286
- end: dataPoint
4287
- });
4288
- return;
4289
- }
4290
- commitDrawing({
4291
- chartId: drawingLayout.chart.id,
4292
- type: "trendline",
4293
- start: session.startPoint,
4294
- end: dataPoint
4295
- });
4296
- return;
4297
- }
4298
- if (drawingLayout && Array.isArray(chartDrawings) && chartDrawings.length > 0) {
4299
- const hitDrawings = getDrawingsForChart(
4300
- chartDrawings,
4301
- drawingLayout.chart.id
4302
- );
4303
- for (let i = hitDrawings.length - 1; i >= 0; i -= 1) {
4304
- const hit = hitTestDrawing({
4305
- point,
4306
- drawing: hitDrawings[i],
4307
- layout: drawingLayout,
4308
- projectPoint: (dataPoint, targetLayout) => projectDataPointToScreenPoint({
4309
- point: dataPoint,
4310
- layout: targetLayout,
4311
- initialVisiblePoints,
4312
- viewStateRef,
4313
- yScaleRef,
4314
- yCenterOffsetRef
4315
- })
4316
- });
4317
- if (hit) {
4318
- event.preventDefault();
4319
- setFocusedChartId(drawingLayout.chart.id);
4320
- onSelectedDrawingIdChange?.(hit.drawing.id);
4321
- if (hit.endpoint || hit.drawing.type === "hline" || hit.drawing.type === "vline" || hit.drawing.type === "pin") {
4322
- startEditDrawing({
4323
- drawingId: hit.drawing.id,
4324
- endpoint: hit.endpoint || "move"
4325
- });
4326
- event.currentTarget.setPointerCapture?.(event.pointerId);
4327
- }
4328
- return;
4329
- }
4330
- }
4331
- onSelectedDrawingIdChange?.(null);
4332
- }
4333
- if (rectangleZoomChartId) {
4334
- const layout2 = findLayoutAt(point.x, point.y);
4335
- if (!layout2 || layout2.chart.id !== rectangleZoomChartId || event.button !== 0) {
4336
- return;
4337
- }
4338
- event.preventDefault();
4339
- const start = clampPointToPlot(point, layout2.plot);
4340
- rectangleZoomDragRef.current = {
4341
- chartId: layout2.chart.id,
4342
- start,
4343
- end: start
4344
- };
4345
- setRectangleZoomRect({ start, end: start });
4346
- event.currentTarget.setPointerCapture?.(event.pointerId);
4347
- return;
4348
- }
4349
- const yAxisLayout = findYAxisLayoutAt(point.x, point.y);
4350
- if (yAxisLayout) {
4351
- event.preventDefault();
4352
- dragRef.current = {
4353
- type: "y-scale",
4354
- chartId: yAxisLayout.chart.id,
4355
- startY: point.y,
4356
- startScale: yScaleRef.current.get(yAxisLayout.chart.id) ?? 1
4357
- };
4358
- event.currentTarget.setPointerCapture?.(event.pointerId);
4359
- return;
4360
- }
4361
- const xAxisLayout = findXAxisLayoutAt(point.x, point.y);
4362
- if (xAxisLayout) {
4363
- event.preventDefault();
4364
- const state2 = viewStateRef.current.get(xAxisLayout.chart.id) || getInitialView(xAxisLayout.chart, initialVisiblePoints);
4365
- dragRef.current = {
4366
- type: "x-scale",
4367
- chartId: xAxisLayout.chart.id,
4368
- startX: point.x,
4369
- startMin: state2.xMin,
4370
- startMax: state2.xMax
4371
- };
4372
- event.currentTarget.setPointerCapture?.(event.pointerId);
4373
- return;
4374
- }
4375
- const layout = findLayoutAt(point.x, point.y);
4376
- if (!layout) return;
4377
- const state = viewStateRef.current.get(layout.chart.id) || getInitialView(layout.chart, initialVisiblePoints);
4378
- const yRange = getYRange(
4379
- layout.chart,
4380
- state.xMin,
4381
- state.xMax,
4382
- layout.plot.width
4383
- );
4384
- const scaledYRange = applyYScale(
4385
- yRange,
4386
- yScaleRef.current.get(layout.chart.id) ?? 1,
4387
- yCenterOffsetRef.current.get(layout.chart.id) ?? 0
4388
- );
4389
- dragRef.current = {
4390
- type: "x-pan",
4391
- chartId: layout.chart.id,
4392
- startX: point.x,
4393
- startY: point.y,
4394
- startMin: state.xMin,
4395
- startMax: state.xMax,
4396
- startYOffset: yCenterOffsetRef.current.get(layout.chart.id) ?? 0,
4397
- canPanY: yManualScaleRef.current.has(layout.chart.id),
4398
- yValueSpan: scaledYRange.maxY - scaledYRange.minY,
4399
- width: layout.plot.width,
4400
- height: layout.plot.height
4401
- };
4402
- event.currentTarget.setPointerCapture?.(event.pointerId);
4403
- },
4404
- [
4405
- findLayoutAt,
4406
- findXAxisLayoutAt,
4407
- findYAxisLayoutAt,
4408
- getLocalPoint,
4409
- initialVisiblePoints,
4410
- fullscreenChartId,
4411
- rectangleZoomChartId,
4412
- chartActiveDrawingTool,
4413
- chartDrawings,
4414
- commitDrawing,
4415
- onSelectedDrawingIdChange
4416
- ]
4417
- );
4418
- const handlePointerMove = (0, import_react9.useCallback)(
4419
- (event) => {
4420
- if (fullscreenChartId) return;
4421
- if (showTooltips) updateCrosshair(event);
4422
- const drawingPoint = getLocalPoint(event);
4423
- if (drawingPoint) {
4424
- if (drawingEditRef.current) {
4425
- const editLayout = getChartLayout().find(
4426
- (layout) => getDrawingsForChart(chartDrawings, layout.chart.id).some(
4427
- (drawing) => drawing.id === drawingEditRef.current.id
4428
- )
4429
- );
4430
- if (editLayout) {
4431
- const dataPoint = screenPointToDataPoint({
4432
- point: clampPointToPlot(drawingPoint, editLayout.plot),
4433
- chart: editLayout.chart,
4434
- plot: editLayout.plot,
4435
- initialVisiblePoints,
4436
- viewStateRef,
4437
- yScaleRef,
4438
- yCenterOffsetRef
4439
- });
4440
- const edit = drawingEditRef.current;
4441
- const nextDrawings = updateDrawingById(chartDrawings, edit.id, (drawing) => {
4442
- if (drawing.type === "hline") {
4443
- const deltaX = (drawing.end?.x ?? drawing.start.x) - drawing.start.x;
4444
- return {
4445
- ...drawing,
4446
- start: { ...drawing.start, x: dataPoint.x, y: dataPoint.y },
4447
- end: {
4448
- ...drawing.end,
4449
- x: dataPoint.x + deltaX,
4450
- y: dataPoint.y
4451
- }
4452
- };
4453
- }
4454
- if (drawing.type === "vline") {
4455
- return { ...drawing, start: { ...drawing.start, x: dataPoint.x }, end: { ...drawing.end, x: dataPoint.x } };
4456
- }
4457
- if (drawing.type === "pin") {
4458
- return { ...drawing, start: dataPoint, end: dataPoint };
4459
- }
4460
- if (edit.endpoint === "start" || edit.endpoint === "end") {
4461
- return { ...drawing, [edit.endpoint]: dataPoint };
4462
- }
4463
- return drawing;
4464
- });
4465
- onDrawingsChange?.(nextDrawings);
4466
- }
4467
- return;
4468
- }
4469
- if (chartActiveDrawingTool && DRAWING_TOOLS.has(chartActiveDrawingTool)) {
4470
- const drawingLayout = findLayoutAt(drawingPoint.x, drawingPoint.y);
4471
- if (!drawingLayout) return;
4472
- const dataPoint = screenPointToDataPoint({
4473
- point: drawingPoint,
4474
- chart: drawingLayout.chart,
4475
- plot: drawingLayout.plot,
4476
- initialVisiblePoints,
4477
- viewStateRef,
4478
- yScaleRef,
4479
- yCenterOffsetRef
4480
- });
4481
- const session = drawingSessionRef.current;
4482
- const startPoint = chartActiveDrawingTool === "trendline" && session.chartId === drawingLayout.chart.id ? session.startPoint : dataPoint;
4483
- if (startPoint || chartActiveDrawingTool !== "trendline") {
4484
- updateDraftDrawing({
4485
- chartId: drawingLayout.chart.id,
4486
- type: chartActiveDrawingTool,
4487
- start: startPoint || dataPoint,
4488
- end: dataPoint
4489
- });
4490
- }
4491
- return;
4492
- }
4493
- }
4494
- if (rectangleZoomDragRef.current) {
4495
- const point2 = getLocalPoint(event);
4496
- if (!point2) return;
4497
- const layout = getChartLayout().find(
4498
- (candidate) => candidate.chart.id === rectangleZoomDragRef.current.chartId
4499
- );
4500
- if (!layout) return;
4501
- const end = clampPointToPlot(point2, layout.plot);
4502
- rectangleZoomDragRef.current = {
4503
- ...rectangleZoomDragRef.current,
4504
- end
4505
- };
4506
- setRectangleZoomRect({
4507
- start: rectangleZoomDragRef.current.start,
4508
- end
4509
- });
4510
- return;
4511
- }
4512
- const drag = dragRef.current;
4513
- if (!drag) return;
4514
- const point = getLocalPoint(event);
4515
- if (!point) return;
4516
- if (drag.type === "y-scale") {
4517
- const nextScale = Math.min(
4518
- Y_SCALE_MAX,
4519
- Math.max(
4520
- Y_SCALE_MIN,
4521
- drag.startScale * Math.exp((point.y - drag.startY) * 0.01)
4522
- )
4523
- );
4524
- yScaleRef.current.set(drag.chartId, nextScale);
4525
- yManualScaleRef.current.add(drag.chartId);
4526
- requestRender();
4527
- return;
4528
- }
4529
- if (drag.type === "x-scale") {
4530
- const startSpan = drag.startMax - drag.startMin;
4531
- const nextSpan = Math.min(
4532
- X_SCALE_MAX_SPAN,
4533
- Math.max(
4534
- X_SCALE_MIN_SPAN,
4535
- startSpan * Math.exp((drag.startX - point.x) * 0.01)
4536
- )
4537
- );
4538
- viewStateRef.current.set(drag.chartId, {
4539
- xMin: drag.startMax - nextSpan,
4540
- xMax: drag.startMax
4541
- });
4542
- requestRender();
4543
- return;
4544
- }
4545
- const range = drag.startMax - drag.startMin;
4546
- const delta = (point.x - drag.startX) / drag.width * range;
4547
- viewStateRef.current.set(drag.chartId, {
4548
- xMin: drag.startMin - delta,
4549
- xMax: drag.startMax - delta
4550
- });
4551
- if (drag.canPanY) {
4552
- const yDelta = (point.y - drag.startY) / drag.height * drag.yValueSpan;
4553
- yCenterOffsetRef.current.set(drag.chartId, drag.startYOffset + yDelta);
4554
- }
4555
- requestRender();
4556
- },
4557
- [
4558
- fullscreenChartId,
4559
- chartActiveDrawingTool,
4560
- chartDrawings,
4561
- findLayoutAt,
4562
- getChartLayout,
4563
- getLocalPoint,
4564
- initialVisiblePoints,
4565
- onDrawingsChange,
4566
- requestRender,
4567
- showTooltips,
4568
- updateCrosshair
4569
- ]
4570
- );
4571
- const handlePointerUp = (0, import_react9.useCallback)(
4572
- (event) => {
4573
- if (drawingEditRef.current) {
4574
- finishEditDrawing();
4575
- event.currentTarget.releasePointerCapture?.(event.pointerId);
4576
- return;
4577
- }
4578
- if (rectangleZoomDragRef.current) {
4579
- const { chartId, start, end } = rectangleZoomDragRef.current;
4580
- const layout = getChartLayout().find(
4581
- (candidate) => candidate.chart.id === chartId
4582
- );
4583
- rectangleZoomDragRef.current = null;
4584
- setRectangleZoomRect(null);
4585
- event.currentTarget.releasePointerCapture?.(event.pointerId);
4586
- if (layout) {
4587
- const applied = applyRectangleZoom({
4588
- chart: layout.chart,
4589
- plot: layout.plot,
4590
- start,
4591
- end,
4592
- initialVisiblePoints,
4593
- viewStateRef,
4594
- yScaleRef,
4595
- yCenterOffsetRef,
4596
- yManualScaleRef
4597
- });
4598
- if (applied) {
4599
- setRectangleZoomChartId(null);
4600
- requestRender();
4601
- }
4602
- }
4603
- return;
4604
- }
4605
- dragRef.current = null;
4606
- event.currentTarget.releasePointerCapture?.(event.pointerId);
4607
- },
4608
- [
4609
- getChartLayout,
4610
- initialVisiblePoints,
4611
- requestRender,
4612
- viewStateRef,
4613
- yCenterOffsetRef,
4614
- yManualScaleRef,
4615
- yScaleRef
4616
- ]
4617
- );
4618
- const handleWheel = (0, import_react9.useCallback)(
4619
- (event) => {
4620
- if (fullscreenChartId) return;
4621
- if (event.altKey) {
4622
- event.preventDefault();
4623
- const container = containerRef.current;
4624
- if (!container) return;
4625
- const deltaScale = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? 16 : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? container.clientHeight : 1;
4626
- container.scrollBy({
4627
- left: event.deltaX * deltaScale,
4628
- top: event.deltaY * deltaScale,
4629
- behavior: "auto"
4630
- });
4631
- return;
4632
- }
4633
- const point = getLocalPoint(event);
4634
- if (!point) return;
4635
- const layout = findLayoutAt(point.x, point.y);
4636
- if (!layout) return;
4637
- event.preventDefault();
4638
- if (event.shiftKey) {
4639
- const currentScale = yScaleRef.current.get(layout.chart.id) ?? 1;
4640
- yScaleRef.current.set(
4641
- layout.chart.id,
4642
- Math.min(
4643
- Y_SCALE_MAX,
4644
- Math.max(
4645
- Y_SCALE_MIN,
4646
- currentScale * Math.exp(event.deltaY * 0.01)
4647
- )
4648
- )
4649
- );
4650
- yManualScaleRef.current.add(layout.chart.id);
4651
- requestRender();
4652
- return;
4653
- }
4654
- const state = viewStateRef.current.get(layout.chart.id) || getInitialView(layout.chart, initialVisiblePoints);
4655
- const ratio = (point.x - layout.plot.x) / layout.plot.width;
4656
- const anchor = state.xMin + ratio * (state.xMax - state.xMin);
4657
- const zoom = Math.exp(event.deltaY * 15e-4);
4658
- const nextMin = anchor - (anchor - state.xMin) * zoom;
4659
- const nextMax = anchor + (state.xMax - anchor) * zoom;
4660
- if (nextMax - nextMin < 10) return;
4661
- viewStateRef.current.set(layout.chart.id, {
4662
- xMin: nextMin,
4663
- xMax: nextMax
4664
- });
4665
- requestRender();
4666
- },
4667
- [
4668
- findLayoutAt,
4669
- fullscreenChartId,
4670
- getLocalPoint,
4671
- initialVisiblePoints,
4672
- requestRender,
4673
- yScaleRef
4674
- ]
4675
- );
4676
- (0, import_react9.useEffect)(() => {
4677
- const container = containerRef.current;
4678
- if (!container) return void 0;
4679
- container.addEventListener("wheel", handleWheel, { passive: false });
4680
- return () => {
4681
- container.removeEventListener("wheel", handleWheel);
4682
- };
4683
- }, [handleWheel]);
4684
- const focusChartByKeyboard = (0, import_react9.useCallback)(
4685
- (direction) => {
4686
- if (fullscreenChartId || charts.length === 0) return false;
4687
- if (!focusedChartId) {
4688
- setFocusedChartId(charts[0].id);
4689
- chartRefs.current.get(charts[0].id)?.scrollIntoView({
4690
- block: "nearest",
4691
- inline: "nearest"
4692
- });
4693
- return true;
4694
- }
4695
- const currentIndex = Math.max(
4696
- 0,
4697
- charts.findIndex((chart) => chart.id === focusedChartId)
4698
- );
4699
- const safeColumns = Math.max(1, Math.min(charts.length, Number(columns) || 1));
4700
- const rowStart = Math.floor(currentIndex / safeColumns) * safeColumns;
4701
- const rowEnd = Math.min(charts.length - 1, rowStart + safeColumns - 1);
4702
- let nextIndex = currentIndex;
4703
- if (direction === "left") {
4704
- nextIndex = Math.max(rowStart, currentIndex - 1);
4705
- } else if (direction === "right") {
4706
- nextIndex = Math.min(rowEnd, currentIndex + 1);
4707
- } else if (direction === "up") {
4708
- nextIndex = Math.max(0, currentIndex - safeColumns);
4709
- } else if (direction === "down") {
4710
- nextIndex = Math.min(charts.length - 1, currentIndex + safeColumns);
4711
- }
4712
- const nextChart = charts[nextIndex];
4713
- if (!nextChart || nextChart.id === focusedChartId) return false;
4714
- setFocusedChartId(nextChart.id);
4715
- setCrosshair(null);
4716
- setRectangleZoomChartId(null);
4717
- setRectangleZoomRect(null);
4718
- rectangleZoomDragRef.current = null;
4719
- clearDraft();
4720
- onActiveDrawingToolChange?.(null);
4721
- chartRefs.current.get(nextChart.id)?.scrollIntoView({
4722
- block: "nearest",
4723
- inline: "nearest"
4724
- });
4725
- return true;
4726
- },
4727
- [
4728
- charts,
4729
- clearDraft,
4730
- columns,
4731
- focusedChartId,
4732
- fullscreenChartId,
4733
- onActiveDrawingToolChange
4734
- ]
4735
- );
4736
- (0, import_react9.useEffect)(() => {
4737
- const handleKeyDown = (event) => {
4738
- if (fullscreenChartId) return;
4739
- const target = event.target;
4740
- const editable = target instanceof HTMLElement && (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName));
4741
- if (editable) {
4742
- return;
4743
- }
4744
- const arrowDirectionByKey = {
4745
- ArrowLeft: "left",
4746
- ArrowRight: "right",
4747
- ArrowUp: "up",
4748
- ArrowDown: "down"
4749
- };
4750
- const arrowDirection = arrowDirectionByKey[event.key];
4751
- if (arrowDirection) {
4752
- if (focusChartByKeyboard(arrowDirection)) {
4753
- event.preventDefault();
4754
- }
4755
- return;
4756
- }
4757
- if (event.key.toLowerCase() === "r") {
4758
- event.preventDefault();
4759
- resetChartView(focusedChartId);
4760
- return;
4761
- }
4762
- if (event.key.toLowerCase() === "z") {
4763
- event.preventDefault();
4764
- toggleRectangleZoom(focusedChartId);
4765
- return;
4766
- }
4767
- if (!disableDrawings && event.key.toLowerCase() === "m") {
4768
- event.preventDefault();
4769
- toggleMovingAverage(focusedChartId);
4770
- return;
4771
- }
4772
- if (!disableDrawings && ["t", "h", "v", "p"].includes(event.key.toLowerCase())) {
4773
- event.preventDefault();
4774
- const toolByKey = { t: "trendline", h: "hline", v: "vline", p: "pin" };
4775
- toggleDrawingTool(toolByKey[event.key.toLowerCase()]);
4776
- return;
4777
- }
4778
- if (event.key === "Delete" || event.key === "Backspace") {
4779
- if (disableDrawings || !chartSelectedDrawingId) return;
4780
- event.preventDefault();
4781
- deleteSelectedDrawing();
4782
- return;
4783
- }
4784
- if (event.key === "Escape" && (chartActiveDrawingTool || draftDrawing || drawingEditRef.current)) {
4785
- event.preventDefault();
4786
- cancelDrawing();
4787
- return;
4788
- }
4789
- if (event.key === "Escape" && rectangleZoomChartId) {
4790
- event.preventDefault();
4791
- setRectangleZoomChartId(null);
4792
- setRectangleZoomRect(null);
4793
- rectangleZoomDragRef.current = null;
4794
- return;
4795
- }
4796
- if (event.key.toLowerCase() === "f") {
4797
- if (fullscreenChartId) return;
4798
- event.preventDefault();
4799
- openFullscreen(focusedChartId);
4800
- }
4801
- };
4802
- window.addEventListener("keydown", handleKeyDown);
4803
- return () => window.removeEventListener("keydown", handleKeyDown);
4804
- }, [
4805
- focusedChartId,
4806
- fullscreenChartId,
4807
- chartActiveDrawingTool,
4808
- chartSelectedDrawingId,
4809
- cancelDrawing,
4810
- deleteSelectedDrawing,
4811
- disableDrawings,
4812
- draftDrawing,
4813
- focusChartByKeyboard,
4814
- openFullscreen,
4815
- rectangleZoomChartId,
4816
- resetChartView,
4817
- toggleMovingAverage,
4818
- toggleDrawingTool,
4819
- toggleRectangleZoom
4820
- ]);
4821
- const fullscreenChart = fullscreenChartId ? charts.find((chart) => chart.id === fullscreenChartId) : null;
4822
- const chartLayouts = fullscreenChart ? [] : getChartLayout();
4823
- const selectedDrawingLayout = getSelectedDrawingLayout(
4824
- chartLayouts,
4825
- chartDrawings,
4826
- chartSelectedDrawingId
4827
- );
4828
- return /* @__PURE__ */ import_react9.default.createElement(
4829
- "div",
4830
- {
4831
- ref: containerRef,
4832
- className: `aliencharts-root relative h-full overflow-y-auto ${className}`,
4833
- style: {
4834
- cursor: rectangleZoomChartId || chartActiveDrawingTool ? "crosshair" : void 0
4835
- },
4836
- onPointerDown: handlePointerDown,
4837
- onPointerMove: handlePointerMove,
4838
- onPointerLeave: () => setCrosshair(null),
4839
- onPointerUp: handlePointerUp,
4840
- onPointerCancel: handlePointerUp,
4841
- onContextMenu: handleContextMenu
4842
- },
4843
- /* @__PURE__ */ import_react9.default.createElement(
4844
- "canvas",
4845
- {
4846
- ref: canvasRef,
4847
- className: "pointer-events-none absolute left-0 top-0 z-10 block",
4848
- style: {
4849
- width: "100%"
4850
- }
4851
- }
4852
- ),
4853
- /* @__PURE__ */ import_react9.default.createElement(
4854
- "div",
4855
- {
4856
- ref: gridRef,
4857
- className: "relative z-0 grid gap-1 pt-1",
4858
- style: {
4859
- gridTemplateColumns: `repeat(${columns}, minmax(18rem, 1fr))`
4860
- }
4861
- },
4862
- charts.map((chart) => /* @__PURE__ */ import_react9.default.createElement(
4863
- ChartCell,
4864
- {
4865
- key: chart.id,
4866
- chart,
4867
- axisOverlay: axisOverlays[chart.id],
4868
- focused: focusedChartId === chart.id,
4869
- backgroundColor,
4870
- onFocus: setFocusedChartId,
4871
- onFullscreenToggle: () => openFullscreen(chart.id),
4872
- onReset: resetChartView,
4873
- onJumpLatest: jumpChartToLatest,
4874
- onRectangleZoomToggle: toggleRectangleZoom,
4875
- rectangleZoomActive: rectangleZoomChartId === chart.id,
4876
- movingAverage: disableDrawings ? null : movingAverageByChart?.[chart.id],
4877
- onMovingAverageToggle: toggleMovingAverage,
4878
- activeDrawingTool: focusedChartId === chart.id ? chartActiveDrawingTool : null,
4879
- onDrawingToolToggle: toggleDrawingTool,
4880
- hasDrawings: getDrawingsForChart(chartDrawings, chart.id).length > 0,
4881
- onClearDrawingsRequest,
4882
- disableDrawings,
4883
- gridLines,
4884
- showToolbar,
4885
- showLatestValueLine,
4886
- formatXTick,
4887
- formatYValue,
4888
- setRef: (node) => {
4889
- if (node) chartRefs.current.set(chart.id, node);
4890
- else chartRefs.current.delete(chart.id);
4891
- }
4892
- }
4893
- ))
4894
- ),
4895
- /* @__PURE__ */ import_react9.default.createElement(
4896
- CrosshairOverlay,
4897
- {
4898
- crosshair: fullscreenChart || !showTooltips ? null : crosshair,
4899
- height: containerRef.current?.scrollHeight ?? "100%",
4900
- xAxisLabel,
4901
- formatXValue,
4902
- formatYValue
4903
- }
4904
- ),
4905
- /* @__PURE__ */ import_react9.default.createElement(
4906
- DrawingOverlay,
4907
- {
4908
- layouts: chartLayouts,
4909
- drawings: chartDrawings,
4910
- draftDrawing: fullscreenChart || disableDrawings ? null : draftDrawing,
4911
- selectedDrawingId: chartSelectedDrawingId,
4912
- projectPoint: (dataPoint, targetLayout) => projectDataPointToScreenPoint({
4913
- point: dataPoint,
4914
- layout: targetLayout,
4915
- initialVisiblePoints,
4916
- viewStateRef,
4917
- yScaleRef,
4918
- yCenterOffsetRef
4919
- }),
4920
- height: containerRef.current?.scrollHeight ?? "100%"
4921
- }
4922
- ),
4923
- /* @__PURE__ */ import_react9.default.createElement(
4924
- TopMarkersOverlay,
4925
- {
4926
- layouts: fullscreenChart ? [] : chartLayouts,
4927
- markers: topMarkers,
4928
- viewStateRef,
4929
- initialVisiblePoints,
4930
- onMarkerClick: onTopMarkerClick,
4931
- height: containerRef.current?.scrollHeight ?? "100%"
4932
- }
4933
- ),
4934
- selectedDrawingLayout ? /* @__PURE__ */ import_react9.default.createElement(
4935
- DrawingOptionsToolbar,
4936
- {
4937
- drawing: selectedDrawingLayout.drawing,
4938
- style: getDrawingOptionsToolbarStyle(selectedDrawingLayout.layout),
4939
- onToggleExtend: toggleSelectedDrawingExtend,
4940
- onColorChange: updateSelectedDrawingColor,
4941
- onTextChange: updateSelectedDrawingText
4942
- }
4943
- ) : null,
4944
- !disableDrawings && !fullscreenChart && focusedChartId && movingAverageByChart?.[focusedChartId]?.enabled ? (() => {
4945
- const layout = chartLayouts.find(
4946
- (candidate) => candidate.chart.id === focusedChartId
4947
- );
4948
- if (!layout) return null;
4949
- return /* @__PURE__ */ import_react9.default.createElement(
4950
- MovingAverageOptionsToolbar,
4951
- {
4952
- movingAverage: movingAverageByChart[focusedChartId],
4953
- style: getMovingAverageOptionsToolbarStyle(layout),
4954
- onChange: (nextMovingAverage) => onMovingAverageChange?.(focusedChartId, nextMovingAverage)
4955
- }
4956
- );
4957
- })() : null,
4958
- /* @__PURE__ */ import_react9.default.createElement(RectangleZoomOverlay, { rect: fullscreenChart ? null : rectangleZoomRect }),
4959
- fullscreenChart ? /* @__PURE__ */ import_react9.default.createElement(
4960
- ChartFullscreenOverlay,
4961
- {
4962
- chart: fullscreenChart,
4963
- dataRevision,
4964
- renderRevision: revision,
4965
- initialVisiblePoints,
4966
- backgroundColor,
4967
- antialiasLines,
4968
- getAppendAnimatedPoint,
4969
- viewStateRef,
4970
- yScaleRef,
4971
- yCenterOffsetRef,
4972
- yManualScaleRef,
4973
- movingAverageByChart,
4974
- movingAverageCache: movingAverageCacheRef.current,
4975
- onMovingAverageToggle: toggleMovingAverage,
4976
- onMovingAverageChange,
4977
- seriesOrderByChart,
4978
- onClose: closeFullscreen,
4979
- onReset: resetChartView,
4980
- onJumpLatest: jumpChartToLatest,
4981
- xAxisLabel,
4982
- drawings: chartDrawings,
4983
- onDrawingsChange,
4984
- activeDrawingTool: chartActiveDrawingTool,
4985
- onActiveDrawingToolChange,
4986
- selectedDrawingId: chartSelectedDrawingId,
4987
- onSelectedDrawingIdChange,
4988
- createDrawingId,
4989
- onClearDrawingsRequest,
4990
- onChartContextMenu,
4991
- topMarkers,
4992
- onTopMarkerClick,
4993
- disableDrawings,
4994
- gridLines,
4995
- showToolbar,
4996
- showLatestValueLine,
4997
- showTooltips,
4998
- formatXTick,
4999
- formatXValue,
5000
- formatYValue
5001
- }
5002
- ) : null
5003
- );
5004
- }
5005
- //# sourceMappingURL=index.cjs.map