@supersolutions/super-chart-web 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3082 @@
1
+ // packages/bindings/web/src/canvasRenderer.ts
2
+ import { nearestChartIndex, derivePrecision, formatBarDate } from "@supersolutions/super-chart-core";
3
+
4
+ // packages/bindings/web/src/geometry.ts
5
+ function columnExtents(bars) {
6
+ return bars.map((bar, i) => {
7
+ const base = i > 0 ? bars[i - 1].close : bar.open;
8
+ return { index: i, from: base, to: bar.close, up: bar.close >= base };
9
+ });
10
+ }
11
+ function stepLinePoints(bars) {
12
+ const points = [];
13
+ bars.forEach((bar, i) => {
14
+ if (i > 0)
15
+ points.push({ index: i, value: bars[i - 1].close });
16
+ points.push({ index: i, value: bar.close });
17
+ });
18
+ return points;
19
+ }
20
+ function typicalPrice(bar) {
21
+ return (bar.high + bar.low + bar.close) / 3;
22
+ }
23
+ function hlcAreaPoints(bars) {
24
+ return bars.map((bar, index) => ({ index, value: typicalPrice(bar) }));
25
+ }
26
+ function baselineSegments(bars, baseline) {
27
+ if (bars.length === 0)
28
+ return [];
29
+ const segments = [];
30
+ let start = 0;
31
+ let above = bars[0].close >= baseline;
32
+ for (let i = 1; i < bars.length; i++) {
33
+ const nowAbove = bars[i].close >= baseline;
34
+ if (nowAbove !== above) {
35
+ segments.push({ from: start, to: i - 1, above });
36
+ start = i - 1;
37
+ above = nowAbove;
38
+ }
39
+ }
40
+ segments.push({ from: start, to: bars.length - 1, above });
41
+ return segments;
42
+ }
43
+ function halfBarWidth(model, slotWidth) {
44
+ let spacing = slotWidth !== void 0 && Number.isFinite(slotWidth) && slotWidth > 0 ? slotWidth : model.geometry.plotWidth;
45
+ for (let i = 1; i < model.xPositions.length; i++) {
46
+ const gap = model.xPositions[i] - model.xPositions[i - 1];
47
+ if (gap > 0)
48
+ spacing = Math.min(spacing, gap);
49
+ }
50
+ return Math.max(0.25, spacing * 0.34);
51
+ }
52
+
53
+ // packages/bindings/web/src/seriesRenderer.ts
54
+ import { nearestBarIndex } from "@supersolutions/super-chart-core";
55
+ import { ENGINE_CHART_TYPES } from "@supersolutions/super-chart-core";
56
+ function barsOf(candles) {
57
+ return candles.map((bar) => ({ open: bar.o, high: bar.h, low: bar.l, close: bar.c, volume: bar.v }));
58
+ }
59
+ function path(ctx, points) {
60
+ for (let i = 0; i < points.length; i++) {
61
+ const p = points[i];
62
+ if (i === 0)
63
+ ctx.moveTo(p.x, p.y);
64
+ else
65
+ ctx.lineTo(p.x, p.y);
66
+ }
67
+ }
68
+ function runs(candles, y, value) {
69
+ const result = [];
70
+ let previous = -2, run = [];
71
+ for (const candle of candles) {
72
+ if (!Number.isFinite(candle.x) || !Number.isFinite(y(value(candle)))) {
73
+ previous = -2;
74
+ continue;
75
+ }
76
+ if (candle.index !== previous + 1) {
77
+ run = [];
78
+ result.push(run);
79
+ }
80
+ run.push(candle);
81
+ previous = candle.index;
82
+ }
83
+ return result;
84
+ }
85
+ function drawBaseline(ctx, model, baseline, up, down, neutral) {
86
+ const baseY = model.y(baseline);
87
+ if (!Number.isFinite(baseY))
88
+ return;
89
+ const cross = (a, b) => {
90
+ const ay = model.y(a.c), by = model.y(b.c);
91
+ const fraction = by === ay ? 0 : Math.max(0, Math.min(1, (baseY - ay) / (by - ay)));
92
+ return { x: a.x + (b.x - a.x) * fraction, y: baseY };
93
+ };
94
+ for (const candles of runs(model.plottedCandles, model.y, (candle) => candle.c)) {
95
+ const bars = barsOf(candles);
96
+ for (const segment of baselineSegments(bars, baseline)) {
97
+ const points = [];
98
+ for (let i = segment.from; i <= segment.to; i++) {
99
+ const candle = candles[i];
100
+ if (i === segment.from && candle.c >= baseline !== segment.above && i + 1 < candles.length)
101
+ points.push(cross(candle, candles[i + 1]));
102
+ else
103
+ points.push({ x: candle.x, y: model.y(candle.c) });
104
+ }
105
+ if (segment.to + 1 < candles.length && candles[segment.to + 1].c >= baseline !== segment.above) {
106
+ points.push(cross(candles[segment.to], candles[segment.to + 1]));
107
+ }
108
+ const first = points[0], last = points[points.length - 1];
109
+ if (first === void 0)
110
+ continue;
111
+ const color = segment.above ? up : down;
112
+ ctx.fillStyle = ctx.strokeStyle = color;
113
+ ctx.beginPath();
114
+ ctx.moveTo(first.x, baseY);
115
+ for (const p of points)
116
+ ctx.lineTo(p.x, p.y);
117
+ ctx.lineTo(last.x, baseY);
118
+ ctx.closePath();
119
+ ctx.globalAlpha = 0.22;
120
+ ctx.fill();
121
+ ctx.globalAlpha = 1;
122
+ ctx.beginPath();
123
+ path(ctx, points);
124
+ ctx.stroke();
125
+ }
126
+ }
127
+ ctx.strokeStyle = neutral;
128
+ ctx.lineWidth = 1;
129
+ ctx.setLineDash([4, 4]);
130
+ ctx.beginPath();
131
+ ctx.moveTo(model.geometry.left, baseY);
132
+ ctx.lineTo(model.geometry.right, baseY);
133
+ ctx.stroke();
134
+ }
135
+ function paintPriceSeries(ctx, frame, options = {}, bodyHalfWidth) {
136
+ const type = options.chartType ?? "candles";
137
+ if (type === "candles" || type === "ohlc" || type === "line" || type === "area")
138
+ return false;
139
+ const model = frame.model, candles = model.plottedCandles, half = bodyHalfWidth ?? halfBarWidth(model);
140
+ const up = options.up ?? "#58c9ac", down = options.down ?? "#f28b82", line = options.line ?? "#8bafff";
141
+ const color = (rising) => rising ? up : down;
142
+ ctx.save();
143
+ try {
144
+ ctx.globalAlpha = 1;
145
+ ctx.globalCompositeOperation = "source-over";
146
+ ctx.setLineDash([]);
147
+ ctx.lineWidth = 1.5;
148
+ ctx.strokeStyle = ctx.fillStyle = line;
149
+ switch (type) {
150
+ case "hollow-candles":
151
+ case "outline-candles":
152
+ case "hollow-down-candles": {
153
+ for (let group = 0; group < 2; group++) {
154
+ const rising = group === 0;
155
+ const hollow = type === "outline-candles" || (type === "hollow-candles" ? rising : !rising);
156
+ ctx.strokeStyle = ctx.fillStyle = color(rising);
157
+ ctx.lineWidth = 1;
158
+ ctx.beginPath();
159
+ for (const candle of candles) {
160
+ if (candle.c >= candle.o !== rising)
161
+ continue;
162
+ const { bodyTop, bodyBottom, wickTop, wickBottom } = candle;
163
+ if (!Number.isFinite(bodyTop) || !Number.isFinite(bodyBottom) || !Number.isFinite(wickTop) || !Number.isFinite(wickBottom))
164
+ continue;
165
+ const height = Math.max(1, bodyBottom - bodyTop), top = (bodyTop + bodyBottom - height) / 2;
166
+ ctx.moveTo(candle.x, candle.wickTop);
167
+ ctx.lineTo(candle.x, top);
168
+ ctx.moveTo(candle.x, top + height);
169
+ ctx.lineTo(candle.x, candle.wickBottom);
170
+ }
171
+ ctx.stroke();
172
+ ctx.beginPath();
173
+ for (const candle of candles) {
174
+ if (candle.c >= candle.o !== rising)
175
+ continue;
176
+ const { bodyTop, bodyBottom } = candle;
177
+ if (!Number.isFinite(bodyTop) || !Number.isFinite(bodyBottom))
178
+ continue;
179
+ const height = Math.max(1, bodyBottom - bodyTop);
180
+ ctx.rect(candle.x - half, (bodyTop + bodyBottom - height) / 2, half * 2, height);
181
+ }
182
+ if (hollow)
183
+ ctx.stroke();
184
+ else
185
+ ctx.fill();
186
+ }
187
+ break;
188
+ }
189
+ case "columns": {
190
+ const extents = columnExtents(frame.snapshot.bars), preceding = frame.previousClose;
191
+ if (extents.length > 0 && preceding !== void 0 && Number.isFinite(preceding)) {
192
+ extents[0] = { ...extents[0], from: preceding, up: extents[0].to >= preceding };
193
+ }
194
+ for (let group = 0; group < 2; group++) {
195
+ const rising = group === 0;
196
+ ctx.fillStyle = color(rising);
197
+ ctx.beginPath();
198
+ for (const candle of candles) {
199
+ const index = frame.snapshot.bars[candle.index]?.timestamp === candle.t ? candle.index : typeof candle.t === "number" ? nearestBarIndex(frame.snapshot.bars, candle.t) : null;
200
+ if (index === null || frame.snapshot.bars[index].timestamp !== candle.t)
201
+ continue;
202
+ const extent = extents[index];
203
+ if (extent.up !== rising)
204
+ continue;
205
+ const a = model.y(extent.from), b = model.y(extent.to);
206
+ if (!Number.isFinite(a) || !Number.isFinite(b))
207
+ continue;
208
+ ctx.rect(candle.x - half, Math.min(a, b), half * 2, Math.max(1, Math.abs(b - a)));
209
+ }
210
+ ctx.fill();
211
+ }
212
+ break;
213
+ }
214
+ case "high-low": {
215
+ ctx.lineWidth = 2;
216
+ for (let group = 0; group < 2; group++) {
217
+ const rising = group === 0;
218
+ ctx.strokeStyle = color(rising);
219
+ ctx.beginPath();
220
+ for (const candle of candles) {
221
+ if (candle.c >= candle.o !== rising)
222
+ continue;
223
+ const x = candle.x, a = model.y(candle.h), b = model.y(candle.l);
224
+ if (!Number.isFinite(a) || !Number.isFinite(b))
225
+ continue;
226
+ ctx.moveTo(x, a);
227
+ ctx.lineTo(x, b);
228
+ }
229
+ ctx.stroke();
230
+ }
231
+ break;
232
+ }
233
+ case "volume-bars": {
234
+ let maximum = 0;
235
+ for (const candle of candles)
236
+ maximum = Math.max(maximum, candle.v ?? 0);
237
+ ctx.lineWidth = 2;
238
+ for (let i = 0; i < candles.length; i++) {
239
+ const candle = candles[i], open = model.y(candle.o), close = model.y(candle.c);
240
+ if (!Number.isFinite(open) || !Number.isFinite(close))
241
+ continue;
242
+ ctx.strokeStyle = color(candle.c >= candle.o);
243
+ ctx.globalAlpha = 0.25 + 0.65 * (maximum <= 0 ? 0 : Math.max(0, Math.min(1, (candle.v ?? 0) / maximum)));
244
+ ctx.beginPath();
245
+ ctx.moveTo(candle.x, candle.wickTop);
246
+ ctx.lineTo(candle.x, candle.wickBottom);
247
+ ctx.moveTo(candle.x - half, open);
248
+ ctx.lineTo(candle.x, open);
249
+ ctx.moveTo(candle.x, close);
250
+ ctx.lineTo(candle.x + half, close);
251
+ ctx.stroke();
252
+ }
253
+ break;
254
+ }
255
+ case "step-line":
256
+ case "hlc-area":
257
+ case "line-markers": {
258
+ const hlc = type === "hlc-area";
259
+ for (const run of runs(candles, model.y, (candle) => hlc ? candle.h / 3 + candle.l / 3 + candle.c / 3 : candle.c)) {
260
+ const bars = barsOf(run);
261
+ const values = type === "step-line" ? stepLinePoints(bars) : hlc ? hlcAreaPoints(bars) : bars.map((bar, index) => ({ index, value: bar.close }));
262
+ if (hlc) {
263
+ for (const value of values) {
264
+ if (!Number.isFinite(value.value)) {
265
+ const bar = bars[value.index];
266
+ value.value = bar.high / 3 + bar.low / 3 + bar.close / 3;
267
+ }
268
+ }
269
+ }
270
+ const points = values.map((value) => ({ x: run[value.index].x, y: model.y(value.value) })).filter((p) => Number.isFinite(p.y));
271
+ if (points.length === 0)
272
+ continue;
273
+ if (hlc) {
274
+ ctx.beginPath();
275
+ ctx.moveTo(points[0].x, model.geometry.bottom);
276
+ for (const p of points)
277
+ ctx.lineTo(p.x, p.y);
278
+ ctx.lineTo(points[points.length - 1].x, model.geometry.bottom);
279
+ ctx.closePath();
280
+ ctx.globalAlpha = 0.18;
281
+ ctx.fill();
282
+ ctx.globalAlpha = 1;
283
+ }
284
+ ctx.lineWidth = 1.75;
285
+ ctx.beginPath();
286
+ path(ctx, points);
287
+ ctx.stroke();
288
+ if (type === "line-markers") {
289
+ ctx.beginPath();
290
+ for (const p of points) {
291
+ ctx.moveTo(p.x + 2.5, p.y);
292
+ ctx.arc(p.x, p.y, 2.5, 0, Math.PI * 2);
293
+ }
294
+ ctx.fill();
295
+ }
296
+ }
297
+ break;
298
+ }
299
+ case "baseline": {
300
+ const baseline = options.baseline ?? frame.snapshot.bars[0]?.close;
301
+ if (baseline !== void 0 && Number.isFinite(baseline))
302
+ drawBaseline(ctx, model, baseline, up, down, options.text ?? "#aebcca");
303
+ break;
304
+ }
305
+ default:
306
+ return false;
307
+ }
308
+ return true;
309
+ } finally {
310
+ ctx.restore();
311
+ }
312
+ }
313
+
314
+ // packages/bindings/web/src/canvasRenderer.ts
315
+ var DEFAULTS = {
316
+ background: "#10151d",
317
+ grid: "#283444",
318
+ text: "#aebcca",
319
+ up: "#58c9ac",
320
+ down: "#f28b82",
321
+ line: "#8bafff",
322
+ font: "12px system-ui"
323
+ };
324
+ var timeFormats = /* @__PURE__ */ new Map();
325
+ function timeFormat(timeZone) {
326
+ let format = timeFormats.get(timeZone);
327
+ if (format !== void 0)
328
+ return format;
329
+ format = new Intl.DateTimeFormat("en", { timeZone, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" });
330
+ if (timeFormats.size === 8)
331
+ timeFormats.delete(timeFormats.keys().next().value);
332
+ timeFormats.set(timeZone, format);
333
+ return format;
334
+ }
335
+ function precision(value) {
336
+ return value !== void 0 && Number.isFinite(value) ? Math.max(0, Math.min(20, Math.trunc(value))) : 2;
337
+ }
338
+ var dateLabels = /* @__PURE__ */ new Map();
339
+ var calendarDates = /* @__PURE__ */ new Map();
340
+ function boundedSet(cache, key, value) {
341
+ if (cache.size >= 512)
342
+ cache.delete(cache.keys().next().value);
343
+ cache.set(key, value);
344
+ return value;
345
+ }
346
+ function timeLabel(displayDtf, timestamp, options, type, format = "HH:mm") {
347
+ if (!Number.isFinite(timestamp) || Math.abs(timestamp) > 864e13)
348
+ return String(timestamp);
349
+ const key = JSON.stringify([options.timeZone ?? "UTC", timestamp, options.timespan ?? "", options.symbolTimezone, type, format]);
350
+ const cached = dateLabels.get(key);
351
+ return cached ?? boundedSet(dateLabels, key, formatBarDate({ displayDtf, timestamp, timespan: options.timespan ?? "", symbolTimezone: options.symbolTimezone, type, format }));
352
+ }
353
+ function calendarDate(format, timestamp, timezone) {
354
+ const key = JSON.stringify([timezone, timestamp]), cached = calendarDates.get(key);
355
+ if (cached !== void 0)
356
+ return cached;
357
+ const date = {};
358
+ for (const part of format.formatToParts(timestamp))
359
+ if (part.type === "year" || part.type === "month" || part.type === "day")
360
+ date[part.type] = part.value;
361
+ return boundedSet(calendarDates, key, date);
362
+ }
363
+ function boundaryFormat(date, previous) {
364
+ if (previous === null)
365
+ return "MM-DD";
366
+ if (date.year !== previous.year)
367
+ return "YYYY";
368
+ if (date.month !== previous.month)
369
+ return "YYYY-MM";
370
+ return date.day !== previous.day ? "MM-DD" : "HH:mm";
371
+ }
372
+ function ready(m) {
373
+ const g = m.geometry;
374
+ return Number.isFinite(g.width) && Number.isFinite(g.height) && g.width > 0 && g.height > 0 && Number.isFinite(g.left) && Number.isFinite(g.top) && Number.isFinite(g.plotWidth) && Number.isFinite(g.plotHeight) && g.plotWidth > 0 && g.plotHeight > 0;
375
+ }
376
+ function clipPlot(ctx, m) {
377
+ const g = m.geometry;
378
+ ctx.beginPath();
379
+ ctx.rect(g.left, g.top, g.plotWidth, g.plotHeight);
380
+ ctx.clip();
381
+ }
382
+ var closeGeometry = /* @__PURE__ */ new WeakMap();
383
+ function closePoints(m) {
384
+ let geometry = closeGeometry.get(m);
385
+ if (geometry !== void 0)
386
+ return geometry;
387
+ geometry = { points: [] };
388
+ for (const candle of m.plottedCandles) {
389
+ const y = m.y(candle.c);
390
+ if (Number.isFinite(candle.x) && Number.isFinite(y))
391
+ geometry.points.push({ x: candle.x, y, index: candle.index });
392
+ }
393
+ closeGeometry.set(m, geometry);
394
+ return geometry;
395
+ }
396
+ function closePath(target, m, area) {
397
+ let previous = -2, firstX = 0, lastX = 0, open = false;
398
+ for (const point of closePoints(m).points) {
399
+ if (point.index !== previous + 1) {
400
+ if (area && open) {
401
+ target.lineTo(lastX, m.geometry.bottom);
402
+ target.lineTo(firstX, m.geometry.bottom);
403
+ target.closePath();
404
+ }
405
+ target.moveTo(point.x, point.y);
406
+ firstX = point.x;
407
+ open = true;
408
+ } else
409
+ target.lineTo(point.x, point.y);
410
+ previous = point.index;
411
+ lastX = point.x;
412
+ }
413
+ if (area && open) {
414
+ target.lineTo(lastX, m.geometry.bottom);
415
+ target.lineTo(firstX, m.geometry.bottom);
416
+ target.closePath();
417
+ }
418
+ }
419
+ function paintClose(ctx, m, area) {
420
+ if (typeof Path2D !== "undefined") {
421
+ const geometry = closePoints(m), key = area ? "area" : "stroke";
422
+ let path2 = geometry[key];
423
+ if (path2 === void 0) {
424
+ path2 = new Path2D();
425
+ closePath(path2, m, area);
426
+ geometry[key] = path2;
427
+ }
428
+ if (area)
429
+ ctx.fill(path2);
430
+ else
431
+ ctx.stroke(path2);
432
+ } else {
433
+ ctx.beginPath();
434
+ closePath(ctx, m, area);
435
+ if (area)
436
+ ctx.fill();
437
+ else
438
+ ctx.stroke();
439
+ }
440
+ }
441
+ var envelopes = /* @__PURE__ */ new WeakMap();
442
+ function candleEnvelope(m) {
443
+ if (m.plottedCandles.length <= m.geometry.plotWidth)
444
+ return m.plottedCandles;
445
+ const cached = envelopes.get(m);
446
+ if (cached !== void 0)
447
+ return cached;
448
+ const result = [];
449
+ let bucket = NaN;
450
+ for (const candle of m.plottedCandles) {
451
+ const pixel = Math.floor(candle.x);
452
+ if (!Number.isFinite(pixel))
453
+ continue;
454
+ const previous = result[result.length - 1];
455
+ if (pixel !== bucket || previous === void 0) {
456
+ result.push({ ...candle });
457
+ bucket = pixel;
458
+ continue;
459
+ }
460
+ previous.c = candle.c;
461
+ previous.h = Math.max(previous.h, candle.h);
462
+ previous.l = Math.min(previous.l, candle.l);
463
+ previous.wickTop = Math.min(previous.wickTop, candle.wickTop);
464
+ previous.wickBottom = Math.max(previous.wickBottom, candle.wickBottom);
465
+ const open = m.y(previous.o), close = m.y(previous.c);
466
+ previous.bodyTop = Math.min(open, close);
467
+ previous.bodyBottom = Math.max(open, close);
468
+ }
469
+ envelopes.set(m, result);
470
+ return result;
471
+ }
472
+ function paintEngineFrame(ctx, frame, options = {}) {
473
+ const m = frame.model, g = m.geometry;
474
+ if (!ready(m))
475
+ return;
476
+ const digits = precision(options.pricePrecision ?? frame.pricePrecision ?? derivePrecision(frame.snapshot.bars));
477
+ const formatter = timeFormat(options.timeZone ?? "UTC");
478
+ ctx.save();
479
+ try {
480
+ ctx.globalAlpha = 1;
481
+ ctx.globalCompositeOperation = "source-over";
482
+ ctx.setLineDash([]);
483
+ ctx.lineWidth = 1;
484
+ ctx.fillStyle = options.background ?? DEFAULTS.background;
485
+ ctx.fillRect(0, 0, g.width, g.height);
486
+ ctx.font = options.font ?? DEFAULTS.font;
487
+ ctx.textBaseline = "middle";
488
+ ctx.save();
489
+ try {
490
+ clipPlot(ctx, m);
491
+ ctx.strokeStyle = options.grid ?? DEFAULTS.grid;
492
+ ctx.beginPath();
493
+ for (const tick of m.yTicks) {
494
+ const y = m.y(tick);
495
+ ctx.moveTo(g.left, y);
496
+ ctx.lineTo(g.right, y);
497
+ }
498
+ for (const index of m.xTickIndices) {
499
+ const x = m.xPositions[index];
500
+ ctx.moveTo(x, g.top);
501
+ ctx.lineTo(x, g.bottom);
502
+ }
503
+ ctx.stroke();
504
+ const type = options.chartType ?? "candles";
505
+ const span = frame.snapshot.viewport.i1 - frame.snapshot.viewport.i0;
506
+ const half = type !== "line" && type !== "area" || m.plottedSeries.some((series) => series.source.type === "bar") ? halfBarWidth(m, frame.snapshot.dataStart !== void 0 && span > 0 ? g.plotWidth / span : void 0) : 0;
507
+ if (paintPriceSeries(ctx, frame, options, half)) {
508
+ } else if (type === "line" || type === "area") {
509
+ const color = options.line ?? DEFAULTS.line;
510
+ if (type === "area") {
511
+ ctx.fillStyle = color;
512
+ ctx.globalAlpha = 0.14;
513
+ paintClose(ctx, m, true);
514
+ ctx.globalAlpha = 1;
515
+ }
516
+ ctx.strokeStyle = color;
517
+ ctx.lineWidth = 1.5;
518
+ paintClose(ctx, m, false);
519
+ } else {
520
+ const candles = options.candleDensity === "pixel-envelope" ? candleEnvelope(m) : m.plottedCandles;
521
+ for (let group = 0; group < 2; group++) {
522
+ const up = group === 0;
523
+ const color = up ? options.up ?? DEFAULTS.up : options.down ?? DEFAULTS.down;
524
+ ctx.fillStyle = ctx.strokeStyle = color;
525
+ ctx.beginPath();
526
+ for (const candle of candles) {
527
+ if (candle.c >= candle.o !== up)
528
+ continue;
529
+ ctx.moveTo(candle.x, candle.wickTop);
530
+ ctx.lineTo(candle.x, candle.wickBottom);
531
+ if (type === "ohlc") {
532
+ const openY = m.y(candle.o), closeY = m.y(candle.c);
533
+ ctx.moveTo(candle.x - half, openY);
534
+ ctx.lineTo(candle.x, openY);
535
+ ctx.moveTo(candle.x, closeY);
536
+ ctx.lineTo(candle.x + half, closeY);
537
+ }
538
+ }
539
+ ctx.stroke();
540
+ if (type === "candles") {
541
+ ctx.beginPath();
542
+ for (const candle of candles) {
543
+ if (candle.c >= candle.o !== up)
544
+ continue;
545
+ const bodyHeight = Math.max(1, candle.bodyBottom - candle.bodyTop);
546
+ ctx.rect(candle.x - half, candle.bodyTop, 2 * half, bodyHeight);
547
+ }
548
+ ctx.fill();
549
+ }
550
+ }
551
+ }
552
+ ctx.lineWidth = 1.5;
553
+ for (const series of m.plottedSeries) {
554
+ const style = frame.figureStyles?.get(series.source.id);
555
+ ctx.lineWidth = style?.lineWidth ?? 1.5;
556
+ ctx.setLineDash(style?.dash === void 0 ? [] : [...style.dash]);
557
+ ctx.strokeStyle = series.source.color ?? options.line ?? DEFAULTS.line;
558
+ if (series.source.type === "bar") {
559
+ ctx.fillStyle = ctx.strokeStyle;
560
+ const base = Math.max(m.min, Math.min(m.max, style?.base ?? 0));
561
+ const zero = Math.max(g.top, Math.min(g.bottom, m.y(base)));
562
+ ctx.beginPath();
563
+ for (const point of series.points)
564
+ ctx.rect(point.x - half, Math.min(zero, point.y), half * 2, Math.max(1, Math.abs(point.y - zero)));
565
+ ctx.fill();
566
+ continue;
567
+ }
568
+ if (series.source.type === "scatter") {
569
+ ctx.fillStyle = ctx.strokeStyle;
570
+ ctx.beginPath();
571
+ for (const point of series.points) {
572
+ ctx.moveTo(point.x + ctx.lineWidth, point.y);
573
+ ctx.arc(point.x, point.y, ctx.lineWidth, 0, 2 * Math.PI);
574
+ }
575
+ ctx.fill();
576
+ continue;
577
+ }
578
+ ctx.beginPath();
579
+ let previous = -2;
580
+ for (const point of series.points) {
581
+ if (point.index !== previous + 1)
582
+ ctx.moveTo(point.x, point.y);
583
+ else
584
+ ctx.lineTo(point.x, point.y);
585
+ previous = point.index;
586
+ }
587
+ ctx.stroke();
588
+ }
589
+ } finally {
590
+ ctx.restore();
591
+ }
592
+ ctx.fillStyle = options.text ?? DEFAULTS.text;
593
+ if (frame.indicatorPane === true && frame.title !== void 0) {
594
+ ctx.textAlign = "left";
595
+ ctx.fillText(frame.title, g.left + 4, Math.max(8, g.top - 7), Math.max(1, g.plotWidth - 8));
596
+ }
597
+ ctx.textAlign = "right";
598
+ for (const tick of m.yTicks)
599
+ ctx.fillText(options.formatPrice?.(tick, digits) ?? tick.toFixed(digits), g.left - 6, m.y(tick), Math.max(1, g.left - 10));
600
+ if (m.hasRightAxis) {
601
+ ctx.textAlign = "left";
602
+ for (const tick of m.rightTicks)
603
+ ctx.fillText(tick.toFixed(digits), g.right + 6, m.y(tick, "right"), Math.max(1, g.width - g.right - 10));
604
+ }
605
+ let labelRight = -Infinity;
606
+ let previousDate = null;
607
+ ctx.textAlign = "center";
608
+ for (const index of options.showTimeAxis === false ? [] : m.xTickIndices) {
609
+ const key = m.xKeys[index];
610
+ const date = (options.timespan === "minute" || options.timespan === "second") && typeof key === "number" && Number.isFinite(key) && Math.abs(key) <= 864e13 ? calendarDate(formatter, key, options.timeZone ?? "UTC") : null;
611
+ const label = typeof key === "number" ? timeLabel(formatter, key, options, "xAxis", date === null ? "HH:mm" : boundaryFormat(date, previousDate)) : String(key);
612
+ const width = Math.min(ctx.measureText(label).width, Math.max(1, g.width - 8));
613
+ const x = Math.max(4 + width / 2, Math.min(g.width - 4 - width / 2, m.xPositions[index]));
614
+ if (x - width / 2 < labelRight + 8)
615
+ continue;
616
+ ctx.fillText(label, x, Math.min(g.height - 8, g.bottom + 18), width);
617
+ labelRight = x + width / 2;
618
+ previousDate = date;
619
+ }
620
+ } finally {
621
+ ctx.restore();
622
+ }
623
+ }
624
+ function paintEngineCursor(ctx, frame, x, options = {}) {
625
+ const m = frame.model, g = m.geometry;
626
+ if (!ready(m) || !Number.isFinite(x) || x < g.left || x > g.right)
627
+ return;
628
+ const index = nearestChartIndex(m, x), hit = frame.inspectBar !== void 0 ? frame.inspectBar(x) : frame.inspect(x);
629
+ if (index === null || hit === null)
630
+ return;
631
+ const bar = hit.displayBar;
632
+ const typical = typicalPrice(bar);
633
+ const displayed = options.chartType === "hlc-area" ? Number.isFinite(typical) ? typical : bar.high / 3 + bar.low / 3 + bar.close / 3 : bar.close;
634
+ const px = m.xPositions[index], py = m.y(displayed);
635
+ const indicator = frame.indicatorPane === true;
636
+ if (!Number.isFinite(px) || !indicator && !Number.isFinite(py))
637
+ return;
638
+ const digits = precision(options.pricePrecision ?? frame.pricePrecision ?? derivePrecision(frame.snapshot.bars));
639
+ ctx.save();
640
+ try {
641
+ ctx.globalAlpha = 1;
642
+ ctx.globalCompositeOperation = "source-over";
643
+ ctx.strokeStyle = ctx.fillStyle = options.text ?? DEFAULTS.text;
644
+ ctx.lineWidth = 1;
645
+ ctx.setLineDash([3, 3]);
646
+ ctx.save();
647
+ try {
648
+ clipPlot(ctx, m);
649
+ ctx.beginPath();
650
+ ctx.moveTo(px, g.top);
651
+ ctx.lineTo(px, g.bottom);
652
+ if (!indicator) {
653
+ ctx.moveTo(g.left, py);
654
+ ctx.lineTo(g.right, py);
655
+ }
656
+ ctx.stroke();
657
+ } finally {
658
+ ctx.restore();
659
+ }
660
+ ctx.setLineDash([]);
661
+ ctx.font = options.font ?? DEFAULTS.font;
662
+ ctx.textAlign = "left";
663
+ ctx.textBaseline = "top";
664
+ const label = indicator ? m.plottedSeries.map((series) => {
665
+ let lo = 0, hi = series.points.length;
666
+ while (lo < hi) {
667
+ const mid = lo + hi >>> 1;
668
+ if (series.points[mid].index < index)
669
+ lo = mid + 1;
670
+ else
671
+ hi = mid;
672
+ }
673
+ const point = series.points[lo]?.index === index ? series.points[lo] : void 0;
674
+ return `${series.source.name ?? series.source.id} ${point === void 0 ? "\u2014" : Number(point.value.toPrecision(6))}`;
675
+ }).join(" ") : `${hit.mode === "heikin-ashi" ? "HA " : ""}O ${bar.open.toFixed(digits)} H ${bar.high.toFixed(digits)} L ${bar.low.toFixed(digits)} C ${bar.close.toFixed(digits)}`;
676
+ const width = Math.min(g.plotWidth, ctx.measureText(label).width + 8);
677
+ ctx.fillStyle = options.background ?? DEFAULTS.background;
678
+ ctx.fillRect(g.left, g.top, width, 20);
679
+ ctx.fillStyle = options.text ?? DEFAULTS.text;
680
+ ctx.fillText(label, g.left + 4, g.top + 3, Math.max(1, width - 8));
681
+ ctx.textAlign = "right";
682
+ ctx.textBaseline = "middle";
683
+ if (!indicator)
684
+ ctx.fillText(options.formatPrice?.(displayed, digits) ?? displayed.toFixed(digits), g.left - 6, py, Math.max(1, g.left - 10));
685
+ const time = timeLabel(timeFormat(options.timeZone ?? "UTC"), hit.timestamp, options, "crosshair");
686
+ const timeWidth = Math.min(g.width - 8, ctx.measureText(time).width);
687
+ const timeX = Math.max(4 + timeWidth / 2, Math.min(g.width - 4 - timeWidth / 2, px));
688
+ ctx.textAlign = "center";
689
+ if (options.showTimeAxis !== false)
690
+ ctx.fillText(time, timeX, Math.min(g.height - 8, g.bottom + 18), timeWidth);
691
+ } finally {
692
+ ctx.restore();
693
+ }
694
+ }
695
+
696
+ // packages/bindings/web/src/drawingMedia.ts
697
+ import { DRAWING_IMAGE_LIMITS, dimensions, fail, header, raster, validateDrawingImage } from "@supersolutions/super-chart-core";
698
+ async function createDrawingImage(blob, signal) {
699
+ if (!["image/png", "image/jpeg", "image/webp"].includes(blob.type) || blob.size <= 0 || blob.size > Math.floor((DRAWING_IMAGE_LIMITS.encodedCharacters - 32) * 3 / 4))
700
+ fail();
701
+ signal?.throwIfAborted();
702
+ const bytes = new Uint8Array(await blob.arrayBuffer());
703
+ signal?.throwIfAborted();
704
+ header(bytes, blob.type);
705
+ let binary = "";
706
+ for (let at = 0; at < bytes.length; at += 8192)
707
+ binary += String.fromCharCode(...bytes.subarray(at, at + 8192));
708
+ return validateDrawingImage(`data:${blob.type};base64,${btoa(binary)}`);
709
+ }
710
+ var DrawingImageRenderer = class {
711
+ /** Optional owner-window decoder is useful when the chart lives in another document. */
712
+ constructor(decode = async (blob) => await createImageBitmap(blob)) {
713
+ this.decode = decode;
714
+ }
715
+ entries = /* @__PURE__ */ new Map();
716
+ held = /* @__PURE__ */ new Map();
717
+ pixels = 0;
718
+ disposed = false;
719
+ // Return the same cached decode promise to every caller; an async wrapper would change its identity.
720
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
721
+ prepare(source) {
722
+ if (this.disposed)
723
+ return Promise.reject(new Error("Drawing image renderer is disposed"));
724
+ const previous = this.entries.get(source);
725
+ if (previous !== void 0)
726
+ return previous.promise;
727
+ let input;
728
+ try {
729
+ input = raster(source);
730
+ } catch (error) {
731
+ return Promise.reject(error);
732
+ }
733
+ const pixels = input.width * input.height;
734
+ if (this.entries.size >= DRAWING_IMAGE_LIMITS.cachedImages || this.pixels + pixels > DRAWING_IMAGE_LIMITS.cachedPixels)
735
+ return Promise.reject(new RangeError("Drawing image cache is full; release unused images before adding another"));
736
+ const entry = { promise: Promise.resolve(), bitmap: null, error: null, pixels, invalidations: /* @__PURE__ */ new Set() };
737
+ this.entries.set(source, entry);
738
+ this.pixels += pixels;
739
+ entry.promise = Promise.resolve().then(async () => {
740
+ if (this.disposed || this.entries.get(source) !== entry)
741
+ throw new Error("Drawing image decode was cancelled");
742
+ const bitmap = await this.decode(new Blob([input.bytes], { type: input.mime }));
743
+ if (this.disposed || this.entries.get(source) !== entry) {
744
+ bitmap.close();
745
+ throw new Error("Drawing image decode was cancelled");
746
+ }
747
+ try {
748
+ dimensions(bitmap.width, bitmap.height);
749
+ if (bitmap.width * bitmap.height !== pixels)
750
+ throw new RangeError("Decoded image dimensions disagree with the raster header");
751
+ entry.bitmap = bitmap;
752
+ } catch (error) {
753
+ bitmap.close();
754
+ throw error;
755
+ }
756
+ }).catch((error) => {
757
+ entry.error = error instanceof Error ? error : new Error(String(error));
758
+ throw entry.error;
759
+ }).finally(() => {
760
+ const callbacks = [...entry.invalidations];
761
+ entry.invalidations.clear();
762
+ if (!this.disposed && this.entries.get(source) === entry)
763
+ for (const callback of callbacks) {
764
+ try {
765
+ callback();
766
+ } catch {
767
+ }
768
+ }
769
+ });
770
+ void entry.promise.catch(() => {
771
+ });
772
+ return entry.promise;
773
+ }
774
+ getError(source) {
775
+ return this.entries.get(source)?.error ?? null;
776
+ }
777
+ paint(context, source, bounds, clip, onInvalidate) {
778
+ if (this.disposed || ![bounds.left, bounds.top, bounds.right, bounds.bottom, clip.left, clip.top, clip.right, clip.bottom].every(Number.isFinite) || bounds.right <= bounds.left || bounds.bottom <= bounds.top || clip.right <= clip.left || clip.bottom <= clip.top)
779
+ return "unavailable";
780
+ if (!this.entries.has(source))
781
+ void this.prepare(source).catch(() => {
782
+ });
783
+ const entry = this.entries.get(source);
784
+ if (entry === void 0 || entry.error !== null)
785
+ return "unavailable";
786
+ if (entry.bitmap === null) {
787
+ if (onInvalidate !== void 0)
788
+ entry.invalidations.add(onInvalidate);
789
+ return "loading";
790
+ }
791
+ context.save();
792
+ try {
793
+ context.beginPath();
794
+ context.rect(clip.left, clip.top, clip.right - clip.left, clip.bottom - clip.top);
795
+ context.clip();
796
+ context.globalAlpha = 1;
797
+ context.globalCompositeOperation = "source-over";
798
+ context.drawImage(entry.bitmap, bounds.left, bounds.top, bounds.right - bounds.left, bounds.bottom - bounds.top);
799
+ } finally {
800
+ context.restore();
801
+ }
802
+ return "ready";
803
+ }
804
+ /** Source data stays in drawing history; a released bitmap can be decoded again on undo. */
805
+ retain(sources) {
806
+ for (const [source, entry] of this.entries) {
807
+ const pending = entry.bitmap === null && entry.error === null;
808
+ if (!sources.has(source) && !this.held.has(source) && !pending)
809
+ this.release(source);
810
+ }
811
+ }
812
+ /** Keep validated resources across an asynchronous market/document restore. */
813
+ hold(sources) {
814
+ if (this.disposed)
815
+ throw new Error("Drawing image renderer is disposed");
816
+ if (sources.size > DRAWING_IMAGE_LIMITS.cachedImages)
817
+ throw new RangeError("Too many held drawing images");
818
+ const keys = [...sources];
819
+ for (const source of keys)
820
+ validateDrawingImage(source);
821
+ if ((/* @__PURE__ */ new Set([...this.held.keys(), ...keys])).size > DRAWING_IMAGE_LIMITS.cachedImages)
822
+ throw new RangeError("Too many held drawing images");
823
+ for (const source of keys)
824
+ this.held.set(source, (this.held.get(source) ?? 0) + 1);
825
+ let released = false;
826
+ return () => {
827
+ if (released)
828
+ return;
829
+ released = true;
830
+ for (const source of keys) {
831
+ const count = this.held.get(source) ?? 0;
832
+ if (count <= 1)
833
+ this.held.delete(source);
834
+ else
835
+ this.held.set(source, count - 1);
836
+ }
837
+ };
838
+ }
839
+ has(source) {
840
+ return this.entries.has(source);
841
+ }
842
+ release(source) {
843
+ if (this.held.has(source))
844
+ return;
845
+ const entry = this.entries.get(source);
846
+ if (entry === void 0)
847
+ return;
848
+ this.entries.delete(source);
849
+ this.pixels -= entry.pixels;
850
+ entry.invalidations.clear();
851
+ entry.bitmap?.close();
852
+ }
853
+ dispose() {
854
+ if (this.disposed)
855
+ return;
856
+ this.disposed = true;
857
+ this.held.clear();
858
+ for (const source of this.entries.keys())
859
+ this.release(source);
860
+ }
861
+ };
862
+
863
+ // packages/bindings/web/src/chart.ts
864
+ import { RealtimeSeries, RealtimeView, normalizeBars, normalizeBar, ENGINE_CHART_TYPES as ENGINE_CHART_TYPES2, buildEngineFrame, validateEngineLayout, validatePriceScale, EngineIndicators, DrawingStore as DrawingStore2, DRAWING_TYPES, projectDrawing, viewportLimitsForWidth, financialGeometry } from "@supersolutions/super-chart-core";
865
+
866
+ // packages/bindings/web/src/gestures.ts
867
+ import { viewportSpan, clampContinuousViewport as clampViewport, zoomContinuousViewport as zoomViewport } from "@supersolutions/super-chart-core";
868
+ function attachEngineGestures(element, view, geometry, cursor, options = {}) {
869
+ const doc = element.ownerDocument, win = doc.defaultView;
870
+ if (win === null)
871
+ return Object.assign(() => {
872
+ }, { cancelInput: () => {
873
+ } });
874
+ const frameScheduler = options.frameScheduler ?? {
875
+ request: (callback) => win.requestAnimationFrame(callback),
876
+ cancel: (id) => win.cancelAnimationFrame(id)
877
+ };
878
+ const abort = new win.AbortController(), pointers = /* @__PURE__ */ new Map();
879
+ const reduced = win.matchMedia?.("(prefers-reduced-motion: reduce)");
880
+ const oldTouchAction = element.style.touchAction;
881
+ element.style.touchAction = "none";
882
+ let disposed = false, inspection = false;
883
+ let drag = null;
884
+ let pending = null;
885
+ let wheel = null;
886
+ let raf = null, rectFrame = null;
887
+ let rect = null;
888
+ let velocity = 0, sample = { x: 0, time: 0 };
889
+ let coast = null;
890
+ let hold = null;
891
+ const editable = (target) => target instanceof win.Element && target.closest('input,textarea,select,button,a,[contenteditable]:not([contenteditable="false"])') !== null;
892
+ const invalidateRect = () => {
893
+ rect = null;
894
+ };
895
+ const bounds = () => {
896
+ if (rect === null)
897
+ rect = element.getBoundingClientRect();
898
+ if (rectFrame === null)
899
+ rectFrame = frameScheduler.request(() => {
900
+ rectFrame = null;
901
+ rect = null;
902
+ });
903
+ return rect;
904
+ };
905
+ const eventTime = (event) => {
906
+ const now = win.performance.now();
907
+ return Number.isFinite(event.timeStamp) && Math.abs(event.timeStamp - now) < 6e4 ? event.timeStamp : now;
908
+ };
909
+ const cancelHold = () => {
910
+ if (hold !== null)
911
+ win.clearTimeout(hold.timer);
912
+ hold = null;
913
+ };
914
+ const readFrame = () => {
915
+ const g = geometry(), snapshot = view.getSnapshot();
916
+ return g !== null && Number.isFinite(g.left) && Number.isFinite(g.plotWidth) && g.plotWidth > 0 && snapshot.length > 0 ? { length: snapshot.length, left: g.left, width: g.plotWidth, observed: snapshot.viewport } : null;
917
+ };
918
+ const valid = (frame) => {
919
+ const now = readFrame();
920
+ return now !== null && now.length === frame.length && now.left === frame.left && now.width === frame.width && now.observed === frame.observed;
921
+ };
922
+ const commit = () => {
923
+ const value = pending;
924
+ pending = null;
925
+ if (value === null || !valid(value.frame))
926
+ return;
927
+ view.setViewport(value.next);
928
+ const snapshot = view.getSnapshot();
929
+ if (drag !== null)
930
+ drag.observed = snapshot.viewport;
931
+ if (coast !== null)
932
+ coast.frame.observed = snapshot.viewport;
933
+ if (wheel !== null)
934
+ wheel.frame.observed = snapshot.viewport;
935
+ if (snapshot.viewport.i0 === 0 && snapshot.hasMore)
936
+ view.requestMore();
937
+ };
938
+ const schedule = () => {
939
+ if (disposed || raf !== null)
940
+ return;
941
+ raf = frameScheduler.request((time) => {
942
+ raf = null;
943
+ if (disposed)
944
+ return;
945
+ if (coast !== null) {
946
+ if (!valid(coast.frame) || reduced?.matches === true || options.reducedMotion === true)
947
+ coast = null;
948
+ else {
949
+ const elapsed = Math.max(0, time - coast.started), decay = Math.exp(-elapsed / 220);
950
+ const distance = coast.velocity * 220 * (1 - decay);
951
+ const delta = -distance / coast.frame.width * Math.max(0, viewportSpan(coast.start) - 1);
952
+ const proposed = { i0: coast.start.i0 + delta, i1: coast.start.i1 + delta };
953
+ const next = clampViewport(proposed, coast.frame.length, view.getLimits());
954
+ pending = { next, frame: coast.frame };
955
+ const blocked = delta < 0 && next.i0 > proposed.i0 + 1e-9 || delta > 0 && next.i1 < proposed.i1 - 1e-9;
956
+ if (Math.abs(coast.velocity * decay) < 0.01 || elapsed >= 1400 || blocked)
957
+ coast = null;
958
+ }
959
+ }
960
+ if (wheel !== null) {
961
+ if (!valid(wheel.frame))
962
+ wheel = null;
963
+ else {
964
+ const current = view.getSnapshot().viewport, target = wheel.target;
965
+ const dt = Math.max(0, time - wheel.time);
966
+ wheel.time = time;
967
+ const alpha = options.reducedMotion === true || reduced?.matches === true ? 1 : 1 - Math.exp(-dt / 40);
968
+ const span = viewportSpan(current), targetSpan = viewportSpan(target);
969
+ const nextSpan = span * Math.exp(Math.log(targetSpan / span) * alpha);
970
+ const blend = Math.abs(targetSpan - span) > 1e-9 ? (nextSpan - span) / (targetSpan - span) : alpha;
971
+ const next = { i0: current.i0 + (target.i0 - current.i0) * blend, i1: current.i1 + (target.i1 - current.i1) * blend };
972
+ const remainingPixels = Math.max(Math.abs(next.i0 - target.i0), Math.abs(next.i1 - target.i1)) * wheel.frame.width / Math.max(1, targetSpan - 1);
973
+ const settled = alpha === 1 || remainingPixels < 0.15 || time - wheel.lastInput >= 280;
974
+ pending = { next: settled ? target : next, frame: wheel.frame };
975
+ if (settled)
976
+ wheel = null;
977
+ }
978
+ }
979
+ if (coast !== null || wheel !== null)
980
+ schedule();
981
+ commit();
982
+ if (pointers.size === 0 && coast === null && wheel === null)
983
+ view.endNavigation();
984
+ });
985
+ };
986
+ const cancelScheduled = () => {
987
+ if (raf !== null)
988
+ frameScheduler.cancel(raf);
989
+ raf = null;
990
+ };
991
+ const flush = () => {
992
+ cancelScheduled();
993
+ wheel = null;
994
+ coast = null;
995
+ commit();
996
+ };
997
+ const stop = () => {
998
+ cancelScheduled();
999
+ pending = null;
1000
+ wheel = null;
1001
+ coast = null;
1002
+ velocity = 0;
1003
+ };
1004
+ const position = () => {
1005
+ const iterator = pointers.values(), a = iterator.next().value, b = iterator.next().value;
1006
+ return b === void 0 ? { x: a.x, distance: 0 } : { x: (a.x + b.x) / 2, distance: Math.hypot(b.x - a.x, b.y - a.y) };
1007
+ };
1008
+ const rebase = () => {
1009
+ const frame = readFrame();
1010
+ if (pointers.size === 0 || frame === null) {
1011
+ drag = null;
1012
+ return;
1013
+ }
1014
+ const p = position();
1015
+ view.beginNavigation();
1016
+ drag = { ...frame, start: { ...frame.observed }, x: p.x, distance: p.distance };
1017
+ sample = { x: p.x, time: win.performance.now() };
1018
+ velocity = 0;
1019
+ };
1020
+ const clear = () => {
1021
+ stop();
1022
+ cancelHold();
1023
+ drag = null;
1024
+ inspection = false;
1025
+ const ids = [...pointers.keys()];
1026
+ pointers.clear();
1027
+ for (const id of ids)
1028
+ if (element.hasPointerCapture(id))
1029
+ element.releasePointerCapture(id);
1030
+ view.endNavigation();
1031
+ cursor(null);
1032
+ };
1033
+ const on = (type, listener, passive = true) => {
1034
+ element.addEventListener(type, listener, { signal: abort.signal, passive });
1035
+ };
1036
+ on("pointerdown", (event) => {
1037
+ const e = event;
1038
+ if (e.button !== 0 || pointers.size >= 2 || editable(e.target) || readFrame() === null)
1039
+ return;
1040
+ flush();
1041
+ stop();
1042
+ cancelHold();
1043
+ if (pointers.size === 0)
1044
+ view.endNavigation();
1045
+ inspection = false;
1046
+ view.beginNavigation();
1047
+ const r = bounds(), point = { x: e.clientX - r.left, y: e.clientY - r.top };
1048
+ pointers.set(e.pointerId, point);
1049
+ element.setPointerCapture(e.pointerId);
1050
+ element.focus({ preventScroll: true });
1051
+ rebase();
1052
+ if (e.pointerType === "touch" && pointers.size === 1) {
1053
+ const id = e.pointerId;
1054
+ hold = {
1055
+ id,
1056
+ point,
1057
+ timer: win.setTimeout(() => {
1058
+ const current = pointers.get(id);
1059
+ if (hold?.id !== id || current === void 0 || pointers.size !== 1 || drag === null || !valid(drag)) {
1060
+ cancelHold();
1061
+ return;
1062
+ }
1063
+ hold = null;
1064
+ inspection = true;
1065
+ pending = null;
1066
+ velocity = 0;
1067
+ view.endNavigation();
1068
+ cursor(current.x);
1069
+ }, 450)
1070
+ };
1071
+ } else
1072
+ cursor(null);
1073
+ });
1074
+ const movePointer = (e, target = e.target) => {
1075
+ const r = bounds(), x = e.clientX - r.left, y = e.clientY - r.top;
1076
+ if (!pointers.has(e.pointerId)) {
1077
+ if (pointers.size === 0 && !inspection && !editable(target))
1078
+ cursor(x);
1079
+ return;
1080
+ }
1081
+ pointers.set(e.pointerId, { x, y });
1082
+ if (hold !== null) {
1083
+ if (Math.hypot(x - hold.point.x, y - hold.point.y) <= 8)
1084
+ return;
1085
+ cancelHold();
1086
+ }
1087
+ if (inspection && pointers.size === 1) {
1088
+ cursor(x);
1089
+ return;
1090
+ }
1091
+ if (drag === null || !valid(drag)) {
1092
+ pending = null;
1093
+ rebase();
1094
+ return;
1095
+ }
1096
+ const p = position(), now = eventTime(e);
1097
+ const factor = pointers.size === 2 && drag.distance > 1 ? Math.max(1, p.distance) / drag.distance : 1;
1098
+ const anchor = Math.max(0, Math.min(1, (drag.x - drag.left) / drag.width));
1099
+ const zoomed = zoomViewport(drag.start, drag.length, factor, anchor, view.getLimits());
1100
+ const delta = (drag.x - p.x) / drag.width * Math.max(0, viewportSpan(zoomed) - 1);
1101
+ pending = { next: { i0: zoomed.i0 + delta, i1: zoomed.i1 + delta }, frame: drag };
1102
+ const elapsed = now - sample.time;
1103
+ if (pointers.size === 1 && elapsed > 0) {
1104
+ const instantaneous = Math.max(-3, Math.min(3, (p.x - sample.x) / elapsed));
1105
+ if (elapsed > 80 || instantaneous * velocity < 0)
1106
+ velocity = 0;
1107
+ velocity += (instantaneous - velocity) * (1 - Math.exp(-elapsed / 30));
1108
+ } else if (pointers.size !== 1)
1109
+ velocity = 0;
1110
+ sample = { x: p.x, time: now };
1111
+ schedule();
1112
+ cursor(p.x);
1113
+ };
1114
+ on("pointermove", (event) => {
1115
+ const e = event, coalesced = e.getCoalescedEvents?.();
1116
+ const latest = coalesced === void 0 || coalesced.length === 0 ? e : coalesced[coalesced.length - 1];
1117
+ movePointer(latest, e.target);
1118
+ });
1119
+ on("pointerup", (event) => {
1120
+ const e = event;
1121
+ if (!pointers.has(e.pointerId))
1122
+ return;
1123
+ const last = pointers.get(e.pointerId), r = bounds();
1124
+ if (last.x !== e.clientX - r.left || last.y !== e.clientY - r.top)
1125
+ movePointer(e);
1126
+ const wasSingle = pointers.size === 1, recent = win.performance.now() - sample.time < 80;
1127
+ const currentDrag = drag !== null && valid(drag);
1128
+ flush();
1129
+ cancelHold();
1130
+ pointers.delete(e.pointerId);
1131
+ if (element.hasPointerCapture(e.pointerId))
1132
+ element.releasePointerCapture(e.pointerId);
1133
+ if (pointers.size > 0) {
1134
+ rebase();
1135
+ return;
1136
+ }
1137
+ const frame = readFrame(), speed = velocity;
1138
+ drag = null;
1139
+ if (!inspection && wasSingle && currentDrag && recent && frame !== null && options.inertia === true && options.reducedMotion !== true && reduced?.matches !== true && Math.abs(speed) > 0.05) {
1140
+ coast = { frame, start: { ...frame.observed }, velocity: speed, started: win.performance.now() };
1141
+ schedule();
1142
+ } else
1143
+ view.endNavigation();
1144
+ if (e.pointerType !== "mouse" && !inspection)
1145
+ cursor(null);
1146
+ });
1147
+ on("pointercancel", clear);
1148
+ on("lostpointercapture", (event) => {
1149
+ if (pointers.has(event.pointerId))
1150
+ clear();
1151
+ });
1152
+ on("pointerleave", () => {
1153
+ if (pointers.size === 0 && !inspection)
1154
+ cursor(null);
1155
+ });
1156
+ on("blur", clear);
1157
+ on("focusin", (event) => {
1158
+ if (editable(event.target))
1159
+ clear();
1160
+ });
1161
+ win.addEventListener("blur", clear, { signal: abort.signal });
1162
+ win.addEventListener("resize", invalidateRect, { signal: abort.signal });
1163
+ doc.addEventListener("scroll", invalidateRect, { signal: abort.signal, capture: true, passive: true });
1164
+ doc.addEventListener("visibilitychange", () => {
1165
+ if (doc.hidden)
1166
+ clear();
1167
+ }, { signal: abort.signal });
1168
+ on("wheel", (event) => {
1169
+ const e = event, frame = readFrame();
1170
+ if (frame === null || editable(e.target) || pointers.size > 0)
1171
+ return;
1172
+ const unitX = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? frame.width : 1;
1173
+ const unitY = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? element.clientHeight : 1;
1174
+ const dx = e.deltaX * unitX, dy = e.deltaY * unitY, now = win.performance.now();
1175
+ const locked = wheel !== null && now - wheel.lastInput < 160 ? wheel.mode : null;
1176
+ const mode = e.ctrlKey ? "zoom" : e.shiftKey ? "pan" : locked ?? (Math.abs(dx) > Math.abs(dy) * 1.2 ? "pan" : "zoom");
1177
+ const raw = mode === "pan" ? e.shiftKey && dx === 0 ? dy : dx : dy;
1178
+ const delta = Math.max(-400, Math.min(400, raw));
1179
+ if (!Number.isFinite(delta) || delta === 0)
1180
+ return;
1181
+ e.preventDefault();
1182
+ if (coast !== null) {
1183
+ flush();
1184
+ view.endNavigation();
1185
+ }
1186
+ coast = null;
1187
+ cancelHold();
1188
+ inspection = false;
1189
+ const direction = Math.sign(delta);
1190
+ if (wheel === null || !valid(wheel.frame) || wheel.direction !== direction || wheel.mode !== mode) {
1191
+ if (wheel !== null && wheel.mode !== mode)
1192
+ view.endNavigation();
1193
+ view.beginNavigation();
1194
+ wheel = { mode, target: { ...frame.observed }, direction, frame, time: now, lastInput: now };
1195
+ }
1196
+ const x = e.clientX - bounds().left;
1197
+ if (mode === "zoom")
1198
+ wheel.target = zoomViewport(wheel.target, frame.length, Math.exp(-delta * 2e-3), (x - frame.left) / frame.width, view.getLimits());
1199
+ else {
1200
+ const bars = delta / frame.width * Math.max(0, viewportSpan(wheel.target) - 1);
1201
+ wheel.target = clampViewport({ i0: wheel.target.i0 + bars, i1: wheel.target.i1 + bars }, frame.length, view.getLimits());
1202
+ }
1203
+ wheel.lastInput = now;
1204
+ schedule();
1205
+ cursor(x);
1206
+ }, false);
1207
+ on("keydown", (event) => {
1208
+ const e = event;
1209
+ if (editable(e.target) || e.ctrlKey || e.metaKey || e.isComposing)
1210
+ return;
1211
+ if (e.altKey) {
1212
+ if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
1213
+ flush();
1214
+ clear();
1215
+ if (e.key === "ArrowLeft")
1216
+ view.undoNavigation();
1217
+ else
1218
+ view.redoNavigation();
1219
+ e.preventDefault();
1220
+ }
1221
+ return;
1222
+ }
1223
+ if (e.key === "Escape") {
1224
+ clear();
1225
+ e.preventDefault();
1226
+ return;
1227
+ }
1228
+ if (!["ArrowLeft", "ArrowRight", "PageUp", "PageDown", "+", "=", "-", "Home"].includes(e.key))
1229
+ return;
1230
+ flush();
1231
+ clear();
1232
+ const span = viewportSpan(view.getSnapshot().viewport);
1233
+ switch (e.key) {
1234
+ case "ArrowLeft":
1235
+ view.pan(-10);
1236
+ break;
1237
+ case "ArrowRight":
1238
+ view.pan(10);
1239
+ break;
1240
+ case "PageUp":
1241
+ view.pan(-Math.round(span * 0.8));
1242
+ break;
1243
+ case "PageDown":
1244
+ view.pan(Math.round(span * 0.8));
1245
+ break;
1246
+ case "+":
1247
+ case "=":
1248
+ view.zoom(1.5);
1249
+ break;
1250
+ case "-":
1251
+ view.zoom(1 / 1.5);
1252
+ break;
1253
+ case "Home":
1254
+ view.followLatest();
1255
+ break;
1256
+ }
1257
+ const snapshot = view.getSnapshot();
1258
+ if (e.key !== "Home" && snapshot.viewport.i0 === 0 && snapshot.hasMore)
1259
+ view.requestMore();
1260
+ e.preventDefault();
1261
+ }, false);
1262
+ const cleanup = () => {
1263
+ if (disposed)
1264
+ return;
1265
+ disposed = true;
1266
+ abort.abort();
1267
+ clear();
1268
+ if (rectFrame !== null)
1269
+ frameScheduler.cancel(rectFrame);
1270
+ rectFrame = null;
1271
+ rect = null;
1272
+ element.style.touchAction = oldTouchAction;
1273
+ };
1274
+ return Object.assign(cleanup, { cancelInput: clear });
1275
+ }
1276
+
1277
+ // packages/bindings/web/src/drawingInteraction.ts
1278
+ import { drawingAnchorCount, drawingUsesText } from "@supersolutions/super-chart-core";
1279
+
1280
+ // packages/bindings/web/src/freehandDrawing.ts
1281
+ import { MAX_FREEHAND_POINTS as MAX_POINTS } from "@supersolutions/super-chart-core";
1282
+ import { projectFreehandDrawing } from "@supersolutions/super-chart-core";
1283
+ var sequence = 0;
1284
+ var validAnchor = (point) => point !== null && Number.isFinite(point.timestamp) && Math.abs(point.timestamp) <= 864e13 && Number.isFinite(point.price);
1285
+ function attachFreehandDrawingInput(element, store, getFrame, onPreview, onToolComplete, onInteractionStart) {
1286
+ const doc = element.ownerDocument, win = doc.defaultView;
1287
+ if (win === null)
1288
+ return { setTool: () => {
1289
+ }, dispose: () => {
1290
+ } };
1291
+ const abort = new win.AbortController();
1292
+ let tool = null, disposed = false, frameId = 0, generation = 0;
1293
+ let stroke = null;
1294
+ const consume = (event) => {
1295
+ event.preventDefault();
1296
+ event.stopImmediatePropagation();
1297
+ };
1298
+ const editable = (target) => target instanceof win.Element && target.closest('input,textarea,select,button,a,[contenteditable]:not([contenteditable="false"])') !== null;
1299
+ const frame = () => {
1300
+ const current = getFrame(), rect = element.getBoundingClientRect(), g = current?.model.geometry;
1301
+ return current !== null && current.snapshot.length > 0 && rect.width > 0 && rect.height > 0 && g !== void 0 && [g.left, g.right, g.top, g.bottom].every(Number.isFinite) && g.right > g.left && g.bottom > g.top ? current : null;
1302
+ };
1303
+ const release = (id) => {
1304
+ if (element.hasPointerCapture(id))
1305
+ element.releasePointerCapture(id);
1306
+ };
1307
+ const cancel = () => {
1308
+ generation++;
1309
+ if (frameId !== 0)
1310
+ win.cancelAnimationFrame(frameId);
1311
+ frameId = 0;
1312
+ const active = stroke;
1313
+ stroke = null;
1314
+ if (active !== null)
1315
+ release(active.pointerId);
1316
+ onPreview(null);
1317
+ };
1318
+ const preview = () => {
1319
+ const current = frame(), active = stroke;
1320
+ if (active === null || current === null || active.points.length < 2)
1321
+ return null;
1322
+ const anchors = [];
1323
+ for (const point2 of active.points) {
1324
+ const anchor = current.unproject(point2.x, point2.y);
1325
+ if (!validAnchor(anchor))
1326
+ return null;
1327
+ const projected = current.project(anchor.timestamp, anchor.price);
1328
+ if (projected === null || Math.abs(projected.x - point2.x) > 0.5 || Math.abs(projected.y - point2.y) > 0.5)
1329
+ return null;
1330
+ anchors.push(Object.freeze({ ...anchor }));
1331
+ }
1332
+ return Object.freeze({ id: active.id, type: active.type, anchors: Object.freeze(anchors), locked: false, hidden: false, strokeWidth: active.type === "highlighter" ? 16 : 2 });
1333
+ };
1334
+ const publish = () => {
1335
+ frameId = 0;
1336
+ if (!disposed && stroke !== null)
1337
+ onPreview(preview());
1338
+ };
1339
+ const point = (event) => {
1340
+ const current = frame();
1341
+ if (current === null)
1342
+ return null;
1343
+ const rect = element.getBoundingClientRect(), g = current.model.geometry;
1344
+ const x = event.clientX - rect.left, y = event.clientY - rect.top;
1345
+ if (!Number.isFinite(x) || !Number.isFinite(y))
1346
+ return null;
1347
+ return { x: Math.max(g.left, Math.min(g.right, x)), y: Math.max(g.top, Math.min(g.bottom, y)) };
1348
+ };
1349
+ const sample = (event, force = false) => {
1350
+ const active = stroke, next = point(event);
1351
+ if (active === null || next === null)
1352
+ return;
1353
+ const last = active.points[active.points.length - 1], distance = Math.hypot(next.x - last.x, next.y - last.y);
1354
+ if (distance < (force ? 0.01 : active.spacing))
1355
+ return;
1356
+ if (active.points.length >= MAX_POINTS) {
1357
+ active.points = active.points.filter((_, index) => index % 2 === 0 || index === active.points.length - 1);
1358
+ active.spacing *= 2;
1359
+ }
1360
+ active.points.push(next);
1361
+ };
1362
+ const on = (type, callback) => {
1363
+ element.addEventListener(type, callback, { capture: true, passive: false, signal: abort.signal });
1364
+ };
1365
+ on("pointerdown", (event) => {
1366
+ const e = event;
1367
+ if (tool === null || editable(e.target) || e.button !== 0)
1368
+ return;
1369
+ if (stroke !== null) {
1370
+ consume(e);
1371
+ return;
1372
+ }
1373
+ const current = frame(), rect = element.getBoundingClientRect(), g = current?.model.geometry;
1374
+ if (current === null || g === void 0 || e.clientX - rect.left < g.left || e.clientX - rect.left > g.right || e.clientY - rect.top < g.top || e.clientY - rect.top > g.bottom)
1375
+ return;
1376
+ const epoch = generation, selectedTool = tool;
1377
+ consume(e);
1378
+ onInteractionStart?.();
1379
+ if (disposed || epoch !== generation || tool !== selectedTool)
1380
+ return;
1381
+ const first = point(e);
1382
+ if (first === null)
1383
+ return;
1384
+ let id;
1385
+ do {
1386
+ id = `stroke-${Date.now().toString(36)}-${(++sequence).toString(36)}`;
1387
+ } while (store.getSnapshot().drawings.some((item) => item.id === id));
1388
+ stroke = { id, type: selectedTool, pointerId: e.pointerId, points: [first], spacing: 2, original: store.getSnapshot().drawings };
1389
+ try {
1390
+ element.setPointerCapture(e.pointerId);
1391
+ } catch {
1392
+ cancel();
1393
+ return;
1394
+ }
1395
+ element.focus({ preventScroll: true });
1396
+ });
1397
+ on("pointermove", (event) => {
1398
+ const e = event;
1399
+ if (stroke === null)
1400
+ return;
1401
+ consume(e);
1402
+ if (e.pointerId !== stroke.pointerId)
1403
+ return;
1404
+ const samples = typeof e.getCoalescedEvents === "function" ? e.getCoalescedEvents() : [];
1405
+ const step = Math.max(1, Math.ceil(samples.length / 64));
1406
+ for (let i = 0; i < samples.length; i += step)
1407
+ sample(samples[i]);
1408
+ sample(e);
1409
+ if (frameId === 0)
1410
+ frameId = win.requestAnimationFrame(publish);
1411
+ });
1412
+ on("pointerup", (event) => {
1413
+ const e = event;
1414
+ if (stroke === null)
1415
+ return;
1416
+ consume(e);
1417
+ if (e.pointerId !== stroke.pointerId)
1418
+ return;
1419
+ sample(e, true);
1420
+ const drawing = preview(), active = stroke, epoch = generation + 1;
1421
+ cancel();
1422
+ if (disposed || epoch !== generation || drawing === null || active === null || tool !== active.type || store.getSnapshot().drawings !== active.original || active.points.every((point2) => Math.hypot(point2.x - active.points[0].x, point2.y - active.points[0].y) < 2))
1423
+ return;
1424
+ store.add(drawing);
1425
+ store.select(drawing.id);
1426
+ tool = null;
1427
+ onToolComplete?.();
1428
+ });
1429
+ on("pointercancel", (event) => {
1430
+ if (stroke?.pointerId === event.pointerId) {
1431
+ consume(event);
1432
+ cancel();
1433
+ }
1434
+ });
1435
+ on("lostpointercapture", (event) => {
1436
+ if (stroke?.pointerId === event.pointerId)
1437
+ cancel();
1438
+ });
1439
+ on("keydown", (event) => {
1440
+ const key = event;
1441
+ if (key.key === "Escape" && !key.isComposing && !editable(key.target) && (stroke !== null || tool !== null)) {
1442
+ consume(key);
1443
+ cancel();
1444
+ tool = null;
1445
+ onToolComplete?.();
1446
+ }
1447
+ });
1448
+ on("blur", (event) => {
1449
+ const next = event.relatedTarget;
1450
+ if (!(next instanceof win.Node) || !element.contains(next))
1451
+ cancel();
1452
+ });
1453
+ win.addEventListener("blur", cancel, { signal: abort.signal });
1454
+ doc.addEventListener("visibilitychange", () => {
1455
+ if (doc.hidden)
1456
+ cancel();
1457
+ }, { signal: abort.signal });
1458
+ const unsubscribe = store.subscribe?.(() => {
1459
+ if (stroke !== null && store.getSnapshot().drawings !== stroke.original)
1460
+ cancel();
1461
+ });
1462
+ return {
1463
+ setTool: (value) => {
1464
+ if (disposed)
1465
+ return;
1466
+ if (value !== null && value !== "brush" && value !== "highlighter")
1467
+ throw new TypeError("Unknown freehand tool");
1468
+ cancel();
1469
+ tool = value;
1470
+ },
1471
+ dispose: () => {
1472
+ if (disposed)
1473
+ return;
1474
+ disposed = true;
1475
+ abort.abort();
1476
+ unsubscribe?.();
1477
+ cancel();
1478
+ tool = null;
1479
+ }
1480
+ };
1481
+ }
1482
+
1483
+ // packages/bindings/web/src/drawingInteraction.ts
1484
+ var nextId = 0;
1485
+ function attachDrawingInteraction(element, store, getFrame, onPreview, onToolComplete, onInteractionStart, imageRenderer) {
1486
+ const doc = element.ownerDocument, win = doc.defaultView;
1487
+ if (win === null)
1488
+ return { setTool: () => {
1489
+ }, dispose: () => {
1490
+ } };
1491
+ const abort = new win.AbortController();
1492
+ let tool = null;
1493
+ let drag = null;
1494
+ let placement = null;
1495
+ let editor = null;
1496
+ let media = null;
1497
+ let disposed = false;
1498
+ const editable = (target) => target instanceof win.Element && target.closest('input,textarea,select,button,a,[contenteditable]:not([contenteditable="false"])') !== null;
1499
+ const consume = (event) => {
1500
+ event.preventDefault();
1501
+ event.stopImmediatePropagation();
1502
+ };
1503
+ const point = (event) => {
1504
+ const rect = element.getBoundingClientRect();
1505
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
1506
+ };
1507
+ const frame = () => {
1508
+ const value = getFrame(), g = value?.model.geometry, rect = element.getBoundingClientRect();
1509
+ return rect.width > 0 && rect.height > 0 && value !== null && value.snapshot.length > 0 && g !== void 0 && [g.left, g.right, g.top, g.bottom].every(Number.isFinite) && g.left < g.right && g.top < g.bottom ? value : null;
1510
+ };
1511
+ const anchor = (value) => value !== null && Number.isFinite(value.timestamp) && Math.abs(value.timestamp) <= 864e13 && Number.isFinite(value.price);
1512
+ const release = (id) => {
1513
+ if (element.hasPointerCapture(id))
1514
+ element.releasePointerCapture(id);
1515
+ };
1516
+ const cancel = () => {
1517
+ const previous = drag;
1518
+ drag = null;
1519
+ placement = null;
1520
+ if (previous !== null)
1521
+ release(previous.pointerId);
1522
+ editor?.close();
1523
+ if (media !== null) {
1524
+ media.abort.abort();
1525
+ media.input.remove();
1526
+ media = null;
1527
+ }
1528
+ onPreview(null);
1529
+ };
1530
+ const finishTool = () => {
1531
+ if (tool === null)
1532
+ return;
1533
+ tool = null;
1534
+ onToolComplete?.();
1535
+ };
1536
+ const freehand = attachFreehandDrawingInput(element, store, getFrame, onPreview, onToolComplete, onInteractionStart);
1537
+ const current = (value) => value.original === null || store.getSnapshot().drawings.find((item) => item.id === value.original?.id) === value.original;
1538
+ const setPlacementAnchor = (preview, index, next) => ({
1539
+ ...preview,
1540
+ anchors: preview.anchors.map((item, i) => i >= index ? next : item)
1541
+ });
1542
+ const editText = (preview, original) => {
1543
+ editor?.close();
1544
+ const currentFrame = frame(), first = preview.anchors[preview.type === "callout" ? 1 : 0];
1545
+ const position = currentFrame?.project(first.timestamp, first.price);
1546
+ if (position === void 0 || position === null) {
1547
+ onPreview(null);
1548
+ finishTool();
1549
+ return;
1550
+ }
1551
+ const input = doc.createElement("textarea");
1552
+ input.setAttribute("aria-label", "Drawing text");
1553
+ input.placeholder = "Text \xB7 Enter to save \xB7 Shift+Enter for a new line";
1554
+ input.maxLength = 512;
1555
+ input.rows = 3;
1556
+ input.value = preview.text ?? "";
1557
+ const width = Math.min(260, element.clientWidth), height = Math.min(100, element.clientHeight);
1558
+ input.style.cssText = `position:absolute;z-index:4;box-sizing:border-box;resize:none;width:${width}px;height:${height}px;left:${Math.max(0, Math.min(position.x, element.clientWidth - width))}px;top:${Math.max(0, Math.min(position.y, element.clientHeight - height))}px;padding:6px;background:#10151d;color:#eee;border:1px solid #8bafff;font:12px system-ui;`;
1559
+ let closed = false;
1560
+ const close = () => {
1561
+ if (closed)
1562
+ return;
1563
+ closed = true;
1564
+ if (editor?.input === input)
1565
+ editor = null;
1566
+ input.remove();
1567
+ onPreview(null);
1568
+ };
1569
+ const commit = () => {
1570
+ if (closed)
1571
+ return;
1572
+ if (original !== null && store.getSnapshot().drawings.find((item) => item.id === original.id) !== original) {
1573
+ close();
1574
+ return;
1575
+ }
1576
+ try {
1577
+ const content = input.value;
1578
+ if (original === null) {
1579
+ store.add({ ...preview, text: content });
1580
+ store.select(preview.id);
1581
+ } else
1582
+ store.update(original.id, { text: content });
1583
+ } catch (error) {
1584
+ input.setCustomValidity(error instanceof Error ? error.message : "Invalid drawing text");
1585
+ input.reportValidity();
1586
+ return;
1587
+ }
1588
+ close();
1589
+ finishTool();
1590
+ element.focus({ preventScroll: true });
1591
+ };
1592
+ editor = { input, original, close };
1593
+ element.append(input);
1594
+ input.addEventListener("input", () => {
1595
+ input.setCustomValidity("");
1596
+ onPreview({ ...preview, text: input.value });
1597
+ });
1598
+ input.addEventListener("keydown", (event) => {
1599
+ event.stopPropagation();
1600
+ if (event.key === "Escape") {
1601
+ event.preventDefault();
1602
+ close();
1603
+ finishTool();
1604
+ element.focus({ preventScroll: true });
1605
+ } else if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
1606
+ event.preventDefault();
1607
+ commit();
1608
+ }
1609
+ });
1610
+ input.addEventListener("blur", () => {
1611
+ close();
1612
+ finishTool();
1613
+ });
1614
+ onPreview(preview);
1615
+ input.focus();
1616
+ input.select();
1617
+ };
1618
+ const insertImage = (preview) => {
1619
+ const input = doc.createElement("input"), controller = new win.AbortController();
1620
+ input.type = "file";
1621
+ input.accept = "image/png,image/jpeg,image/webp";
1622
+ input.hidden = true;
1623
+ media = { abort: controller, input };
1624
+ element.append(input);
1625
+ const finish = () => {
1626
+ input.remove();
1627
+ if (media?.abort === controller) {
1628
+ media = null;
1629
+ onPreview(null);
1630
+ finishTool();
1631
+ }
1632
+ };
1633
+ input.addEventListener("cancel", finish, { once: true });
1634
+ input.addEventListener("change", () => {
1635
+ const file = input.files?.[0];
1636
+ if (file === void 0) {
1637
+ finish();
1638
+ return;
1639
+ }
1640
+ const decoder = imageRenderer ?? new DrawingImageRenderer(async (blob) => await win.createImageBitmap(blob));
1641
+ let source;
1642
+ void (async () => {
1643
+ try {
1644
+ const image = await createDrawingImage(file, controller.signal);
1645
+ source = image;
1646
+ await decoder.prepare(image);
1647
+ if (disposed || controller.signal.aborted || media?.abort !== controller)
1648
+ return;
1649
+ store.add({ ...preview, image });
1650
+ store.select(preview.id);
1651
+ } catch (error) {
1652
+ if (!controller.signal.aborted) {
1653
+ console.warn("[super-chart] Image drawing was not saved", error);
1654
+ element.dispatchEvent(new win.CustomEvent("drawingerror", { bubbles: true, detail: error }));
1655
+ }
1656
+ } finally {
1657
+ if (imageRenderer === void 0)
1658
+ decoder.dispose();
1659
+ else if (source !== void 0 && !store.getSnapshot().drawings.some((item) => item.image === source))
1660
+ decoder.release(source);
1661
+ finish();
1662
+ }
1663
+ })();
1664
+ }, { once: true });
1665
+ input.click();
1666
+ };
1667
+ const completeWave = () => {
1668
+ if (placement?.preview.type !== "any-waves")
1669
+ return;
1670
+ const value = placement.preview, committed = value.anchors.slice(0, placement.nextAnchor).filter((point2, i, all) => i === 0 || point2.timestamp !== all[i - 1].timestamp || point2.price !== all[i - 1].price);
1671
+ if (committed.length < 2)
1672
+ return;
1673
+ placement = null;
1674
+ store.add({ ...value, anchors: committed });
1675
+ store.select(value.id);
1676
+ onPreview(null);
1677
+ finishTool();
1678
+ };
1679
+ const updatePreview = (event) => {
1680
+ const value = drag, currentFrame = frame();
1681
+ if (value === null)
1682
+ return false;
1683
+ if (value.preview.locked)
1684
+ return true;
1685
+ if (currentFrame === null) {
1686
+ cancel();
1687
+ return false;
1688
+ }
1689
+ if (!current(value)) {
1690
+ cancel();
1691
+ return false;
1692
+ }
1693
+ const p = point(event), dx = p.x - value.start.x, dy = p.y - value.start.y;
1694
+ if (!Number.isFinite(dx) || !Number.isFinite(dy))
1695
+ return false;
1696
+ if (Math.hypot(dx, dy) > 2)
1697
+ value.moved = true;
1698
+ if (value.original !== null && !value.moved)
1699
+ return true;
1700
+ let anchors;
1701
+ if (value.original === null) {
1702
+ const next = currentFrame.unproject(p.x, p.y);
1703
+ if (!anchor(next))
1704
+ return false;
1705
+ anchors = drawingAnchorCount(value.preview.type) === 1 ? [next] : setPlacementAnchor(value.preview, value.anchorIndex ?? 1, next).anchors;
1706
+ } else if (value.anchorIndex !== null) {
1707
+ const next = currentFrame.unproject(p.x, p.y);
1708
+ if (!anchor(next))
1709
+ return false;
1710
+ anchors = value.original.anchors.map((item, i) => i === value.anchorIndex ? next : item);
1711
+ } else {
1712
+ const moved = [];
1713
+ for (const projected of value.projected) {
1714
+ const x = projected.x + dx, y = projected.y + dy;
1715
+ const next = currentFrame.unproject(x, y);
1716
+ if (!anchor(next))
1717
+ return false;
1718
+ const checked = currentFrame.project(next.timestamp, next.price);
1719
+ if (checked === null || Math.abs(checked.x - x) > 0.5 || Math.abs(checked.y - y) > 0.5)
1720
+ return false;
1721
+ moved.push(next);
1722
+ }
1723
+ anchors = moved;
1724
+ }
1725
+ value.preview = { ...value.preview, anchors };
1726
+ onPreview(value.preview);
1727
+ return true;
1728
+ };
1729
+ const on = (type, listener) => {
1730
+ element.addEventListener(type, listener, { capture: true, passive: false, signal: abort.signal });
1731
+ };
1732
+ on("pointerdown", (event) => {
1733
+ const e = event;
1734
+ if (editor !== null && editable(e.target))
1735
+ return;
1736
+ if (media !== null) {
1737
+ consume(e);
1738
+ return;
1739
+ }
1740
+ if (drag !== null) {
1741
+ consume(e);
1742
+ return;
1743
+ }
1744
+ if (e.button !== 0 || editable(e.target))
1745
+ return;
1746
+ if (tool !== null) {
1747
+ consume(e);
1748
+ onInteractionStart?.();
1749
+ }
1750
+ const currentFrame = frame();
1751
+ if (currentFrame === null)
1752
+ return;
1753
+ const p = point(e), g = currentFrame.model.geometry;
1754
+ const inside = p.x >= g.left && p.x <= g.right && p.y >= g.top && p.y <= g.bottom;
1755
+ if (tool !== null) {
1756
+ if (!inside)
1757
+ return;
1758
+ const first = currentFrame.unproject(p.x, p.y);
1759
+ if (!anchor(first))
1760
+ return;
1761
+ let preview, nextAnchor = null;
1762
+ if (placement !== null) {
1763
+ nextAnchor = placement.nextAnchor;
1764
+ preview = setPlacementAnchor(placement.preview, nextAnchor, first);
1765
+ placement = null;
1766
+ } else {
1767
+ let id;
1768
+ do {
1769
+ id = `drawing-${Date.now().toString(36)}-${(++nextId).toString(36)}`;
1770
+ } while (store.getSnapshot().drawings.some((item) => item.id === id));
1771
+ preview = { id, type: tool, anchors: Array.from({ length: drawingAnchorCount(tool) }, () => first), locked: false, hidden: false };
1772
+ }
1773
+ drag = { pointerId: e.pointerId, start: p, original: null, preview, projected: [], anchorIndex: nextAnchor, moved: false };
1774
+ onPreview(preview);
1775
+ } else {
1776
+ const hit = inside ? store.hitTest(p, currentFrame.project, g, e.pointerType === "touch" ? 10 : 6, currentFrame.drawingContext) : null;
1777
+ if (hit === null) {
1778
+ store.select(null);
1779
+ return;
1780
+ }
1781
+ const original = store.getSnapshot().drawings.find((item) => item.id === hit.id);
1782
+ if (original === void 0)
1783
+ return;
1784
+ store.select(hit.id);
1785
+ if (original.locked)
1786
+ return;
1787
+ consume(e);
1788
+ onInteractionStart?.();
1789
+ const projected = [];
1790
+ for (const item of original.anchors) {
1791
+ const position = currentFrame.project(item.timestamp, item.price);
1792
+ if (position === null || !Number.isFinite(position.x) || !Number.isFinite(position.y))
1793
+ return;
1794
+ projected.push({ ...position });
1795
+ }
1796
+ drag = { pointerId: e.pointerId, start: p, original, preview: original, projected, anchorIndex: hit.anchorIndex, moved: false };
1797
+ }
1798
+ try {
1799
+ element.setPointerCapture(e.pointerId);
1800
+ } catch {
1801
+ cancel();
1802
+ return;
1803
+ }
1804
+ element.focus({ preventScroll: true });
1805
+ });
1806
+ on("pointermove", (event) => {
1807
+ const e = event;
1808
+ if (drag === null) {
1809
+ if (placement === null || editable(e.target))
1810
+ return;
1811
+ const p = point(e), next = frame()?.unproject(p.x, p.y);
1812
+ if (next !== void 0 && anchor(next)) {
1813
+ placement.preview = setPlacementAnchor(placement.preview, placement.nextAnchor, next);
1814
+ onPreview(placement.preview);
1815
+ }
1816
+ consume(e);
1817
+ return;
1818
+ }
1819
+ consume(e);
1820
+ if (e.pointerId === drag.pointerId)
1821
+ updatePreview(e);
1822
+ });
1823
+ on("pointerup", (event) => {
1824
+ if (drag === null)
1825
+ return;
1826
+ const e = event;
1827
+ consume(e);
1828
+ if (e.pointerId !== drag.pointerId)
1829
+ return;
1830
+ if (!updatePreview(e)) {
1831
+ cancel();
1832
+ return;
1833
+ }
1834
+ const value = drag;
1835
+ if (value === null)
1836
+ return;
1837
+ drag = null;
1838
+ release(value.pointerId);
1839
+ if (value.original === null && value.preview.type === "any-waves") {
1840
+ const nextAnchor = value.anchorIndex === null ? value.moved ? 2 : 1 : value.anchorIndex + 1;
1841
+ if (nextAnchor >= 256) {
1842
+ store.add(value.preview);
1843
+ store.select(value.preview.id);
1844
+ onPreview(null);
1845
+ finishTool();
1846
+ return;
1847
+ }
1848
+ const anchors = value.preview.anchors.slice(0, nextAnchor);
1849
+ anchors.push(anchors[anchors.length - 1]);
1850
+ placement = { preview: { ...value.preview, anchors }, nextAnchor };
1851
+ onPreview(placement.preview);
1852
+ return;
1853
+ }
1854
+ const count = drawingAnchorCount(value.preview.type);
1855
+ if (value.original === null && count > 2 && value.anchorIndex !== count - 1) {
1856
+ placement = { preview: value.preview, nextAnchor: value.anchorIndex === null ? value.moved ? 2 : 1 : value.anchorIndex + 1 };
1857
+ onPreview(value.preview);
1858
+ return;
1859
+ }
1860
+ if (value.original === null && drawingUsesText(value.preview.type)) {
1861
+ editText(value.preview, null);
1862
+ return;
1863
+ }
1864
+ if (value.original === null && value.preview.type === "image") {
1865
+ if (value.moved)
1866
+ insertImage(value.preview);
1867
+ else
1868
+ onPreview(null);
1869
+ return;
1870
+ }
1871
+ try {
1872
+ if (value.original === null) {
1873
+ if (!value.moved && drawingAnchorCount(value.preview.type) === 2)
1874
+ return;
1875
+ store.add(value.preview);
1876
+ store.select(value.preview.id);
1877
+ finishTool();
1878
+ } else if (value.moved && !value.original.locked && current(value))
1879
+ store.update(value.original.id, { anchors: value.preview.anchors });
1880
+ } finally {
1881
+ onPreview(null);
1882
+ }
1883
+ });
1884
+ on("dblclick", (event) => {
1885
+ if (placement?.preview.type === "any-waves") {
1886
+ consume(event);
1887
+ completeWave();
1888
+ return;
1889
+ }
1890
+ if (tool !== null || drag !== null || placement !== null || editable(event.target))
1891
+ return;
1892
+ const currentFrame = frame();
1893
+ if (currentFrame === null)
1894
+ return;
1895
+ const p = point(event), hit = store.hitTest(p, currentFrame.project, currentFrame.model.geometry, 6, currentFrame.drawingContext);
1896
+ const original = store.getSnapshot().drawings.find((item) => item.id === hit?.id);
1897
+ if (original === void 0 || !drawingUsesText(original.type) || original.locked)
1898
+ return;
1899
+ consume(event);
1900
+ store.select(original.id);
1901
+ onInteractionStart?.();
1902
+ editText(original, original);
1903
+ });
1904
+ on("pointercancel", (event) => {
1905
+ if (drag !== null && event.pointerId === drag.pointerId) {
1906
+ consume(event);
1907
+ cancel();
1908
+ }
1909
+ });
1910
+ on("lostpointercapture", (event) => {
1911
+ if (drag !== null && event.pointerId === drag.pointerId)
1912
+ cancel();
1913
+ });
1914
+ on("blur", (event) => {
1915
+ if (media !== null)
1916
+ return;
1917
+ const next = event.relatedTarget;
1918
+ if (!(next instanceof win.Node) || !element.contains(next))
1919
+ cancel();
1920
+ });
1921
+ win.addEventListener("blur", () => {
1922
+ if (media === null)
1923
+ cancel();
1924
+ }, { signal: abort.signal });
1925
+ doc.addEventListener("visibilitychange", () => {
1926
+ if (doc.hidden && media === null)
1927
+ cancel();
1928
+ }, { signal: abort.signal });
1929
+ on("keydown", (event) => {
1930
+ const e = event, active = doc.activeElement;
1931
+ if (editable(e.target) || e.isComposing || active === null || !element.contains(active))
1932
+ return;
1933
+ if (e.key === "Enter" && placement?.preview.type === "any-waves") {
1934
+ consume(e);
1935
+ completeWave();
1936
+ return;
1937
+ }
1938
+ if (e.key === "Escape" && (drag !== null || placement !== null || tool !== null)) {
1939
+ consume(e);
1940
+ cancel();
1941
+ finishTool();
1942
+ return;
1943
+ }
1944
+ const snapshot = store.getSnapshot(), modified = e.ctrlKey || e.metaKey;
1945
+ if (modified && !e.altKey) {
1946
+ const key = e.key.toLowerCase(), redo = key === "z" && e.shiftKey || key === "y" && !e.shiftKey;
1947
+ const undo = key === "z" && !e.shiftKey;
1948
+ if (redo && snapshot.canRedo || undo && snapshot.canUndo) {
1949
+ consume(e);
1950
+ cancel();
1951
+ freehand.setTool(null);
1952
+ if (redo)
1953
+ store.redo();
1954
+ else
1955
+ store.undo();
1956
+ }
1957
+ } else if (!e.altKey && !e.shiftKey && (e.key === "Delete" || e.key === "Backspace")) {
1958
+ const selected = snapshot.drawings.find((item) => item.id === snapshot.selectedId);
1959
+ if (selected !== void 0 && !selected.locked) {
1960
+ consume(e);
1961
+ cancel();
1962
+ store.remove(selected.id);
1963
+ }
1964
+ }
1965
+ });
1966
+ const unsubscribe = store.subscribe(() => {
1967
+ if (drag !== null && !current(drag))
1968
+ cancel();
1969
+ if (editor?.original !== null && editor?.original !== void 0 && store.getSnapshot().drawings.find((item) => item.id === editor?.original?.id) !== editor.original)
1970
+ editor.close();
1971
+ });
1972
+ return {
1973
+ setTool: (next) => {
1974
+ if (disposed)
1975
+ return;
1976
+ cancel();
1977
+ freehand.setTool(next === "brush" || next === "highlighter" ? next : null);
1978
+ tool = next === "brush" || next === "highlighter" ? null : next;
1979
+ },
1980
+ dispose: () => {
1981
+ if (disposed)
1982
+ return;
1983
+ disposed = true;
1984
+ abort.abort();
1985
+ unsubscribe();
1986
+ cancel();
1987
+ freehand.dispose();
1988
+ tool = null;
1989
+ }
1990
+ };
1991
+ }
1992
+
1993
+ // packages/bindings/web/src/drawingRenderer.ts
1994
+ function paintDrawings(ctx, geometries, selectedId, options = {}) {
1995
+ const lineWidth = options.lineWidth !== void 0 && Number.isFinite(options.lineWidth) && options.lineWidth > 0 ? options.lineWidth : 1.5;
1996
+ const radius = options.handleRadius !== void 0 && Number.isFinite(options.handleRadius) && options.handleRadius > 0 ? options.handleRadius : 4;
1997
+ const fillOpacity = options.fillOpacity !== void 0 && Number.isFinite(options.fillOpacity) ? Math.max(0, Math.min(1, options.fillOpacity)) : 0.1;
1998
+ ctx.save();
1999
+ try {
2000
+ ctx.globalAlpha = 1;
2001
+ ctx.globalCompositeOperation = "source-over";
2002
+ const bounds = options.bounds;
2003
+ if (bounds !== void 0) {
2004
+ if (![bounds.left, bounds.right, bounds.top, bounds.bottom].every(Number.isFinite) || bounds.left >= bounds.right || bounds.top >= bounds.bottom)
2005
+ return;
2006
+ ctx.beginPath();
2007
+ ctx.rect(bounds.left, bounds.top, bounds.right - bounds.left, bounds.bottom - bounds.top);
2008
+ ctx.clip();
2009
+ }
2010
+ ctx.lineWidth = lineWidth;
2011
+ ctx.lineCap = "round";
2012
+ ctx.lineJoin = "round";
2013
+ ctx.setLineDash([]);
2014
+ ctx.font = options.font ?? "11px system-ui";
2015
+ ctx.textBaseline = "bottom";
2016
+ for (const geometry of geometries) {
2017
+ const selected = geometry.id === selectedId;
2018
+ const color = selected ? options.selectedColor ?? "#c3d5ff" : options.color ?? "#8bafff";
2019
+ ctx.strokeStyle = color;
2020
+ for (const primitive of geometry.primitives) {
2021
+ if (primitive.kind === "image") {
2022
+ const status = options.imageRenderer?.paint(ctx, primitive.source, primitive.bounds, primitive.clip, options.onInvalidate);
2023
+ if (options.imageRenderer === void 0 || status === "unavailable") {
2024
+ ctx.save();
2025
+ try {
2026
+ const box = primitive.bounds, clip = primitive.clip;
2027
+ ctx.beginPath();
2028
+ ctx.rect(clip.left, clip.top, clip.right - clip.left, clip.bottom - clip.top);
2029
+ ctx.clip();
2030
+ ctx.strokeRect(box.left, box.top, box.right - box.left, box.bottom - box.top);
2031
+ ctx.fillStyle = color;
2032
+ ctx.font = "12px system-ui";
2033
+ ctx.textAlign = "left";
2034
+ ctx.textBaseline = "top";
2035
+ ctx.fillText("Image unavailable", box.left + 4, box.top + 4, Math.max(1, box.right - box.left - 8));
2036
+ } finally {
2037
+ ctx.restore();
2038
+ }
2039
+ }
2040
+ } else if (primitive.kind === "stroke") {
2041
+ ctx.save();
2042
+ try {
2043
+ const clip = primitive.clip;
2044
+ ctx.beginPath();
2045
+ ctx.rect(clip.left, clip.top, clip.right - clip.left, clip.bottom - clip.top);
2046
+ ctx.clip();
2047
+ ctx.lineWidth = primitive.width;
2048
+ ctx.globalAlpha = primitive.opacity;
2049
+ ctx.beginPath();
2050
+ primitive.points.forEach((point, i) => {
2051
+ if (i === 0)
2052
+ ctx.moveTo(point.x, point.y);
2053
+ else
2054
+ ctx.lineTo(point.x, point.y);
2055
+ });
2056
+ ctx.stroke();
2057
+ } finally {
2058
+ ctx.restore();
2059
+ }
2060
+ }
2061
+ }
2062
+ for (const primitive of geometry.primitives) {
2063
+ if (primitive.kind !== "rect" && primitive.kind !== "polygon")
2064
+ continue;
2065
+ ctx.fillStyle = options.fillColor ?? color;
2066
+ ctx.globalAlpha = fillOpacity;
2067
+ if (primitive.kind === "rect")
2068
+ ctx.fillRect(primitive.from.x, primitive.from.y, primitive.to.x - primitive.from.x, primitive.to.y - primitive.from.y);
2069
+ else {
2070
+ ctx.beginPath();
2071
+ primitive.points.forEach((point, index) => {
2072
+ if (index === 0)
2073
+ ctx.moveTo(point.x, point.y);
2074
+ else
2075
+ ctx.lineTo(point.x, point.y);
2076
+ });
2077
+ ctx.closePath();
2078
+ ctx.fill();
2079
+ }
2080
+ }
2081
+ for (const primitive of geometry.primitives) {
2082
+ if (primitive.kind !== "ellipse")
2083
+ continue;
2084
+ ctx.save();
2085
+ try {
2086
+ const clip = primitive.clip;
2087
+ ctx.beginPath();
2088
+ ctx.rect(clip.left, clip.top, clip.right - clip.left, clip.bottom - clip.top);
2089
+ ctx.clip();
2090
+ ctx.beginPath();
2091
+ ctx.ellipse(primitive.center.x, primitive.center.y, primitive.radiusX, primitive.radiusY, 0, 0, Math.PI * 2);
2092
+ ctx.fillStyle = options.fillColor ?? color;
2093
+ ctx.globalAlpha = fillOpacity;
2094
+ ctx.fill();
2095
+ ctx.globalAlpha = 1;
2096
+ ctx.stroke();
2097
+ } finally {
2098
+ ctx.restore();
2099
+ }
2100
+ }
2101
+ ctx.globalAlpha = 1;
2102
+ ctx.beginPath();
2103
+ for (const primitive of geometry.primitives) {
2104
+ if (primitive.kind !== "line")
2105
+ continue;
2106
+ ctx.moveTo(primitive.from.x, primitive.from.y);
2107
+ ctx.lineTo(primitive.to.x, primitive.to.y);
2108
+ }
2109
+ ctx.stroke();
2110
+ ctx.fillStyle = color;
2111
+ for (const primitive of geometry.primitives) {
2112
+ if (primitive.kind === "label") {
2113
+ ctx.save();
2114
+ try {
2115
+ const box = primitive.bounds, clip = primitive.clip;
2116
+ ctx.beginPath();
2117
+ ctx.rect(clip.left, clip.top, clip.right - clip.left, clip.bottom - clip.top);
2118
+ ctx.clip();
2119
+ if (primitive.boxed !== false) {
2120
+ ctx.fillStyle = options.handleFill ?? "#10151d";
2121
+ ctx.globalAlpha = 0.92;
2122
+ ctx.fillRect(box.left, box.top, box.right - box.left, box.bottom - box.top);
2123
+ ctx.globalAlpha = 1;
2124
+ ctx.strokeRect(box.left, box.top, box.right - box.left, box.bottom - box.top);
2125
+ }
2126
+ ctx.beginPath();
2127
+ ctx.rect(box.left, box.top, box.right - box.left, box.bottom - box.top);
2128
+ ctx.clip();
2129
+ ctx.fillStyle = color;
2130
+ ctx.font = "12px system-ui";
2131
+ ctx.textAlign = "left";
2132
+ ctx.textBaseline = "top";
2133
+ const lines = primitive.price === void 0 ? primitive.lines : [options.formatPrice?.(primitive.price) ?? String(primitive.price)];
2134
+ lines.forEach((text2, index) => ctx.fillText(text2, box.left + 6, box.top + 4 + index * 16, box.right - box.left - 12));
2135
+ } finally {
2136
+ ctx.restore();
2137
+ }
2138
+ continue;
2139
+ }
2140
+ if (primitive.kind !== "text")
2141
+ continue;
2142
+ ctx.textAlign = primitive.align;
2143
+ const text = primitive.price === void 0 ? primitive.text : `${primitive.text} ${options.formatPrice?.(primitive.price) ?? String(primitive.price)}`;
2144
+ ctx.fillText(text, primitive.position.x, primitive.position.y - 3);
2145
+ }
2146
+ if (!selected || geometry.locked)
2147
+ continue;
2148
+ ctx.fillStyle = options.handleFill ?? "#10151d";
2149
+ ctx.beginPath();
2150
+ for (const handle of geometry.handles) {
2151
+ ctx.moveTo(handle.point.x + radius, handle.point.y);
2152
+ ctx.arc(handle.point.x, handle.point.y, radius, 0, Math.PI * 2);
2153
+ }
2154
+ ctx.fill();
2155
+ ctx.stroke();
2156
+ }
2157
+ } finally {
2158
+ ctx.restore();
2159
+ }
2160
+ }
2161
+
2162
+ // packages/bindings/web/src/paneInteraction.ts
2163
+ import { zoomContinuousViewport } from "@supersolutions/super-chart-core";
2164
+ function attachPaneInteraction(element, frame, layout, setLayout, cancelNavigation, view, scheduler) {
2165
+ const win = element.ownerDocument.defaultView;
2166
+ if (win === null)
2167
+ return () => {
2168
+ };
2169
+ const frames = scheduler ?? { request: (callback) => win.requestAnimationFrame(callback), cancel: (id) => win.cancelAnimationFrame(id) };
2170
+ const abort = new win.AbortController(), originalCursor = element.style.cursor;
2171
+ let drag = null, pending = null;
2172
+ let raf = null, rectFrame = null, rect = null;
2173
+ const invalidateRect = () => {
2174
+ rect = null;
2175
+ };
2176
+ const bounds = () => {
2177
+ if (rect === null)
2178
+ rect = element.getBoundingClientRect();
2179
+ if (rectFrame === null)
2180
+ rectFrame = frames.request(() => {
2181
+ rectFrame = null;
2182
+ rect = null;
2183
+ });
2184
+ return rect;
2185
+ };
2186
+ const point = (event) => {
2187
+ const r = bounds();
2188
+ return { x: event.clientX - r.left, y: event.clientY - r.top };
2189
+ };
2190
+ const area = (value, x, y) => {
2191
+ for (let i = 1; i < value.panes.length; i++)
2192
+ if (Math.abs(y - value.panes[i].top) <= 5)
2193
+ return i;
2194
+ const g = value.model.geometry;
2195
+ if (x >= 0 && x < g.left && y >= g.top && y <= g.bottom)
2196
+ return null;
2197
+ const last = value.panes[value.panes.length - 1];
2198
+ if (view !== void 0 && last !== void 0 && x >= g.left && x <= g.right && y >= last.top + last.frame.model.geometry.bottom && y <= last.top + last.height)
2199
+ return "time";
2200
+ return void 0;
2201
+ };
2202
+ const stop = () => {
2203
+ if (raf !== null)
2204
+ frames.cancel(raf);
2205
+ raf = null;
2206
+ pending = null;
2207
+ const previous = drag;
2208
+ drag = null;
2209
+ element.style.cursor = originalCursor;
2210
+ if (previous?.area === "time")
2211
+ view?.endNavigation();
2212
+ if (previous !== null && element.hasPointerCapture(previous.pointer))
2213
+ element.releasePointerCapture(previous.pointer);
2214
+ };
2215
+ const applyPending = () => {
2216
+ const p = pending;
2217
+ pending = null;
2218
+ if (p === null || drag === null)
2219
+ return;
2220
+ const value = frame(), r = bounds(), initial = drag.initial;
2221
+ if (value === null || r.width !== drag.width || r.height !== drag.height || value.panes.map((pane) => pane.id).join("\n") !== initial.panes.map((pane) => pane.id).join("\n")) {
2222
+ stop();
2223
+ return;
2224
+ }
2225
+ if (drag.area === "time") {
2226
+ if (view === void 0 || view.getSnapshot().viewport !== drag.observed || view.getSnapshot().length !== initial.snapshot.length) {
2227
+ stop();
2228
+ return;
2229
+ }
2230
+ const g = initial.model.geometry, anchor = Math.max(0, Math.min(1, (drag.x - g.left) / g.plotWidth));
2231
+ const factor = Math.exp(Math.max(-4, Math.min(4, (p.x - drag.x) / g.plotWidth * 3)));
2232
+ view.setViewport(zoomContinuousViewport(initial.snapshot.viewport, initial.snapshot.length, factor, anchor, view.getLimits()));
2233
+ drag.observed = view.getSnapshot().viewport;
2234
+ return;
2235
+ }
2236
+ if (value.snapshot.viewport !== initial.snapshot.viewport) {
2237
+ stop();
2238
+ return;
2239
+ }
2240
+ if (typeof drag.area === "number") {
2241
+ const before = initial.panes[drag.area - 1], after = initial.panes[drag.area];
2242
+ const minimum = Math.min(48, drag.height / initial.panes.length);
2243
+ const delta = Math.max(minimum - before.height, Math.min(after.height - minimum, p.y - drag.y));
2244
+ const weights = /* @__PURE__ */ Object.create(null);
2245
+ for (const pane of initial.panes)
2246
+ weights[pane.id] = Math.max(1e-9, pane.height + (pane === before ? delta : pane === after ? -delta : 0));
2247
+ setLayout({ paneWeights: weights });
2248
+ } else {
2249
+ const scale = initial.panes[0].scale, g = initial.model.geometry;
2250
+ const factor = Math.exp(Math.max(-4, Math.min(4, (p.y - drag.y) / g.plotHeight * 2)));
2251
+ const a = scale.value(drag.y + (g.top - drag.y) * factor), b = scale.value(drag.y + (g.bottom - drag.y) * factor);
2252
+ if (Number.isFinite(a) && Number.isFinite(b) && a !== b && (layout().priceScale?.mode !== "log" || Math.min(a, b) > 0)) {
2253
+ setLayout({ priceScale: { ...layout().priceScale, range: [Math.min(a, b), Math.max(a, b)] } });
2254
+ }
2255
+ }
2256
+ };
2257
+ const schedule = () => {
2258
+ if (raf === null)
2259
+ raf = frames.request(() => {
2260
+ raf = null;
2261
+ applyPending();
2262
+ });
2263
+ };
2264
+ const consume = (event) => {
2265
+ event.preventDefault();
2266
+ event.stopImmediatePropagation();
2267
+ };
2268
+ element.addEventListener("pointerdown", (event) => {
2269
+ if (event.button !== 0 || !event.isPrimary || drag !== null)
2270
+ return;
2271
+ const value = frame(), p = point(event);
2272
+ if (value === null || value.snapshot.length === 0)
2273
+ return;
2274
+ const hit = area(value, p.x, p.y);
2275
+ if (hit === void 0)
2276
+ return;
2277
+ consume(event);
2278
+ cancelNavigation();
2279
+ if (hit === "time")
2280
+ view?.beginNavigation();
2281
+ const r = bounds();
2282
+ drag = { pointer: event.pointerId, x: p.x, y: p.y, initial: value, width: r.width, height: r.height, area: hit, observed: value.snapshot.viewport };
2283
+ element.setPointerCapture(event.pointerId);
2284
+ element.focus({ preventScroll: true });
2285
+ }, { signal: abort.signal, passive: false });
2286
+ element.addEventListener("pointermove", (event) => {
2287
+ const coalesced = event.getCoalescedEvents?.();
2288
+ const latest = coalesced === void 0 || coalesced.length === 0 ? event : coalesced[coalesced.length - 1];
2289
+ const p = point(latest);
2290
+ if (drag === null) {
2291
+ const value = frame(), hit = value === null ? void 0 : area(value, p.x, p.y);
2292
+ element.style.cursor = hit === void 0 ? originalCursor : hit === "time" ? "ew-resize" : "ns-resize";
2293
+ return;
2294
+ }
2295
+ if (event.pointerId !== drag.pointer)
2296
+ return;
2297
+ consume(event);
2298
+ pending = p;
2299
+ schedule();
2300
+ }, { signal: abort.signal, passive: false });
2301
+ element.addEventListener("pointerup", (event) => {
2302
+ if (event.pointerId !== drag?.pointer)
2303
+ return;
2304
+ consume(event);
2305
+ pending = point(event);
2306
+ applyPending();
2307
+ stop();
2308
+ }, { signal: abort.signal });
2309
+ element.addEventListener("pointercancel", stop, { signal: abort.signal });
2310
+ element.addEventListener("lostpointercapture", (event) => {
2311
+ if (event.pointerId === drag?.pointer)
2312
+ stop();
2313
+ }, { signal: abort.signal });
2314
+ element.addEventListener("dblclick", (event) => {
2315
+ const value = frame(), p = point(event);
2316
+ if (value === null)
2317
+ return;
2318
+ const hit = area(value, p.x, p.y);
2319
+ if (hit === void 0)
2320
+ return;
2321
+ consume(event);
2322
+ stop();
2323
+ cancelNavigation();
2324
+ if (hit === "time")
2325
+ view?.resetViewport();
2326
+ else
2327
+ setLayout(hit === null ? { priceScale: { ...layout().priceScale, range: void 0 } } : { paneWeights: void 0 });
2328
+ }, { signal: abort.signal });
2329
+ element.addEventListener("keydown", (event) => {
2330
+ if (event.key === "Escape" && drag !== null) {
2331
+ consume(event);
2332
+ stop();
2333
+ }
2334
+ }, { signal: abort.signal });
2335
+ element.addEventListener("blur", stop, { signal: abort.signal });
2336
+ win.addEventListener("blur", stop, { signal: abort.signal });
2337
+ win.addEventListener("resize", invalidateRect, { signal: abort.signal });
2338
+ element.ownerDocument.addEventListener("scroll", invalidateRect, { signal: abort.signal, capture: true, passive: true });
2339
+ element.ownerDocument.addEventListener("visibilitychange", () => {
2340
+ if (element.ownerDocument.hidden)
2341
+ stop();
2342
+ }, { signal: abort.signal });
2343
+ return () => {
2344
+ abort.abort();
2345
+ stop();
2346
+ if (rectFrame !== null)
2347
+ frames.cancel(rectFrame);
2348
+ rectFrame = null;
2349
+ rect = null;
2350
+ };
2351
+ }
2352
+
2353
+ // packages/bindings/web/src/chart.ts
2354
+ function copyLayout(layout) {
2355
+ return {
2356
+ ...layout,
2357
+ ...layout.priceScale === void 0 ? {} : {
2358
+ priceScale: {
2359
+ ...layout.priceScale,
2360
+ ...layout.priceScale.range === void 0 ? {} : { range: [...layout.priceScale.range] }
2361
+ }
2362
+ },
2363
+ ...layout.paneWeights === void 0 ? {} : { paneWeights: { ...layout.paneWeights } }
2364
+ };
2365
+ }
2366
+ var EngineChart = class {
2367
+ series = new RealtimeSeries({ referenceIndicators: false });
2368
+ /** Loader boundary. Data reads return copies and are not used by the render hot path. */
2369
+ dataPort = Object.freeze({
2370
+ applyNewData: (bars, more) => {
2371
+ this.setData(bars, more);
2372
+ },
2373
+ applyNormalizedData: (bars, more) => {
2374
+ this.assertLive();
2375
+ this.validateBars(bars, this.layout, bars[0]?.close, true);
2376
+ this.series.applyNewData(bars, more);
2377
+ },
2378
+ truncateData: (length, expectedLength, expectedLastTimestamp) => {
2379
+ this.assertLive();
2380
+ return this.series.truncateData(length, expectedLength, expectedLastTimestamp);
2381
+ },
2382
+ applyMoreData: (bars, more) => {
2383
+ this.prependData(bars, more);
2384
+ },
2385
+ updateData: (bar) => {
2386
+ this.update(bar);
2387
+ },
2388
+ updateDataBatch: (bars, more) => {
2389
+ this.updateBatch(bars, more);
2390
+ },
2391
+ getDataList: () => this.getBars(),
2392
+ getLastTimestamp: () => {
2393
+ this.assertLive();
2394
+ return this.series.getLastTimestamp();
2395
+ },
2396
+ loadMore: (callback) => {
2397
+ this.assertLive();
2398
+ this.series.loadMore(callback);
2399
+ }
2400
+ });
2401
+ view;
2402
+ drawings = new DrawingStore2();
2403
+ indicators = new EngineIndicators();
2404
+ doc;
2405
+ win;
2406
+ root;
2407
+ canvas;
2408
+ cursorCanvas;
2409
+ drawingCanvas;
2410
+ drawingContext;
2411
+ drawingImages;
2412
+ context;
2413
+ cursorContext;
2414
+ resizeObserver;
2415
+ abort;
2416
+ unsubscribers = [];
2417
+ stopGestures;
2418
+ drawingInput;
2419
+ onInspect;
2420
+ inputs = /* @__PURE__ */ new Map();
2421
+ layers = /* @__PURE__ */ new Set();
2422
+ indicatorListeners = /* @__PURE__ */ new Set();
2423
+ indicatorNoticeQueued = false;
2424
+ inputId = 0;
2425
+ draining = false;
2426
+ preview = null;
2427
+ frame = null;
2428
+ frameId = 0;
2429
+ cursorX = null;
2430
+ width = 0;
2431
+ height = 0;
2432
+ ratio = 0;
2433
+ geometryDirty = true;
2434
+ paintDirty = true;
2435
+ drawingDirty = true;
2436
+ cursorDirty = true;
2437
+ disposed = false;
2438
+ invalidateDrawings = () => {
2439
+ if (!this.disposed) {
2440
+ this.drawingDirty = true;
2441
+ this.schedule();
2442
+ }
2443
+ };
2444
+ viewportOptions;
2445
+ resolution;
2446
+ densityWidth = -1;
2447
+ layout;
2448
+ renderOptions;
2449
+ dprQuery = null;
2450
+ counters = { frames: 0, geometryBuilds: 0, plotPaints: 0, drawingPaints: 0, cursorPaints: 0, lastFrameMs: 0, firstDataFrameMs: 0, maxFrameMs: 0 };
2451
+ constructor(host, options = {}) {
2452
+ this.doc = host.ownerDocument;
2453
+ if (this.doc.defaultView === null)
2454
+ throw new Error("Chart host needs an active document");
2455
+ this.win = this.doc.defaultView;
2456
+ this.drawingImages = new DrawingImageRenderer(async (blob) => await this.win.createImageBitmap(blob));
2457
+ this.abort = new this.win.AbortController();
2458
+ this.layout = copyLayout(options.layout ?? {});
2459
+ validateEngineLayout(this.layout);
2460
+ validatePriceScale(this.layout.priceScale ?? {});
2461
+ this.viewportOptions = { minBars: 20, maxBars: 1200, rightPaddingBars: 12, ...options.viewport };
2462
+ viewportLimitsForWidth(0, this.viewportOptions);
2463
+ this.resolution = { ...options.resolution };
2464
+ if (this.resolution.maxPixelRatio !== void 0 && (!Number.isFinite(this.resolution.maxPixelRatio) || this.resolution.maxPixelRatio <= 0))
2465
+ throw new RangeError("Pixel ratio cap must be positive and finite");
2466
+ if (this.resolution.maxCanvasPixels !== void 0 && (!Number.isSafeInteger(this.resolution.maxCanvasPixels) || this.resolution.maxCanvasPixels < 1))
2467
+ throw new RangeError("Canvas pixel budget must be a positive safe integer");
2468
+ this.renderOptions = { ...options.render };
2469
+ this.validateRender(this.renderOptions);
2470
+ for (const spec of options.indicators ?? [{ name: "MA", params: [20] }, { name: "EMA", params: [21] }])
2471
+ this.indicators.add(spec);
2472
+ this.view = new RealtimeView(this.series, { count: options.count, mode: options.mode, continuous: true, limits: this.viewportOptions });
2473
+ this.onInspect = options.onInspect;
2474
+ this.root = this.doc.createElement("div");
2475
+ this.root.style.cssText = "position:relative;width:100%;height:100%;min-height:0;overflow:hidden;touch-action:none;";
2476
+ this.root.tabIndex = 0;
2477
+ this.root.setAttribute("role", "group");
2478
+ this.root.setAttribute("aria-label", "Chart. Drag to pan, pinch or wheel to zoom; Home follows the latest candle.");
2479
+ this.canvas = this.doc.createElement("canvas");
2480
+ this.drawingCanvas = this.doc.createElement("canvas");
2481
+ this.drawingCanvas.width = this.drawingCanvas.height = 1;
2482
+ this.cursorCanvas = this.doc.createElement("canvas");
2483
+ for (const canvas of [this.canvas, this.drawingCanvas, this.cursorCanvas]) {
2484
+ canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;";
2485
+ canvas.setAttribute("aria-hidden", "true");
2486
+ this.root.append(canvas);
2487
+ }
2488
+ const context = this.canvas.getContext("2d"), drawingContext = this.drawingCanvas.getContext("2d"), cursorContext = this.cursorCanvas.getContext("2d");
2489
+ if (context === null || drawingContext === null || cursorContext === null) {
2490
+ this.view.dispose();
2491
+ throw new Error("Canvas 2D is unavailable");
2492
+ }
2493
+ this.context = context;
2494
+ this.drawingContext = drawingContext;
2495
+ this.cursorContext = cursorContext;
2496
+ host.append(this.root);
2497
+ this.unsubscribers.push(this.series.subscribe((change) => {
2498
+ this.indicators.invalidate(change);
2499
+ if (change.kind !== "availability") {
2500
+ if (this.indicators.hasCustom() || this.layers.size > 0)
2501
+ this.geometryDirty = true;
2502
+ if (this.drawings.getSnapshot().drawings.length > 0)
2503
+ this.drawingDirty = true;
2504
+ if (this.geometryDirty || this.drawingDirty)
2505
+ this.schedule();
2506
+ }
2507
+ }));
2508
+ this.unsubscribers.push(this.view.subscribe(() => {
2509
+ const snapshot = this.view.getSnapshot(), previous = this.frame?.snapshot;
2510
+ if (previous === void 0 || snapshot.viewport !== previous.viewport || snapshot.bars !== previous.bars || snapshot.ma20 !== previous.ma20 || snapshot.ema21 !== previous.ema21) {
2511
+ this.geometryDirty = true;
2512
+ }
2513
+ this.cursorDirty = true;
2514
+ this.schedule();
2515
+ }));
2516
+ this.unsubscribers.push(this.drawings.subscribe(() => {
2517
+ this.retainDrawingImages();
2518
+ this.drawingDirty = true;
2519
+ this.schedule();
2520
+ }));
2521
+ const frameScheduler = {
2522
+ request: (callback) => {
2523
+ const id = ++this.inputId;
2524
+ this.inputs.set(id, callback);
2525
+ this.schedule();
2526
+ return id;
2527
+ },
2528
+ cancel: (id) => {
2529
+ this.inputs.delete(id);
2530
+ }
2531
+ };
2532
+ this.unsubscribers.push(attachPaneInteraction(this.root, () => this.frame, () => this.layout, (options2) => this.setLayout(options2), () => this.stopGestures.cancelInput(), this.view, frameScheduler));
2533
+ this.drawingInput = attachDrawingInteraction(this.root, this.drawings, () => this.frame, (preview) => {
2534
+ this.preview = preview;
2535
+ this.drawingDirty = true;
2536
+ this.schedule();
2537
+ }, options.onToolComplete, () => this.stopGestures.cancelInput(), this.drawingImages);
2538
+ this.stopGestures = attachEngineGestures(this.root, this.view, () => this.frame?.model.geometry ?? null, (x) => {
2539
+ if (this.cursorX !== x) {
2540
+ this.cursorX = x;
2541
+ this.cursorDirty = true;
2542
+ this.schedule();
2543
+ }
2544
+ }, {
2545
+ inertia: options.inertia !== false,
2546
+ frameScheduler
2547
+ });
2548
+ this.resizeObserver = new this.win.ResizeObserver(() => this.schedule());
2549
+ this.resizeObserver.observe(this.root);
2550
+ this.doc.addEventListener("visibilitychange", () => {
2551
+ if (this.doc.hidden && this.frameId !== 0) {
2552
+ this.win.cancelAnimationFrame(this.frameId);
2553
+ this.frameId = 0;
2554
+ } else
2555
+ this.schedule();
2556
+ }, { signal: this.abort.signal });
2557
+ this.watchPixelRatio();
2558
+ this.schedule();
2559
+ }
2560
+ /** Millisecond OHLC input is normalized once at the data boundary. */
2561
+ setData(bars, more = false) {
2562
+ this.assertLive();
2563
+ const normalized = normalizeBars(bars, 1).bars;
2564
+ this.validateBars(normalized, this.layout, normalized[0]?.close);
2565
+ this.series.applyNewData(normalized, more);
2566
+ }
2567
+ update(bar) {
2568
+ this.assertLive();
2569
+ const normalized = normalizeBar(bar, 1);
2570
+ if (normalized === null)
2571
+ throw new TypeError("Invalid OHLC candle");
2572
+ this.validateBars([normalized], this.layout, this.updateReference([normalized]));
2573
+ this.series.updateData(normalized);
2574
+ }
2575
+ prependData(bars, more = false) {
2576
+ this.assertLive();
2577
+ const normalized = normalizeBars(bars, 1).bars, first = this.series.getDataList()[0];
2578
+ const base = first === void 0 || normalized[0] !== void 0 && normalized[0].timestamp < first.timestamp ? normalized[0]?.close : first.close;
2579
+ this.validateBars(normalized, this.layout, base);
2580
+ this.series.applyMoreData(normalized, more);
2581
+ }
2582
+ updateBatch(bars, more) {
2583
+ this.assertLive();
2584
+ const normalized = normalizeBars(bars, 1).bars;
2585
+ this.validateBars(normalized, this.layout, this.updateReference(normalized));
2586
+ this.series.updateDataBatch(normalized, more);
2587
+ }
2588
+ registerIndicator(definition) {
2589
+ this.assertLive();
2590
+ this.indicators.register(definition);
2591
+ this.notifyIndicators();
2592
+ }
2593
+ unregisterIndicator(name, expectedCalculate) {
2594
+ this.assertLive();
2595
+ const removed = this.indicators.unregister(name, expectedCalculate);
2596
+ if (removed)
2597
+ this.notifyIndicators();
2598
+ return removed;
2599
+ }
2600
+ addIndicator(spec) {
2601
+ this.assertLive();
2602
+ const id = this.indicators.add(spec);
2603
+ this.geometryDirty = true;
2604
+ this.schedule();
2605
+ this.notifyIndicators();
2606
+ return id;
2607
+ }
2608
+ removeIndicator(id) {
2609
+ this.assertLive();
2610
+ const removed = this.indicators.remove(id);
2611
+ if (removed) {
2612
+ this.geometryDirty = true;
2613
+ this.schedule();
2614
+ this.notifyIndicators();
2615
+ }
2616
+ return removed;
2617
+ }
2618
+ updateIndicator(id, changes) {
2619
+ this.assertLive();
2620
+ const updated = this.indicators.update(id, changes);
2621
+ if (updated) {
2622
+ this.geometryDirty = true;
2623
+ this.schedule();
2624
+ this.notifyIndicators();
2625
+ }
2626
+ return updated;
2627
+ }
2628
+ listIndicators() {
2629
+ return this.indicators.list();
2630
+ }
2631
+ getRegisteredIndicatorNames() {
2632
+ return this.indicators.registeredNames();
2633
+ }
2634
+ validateIndicatorReplacement(specs) {
2635
+ this.indicators.validateReplacement(specs);
2636
+ }
2637
+ replaceIndicators(specs) {
2638
+ this.assertLive();
2639
+ this.indicators.replace(specs);
2640
+ this.geometryDirty = true;
2641
+ this.schedule();
2642
+ this.notifyIndicators();
2643
+ }
2644
+ onIndicatorsChanged(listener) {
2645
+ this.assertLive();
2646
+ this.indicatorListeners.add(listener);
2647
+ return () => {
2648
+ this.indicatorListeners.delete(listener);
2649
+ };
2650
+ }
2651
+ notifyIndicators() {
2652
+ if (this.indicatorNoticeQueued || this.disposed)
2653
+ return;
2654
+ this.indicatorNoticeQueued = true;
2655
+ this.win.queueMicrotask(() => {
2656
+ this.indicatorNoticeQueued = false;
2657
+ if (this.disposed)
2658
+ return;
2659
+ for (const listener of [...this.indicatorListeners]) {
2660
+ if (!this.indicatorListeners.has(listener))
2661
+ continue;
2662
+ try {
2663
+ listener();
2664
+ } catch (error) {
2665
+ this.win.console.warn("Indicator change listener failed", error);
2666
+ }
2667
+ }
2668
+ });
2669
+ }
2670
+ setDrawingTool(tool) {
2671
+ this.assertLive();
2672
+ if (tool !== null && !DRAWING_TYPES.includes(tool))
2673
+ throw new RangeError(`Unknown drawing tool ${JSON.stringify(tool)}`);
2674
+ this.drawingInput.setTool(tool);
2675
+ }
2676
+ /** Own this layer until removal or chart disposal. It receives only revealed replay data. */
2677
+ addLayer(layer) {
2678
+ this.assertLive();
2679
+ if (this.layers.has(layer))
2680
+ throw new Error("Layer is already attached");
2681
+ this.layers.add(layer);
2682
+ this.geometryDirty = true;
2683
+ this.schedule();
2684
+ return () => {
2685
+ if (!this.layers.delete(layer))
2686
+ return;
2687
+ this.paintDirty = true;
2688
+ this.schedule();
2689
+ layer.dispose?.();
2690
+ };
2691
+ }
2692
+ setRenderOptions(options) {
2693
+ this.assertLive();
2694
+ const next = { ...this.renderOptions, ...options };
2695
+ this.validateRender(next);
2696
+ this.renderOptions = next;
2697
+ this.paintDirty = true;
2698
+ this.drawingDirty = true;
2699
+ this.cursorDirty = true;
2700
+ this.schedule();
2701
+ }
2702
+ setLayout(options) {
2703
+ this.assertLive();
2704
+ const next = copyLayout({ ...this.layout, ...options });
2705
+ validateEngineLayout(next);
2706
+ validatePriceScale(next.priceScale ?? {});
2707
+ if ((next.priceScale?.mode ?? "linear") !== (this.layout.priceScale?.mode ?? "linear"))
2708
+ this.validateBars(this.series.getDataList(), next);
2709
+ this.layout = next;
2710
+ this.geometryDirty = true;
2711
+ this.schedule();
2712
+ }
2713
+ getMetrics() {
2714
+ return { ...this.counters };
2715
+ }
2716
+ /** Copy only the requested tail; the feed/render hot path keeps its owned array. */
2717
+ getBars(options = {}) {
2718
+ this.assertLive();
2719
+ if (Object.keys(options).some((key) => key !== "limit"))
2720
+ throw new TypeError("Only the bars limit option is supported");
2721
+ const limit = options.limit;
2722
+ if (limit !== void 0 && (!Number.isSafeInteger(limit) || limit < 0))
2723
+ throw new RangeError("Bars limit must be a nonnegative safe integer");
2724
+ const bars = this.series.getDataList(), start = limit === void 0 ? 0 : Math.max(0, bars.length - limit);
2725
+ const result = [];
2726
+ for (let index = start; index < bars.length; index++)
2727
+ result.push({ ...bars[index] });
2728
+ return result;
2729
+ }
2730
+ getLastBar() {
2731
+ this.assertLive();
2732
+ const bars = this.series.getDataList(), bar = bars[bars.length - 1];
2733
+ return bar === void 0 ? null : { ...bar };
2734
+ }
2735
+ getLayoutOptions() {
2736
+ this.assertLive();
2737
+ return copyLayout(this.layout);
2738
+ }
2739
+ getRenderOptions() {
2740
+ this.assertLive();
2741
+ return { ...this.renderOptions };
2742
+ }
2743
+ /** Validate all supplied state before replacing any live configuration. */
2744
+ restoreConfiguration(configuration, data) {
2745
+ this.assertLive();
2746
+ if (configuration.mode !== "raw" && configuration.mode !== "heikin-ashi")
2747
+ throw new TypeError("Invalid candle mode");
2748
+ const render = { ...configuration.render }, layout = copyLayout(configuration.layout);
2749
+ this.validateRender(render);
2750
+ validateEngineLayout(layout);
2751
+ validatePriceScale(layout.priceScale ?? {});
2752
+ const bars = data === void 0 ? this.series.getDataList() : normalizeBars(data.bars, 1).bars;
2753
+ this.validateBars(bars, layout, bars[0]?.close);
2754
+ const drawings = new DrawingStore2({ historyLimit: 0 });
2755
+ drawings.importJson(configuration.drawingsJson);
2756
+ const normalizedDrawings = drawings.exportJson();
2757
+ this.indicators.replace(configuration.indicators);
2758
+ this.renderOptions = render;
2759
+ this.layout = layout;
2760
+ this.drawings.importJson(normalizedDrawings, { recordHistory: false });
2761
+ try {
2762
+ this.view.setMode(configuration.mode);
2763
+ if (data !== void 0)
2764
+ this.series.applyNewData(bars, data.more);
2765
+ } finally {
2766
+ this.geometryDirty = true;
2767
+ this.drawingDirty = true;
2768
+ this.cursorDirty = true;
2769
+ this.schedule();
2770
+ this.notifyIndicators();
2771
+ }
2772
+ }
2773
+ /** Repaint owned layers after their model changes without rebuilding candle geometry. */
2774
+ requestPaint() {
2775
+ this.assertLive();
2776
+ this.paintDirty = true;
2777
+ this.schedule();
2778
+ }
2779
+ /** Decode local raster resources before an import commits retained drawings. */
2780
+ async prepareDrawingImages(drawings) {
2781
+ this.assertLive();
2782
+ const release = this.holdDrawingImages(drawings);
2783
+ const sources = new Set(drawings.flatMap((drawing) => drawing.image === void 0 ? [] : [drawing.image]));
2784
+ try {
2785
+ const results = await Promise.allSettled([...sources].map(async (source) => await this.drawingImages.prepare(source)));
2786
+ const failed = results.find((result) => result.status === "rejected");
2787
+ if (failed !== void 0)
2788
+ throw failed.reason;
2789
+ this.assertLive();
2790
+ } finally {
2791
+ release();
2792
+ }
2793
+ }
2794
+ /** Hold decoded resources across asynchronous market loading; release in finally. */
2795
+ holdDrawingImages(drawings) {
2796
+ this.assertLive();
2797
+ const release = this.drawingImages.hold(new Set(drawings.flatMap((drawing) => drawing.image === void 0 ? [] : [drawing.image])));
2798
+ return () => {
2799
+ release();
2800
+ this.retainDrawingImages();
2801
+ this.invalidateDrawings();
2802
+ };
2803
+ }
2804
+ retainDrawingImages() {
2805
+ const sources = new Set(this.drawings.getSnapshot().drawings.flatMap((drawing) => drawing.image === void 0 ? [] : [drawing.image]));
2806
+ if (this.preview?.image !== void 0)
2807
+ sources.add(this.preview.image);
2808
+ this.drawingImages.retain(sources);
2809
+ }
2810
+ async snapshot() {
2811
+ this.assertLive();
2812
+ const drawings = this.drawings.getSnapshot().drawings;
2813
+ const release = this.holdDrawingImages(drawings);
2814
+ try {
2815
+ await this.prepareDrawingImages(drawings);
2816
+ if (this.drawings.getSnapshot().drawings !== drawings)
2817
+ throw new Error("Drawings changed while preparing the image; capture again");
2818
+ if (this.frameId !== 0) {
2819
+ this.win.cancelAnimationFrame(this.frameId);
2820
+ this.frameId = 0;
2821
+ }
2822
+ this.renderTick(this.win.performance.now());
2823
+ this.assertLive();
2824
+ if (this.width <= 0 || this.height <= 0)
2825
+ throw new Error("Cannot capture a zero-sized chart");
2826
+ if (this.frame === null || this.drawings.getSnapshot().drawings !== drawings)
2827
+ throw new Error("Chart changed while preparing the image; capture again");
2828
+ const copy = this.doc.createElement("canvas");
2829
+ copy.width = this.canvas.width;
2830
+ copy.height = this.canvas.height;
2831
+ const ctx = copy.getContext("2d");
2832
+ if (ctx === null)
2833
+ throw new Error("Canvas 2D is unavailable");
2834
+ ctx.drawImage(this.canvas, 0, 0);
2835
+ ctx.setTransform(this.canvas.width / this.width, 0, 0, this.canvas.height / this.height, 0, 0);
2836
+ const bounds = this.frame.model.geometry;
2837
+ paintDrawings(ctx, this.drawings.geometry(this.frame.project, bounds, this.frame.drawingContext), null, { bounds, imageRenderer: this.drawingImages });
2838
+ return await new Promise((resolve, reject) => copy.toBlob((blob) => {
2839
+ if (blob === null)
2840
+ reject(new Error("PNG encoding failed"));
2841
+ else
2842
+ resolve(blob);
2843
+ }, "image/png"));
2844
+ } finally {
2845
+ release();
2846
+ }
2847
+ }
2848
+ dispose() {
2849
+ if (this.disposed)
2850
+ return;
2851
+ this.disposed = true;
2852
+ this.abort.abort();
2853
+ this.resizeObserver.disconnect();
2854
+ this.unsubscribers.forEach((fn) => fn());
2855
+ this.drawingInput.dispose();
2856
+ this.drawingImages.dispose();
2857
+ this.stopGestures();
2858
+ this.view.dispose();
2859
+ this.dprQuery?.removeEventListener("change", this.pixelRatioChanged);
2860
+ if (this.frameId !== 0)
2861
+ this.win.cancelAnimationFrame(this.frameId);
2862
+ this.frameId = 0;
2863
+ this.inputs.clear();
2864
+ this.frame = null;
2865
+ this.preview = null;
2866
+ this.series.loadMore(() => {
2867
+ });
2868
+ this.series.applyNewData([]);
2869
+ this.indicators.dispose();
2870
+ this.indicatorListeners.clear();
2871
+ this.root.remove();
2872
+ this.canvas.width = this.canvas.height = this.drawingCanvas.width = this.drawingCanvas.height = this.cursorCanvas.width = this.cursorCanvas.height = 0;
2873
+ const layers = [...this.layers];
2874
+ this.layers.clear();
2875
+ const failures = [];
2876
+ for (const layer of layers) {
2877
+ try {
2878
+ layer.dispose?.();
2879
+ } catch (error) {
2880
+ failures.push(error);
2881
+ }
2882
+ }
2883
+ if (failures.length > 0)
2884
+ throw failures[0];
2885
+ }
2886
+ assertLive() {
2887
+ if (this.disposed)
2888
+ throw new Error("Chart is disposed");
2889
+ }
2890
+ updateReference(bars) {
2891
+ const current = this.series.getDataList();
2892
+ return current.length === 0 ? bars[0]?.close : current.length === 1 ? bars.find((bar) => bar.timestamp === current[0].timestamp)?.close ?? current[0].close : current[0].close;
2893
+ }
2894
+ validateBars(bars, layout = this.layout, base = this.series.getDataList()[0]?.close ?? bars[0]?.close, normalized = false) {
2895
+ if (layout.priceScale?.mode === "percentage" && base === 0)
2896
+ throw new RangeError("Percentage scale needs a nonzero reference price");
2897
+ for (const bar of bars) {
2898
+ const { timestamp, open, high, low, close, volume, turnover } = bar;
2899
+ if (normalized && (volume != null && !Number.isFinite(volume) || turnover != null && !Number.isFinite(turnover)))
2900
+ throw new TypeError("Normalized candle has invalid volume or turnover");
2901
+ if (!Number.isFinite(timestamp) || !Number.isFinite(open) || !Number.isFinite(high) || !Number.isFinite(low) || !Number.isFinite(close) || high < Math.max(open, close) || low > Math.min(open, close))
2902
+ throw new RangeError("Candle has invalid OHLC bounds");
2903
+ if (Math.abs(timestamp) > 864e13)
2904
+ throw new RangeError("Candle timestamp is outside the supported Date range");
2905
+ if (layout.priceScale?.mode === "log" && Math.min(open, high, low, close) <= 0)
2906
+ throw new RangeError("Log scale requires positive prices");
2907
+ }
2908
+ }
2909
+ validateRender(options) {
2910
+ if (options.timeZone !== void 0)
2911
+ new Intl.DateTimeFormat("en", { timeZone: options.timeZone }).format(0);
2912
+ if (options.candleDensity !== void 0 && options.candleDensity !== "raw" && options.candleDensity !== "pixel-envelope")
2913
+ throw new RangeError("Unknown candle density mode");
2914
+ if (options.chartType !== void 0 && !ENGINE_CHART_TYPES2.includes(options.chartType))
2915
+ throw new RangeError("Unknown chart type");
2916
+ if (options.baseline !== void 0 && !Number.isFinite(options.baseline))
2917
+ throw new RangeError("Baseline must be a finite price");
2918
+ }
2919
+ pixelRatioChanged = () => {
2920
+ this.watchPixelRatio();
2921
+ this.schedule();
2922
+ };
2923
+ watchPixelRatio() {
2924
+ this.dprQuery?.removeEventListener("change", this.pixelRatioChanged);
2925
+ this.dprQuery = this.win.matchMedia(`(resolution: ${this.win.devicePixelRatio || 1}dppx)`);
2926
+ this.dprQuery.addEventListener("change", this.pixelRatioChanged);
2927
+ }
2928
+ schedule() {
2929
+ if (this.disposed || this.doc.hidden || this.frameId !== 0 || this.draining)
2930
+ return;
2931
+ this.frameId = this.win.requestAnimationFrame((time) => {
2932
+ this.frameId = 0;
2933
+ this.renderTick(time);
2934
+ });
2935
+ }
2936
+ renderTick(time) {
2937
+ this.draining = true;
2938
+ let rendered = false;
2939
+ try {
2940
+ const pending = [...this.inputs.entries()];
2941
+ for (const [id, callback] of pending) {
2942
+ if (this.inputs.delete(id))
2943
+ callback(time);
2944
+ }
2945
+ this.flush();
2946
+ rendered = true;
2947
+ } finally {
2948
+ this.draining = false;
2949
+ if (rendered && (this.inputs.size > 0 || this.width > 0 && this.height > 0 && (this.geometryDirty || this.paintDirty || this.drawingDirty || this.cursorDirty)))
2950
+ this.schedule();
2951
+ }
2952
+ }
2953
+ /** Keep an unused transparent layer tiny without changing its CSS or hit geometry. */
2954
+ sizeDrawingCanvas(active) {
2955
+ const width = active ? this.canvas.width : 1, height = active ? this.canvas.height : 1;
2956
+ if (this.drawingCanvas.width !== width)
2957
+ this.drawingCanvas.width = width;
2958
+ if (this.drawingCanvas.height !== height)
2959
+ this.drawingCanvas.height = height;
2960
+ }
2961
+ flush() {
2962
+ if (this.disposed)
2963
+ return;
2964
+ const started = this.win.performance.now(), rect = this.root.getBoundingClientRect();
2965
+ const pixelBudget = this.resolution.maxCanvasPixels;
2966
+ const ratio = Math.min(
2967
+ this.win.devicePixelRatio || 1,
2968
+ this.resolution.maxPixelRatio ?? Infinity,
2969
+ pixelBudget === void 0 || rect.width <= 0 || rect.height <= 0 ? Infinity : Math.sqrt(pixelBudget / (rect.width * rect.height))
2970
+ );
2971
+ const plotWidth = rect.width > 0 ? financialGeometry(rect.width, rect.height, this.layout.axisGutter, 12).plotWidth : 0;
2972
+ if (plotWidth > 0 && plotWidth !== this.densityWidth) {
2973
+ this.densityWidth = plotWidth;
2974
+ this.view.setLimits(viewportLimitsForWidth(plotWidth, this.viewportOptions));
2975
+ }
2976
+ const sizeChanged = rect.width !== this.width || rect.height !== this.height;
2977
+ if (sizeChanged || ratio !== this.ratio) {
2978
+ this.width = rect.width;
2979
+ this.height = rect.height;
2980
+ this.ratio = ratio;
2981
+ for (const canvas of [this.canvas, this.cursorCanvas]) {
2982
+ canvas.width = Math.min(pixelBudget ?? Infinity, Math.max(this.width > 0 ? 1 : 0, Math.floor(this.width * ratio)));
2983
+ canvas.height = Math.min(pixelBudget === void 0 || canvas.width === 0 ? Infinity : Math.floor(pixelBudget / canvas.width), Math.max(this.height > 0 ? 1 : 0, Math.floor(this.height * ratio)));
2984
+ }
2985
+ this.geometryDirty = this.geometryDirty || sizeChanged;
2986
+ this.paintDirty = true;
2987
+ this.drawingDirty = true;
2988
+ this.cursorDirty = true;
2989
+ }
2990
+ if (this.width <= 0 || this.height <= 0) {
2991
+ if (this.preview === null && this.drawings.getSnapshot().drawings.length === 0)
2992
+ this.sizeDrawingCanvas(false);
2993
+ return;
2994
+ }
2995
+ if (this.geometryDirty || this.frame === null) {
2996
+ const bars = this.series.getDataList();
2997
+ const indicators = this.indicators.compute(bars).map((result) => ({ ...result, pane: result.pane === "price" ? "price" : `indicator:${result.paneId ?? result.id}` }));
2998
+ const snapshot = this.view.getSnapshot(), from = snapshot.dataStart ?? snapshot.viewport.i0;
2999
+ const previousClose = from > 0 ? this.series.snapshot(from - 1, from, snapshot.mode).bars[0]?.close : void 0;
3000
+ this.frame = buildEngineFrame(snapshot, bars, indicators, this.width, this.height, this.layout, previousClose, this.series.version);
3001
+ for (const layer of this.layers)
3002
+ layer.update(this.frame, bars);
3003
+ if (this.disposed)
3004
+ return;
3005
+ this.geometryDirty = false;
3006
+ this.paintDirty = true;
3007
+ this.drawingDirty = true;
3008
+ this.cursorDirty = true;
3009
+ this.counters.geometryBuilds++;
3010
+ }
3011
+ if (this.paintDirty) {
3012
+ this.context.setTransform(this.canvas.width / this.width, 0, 0, this.canvas.height / this.height, 0, 0);
3013
+ this.context.clearRect(0, 0, this.width, this.height);
3014
+ for (const [i, pane] of this.frame.panes.entries()) {
3015
+ this.context.save();
3016
+ this.context.translate(0, pane.top);
3017
+ paintEngineFrame(this.context, pane.frame, { ...this.renderOptions, formatPrice: i === 0 ? pane.scale.label : (price) => String(Number(price.toPrecision(6))), showTimeAxis: i === this.frame.panes.length - 1 });
3018
+ this.context.restore();
3019
+ }
3020
+ for (const layer of this.layers) {
3021
+ this.context.save();
3022
+ try {
3023
+ layer.paint(this.context);
3024
+ } finally {
3025
+ this.context.restore();
3026
+ }
3027
+ if (this.disposed)
3028
+ return;
3029
+ }
3030
+ this.paintDirty = false;
3031
+ this.counters.plotPaints++;
3032
+ }
3033
+ if (this.drawingDirty) {
3034
+ const drawingState = this.drawings.getSnapshot();
3035
+ this.sizeDrawingCanvas(this.preview !== null || drawingState.drawings.length > 0);
3036
+ this.drawingContext.setTransform(this.canvas.width / this.width, 0, 0, this.canvas.height / this.height, 0, 0);
3037
+ this.drawingContext.clearRect(0, 0, this.width, this.height);
3038
+ const g = this.frame.model.geometry;
3039
+ this.retainDrawingImages();
3040
+ this.frame.drawingContext = { bars: this.series.getDataList(), revision: this.series.version, visibleRange: [this.frame.snapshot.viewport.i0, this.frame.snapshot.viewport.i1] };
3041
+ const geometries = this.drawings.geometry(this.frame.project, g, this.frame.drawingContext).filter((item) => item.id !== this.preview?.id);
3042
+ if (this.preview !== null) {
3043
+ const geometry = projectDrawing(this.preview, this.frame.project, g, this.frame.drawingContext);
3044
+ if (geometry !== null)
3045
+ geometries.push(geometry);
3046
+ }
3047
+ paintDrawings(this.drawingContext, geometries, this.preview?.id ?? drawingState.selectedId, { bounds: g, imageRenderer: this.drawingImages, onInvalidate: this.invalidateDrawings });
3048
+ this.drawingDirty = false;
3049
+ this.counters.drawingPaints++;
3050
+ }
3051
+ if (this.cursorDirty) {
3052
+ this.cursorContext.setTransform(this.canvas.width / this.width, 0, 0, this.canvas.height / this.height, 0, 0);
3053
+ this.cursorContext.clearRect(0, 0, this.width, this.height);
3054
+ if (this.cursorX !== null) {
3055
+ for (const [i, pane] of this.frame.panes.entries()) {
3056
+ this.cursorContext.save();
3057
+ this.cursorContext.translate(0, pane.top);
3058
+ paintEngineCursor(this.cursorContext, pane.frame, this.cursorX, { ...this.renderOptions, formatPrice: pane.scale.label, showTimeAxis: i === this.frame.panes.length - 1 });
3059
+ this.cursorContext.restore();
3060
+ }
3061
+ }
3062
+ this.cursorDirty = false;
3063
+ this.counters.cursorPaints++;
3064
+ this.onInspect?.(this.cursorX === null ? null : this.frame.inspect(this.cursorX));
3065
+ }
3066
+ if (this.disposed || this.frame === null)
3067
+ return;
3068
+ this.counters.frames++;
3069
+ this.counters.lastFrameMs = this.win.performance.now() - started;
3070
+ if (this.counters.firstDataFrameMs === 0 && this.frame.snapshot.bars.length > 0)
3071
+ this.counters.firstDataFrameMs = this.counters.lastFrameMs;
3072
+ this.counters.maxFrameMs = Math.max(this.counters.maxFrameMs, this.counters.lastFrameMs);
3073
+ }
3074
+ };
3075
+
3076
+ export {
3077
+ paintEngineFrame,
3078
+ paintEngineCursor,
3079
+ createDrawingImage,
3080
+ DrawingImageRenderer,
3081
+ EngineChart
3082
+ };