trendkit 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,1220 @@
1
+ import { LineSeries, HistogramSeries } from 'lightweight-charts';
2
+
3
+ // src/core/interaction.ts
4
+ var InteractionController = class {
5
+ constructor(options) {
6
+ this.options = options;
7
+ this.state = { name: "idle" };
8
+ this.activeTool = null;
9
+ this.savedScroll = true;
10
+ this.savedScale = true;
11
+ this.onPointerDown = (event) => {
12
+ if (event.button !== 0) return;
13
+ const at = this.local(event);
14
+ if (this.activeTool !== null) {
15
+ this.beginPlacing(this.activeTool, at);
16
+ event.preventDefault();
17
+ return;
18
+ }
19
+ const hit = this.options.hits.hitTestDetailed(at);
20
+ if (hit.kind === "none") {
21
+ this.options.store.select(null);
22
+ return;
23
+ }
24
+ const drawing = this.options.store.get(hit.id);
25
+ if (drawing === void 0) return;
26
+ this.options.store.select(hit.id);
27
+ this.suppressChartGestures();
28
+ this.state = {
29
+ name: "dragging",
30
+ id: hit.id,
31
+ handle: hit.kind === "handle" ? hit.index : null,
32
+ from: this.toPoint(at, false),
33
+ grabbed: drawing.points.map((p) => ({ ...p }))
34
+ };
35
+ event.preventDefault();
36
+ };
37
+ this.onPointerMove = (event) => {
38
+ const at = this.local(event);
39
+ if (this.state.name === "idle") {
40
+ if (this.activeTool === null) {
41
+ const hit = this.options.hits.hitTestDetailed(at);
42
+ this.element.style.cursor = hit.kind === "handle" ? "grab" : hit.kind === "body" ? "move" : "";
43
+ }
44
+ return;
45
+ }
46
+ if (this.state.name === "placing") {
47
+ const point = this.toPoint(at, event.shiftKey);
48
+ const drawing2 = this.options.store.get(this.state.id);
49
+ if (drawing2 === void 0) return;
50
+ const points2 = [...drawing2.points];
51
+ const moving = Math.min(this.state.placed, this.options.pointCount(this.state.tool) - 1);
52
+ points2[moving] = point;
53
+ this.options.store.update(this.state.id, { points: points2 }, { undoable: false });
54
+ return;
55
+ }
56
+ const now = this.toPoint(at, event.shiftKey);
57
+ const drawing = this.options.store.get(this.state.id);
58
+ if (drawing === void 0) return;
59
+ if (this.state.handle !== null) {
60
+ const points2 = [...drawing.points];
61
+ points2[this.state.handle] = now;
62
+ this.options.store.update(this.state.id, { points: points2 }, { undoable: false });
63
+ } else {
64
+ const dPrice = now.price - this.state.from.price;
65
+ const dLogical = now.logical - this.state.from.logical;
66
+ const points2 = this.state.grabbed.map((p) => this.shift(p, dLogical, dPrice));
67
+ this.options.store.update(this.state.id, { points: points2 }, { undoable: false });
68
+ }
69
+ };
70
+ this.onPointerUp = () => {
71
+ if (this.state.name === "placing") {
72
+ const needed = this.options.pointCount(this.state.tool);
73
+ const placed = this.state.placed + 1;
74
+ if (placed >= needed) {
75
+ if (this.options.isTransient(this.state.tool)) {
76
+ this.setActiveTool(null);
77
+ this.options.store.remove(this.state.id, { undoable: false });
78
+ } else {
79
+ this.options.store.update(this.state.id, { draft: false }, { undoable: false });
80
+ this.finishPlacing(this.state.id);
81
+ }
82
+ } else {
83
+ this.state = { ...this.state, placed };
84
+ return;
85
+ }
86
+ } else if (this.state.name === "dragging") {
87
+ this.state = { name: "idle" };
88
+ this.restoreChartGestures();
89
+ return;
90
+ }
91
+ this.state = { name: "idle" };
92
+ this.restoreChartGestures();
93
+ };
94
+ // -- keyboard -------------------------------------------------------------
95
+ this.onKeyDown = (event) => {
96
+ const target = event.target;
97
+ if (target !== null && (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName))) {
98
+ return;
99
+ }
100
+ const mod = event.metaKey || event.ctrlKey;
101
+ if (event.key === "Escape") {
102
+ if (this.state.name === "placing") {
103
+ this.options.store.remove(this.state.id);
104
+ this.state = { name: "idle" };
105
+ this.restoreChartGestures();
106
+ }
107
+ this.setActiveTool(null);
108
+ this.options.store.select(null);
109
+ return;
110
+ }
111
+ if (mod && event.key.toLowerCase() === "z") {
112
+ event.preventDefault();
113
+ if (event.shiftKey) this.options.store.redo();
114
+ else this.options.store.undo();
115
+ return;
116
+ }
117
+ if (event.key === "Backspace" || event.key === "Delete") {
118
+ const selected = this.options.store.selectedId();
119
+ if (selected !== null) {
120
+ event.preventDefault();
121
+ this.options.store.remove(selected);
122
+ }
123
+ }
124
+ };
125
+ this.element = options.chart.chartElement();
126
+ this.element.addEventListener("pointerdown", this.onPointerDown);
127
+ this.element.addEventListener("pointermove", this.onPointerMove);
128
+ window.addEventListener("pointerup", this.onPointerUp);
129
+ window.addEventListener("keydown", this.onKeyDown);
130
+ }
131
+ destroy() {
132
+ this.element.removeEventListener("pointerdown", this.onPointerDown);
133
+ this.element.removeEventListener("pointermove", this.onPointerMove);
134
+ window.removeEventListener("pointerup", this.onPointerUp);
135
+ window.removeEventListener("keydown", this.onKeyDown);
136
+ this.restoreChartGestures();
137
+ }
138
+ /** Arm a tool. The next drag on the chart draws one, then disarms. */
139
+ setActiveTool(tool) {
140
+ this.activeTool = tool;
141
+ this.element.style.cursor = tool === null ? "" : "crosshair";
142
+ }
143
+ getActiveTool() {
144
+ return this.activeTool;
145
+ }
146
+ // -- pointer --------------------------------------------------------------
147
+ local(event) {
148
+ const rect = this.element.getBoundingClientRect();
149
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
150
+ }
151
+ beginPlacing(tool, at) {
152
+ const id = globalThis.crypto?.randomUUID?.() ?? `d${String(Date.now())}${String(Math.random())}`;
153
+ const point = this.toPoint(at, false);
154
+ const needed = this.options.pointCount(tool);
155
+ this.suppressChartGestures();
156
+ this.options.store.add({
157
+ id,
158
+ tool,
159
+ points: Array.from({ length: needed }, () => ({ ...point })),
160
+ draft: true
161
+ });
162
+ this.state = { name: "placing", id, tool, placed: 1 };
163
+ }
164
+ finishPlacing(id) {
165
+ this.setActiveTool(null);
166
+ this.options.store.select(id);
167
+ }
168
+ // -- coordinates ----------------------------------------------------------
169
+ /**
170
+ * Pixels to a data-space anchor.
171
+ *
172
+ * `magnet` snaps the price to the nearest of the bar's open/high/low/close
173
+ * when the pointer is close to one. Traders anchor to actual highs and lows
174
+ * constantly -- a trendline off by two paise looks wrong and misprices
175
+ * every projection off it -- and hitting an exact value by hand at this
176
+ * zoom is impossible. Holding shift turns it off for freehand placement.
177
+ */
178
+ toPoint(at, disableMagnet) {
179
+ const timeScale = this.options.chart.timeScale();
180
+ const logical = timeScale.coordinateToLogical(at.x) ?? 0;
181
+ const time = timeScale.coordinateToTime(at.x);
182
+ const rawPrice = this.options.series.coordinateToPrice(at.y);
183
+ const price = rawPrice === null ? 0 : Number(rawPrice);
184
+ const magnetPx = this.options.magnetPx ?? 0;
185
+ if (disableMagnet || magnetPx <= 0) return { time, logical, price };
186
+ const snapped = this.snapToBar(logical, at.y, magnetPx);
187
+ return { time, logical, price: snapped ?? price };
188
+ }
189
+ /** Nearest OHLC of the bar under the cursor, if it is within `within` px. */
190
+ snapToBar(logical, y, within) {
191
+ const bar = this.options.series.dataByIndex(Math.round(logical));
192
+ if (bar === null || bar === void 0) return null;
193
+ const candle = bar;
194
+ const candidates = [candle.open, candle.high, candle.low, candle.close].filter(
195
+ (v) => typeof v === "number"
196
+ );
197
+ if (candidates.length === 0) return null;
198
+ let best = null;
199
+ let bestDistance = within;
200
+ for (const value of candidates) {
201
+ const coordinate = this.options.series.priceToCoordinate(value);
202
+ if (coordinate === null) continue;
203
+ const distance = Math.abs(coordinate - y);
204
+ if (distance < bestDistance) {
205
+ bestDistance = distance;
206
+ best = value;
207
+ }
208
+ }
209
+ return best;
210
+ }
211
+ /** Move an anchor by a delta expressed in bars and price. */
212
+ shift(point, dLogical, dPrice) {
213
+ const logical = point.logical + dLogical;
214
+ const time = this.options.chart.timeScale().coordinateToTime(
215
+ this.options.chart.timeScale().logicalToCoordinate(logical) ?? 0
216
+ );
217
+ return { time, logical, price: point.price + dPrice };
218
+ }
219
+ // -- chart gesture suppression -------------------------------------------
220
+ suppressChartGestures() {
221
+ const current = this.options.chart.options();
222
+ this.savedScroll = current.handleScroll;
223
+ this.savedScale = current.handleScale;
224
+ this.options.chart.applyOptions({ handleScroll: false, handleScale: false });
225
+ }
226
+ restoreChartGestures() {
227
+ this.options.chart.applyOptions({
228
+ handleScroll: this.savedScroll,
229
+ handleScale: this.savedScale
230
+ });
231
+ }
232
+ };
233
+
234
+ // src/core/serialise.ts
235
+ var SNAPSHOT_VERSION = 1;
236
+ function toSnapshot(drawings) {
237
+ return {
238
+ version: SNAPSHOT_VERSION,
239
+ drawings: drawings.filter((d) => d.draft !== true).map((d) => ({ ...d, points: d.points.map((p) => ({ ...p })) }))
240
+ };
241
+ }
242
+ function fromSnapshot(snapshot) {
243
+ if (snapshot.version !== SNAPSHOT_VERSION) {
244
+ throw new Error(
245
+ `trendkit: snapshot version ${String(snapshot.version)} is not supported by this build (expected ${String(SNAPSHOT_VERSION)})`
246
+ );
247
+ }
248
+ return snapshot.drawings.map((d) => ({
249
+ ...d,
250
+ points: d.points.map((p) => ({ ...p })),
251
+ // A stored drawing is never a draft, whatever the blob claims.
252
+ draft: false
253
+ }));
254
+ }
255
+
256
+ // src/core/store.ts
257
+ var DrawingStore = class {
258
+ constructor() {
259
+ this.items = [];
260
+ this.selected = null;
261
+ this.listeners = /* @__PURE__ */ new Set();
262
+ /** Undo history. Snapshots, not diffs -- drawings are small and few. */
263
+ this.past = [];
264
+ this.future = [];
265
+ }
266
+ subscribe(listener) {
267
+ this.listeners.add(listener);
268
+ return () => this.listeners.delete(listener);
269
+ }
270
+ notify() {
271
+ for (const listener of this.listeners) listener();
272
+ }
273
+ /** Snapshot for undo. Called before any mutation that should be undoable. */
274
+ checkpoint() {
275
+ this.past.push(structuredClone(this.items));
276
+ this.future.length = 0;
277
+ if (this.past.length > 100) this.past.shift();
278
+ }
279
+ all() {
280
+ return this.items;
281
+ }
282
+ selectedId() {
283
+ return this.selected;
284
+ }
285
+ get(id) {
286
+ return this.items.find((d) => d.id === id);
287
+ }
288
+ add(drawing, options = {}) {
289
+ if (options.undoable !== false) this.checkpoint();
290
+ this.items.push(drawing);
291
+ this.notify();
292
+ }
293
+ update(id, patch, options = {}) {
294
+ const index = this.items.findIndex((d) => d.id === id);
295
+ if (index === -1) return;
296
+ const current = this.items[index];
297
+ if (current === void 0) return;
298
+ if (options.undoable === true) this.checkpoint();
299
+ this.items[index] = { ...current, ...patch };
300
+ this.notify();
301
+ }
302
+ remove(id, options = {}) {
303
+ const index = this.items.findIndex((d) => d.id === id);
304
+ if (index === -1) return;
305
+ if (options.undoable !== false) this.checkpoint();
306
+ this.items.splice(index, 1);
307
+ if (this.selected === id) this.selected = null;
308
+ this.notify();
309
+ }
310
+ select(id) {
311
+ if (this.selected === id) return;
312
+ this.selected = id;
313
+ this.notify();
314
+ }
315
+ replaceAll(drawings) {
316
+ this.checkpoint();
317
+ this.items = drawings;
318
+ this.selected = null;
319
+ this.notify();
320
+ }
321
+ undo() {
322
+ const previous = this.past.pop();
323
+ if (previous === void 0) return;
324
+ this.future.push(structuredClone(this.items));
325
+ this.items = previous;
326
+ this.selected = null;
327
+ this.notify();
328
+ }
329
+ redo() {
330
+ const next = this.future.pop();
331
+ if (next === void 0) return;
332
+ this.past.push(structuredClone(this.items));
333
+ this.items = next;
334
+ this.selected = null;
335
+ this.notify();
336
+ }
337
+ canUndo() {
338
+ return this.past.length > 0;
339
+ }
340
+ canRedo() {
341
+ return this.future.length > 0;
342
+ }
343
+ };
344
+
345
+ // src/render/primitive.ts
346
+ var HIT_TOLERANCE = 6;
347
+ var HANDLE_RADIUS = 7;
348
+ var TrendkitPrimitive = class {
349
+ constructor(chart, series, source) {
350
+ this.chart = chart;
351
+ this.series = series;
352
+ this.source = source;
353
+ /** Cached axis views, and the signature they were built from. */
354
+ this.axisViews = [];
355
+ this.axisSignature = "";
356
+ /**
357
+ * Resolve a data-space anchor to pixels.
358
+ *
359
+ * Time first, logical as the fallback. `timeToCoordinate` returns null for
360
+ * anything past the last bar -- which is exactly where a user drags a
361
+ * trendline to project it forward -- and `logicalToCoordinate` keeps
362
+ * counting into that empty space.
363
+ */
364
+ this.toScreen = (point) => {
365
+ const timeScale = this.chart.timeScale();
366
+ const x = (point.time !== null ? timeScale.timeToCoordinate(point.time) : null) ?? timeScale.logicalToCoordinate(point.logical);
367
+ const y = this.series.priceToCoordinate(point.price);
368
+ if (x === null || y === null) return null;
369
+ return { x, y };
370
+ };
371
+ this.toY = (price) => this.series.priceToCoordinate(price);
372
+ const renderer = {
373
+ draw: (target) => {
374
+ target.useBitmapCoordinateSpace((scope) => {
375
+ const context = this.context(
376
+ scope.context,
377
+ scope.horizontalPixelRatio,
378
+ scope.mediaSize.width,
379
+ scope.mediaSize.height
380
+ );
381
+ const selected = this.source.selectedId();
382
+ for (const drawing of this.source.drawings()) {
383
+ const tool = this.source.tool(drawing.tool);
384
+ tool?.draw(drawing, drawing.id === selected, context);
385
+ }
386
+ });
387
+ }
388
+ };
389
+ this.view = {
390
+ // Above the series. A trendline drawn behind the candles it references
391
+ // is worse than useless -- the candles are exactly what it marks.
392
+ zOrder: () => "top",
393
+ renderer: () => renderer
394
+ };
395
+ }
396
+ // -- ISeriesPrimitive -----------------------------------------------------
397
+ attached(param) {
398
+ this.requestUpdate = param.requestUpdate;
399
+ }
400
+ detached() {
401
+ this.requestUpdate = void 0;
402
+ }
403
+ /**
404
+ * Called by the library before it re-reads the views.
405
+ *
406
+ * A no-op only because everything here is computed lazily inside the
407
+ * renderer and `priceAxisViews`. It still has to EXIST: without it the
408
+ * library has no signal that this primitive's views may have changed, and
409
+ * keeps the empty axis-view array it collected at attach time -- so axis
410
+ * labels never appear no matter how many drawings are added.
411
+ */
412
+ updateAllViews() {
413
+ }
414
+ paneViews() {
415
+ return [this.view];
416
+ }
417
+ /**
418
+ * Price-scale labels for every drawing that wants one.
419
+ *
420
+ * Rebuilt only when the set of labelled prices actually changes. The
421
+ * library caches on array IDENTITY, so returning a fresh array each call
422
+ * invalidates that cache on every frame of every pan -- the interface
423
+ * documentation asks for this explicitly.
424
+ */
425
+ priceAxisViews() {
426
+ const wanted = [];
427
+ for (const drawing of this.source.drawings()) {
428
+ const tool = this.source.tool(drawing.tool);
429
+ for (const price of tool?.axisPrices?.(drawing) ?? []) {
430
+ wanted.push({
431
+ price,
432
+ colour: drawing.style?.colour ?? this.source.theme().accent
433
+ });
434
+ }
435
+ }
436
+ const signature = wanted.map((w) => `${String(w.price)}:${w.colour}`).join("|");
437
+ if (signature === this.axisSignature) return this.axisViews;
438
+ this.axisSignature = signature;
439
+ this.axisViews = wanted.map(({ price, colour }) => ({
440
+ coordinate: () => this.series.priceToCoordinate(price) ?? -100,
441
+ text: () => this.formatPrice(price),
442
+ textColor: () => "#ffffff",
443
+ backColor: () => colour
444
+ }));
445
+ return this.axisViews;
446
+ }
447
+ /**
448
+ * What the pointer is over, in the form Lightweight Charts wants.
449
+ *
450
+ * `externalId` encodes the target so the interaction controller can decode
451
+ * it without hit-testing a second time: `handle:<id>:<index>` or
452
+ * `body:<id>`. The library gives us one string, so it carries the lot.
453
+ */
454
+ hitTest(x, y) {
455
+ const hit = this.hitTestDetailed({ x, y });
456
+ if (hit.kind === "none") return null;
457
+ return {
458
+ externalId: hit.kind === "handle" ? `handle:${hit.id}:${hit.index}` : `body:${hit.id}`,
459
+ zOrder: "top",
460
+ cursorStyle: hit.kind === "handle" ? "grab" : "move"
461
+ };
462
+ }
463
+ // -- Used by the interaction controller -----------------------------------
464
+ /** Ask the chart to repaint. Called whenever the store changes. */
465
+ update() {
466
+ this.requestUpdate?.();
467
+ }
468
+ /**
469
+ * Full hit test, in priority order.
470
+ *
471
+ * Handles before bodies, and the selected drawing before the rest. Both
472
+ * matter: a handle sits on top of its own body, so testing bodies first
473
+ * would make endpoints ungrabbable; and once something is selected the user
474
+ * is working on it, so it should win ties against whatever it overlaps.
475
+ */
476
+ hitTestDetailed(at) {
477
+ const context = this.contextForHitTest();
478
+ if (context === null) return { kind: "none" };
479
+ const drawings = this.source.drawings();
480
+ const selectedId = this.source.selectedId();
481
+ const ordered = [...drawings].sort((a, b) => {
482
+ if (a.id === selectedId) return 1;
483
+ if (b.id === selectedId) return -1;
484
+ return 0;
485
+ });
486
+ for (let i = ordered.length - 1; i >= 0; i--) {
487
+ const drawing = ordered[i];
488
+ if (drawing === void 0) continue;
489
+ for (let index = 0; index < drawing.points.length; index++) {
490
+ const point = drawing.points[index];
491
+ if (point === void 0) continue;
492
+ const screen = context.toScreen(point);
493
+ if (screen === null) continue;
494
+ if (Math.hypot(at.x - screen.x, at.y - screen.y) <= HANDLE_RADIUS) {
495
+ return { kind: "handle", id: drawing.id, index };
496
+ }
497
+ }
498
+ }
499
+ for (let i = ordered.length - 1; i >= 0; i--) {
500
+ const drawing = ordered[i];
501
+ if (drawing === void 0) continue;
502
+ const tool = this.source.tool(drawing.tool);
503
+ const distance = tool?.distance(drawing, at, context);
504
+ if (distance !== null && distance !== void 0 && distance <= HIT_TOLERANCE) {
505
+ return { kind: "body", id: drawing.id };
506
+ }
507
+ }
508
+ return { kind: "none" };
509
+ }
510
+ context(ctx, ratio, width, height) {
511
+ return {
512
+ ctx,
513
+ ratio,
514
+ width,
515
+ height,
516
+ theme: this.source.theme(),
517
+ toScreen: this.toScreen,
518
+ toY: this.toY
519
+ };
520
+ }
521
+ /**
522
+ * Format a price for an axis label.
523
+ *
524
+ * Uses the series' own formatter where it has one, so a drawing's label
525
+ * matches the scale beside it -- a chart reading 1,240.50 on the axis and
526
+ * 1240.5 on a drawing looks like two different numbers at a glance.
527
+ */
528
+ formatPrice(price) {
529
+ const format = this.series.priceFormatter;
530
+ if (typeof format === "function") {
531
+ try {
532
+ return format.call(this.series).format(price);
533
+ } catch {
534
+ }
535
+ }
536
+ return price.toFixed(2);
537
+ }
538
+ /** The same context minus the canvas, for hit tests outside a paint. */
539
+ contextForHitTest() {
540
+ return {
541
+ ratio: 1,
542
+ width: this.chart.timeScale().width(),
543
+ height: 0,
544
+ theme: this.source.theme(),
545
+ toScreen: this.toScreen,
546
+ toY: this.toY
547
+ };
548
+ }
549
+ };
550
+
551
+ // src/render/theme.ts
552
+ var LIGHT_THEME = {
553
+ accent: "#2962ff",
554
+ selection: "#2962ff",
555
+ text: "#131722",
556
+ labelBackground: "#ffffffe6",
557
+ fill: "#2962ff1f"
558
+ };
559
+ var DARK_THEME = {
560
+ accent: "#5b8def",
561
+ selection: "#5b8def",
562
+ text: "#d1d4dc",
563
+ labelBackground: "#1e222de6",
564
+ fill: "#5b8def24"
565
+ };
566
+
567
+ // src/core/geometry.ts
568
+ function distanceToSegment(p, a, b) {
569
+ const dx = b.x - a.x;
570
+ const dy = b.y - a.y;
571
+ const lengthSquared = dx * dx + dy * dy;
572
+ if (lengthSquared === 0) return Math.hypot(p.x - a.x, p.y - a.y);
573
+ const t = Math.max(
574
+ 0,
575
+ Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / lengthSquared)
576
+ );
577
+ return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
578
+ }
579
+ function distanceToHorizontal(p, y) {
580
+ return Math.abs(p.y - y);
581
+ }
582
+ function distanceToRect(p, a, b) {
583
+ const left = Math.min(a.x, b.x);
584
+ const right = Math.max(a.x, b.x);
585
+ const top = Math.min(a.y, b.y);
586
+ const bottom = Math.max(a.y, b.y);
587
+ if (p.x >= left && p.x <= right && p.y >= top && p.y <= bottom) return 0;
588
+ const dx = Math.max(left - p.x, 0, p.x - right);
589
+ const dy = Math.max(top - p.y, 0, p.y - bottom);
590
+ return Math.hypot(dx, dy);
591
+ }
592
+ var FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
593
+ function fibonacciPrices(from, to) {
594
+ const span = to - from;
595
+ return FIB_LEVELS.map((level) => from + span * level);
596
+ }
597
+ function percentChange(from, to) {
598
+ if (from === 0) return 0;
599
+ return (to - from) / Math.abs(from) * 100;
600
+ }
601
+
602
+ // src/render/draw.ts
603
+ function toBitmap(value, ratio) {
604
+ return Math.round(value * ratio);
605
+ }
606
+ function snapLine(value, ratio, width) {
607
+ const scaled = Math.round(value * ratio);
608
+ return width % 2 === 1 ? scaled + 0.5 : scaled;
609
+ }
610
+ function dashPattern(style, ratio) {
611
+ switch (style) {
612
+ case "dashed":
613
+ return [6 * ratio, 4 * ratio];
614
+ case "dotted":
615
+ return [1 * ratio, 3 * ratio];
616
+ case "solid":
617
+ return [];
618
+ }
619
+ }
620
+ function strokeSegment(ctx, a, b, options) {
621
+ const { colour, width, style = "solid", ratio } = options;
622
+ const deviceWidth = Math.max(1, Math.round(width * ratio));
623
+ ctx.save();
624
+ ctx.strokeStyle = colour;
625
+ ctx.lineWidth = deviceWidth;
626
+ ctx.setLineDash(dashPattern(style, ratio));
627
+ ctx.beginPath();
628
+ ctx.moveTo(snapLine(a.x, ratio, deviceWidth), snapLine(a.y, ratio, deviceWidth));
629
+ ctx.lineTo(snapLine(b.x, ratio, deviceWidth), snapLine(b.y, ratio, deviceWidth));
630
+ ctx.stroke();
631
+ ctx.restore();
632
+ }
633
+ function fillRect(ctx, a, b, colour, ratio) {
634
+ const x = toBitmap(Math.min(a.x, b.x), ratio);
635
+ const y = toBitmap(Math.min(a.y, b.y), ratio);
636
+ const w = toBitmap(Math.abs(b.x - a.x), ratio);
637
+ const h = toBitmap(Math.abs(b.y - a.y), ratio);
638
+ ctx.save();
639
+ ctx.fillStyle = colour;
640
+ ctx.fillRect(x, y, w, h);
641
+ ctx.restore();
642
+ }
643
+ function drawHandle(ctx, point, colour, ratio, radius = 4) {
644
+ const r = radius * ratio;
645
+ ctx.save();
646
+ ctx.beginPath();
647
+ ctx.arc(toBitmap(point.x, ratio), toBitmap(point.y, ratio), r, 0, Math.PI * 2);
648
+ ctx.fillStyle = "#ffffff";
649
+ ctx.fill();
650
+ ctx.lineWidth = Math.max(1, Math.round(1.5 * ratio));
651
+ ctx.strokeStyle = colour;
652
+ ctx.stroke();
653
+ ctx.restore();
654
+ }
655
+ function drawLabel(ctx, text, at, options) {
656
+ const { colour, background, ratio, align = "left" } = options;
657
+ const fontSize = Math.round(11 * ratio);
658
+ const padX = Math.round(4 * ratio);
659
+ const padY = Math.round(2 * ratio);
660
+ ctx.save();
661
+ ctx.font = `${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`;
662
+ ctx.textBaseline = "middle";
663
+ const width = ctx.measureText(text).width;
664
+ const x = align === "right" ? toBitmap(at.x, ratio) - width - padX * 2 : toBitmap(at.x, ratio);
665
+ const y = toBitmap(at.y, ratio);
666
+ ctx.fillStyle = background;
667
+ ctx.fillRect(x, y - fontSize / 2 - padY, width + padX * 2, fontSize + padY * 2);
668
+ ctx.fillStyle = colour;
669
+ ctx.fillText(text, x + padX, y);
670
+ ctx.restore();
671
+ }
672
+
673
+ // src/tools/trendline.ts
674
+ var Trendline = {
675
+ id: "trendline",
676
+ pointCount: 2,
677
+ draw(drawing, selected, context) {
678
+ const [start, end] = resolve(drawing, context);
679
+ if (start === null || end === null) return;
680
+ const style = drawing.style ?? {};
681
+ strokeSegment(context.ctx, start, end, {
682
+ colour: style.colour ?? context.theme.accent,
683
+ width: style.lineWidth ?? 2,
684
+ style: style.lineStyle ?? "solid",
685
+ ratio: context.ratio
686
+ });
687
+ if (selected) {
688
+ const colour = context.theme.selection;
689
+ drawHandle(context.ctx, start, colour, context.ratio);
690
+ drawHandle(context.ctx, end, colour, context.ratio);
691
+ }
692
+ },
693
+ distance(drawing, at, context) {
694
+ const [start, end] = resolve(drawing, context);
695
+ if (start === null || end === null) return null;
696
+ return distanceToSegment(at, start, end);
697
+ }
698
+ };
699
+ function resolve(drawing, context) {
700
+ const a = drawing.points[0];
701
+ const b = drawing.points[1];
702
+ return [
703
+ a === void 0 ? null : context.toScreen(a),
704
+ b === void 0 ? null : context.toScreen(b)
705
+ ];
706
+ }
707
+
708
+ // src/tools/horizontal-ray.ts
709
+ var HorizontalRay = {
710
+ id: "horizontal-ray",
711
+ pointCount: 1,
712
+ draw(drawing, selected, context) {
713
+ const anchor = drawing.points[0];
714
+ if (anchor === void 0) return;
715
+ const at = context.toScreen(anchor);
716
+ if (at === null) return;
717
+ const style = drawing.style ?? {};
718
+ const colour = style.colour ?? context.theme.accent;
719
+ strokeSegment(
720
+ context.ctx,
721
+ { x: at.x, y: at.y },
722
+ // To the right edge, not to a second anchor.
723
+ { x: context.width, y: at.y },
724
+ {
725
+ colour,
726
+ width: style.lineWidth ?? 2,
727
+ style: style.lineStyle ?? "solid",
728
+ ratio: context.ratio
729
+ }
730
+ );
731
+ if (selected) drawHandle(context.ctx, at, context.theme.selection, context.ratio);
732
+ },
733
+ axisPrices(drawing) {
734
+ const anchor = drawing.points[0];
735
+ return anchor === void 0 ? [] : [anchor.price];
736
+ },
737
+ distance(drawing, at, context) {
738
+ const anchor = drawing.points[0];
739
+ if (anchor === void 0) return null;
740
+ const screen = context.toScreen(anchor);
741
+ if (screen === null) return null;
742
+ if (at.x < screen.x) return null;
743
+ return distanceToHorizontal(at, screen.y);
744
+ }
745
+ };
746
+
747
+ // src/tools/fibonacci.ts
748
+ var BAND_ALPHA = "14";
749
+ var Fibonacci = {
750
+ id: "fibonacci",
751
+ pointCount: 2,
752
+ draw(drawing, selected, context) {
753
+ const a = drawing.points[0];
754
+ const b = drawing.points[1];
755
+ if (a === void 0 || b === void 0) return;
756
+ const pa = context.toScreen(a);
757
+ const pb = context.toScreen(b);
758
+ if (pa === null || pb === null) return;
759
+ const colour = drawing.style?.colour ?? context.theme.accent;
760
+ const left = Math.min(pa.x, pb.x);
761
+ const right = Math.max(pa.x, pb.x);
762
+ const prices = fibonacciPrices(a.price, b.price);
763
+ for (let i = 0; i < prices.length - 1; i++) {
764
+ const top = yOf(prices[i], context);
765
+ const bottom = yOf(prices[i + 1], context);
766
+ if (top === null || bottom === null) continue;
767
+ if (i % 2 === 0) continue;
768
+ fillRect(
769
+ context.ctx,
770
+ { x: left, y: top },
771
+ { x: right, y: bottom },
772
+ `${colour}${BAND_ALPHA}`,
773
+ context.ratio
774
+ );
775
+ }
776
+ prices.forEach((price, i) => {
777
+ const y = yOf(price, context);
778
+ if (y === null) return;
779
+ const level = FIB_LEVELS[i] ?? 0;
780
+ strokeSegment(
781
+ context.ctx,
782
+ { x: left, y },
783
+ { x: right, y },
784
+ {
785
+ colour,
786
+ // The 0.618 line drawn heavier: it is the one people act on, and a
787
+ // ladder of seven identical lines makes it hunt for.
788
+ width: level === 0.618 ? 2 : 1,
789
+ style: level === 0 || level === 1 ? "solid" : "dashed",
790
+ ratio: context.ratio
791
+ }
792
+ );
793
+ drawLabel(context.ctx, `${level.toFixed(3)} ${price.toFixed(2)}`, { x: left + 4, y }, {
794
+ colour: context.theme.text,
795
+ background: context.theme.labelBackground,
796
+ ratio: context.ratio
797
+ });
798
+ });
799
+ if (selected) {
800
+ drawHandle(context.ctx, pa, context.theme.selection, context.ratio);
801
+ drawHandle(context.ctx, pb, context.theme.selection, context.ratio);
802
+ }
803
+ },
804
+ distance(drawing, at, context) {
805
+ const a = drawing.points[0];
806
+ const b = drawing.points[1];
807
+ if (a === void 0 || b === void 0) return null;
808
+ const pa = context.toScreen(a);
809
+ const pb = context.toScreen(b);
810
+ if (pa === null || pb === null) return null;
811
+ return distanceToRect(at, pa, pb);
812
+ }
813
+ };
814
+ function yOf(price, context) {
815
+ return price === void 0 ? null : context.toY(price);
816
+ }
817
+
818
+ // src/tools/rectangle.ts
819
+ var Rectangle = {
820
+ id: "rectangle",
821
+ pointCount: 2,
822
+ draw(drawing, selected, context) {
823
+ const a = drawing.points[0];
824
+ const b = drawing.points[1];
825
+ if (a === void 0 || b === void 0) return;
826
+ const pa = context.toScreen(a);
827
+ const pb = context.toScreen(b);
828
+ if (pa === null || pb === null) return;
829
+ const style = drawing.style ?? {};
830
+ const colour = style.colour ?? context.theme.accent;
831
+ fillRect(context.ctx, pa, pb, style.fill ?? context.theme.fill, context.ratio);
832
+ const corners = [
833
+ [{ x: pa.x, y: pa.y }, { x: pb.x, y: pa.y }],
834
+ [{ x: pb.x, y: pa.y }, { x: pb.x, y: pb.y }],
835
+ [{ x: pb.x, y: pb.y }, { x: pa.x, y: pb.y }],
836
+ [{ x: pa.x, y: pb.y }, { x: pa.x, y: pa.y }]
837
+ ];
838
+ for (const [from, to] of corners) {
839
+ strokeSegment(context.ctx, from, to, {
840
+ colour,
841
+ width: style.lineWidth ?? 1,
842
+ style: style.lineStyle ?? "solid",
843
+ ratio: context.ratio
844
+ });
845
+ }
846
+ if (selected) {
847
+ drawHandle(context.ctx, pa, context.theme.selection, context.ratio);
848
+ drawHandle(context.ctx, pb, context.theme.selection, context.ratio);
849
+ }
850
+ },
851
+ axisPrices(drawing) {
852
+ return drawing.points.map((p) => p.price);
853
+ },
854
+ distance(drawing, at, context) {
855
+ const a = drawing.points[0];
856
+ const b = drawing.points[1];
857
+ if (a === void 0 || b === void 0) return null;
858
+ const pa = context.toScreen(a);
859
+ const pb = context.toScreen(b);
860
+ if (pa === null || pb === null) return null;
861
+ return distanceToRect(at, pa, pb);
862
+ }
863
+ };
864
+
865
+ // src/tools/measure.ts
866
+ var Measure = {
867
+ id: "measure",
868
+ pointCount: 2,
869
+ transient: true,
870
+ draw(drawing, _selected, context) {
871
+ const a = drawing.points[0];
872
+ const b = drawing.points[1];
873
+ if (a === void 0 || b === void 0) return;
874
+ const pa = context.toScreen(a);
875
+ const pb = context.toScreen(b);
876
+ if (pa === null || pb === null) return;
877
+ const up = b.price >= a.price;
878
+ const colour = up ? "#26a69a" : "#ef5350";
879
+ fillRect(context.ctx, pa, pb, `${colour}1f`, context.ratio);
880
+ strokeSegment(context.ctx, pa, pb, {
881
+ colour,
882
+ width: 1,
883
+ style: "dashed",
884
+ ratio: context.ratio
885
+ });
886
+ const change = percentChange(a.price, b.price);
887
+ const delta = b.price - a.price;
888
+ const bars = Math.abs(Math.round(b.logical - a.logical));
889
+ const text = `${delta >= 0 ? "+" : ""}${delta.toFixed(2)} (${change >= 0 ? "+" : ""}${change.toFixed(2)}%) ${String(bars)} ${bars === 1 ? "bar" : "bars"}`;
890
+ drawLabel(
891
+ context.ctx,
892
+ text,
893
+ { x: Math.min(pa.x, pb.x) + 6, y: up ? Math.min(pa.y, pb.y) - 12 : Math.max(pa.y, pb.y) + 12 },
894
+ {
895
+ colour: "#ffffff",
896
+ background: colour,
897
+ ratio: context.ratio
898
+ }
899
+ );
900
+ },
901
+ distance() {
902
+ return null;
903
+ }
904
+ };
905
+
906
+ // src/indicators/math.ts
907
+ function sma(values, period) {
908
+ if (period <= 0) throw new Error("sma: period must be positive");
909
+ const out = [];
910
+ let sum = 0;
911
+ for (let i = 0; i < values.length; i++) {
912
+ sum += values[i] ?? 0;
913
+ if (i >= period) sum -= values[i - period] ?? 0;
914
+ out.push(i >= period - 1 ? sum / period : null);
915
+ }
916
+ return out;
917
+ }
918
+ function ema(values, period) {
919
+ if (period <= 0) throw new Error("ema: period must be positive");
920
+ const k = 2 / (period + 1);
921
+ const out = [];
922
+ let previous = null;
923
+ let seed = 0;
924
+ for (let i = 0; i < values.length; i++) {
925
+ const value = values[i] ?? 0;
926
+ if (previous === null) {
927
+ seed += value;
928
+ if (i === period - 1) {
929
+ previous = seed / period;
930
+ out.push(previous);
931
+ } else {
932
+ out.push(null);
933
+ }
934
+ continue;
935
+ }
936
+ previous = value * k + previous * (1 - k);
937
+ out.push(previous);
938
+ }
939
+ return out;
940
+ }
941
+ function wilder(values, period) {
942
+ if (period <= 0) throw new Error("wilder: period must be positive");
943
+ const out = [];
944
+ let previous = null;
945
+ let seed = 0;
946
+ for (let i = 0; i < values.length; i++) {
947
+ const value = values[i] ?? 0;
948
+ if (previous === null) {
949
+ seed += value;
950
+ if (i === period - 1) {
951
+ previous = seed / period;
952
+ out.push(previous);
953
+ } else {
954
+ out.push(null);
955
+ }
956
+ continue;
957
+ }
958
+ previous = (previous * (period - 1) + value) / period;
959
+ out.push(previous);
960
+ }
961
+ return out;
962
+ }
963
+ function stdev(values, period) {
964
+ if (period <= 0) throw new Error("stdev: period must be positive");
965
+ const out = [];
966
+ for (let i = 0; i < values.length; i++) {
967
+ if (i < period - 1) {
968
+ out.push(null);
969
+ continue;
970
+ }
971
+ let sum = 0;
972
+ for (let j = i - period + 1; j <= i; j++) sum += values[j] ?? 0;
973
+ const mean = sum / period;
974
+ let variance = 0;
975
+ for (let j = i - period + 1; j <= i; j++) {
976
+ const d = (values[j] ?? 0) - mean;
977
+ variance += d * d;
978
+ }
979
+ out.push(Math.sqrt(Math.max(0, variance / period)));
980
+ }
981
+ return out;
982
+ }
983
+
984
+ // src/indicators/bollinger.ts
985
+ function bollinger(closes, period = 20, multiplier = 2) {
986
+ const middle = sma(closes, period);
987
+ const deviation = stdev(closes, period);
988
+ const upper = [];
989
+ const lower = [];
990
+ for (let i = 0; i < closes.length; i++) {
991
+ const m = middle[i];
992
+ const d = deviation[i];
993
+ if (m === null || d === null || m === void 0 || d === void 0) {
994
+ upper.push(null);
995
+ lower.push(null);
996
+ continue;
997
+ }
998
+ upper.push(m + multiplier * d);
999
+ lower.push(m - multiplier * d);
1000
+ }
1001
+ return { upper, middle, lower };
1002
+ }
1003
+
1004
+ // src/indicators/macd.ts
1005
+ function macd(closes, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) {
1006
+ const fast = ema(closes, fastPeriod);
1007
+ const slow = ema(closes, slowPeriod);
1008
+ const line = closes.map((_, i) => {
1009
+ const f = fast[i];
1010
+ const s = slow[i];
1011
+ return f === null || s === null || f === void 0 || s === void 0 ? null : f - s;
1012
+ });
1013
+ const firstReal = line.findIndex((v) => v !== null);
1014
+ const signal = line.map(() => null);
1015
+ if (firstReal !== -1) {
1016
+ const dense = line.slice(firstReal);
1017
+ const smoothed = ema(dense, signalPeriod);
1018
+ for (let i = 0; i < smoothed.length; i++) signal[firstReal + i] = smoothed[i] ?? null;
1019
+ }
1020
+ const histogram = line.map((value, i) => {
1021
+ const s = signal[i];
1022
+ return value === null || s === null || s === void 0 ? null : value - s;
1023
+ });
1024
+ return { macd: line, signal, histogram };
1025
+ }
1026
+
1027
+ // src/indicators/rsi.ts
1028
+ function rsi(closes, period = 14) {
1029
+ if (closes.length === 0) return [];
1030
+ const gains = [];
1031
+ const losses = [];
1032
+ for (let i = 1; i < closes.length; i++) {
1033
+ const change = (closes[i] ?? 0) - (closes[i - 1] ?? 0);
1034
+ gains.push(Math.max(0, change));
1035
+ losses.push(Math.max(0, -change));
1036
+ }
1037
+ const avgGain = wilder(gains, period);
1038
+ const avgLoss = wilder(losses, period);
1039
+ const out = [null];
1040
+ for (let i = 0; i < avgGain.length; i++) {
1041
+ const g = avgGain[i];
1042
+ const l = avgLoss[i];
1043
+ if (g === null || l === null || g === void 0 || l === void 0) {
1044
+ out.push(null);
1045
+ continue;
1046
+ }
1047
+ out.push(l === 0 ? 100 : 100 - 100 / (1 + g / l));
1048
+ }
1049
+ return out;
1050
+ }
1051
+
1052
+ // src/indicators/attach.ts
1053
+ function points(bars, values) {
1054
+ const out = [];
1055
+ for (let i = 0; i < bars.length; i++) {
1056
+ const value = values[i];
1057
+ const bar = bars[i];
1058
+ if (value === null || value === void 0 || bar === void 0) continue;
1059
+ out.push({ time: bar.time, value });
1060
+ }
1061
+ return out;
1062
+ }
1063
+ function addIndicator(chart, spec, bars) {
1064
+ const created = [];
1065
+ let ownPaneIndex = null;
1066
+ function newPane() {
1067
+ const index = chart.panes().length;
1068
+ ownPaneIndex = index;
1069
+ return index;
1070
+ }
1071
+ if (spec.type === "bollinger") {
1072
+ const colour = spec.colour ?? "#2962ff";
1073
+ const band = { lineWidth: 1, priceLineVisible: false, lastValueVisible: false };
1074
+ const upper = chart.addSeries(LineSeries, { color: colour, ...band });
1075
+ const middle = chart.addSeries(LineSeries, {
1076
+ color: colour,
1077
+ lineWidth: 1,
1078
+ lineStyle: 2,
1079
+ priceLineVisible: false,
1080
+ lastValueVisible: false
1081
+ });
1082
+ const lower = chart.addSeries(LineSeries, { color: colour, ...band });
1083
+ created.push(upper, middle, lower);
1084
+ const apply2 = (data) => {
1085
+ const closes = data.map((b) => b.close);
1086
+ const result = bollinger(closes, spec.period ?? 20, spec.multiplier ?? 2);
1087
+ upper.setData(points(data, result.upper));
1088
+ middle.setData(points(data, result.middle));
1089
+ lower.setData(points(data, result.lower));
1090
+ };
1091
+ apply2(bars);
1092
+ return { setData: apply2, remove: () => removeAll(chart, created, ownPaneIndex) };
1093
+ }
1094
+ if (spec.type === "rsi") {
1095
+ const pane2 = newPane();
1096
+ const line2 = chart.addSeries(
1097
+ LineSeries,
1098
+ {
1099
+ color: spec.colour ?? "#7e57c2",
1100
+ lineWidth: 2,
1101
+ priceLineVisible: false,
1102
+ // 0-100 by definition, so pinning the scale stops it rescaling to
1103
+ // the visible range and making a move from 45 to 55 look violent.
1104
+ autoscaleInfoProvider: () => ({ priceRange: { minValue: 0, maxValue: 100 } })
1105
+ },
1106
+ pane2
1107
+ );
1108
+ for (const level of [70, 30]) {
1109
+ line2.createPriceLine({
1110
+ price: level,
1111
+ color: "#9598a1",
1112
+ lineWidth: 1,
1113
+ lineStyle: 2,
1114
+ axisLabelVisible: true,
1115
+ title: ""
1116
+ });
1117
+ }
1118
+ created.push(line2);
1119
+ const apply2 = (data) => {
1120
+ line2.setData(points(data, rsi(data.map((b) => b.close), spec.period ?? 14)));
1121
+ };
1122
+ apply2(bars);
1123
+ chart.panes()[pane2]?.setHeight(110);
1124
+ return { setData: apply2, remove: () => removeAll(chart, created, ownPaneIndex) };
1125
+ }
1126
+ const pane = newPane();
1127
+ const histogram = chart.addSeries(
1128
+ HistogramSeries,
1129
+ { priceLineVisible: false, lastValueVisible: false },
1130
+ pane
1131
+ );
1132
+ const line = chart.addSeries(
1133
+ LineSeries,
1134
+ { color: "#2962ff", lineWidth: 2, priceLineVisible: false },
1135
+ pane
1136
+ );
1137
+ const signal = chart.addSeries(
1138
+ LineSeries,
1139
+ { color: "#ff6d00", lineWidth: 2, priceLineVisible: false },
1140
+ pane
1141
+ );
1142
+ created.push(histogram, line, signal);
1143
+ const apply = (data) => {
1144
+ const result = macd(
1145
+ data.map((b) => b.close),
1146
+ spec.fastPeriod ?? 12,
1147
+ spec.slowPeriod ?? 26,
1148
+ spec.signalPeriod ?? 9
1149
+ );
1150
+ line.setData(points(data, result.macd));
1151
+ signal.setData(points(data, result.signal));
1152
+ histogram.setData(
1153
+ points(data, result.histogram).map((p) => ({
1154
+ ...p,
1155
+ color: p.value >= 0 ? "#26a69a80" : "#ef535080"
1156
+ }))
1157
+ );
1158
+ };
1159
+ apply(bars);
1160
+ chart.panes()[pane]?.setHeight(110);
1161
+ return { setData: apply, remove: () => removeAll(chart, created, ownPaneIndex) };
1162
+ }
1163
+ function removeAll(chart, series, paneIndex) {
1164
+ for (const s of series) chart.removeSeries(s);
1165
+ series.length = 0;
1166
+ if (paneIndex !== null && chart.panes().length > paneIndex) {
1167
+ const pane = chart.panes()[paneIndex];
1168
+ if (pane !== void 0 && pane.getSeries().length === 0) chart.removePane(paneIndex);
1169
+ }
1170
+ }
1171
+
1172
+ // src/index.ts
1173
+ function createTrendkit(options) {
1174
+ const store = new DrawingStore();
1175
+ const tools = new Map(options.tools.map((tool) => [tool.id, tool]));
1176
+ let theme = options.theme ?? LIGHT_THEME;
1177
+ const primitive = new TrendkitPrimitive(options.chart, options.series, {
1178
+ drawings: () => store.all(),
1179
+ selectedId: () => store.selectedId(),
1180
+ tool: (id) => tools.get(id),
1181
+ theme: () => theme
1182
+ });
1183
+ options.series.attachPrimitive(primitive);
1184
+ const unsubscribe = store.subscribe(() => primitive.update());
1185
+ const interaction = new InteractionController({
1186
+ chart: options.chart,
1187
+ series: options.series,
1188
+ store,
1189
+ hits: primitive,
1190
+ pointCount: (id) => tools.get(id)?.pointCount ?? 2,
1191
+ isTransient: (id) => tools.get(id)?.transient === true,
1192
+ magnetPx: options.magnetPx ?? 6
1193
+ });
1194
+ return {
1195
+ store,
1196
+ setActiveTool(tool) {
1197
+ interaction.setActiveTool(tool);
1198
+ },
1199
+ activeTool() {
1200
+ return interaction.getActiveTool();
1201
+ },
1202
+ setTheme(next) {
1203
+ theme = next;
1204
+ primitive.update();
1205
+ },
1206
+ toJSON() {
1207
+ return toSnapshot(store.all());
1208
+ },
1209
+ fromJSON(snapshot) {
1210
+ store.replaceAll(fromSnapshot(snapshot));
1211
+ },
1212
+ destroy() {
1213
+ interaction.destroy();
1214
+ unsubscribe();
1215
+ options.series.detachPrimitive(primitive);
1216
+ }
1217
+ };
1218
+ }
1219
+
1220
+ export { DARK_THEME, FIB_LEVELS, Fibonacci, HorizontalRay, LIGHT_THEME, Measure, Rectangle, Trendline, addIndicator, bollinger, createTrendkit, distanceToRect, distanceToSegment, ema, fibonacciPrices, macd, percentChange, rsi, sma, stdev, wilder };