hyperprop-charting-library 0.1.134 → 0.1.136
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/dist/hyperprop-charting-library.cjs +753 -8
- package/dist/hyperprop-charting-library.d.ts +66 -2
- package/dist/hyperprop-charting-library.js +753 -8
- package/dist/index.cjs +753 -8
- package/dist/index.d.cts +66 -2
- package/dist/index.d.ts +66 -2
- package/dist/index.js +753 -8
- package/docs/API.md +61 -3
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2970,8 +2970,11 @@ function createChart(element, options = {}) {
|
|
|
2970
2970
|
);
|
|
2971
2971
|
let activeDrawingTool = null;
|
|
2972
2972
|
let draftDrawing = null;
|
|
2973
|
+
let measureState = null;
|
|
2973
2974
|
let drawingDragState = null;
|
|
2974
2975
|
let drawingsChangeHandler = null;
|
|
2976
|
+
let drawingToolChangeHandler = null;
|
|
2977
|
+
let lastNotifiedDrawingTool = null;
|
|
2975
2978
|
let drawingSelectHandler = null;
|
|
2976
2979
|
let drawingDoubleClickHandler = null;
|
|
2977
2980
|
let drawingEditTextHandler = null;
|
|
@@ -3564,6 +3567,13 @@ function createChart(element, options = {}) {
|
|
|
3564
3567
|
points: draftDrawing.points.map((point) => reanchorDrawingPoint(point))
|
|
3565
3568
|
};
|
|
3566
3569
|
}
|
|
3570
|
+
if (measureState) {
|
|
3571
|
+
measureState = {
|
|
3572
|
+
...measureState,
|
|
3573
|
+
start: reanchorDrawingPoint(measureState.start),
|
|
3574
|
+
end: reanchorDrawingPoint(measureState.end)
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3567
3577
|
};
|
|
3568
3578
|
const getCandleDirectionByIndex = (index) => {
|
|
3569
3579
|
const point = data[index];
|
|
@@ -3665,6 +3675,22 @@ function createChart(element, options = {}) {
|
|
|
3665
3675
|
const pad = (value) => String(value).padStart(2, "0");
|
|
3666
3676
|
return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
|
|
3667
3677
|
};
|
|
3678
|
+
const formatTimeSpan = (ms) => {
|
|
3679
|
+
const totalMinutes = Math.max(0, Math.round(ms / 6e4));
|
|
3680
|
+
if (totalMinutes < 1) {
|
|
3681
|
+
return `${Math.max(1, Math.round(ms / 1e3))}s`;
|
|
3682
|
+
}
|
|
3683
|
+
const days = Math.floor(totalMinutes / 1440);
|
|
3684
|
+
const hours = Math.floor(totalMinutes % 1440 / 60);
|
|
3685
|
+
const minutes = totalMinutes % 60;
|
|
3686
|
+
if (days > 0) {
|
|
3687
|
+
return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
|
3688
|
+
}
|
|
3689
|
+
if (hours > 0) {
|
|
3690
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
3691
|
+
}
|
|
3692
|
+
return `${minutes}m`;
|
|
3693
|
+
};
|
|
3668
3694
|
const getRightAxisLabelX = (chartRight) => chartRight + 1;
|
|
3669
3695
|
const getRightAxisLabelWidth = (chartRight, contentWidth) => {
|
|
3670
3696
|
const scaleWidth = Math.max(0, Math.floor(width - getRightAxisLabelX(chartRight)));
|
|
@@ -4033,6 +4059,10 @@ function createChart(element, options = {}) {
|
|
|
4033
4059
|
let pendingDrawUpdateAutoScale = false;
|
|
4034
4060
|
let pendingDrawOverlayOnly = true;
|
|
4035
4061
|
const scheduleDraw = (options2 = {}) => {
|
|
4062
|
+
if (activeDrawingTool !== lastNotifiedDrawingTool) {
|
|
4063
|
+
lastNotifiedDrawingTool = activeDrawingTool;
|
|
4064
|
+
drawingToolChangeHandler?.(activeDrawingTool);
|
|
4065
|
+
}
|
|
4036
4066
|
armCountdownHeartbeat();
|
|
4037
4067
|
const overlayOnly = options2.overlayOnly ?? false;
|
|
4038
4068
|
pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
|
|
@@ -4788,6 +4818,216 @@ function createChart(element, options = {}) {
|
|
|
4788
4818
|
drawDrawingLabel(drawing.label, (leftX + rightX) / 2, topY - 4, drawing.color);
|
|
4789
4819
|
}
|
|
4790
4820
|
}
|
|
4821
|
+
} else if (drawing.type === "ellipse") {
|
|
4822
|
+
const p0 = drawing.points[0];
|
|
4823
|
+
const p1 = drawing.points[1];
|
|
4824
|
+
if (p0 && p1) {
|
|
4825
|
+
const px0 = xFromDrawingPoint(p0);
|
|
4826
|
+
const px1 = xFromDrawingPoint(p1);
|
|
4827
|
+
const y0 = yFromPrice(p0.price);
|
|
4828
|
+
const y1 = yFromPrice(p1.price);
|
|
4829
|
+
const cx = (px0 + px1) / 2;
|
|
4830
|
+
const cy = (y0 + y1) / 2;
|
|
4831
|
+
const rx = Math.max(1, Math.abs(px1 - px0) / 2);
|
|
4832
|
+
const ry = Math.max(1, Math.abs(y1 - y0) / 2);
|
|
4833
|
+
ctx.save();
|
|
4834
|
+
ctx.globalAlpha = draft ? 0.6 : 1;
|
|
4835
|
+
ctx.beginPath();
|
|
4836
|
+
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
|
|
4837
|
+
ctx.fillStyle = hexToRgba(drawing.color, 0.14);
|
|
4838
|
+
ctx.fill();
|
|
4839
|
+
ctx.lineWidth = Math.max(1, drawing.width);
|
|
4840
|
+
ctx.strokeStyle = drawing.color;
|
|
4841
|
+
applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
4842
|
+
ctx.stroke();
|
|
4843
|
+
ctx.restore();
|
|
4844
|
+
handleAt(px0, y0, drawing.color);
|
|
4845
|
+
handleAt(px1, y1, drawing.color);
|
|
4846
|
+
handleAt(px0, y1, drawing.color);
|
|
4847
|
+
handleAt(px1, y0, drawing.color);
|
|
4848
|
+
if (drawing.label) {
|
|
4849
|
+
drawDrawingLabel(drawing.label, cx, cy - ry - 4, drawing.color);
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
} else if (drawing.type === "parallel-channel") {
|
|
4853
|
+
const p0 = drawing.points[0];
|
|
4854
|
+
const p1 = drawing.points[1];
|
|
4855
|
+
const p2 = drawing.points[2];
|
|
4856
|
+
if (p0 && p1) {
|
|
4857
|
+
const x0 = xFromDrawingPoint(p0);
|
|
4858
|
+
const y0 = yFromPrice(p0.price);
|
|
4859
|
+
const x1 = xFromDrawingPoint(p1);
|
|
4860
|
+
const y1 = yFromPrice(p1.price);
|
|
4861
|
+
ctx.beginPath();
|
|
4862
|
+
ctx.moveTo(x0, y0);
|
|
4863
|
+
ctx.lineTo(x1, y1);
|
|
4864
|
+
ctx.stroke();
|
|
4865
|
+
handleAt(x0, y0, drawing.color);
|
|
4866
|
+
handleAt(x1, y1, drawing.color);
|
|
4867
|
+
if (p2) {
|
|
4868
|
+
const indexSpan = p1.index - p0.index;
|
|
4869
|
+
const slope = indexSpan !== 0 ? (p1.price - p0.price) / indexSpan : 0;
|
|
4870
|
+
const basePriceAtP2 = p0.price + slope * (p2.index - p0.index);
|
|
4871
|
+
const offset = p2.price - basePriceAtP2;
|
|
4872
|
+
const q0x = x0;
|
|
4873
|
+
const q0y = yFromPrice(p0.price + offset);
|
|
4874
|
+
const q1x = x1;
|
|
4875
|
+
const q1y = yFromPrice(p1.price + offset);
|
|
4876
|
+
ctx.save();
|
|
4877
|
+
ctx.globalAlpha = draft ? 0.5 : 1;
|
|
4878
|
+
ctx.fillStyle = hexToRgba(drawing.color, 0.08);
|
|
4879
|
+
ctx.beginPath();
|
|
4880
|
+
ctx.moveTo(x0, y0);
|
|
4881
|
+
ctx.lineTo(x1, y1);
|
|
4882
|
+
ctx.lineTo(q1x, q1y);
|
|
4883
|
+
ctx.lineTo(q0x, q0y);
|
|
4884
|
+
ctx.closePath();
|
|
4885
|
+
ctx.fill();
|
|
4886
|
+
ctx.restore();
|
|
4887
|
+
ctx.beginPath();
|
|
4888
|
+
ctx.moveTo(q0x, q0y);
|
|
4889
|
+
ctx.lineTo(q1x, q1y);
|
|
4890
|
+
ctx.stroke();
|
|
4891
|
+
ctx.save();
|
|
4892
|
+
ctx.setLineDash([4, 4]);
|
|
4893
|
+
ctx.globalAlpha = draft ? 0.5 : 0.7;
|
|
4894
|
+
ctx.beginPath();
|
|
4895
|
+
ctx.moveTo(x0, (y0 + q0y) / 2);
|
|
4896
|
+
ctx.lineTo(x1, (y1 + q1y) / 2);
|
|
4897
|
+
ctx.stroke();
|
|
4898
|
+
ctx.restore();
|
|
4899
|
+
handleAt(xFromDrawingPoint(p2), yFromPrice(p2.price), drawing.color);
|
|
4900
|
+
}
|
|
4901
|
+
if (drawing.label) {
|
|
4902
|
+
drawDrawingLabel(drawing.label, (x0 + x1) / 2, Math.min(y0, y1) - 4, drawing.color);
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
4905
|
+
} else if (drawing.type === "arrow") {
|
|
4906
|
+
const first = drawing.points[0];
|
|
4907
|
+
const second = drawing.points[1];
|
|
4908
|
+
if (first && second) {
|
|
4909
|
+
const x0 = xFromDrawingPoint(first);
|
|
4910
|
+
const y0 = yFromPrice(first.price);
|
|
4911
|
+
const x1 = xFromDrawingPoint(second);
|
|
4912
|
+
const y1 = yFromPrice(second.price);
|
|
4913
|
+
const angle = Math.atan2(y1 - y0, x1 - x0);
|
|
4914
|
+
const head = 5 + Math.max(1, drawing.width) * 3;
|
|
4915
|
+
const shaftX = x1 - Math.cos(angle) * head * 0.6;
|
|
4916
|
+
const shaftY = y1 - Math.sin(angle) * head * 0.6;
|
|
4917
|
+
ctx.beginPath();
|
|
4918
|
+
ctx.moveTo(x0, y0);
|
|
4919
|
+
ctx.lineTo(shaftX, shaftY);
|
|
4920
|
+
ctx.stroke();
|
|
4921
|
+
ctx.save();
|
|
4922
|
+
ctx.setLineDash([]);
|
|
4923
|
+
ctx.fillStyle = drawing.color;
|
|
4924
|
+
ctx.beginPath();
|
|
4925
|
+
ctx.moveTo(x1, y1);
|
|
4926
|
+
ctx.lineTo(x1 - Math.cos(angle - Math.PI / 7) * head, y1 - Math.sin(angle - Math.PI / 7) * head);
|
|
4927
|
+
ctx.lineTo(x1 - Math.cos(angle + Math.PI / 7) * head, y1 - Math.sin(angle + Math.PI / 7) * head);
|
|
4928
|
+
ctx.closePath();
|
|
4929
|
+
ctx.fill();
|
|
4930
|
+
ctx.restore();
|
|
4931
|
+
handleAt(x0, y0, drawing.color);
|
|
4932
|
+
handleAt(x1, y1, drawing.color);
|
|
4933
|
+
if (drawing.label) {
|
|
4934
|
+
const midX = (x0 + x1) / 2;
|
|
4935
|
+
const midY = (y0 + y1) / 2;
|
|
4936
|
+
drawDrawingLabel(drawing.label, midX, midY, drawing.color);
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
} else if (drawing.type === "brush") {
|
|
4940
|
+
const pts = drawing.points;
|
|
4941
|
+
if (pts.length >= 2) {
|
|
4942
|
+
ctx.save();
|
|
4943
|
+
ctx.lineJoin = "round";
|
|
4944
|
+
ctx.lineCap = "round";
|
|
4945
|
+
ctx.beginPath();
|
|
4946
|
+
const startX = xFromDrawingPoint(pts[0]);
|
|
4947
|
+
const startY = yFromPrice(pts[0].price);
|
|
4948
|
+
ctx.moveTo(startX, startY);
|
|
4949
|
+
if (pts.length === 2) {
|
|
4950
|
+
ctx.lineTo(xFromDrawingPoint(pts[1]), yFromPrice(pts[1].price));
|
|
4951
|
+
} else {
|
|
4952
|
+
for (let i = 1; i < pts.length - 1; i += 1) {
|
|
4953
|
+
const cxp = xFromDrawingPoint(pts[i]);
|
|
4954
|
+
const cyp = yFromPrice(pts[i].price);
|
|
4955
|
+
const nx = xFromDrawingPoint(pts[i + 1]);
|
|
4956
|
+
const ny = yFromPrice(pts[i + 1].price);
|
|
4957
|
+
ctx.quadraticCurveTo(cxp, cyp, (cxp + nx) / 2, (cyp + ny) / 2);
|
|
4958
|
+
}
|
|
4959
|
+
const last = pts[pts.length - 1];
|
|
4960
|
+
ctx.lineTo(xFromDrawingPoint(last), yFromPrice(last.price));
|
|
4961
|
+
}
|
|
4962
|
+
ctx.stroke();
|
|
4963
|
+
ctx.restore();
|
|
4964
|
+
if (isSelected) {
|
|
4965
|
+
handleAt(startX, startY, drawing.color);
|
|
4966
|
+
const last = pts[pts.length - 1];
|
|
4967
|
+
handleAt(xFromDrawingPoint(last), yFromPrice(last.price), drawing.color);
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
} else if (drawing.type === "callout") {
|
|
4971
|
+
const anchor = drawing.points[0];
|
|
4972
|
+
const boxPoint = drawing.points[1];
|
|
4973
|
+
if (anchor && boxPoint) {
|
|
4974
|
+
const ax = xFromDrawingPoint(anchor);
|
|
4975
|
+
const ay = yFromPrice(anchor.price);
|
|
4976
|
+
const bx = xFromDrawingPoint(boxPoint);
|
|
4977
|
+
const by = yFromPrice(boxPoint.price);
|
|
4978
|
+
const fontSize = Math.max(6, drawing.fontSize);
|
|
4979
|
+
const prevFont = ctx.font;
|
|
4980
|
+
ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
|
|
4981
|
+
const rawLabel = drawing.label ?? "";
|
|
4982
|
+
const isPlaceholder = rawLabel.trim().length === 0;
|
|
4983
|
+
const lines = (isPlaceholder ? "Text" : rawLabel).split("\n");
|
|
4984
|
+
const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
|
|
4985
|
+
const padX = 10;
|
|
4986
|
+
const padY = 7;
|
|
4987
|
+
const lineH = Math.round(fontSize * 1.35);
|
|
4988
|
+
const blockW = textW + padX * 2;
|
|
4989
|
+
const blockH = lines.length * lineH + padY * 2;
|
|
4990
|
+
const edgeX = Math.max(bx, Math.min(bx + blockW, ax));
|
|
4991
|
+
const edgeY = Math.max(by, Math.min(by + blockH, ay));
|
|
4992
|
+
const insideBox = ax >= bx && ax <= bx + blockW && ay >= by && ay <= by + blockH;
|
|
4993
|
+
if (!insideBox) {
|
|
4994
|
+
ctx.save();
|
|
4995
|
+
ctx.setLineDash([]);
|
|
4996
|
+
ctx.lineWidth = Math.max(1, drawing.width);
|
|
4997
|
+
ctx.beginPath();
|
|
4998
|
+
ctx.moveTo(edgeX, edgeY);
|
|
4999
|
+
ctx.lineTo(ax, ay);
|
|
5000
|
+
ctx.stroke();
|
|
5001
|
+
const angle = Math.atan2(ay - edgeY, ax - edgeX);
|
|
5002
|
+
const head = 7;
|
|
5003
|
+
ctx.fillStyle = drawing.color;
|
|
5004
|
+
ctx.beginPath();
|
|
5005
|
+
ctx.moveTo(ax, ay);
|
|
5006
|
+
ctx.lineTo(ax - Math.cos(angle - Math.PI / 7) * head, ay - Math.sin(angle - Math.PI / 7) * head);
|
|
5007
|
+
ctx.lineTo(ax - Math.cos(angle + Math.PI / 7) * head, ay - Math.sin(angle + Math.PI / 7) * head);
|
|
5008
|
+
ctx.closePath();
|
|
5009
|
+
ctx.fill();
|
|
5010
|
+
ctx.restore();
|
|
5011
|
+
}
|
|
5012
|
+
ctx.save();
|
|
5013
|
+
ctx.setLineDash([]);
|
|
5014
|
+
ctx.fillStyle = hexToRgba(drawing.color, 0.16);
|
|
5015
|
+
fillRoundedRect(bx, by, blockW, blockH, 6);
|
|
5016
|
+
ctx.strokeStyle = drawing.color;
|
|
5017
|
+
ctx.lineWidth = Math.max(1, drawing.width);
|
|
5018
|
+
strokeRoundedRect(bx, by, blockW, blockH, 6);
|
|
5019
|
+
ctx.fillStyle = drawing.color;
|
|
5020
|
+
ctx.globalAlpha = (draft ? 0.72 : 1) * (isPlaceholder ? 0.55 : 1);
|
|
5021
|
+
ctx.textAlign = "left";
|
|
5022
|
+
ctx.textBaseline = "middle";
|
|
5023
|
+
lines.forEach((line, lineIndex) => {
|
|
5024
|
+
ctx.fillText(line, bx + padX, by + padY + lineIndex * lineH + lineH / 2);
|
|
5025
|
+
});
|
|
5026
|
+
ctx.restore();
|
|
5027
|
+
ctx.font = prevFont;
|
|
5028
|
+
handleAt(ax, ay, drawing.color);
|
|
5029
|
+
handleAt(bx, by, drawing.color);
|
|
5030
|
+
}
|
|
4791
5031
|
} else if (drawing.type === "price-range") {
|
|
4792
5032
|
const p0 = drawing.points[0];
|
|
4793
5033
|
const p1 = drawing.points[1];
|
|
@@ -4845,7 +5085,7 @@ function createChart(element, options = {}) {
|
|
|
4845
5085
|
const t0 = getTimeForIndex(Math.round(p0.index));
|
|
4846
5086
|
const t1 = getTimeForIndex(Math.round(p1.index));
|
|
4847
5087
|
const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
|
|
4848
|
-
labelLines.push(spanMs > 0 ? `${barSpan} bars, ${
|
|
5088
|
+
labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatTimeSpan(spanMs)}` : `${barSpan} bars`);
|
|
4849
5089
|
}
|
|
4850
5090
|
const pointValue = Number(drawing.pointValue);
|
|
4851
5091
|
if (Number.isFinite(pointValue) && pointValue > 0) {
|
|
@@ -5322,6 +5562,96 @@ function createChart(element, options = {}) {
|
|
|
5322
5562
|
if (draftDrawing) {
|
|
5323
5563
|
drawDrawing(draftDrawing, true);
|
|
5324
5564
|
}
|
|
5565
|
+
if (measureState) {
|
|
5566
|
+
const mStart = measureState.start;
|
|
5567
|
+
const mEnd = measureState.end;
|
|
5568
|
+
const mx0 = xFromDrawingPoint(mStart);
|
|
5569
|
+
const mx1 = xFromDrawingPoint(mEnd);
|
|
5570
|
+
const my0 = yFromPrice(mStart.price);
|
|
5571
|
+
const my1 = yFromPrice(mEnd.price);
|
|
5572
|
+
const mLeft = Math.min(mx0, mx1);
|
|
5573
|
+
const mRight = Math.max(mx0, mx1);
|
|
5574
|
+
const mTop = Math.min(my0, my1);
|
|
5575
|
+
const mBottom = Math.max(my0, my1);
|
|
5576
|
+
const mUp = mEnd.price >= mStart.price;
|
|
5577
|
+
const mColor = mUp ? "#2962ff" : "#f23645";
|
|
5578
|
+
ctx.save();
|
|
5579
|
+
ctx.fillStyle = hexToRgba(mColor, 0.12);
|
|
5580
|
+
ctx.fillRect(mLeft, mTop, Math.max(1, mRight - mLeft), Math.max(1, mBottom - mTop));
|
|
5581
|
+
ctx.strokeStyle = mColor;
|
|
5582
|
+
ctx.lineWidth = 1;
|
|
5583
|
+
ctx.setLineDash([]);
|
|
5584
|
+
const mArrowHead = 5;
|
|
5585
|
+
const mMidX = (mLeft + mRight) / 2;
|
|
5586
|
+
if (Math.abs(my1 - my0) > mArrowHead * 2) {
|
|
5587
|
+
ctx.beginPath();
|
|
5588
|
+
ctx.moveTo(crisp(mMidX), crisp(my0));
|
|
5589
|
+
ctx.lineTo(crisp(mMidX), crisp(my1));
|
|
5590
|
+
ctx.stroke();
|
|
5591
|
+
const dirY = my1 > my0 ? 1 : -1;
|
|
5592
|
+
ctx.beginPath();
|
|
5593
|
+
ctx.moveTo(mMidX, my1);
|
|
5594
|
+
ctx.lineTo(mMidX - mArrowHead, my1 - dirY * mArrowHead);
|
|
5595
|
+
ctx.moveTo(mMidX, my1);
|
|
5596
|
+
ctx.lineTo(mMidX + mArrowHead, my1 - dirY * mArrowHead);
|
|
5597
|
+
ctx.stroke();
|
|
5598
|
+
}
|
|
5599
|
+
const mMidY = (mTop + mBottom) / 2;
|
|
5600
|
+
if (Math.abs(mx1 - mx0) > mArrowHead * 2) {
|
|
5601
|
+
ctx.beginPath();
|
|
5602
|
+
ctx.moveTo(crisp(mx0), crisp(mMidY));
|
|
5603
|
+
ctx.lineTo(crisp(mx1), crisp(mMidY));
|
|
5604
|
+
ctx.stroke();
|
|
5605
|
+
const dirX = mx1 > mx0 ? 1 : -1;
|
|
5606
|
+
ctx.beginPath();
|
|
5607
|
+
ctx.moveTo(mx1, mMidY);
|
|
5608
|
+
ctx.lineTo(mx1 - dirX * mArrowHead, mMidY - mArrowHead);
|
|
5609
|
+
ctx.moveTo(mx1, mMidY);
|
|
5610
|
+
ctx.lineTo(mx1 - dirX * mArrowHead, mMidY + mArrowHead);
|
|
5611
|
+
ctx.stroke();
|
|
5612
|
+
}
|
|
5613
|
+
const mDiff = mEnd.price - mStart.price;
|
|
5614
|
+
const mBase = Math.abs(mStart.price) > 0 ? Math.abs(mStart.price) : 1;
|
|
5615
|
+
const mPct = mDiff / mBase * 100;
|
|
5616
|
+
const mTick = getConfiguredTickSize();
|
|
5617
|
+
const mTicks = mTick > 0 ? Math.round(mDiff / mTick) : 0;
|
|
5618
|
+
const mSigned = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
|
|
5619
|
+
const mLines = [
|
|
5620
|
+
mTick > 0 ? `${mSigned(mDiff, formatPrice(Math.abs(mDiff)))} (${mSigned(mPct, `${Math.abs(mPct).toFixed(2)}%`)}) ${mSigned(mTicks, String(Math.abs(mTicks)))}` : `${mSigned(mDiff, formatPrice(Math.abs(mDiff)))} (${mSigned(mPct, `${Math.abs(mPct).toFixed(2)}%`)})`
|
|
5621
|
+
];
|
|
5622
|
+
const mBars = Math.round(mEnd.index) - Math.round(mStart.index);
|
|
5623
|
+
const mT0 = getTimeForIndex(Math.round(mStart.index));
|
|
5624
|
+
const mT1 = getTimeForIndex(Math.round(mEnd.index));
|
|
5625
|
+
const mSpanMs = mT0 && mT1 ? Math.abs(mT1.getTime() - mT0.getTime()) : 0;
|
|
5626
|
+
mLines.push(
|
|
5627
|
+
mSpanMs > 0 ? `${mSigned(mBars, String(Math.abs(mBars)))} bars, ${formatTimeSpan(mSpanMs)}` : `${mSigned(mBars, String(Math.abs(mBars)))} bars`
|
|
5628
|
+
);
|
|
5629
|
+
const prevMeasureFont = ctx.font;
|
|
5630
|
+
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
5631
|
+
const mPad = 7;
|
|
5632
|
+
const mLineHeight = 15;
|
|
5633
|
+
const mTextW = Math.max(...mLines.map((line) => measureTextWidth(line)));
|
|
5634
|
+
const mPillW = mTextW + mPad * 2;
|
|
5635
|
+
const mPillH = mLines.length * mLineHeight + mPad;
|
|
5636
|
+
const mPillX = clamp(mMidX - mPillW / 2, chartLeft + 2, chartLeft + chartWidth - mPillW - 2);
|
|
5637
|
+
let mPillY = mUp ? mTop - 8 - mPillH : mBottom + 8;
|
|
5638
|
+
if (mPillY < chartTop + 2) {
|
|
5639
|
+
mPillY = mBottom + 8;
|
|
5640
|
+
}
|
|
5641
|
+
if (mPillY + mPillH > fullChartBottom - 2) {
|
|
5642
|
+
mPillY = Math.max(chartTop + 2, mTop - 8 - mPillH);
|
|
5643
|
+
}
|
|
5644
|
+
ctx.fillStyle = mColor;
|
|
5645
|
+
fillRoundedRect(mPillX, mPillY, mPillW, mPillH, 4);
|
|
5646
|
+
ctx.fillStyle = "#ffffff";
|
|
5647
|
+
ctx.textAlign = "center";
|
|
5648
|
+
ctx.textBaseline = "middle";
|
|
5649
|
+
mLines.forEach((line, lineIndex) => {
|
|
5650
|
+
ctx.fillText(line, mPillX + mPillW / 2, mPillY + mPad / 2 + lineIndex * mLineHeight + mLineHeight / 2);
|
|
5651
|
+
});
|
|
5652
|
+
ctx.font = prevMeasureFont;
|
|
5653
|
+
ctx.restore();
|
|
5654
|
+
}
|
|
5325
5655
|
if (tradeMarkers.length > 0 && data.length > 0) {
|
|
5326
5656
|
const visibleStart = Math.floor(xStart) - 1;
|
|
5327
5657
|
const visibleEnd = Math.ceil(xStart + xSpan) + 1;
|
|
@@ -6752,6 +7082,126 @@ function createChart(element, options = {}) {
|
|
|
6752
7082
|
if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
|
|
6753
7083
|
return { drawing, target: "line" };
|
|
6754
7084
|
}
|
|
7085
|
+
} else if (drawing.type === "ellipse") {
|
|
7086
|
+
const p0 = drawing.points[0];
|
|
7087
|
+
const p1 = drawing.points[1];
|
|
7088
|
+
if (!p0 || !p1) continue;
|
|
7089
|
+
const px0 = canvasXFromDrawingPoint(p0);
|
|
7090
|
+
const px1 = canvasXFromDrawingPoint(p1);
|
|
7091
|
+
const y0 = canvasYFromDrawingPrice(p0.price);
|
|
7092
|
+
const y1 = canvasYFromDrawingPrice(p1.price);
|
|
7093
|
+
if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
|
|
7094
|
+
if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
|
|
7095
|
+
if (Math.hypot(x - px0, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 2 };
|
|
7096
|
+
if (Math.hypot(x - px1, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 3 };
|
|
7097
|
+
const cx = (px0 + px1) / 2;
|
|
7098
|
+
const cy = (y0 + y1) / 2;
|
|
7099
|
+
const rx = Math.max(1, Math.abs(px1 - px0) / 2);
|
|
7100
|
+
const ry = Math.max(1, Math.abs(y1 - y0) / 2);
|
|
7101
|
+
const norm = ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2;
|
|
7102
|
+
if (norm <= 1) {
|
|
7103
|
+
return { drawing, target: "line" };
|
|
7104
|
+
}
|
|
7105
|
+
} else if (drawing.type === "parallel-channel") {
|
|
7106
|
+
const p0 = drawing.points[0];
|
|
7107
|
+
const p1 = drawing.points[1];
|
|
7108
|
+
const p2 = drawing.points[2];
|
|
7109
|
+
if (!p0 || !p1) continue;
|
|
7110
|
+
const x0 = canvasXFromDrawingPoint(p0);
|
|
7111
|
+
const y0 = canvasYFromDrawingPrice(p0.price);
|
|
7112
|
+
const x1 = canvasXFromDrawingPoint(p1);
|
|
7113
|
+
const y1 = canvasYFromDrawingPrice(p1.price);
|
|
7114
|
+
if (Math.hypot(x - x0, y - y0) <= 8) return { drawing, target: "handle", pointIndex: 0 };
|
|
7115
|
+
if (Math.hypot(x - x1, y - y1) <= 8) return { drawing, target: "handle", pointIndex: 1 };
|
|
7116
|
+
if (p2) {
|
|
7117
|
+
const x2 = canvasXFromDrawingPoint(p2);
|
|
7118
|
+
const y2 = canvasYFromDrawingPrice(p2.price);
|
|
7119
|
+
if (Math.hypot(x - x2, y - y2) <= 8) return { drawing, target: "handle", pointIndex: 2 };
|
|
7120
|
+
const indexSpan = p1.index - p0.index;
|
|
7121
|
+
const slope = indexSpan !== 0 ? (p1.price - p0.price) / indexSpan : 0;
|
|
7122
|
+
const offset = p2.price - (p0.price + slope * (p2.index - p0.index));
|
|
7123
|
+
const q0y = canvasYFromDrawingPrice(p0.price + offset);
|
|
7124
|
+
const q1y = canvasYFromDrawingPrice(p1.price + offset);
|
|
7125
|
+
if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x0, q0y, x1, q1y) <= 6) {
|
|
7126
|
+
return { drawing, target: "line" };
|
|
7127
|
+
}
|
|
7128
|
+
const minX = Math.min(x0, x1);
|
|
7129
|
+
const maxX = Math.max(x0, x1);
|
|
7130
|
+
if (x >= minX && x <= maxX && maxX > minX) {
|
|
7131
|
+
const t = (x - x0) / (x1 - x0);
|
|
7132
|
+
const baseY = y0 + t * (y1 - y0);
|
|
7133
|
+
const parY = q0y + t * (q1y - q0y);
|
|
7134
|
+
if (y >= Math.min(baseY, parY) && y <= Math.max(baseY, parY)) {
|
|
7135
|
+
return { drawing, target: "line" };
|
|
7136
|
+
}
|
|
7137
|
+
}
|
|
7138
|
+
} else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
|
|
7139
|
+
return { drawing, target: "line" };
|
|
7140
|
+
}
|
|
7141
|
+
} else if (drawing.type === "arrow") {
|
|
7142
|
+
const first = drawing.points[0];
|
|
7143
|
+
const second = drawing.points[1];
|
|
7144
|
+
if (!first || !second) continue;
|
|
7145
|
+
const x1 = canvasXFromDrawingPoint(first);
|
|
7146
|
+
const y1 = canvasYFromDrawingPrice(first.price);
|
|
7147
|
+
const x2 = canvasXFromDrawingPoint(second);
|
|
7148
|
+
const y2 = canvasYFromDrawingPrice(second.price);
|
|
7149
|
+
if (Math.hypot(x - x1, y - y1) <= 8) {
|
|
7150
|
+
return { drawing, target: "handle", pointIndex: 0 };
|
|
7151
|
+
}
|
|
7152
|
+
if (Math.hypot(x - x2, y - y2) <= 8) {
|
|
7153
|
+
return { drawing, target: "handle", pointIndex: 1 };
|
|
7154
|
+
}
|
|
7155
|
+
if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
|
|
7156
|
+
return { drawing, target: "line" };
|
|
7157
|
+
}
|
|
7158
|
+
} else if (drawing.type === "brush") {
|
|
7159
|
+
const pts = drawing.points;
|
|
7160
|
+
if (pts.length < 2) continue;
|
|
7161
|
+
let hitBody = false;
|
|
7162
|
+
let prevX = canvasXFromDrawingPoint(pts[0]);
|
|
7163
|
+
let prevY = canvasYFromDrawingPrice(pts[0].price);
|
|
7164
|
+
for (let i = 1; i < pts.length; i += 1) {
|
|
7165
|
+
const nextX = canvasXFromDrawingPoint(pts[i]);
|
|
7166
|
+
const nextY = canvasYFromDrawingPrice(pts[i].price);
|
|
7167
|
+
if (distanceToSegment(x, y, prevX, prevY, nextX, nextY) <= 7) {
|
|
7168
|
+
hitBody = true;
|
|
7169
|
+
break;
|
|
7170
|
+
}
|
|
7171
|
+
prevX = nextX;
|
|
7172
|
+
prevY = nextY;
|
|
7173
|
+
}
|
|
7174
|
+
if (hitBody) {
|
|
7175
|
+
return { drawing, target: "line" };
|
|
7176
|
+
}
|
|
7177
|
+
} else if (drawing.type === "callout") {
|
|
7178
|
+
const anchor = drawing.points[0];
|
|
7179
|
+
const boxPoint = drawing.points[1];
|
|
7180
|
+
if (!anchor || !boxPoint) continue;
|
|
7181
|
+
const ax = canvasXFromDrawingPoint(anchor);
|
|
7182
|
+
const ay = canvasYFromDrawingPrice(anchor.price);
|
|
7183
|
+
const bx = canvasXFromDrawingPoint(boxPoint);
|
|
7184
|
+
const by = canvasYFromDrawingPrice(boxPoint.price);
|
|
7185
|
+
if (Math.hypot(x - ax, y - ay) <= 9) return { drawing, target: "handle", pointIndex: 0 };
|
|
7186
|
+
if (Math.hypot(x - bx, y - by) <= 9) return { drawing, target: "handle", pointIndex: 1 };
|
|
7187
|
+
const fontSize = Math.max(6, drawing.fontSize);
|
|
7188
|
+
const prevFont = ctx.font;
|
|
7189
|
+
ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
|
|
7190
|
+
const rawLabel = drawing.label ?? "";
|
|
7191
|
+
const lines = (rawLabel.trim().length === 0 ? "Text" : rawLabel).split("\n");
|
|
7192
|
+
const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
|
|
7193
|
+
ctx.font = prevFont;
|
|
7194
|
+
const padX = 10;
|
|
7195
|
+
const padY = 7;
|
|
7196
|
+
const lineH = Math.round(fontSize * 1.35);
|
|
7197
|
+
const blockW = textW + padX * 2;
|
|
7198
|
+
const blockH = lines.length * lineH + padY * 2;
|
|
7199
|
+
if (x >= bx && x <= bx + blockW && y >= by && y <= by + blockH) {
|
|
7200
|
+
return { drawing, target: "line" };
|
|
7201
|
+
}
|
|
7202
|
+
if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5) {
|
|
7203
|
+
return { drawing, target: "line" };
|
|
7204
|
+
}
|
|
6755
7205
|
} else if (drawing.type === "price-range") {
|
|
6756
7206
|
const p0 = drawing.points[0];
|
|
6757
7207
|
const p1 = drawing.points[1];
|
|
@@ -6980,6 +7430,128 @@ function createChart(element, options = {}) {
|
|
|
6980
7430
|
scheduleDraw();
|
|
6981
7431
|
return true;
|
|
6982
7432
|
}
|
|
7433
|
+
if (activeDrawingTool === "arrow") {
|
|
7434
|
+
if (draftDrawing?.type === "arrow") {
|
|
7435
|
+
const anchor = draftDrawing.points[0];
|
|
7436
|
+
const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
|
|
7437
|
+
const completed = normalizeDrawingState({
|
|
7438
|
+
...serializeDrawing(draftDrawing),
|
|
7439
|
+
points: [anchor, endPoint]
|
|
7440
|
+
});
|
|
7441
|
+
drawings.push(completed);
|
|
7442
|
+
draftDrawing = null;
|
|
7443
|
+
activeDrawingTool = null;
|
|
7444
|
+
emitDrawingsChange();
|
|
7445
|
+
scheduleDraw();
|
|
7446
|
+
return true;
|
|
7447
|
+
}
|
|
7448
|
+
const defaults = getDrawingToolDefaults("arrow");
|
|
7449
|
+
draftDrawing = normalizeDrawingState({
|
|
7450
|
+
type: "arrow",
|
|
7451
|
+
points: [point, point],
|
|
7452
|
+
color: defaults.color ?? "#2563eb",
|
|
7453
|
+
style: defaults.style ?? "solid",
|
|
7454
|
+
width: defaults.width ?? 2
|
|
7455
|
+
});
|
|
7456
|
+
scheduleDraw();
|
|
7457
|
+
return true;
|
|
7458
|
+
}
|
|
7459
|
+
if (activeDrawingTool === "parallel-channel") {
|
|
7460
|
+
if (draftDrawing?.type === "parallel-channel") {
|
|
7461
|
+
if (draftDrawing.points.length < 3) {
|
|
7462
|
+
draftDrawing = normalizeDrawingState({
|
|
7463
|
+
...serializeDrawing(draftDrawing),
|
|
7464
|
+
points: [...draftDrawing.points.slice(0, -1), point, point]
|
|
7465
|
+
});
|
|
7466
|
+
scheduleDraw();
|
|
7467
|
+
return true;
|
|
7468
|
+
}
|
|
7469
|
+
const completed = normalizeDrawingState({
|
|
7470
|
+
...serializeDrawing(draftDrawing),
|
|
7471
|
+
points: [draftDrawing.points[0], draftDrawing.points[1], point]
|
|
7472
|
+
});
|
|
7473
|
+
drawings.push(completed);
|
|
7474
|
+
draftDrawing = null;
|
|
7475
|
+
activeDrawingTool = null;
|
|
7476
|
+
emitDrawingsChange();
|
|
7477
|
+
scheduleDraw();
|
|
7478
|
+
return true;
|
|
7479
|
+
}
|
|
7480
|
+
const defaults = getDrawingToolDefaults("parallel-channel");
|
|
7481
|
+
draftDrawing = normalizeDrawingState({
|
|
7482
|
+
type: "parallel-channel",
|
|
7483
|
+
points: [point, point],
|
|
7484
|
+
color: defaults.color ?? "#2563eb",
|
|
7485
|
+
style: defaults.style ?? "solid",
|
|
7486
|
+
width: defaults.width ?? 2
|
|
7487
|
+
});
|
|
7488
|
+
scheduleDraw();
|
|
7489
|
+
return true;
|
|
7490
|
+
}
|
|
7491
|
+
if (activeDrawingTool === "ellipse") {
|
|
7492
|
+
if (draftDrawing?.type === "ellipse") {
|
|
7493
|
+
const completed = normalizeDrawingState({
|
|
7494
|
+
...serializeDrawing(draftDrawing),
|
|
7495
|
+
points: [draftDrawing.points[0], point]
|
|
7496
|
+
});
|
|
7497
|
+
drawings.push(completed);
|
|
7498
|
+
draftDrawing = null;
|
|
7499
|
+
activeDrawingTool = null;
|
|
7500
|
+
emitDrawingsChange();
|
|
7501
|
+
scheduleDraw();
|
|
7502
|
+
return true;
|
|
7503
|
+
}
|
|
7504
|
+
const defaults = getDrawingToolDefaults("ellipse");
|
|
7505
|
+
draftDrawing = normalizeDrawingState({
|
|
7506
|
+
type: "ellipse",
|
|
7507
|
+
points: [point, point],
|
|
7508
|
+
color: defaults.color ?? "#2962ff",
|
|
7509
|
+
style: defaults.style ?? "solid",
|
|
7510
|
+
width: defaults.width ?? 1
|
|
7511
|
+
});
|
|
7512
|
+
scheduleDraw();
|
|
7513
|
+
return true;
|
|
7514
|
+
}
|
|
7515
|
+
if (activeDrawingTool === "callout") {
|
|
7516
|
+
if (draftDrawing?.type === "callout") {
|
|
7517
|
+
const completed = normalizeDrawingState({
|
|
7518
|
+
...serializeDrawing(draftDrawing),
|
|
7519
|
+
points: [draftDrawing.points[0], point]
|
|
7520
|
+
});
|
|
7521
|
+
drawings.push(completed);
|
|
7522
|
+
draftDrawing = null;
|
|
7523
|
+
activeDrawingTool = null;
|
|
7524
|
+
selectedDrawingId = completed.id;
|
|
7525
|
+
emitDrawingsChange();
|
|
7526
|
+
scheduleDraw();
|
|
7527
|
+
drawingEditTextHandler?.({ drawing: serializeDrawing(completed), target: "line", x, y });
|
|
7528
|
+
return true;
|
|
7529
|
+
}
|
|
7530
|
+
const defaults = getDrawingToolDefaults("callout");
|
|
7531
|
+
draftDrawing = normalizeDrawingState({
|
|
7532
|
+
type: "callout",
|
|
7533
|
+
points: [point, point],
|
|
7534
|
+
color: defaults.color ?? "#2962ff",
|
|
7535
|
+
style: defaults.style ?? "solid",
|
|
7536
|
+
width: defaults.width ?? 1,
|
|
7537
|
+
...defaults.fontSize === void 0 ? {} : { fontSize: defaults.fontSize },
|
|
7538
|
+
label: ""
|
|
7539
|
+
});
|
|
7540
|
+
scheduleDraw();
|
|
7541
|
+
return true;
|
|
7542
|
+
}
|
|
7543
|
+
if (activeDrawingTool === "brush") {
|
|
7544
|
+
const defaults = getDrawingToolDefaults("brush");
|
|
7545
|
+
draftDrawing = normalizeDrawingState({
|
|
7546
|
+
type: "brush",
|
|
7547
|
+
points: [point],
|
|
7548
|
+
color: defaults.color ?? "#f59e0b",
|
|
7549
|
+
style: defaults.style ?? "solid",
|
|
7550
|
+
width: defaults.width ?? 2
|
|
7551
|
+
});
|
|
7552
|
+
scheduleDraw();
|
|
7553
|
+
return true;
|
|
7554
|
+
}
|
|
6983
7555
|
if (activeDrawingTool === "fib-retracement") {
|
|
6984
7556
|
if (draftDrawing?.type === "fib-retracement") {
|
|
6985
7557
|
const completed = normalizeDrawingState({
|
|
@@ -7183,7 +7755,7 @@ function createChart(element, options = {}) {
|
|
|
7183
7755
|
}
|
|
7184
7756
|
return { ...drawing, points: pts };
|
|
7185
7757
|
}
|
|
7186
|
-
if (drawingDragState.target === "handle" && drawing.type === "rectangle") {
|
|
7758
|
+
if (drawingDragState.target === "handle" && (drawing.type === "rectangle" || drawing.type === "ellipse")) {
|
|
7187
7759
|
const pts = drawing.points.map((point) => ({ ...point }));
|
|
7188
7760
|
const p0 = pts[0];
|
|
7189
7761
|
const p1 = pts[1];
|
|
@@ -7202,7 +7774,7 @@ function createChart(element, options = {}) {
|
|
|
7202
7774
|
}
|
|
7203
7775
|
return { ...drawing, points: pts };
|
|
7204
7776
|
}
|
|
7205
|
-
if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray")) {
|
|
7777
|
+
if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray" || drawing.type === "arrow")) {
|
|
7206
7778
|
const pointIndex = drawingDragState.pointIndex ?? 0;
|
|
7207
7779
|
const anchor = drawing.points[pointIndex === 0 ? 1 : 0];
|
|
7208
7780
|
const constrained = anchor ? constrainAngleFromAnchor(anchor, x, y) : null;
|
|
@@ -7320,6 +7892,19 @@ function createChart(element, options = {}) {
|
|
|
7320
7892
|
return;
|
|
7321
7893
|
}
|
|
7322
7894
|
}
|
|
7895
|
+
if (measureState) {
|
|
7896
|
+
if (measureState.dragging) {
|
|
7897
|
+
measureState = { ...measureState, dragging: false };
|
|
7898
|
+
if (activeDrawingTool === "measure") {
|
|
7899
|
+
activeDrawingTool = null;
|
|
7900
|
+
canvas.style.cursor = "default";
|
|
7901
|
+
}
|
|
7902
|
+
scheduleDraw();
|
|
7903
|
+
return;
|
|
7904
|
+
}
|
|
7905
|
+
measureState = null;
|
|
7906
|
+
scheduleDraw();
|
|
7907
|
+
}
|
|
7323
7908
|
const drawingToolCapturesPointer = activeDrawingTool !== null || draftDrawing !== null;
|
|
7324
7909
|
if (!drawingToolCapturesPointer) {
|
|
7325
7910
|
const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
|
|
@@ -7394,6 +7979,18 @@ function createChart(element, options = {}) {
|
|
|
7394
7979
|
if (region === "outside") {
|
|
7395
7980
|
return;
|
|
7396
7981
|
}
|
|
7982
|
+
if (region === "plot" && (activeDrawingTool === "measure" || activeDrawingTool === null && draftDrawing === null && event.shiftKey && event.pointerType === "mouse")) {
|
|
7983
|
+
const measurePoint = drawingPointFromCanvas(point.x, point.y);
|
|
7984
|
+
if (measurePoint) {
|
|
7985
|
+
measureState = { start: measurePoint, end: measurePoint, dragging: true };
|
|
7986
|
+
activePointerId = event.pointerId;
|
|
7987
|
+
capturePointer(event.pointerId);
|
|
7988
|
+
setCrosshairPoint(null);
|
|
7989
|
+
canvas.style.cursor = "crosshair";
|
|
7990
|
+
scheduleDraw();
|
|
7991
|
+
return;
|
|
7992
|
+
}
|
|
7993
|
+
}
|
|
7397
7994
|
if (region === "plot" && !activeDrawingTool) {
|
|
7398
7995
|
const drawingHit = getDrawingHit(point.x, point.y);
|
|
7399
7996
|
if (drawingHit) {
|
|
@@ -7427,6 +8024,10 @@ function createChart(element, options = {}) {
|
|
|
7427
8024
|
if (region === "plot" && handleDrawingToolPointerDown(point.x, point.y)) {
|
|
7428
8025
|
setCrosshairPoint(null);
|
|
7429
8026
|
canvas.style.cursor = "crosshair";
|
|
8027
|
+
if (activeDrawingTool === "brush" && draftDrawing?.type === "brush") {
|
|
8028
|
+
activePointerId = event.pointerId;
|
|
8029
|
+
capturePointer(event.pointerId);
|
|
8030
|
+
}
|
|
7430
8031
|
return;
|
|
7431
8032
|
}
|
|
7432
8033
|
isDragging = true;
|
|
@@ -7489,6 +8090,19 @@ function createChart(element, options = {}) {
|
|
|
7489
8090
|
}
|
|
7490
8091
|
return;
|
|
7491
8092
|
}
|
|
8093
|
+
if (measureState?.dragging) {
|
|
8094
|
+
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
8095
|
+
return;
|
|
8096
|
+
}
|
|
8097
|
+
const nextMeasurePoint = drawingPointFromCanvas(point.x, point.y);
|
|
8098
|
+
if (nextMeasurePoint) {
|
|
8099
|
+
measureState = { ...measureState, end: nextMeasurePoint };
|
|
8100
|
+
setCrosshairPoint(null);
|
|
8101
|
+
canvas.style.cursor = "crosshair";
|
|
8102
|
+
scheduleDraw();
|
|
8103
|
+
}
|
|
8104
|
+
return;
|
|
8105
|
+
}
|
|
7492
8106
|
if (drawingDragState) {
|
|
7493
8107
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
7494
8108
|
return;
|
|
@@ -7498,9 +8112,30 @@ function createChart(element, options = {}) {
|
|
|
7498
8112
|
setCrosshairPoint(null);
|
|
7499
8113
|
return;
|
|
7500
8114
|
}
|
|
7501
|
-
if (draftDrawing
|
|
8115
|
+
if (draftDrawing?.type === "brush" && activeDrawingTool === "brush") {
|
|
8116
|
+
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
8117
|
+
return;
|
|
8118
|
+
}
|
|
8119
|
+
const nextPoint = drawingPointFromCanvas(point.x, point.y);
|
|
8120
|
+
if (nextPoint) {
|
|
8121
|
+
const last = draftDrawing.points[draftDrawing.points.length - 1];
|
|
8122
|
+
const lastX = last ? canvasXFromDrawingPoint(last) : Number.NEGATIVE_INFINITY;
|
|
8123
|
+
const lastY = last ? canvasYFromDrawingPrice(last.price) : Number.NEGATIVE_INFINITY;
|
|
8124
|
+
if (Math.hypot(point.x - lastX, point.y - lastY) >= 3) {
|
|
8125
|
+
draftDrawing = {
|
|
8126
|
+
...draftDrawing,
|
|
8127
|
+
points: [...draftDrawing.points, nextPoint]
|
|
8128
|
+
};
|
|
8129
|
+
}
|
|
8130
|
+
canvas.style.cursor = "crosshair";
|
|
8131
|
+
setCrosshairPoint(null);
|
|
8132
|
+
scheduleDraw();
|
|
8133
|
+
}
|
|
8134
|
+
return;
|
|
8135
|
+
}
|
|
8136
|
+
if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "arrow" || activeDrawingTool === "rectangle" || activeDrawingTool === "ellipse" || activeDrawingTool === "callout" || activeDrawingTool === "parallel-channel" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
|
|
7502
8137
|
let nextPoint = drawingPointFromCanvas(point.x, point.y);
|
|
7503
|
-
if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray") && draftDrawing.points[0]) {
|
|
8138
|
+
if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray" || draftDrawing.type === "arrow") && draftDrawing.points[0]) {
|
|
7504
8139
|
nextPoint = constrainAngleFromAnchor(draftDrawing.points[0], point.x, point.y) ?? nextPoint;
|
|
7505
8140
|
}
|
|
7506
8141
|
if (nextPoint) {
|
|
@@ -7719,6 +8354,43 @@ function createChart(element, options = {}) {
|
|
|
7719
8354
|
if (event && activePointerId !== null && event.pointerId !== activePointerId) {
|
|
7720
8355
|
return;
|
|
7721
8356
|
}
|
|
8357
|
+
if (measureState?.dragging) {
|
|
8358
|
+
const movedMeasure = measureState.start.index !== measureState.end.index || measureState.start.price !== measureState.end.price;
|
|
8359
|
+
if (movedMeasure) {
|
|
8360
|
+
measureState = { ...measureState, dragging: false };
|
|
8361
|
+
if (activeDrawingTool === "measure") {
|
|
8362
|
+
activeDrawingTool = null;
|
|
8363
|
+
}
|
|
8364
|
+
canvas.style.cursor = "default";
|
|
8365
|
+
} else {
|
|
8366
|
+
canvas.style.cursor = "crosshair";
|
|
8367
|
+
}
|
|
8368
|
+
isDragging = false;
|
|
8369
|
+
dragMode = null;
|
|
8370
|
+
activePointerId = null;
|
|
8371
|
+
pointerDownInfo = null;
|
|
8372
|
+
scheduleDraw();
|
|
8373
|
+
return;
|
|
8374
|
+
}
|
|
8375
|
+
if (draftDrawing?.type === "brush" && activeDrawingTool === "brush") {
|
|
8376
|
+
if (draftDrawing.points.length >= 2) {
|
|
8377
|
+
const completed = normalizeDrawingState(serializeDrawing(draftDrawing));
|
|
8378
|
+
drawings.push(completed);
|
|
8379
|
+
draftDrawing = null;
|
|
8380
|
+
activeDrawingTool = null;
|
|
8381
|
+
canvas.style.cursor = "default";
|
|
8382
|
+
emitDrawingsChange();
|
|
8383
|
+
} else {
|
|
8384
|
+
draftDrawing = null;
|
|
8385
|
+
canvas.style.cursor = "crosshair";
|
|
8386
|
+
}
|
|
8387
|
+
isDragging = false;
|
|
8388
|
+
dragMode = null;
|
|
8389
|
+
activePointerId = null;
|
|
8390
|
+
pointerDownInfo = null;
|
|
8391
|
+
scheduleDraw();
|
|
8392
|
+
return;
|
|
8393
|
+
}
|
|
7722
8394
|
if (orderDragState) {
|
|
7723
8395
|
const moved = orderDragState.lastPrice !== orderDragState.startPrice;
|
|
7724
8396
|
const finalLine = orderLines.find((line) => line.id === orderDragState?.orderId);
|
|
@@ -7862,7 +8534,7 @@ function createChart(element, options = {}) {
|
|
|
7862
8534
|
x: point.x,
|
|
7863
8535
|
y: point.y
|
|
7864
8536
|
};
|
|
7865
|
-
if (drawingHit.drawing.type === "text" || drawingHit.drawing.type === "note") {
|
|
8537
|
+
if (drawingHit.drawing.type === "text" || drawingHit.drawing.type === "note" || drawingHit.drawing.type === "callout") {
|
|
7866
8538
|
drawingEditTextHandler?.(payload);
|
|
7867
8539
|
} else {
|
|
7868
8540
|
drawingDoubleClickHandler?.(payload);
|
|
@@ -7899,6 +8571,15 @@ function createChart(element, options = {}) {
|
|
|
7899
8571
|
canvas.addEventListener("dblclick", onDoubleClick);
|
|
7900
8572
|
canvas.addEventListener("contextmenu", onContextMenu);
|
|
7901
8573
|
const onModifierKeyChange = (event) => {
|
|
8574
|
+
if (event.key === "Escape" && event.type === "keydown" && measureState) {
|
|
8575
|
+
measureState = null;
|
|
8576
|
+
if (activeDrawingTool === "measure") {
|
|
8577
|
+
activeDrawingTool = null;
|
|
8578
|
+
canvas.style.cursor = "default";
|
|
8579
|
+
}
|
|
8580
|
+
scheduleDraw();
|
|
8581
|
+
return;
|
|
8582
|
+
}
|
|
7902
8583
|
const active = event.metaKey || event.ctrlKey;
|
|
7903
8584
|
const shift = event.shiftKey;
|
|
7904
8585
|
const changed = active !== magnetModifierActive || shift !== shiftKeyActive;
|
|
@@ -8211,17 +8892,29 @@ function createChart(element, options = {}) {
|
|
|
8211
8892
|
const setActiveDrawingTool = (tool) => {
|
|
8212
8893
|
activeDrawingTool = tool;
|
|
8213
8894
|
draftDrawing = null;
|
|
8895
|
+
measureState = null;
|
|
8214
8896
|
canvas.style.cursor = tool ? "crosshair" : "default";
|
|
8215
8897
|
scheduleDraw();
|
|
8216
8898
|
};
|
|
8217
8899
|
const getActiveDrawingTool = () => activeDrawingTool;
|
|
8218
8900
|
const cancelDrawing = () => {
|
|
8901
|
+
let cancelled = false;
|
|
8219
8902
|
if (draftDrawing) {
|
|
8220
8903
|
draftDrawing = null;
|
|
8904
|
+
cancelled = true;
|
|
8905
|
+
}
|
|
8906
|
+
if (measureState) {
|
|
8907
|
+
measureState = null;
|
|
8908
|
+
if (activeDrawingTool === "measure") {
|
|
8909
|
+
activeDrawingTool = null;
|
|
8910
|
+
canvas.style.cursor = "default";
|
|
8911
|
+
}
|
|
8912
|
+
cancelled = true;
|
|
8913
|
+
}
|
|
8914
|
+
if (cancelled) {
|
|
8221
8915
|
scheduleDraw();
|
|
8222
|
-
return true;
|
|
8223
8916
|
}
|
|
8224
|
-
return
|
|
8917
|
+
return cancelled;
|
|
8225
8918
|
};
|
|
8226
8919
|
const setTradeMarkers = (markers) => {
|
|
8227
8920
|
tradeMarkers = Array.isArray(markers) ? markers.slice() : [];
|
|
@@ -8273,6 +8966,9 @@ function createChart(element, options = {}) {
|
|
|
8273
8966
|
const onDrawingsChange = (handler) => {
|
|
8274
8967
|
drawingsChangeHandler = handler;
|
|
8275
8968
|
};
|
|
8969
|
+
const onActiveDrawingToolChange = (handler) => {
|
|
8970
|
+
drawingToolChangeHandler = handler;
|
|
8971
|
+
};
|
|
8276
8972
|
const onDrawingSelect = (handler) => {
|
|
8277
8973
|
drawingSelectHandler = handler;
|
|
8278
8974
|
};
|
|
@@ -8285,6 +8981,51 @@ function createChart(element, options = {}) {
|
|
|
8285
8981
|
const onDrawingHover = (handler) => {
|
|
8286
8982
|
drawingHoverHandler = handler;
|
|
8287
8983
|
};
|
|
8984
|
+
const saveState = () => {
|
|
8985
|
+
const drawingDefaults = {};
|
|
8986
|
+
for (const [tool, defaults] of drawingToolDefaults) {
|
|
8987
|
+
drawingDefaults[tool] = { ...defaults };
|
|
8988
|
+
}
|
|
8989
|
+
return {
|
|
8990
|
+
version: 1,
|
|
8991
|
+
chartType: getChartType(),
|
|
8992
|
+
viewport: getViewport(),
|
|
8993
|
+
drawings: getDrawings(),
|
|
8994
|
+
indicators: getIndicators(),
|
|
8995
|
+
magnetMode: getMagnetMode(),
|
|
8996
|
+
drawingDefaults
|
|
8997
|
+
};
|
|
8998
|
+
};
|
|
8999
|
+
const loadState = (state) => {
|
|
9000
|
+
if (!state || typeof state !== "object") {
|
|
9001
|
+
return;
|
|
9002
|
+
}
|
|
9003
|
+
if (typeof state.chartType === "string") {
|
|
9004
|
+
setChartType(state.chartType);
|
|
9005
|
+
}
|
|
9006
|
+
if (Array.isArray(state.indicators)) {
|
|
9007
|
+
setIndicators(state.indicators);
|
|
9008
|
+
}
|
|
9009
|
+
if (Array.isArray(state.drawings)) {
|
|
9010
|
+
setDrawings(state.drawings);
|
|
9011
|
+
}
|
|
9012
|
+
if (state.magnetMode === "none" || state.magnetMode === "weak" || state.magnetMode === "strong") {
|
|
9013
|
+
setMagnetMode(state.magnetMode);
|
|
9014
|
+
}
|
|
9015
|
+
if (state.drawingDefaults && typeof state.drawingDefaults === "object") {
|
|
9016
|
+
for (const [tool, defaults] of Object.entries(state.drawingDefaults)) {
|
|
9017
|
+
if (defaults && typeof defaults === "object") {
|
|
9018
|
+
setDrawingDefaults(tool, defaults);
|
|
9019
|
+
}
|
|
9020
|
+
}
|
|
9021
|
+
}
|
|
9022
|
+
if (state.viewport && typeof state.viewport === "object") {
|
|
9023
|
+
setViewport(state.viewport);
|
|
9024
|
+
}
|
|
9025
|
+
};
|
|
9026
|
+
const takeScreenshot = (options2 = {}) => {
|
|
9027
|
+
return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
|
|
9028
|
+
};
|
|
8288
9029
|
const destroy = () => {
|
|
8289
9030
|
cancelKineticPan();
|
|
8290
9031
|
if (smoothingRafId !== null) {
|
|
@@ -8359,6 +9100,7 @@ function createChart(element, options = {}) {
|
|
|
8359
9100
|
removeDrawing,
|
|
8360
9101
|
clearDrawings,
|
|
8361
9102
|
onDrawingsChange,
|
|
9103
|
+
onActiveDrawingToolChange,
|
|
8362
9104
|
onDrawingSelect,
|
|
8363
9105
|
onDrawingDoubleClick,
|
|
8364
9106
|
onDrawingEditText,
|
|
@@ -8379,6 +9121,9 @@ function createChart(element, options = {}) {
|
|
|
8379
9121
|
updateIndicator,
|
|
8380
9122
|
removeIndicator,
|
|
8381
9123
|
setIndicators,
|
|
9124
|
+
saveState,
|
|
9125
|
+
loadState,
|
|
9126
|
+
takeScreenshot,
|
|
8382
9127
|
resize,
|
|
8383
9128
|
destroy
|
|
8384
9129
|
};
|