emberwick 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/index.d.ts +90 -0
- package/index.js +325 -13
- package/index.js.map +1 -1
- package/package.json +1 -1
- package/umd/emberwick.umd.js +1 -1
- package/umd/emberwick.umd.js.map +1 -1
package/index.js
CHANGED
|
@@ -169,7 +169,7 @@ class Tween {
|
|
|
169
169
|
return true;
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
-
const clamp$
|
|
172
|
+
const clamp$2 = (v, a, b) => v < a ? a : v > b ? b : v;
|
|
173
173
|
class TimeScale {
|
|
174
174
|
constructor({ spacing = 9, minSpacing = 0.8, maxSpacing = 160, rightOffset = 12 } = {}) {
|
|
175
175
|
this.minSpacing = minSpacing;
|
|
@@ -216,14 +216,14 @@ class TimeScale {
|
|
|
216
216
|
const first = Math.floor(this.index(0)) - 1;
|
|
217
217
|
const last = Math.ceil(this.index(this.width)) + 1;
|
|
218
218
|
return {
|
|
219
|
-
from: clamp$
|
|
220
|
-
to: clamp$
|
|
219
|
+
from: clamp$2(first, 0, Math.max(0, this.barCount - 1)),
|
|
220
|
+
to: clamp$2(last, 0, Math.max(0, this.barCount - 1))
|
|
221
221
|
};
|
|
222
222
|
}
|
|
223
223
|
_clampRight(v) {
|
|
224
224
|
const max = this.barCount - 1 + this.rightOffset + this.width / this._spacing.target;
|
|
225
225
|
const min = Math.min(4, this.barCount - 1 + this.rightOffset);
|
|
226
|
-
return clamp$
|
|
226
|
+
return clamp$2(v, min, max);
|
|
227
227
|
}
|
|
228
228
|
/** Immediate pan, in pixels. Positive dx drags content right (back in time). */
|
|
229
229
|
panBy(dxPx) {
|
|
@@ -236,7 +236,7 @@ class TimeScale {
|
|
|
236
236
|
/** Zoom by `factor`, keeping the bar under `x` pinned. */
|
|
237
237
|
zoomAt(x, factor) {
|
|
238
238
|
const s0 = this._spacing.target;
|
|
239
|
-
const s1 = clamp$
|
|
239
|
+
const s1 = clamp$2(s0 * factor, this.minSpacing, this.maxSpacing);
|
|
240
240
|
if (Math.abs(s1 - s0) < 1e-9) return false;
|
|
241
241
|
const anchorX = this.follow ? this.width : x;
|
|
242
242
|
const r0 = this._right.target;
|
|
@@ -250,6 +250,15 @@ class TimeScale {
|
|
|
250
250
|
this.follow = true;
|
|
251
251
|
this._right.set(this.barCount - 1 + this.rightOffset);
|
|
252
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Same anchor, no easing. Scrubbing re-targets the right edge many times a
|
|
255
|
+
* second; easing each one reads as the chart lagging the scrubber, which is
|
|
256
|
+
* the same reason a drag uses jump().
|
|
257
|
+
*/
|
|
258
|
+
jumpToRealtime() {
|
|
259
|
+
this.follow = true;
|
|
260
|
+
this._right.jump(this.barCount - 1 + this.rightOffset);
|
|
261
|
+
}
|
|
253
262
|
reset() {
|
|
254
263
|
this._spacing.set(this._initial);
|
|
255
264
|
this.snapToRealtime();
|
|
@@ -264,7 +273,7 @@ class TimeScale {
|
|
|
264
273
|
return this._spacing.settled && this._right.settled;
|
|
265
274
|
}
|
|
266
275
|
}
|
|
267
|
-
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
|
|
276
|
+
const clamp$1 = (v, a, b) => v < a ? a : v > b ? b : v;
|
|
268
277
|
class PriceScale {
|
|
269
278
|
constructor({ mode = "linear", tau = 120, marginTop = 0.12, marginBottom = 0.12 } = {}) {
|
|
270
279
|
this.mode = mode;
|
|
@@ -349,7 +358,7 @@ class PriceScale {
|
|
|
349
358
|
const a = this._lo.target;
|
|
350
359
|
const b = this._hi.target;
|
|
351
360
|
const mid = (a + b) / 2;
|
|
352
|
-
const half = (b - a) / 2 * clamp(factor, 0.2, 5);
|
|
361
|
+
const half = (b - a) / 2 * clamp$1(factor, 0.2, 5);
|
|
353
362
|
this._lo.set(mid - half);
|
|
354
363
|
this._hi.set(mid + half);
|
|
355
364
|
}
|
|
@@ -492,6 +501,202 @@ class Inertia {
|
|
|
492
501
|
return dx;
|
|
493
502
|
}
|
|
494
503
|
}
|
|
504
|
+
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
|
|
505
|
+
const MIN_SPEED = 0.25;
|
|
506
|
+
const MAX_SPEED = 500;
|
|
507
|
+
const MAX_STEPS_PER_FRAME = 240;
|
|
508
|
+
class Replay {
|
|
509
|
+
constructor(chart, options = {}) {
|
|
510
|
+
const src = Array.isArray(options.bars) ? options.bars : [];
|
|
511
|
+
this.chart = chart;
|
|
512
|
+
this.source = src.slice();
|
|
513
|
+
this.baseInterval = Math.max(16, +options.baseInterval || 1e3);
|
|
514
|
+
this.speed = clamp(+options.speed || 1, MIN_SPEED, MAX_SPEED);
|
|
515
|
+
this.looping = options.loop === true;
|
|
516
|
+
this.follow = options.follow !== false;
|
|
517
|
+
this.playing = false;
|
|
518
|
+
this._acc = 0;
|
|
519
|
+
this._markerKey = "";
|
|
520
|
+
this._markerView = null;
|
|
521
|
+
this.minIndex = Math.min(1, this.lastIndex);
|
|
522
|
+
const from = +options.from;
|
|
523
|
+
this.index = clamp(
|
|
524
|
+
Number.isFinite(from) ? Math.round(from) : Math.floor(this.lastIndex / 2),
|
|
525
|
+
this.minIndex,
|
|
526
|
+
this.lastIndex
|
|
527
|
+
);
|
|
528
|
+
this._apply("seek");
|
|
529
|
+
}
|
|
530
|
+
// ----------------------------------------------------------------- state --
|
|
531
|
+
get length() {
|
|
532
|
+
return this.source.length;
|
|
533
|
+
}
|
|
534
|
+
get lastIndex() {
|
|
535
|
+
return Math.max(0, this.source.length - 1);
|
|
536
|
+
}
|
|
537
|
+
get atEnd() {
|
|
538
|
+
return this.index >= this.lastIndex;
|
|
539
|
+
}
|
|
540
|
+
get bar() {
|
|
541
|
+
return this.source[this.index] || null;
|
|
542
|
+
}
|
|
543
|
+
get time() {
|
|
544
|
+
return this.bar ? this.bar.time : null;
|
|
545
|
+
}
|
|
546
|
+
/** 0 at the first playable bar, 1 at the last. */
|
|
547
|
+
get progress() {
|
|
548
|
+
const span = this.lastIndex - this.minIndex;
|
|
549
|
+
return span > 0 ? (this.index - this.minIndex) / span : 1;
|
|
550
|
+
}
|
|
551
|
+
/** Real ms between bars at the current speed. */
|
|
552
|
+
get interval() {
|
|
553
|
+
return this.baseInterval / this.speed;
|
|
554
|
+
}
|
|
555
|
+
/** Snapshot handed to `subscribe('replay', fn)`. */
|
|
556
|
+
state() {
|
|
557
|
+
return {
|
|
558
|
+
active: true,
|
|
559
|
+
playing: this.playing,
|
|
560
|
+
index: this.index,
|
|
561
|
+
length: this.length,
|
|
562
|
+
progress: this.progress,
|
|
563
|
+
speed: this.speed,
|
|
564
|
+
time: this.time,
|
|
565
|
+
bar: this.bar,
|
|
566
|
+
atEnd: this.atEnd
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
// ------------------------------------------------------------- transport --
|
|
570
|
+
play() {
|
|
571
|
+
if (this.playing || this.length < 2) return this;
|
|
572
|
+
if (this.atEnd) {
|
|
573
|
+
this.index = this.minIndex;
|
|
574
|
+
this._apply("seek");
|
|
575
|
+
}
|
|
576
|
+
this.playing = true;
|
|
577
|
+
this._acc = 0;
|
|
578
|
+
this._changed();
|
|
579
|
+
return this;
|
|
580
|
+
}
|
|
581
|
+
pause() {
|
|
582
|
+
if (!this.playing) return this;
|
|
583
|
+
this.playing = false;
|
|
584
|
+
this._acc = 0;
|
|
585
|
+
this._changed();
|
|
586
|
+
return this;
|
|
587
|
+
}
|
|
588
|
+
toggle() {
|
|
589
|
+
return this.playing ? this.pause() : this.play();
|
|
590
|
+
}
|
|
591
|
+
/** Multiplier on `baseInterval`. Clamped to 0.25×–500×. */
|
|
592
|
+
setSpeed(speed) {
|
|
593
|
+
const s = clamp(+speed || 1, MIN_SPEED, MAX_SPEED);
|
|
594
|
+
if (s === this.speed) return this;
|
|
595
|
+
this.speed = s;
|
|
596
|
+
this._acc = 0;
|
|
597
|
+
this._changed();
|
|
598
|
+
return this;
|
|
599
|
+
}
|
|
600
|
+
setLoop(on) {
|
|
601
|
+
this.looping = !!on;
|
|
602
|
+
this._changed();
|
|
603
|
+
return this;
|
|
604
|
+
}
|
|
605
|
+
/** Move the cursor. Out-of-range values clamp; playback keeps running. */
|
|
606
|
+
seek(index) {
|
|
607
|
+
const next = clamp(Math.round(+index), this.minIndex, this.lastIndex);
|
|
608
|
+
if (next === this.index) return this;
|
|
609
|
+
this.index = next;
|
|
610
|
+
this._acc = 0;
|
|
611
|
+
this._apply("seek");
|
|
612
|
+
this._changed();
|
|
613
|
+
return this;
|
|
614
|
+
}
|
|
615
|
+
step(n = 1) {
|
|
616
|
+
return this.seek(this.index + (n || 0));
|
|
617
|
+
}
|
|
618
|
+
toStart() {
|
|
619
|
+
return this.seek(this.minIndex);
|
|
620
|
+
}
|
|
621
|
+
toEnd() {
|
|
622
|
+
return this.seek(this.lastIndex);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Advance with wall-clock time. Called once per frame by the chart; returns
|
|
626
|
+
* true while playback is in flight, which is what keeps the loop awake.
|
|
627
|
+
*/
|
|
628
|
+
tick(dt) {
|
|
629
|
+
if (!this.playing || this.length < 2) return false;
|
|
630
|
+
this._acc += dt * this.speed;
|
|
631
|
+
const steps = Math.floor(this._acc / this.baseInterval);
|
|
632
|
+
if (steps <= 0) return true;
|
|
633
|
+
this._acc -= steps * this.baseInterval;
|
|
634
|
+
const last = this.lastIndex;
|
|
635
|
+
let next = this.index + Math.min(steps, MAX_STEPS_PER_FRAME);
|
|
636
|
+
if (next > last) {
|
|
637
|
+
if (this.looping) {
|
|
638
|
+
this.index = this.minIndex;
|
|
639
|
+
this._apply("seek");
|
|
640
|
+
this._changed();
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
next = last;
|
|
644
|
+
}
|
|
645
|
+
const single = next === this.index + 1;
|
|
646
|
+
this.index = next;
|
|
647
|
+
this._apply(single ? "step" : "seek");
|
|
648
|
+
if (this.atEnd && !this.looping) {
|
|
649
|
+
this.playing = false;
|
|
650
|
+
this._acc = 0;
|
|
651
|
+
}
|
|
652
|
+
this._changed();
|
|
653
|
+
return this.playing;
|
|
654
|
+
}
|
|
655
|
+
// --------------------------------------------------------------- markers --
|
|
656
|
+
/**
|
|
657
|
+
* Markers after the cursor are future information: hidden, not clamped.
|
|
658
|
+
* Without this they would all pin to the newest revealed bar, because
|
|
659
|
+
* time→index resolution snaps to the NEAREST bar.
|
|
660
|
+
*
|
|
661
|
+
* Cached on the cursor, so a paused chart allocates nothing per frame.
|
|
662
|
+
*/
|
|
663
|
+
markerFilter(markers) {
|
|
664
|
+
const t = this.time;
|
|
665
|
+
if (t == null) return markers;
|
|
666
|
+
const key = this.index + ":" + markers.length;
|
|
667
|
+
if (key !== this._markerKey || !this._markerView) {
|
|
668
|
+
const cut = t + (this.chart.ts.timeframeMs || 0) / 2;
|
|
669
|
+
this._markerView = markers.filter((m) => m.time <= cut);
|
|
670
|
+
this._markerKey = key;
|
|
671
|
+
}
|
|
672
|
+
return this._markerView;
|
|
673
|
+
}
|
|
674
|
+
/** Drop the cached slice — the marker set itself changed. */
|
|
675
|
+
invalidateMarkers() {
|
|
676
|
+
this._markerKey = "";
|
|
677
|
+
this._markerView = null;
|
|
678
|
+
}
|
|
679
|
+
// ---------------------------------------------------------------- private --
|
|
680
|
+
/**
|
|
681
|
+
* Push the revealed prefix into the chart.
|
|
682
|
+
*
|
|
683
|
+
* 'step' (exactly one bar forward) takes the live path so the new candle
|
|
684
|
+
* animates; anything else swaps the prefix and re-anchors without easing.
|
|
685
|
+
*/
|
|
686
|
+
_apply(mode) {
|
|
687
|
+
const chart = this.chart;
|
|
688
|
+
if (mode === "step" && chart.bars.length === this.index) {
|
|
689
|
+
chart.append(this.source[this.index]);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
chart._swapBars(this.source.slice(0, this.index + 1));
|
|
693
|
+
if (this.follow) chart.ts.jumpToRealtime();
|
|
694
|
+
}
|
|
695
|
+
/** Any state change needs a frame: that frame is what emits 'replay'. */
|
|
696
|
+
_changed() {
|
|
697
|
+
this.chart.loop.invalidate("main");
|
|
698
|
+
}
|
|
699
|
+
}
|
|
495
700
|
function niceStep(span, count) {
|
|
496
701
|
const raw = span / Math.max(1, count);
|
|
497
702
|
if (!(raw > 0) || !isFinite(raw)) return 1;
|
|
@@ -1059,6 +1264,17 @@ function drawMarkers(ctx, s, placed, hoverId) {
|
|
|
1059
1264
|
ctx.restore();
|
|
1060
1265
|
return hits;
|
|
1061
1266
|
}
|
|
1267
|
+
const inactiveReplay = () => ({
|
|
1268
|
+
active: false,
|
|
1269
|
+
playing: false,
|
|
1270
|
+
index: -1,
|
|
1271
|
+
length: 0,
|
|
1272
|
+
progress: 0,
|
|
1273
|
+
speed: 1,
|
|
1274
|
+
time: null,
|
|
1275
|
+
bar: null,
|
|
1276
|
+
atEnd: false
|
|
1277
|
+
});
|
|
1062
1278
|
class Chart {
|
|
1063
1279
|
constructor(container, options = {}) {
|
|
1064
1280
|
if (!container) throw new Error("Chart: container element is required");
|
|
@@ -1075,11 +1291,13 @@ class Chart {
|
|
|
1075
1291
|
this._unsub = null;
|
|
1076
1292
|
this._loadingHistory = false;
|
|
1077
1293
|
this._exhausted = false;
|
|
1294
|
+
this._replay = null;
|
|
1078
1295
|
this._listeners = {
|
|
1079
1296
|
crosshair: /* @__PURE__ */ new Set(),
|
|
1080
1297
|
visibleRange: /* @__PURE__ */ new Set(),
|
|
1081
1298
|
markerClick: /* @__PURE__ */ new Set(),
|
|
1082
|
-
markerHover: /* @__PURE__ */ new Set()
|
|
1299
|
+
markerHover: /* @__PURE__ */ new Set(),
|
|
1300
|
+
replay: /* @__PURE__ */ new Set()
|
|
1083
1301
|
};
|
|
1084
1302
|
this._markers = normalizeMarkers(options.markers);
|
|
1085
1303
|
this.priceLines = Array.isArray(options.priceLines) ? options.priceLines.slice() : [];
|
|
@@ -1115,6 +1333,7 @@ class Chart {
|
|
|
1115
1333
|
}
|
|
1116
1334
|
// ------------------------------------------------------------------ data --
|
|
1117
1335
|
setData(bars) {
|
|
1336
|
+
if (this._replay) this._replay = null;
|
|
1118
1337
|
this.bars = Array.isArray(bars) ? bars.slice() : [];
|
|
1119
1338
|
this._exhausted = false;
|
|
1120
1339
|
if (this.bars.length > 1) {
|
|
@@ -1126,6 +1345,23 @@ class Chart {
|
|
|
1126
1345
|
this.ps._primed = false;
|
|
1127
1346
|
this.loop.invalidate("all");
|
|
1128
1347
|
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Replace the bar array WITHOUT re-anchoring the view — the deliberate
|
|
1350
|
+
* difference from setData(), which snaps to the right edge and re-primes
|
|
1351
|
+
* the price scale. Replay swaps its revealed prefix through here on every
|
|
1352
|
+
* scrub, so a snap would fight the user's zoom and the price scale would
|
|
1353
|
+
* pop instead of easing between windows.
|
|
1354
|
+
*/
|
|
1355
|
+
_swapBars(bars) {
|
|
1356
|
+
this.bars = Array.isArray(bars) ? bars : [];
|
|
1357
|
+
if (this.bars.length > 1) {
|
|
1358
|
+
this.ts.timeframeMs = this.bars[1].time - this.bars[0].time;
|
|
1359
|
+
}
|
|
1360
|
+
this.ts.setBarCount(this.bars.length);
|
|
1361
|
+
this.live.reset();
|
|
1362
|
+
this._resolveKey = "";
|
|
1363
|
+
this.loop.invalidate("all");
|
|
1364
|
+
}
|
|
1129
1365
|
/** Merge a tick into the forming candle (animated). */
|
|
1130
1366
|
update(bar) {
|
|
1131
1367
|
if (!bar) return;
|
|
@@ -1167,6 +1403,7 @@ class Chart {
|
|
|
1167
1403
|
if (typeof feed.prime === "function") feed.prime(bars[bars.length - 1]);
|
|
1168
1404
|
this._unsub = feed.subscribe((msg) => {
|
|
1169
1405
|
if (!msg || !msg.bar) return;
|
|
1406
|
+
if (this._replay) return;
|
|
1170
1407
|
if (msg.type === "append") this.append(msg.bar);
|
|
1171
1408
|
else this.update(msg.bar);
|
|
1172
1409
|
});
|
|
@@ -1177,6 +1414,7 @@ class Chart {
|
|
|
1177
1414
|
this.feed = null;
|
|
1178
1415
|
}
|
|
1179
1416
|
async _maybeLoadHistory() {
|
|
1417
|
+
if (this._replay) return;
|
|
1180
1418
|
if (this._loadingHistory || this._exhausted || !this.feed) return;
|
|
1181
1419
|
const { from } = this.ts.visibleRange();
|
|
1182
1420
|
if (from > 80 || !this.bars.length) return;
|
|
@@ -1399,6 +1637,11 @@ class Chart {
|
|
|
1399
1637
|
this._rangeKey = this._rangeIdentity(payload);
|
|
1400
1638
|
fn(payload);
|
|
1401
1639
|
}
|
|
1640
|
+
if (event === "replay") {
|
|
1641
|
+
const payload = this.replayState();
|
|
1642
|
+
this._replayKey = this._replayIdentity(payload);
|
|
1643
|
+
fn(payload);
|
|
1644
|
+
}
|
|
1402
1645
|
return () => set.delete(fn);
|
|
1403
1646
|
}
|
|
1404
1647
|
// ------------------------------------------------------------------ range --
|
|
@@ -1445,11 +1688,72 @@ class Chart {
|
|
|
1445
1688
|
this._rangeKey = key;
|
|
1446
1689
|
for (const fn of set) fn(payload);
|
|
1447
1690
|
}
|
|
1691
|
+
// ----------------------------------------------------------------- replay --
|
|
1692
|
+
/**
|
|
1693
|
+
* Start bar-by-bar playback over a fixed dataset.
|
|
1694
|
+
*
|
|
1695
|
+
* With no `bars`, the chart's CURRENT data becomes the dataset — the usual
|
|
1696
|
+
* case: load history, then replay it. The chart is not switched into a
|
|
1697
|
+
* special mode; it is simply handed the revealed prefix, so scales,
|
|
1698
|
+
* annotations, the crosshair and visibleRange all keep behaving normally.
|
|
1699
|
+
*
|
|
1700
|
+
* chart.startReplay({ from: 200, speed: 4 })
|
|
1701
|
+
* chart.replay.play()
|
|
1702
|
+
*
|
|
1703
|
+
* @param {object} [options]
|
|
1704
|
+
* @param {Bar[]} [options.bars] dataset (defaults to current bars)
|
|
1705
|
+
* @param {number} [options.from] starting index (default: midpoint)
|
|
1706
|
+
* @param {number} [options.speed] rate multiplier, 0.25–500
|
|
1707
|
+
* @param {number} [options.baseInterval] real ms per bar at 1× (default 1000)
|
|
1708
|
+
* @param {boolean} [options.loop] restart at the end
|
|
1709
|
+
* @param {boolean} [options.follow] re-anchor the right edge on scrub
|
|
1710
|
+
* @returns {Replay|null} null if there are fewer than two bars to replay
|
|
1711
|
+
*/
|
|
1712
|
+
startReplay(options = {}) {
|
|
1713
|
+
const source = Array.isArray(options.bars) && options.bars.length ? options.bars : this._replay ? this._replay.source : this.bars;
|
|
1714
|
+
if (!source || source.length < 2) return null;
|
|
1715
|
+
const dataset = source.slice();
|
|
1716
|
+
this._replay = new Replay(this, { ...options, bars: dataset });
|
|
1717
|
+
return this._replay;
|
|
1718
|
+
}
|
|
1719
|
+
/** Leave replay and reveal the whole dataset again. */
|
|
1720
|
+
stopReplay() {
|
|
1721
|
+
if (!this._replay) return;
|
|
1722
|
+
const full = this._replay.source;
|
|
1723
|
+
this._replay = null;
|
|
1724
|
+
this.setData(full);
|
|
1725
|
+
}
|
|
1726
|
+
/** The active controller, or null. */
|
|
1727
|
+
get replay() {
|
|
1728
|
+
return this._replay;
|
|
1729
|
+
}
|
|
1730
|
+
/** Current playback state; `{ active: false, ... }` when not replaying. */
|
|
1731
|
+
replayState() {
|
|
1732
|
+
return this._replay ? this._replay.state() : inactiveReplay();
|
|
1733
|
+
}
|
|
1734
|
+
_replayIdentity(p) {
|
|
1735
|
+
return `${p.active ? 1 : 0}:${p.playing ? 1 : 0}:${p.index}:${p.length}:${p.speed}`;
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Emitted from the frame, like visibleRange, so a handler can safely touch
|
|
1739
|
+
* the chart. Keyed on cursor + transport, not on the payload object, so a
|
|
1740
|
+
* paused replay emits nothing at all.
|
|
1741
|
+
*/
|
|
1742
|
+
_emitReplay() {
|
|
1743
|
+
const set = this._listeners.replay;
|
|
1744
|
+
if (!set.size) return;
|
|
1745
|
+
const payload = this.replayState();
|
|
1746
|
+
const key = this._replayIdentity(payload);
|
|
1747
|
+
if (key === this._replayKey) return;
|
|
1748
|
+
this._replayKey = key;
|
|
1749
|
+
for (const fn of set) fn(payload);
|
|
1750
|
+
}
|
|
1448
1751
|
// ----------------------------------------------------------- annotations --
|
|
1449
1752
|
/** Replace every marker. Each `time` is resolved to its nearest bar. */
|
|
1450
1753
|
setMarkers(markers) {
|
|
1451
1754
|
this._markers = normalizeMarkers(markers);
|
|
1452
1755
|
this._resolveKey = "";
|
|
1756
|
+
if (this._replay) this._replay.invalidateMarkers();
|
|
1453
1757
|
this.loop.invalidate("main");
|
|
1454
1758
|
}
|
|
1455
1759
|
/** The current markers, normalised, each with its resolved bar index. */
|
|
@@ -1479,6 +1783,7 @@ class Chart {
|
|
|
1479
1783
|
// ----------------------------------------------------------------- frame --
|
|
1480
1784
|
_frame(dirty, dt) {
|
|
1481
1785
|
let animating = false;
|
|
1786
|
+
if (this._replay && this._replay.tick(dt)) animating = true;
|
|
1482
1787
|
if (this.ts.tick(dt)) animating = true;
|
|
1483
1788
|
const dx = this.inertia.tick(dt);
|
|
1484
1789
|
if (dx) {
|
|
@@ -1509,10 +1814,12 @@ class Chart {
|
|
|
1509
1814
|
priceLines: this.priceLines,
|
|
1510
1815
|
zones: this.zones
|
|
1511
1816
|
};
|
|
1512
|
-
|
|
1513
|
-
|
|
1817
|
+
let markers = this._markers;
|
|
1818
|
+
if (this._replay && markers.length) markers = this._replay.markerFilter(markers);
|
|
1819
|
+
if (markers.length) {
|
|
1820
|
+
const key = markers.length + ":" + this.bars.length + ":" + (this.bars.length ? this.bars[0].time : 0);
|
|
1514
1821
|
if (key !== this._resolveKey) {
|
|
1515
|
-
resolveMarkers(
|
|
1822
|
+
resolveMarkers(markers, this.bars);
|
|
1516
1823
|
this._resolveKey = key;
|
|
1517
1824
|
}
|
|
1518
1825
|
}
|
|
@@ -1524,7 +1831,7 @@ class Chart {
|
|
|
1524
1831
|
this._markerHits = drawMarkers(
|
|
1525
1832
|
this.layers.ctx.main,
|
|
1526
1833
|
state,
|
|
1527
|
-
layoutMarkers(
|
|
1834
|
+
layoutMarkers(markers, state),
|
|
1528
1835
|
this._hoverMarkerId
|
|
1529
1836
|
);
|
|
1530
1837
|
}
|
|
@@ -1532,6 +1839,7 @@ class Chart {
|
|
|
1532
1839
|
drawCrosshair(this.layers.ctx.overlay, state);
|
|
1533
1840
|
}
|
|
1534
1841
|
this._emitVisibleRange(from, to);
|
|
1842
|
+
this._emitReplay();
|
|
1535
1843
|
return animating;
|
|
1536
1844
|
}
|
|
1537
1845
|
// ------------------------------------------------------------------- api --
|
|
@@ -1578,6 +1886,7 @@ class Chart {
|
|
|
1578
1886
|
this.loop.stop();
|
|
1579
1887
|
this.layers.destroy();
|
|
1580
1888
|
for (const set of Object.values(this._listeners)) set.clear();
|
|
1889
|
+
this._replay = null;
|
|
1581
1890
|
this._markers = [];
|
|
1582
1891
|
this._markerHits = [];
|
|
1583
1892
|
this.priceLines = [];
|
|
@@ -1760,14 +2069,17 @@ class RandomFeed extends DataFeed {
|
|
|
1760
2069
|
function createChart(container, options) {
|
|
1761
2070
|
return new Chart(container, options);
|
|
1762
2071
|
}
|
|
1763
|
-
const version = "0.
|
|
2072
|
+
const version = "0.4.0";
|
|
1764
2073
|
export {
|
|
1765
2074
|
Chart,
|
|
1766
2075
|
DataFeed,
|
|
1767
2076
|
Inertia,
|
|
1768
2077
|
LiveCandle,
|
|
2078
|
+
MAX_SPEED,
|
|
2079
|
+
MIN_SPEED,
|
|
1769
2080
|
PriceScale,
|
|
1770
2081
|
RandomFeed,
|
|
2082
|
+
Replay,
|
|
1771
2083
|
Smoothed,
|
|
1772
2084
|
TimeScale,
|
|
1773
2085
|
Tween,
|