emberwick 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -733,6 +733,332 @@ function drawCrosshair(ctx, s) {
733
733
  ctx.fillText(t, bx, plot.h + 12);
734
734
  }
735
735
  }
736
+ const MARKER_SHAPES = [
737
+ "arrowUp",
738
+ "arrowDown",
739
+ "triangleUp",
740
+ "triangleDown",
741
+ "circle",
742
+ "square",
743
+ "diamond",
744
+ "flag",
745
+ "label"
746
+ ];
747
+ const SHAPE_SET = new Set(MARKER_SHAPES);
748
+ const DEFAULT_POSITION = {
749
+ arrowUp: "belowBar",
750
+ triangleUp: "belowBar",
751
+ arrowDown: "aboveBar",
752
+ triangleDown: "aboveBar"
753
+ };
754
+ const POSITIONS = /* @__PURE__ */ new Set(["aboveBar", "belowBar", "inBar", "atPrice"]);
755
+ function normalizeMarker(raw, i) {
756
+ if (!raw || !isFinite(raw.time)) return null;
757
+ const shape = SHAPE_SET.has(raw.shape) ? raw.shape : "circle";
758
+ const position = POSITIONS.has(raw.position) ? raw.position : DEFAULT_POSITION[shape] || "aboveBar";
759
+ return {
760
+ id: raw.id != null ? String(raw.id) : `mk${i}`,
761
+ time: +raw.time,
762
+ price: isFinite(raw.price) ? +raw.price : null,
763
+ shape,
764
+ position,
765
+ color: raw.color || null,
766
+ textColor: raw.textColor || null,
767
+ text: raw.text != null ? String(raw.text) : "",
768
+ size: isFinite(raw.size) && raw.size > 0 ? +raw.size : 1,
769
+ /** Anything the consumer wants handed back on hover/click. */
770
+ data: raw.data,
771
+ index: -1
772
+ };
773
+ }
774
+ function normalizeMarkers(list) {
775
+ if (!Array.isArray(list)) return [];
776
+ const out = [];
777
+ for (let i = 0; i < list.length; i++) {
778
+ const m = normalizeMarker(list[i], i);
779
+ if (m) out.push(m);
780
+ }
781
+ out.sort((a, b) => a.time - b.time);
782
+ return out;
783
+ }
784
+ function nearestIndex(bars, time) {
785
+ const n = bars.length;
786
+ if (!n) return -1;
787
+ if (time <= bars[0].time) return 0;
788
+ if (time >= bars[n - 1].time) return n - 1;
789
+ let lo = 0;
790
+ let hi = n - 1;
791
+ while (lo <= hi) {
792
+ const mid = lo + hi >> 1;
793
+ const t = bars[mid].time;
794
+ if (t === time) return mid;
795
+ if (t < time) lo = mid + 1;
796
+ else hi = mid - 1;
797
+ }
798
+ const a = Math.max(0, hi);
799
+ const b = Math.min(n - 1, lo);
800
+ return Math.abs(bars[a].time - time) <= Math.abs(bars[b].time - time) ? a : b;
801
+ }
802
+ function resolveMarkers(markers, bars) {
803
+ for (let i = 0; i < markers.length; i++) {
804
+ markers[i].index = nearestIndex(bars, markers[i].time);
805
+ }
806
+ return markers;
807
+ }
808
+ function layoutMarkers(markers, s) {
809
+ const { ts, ps, plot, bars, live } = s;
810
+ if (!markers.length || !bars.length) return [];
811
+ const { from, to } = ts.visibleRange();
812
+ const lastIdx = bars.length - 1;
813
+ const dense = ts.barWidth() <= 3;
814
+ const stacks = /* @__PURE__ */ new Map();
815
+ const placed = [];
816
+ let lastDenseX = -Infinity;
817
+ for (const m of markers) {
818
+ const i = m.index;
819
+ if (i < 0 || i < from - 2 || i > to + 2) continue;
820
+ const bar = live && i === lastIdx ? live : bars[i];
821
+ if (!bar) continue;
822
+ const x = ts.x(i);
823
+ if (x < -48 || x > plot.w + 48) continue;
824
+ if (dense) {
825
+ if (x - lastDenseX < 4) continue;
826
+ lastDenseX = x;
827
+ }
828
+ const r = 5 * m.size;
829
+ let y;
830
+ let dir = 0;
831
+ if (m.position === "atPrice" && m.price != null) {
832
+ y = ps.y(m.price);
833
+ } else if (m.position === "inBar") {
834
+ y = ps.y((bar.high + bar.low) / 2);
835
+ } else if (m.position === "belowBar") {
836
+ y = ps.y(bar.low) + r + 7;
837
+ dir = 1;
838
+ } else {
839
+ y = ps.y(bar.high) - r - 7;
840
+ dir = -1;
841
+ }
842
+ if (dir !== 0) {
843
+ const key = i + m.position;
844
+ const n = stacks.get(key) || 0;
845
+ stacks.set(key, n + 1);
846
+ y += dir * n * (r * 2 + 5);
847
+ }
848
+ placed.push({ m, x, y, r, dir });
849
+ }
850
+ return placed;
851
+ }
852
+ const DASH = { solid: [], dashed: [6, 4], dotted: [1, 3] };
853
+ const DOWN_SHAPES = /* @__PURE__ */ new Set(["arrowDown", "triangleDown"]);
854
+ function roundRect(ctx, x, y, w, h, r) {
855
+ const rr = Math.max(0, Math.min(r, h / 2, w / 2));
856
+ ctx.beginPath();
857
+ ctx.moveTo(x + rr, y);
858
+ ctx.lineTo(x + w - rr, y);
859
+ ctx.quadraticCurveTo(x + w, y, x + w, y + rr);
860
+ ctx.lineTo(x + w, y + h - rr);
861
+ ctx.quadraticCurveTo(x + w, y + h, x + w - rr, y + h);
862
+ ctx.lineTo(x + rr, y + h);
863
+ ctx.quadraticCurveTo(x, y + h, x, y + h - rr);
864
+ ctx.lineTo(x, y + rr);
865
+ ctx.quadraticCurveTo(x, y, x + rr, y);
866
+ ctx.closePath();
867
+ }
868
+ function drawZones(ctx, s) {
869
+ const { zones, theme, ts, ps, plot, bars } = s;
870
+ if (!zones || !zones.length) return;
871
+ ctx.save();
872
+ ctx.font = theme.font;
873
+ ctx.textBaseline = "top";
874
+ ctx.textAlign = "left";
875
+ for (const z of zones) {
876
+ let x;
877
+ let y;
878
+ let w;
879
+ let h;
880
+ if (isFinite(z.from) && isFinite(z.to)) {
881
+ y = ps.y(Math.max(z.from, z.to));
882
+ h = Math.max(1, ps.y(Math.min(z.from, z.to)) - y);
883
+ x = 0;
884
+ w = plot.w;
885
+ } else if (isFinite(z.fromTime) && isFinite(z.toTime) && bars.length) {
886
+ const a = ts.x(nearestIndex(bars, Math.min(z.fromTime, z.toTime)));
887
+ const b = ts.x(nearestIndex(bars, Math.max(z.fromTime, z.toTime)));
888
+ x = a;
889
+ w = Math.max(1, b - a);
890
+ y = 0;
891
+ h = plot.h;
892
+ } else {
893
+ continue;
894
+ }
895
+ if (x > plot.w || x + w < 0 || y > plot.h || y + h < 0) continue;
896
+ ctx.fillStyle = z.color || "rgba(38,166,154,0.10)";
897
+ ctx.fillRect(x, y, w, h);
898
+ if (z.border) {
899
+ ctx.strokeStyle = z.border;
900
+ ctx.lineWidth = 1;
901
+ ctx.strokeRect(Math.round(x) + 0.5, Math.round(y) + 0.5, Math.round(w), Math.round(h));
902
+ }
903
+ if (z.label) {
904
+ ctx.fillStyle = z.labelColor || theme.text;
905
+ ctx.fillText(z.label, Math.max(6, x + 6), Math.max(4, y + 4));
906
+ }
907
+ }
908
+ ctx.restore();
909
+ }
910
+ function drawPriceLines(ctx, s) {
911
+ const { priceLines, theme, ps, plot } = s;
912
+ if (!priceLines || !priceLines.length) return;
913
+ const { step } = priceTicks(ps.lo, ps.hi, Math.max(2, Math.floor(plot.h / 58)));
914
+ const dec = decimalsFor(step);
915
+ ctx.save();
916
+ ctx.font = theme.font;
917
+ ctx.textBaseline = "middle";
918
+ for (const L of priceLines) {
919
+ if (!L || !isFinite(L.price)) continue;
920
+ const y = Math.round(ps.y(L.price)) + 0.5;
921
+ if (y < 0 || y > plot.h) continue;
922
+ const color = L.color || theme.textStrong;
923
+ ctx.setLineDash(DASH[L.lineStyle] || DASH.dashed);
924
+ ctx.strokeStyle = color;
925
+ ctx.lineWidth = L.lineWidth || 1;
926
+ ctx.beginPath();
927
+ ctx.moveTo(0, y);
928
+ ctx.lineTo(plot.w, y);
929
+ ctx.stroke();
930
+ ctx.setLineDash([]);
931
+ if (L.title) {
932
+ ctx.textAlign = "left";
933
+ const tw = ctx.measureText(L.title).width;
934
+ ctx.fillStyle = color;
935
+ roundRect(ctx, 6, y - 9, tw + 12, 18, 4);
936
+ ctx.fill();
937
+ ctx.fillStyle = L.titleColor || theme.tagText;
938
+ ctx.fillText(L.title, 12, y);
939
+ }
940
+ if (L.axisLabel !== false) {
941
+ const label = L.price.toFixed(dec);
942
+ ctx.textAlign = "left";
943
+ const lw = ctx.measureText(label).width;
944
+ ctx.fillStyle = color;
945
+ ctx.fillRect(plot.w + 1, y - 9, lw + 14, 18);
946
+ ctx.fillStyle = L.tagTextColor || theme.tagText;
947
+ ctx.fillText(label, plot.w + 8, y);
948
+ }
949
+ }
950
+ ctx.restore();
951
+ }
952
+ function shapePath(ctx, shape, x, y, r) {
953
+ ctx.beginPath();
954
+ switch (shape) {
955
+ case "arrowUp":
956
+ ctx.moveTo(x, y - r);
957
+ ctx.lineTo(x + r, y);
958
+ ctx.lineTo(x + r * 0.45, y);
959
+ ctx.lineTo(x + r * 0.45, y + r);
960
+ ctx.lineTo(x - r * 0.45, y + r);
961
+ ctx.lineTo(x - r * 0.45, y);
962
+ ctx.lineTo(x - r, y);
963
+ ctx.closePath();
964
+ break;
965
+ case "arrowDown":
966
+ ctx.moveTo(x, y + r);
967
+ ctx.lineTo(x + r, y);
968
+ ctx.lineTo(x + r * 0.45, y);
969
+ ctx.lineTo(x + r * 0.45, y - r);
970
+ ctx.lineTo(x - r * 0.45, y - r);
971
+ ctx.lineTo(x - r * 0.45, y);
972
+ ctx.lineTo(x - r, y);
973
+ ctx.closePath();
974
+ break;
975
+ case "triangleUp":
976
+ ctx.moveTo(x, y - r);
977
+ ctx.lineTo(x + r, y + r);
978
+ ctx.lineTo(x - r, y + r);
979
+ ctx.closePath();
980
+ break;
981
+ case "triangleDown":
982
+ ctx.moveTo(x, y + r);
983
+ ctx.lineTo(x + r, y - r);
984
+ ctx.lineTo(x - r, y - r);
985
+ ctx.closePath();
986
+ break;
987
+ case "square":
988
+ ctx.rect(x - r, y - r, r * 2, r * 2);
989
+ break;
990
+ case "diamond":
991
+ ctx.moveTo(x, y - r);
992
+ ctx.lineTo(x + r, y);
993
+ ctx.lineTo(x, y + r);
994
+ ctx.lineTo(x - r, y);
995
+ ctx.closePath();
996
+ break;
997
+ default:
998
+ ctx.arc(x, y, r, 0, Math.PI * 2);
999
+ }
1000
+ }
1001
+ function drawFlag(ctx, x, y, r, color) {
1002
+ ctx.fillStyle = color;
1003
+ ctx.fillRect(x - r * 0.8, y - r, Math.max(1, r * 0.3), r * 2);
1004
+ ctx.beginPath();
1005
+ ctx.moveTo(x - r * 0.5, y - r);
1006
+ ctx.lineTo(x + r, y - r * 0.55);
1007
+ ctx.lineTo(x - r * 0.5, y - r * 0.1);
1008
+ ctx.closePath();
1009
+ ctx.fill();
1010
+ }
1011
+ function drawMarkers(ctx, s, placed, hoverId) {
1012
+ const hits = [];
1013
+ if (!placed || !placed.length) return hits;
1014
+ const { theme } = s;
1015
+ ctx.save();
1016
+ ctx.font = theme.font;
1017
+ ctx.textAlign = "center";
1018
+ ctx.textBaseline = "middle";
1019
+ for (const p of placed) {
1020
+ const { m, x, y, r, dir } = p;
1021
+ const color = m.color || (DOWN_SHAPES.has(m.shape) ? theme.down : theme.up);
1022
+ const hovered = hoverId != null && m.id === hoverId;
1023
+ if (m.shape === "label") {
1024
+ const text = m.text || "•";
1025
+ const w = ctx.measureText(text).width + 14;
1026
+ const h = 18 * m.size;
1027
+ roundRect(ctx, x - w / 2, y - h / 2, w, h, 4);
1028
+ ctx.fillStyle = color;
1029
+ ctx.fill();
1030
+ if (hovered) {
1031
+ ctx.strokeStyle = theme.textStrong;
1032
+ ctx.lineWidth = 1.5;
1033
+ ctx.stroke();
1034
+ }
1035
+ ctx.fillStyle = m.textColor || theme.tagText;
1036
+ ctx.fillText(text, x, y + 0.5);
1037
+ hits.push({ id: m.id, marker: m, x, y, r: Math.max(w, h) / 2 });
1038
+ continue;
1039
+ }
1040
+ if (hovered) {
1041
+ ctx.beginPath();
1042
+ ctx.arc(x, y, r + 4, 0, Math.PI * 2);
1043
+ ctx.fillStyle = "rgba(255,255,255,0.14)";
1044
+ ctx.fill();
1045
+ }
1046
+ if (m.shape === "flag") {
1047
+ drawFlag(ctx, x, y, r, color);
1048
+ } else {
1049
+ shapePath(ctx, m.shape, x, y, r);
1050
+ ctx.fillStyle = color;
1051
+ ctx.fill();
1052
+ }
1053
+ if (m.text) {
1054
+ ctx.fillStyle = m.textColor || theme.text;
1055
+ ctx.fillText(m.text, x, dir >= 0 ? y + r + 9 : y - r - 9);
1056
+ }
1057
+ hits.push({ id: m.id, marker: m, x, y, r: r + 3 });
1058
+ }
1059
+ ctx.restore();
1060
+ return hits;
1061
+ }
736
1062
  class Chart {
737
1063
  constructor(container, options = {}) {
738
1064
  if (!container) throw new Error("Chart: container element is required");
@@ -749,7 +1075,18 @@ class Chart {
749
1075
  this._unsub = null;
750
1076
  this._loadingHistory = false;
751
1077
  this._exhausted = false;
752
- this._listeners = { crosshair: /* @__PURE__ */ new Set(), visibleRange: /* @__PURE__ */ new Set() };
1078
+ this._listeners = {
1079
+ crosshair: /* @__PURE__ */ new Set(),
1080
+ visibleRange: /* @__PURE__ */ new Set(),
1081
+ markerClick: /* @__PURE__ */ new Set(),
1082
+ markerHover: /* @__PURE__ */ new Set()
1083
+ };
1084
+ this._markers = normalizeMarkers(options.markers);
1085
+ this.priceLines = Array.isArray(options.priceLines) ? options.priceLines.slice() : [];
1086
+ this.zones = Array.isArray(options.zones) ? options.zones.slice() : [];
1087
+ this._markerHits = [];
1088
+ this._hoverMarkerId = null;
1089
+ this._resolveKey = "";
753
1090
  this.layers = new Layers(container, ["base", "main", "overlay"]);
754
1091
  this.ts = new TimeScale(options.timeScale);
755
1092
  this.ps = new PriceScale(options.priceScale);
@@ -1000,6 +1337,14 @@ class Chart {
1000
1337
  } else return;
1001
1338
  e.preventDefault();
1002
1339
  };
1340
+ this._onClick = (e) => {
1341
+ if (moved) return;
1342
+ if (!this._listeners.markerClick.size) return;
1343
+ const p = localPos(e);
1344
+ const hit = this.markerAt(p.x, p.y);
1345
+ if (hit) for (const fn of this._listeners.markerClick) fn(hit);
1346
+ };
1347
+ el.addEventListener("click", this._onClick);
1003
1348
  el.addEventListener("pointerdown", this._onDown);
1004
1349
  el.addEventListener("pointermove", this._onMove);
1005
1350
  el.addEventListener("pointerup", this._onUp);
@@ -1011,21 +1356,126 @@ class Chart {
1011
1356
  if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", "0");
1012
1357
  }
1013
1358
  _emitCrosshair(p) {
1014
- if (!this._listeners.crosshair.size) return;
1015
- let payload = null;
1016
- if (p && this.bars.length && p.x <= this.plot.w && p.y <= this.plot.h) {
1017
- const i = Math.round(this.ts.index(p.x));
1018
- const bar = this.bars[i];
1019
- if (bar) payload = { index: i, bar, price: this.ps.price(p.y) };
1359
+ if (this._listeners.crosshair.size) {
1360
+ let payload = null;
1361
+ if (p && this.bars.length && p.x <= this.plot.w && p.y <= this.plot.h) {
1362
+ const i = Math.round(this.ts.index(p.x));
1363
+ const bar = this.bars[i];
1364
+ if (bar) payload = { index: i, bar, price: this.ps.price(p.y) };
1365
+ }
1366
+ for (const fn of this._listeners.crosshair) fn(payload);
1020
1367
  }
1021
- for (const fn of this._listeners.crosshair) fn(payload);
1368
+ this._updateHover(p);
1369
+ }
1370
+ /**
1371
+ * Topmost marker whose hit circle contains the point, else null.
1372
+ * Hit circles come from the last render, so this costs nothing but a loop.
1373
+ */
1374
+ markerAt(x, y) {
1375
+ const hits = this._markerHits;
1376
+ for (let i = hits.length - 1; i >= 0; i--) {
1377
+ const h = hits[i];
1378
+ const dx = x - h.x;
1379
+ const dy = y - h.y;
1380
+ if (dx * dx + dy * dy <= h.r * h.r) return h.marker;
1381
+ }
1382
+ return null;
1383
+ }
1384
+ _updateHover(p) {
1385
+ const hit = p ? this.markerAt(p.x, p.y) : null;
1386
+ const id = hit ? hit.id : null;
1387
+ if (id === this._hoverMarkerId) return;
1388
+ this._hoverMarkerId = id;
1389
+ this.container.style.cursor = hit ? "pointer" : "crosshair";
1390
+ this.loop.invalidate("main");
1391
+ for (const fn of this._listeners.markerHover) fn(hit);
1022
1392
  }
1023
1393
  subscribe(event, fn) {
1024
1394
  const set = this._listeners[event];
1025
1395
  if (!set) throw new Error(`Chart: unknown event "${event}"`);
1026
1396
  set.add(fn);
1397
+ if (event === "visibleRange") {
1398
+ const payload = this.visibleRange();
1399
+ this._rangeKey = this._rangeIdentity(payload);
1400
+ fn(payload);
1401
+ }
1027
1402
  return () => set.delete(fn);
1028
1403
  }
1404
+ // ------------------------------------------------------------------ range --
1405
+ /**
1406
+ * The window currently on screen. Cheap enough to poll, though
1407
+ * subscribe('visibleRange', fn) is the better way to track it.
1408
+ */
1409
+ visibleRange() {
1410
+ const { from, to } = this.ts.visibleRange();
1411
+ return this._rangePayload(from, to);
1412
+ }
1413
+ _rangePayload(from, to) {
1414
+ const n = this.bars.length;
1415
+ return {
1416
+ from,
1417
+ to,
1418
+ fromTime: n ? this.bars[from].time : null,
1419
+ toTime: n ? this.bars[to].time : null,
1420
+ barCount: n,
1421
+ spacing: this.ts.spacing,
1422
+ settled: this.ts.settled
1423
+ };
1424
+ }
1425
+ _rangeIdentity(p) {
1426
+ return `${p.from}:${p.to}:${p.fromTime}:${p.toTime}:${p.settled ? 1 : 0}`;
1427
+ }
1428
+ /**
1429
+ * Fires only when the window actually changed, so a consumer can hang a
1430
+ * fetch off it without debouncing. Two deliberate choices:
1431
+ *
1432
+ * - `spacing` is NOT part of the identity. It is a float that moves on every
1433
+ * frame of an eased zoom, so keying on it would make this a 60/sec
1434
+ * firehose. It is still reported, for level-of-detail decisions.
1435
+ * - `settled` IS part of the identity, so the final event of a gesture
1436
+ * always arrives with settled:true. Without it, code that defers expensive
1437
+ * work until the view stops moving would wait forever.
1438
+ */
1439
+ _emitVisibleRange(from, to) {
1440
+ const set = this._listeners.visibleRange;
1441
+ if (!set.size) return;
1442
+ const payload = this._rangePayload(from, to);
1443
+ const key = this._rangeIdentity(payload);
1444
+ if (key === this._rangeKey) return;
1445
+ this._rangeKey = key;
1446
+ for (const fn of set) fn(payload);
1447
+ }
1448
+ // ----------------------------------------------------------- annotations --
1449
+ /** Replace every marker. Each `time` is resolved to its nearest bar. */
1450
+ setMarkers(markers) {
1451
+ this._markers = normalizeMarkers(markers);
1452
+ this._resolveKey = "";
1453
+ this.loop.invalidate("main");
1454
+ }
1455
+ /** The current markers, normalised, each with its resolved bar index. */
1456
+ getMarkers() {
1457
+ return this._markers.slice();
1458
+ }
1459
+ addMarker(marker) {
1460
+ this.setMarkers(this._markers.concat([marker]));
1461
+ }
1462
+ removeMarker(id) {
1463
+ const key = String(id);
1464
+ this.setMarkers(this._markers.filter((m) => m.id !== key));
1465
+ }
1466
+ clearMarkers() {
1467
+ this.setMarkers([]);
1468
+ }
1469
+ /** Horizontal lines — entries, stops, targets, alerts. */
1470
+ setPriceLines(lines) {
1471
+ this.priceLines = Array.isArray(lines) ? lines.slice() : [];
1472
+ this.loop.invalidate("main");
1473
+ }
1474
+ /** Shaded regions: a price band ({from,to}) or a time band ({fromTime,toTime}). */
1475
+ setZones(zones) {
1476
+ this.zones = Array.isArray(zones) ? zones.slice() : [];
1477
+ this.loop.invalidate("all");
1478
+ }
1029
1479
  // ----------------------------------------------------------------- frame --
1030
1480
  _frame(dirty, dt) {
1031
1481
  let animating = false;
@@ -1055,15 +1505,33 @@ class Chart {
1055
1505
  live: liveVisible,
1056
1506
  volumeRatio: this.options.volumeRatio,
1057
1507
  cursor: this.cursor,
1058
- magnet: this.options.magnet
1508
+ magnet: this.options.magnet,
1509
+ priceLines: this.priceLines,
1510
+ zones: this.zones
1059
1511
  };
1512
+ if (this._markers.length) {
1513
+ const key = this.bars.length + ":" + (this.bars.length ? this.bars[0].time : 0);
1514
+ if (key !== this._resolveKey) {
1515
+ resolveMarkers(this._markers, this.bars);
1516
+ this._resolveKey = key;
1517
+ }
1518
+ }
1060
1519
  if (redrawAll) {
1061
1520
  drawGrid(this.layers.ctx.base, state);
1521
+ drawZones(this.layers.ctx.base, state);
1062
1522
  drawCandles(this.layers.ctx.main, state);
1523
+ drawPriceLines(this.layers.ctx.main, state);
1524
+ this._markerHits = drawMarkers(
1525
+ this.layers.ctx.main,
1526
+ state,
1527
+ layoutMarkers(this._markers, state),
1528
+ this._hoverMarkerId
1529
+ );
1063
1530
  }
1064
1531
  if (redrawAll || dirty.has("overlay")) {
1065
1532
  drawCrosshair(this.layers.ctx.overlay, state);
1066
1533
  }
1534
+ this._emitVisibleRange(from, to);
1067
1535
  return animating;
1068
1536
  }
1069
1537
  // ------------------------------------------------------------------- api --
@@ -1105,11 +1573,15 @@ class Chart {
1105
1573
  el.removeEventListener("wheel", this._onWheel);
1106
1574
  el.removeEventListener("dblclick", this._onDbl);
1107
1575
  el.removeEventListener("keydown", this._onKey);
1576
+ el.removeEventListener("click", this._onClick);
1108
1577
  this.detachFeed();
1109
1578
  this.loop.stop();
1110
1579
  this.layers.destroy();
1111
- this._listeners.crosshair.clear();
1112
- this._listeners.visibleRange.clear();
1580
+ for (const set of Object.values(this._listeners)) set.clear();
1581
+ this._markers = [];
1582
+ this._markerHits = [];
1583
+ this.priceLines = [];
1584
+ this.zones = [];
1113
1585
  this.bars = [];
1114
1586
  }
1115
1587
  }
@@ -1288,7 +1760,7 @@ class RandomFeed extends DataFeed {
1288
1760
  function createChart(container, options) {
1289
1761
  return new Chart(container, options);
1290
1762
  }
1291
- const version = "0.1.0";
1763
+ const version = "0.2.0";
1292
1764
  export {
1293
1765
  Chart,
1294
1766
  DataFeed,