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.js
CHANGED
|
@@ -2940,8 +2940,11 @@ function createChart(element, options = {}) {
|
|
|
2940
2940
|
);
|
|
2941
2941
|
let activeDrawingTool = null;
|
|
2942
2942
|
let draftDrawing = null;
|
|
2943
|
+
let measureState = null;
|
|
2943
2944
|
let drawingDragState = null;
|
|
2944
2945
|
let drawingsChangeHandler = null;
|
|
2946
|
+
let drawingToolChangeHandler = null;
|
|
2947
|
+
let lastNotifiedDrawingTool = null;
|
|
2945
2948
|
let drawingSelectHandler = null;
|
|
2946
2949
|
let drawingDoubleClickHandler = null;
|
|
2947
2950
|
let drawingEditTextHandler = null;
|
|
@@ -3534,6 +3537,13 @@ function createChart(element, options = {}) {
|
|
|
3534
3537
|
points: draftDrawing.points.map((point) => reanchorDrawingPoint(point))
|
|
3535
3538
|
};
|
|
3536
3539
|
}
|
|
3540
|
+
if (measureState) {
|
|
3541
|
+
measureState = {
|
|
3542
|
+
...measureState,
|
|
3543
|
+
start: reanchorDrawingPoint(measureState.start),
|
|
3544
|
+
end: reanchorDrawingPoint(measureState.end)
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3537
3547
|
};
|
|
3538
3548
|
const getCandleDirectionByIndex = (index) => {
|
|
3539
3549
|
const point = data[index];
|
|
@@ -3635,6 +3645,22 @@ function createChart(element, options = {}) {
|
|
|
3635
3645
|
const pad = (value) => String(value).padStart(2, "0");
|
|
3636
3646
|
return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
|
|
3637
3647
|
};
|
|
3648
|
+
const formatTimeSpan = (ms) => {
|
|
3649
|
+
const totalMinutes = Math.max(0, Math.round(ms / 6e4));
|
|
3650
|
+
if (totalMinutes < 1) {
|
|
3651
|
+
return `${Math.max(1, Math.round(ms / 1e3))}s`;
|
|
3652
|
+
}
|
|
3653
|
+
const days = Math.floor(totalMinutes / 1440);
|
|
3654
|
+
const hours = Math.floor(totalMinutes % 1440 / 60);
|
|
3655
|
+
const minutes = totalMinutes % 60;
|
|
3656
|
+
if (days > 0) {
|
|
3657
|
+
return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
|
3658
|
+
}
|
|
3659
|
+
if (hours > 0) {
|
|
3660
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
3661
|
+
}
|
|
3662
|
+
return `${minutes}m`;
|
|
3663
|
+
};
|
|
3638
3664
|
const getRightAxisLabelX = (chartRight) => chartRight + 1;
|
|
3639
3665
|
const getRightAxisLabelWidth = (chartRight, contentWidth) => {
|
|
3640
3666
|
const scaleWidth = Math.max(0, Math.floor(width - getRightAxisLabelX(chartRight)));
|
|
@@ -4003,6 +4029,10 @@ function createChart(element, options = {}) {
|
|
|
4003
4029
|
let pendingDrawUpdateAutoScale = false;
|
|
4004
4030
|
let pendingDrawOverlayOnly = true;
|
|
4005
4031
|
const scheduleDraw = (options2 = {}) => {
|
|
4032
|
+
if (activeDrawingTool !== lastNotifiedDrawingTool) {
|
|
4033
|
+
lastNotifiedDrawingTool = activeDrawingTool;
|
|
4034
|
+
drawingToolChangeHandler?.(activeDrawingTool);
|
|
4035
|
+
}
|
|
4006
4036
|
armCountdownHeartbeat();
|
|
4007
4037
|
const overlayOnly = options2.overlayOnly ?? false;
|
|
4008
4038
|
pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
|
|
@@ -4758,6 +4788,216 @@ function createChart(element, options = {}) {
|
|
|
4758
4788
|
drawDrawingLabel(drawing.label, (leftX + rightX) / 2, topY - 4, drawing.color);
|
|
4759
4789
|
}
|
|
4760
4790
|
}
|
|
4791
|
+
} else if (drawing.type === "ellipse") {
|
|
4792
|
+
const p0 = drawing.points[0];
|
|
4793
|
+
const p1 = drawing.points[1];
|
|
4794
|
+
if (p0 && p1) {
|
|
4795
|
+
const px0 = xFromDrawingPoint(p0);
|
|
4796
|
+
const px1 = xFromDrawingPoint(p1);
|
|
4797
|
+
const y0 = yFromPrice(p0.price);
|
|
4798
|
+
const y1 = yFromPrice(p1.price);
|
|
4799
|
+
const cx = (px0 + px1) / 2;
|
|
4800
|
+
const cy = (y0 + y1) / 2;
|
|
4801
|
+
const rx = Math.max(1, Math.abs(px1 - px0) / 2);
|
|
4802
|
+
const ry = Math.max(1, Math.abs(y1 - y0) / 2);
|
|
4803
|
+
ctx.save();
|
|
4804
|
+
ctx.globalAlpha = draft ? 0.6 : 1;
|
|
4805
|
+
ctx.beginPath();
|
|
4806
|
+
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
|
|
4807
|
+
ctx.fillStyle = hexToRgba(drawing.color, 0.14);
|
|
4808
|
+
ctx.fill();
|
|
4809
|
+
ctx.lineWidth = Math.max(1, drawing.width);
|
|
4810
|
+
ctx.strokeStyle = drawing.color;
|
|
4811
|
+
applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
4812
|
+
ctx.stroke();
|
|
4813
|
+
ctx.restore();
|
|
4814
|
+
handleAt(px0, y0, drawing.color);
|
|
4815
|
+
handleAt(px1, y1, drawing.color);
|
|
4816
|
+
handleAt(px0, y1, drawing.color);
|
|
4817
|
+
handleAt(px1, y0, drawing.color);
|
|
4818
|
+
if (drawing.label) {
|
|
4819
|
+
drawDrawingLabel(drawing.label, cx, cy - ry - 4, drawing.color);
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
} else if (drawing.type === "parallel-channel") {
|
|
4823
|
+
const p0 = drawing.points[0];
|
|
4824
|
+
const p1 = drawing.points[1];
|
|
4825
|
+
const p2 = drawing.points[2];
|
|
4826
|
+
if (p0 && p1) {
|
|
4827
|
+
const x0 = xFromDrawingPoint(p0);
|
|
4828
|
+
const y0 = yFromPrice(p0.price);
|
|
4829
|
+
const x1 = xFromDrawingPoint(p1);
|
|
4830
|
+
const y1 = yFromPrice(p1.price);
|
|
4831
|
+
ctx.beginPath();
|
|
4832
|
+
ctx.moveTo(x0, y0);
|
|
4833
|
+
ctx.lineTo(x1, y1);
|
|
4834
|
+
ctx.stroke();
|
|
4835
|
+
handleAt(x0, y0, drawing.color);
|
|
4836
|
+
handleAt(x1, y1, drawing.color);
|
|
4837
|
+
if (p2) {
|
|
4838
|
+
const indexSpan = p1.index - p0.index;
|
|
4839
|
+
const slope = indexSpan !== 0 ? (p1.price - p0.price) / indexSpan : 0;
|
|
4840
|
+
const basePriceAtP2 = p0.price + slope * (p2.index - p0.index);
|
|
4841
|
+
const offset = p2.price - basePriceAtP2;
|
|
4842
|
+
const q0x = x0;
|
|
4843
|
+
const q0y = yFromPrice(p0.price + offset);
|
|
4844
|
+
const q1x = x1;
|
|
4845
|
+
const q1y = yFromPrice(p1.price + offset);
|
|
4846
|
+
ctx.save();
|
|
4847
|
+
ctx.globalAlpha = draft ? 0.5 : 1;
|
|
4848
|
+
ctx.fillStyle = hexToRgba(drawing.color, 0.08);
|
|
4849
|
+
ctx.beginPath();
|
|
4850
|
+
ctx.moveTo(x0, y0);
|
|
4851
|
+
ctx.lineTo(x1, y1);
|
|
4852
|
+
ctx.lineTo(q1x, q1y);
|
|
4853
|
+
ctx.lineTo(q0x, q0y);
|
|
4854
|
+
ctx.closePath();
|
|
4855
|
+
ctx.fill();
|
|
4856
|
+
ctx.restore();
|
|
4857
|
+
ctx.beginPath();
|
|
4858
|
+
ctx.moveTo(q0x, q0y);
|
|
4859
|
+
ctx.lineTo(q1x, q1y);
|
|
4860
|
+
ctx.stroke();
|
|
4861
|
+
ctx.save();
|
|
4862
|
+
ctx.setLineDash([4, 4]);
|
|
4863
|
+
ctx.globalAlpha = draft ? 0.5 : 0.7;
|
|
4864
|
+
ctx.beginPath();
|
|
4865
|
+
ctx.moveTo(x0, (y0 + q0y) / 2);
|
|
4866
|
+
ctx.lineTo(x1, (y1 + q1y) / 2);
|
|
4867
|
+
ctx.stroke();
|
|
4868
|
+
ctx.restore();
|
|
4869
|
+
handleAt(xFromDrawingPoint(p2), yFromPrice(p2.price), drawing.color);
|
|
4870
|
+
}
|
|
4871
|
+
if (drawing.label) {
|
|
4872
|
+
drawDrawingLabel(drawing.label, (x0 + x1) / 2, Math.min(y0, y1) - 4, drawing.color);
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4875
|
+
} else if (drawing.type === "arrow") {
|
|
4876
|
+
const first = drawing.points[0];
|
|
4877
|
+
const second = drawing.points[1];
|
|
4878
|
+
if (first && second) {
|
|
4879
|
+
const x0 = xFromDrawingPoint(first);
|
|
4880
|
+
const y0 = yFromPrice(first.price);
|
|
4881
|
+
const x1 = xFromDrawingPoint(second);
|
|
4882
|
+
const y1 = yFromPrice(second.price);
|
|
4883
|
+
const angle = Math.atan2(y1 - y0, x1 - x0);
|
|
4884
|
+
const head = 5 + Math.max(1, drawing.width) * 3;
|
|
4885
|
+
const shaftX = x1 - Math.cos(angle) * head * 0.6;
|
|
4886
|
+
const shaftY = y1 - Math.sin(angle) * head * 0.6;
|
|
4887
|
+
ctx.beginPath();
|
|
4888
|
+
ctx.moveTo(x0, y0);
|
|
4889
|
+
ctx.lineTo(shaftX, shaftY);
|
|
4890
|
+
ctx.stroke();
|
|
4891
|
+
ctx.save();
|
|
4892
|
+
ctx.setLineDash([]);
|
|
4893
|
+
ctx.fillStyle = drawing.color;
|
|
4894
|
+
ctx.beginPath();
|
|
4895
|
+
ctx.moveTo(x1, y1);
|
|
4896
|
+
ctx.lineTo(x1 - Math.cos(angle - Math.PI / 7) * head, y1 - Math.sin(angle - Math.PI / 7) * head);
|
|
4897
|
+
ctx.lineTo(x1 - Math.cos(angle + Math.PI / 7) * head, y1 - Math.sin(angle + Math.PI / 7) * head);
|
|
4898
|
+
ctx.closePath();
|
|
4899
|
+
ctx.fill();
|
|
4900
|
+
ctx.restore();
|
|
4901
|
+
handleAt(x0, y0, drawing.color);
|
|
4902
|
+
handleAt(x1, y1, drawing.color);
|
|
4903
|
+
if (drawing.label) {
|
|
4904
|
+
const midX = (x0 + x1) / 2;
|
|
4905
|
+
const midY = (y0 + y1) / 2;
|
|
4906
|
+
drawDrawingLabel(drawing.label, midX, midY, drawing.color);
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
} else if (drawing.type === "brush") {
|
|
4910
|
+
const pts = drawing.points;
|
|
4911
|
+
if (pts.length >= 2) {
|
|
4912
|
+
ctx.save();
|
|
4913
|
+
ctx.lineJoin = "round";
|
|
4914
|
+
ctx.lineCap = "round";
|
|
4915
|
+
ctx.beginPath();
|
|
4916
|
+
const startX = xFromDrawingPoint(pts[0]);
|
|
4917
|
+
const startY = yFromPrice(pts[0].price);
|
|
4918
|
+
ctx.moveTo(startX, startY);
|
|
4919
|
+
if (pts.length === 2) {
|
|
4920
|
+
ctx.lineTo(xFromDrawingPoint(pts[1]), yFromPrice(pts[1].price));
|
|
4921
|
+
} else {
|
|
4922
|
+
for (let i = 1; i < pts.length - 1; i += 1) {
|
|
4923
|
+
const cxp = xFromDrawingPoint(pts[i]);
|
|
4924
|
+
const cyp = yFromPrice(pts[i].price);
|
|
4925
|
+
const nx = xFromDrawingPoint(pts[i + 1]);
|
|
4926
|
+
const ny = yFromPrice(pts[i + 1].price);
|
|
4927
|
+
ctx.quadraticCurveTo(cxp, cyp, (cxp + nx) / 2, (cyp + ny) / 2);
|
|
4928
|
+
}
|
|
4929
|
+
const last = pts[pts.length - 1];
|
|
4930
|
+
ctx.lineTo(xFromDrawingPoint(last), yFromPrice(last.price));
|
|
4931
|
+
}
|
|
4932
|
+
ctx.stroke();
|
|
4933
|
+
ctx.restore();
|
|
4934
|
+
if (isSelected) {
|
|
4935
|
+
handleAt(startX, startY, drawing.color);
|
|
4936
|
+
const last = pts[pts.length - 1];
|
|
4937
|
+
handleAt(xFromDrawingPoint(last), yFromPrice(last.price), drawing.color);
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
} else if (drawing.type === "callout") {
|
|
4941
|
+
const anchor = drawing.points[0];
|
|
4942
|
+
const boxPoint = drawing.points[1];
|
|
4943
|
+
if (anchor && boxPoint) {
|
|
4944
|
+
const ax = xFromDrawingPoint(anchor);
|
|
4945
|
+
const ay = yFromPrice(anchor.price);
|
|
4946
|
+
const bx = xFromDrawingPoint(boxPoint);
|
|
4947
|
+
const by = yFromPrice(boxPoint.price);
|
|
4948
|
+
const fontSize = Math.max(6, drawing.fontSize);
|
|
4949
|
+
const prevFont = ctx.font;
|
|
4950
|
+
ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
|
|
4951
|
+
const rawLabel = drawing.label ?? "";
|
|
4952
|
+
const isPlaceholder = rawLabel.trim().length === 0;
|
|
4953
|
+
const lines = (isPlaceholder ? "Text" : rawLabel).split("\n");
|
|
4954
|
+
const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
|
|
4955
|
+
const padX = 10;
|
|
4956
|
+
const padY = 7;
|
|
4957
|
+
const lineH = Math.round(fontSize * 1.35);
|
|
4958
|
+
const blockW = textW + padX * 2;
|
|
4959
|
+
const blockH = lines.length * lineH + padY * 2;
|
|
4960
|
+
const edgeX = Math.max(bx, Math.min(bx + blockW, ax));
|
|
4961
|
+
const edgeY = Math.max(by, Math.min(by + blockH, ay));
|
|
4962
|
+
const insideBox = ax >= bx && ax <= bx + blockW && ay >= by && ay <= by + blockH;
|
|
4963
|
+
if (!insideBox) {
|
|
4964
|
+
ctx.save();
|
|
4965
|
+
ctx.setLineDash([]);
|
|
4966
|
+
ctx.lineWidth = Math.max(1, drawing.width);
|
|
4967
|
+
ctx.beginPath();
|
|
4968
|
+
ctx.moveTo(edgeX, edgeY);
|
|
4969
|
+
ctx.lineTo(ax, ay);
|
|
4970
|
+
ctx.stroke();
|
|
4971
|
+
const angle = Math.atan2(ay - edgeY, ax - edgeX);
|
|
4972
|
+
const head = 7;
|
|
4973
|
+
ctx.fillStyle = drawing.color;
|
|
4974
|
+
ctx.beginPath();
|
|
4975
|
+
ctx.moveTo(ax, ay);
|
|
4976
|
+
ctx.lineTo(ax - Math.cos(angle - Math.PI / 7) * head, ay - Math.sin(angle - Math.PI / 7) * head);
|
|
4977
|
+
ctx.lineTo(ax - Math.cos(angle + Math.PI / 7) * head, ay - Math.sin(angle + Math.PI / 7) * head);
|
|
4978
|
+
ctx.closePath();
|
|
4979
|
+
ctx.fill();
|
|
4980
|
+
ctx.restore();
|
|
4981
|
+
}
|
|
4982
|
+
ctx.save();
|
|
4983
|
+
ctx.setLineDash([]);
|
|
4984
|
+
ctx.fillStyle = hexToRgba(drawing.color, 0.16);
|
|
4985
|
+
fillRoundedRect(bx, by, blockW, blockH, 6);
|
|
4986
|
+
ctx.strokeStyle = drawing.color;
|
|
4987
|
+
ctx.lineWidth = Math.max(1, drawing.width);
|
|
4988
|
+
strokeRoundedRect(bx, by, blockW, blockH, 6);
|
|
4989
|
+
ctx.fillStyle = drawing.color;
|
|
4990
|
+
ctx.globalAlpha = (draft ? 0.72 : 1) * (isPlaceholder ? 0.55 : 1);
|
|
4991
|
+
ctx.textAlign = "left";
|
|
4992
|
+
ctx.textBaseline = "middle";
|
|
4993
|
+
lines.forEach((line, lineIndex) => {
|
|
4994
|
+
ctx.fillText(line, bx + padX, by + padY + lineIndex * lineH + lineH / 2);
|
|
4995
|
+
});
|
|
4996
|
+
ctx.restore();
|
|
4997
|
+
ctx.font = prevFont;
|
|
4998
|
+
handleAt(ax, ay, drawing.color);
|
|
4999
|
+
handleAt(bx, by, drawing.color);
|
|
5000
|
+
}
|
|
4761
5001
|
} else if (drawing.type === "price-range") {
|
|
4762
5002
|
const p0 = drawing.points[0];
|
|
4763
5003
|
const p1 = drawing.points[1];
|
|
@@ -4815,7 +5055,7 @@ function createChart(element, options = {}) {
|
|
|
4815
5055
|
const t0 = getTimeForIndex(Math.round(p0.index));
|
|
4816
5056
|
const t1 = getTimeForIndex(Math.round(p1.index));
|
|
4817
5057
|
const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
|
|
4818
|
-
labelLines.push(spanMs > 0 ? `${barSpan} bars, ${
|
|
5058
|
+
labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatTimeSpan(spanMs)}` : `${barSpan} bars`);
|
|
4819
5059
|
}
|
|
4820
5060
|
const pointValue = Number(drawing.pointValue);
|
|
4821
5061
|
if (Number.isFinite(pointValue) && pointValue > 0) {
|
|
@@ -5292,6 +5532,96 @@ function createChart(element, options = {}) {
|
|
|
5292
5532
|
if (draftDrawing) {
|
|
5293
5533
|
drawDrawing(draftDrawing, true);
|
|
5294
5534
|
}
|
|
5535
|
+
if (measureState) {
|
|
5536
|
+
const mStart = measureState.start;
|
|
5537
|
+
const mEnd = measureState.end;
|
|
5538
|
+
const mx0 = xFromDrawingPoint(mStart);
|
|
5539
|
+
const mx1 = xFromDrawingPoint(mEnd);
|
|
5540
|
+
const my0 = yFromPrice(mStart.price);
|
|
5541
|
+
const my1 = yFromPrice(mEnd.price);
|
|
5542
|
+
const mLeft = Math.min(mx0, mx1);
|
|
5543
|
+
const mRight = Math.max(mx0, mx1);
|
|
5544
|
+
const mTop = Math.min(my0, my1);
|
|
5545
|
+
const mBottom = Math.max(my0, my1);
|
|
5546
|
+
const mUp = mEnd.price >= mStart.price;
|
|
5547
|
+
const mColor = mUp ? "#2962ff" : "#f23645";
|
|
5548
|
+
ctx.save();
|
|
5549
|
+
ctx.fillStyle = hexToRgba(mColor, 0.12);
|
|
5550
|
+
ctx.fillRect(mLeft, mTop, Math.max(1, mRight - mLeft), Math.max(1, mBottom - mTop));
|
|
5551
|
+
ctx.strokeStyle = mColor;
|
|
5552
|
+
ctx.lineWidth = 1;
|
|
5553
|
+
ctx.setLineDash([]);
|
|
5554
|
+
const mArrowHead = 5;
|
|
5555
|
+
const mMidX = (mLeft + mRight) / 2;
|
|
5556
|
+
if (Math.abs(my1 - my0) > mArrowHead * 2) {
|
|
5557
|
+
ctx.beginPath();
|
|
5558
|
+
ctx.moveTo(crisp(mMidX), crisp(my0));
|
|
5559
|
+
ctx.lineTo(crisp(mMidX), crisp(my1));
|
|
5560
|
+
ctx.stroke();
|
|
5561
|
+
const dirY = my1 > my0 ? 1 : -1;
|
|
5562
|
+
ctx.beginPath();
|
|
5563
|
+
ctx.moveTo(mMidX, my1);
|
|
5564
|
+
ctx.lineTo(mMidX - mArrowHead, my1 - dirY * mArrowHead);
|
|
5565
|
+
ctx.moveTo(mMidX, my1);
|
|
5566
|
+
ctx.lineTo(mMidX + mArrowHead, my1 - dirY * mArrowHead);
|
|
5567
|
+
ctx.stroke();
|
|
5568
|
+
}
|
|
5569
|
+
const mMidY = (mTop + mBottom) / 2;
|
|
5570
|
+
if (Math.abs(mx1 - mx0) > mArrowHead * 2) {
|
|
5571
|
+
ctx.beginPath();
|
|
5572
|
+
ctx.moveTo(crisp(mx0), crisp(mMidY));
|
|
5573
|
+
ctx.lineTo(crisp(mx1), crisp(mMidY));
|
|
5574
|
+
ctx.stroke();
|
|
5575
|
+
const dirX = mx1 > mx0 ? 1 : -1;
|
|
5576
|
+
ctx.beginPath();
|
|
5577
|
+
ctx.moveTo(mx1, mMidY);
|
|
5578
|
+
ctx.lineTo(mx1 - dirX * mArrowHead, mMidY - mArrowHead);
|
|
5579
|
+
ctx.moveTo(mx1, mMidY);
|
|
5580
|
+
ctx.lineTo(mx1 - dirX * mArrowHead, mMidY + mArrowHead);
|
|
5581
|
+
ctx.stroke();
|
|
5582
|
+
}
|
|
5583
|
+
const mDiff = mEnd.price - mStart.price;
|
|
5584
|
+
const mBase = Math.abs(mStart.price) > 0 ? Math.abs(mStart.price) : 1;
|
|
5585
|
+
const mPct = mDiff / mBase * 100;
|
|
5586
|
+
const mTick = getConfiguredTickSize();
|
|
5587
|
+
const mTicks = mTick > 0 ? Math.round(mDiff / mTick) : 0;
|
|
5588
|
+
const mSigned = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
|
|
5589
|
+
const mLines = [
|
|
5590
|
+
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)}%`)})`
|
|
5591
|
+
];
|
|
5592
|
+
const mBars = Math.round(mEnd.index) - Math.round(mStart.index);
|
|
5593
|
+
const mT0 = getTimeForIndex(Math.round(mStart.index));
|
|
5594
|
+
const mT1 = getTimeForIndex(Math.round(mEnd.index));
|
|
5595
|
+
const mSpanMs = mT0 && mT1 ? Math.abs(mT1.getTime() - mT0.getTime()) : 0;
|
|
5596
|
+
mLines.push(
|
|
5597
|
+
mSpanMs > 0 ? `${mSigned(mBars, String(Math.abs(mBars)))} bars, ${formatTimeSpan(mSpanMs)}` : `${mSigned(mBars, String(Math.abs(mBars)))} bars`
|
|
5598
|
+
);
|
|
5599
|
+
const prevMeasureFont = ctx.font;
|
|
5600
|
+
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
5601
|
+
const mPad = 7;
|
|
5602
|
+
const mLineHeight = 15;
|
|
5603
|
+
const mTextW = Math.max(...mLines.map((line) => measureTextWidth(line)));
|
|
5604
|
+
const mPillW = mTextW + mPad * 2;
|
|
5605
|
+
const mPillH = mLines.length * mLineHeight + mPad;
|
|
5606
|
+
const mPillX = clamp(mMidX - mPillW / 2, chartLeft + 2, chartLeft + chartWidth - mPillW - 2);
|
|
5607
|
+
let mPillY = mUp ? mTop - 8 - mPillH : mBottom + 8;
|
|
5608
|
+
if (mPillY < chartTop + 2) {
|
|
5609
|
+
mPillY = mBottom + 8;
|
|
5610
|
+
}
|
|
5611
|
+
if (mPillY + mPillH > fullChartBottom - 2) {
|
|
5612
|
+
mPillY = Math.max(chartTop + 2, mTop - 8 - mPillH);
|
|
5613
|
+
}
|
|
5614
|
+
ctx.fillStyle = mColor;
|
|
5615
|
+
fillRoundedRect(mPillX, mPillY, mPillW, mPillH, 4);
|
|
5616
|
+
ctx.fillStyle = "#ffffff";
|
|
5617
|
+
ctx.textAlign = "center";
|
|
5618
|
+
ctx.textBaseline = "middle";
|
|
5619
|
+
mLines.forEach((line, lineIndex) => {
|
|
5620
|
+
ctx.fillText(line, mPillX + mPillW / 2, mPillY + mPad / 2 + lineIndex * mLineHeight + mLineHeight / 2);
|
|
5621
|
+
});
|
|
5622
|
+
ctx.font = prevMeasureFont;
|
|
5623
|
+
ctx.restore();
|
|
5624
|
+
}
|
|
5295
5625
|
if (tradeMarkers.length > 0 && data.length > 0) {
|
|
5296
5626
|
const visibleStart = Math.floor(xStart) - 1;
|
|
5297
5627
|
const visibleEnd = Math.ceil(xStart + xSpan) + 1;
|
|
@@ -6722,6 +7052,126 @@ function createChart(element, options = {}) {
|
|
|
6722
7052
|
if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
|
|
6723
7053
|
return { drawing, target: "line" };
|
|
6724
7054
|
}
|
|
7055
|
+
} else if (drawing.type === "ellipse") {
|
|
7056
|
+
const p0 = drawing.points[0];
|
|
7057
|
+
const p1 = drawing.points[1];
|
|
7058
|
+
if (!p0 || !p1) continue;
|
|
7059
|
+
const px0 = canvasXFromDrawingPoint(p0);
|
|
7060
|
+
const px1 = canvasXFromDrawingPoint(p1);
|
|
7061
|
+
const y0 = canvasYFromDrawingPrice(p0.price);
|
|
7062
|
+
const y1 = canvasYFromDrawingPrice(p1.price);
|
|
7063
|
+
if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
|
|
7064
|
+
if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
|
|
7065
|
+
if (Math.hypot(x - px0, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 2 };
|
|
7066
|
+
if (Math.hypot(x - px1, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 3 };
|
|
7067
|
+
const cx = (px0 + px1) / 2;
|
|
7068
|
+
const cy = (y0 + y1) / 2;
|
|
7069
|
+
const rx = Math.max(1, Math.abs(px1 - px0) / 2);
|
|
7070
|
+
const ry = Math.max(1, Math.abs(y1 - y0) / 2);
|
|
7071
|
+
const norm = ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2;
|
|
7072
|
+
if (norm <= 1) {
|
|
7073
|
+
return { drawing, target: "line" };
|
|
7074
|
+
}
|
|
7075
|
+
} else if (drawing.type === "parallel-channel") {
|
|
7076
|
+
const p0 = drawing.points[0];
|
|
7077
|
+
const p1 = drawing.points[1];
|
|
7078
|
+
const p2 = drawing.points[2];
|
|
7079
|
+
if (!p0 || !p1) continue;
|
|
7080
|
+
const x0 = canvasXFromDrawingPoint(p0);
|
|
7081
|
+
const y0 = canvasYFromDrawingPrice(p0.price);
|
|
7082
|
+
const x1 = canvasXFromDrawingPoint(p1);
|
|
7083
|
+
const y1 = canvasYFromDrawingPrice(p1.price);
|
|
7084
|
+
if (Math.hypot(x - x0, y - y0) <= 8) return { drawing, target: "handle", pointIndex: 0 };
|
|
7085
|
+
if (Math.hypot(x - x1, y - y1) <= 8) return { drawing, target: "handle", pointIndex: 1 };
|
|
7086
|
+
if (p2) {
|
|
7087
|
+
const x2 = canvasXFromDrawingPoint(p2);
|
|
7088
|
+
const y2 = canvasYFromDrawingPrice(p2.price);
|
|
7089
|
+
if (Math.hypot(x - x2, y - y2) <= 8) return { drawing, target: "handle", pointIndex: 2 };
|
|
7090
|
+
const indexSpan = p1.index - p0.index;
|
|
7091
|
+
const slope = indexSpan !== 0 ? (p1.price - p0.price) / indexSpan : 0;
|
|
7092
|
+
const offset = p2.price - (p0.price + slope * (p2.index - p0.index));
|
|
7093
|
+
const q0y = canvasYFromDrawingPrice(p0.price + offset);
|
|
7094
|
+
const q1y = canvasYFromDrawingPrice(p1.price + offset);
|
|
7095
|
+
if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x0, q0y, x1, q1y) <= 6) {
|
|
7096
|
+
return { drawing, target: "line" };
|
|
7097
|
+
}
|
|
7098
|
+
const minX = Math.min(x0, x1);
|
|
7099
|
+
const maxX = Math.max(x0, x1);
|
|
7100
|
+
if (x >= minX && x <= maxX && maxX > minX) {
|
|
7101
|
+
const t = (x - x0) / (x1 - x0);
|
|
7102
|
+
const baseY = y0 + t * (y1 - y0);
|
|
7103
|
+
const parY = q0y + t * (q1y - q0y);
|
|
7104
|
+
if (y >= Math.min(baseY, parY) && y <= Math.max(baseY, parY)) {
|
|
7105
|
+
return { drawing, target: "line" };
|
|
7106
|
+
}
|
|
7107
|
+
}
|
|
7108
|
+
} else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
|
|
7109
|
+
return { drawing, target: "line" };
|
|
7110
|
+
}
|
|
7111
|
+
} else if (drawing.type === "arrow") {
|
|
7112
|
+
const first = drawing.points[0];
|
|
7113
|
+
const second = drawing.points[1];
|
|
7114
|
+
if (!first || !second) continue;
|
|
7115
|
+
const x1 = canvasXFromDrawingPoint(first);
|
|
7116
|
+
const y1 = canvasYFromDrawingPrice(first.price);
|
|
7117
|
+
const x2 = canvasXFromDrawingPoint(second);
|
|
7118
|
+
const y2 = canvasYFromDrawingPrice(second.price);
|
|
7119
|
+
if (Math.hypot(x - x1, y - y1) <= 8) {
|
|
7120
|
+
return { drawing, target: "handle", pointIndex: 0 };
|
|
7121
|
+
}
|
|
7122
|
+
if (Math.hypot(x - x2, y - y2) <= 8) {
|
|
7123
|
+
return { drawing, target: "handle", pointIndex: 1 };
|
|
7124
|
+
}
|
|
7125
|
+
if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
|
|
7126
|
+
return { drawing, target: "line" };
|
|
7127
|
+
}
|
|
7128
|
+
} else if (drawing.type === "brush") {
|
|
7129
|
+
const pts = drawing.points;
|
|
7130
|
+
if (pts.length < 2) continue;
|
|
7131
|
+
let hitBody = false;
|
|
7132
|
+
let prevX = canvasXFromDrawingPoint(pts[0]);
|
|
7133
|
+
let prevY = canvasYFromDrawingPrice(pts[0].price);
|
|
7134
|
+
for (let i = 1; i < pts.length; i += 1) {
|
|
7135
|
+
const nextX = canvasXFromDrawingPoint(pts[i]);
|
|
7136
|
+
const nextY = canvasYFromDrawingPrice(pts[i].price);
|
|
7137
|
+
if (distanceToSegment(x, y, prevX, prevY, nextX, nextY) <= 7) {
|
|
7138
|
+
hitBody = true;
|
|
7139
|
+
break;
|
|
7140
|
+
}
|
|
7141
|
+
prevX = nextX;
|
|
7142
|
+
prevY = nextY;
|
|
7143
|
+
}
|
|
7144
|
+
if (hitBody) {
|
|
7145
|
+
return { drawing, target: "line" };
|
|
7146
|
+
}
|
|
7147
|
+
} else if (drawing.type === "callout") {
|
|
7148
|
+
const anchor = drawing.points[0];
|
|
7149
|
+
const boxPoint = drawing.points[1];
|
|
7150
|
+
if (!anchor || !boxPoint) continue;
|
|
7151
|
+
const ax = canvasXFromDrawingPoint(anchor);
|
|
7152
|
+
const ay = canvasYFromDrawingPrice(anchor.price);
|
|
7153
|
+
const bx = canvasXFromDrawingPoint(boxPoint);
|
|
7154
|
+
const by = canvasYFromDrawingPrice(boxPoint.price);
|
|
7155
|
+
if (Math.hypot(x - ax, y - ay) <= 9) return { drawing, target: "handle", pointIndex: 0 };
|
|
7156
|
+
if (Math.hypot(x - bx, y - by) <= 9) return { drawing, target: "handle", pointIndex: 1 };
|
|
7157
|
+
const fontSize = Math.max(6, drawing.fontSize);
|
|
7158
|
+
const prevFont = ctx.font;
|
|
7159
|
+
ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
|
|
7160
|
+
const rawLabel = drawing.label ?? "";
|
|
7161
|
+
const lines = (rawLabel.trim().length === 0 ? "Text" : rawLabel).split("\n");
|
|
7162
|
+
const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
|
|
7163
|
+
ctx.font = prevFont;
|
|
7164
|
+
const padX = 10;
|
|
7165
|
+
const padY = 7;
|
|
7166
|
+
const lineH = Math.round(fontSize * 1.35);
|
|
7167
|
+
const blockW = textW + padX * 2;
|
|
7168
|
+
const blockH = lines.length * lineH + padY * 2;
|
|
7169
|
+
if (x >= bx && x <= bx + blockW && y >= by && y <= by + blockH) {
|
|
7170
|
+
return { drawing, target: "line" };
|
|
7171
|
+
}
|
|
7172
|
+
if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5) {
|
|
7173
|
+
return { drawing, target: "line" };
|
|
7174
|
+
}
|
|
6725
7175
|
} else if (drawing.type === "price-range") {
|
|
6726
7176
|
const p0 = drawing.points[0];
|
|
6727
7177
|
const p1 = drawing.points[1];
|
|
@@ -6950,6 +7400,128 @@ function createChart(element, options = {}) {
|
|
|
6950
7400
|
scheduleDraw();
|
|
6951
7401
|
return true;
|
|
6952
7402
|
}
|
|
7403
|
+
if (activeDrawingTool === "arrow") {
|
|
7404
|
+
if (draftDrawing?.type === "arrow") {
|
|
7405
|
+
const anchor = draftDrawing.points[0];
|
|
7406
|
+
const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
|
|
7407
|
+
const completed = normalizeDrawingState({
|
|
7408
|
+
...serializeDrawing(draftDrawing),
|
|
7409
|
+
points: [anchor, endPoint]
|
|
7410
|
+
});
|
|
7411
|
+
drawings.push(completed);
|
|
7412
|
+
draftDrawing = null;
|
|
7413
|
+
activeDrawingTool = null;
|
|
7414
|
+
emitDrawingsChange();
|
|
7415
|
+
scheduleDraw();
|
|
7416
|
+
return true;
|
|
7417
|
+
}
|
|
7418
|
+
const defaults = getDrawingToolDefaults("arrow");
|
|
7419
|
+
draftDrawing = normalizeDrawingState({
|
|
7420
|
+
type: "arrow",
|
|
7421
|
+
points: [point, point],
|
|
7422
|
+
color: defaults.color ?? "#2563eb",
|
|
7423
|
+
style: defaults.style ?? "solid",
|
|
7424
|
+
width: defaults.width ?? 2
|
|
7425
|
+
});
|
|
7426
|
+
scheduleDraw();
|
|
7427
|
+
return true;
|
|
7428
|
+
}
|
|
7429
|
+
if (activeDrawingTool === "parallel-channel") {
|
|
7430
|
+
if (draftDrawing?.type === "parallel-channel") {
|
|
7431
|
+
if (draftDrawing.points.length < 3) {
|
|
7432
|
+
draftDrawing = normalizeDrawingState({
|
|
7433
|
+
...serializeDrawing(draftDrawing),
|
|
7434
|
+
points: [...draftDrawing.points.slice(0, -1), point, point]
|
|
7435
|
+
});
|
|
7436
|
+
scheduleDraw();
|
|
7437
|
+
return true;
|
|
7438
|
+
}
|
|
7439
|
+
const completed = normalizeDrawingState({
|
|
7440
|
+
...serializeDrawing(draftDrawing),
|
|
7441
|
+
points: [draftDrawing.points[0], draftDrawing.points[1], point]
|
|
7442
|
+
});
|
|
7443
|
+
drawings.push(completed);
|
|
7444
|
+
draftDrawing = null;
|
|
7445
|
+
activeDrawingTool = null;
|
|
7446
|
+
emitDrawingsChange();
|
|
7447
|
+
scheduleDraw();
|
|
7448
|
+
return true;
|
|
7449
|
+
}
|
|
7450
|
+
const defaults = getDrawingToolDefaults("parallel-channel");
|
|
7451
|
+
draftDrawing = normalizeDrawingState({
|
|
7452
|
+
type: "parallel-channel",
|
|
7453
|
+
points: [point, point],
|
|
7454
|
+
color: defaults.color ?? "#2563eb",
|
|
7455
|
+
style: defaults.style ?? "solid",
|
|
7456
|
+
width: defaults.width ?? 2
|
|
7457
|
+
});
|
|
7458
|
+
scheduleDraw();
|
|
7459
|
+
return true;
|
|
7460
|
+
}
|
|
7461
|
+
if (activeDrawingTool === "ellipse") {
|
|
7462
|
+
if (draftDrawing?.type === "ellipse") {
|
|
7463
|
+
const completed = normalizeDrawingState({
|
|
7464
|
+
...serializeDrawing(draftDrawing),
|
|
7465
|
+
points: [draftDrawing.points[0], point]
|
|
7466
|
+
});
|
|
7467
|
+
drawings.push(completed);
|
|
7468
|
+
draftDrawing = null;
|
|
7469
|
+
activeDrawingTool = null;
|
|
7470
|
+
emitDrawingsChange();
|
|
7471
|
+
scheduleDraw();
|
|
7472
|
+
return true;
|
|
7473
|
+
}
|
|
7474
|
+
const defaults = getDrawingToolDefaults("ellipse");
|
|
7475
|
+
draftDrawing = normalizeDrawingState({
|
|
7476
|
+
type: "ellipse",
|
|
7477
|
+
points: [point, point],
|
|
7478
|
+
color: defaults.color ?? "#2962ff",
|
|
7479
|
+
style: defaults.style ?? "solid",
|
|
7480
|
+
width: defaults.width ?? 1
|
|
7481
|
+
});
|
|
7482
|
+
scheduleDraw();
|
|
7483
|
+
return true;
|
|
7484
|
+
}
|
|
7485
|
+
if (activeDrawingTool === "callout") {
|
|
7486
|
+
if (draftDrawing?.type === "callout") {
|
|
7487
|
+
const completed = normalizeDrawingState({
|
|
7488
|
+
...serializeDrawing(draftDrawing),
|
|
7489
|
+
points: [draftDrawing.points[0], point]
|
|
7490
|
+
});
|
|
7491
|
+
drawings.push(completed);
|
|
7492
|
+
draftDrawing = null;
|
|
7493
|
+
activeDrawingTool = null;
|
|
7494
|
+
selectedDrawingId = completed.id;
|
|
7495
|
+
emitDrawingsChange();
|
|
7496
|
+
scheduleDraw();
|
|
7497
|
+
drawingEditTextHandler?.({ drawing: serializeDrawing(completed), target: "line", x, y });
|
|
7498
|
+
return true;
|
|
7499
|
+
}
|
|
7500
|
+
const defaults = getDrawingToolDefaults("callout");
|
|
7501
|
+
draftDrawing = normalizeDrawingState({
|
|
7502
|
+
type: "callout",
|
|
7503
|
+
points: [point, point],
|
|
7504
|
+
color: defaults.color ?? "#2962ff",
|
|
7505
|
+
style: defaults.style ?? "solid",
|
|
7506
|
+
width: defaults.width ?? 1,
|
|
7507
|
+
...defaults.fontSize === void 0 ? {} : { fontSize: defaults.fontSize },
|
|
7508
|
+
label: ""
|
|
7509
|
+
});
|
|
7510
|
+
scheduleDraw();
|
|
7511
|
+
return true;
|
|
7512
|
+
}
|
|
7513
|
+
if (activeDrawingTool === "brush") {
|
|
7514
|
+
const defaults = getDrawingToolDefaults("brush");
|
|
7515
|
+
draftDrawing = normalizeDrawingState({
|
|
7516
|
+
type: "brush",
|
|
7517
|
+
points: [point],
|
|
7518
|
+
color: defaults.color ?? "#f59e0b",
|
|
7519
|
+
style: defaults.style ?? "solid",
|
|
7520
|
+
width: defaults.width ?? 2
|
|
7521
|
+
});
|
|
7522
|
+
scheduleDraw();
|
|
7523
|
+
return true;
|
|
7524
|
+
}
|
|
6953
7525
|
if (activeDrawingTool === "fib-retracement") {
|
|
6954
7526
|
if (draftDrawing?.type === "fib-retracement") {
|
|
6955
7527
|
const completed = normalizeDrawingState({
|
|
@@ -7153,7 +7725,7 @@ function createChart(element, options = {}) {
|
|
|
7153
7725
|
}
|
|
7154
7726
|
return { ...drawing, points: pts };
|
|
7155
7727
|
}
|
|
7156
|
-
if (drawingDragState.target === "handle" && drawing.type === "rectangle") {
|
|
7728
|
+
if (drawingDragState.target === "handle" && (drawing.type === "rectangle" || drawing.type === "ellipse")) {
|
|
7157
7729
|
const pts = drawing.points.map((point) => ({ ...point }));
|
|
7158
7730
|
const p0 = pts[0];
|
|
7159
7731
|
const p1 = pts[1];
|
|
@@ -7172,7 +7744,7 @@ function createChart(element, options = {}) {
|
|
|
7172
7744
|
}
|
|
7173
7745
|
return { ...drawing, points: pts };
|
|
7174
7746
|
}
|
|
7175
|
-
if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray")) {
|
|
7747
|
+
if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray" || drawing.type === "arrow")) {
|
|
7176
7748
|
const pointIndex = drawingDragState.pointIndex ?? 0;
|
|
7177
7749
|
const anchor = drawing.points[pointIndex === 0 ? 1 : 0];
|
|
7178
7750
|
const constrained = anchor ? constrainAngleFromAnchor(anchor, x, y) : null;
|
|
@@ -7290,6 +7862,19 @@ function createChart(element, options = {}) {
|
|
|
7290
7862
|
return;
|
|
7291
7863
|
}
|
|
7292
7864
|
}
|
|
7865
|
+
if (measureState) {
|
|
7866
|
+
if (measureState.dragging) {
|
|
7867
|
+
measureState = { ...measureState, dragging: false };
|
|
7868
|
+
if (activeDrawingTool === "measure") {
|
|
7869
|
+
activeDrawingTool = null;
|
|
7870
|
+
canvas.style.cursor = "default";
|
|
7871
|
+
}
|
|
7872
|
+
scheduleDraw();
|
|
7873
|
+
return;
|
|
7874
|
+
}
|
|
7875
|
+
measureState = null;
|
|
7876
|
+
scheduleDraw();
|
|
7877
|
+
}
|
|
7293
7878
|
const drawingToolCapturesPointer = activeDrawingTool !== null || draftDrawing !== null;
|
|
7294
7879
|
if (!drawingToolCapturesPointer) {
|
|
7295
7880
|
const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
|
|
@@ -7364,6 +7949,18 @@ function createChart(element, options = {}) {
|
|
|
7364
7949
|
if (region === "outside") {
|
|
7365
7950
|
return;
|
|
7366
7951
|
}
|
|
7952
|
+
if (region === "plot" && (activeDrawingTool === "measure" || activeDrawingTool === null && draftDrawing === null && event.shiftKey && event.pointerType === "mouse")) {
|
|
7953
|
+
const measurePoint = drawingPointFromCanvas(point.x, point.y);
|
|
7954
|
+
if (measurePoint) {
|
|
7955
|
+
measureState = { start: measurePoint, end: measurePoint, dragging: true };
|
|
7956
|
+
activePointerId = event.pointerId;
|
|
7957
|
+
capturePointer(event.pointerId);
|
|
7958
|
+
setCrosshairPoint(null);
|
|
7959
|
+
canvas.style.cursor = "crosshair";
|
|
7960
|
+
scheduleDraw();
|
|
7961
|
+
return;
|
|
7962
|
+
}
|
|
7963
|
+
}
|
|
7367
7964
|
if (region === "plot" && !activeDrawingTool) {
|
|
7368
7965
|
const drawingHit = getDrawingHit(point.x, point.y);
|
|
7369
7966
|
if (drawingHit) {
|
|
@@ -7397,6 +7994,10 @@ function createChart(element, options = {}) {
|
|
|
7397
7994
|
if (region === "plot" && handleDrawingToolPointerDown(point.x, point.y)) {
|
|
7398
7995
|
setCrosshairPoint(null);
|
|
7399
7996
|
canvas.style.cursor = "crosshair";
|
|
7997
|
+
if (activeDrawingTool === "brush" && draftDrawing?.type === "brush") {
|
|
7998
|
+
activePointerId = event.pointerId;
|
|
7999
|
+
capturePointer(event.pointerId);
|
|
8000
|
+
}
|
|
7400
8001
|
return;
|
|
7401
8002
|
}
|
|
7402
8003
|
isDragging = true;
|
|
@@ -7459,6 +8060,19 @@ function createChart(element, options = {}) {
|
|
|
7459
8060
|
}
|
|
7460
8061
|
return;
|
|
7461
8062
|
}
|
|
8063
|
+
if (measureState?.dragging) {
|
|
8064
|
+
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
8065
|
+
return;
|
|
8066
|
+
}
|
|
8067
|
+
const nextMeasurePoint = drawingPointFromCanvas(point.x, point.y);
|
|
8068
|
+
if (nextMeasurePoint) {
|
|
8069
|
+
measureState = { ...measureState, end: nextMeasurePoint };
|
|
8070
|
+
setCrosshairPoint(null);
|
|
8071
|
+
canvas.style.cursor = "crosshair";
|
|
8072
|
+
scheduleDraw();
|
|
8073
|
+
}
|
|
8074
|
+
return;
|
|
8075
|
+
}
|
|
7462
8076
|
if (drawingDragState) {
|
|
7463
8077
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
7464
8078
|
return;
|
|
@@ -7468,9 +8082,30 @@ function createChart(element, options = {}) {
|
|
|
7468
8082
|
setCrosshairPoint(null);
|
|
7469
8083
|
return;
|
|
7470
8084
|
}
|
|
7471
|
-
if (draftDrawing
|
|
8085
|
+
if (draftDrawing?.type === "brush" && activeDrawingTool === "brush") {
|
|
8086
|
+
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
8087
|
+
return;
|
|
8088
|
+
}
|
|
8089
|
+
const nextPoint = drawingPointFromCanvas(point.x, point.y);
|
|
8090
|
+
if (nextPoint) {
|
|
8091
|
+
const last = draftDrawing.points[draftDrawing.points.length - 1];
|
|
8092
|
+
const lastX = last ? canvasXFromDrawingPoint(last) : Number.NEGATIVE_INFINITY;
|
|
8093
|
+
const lastY = last ? canvasYFromDrawingPrice(last.price) : Number.NEGATIVE_INFINITY;
|
|
8094
|
+
if (Math.hypot(point.x - lastX, point.y - lastY) >= 3) {
|
|
8095
|
+
draftDrawing = {
|
|
8096
|
+
...draftDrawing,
|
|
8097
|
+
points: [...draftDrawing.points, nextPoint]
|
|
8098
|
+
};
|
|
8099
|
+
}
|
|
8100
|
+
canvas.style.cursor = "crosshair";
|
|
8101
|
+
setCrosshairPoint(null);
|
|
8102
|
+
scheduleDraw();
|
|
8103
|
+
}
|
|
8104
|
+
return;
|
|
8105
|
+
}
|
|
8106
|
+
if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "arrow" || activeDrawingTool === "rectangle" || activeDrawingTool === "ellipse" || activeDrawingTool === "callout" || activeDrawingTool === "parallel-channel" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
|
|
7472
8107
|
let nextPoint = drawingPointFromCanvas(point.x, point.y);
|
|
7473
|
-
if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray") && draftDrawing.points[0]) {
|
|
8108
|
+
if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray" || draftDrawing.type === "arrow") && draftDrawing.points[0]) {
|
|
7474
8109
|
nextPoint = constrainAngleFromAnchor(draftDrawing.points[0], point.x, point.y) ?? nextPoint;
|
|
7475
8110
|
}
|
|
7476
8111
|
if (nextPoint) {
|
|
@@ -7689,6 +8324,43 @@ function createChart(element, options = {}) {
|
|
|
7689
8324
|
if (event && activePointerId !== null && event.pointerId !== activePointerId) {
|
|
7690
8325
|
return;
|
|
7691
8326
|
}
|
|
8327
|
+
if (measureState?.dragging) {
|
|
8328
|
+
const movedMeasure = measureState.start.index !== measureState.end.index || measureState.start.price !== measureState.end.price;
|
|
8329
|
+
if (movedMeasure) {
|
|
8330
|
+
measureState = { ...measureState, dragging: false };
|
|
8331
|
+
if (activeDrawingTool === "measure") {
|
|
8332
|
+
activeDrawingTool = null;
|
|
8333
|
+
}
|
|
8334
|
+
canvas.style.cursor = "default";
|
|
8335
|
+
} else {
|
|
8336
|
+
canvas.style.cursor = "crosshair";
|
|
8337
|
+
}
|
|
8338
|
+
isDragging = false;
|
|
8339
|
+
dragMode = null;
|
|
8340
|
+
activePointerId = null;
|
|
8341
|
+
pointerDownInfo = null;
|
|
8342
|
+
scheduleDraw();
|
|
8343
|
+
return;
|
|
8344
|
+
}
|
|
8345
|
+
if (draftDrawing?.type === "brush" && activeDrawingTool === "brush") {
|
|
8346
|
+
if (draftDrawing.points.length >= 2) {
|
|
8347
|
+
const completed = normalizeDrawingState(serializeDrawing(draftDrawing));
|
|
8348
|
+
drawings.push(completed);
|
|
8349
|
+
draftDrawing = null;
|
|
8350
|
+
activeDrawingTool = null;
|
|
8351
|
+
canvas.style.cursor = "default";
|
|
8352
|
+
emitDrawingsChange();
|
|
8353
|
+
} else {
|
|
8354
|
+
draftDrawing = null;
|
|
8355
|
+
canvas.style.cursor = "crosshair";
|
|
8356
|
+
}
|
|
8357
|
+
isDragging = false;
|
|
8358
|
+
dragMode = null;
|
|
8359
|
+
activePointerId = null;
|
|
8360
|
+
pointerDownInfo = null;
|
|
8361
|
+
scheduleDraw();
|
|
8362
|
+
return;
|
|
8363
|
+
}
|
|
7692
8364
|
if (orderDragState) {
|
|
7693
8365
|
const moved = orderDragState.lastPrice !== orderDragState.startPrice;
|
|
7694
8366
|
const finalLine = orderLines.find((line) => line.id === orderDragState?.orderId);
|
|
@@ -7832,7 +8504,7 @@ function createChart(element, options = {}) {
|
|
|
7832
8504
|
x: point.x,
|
|
7833
8505
|
y: point.y
|
|
7834
8506
|
};
|
|
7835
|
-
if (drawingHit.drawing.type === "text" || drawingHit.drawing.type === "note") {
|
|
8507
|
+
if (drawingHit.drawing.type === "text" || drawingHit.drawing.type === "note" || drawingHit.drawing.type === "callout") {
|
|
7836
8508
|
drawingEditTextHandler?.(payload);
|
|
7837
8509
|
} else {
|
|
7838
8510
|
drawingDoubleClickHandler?.(payload);
|
|
@@ -7869,6 +8541,15 @@ function createChart(element, options = {}) {
|
|
|
7869
8541
|
canvas.addEventListener("dblclick", onDoubleClick);
|
|
7870
8542
|
canvas.addEventListener("contextmenu", onContextMenu);
|
|
7871
8543
|
const onModifierKeyChange = (event) => {
|
|
8544
|
+
if (event.key === "Escape" && event.type === "keydown" && measureState) {
|
|
8545
|
+
measureState = null;
|
|
8546
|
+
if (activeDrawingTool === "measure") {
|
|
8547
|
+
activeDrawingTool = null;
|
|
8548
|
+
canvas.style.cursor = "default";
|
|
8549
|
+
}
|
|
8550
|
+
scheduleDraw();
|
|
8551
|
+
return;
|
|
8552
|
+
}
|
|
7872
8553
|
const active = event.metaKey || event.ctrlKey;
|
|
7873
8554
|
const shift = event.shiftKey;
|
|
7874
8555
|
const changed = active !== magnetModifierActive || shift !== shiftKeyActive;
|
|
@@ -8181,17 +8862,29 @@ function createChart(element, options = {}) {
|
|
|
8181
8862
|
const setActiveDrawingTool = (tool) => {
|
|
8182
8863
|
activeDrawingTool = tool;
|
|
8183
8864
|
draftDrawing = null;
|
|
8865
|
+
measureState = null;
|
|
8184
8866
|
canvas.style.cursor = tool ? "crosshair" : "default";
|
|
8185
8867
|
scheduleDraw();
|
|
8186
8868
|
};
|
|
8187
8869
|
const getActiveDrawingTool = () => activeDrawingTool;
|
|
8188
8870
|
const cancelDrawing = () => {
|
|
8871
|
+
let cancelled = false;
|
|
8189
8872
|
if (draftDrawing) {
|
|
8190
8873
|
draftDrawing = null;
|
|
8874
|
+
cancelled = true;
|
|
8875
|
+
}
|
|
8876
|
+
if (measureState) {
|
|
8877
|
+
measureState = null;
|
|
8878
|
+
if (activeDrawingTool === "measure") {
|
|
8879
|
+
activeDrawingTool = null;
|
|
8880
|
+
canvas.style.cursor = "default";
|
|
8881
|
+
}
|
|
8882
|
+
cancelled = true;
|
|
8883
|
+
}
|
|
8884
|
+
if (cancelled) {
|
|
8191
8885
|
scheduleDraw();
|
|
8192
|
-
return true;
|
|
8193
8886
|
}
|
|
8194
|
-
return
|
|
8887
|
+
return cancelled;
|
|
8195
8888
|
};
|
|
8196
8889
|
const setTradeMarkers = (markers) => {
|
|
8197
8890
|
tradeMarkers = Array.isArray(markers) ? markers.slice() : [];
|
|
@@ -8243,6 +8936,9 @@ function createChart(element, options = {}) {
|
|
|
8243
8936
|
const onDrawingsChange = (handler) => {
|
|
8244
8937
|
drawingsChangeHandler = handler;
|
|
8245
8938
|
};
|
|
8939
|
+
const onActiveDrawingToolChange = (handler) => {
|
|
8940
|
+
drawingToolChangeHandler = handler;
|
|
8941
|
+
};
|
|
8246
8942
|
const onDrawingSelect = (handler) => {
|
|
8247
8943
|
drawingSelectHandler = handler;
|
|
8248
8944
|
};
|
|
@@ -8255,6 +8951,51 @@ function createChart(element, options = {}) {
|
|
|
8255
8951
|
const onDrawingHover = (handler) => {
|
|
8256
8952
|
drawingHoverHandler = handler;
|
|
8257
8953
|
};
|
|
8954
|
+
const saveState = () => {
|
|
8955
|
+
const drawingDefaults = {};
|
|
8956
|
+
for (const [tool, defaults] of drawingToolDefaults) {
|
|
8957
|
+
drawingDefaults[tool] = { ...defaults };
|
|
8958
|
+
}
|
|
8959
|
+
return {
|
|
8960
|
+
version: 1,
|
|
8961
|
+
chartType: getChartType(),
|
|
8962
|
+
viewport: getViewport(),
|
|
8963
|
+
drawings: getDrawings(),
|
|
8964
|
+
indicators: getIndicators(),
|
|
8965
|
+
magnetMode: getMagnetMode(),
|
|
8966
|
+
drawingDefaults
|
|
8967
|
+
};
|
|
8968
|
+
};
|
|
8969
|
+
const loadState = (state) => {
|
|
8970
|
+
if (!state || typeof state !== "object") {
|
|
8971
|
+
return;
|
|
8972
|
+
}
|
|
8973
|
+
if (typeof state.chartType === "string") {
|
|
8974
|
+
setChartType(state.chartType);
|
|
8975
|
+
}
|
|
8976
|
+
if (Array.isArray(state.indicators)) {
|
|
8977
|
+
setIndicators(state.indicators);
|
|
8978
|
+
}
|
|
8979
|
+
if (Array.isArray(state.drawings)) {
|
|
8980
|
+
setDrawings(state.drawings);
|
|
8981
|
+
}
|
|
8982
|
+
if (state.magnetMode === "none" || state.magnetMode === "weak" || state.magnetMode === "strong") {
|
|
8983
|
+
setMagnetMode(state.magnetMode);
|
|
8984
|
+
}
|
|
8985
|
+
if (state.drawingDefaults && typeof state.drawingDefaults === "object") {
|
|
8986
|
+
for (const [tool, defaults] of Object.entries(state.drawingDefaults)) {
|
|
8987
|
+
if (defaults && typeof defaults === "object") {
|
|
8988
|
+
setDrawingDefaults(tool, defaults);
|
|
8989
|
+
}
|
|
8990
|
+
}
|
|
8991
|
+
}
|
|
8992
|
+
if (state.viewport && typeof state.viewport === "object") {
|
|
8993
|
+
setViewport(state.viewport);
|
|
8994
|
+
}
|
|
8995
|
+
};
|
|
8996
|
+
const takeScreenshot = (options2 = {}) => {
|
|
8997
|
+
return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
|
|
8998
|
+
};
|
|
8258
8999
|
const destroy = () => {
|
|
8259
9000
|
cancelKineticPan();
|
|
8260
9001
|
if (smoothingRafId !== null) {
|
|
@@ -8329,6 +9070,7 @@ function createChart(element, options = {}) {
|
|
|
8329
9070
|
removeDrawing,
|
|
8330
9071
|
clearDrawings,
|
|
8331
9072
|
onDrawingsChange,
|
|
9073
|
+
onActiveDrawingToolChange,
|
|
8332
9074
|
onDrawingSelect,
|
|
8333
9075
|
onDrawingDoubleClick,
|
|
8334
9076
|
onDrawingEditText,
|
|
@@ -8349,6 +9091,9 @@ function createChart(element, options = {}) {
|
|
|
8349
9091
|
updateIndicator,
|
|
8350
9092
|
removeIndicator,
|
|
8351
9093
|
setIndicators,
|
|
9094
|
+
saveState,
|
|
9095
|
+
loadState,
|
|
9096
|
+
takeScreenshot,
|
|
8352
9097
|
resize,
|
|
8353
9098
|
destroy
|
|
8354
9099
|
};
|