emberwick 0.1.1 → 0.2.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 +113 -12
- package/index.d.ts +120 -0
- package/index.js +434 -12
- package/index.js.map +1 -1
- package/package.json +1 -2
- package/umd/emberwick.umd.js +1 -1
- package/umd/emberwick.umd.js.map +1 -1
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 = {
|
|
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,14 +1356,39 @@ class Chart {
|
|
|
1011
1356
|
if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", "0");
|
|
1012
1357
|
}
|
|
1013
1358
|
_emitCrosshair(p) {
|
|
1014
|
-
if (
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
-
|
|
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];
|
|
@@ -1026,6 +1396,37 @@ class Chart {
|
|
|
1026
1396
|
set.add(fn);
|
|
1027
1397
|
return () => set.delete(fn);
|
|
1028
1398
|
}
|
|
1399
|
+
// ----------------------------------------------------------- annotations --
|
|
1400
|
+
/** Replace every marker. Each `time` is resolved to its nearest bar. */
|
|
1401
|
+
setMarkers(markers) {
|
|
1402
|
+
this._markers = normalizeMarkers(markers);
|
|
1403
|
+
this._resolveKey = "";
|
|
1404
|
+
this.loop.invalidate("main");
|
|
1405
|
+
}
|
|
1406
|
+
/** The current markers, normalised, each with its resolved bar index. */
|
|
1407
|
+
getMarkers() {
|
|
1408
|
+
return this._markers.slice();
|
|
1409
|
+
}
|
|
1410
|
+
addMarker(marker) {
|
|
1411
|
+
this.setMarkers(this._markers.concat([marker]));
|
|
1412
|
+
}
|
|
1413
|
+
removeMarker(id) {
|
|
1414
|
+
const key = String(id);
|
|
1415
|
+
this.setMarkers(this._markers.filter((m) => m.id !== key));
|
|
1416
|
+
}
|
|
1417
|
+
clearMarkers() {
|
|
1418
|
+
this.setMarkers([]);
|
|
1419
|
+
}
|
|
1420
|
+
/** Horizontal lines — entries, stops, targets, alerts. */
|
|
1421
|
+
setPriceLines(lines) {
|
|
1422
|
+
this.priceLines = Array.isArray(lines) ? lines.slice() : [];
|
|
1423
|
+
this.loop.invalidate("main");
|
|
1424
|
+
}
|
|
1425
|
+
/** Shaded regions: a price band ({from,to}) or a time band ({fromTime,toTime}). */
|
|
1426
|
+
setZones(zones) {
|
|
1427
|
+
this.zones = Array.isArray(zones) ? zones.slice() : [];
|
|
1428
|
+
this.loop.invalidate("all");
|
|
1429
|
+
}
|
|
1029
1430
|
// ----------------------------------------------------------------- frame --
|
|
1030
1431
|
_frame(dirty, dt) {
|
|
1031
1432
|
let animating = false;
|
|
@@ -1055,11 +1456,28 @@ class Chart {
|
|
|
1055
1456
|
live: liveVisible,
|
|
1056
1457
|
volumeRatio: this.options.volumeRatio,
|
|
1057
1458
|
cursor: this.cursor,
|
|
1058
|
-
magnet: this.options.magnet
|
|
1459
|
+
magnet: this.options.magnet,
|
|
1460
|
+
priceLines: this.priceLines,
|
|
1461
|
+
zones: this.zones
|
|
1059
1462
|
};
|
|
1463
|
+
if (this._markers.length) {
|
|
1464
|
+
const key = this.bars.length + ":" + (this.bars.length ? this.bars[0].time : 0);
|
|
1465
|
+
if (key !== this._resolveKey) {
|
|
1466
|
+
resolveMarkers(this._markers, this.bars);
|
|
1467
|
+
this._resolveKey = key;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1060
1470
|
if (redrawAll) {
|
|
1061
1471
|
drawGrid(this.layers.ctx.base, state);
|
|
1472
|
+
drawZones(this.layers.ctx.base, state);
|
|
1062
1473
|
drawCandles(this.layers.ctx.main, state);
|
|
1474
|
+
drawPriceLines(this.layers.ctx.main, state);
|
|
1475
|
+
this._markerHits = drawMarkers(
|
|
1476
|
+
this.layers.ctx.main,
|
|
1477
|
+
state,
|
|
1478
|
+
layoutMarkers(this._markers, state),
|
|
1479
|
+
this._hoverMarkerId
|
|
1480
|
+
);
|
|
1063
1481
|
}
|
|
1064
1482
|
if (redrawAll || dirty.has("overlay")) {
|
|
1065
1483
|
drawCrosshair(this.layers.ctx.overlay, state);
|
|
@@ -1105,11 +1523,15 @@ class Chart {
|
|
|
1105
1523
|
el.removeEventListener("wheel", this._onWheel);
|
|
1106
1524
|
el.removeEventListener("dblclick", this._onDbl);
|
|
1107
1525
|
el.removeEventListener("keydown", this._onKey);
|
|
1526
|
+
el.removeEventListener("click", this._onClick);
|
|
1108
1527
|
this.detachFeed();
|
|
1109
1528
|
this.loop.stop();
|
|
1110
1529
|
this.layers.destroy();
|
|
1111
|
-
this._listeners.
|
|
1112
|
-
this.
|
|
1530
|
+
for (const set of Object.values(this._listeners)) set.clear();
|
|
1531
|
+
this._markers = [];
|
|
1532
|
+
this._markerHits = [];
|
|
1533
|
+
this.priceLines = [];
|
|
1534
|
+
this.zones = [];
|
|
1113
1535
|
this.bars = [];
|
|
1114
1536
|
}
|
|
1115
1537
|
}
|
|
@@ -1288,7 +1710,7 @@ class RandomFeed extends DataFeed {
|
|
|
1288
1710
|
function createChart(container, options) {
|
|
1289
1711
|
return new Chart(container, options);
|
|
1290
1712
|
}
|
|
1291
|
-
const version = "0.
|
|
1713
|
+
const version = "0.2.0";
|
|
1292
1714
|
export {
|
|
1293
1715
|
Chart,
|
|
1294
1716
|
DataFeed,
|