hyperprop-charting-library 0.1.104 → 0.1.106
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 +70 -8
- package/dist/hyperprop-charting-library.d.ts +4 -0
- package/dist/hyperprop-charting-library.js +70 -8
- package/dist/index.cjs +70 -8
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +70 -8
- package/docs/API.md +1 -0
- package/package.json +1 -1
|
@@ -75,7 +75,11 @@ var DEFAULT_CROSSHAIR_OPTIONS = {
|
|
|
75
75
|
priceActionButtonSize: 16,
|
|
76
76
|
priceActionButtonGap: 4,
|
|
77
77
|
priceActionButtonRounded: true,
|
|
78
|
-
priceActionButtonBorderRadius: 8
|
|
78
|
+
priceActionButtonBorderRadius: 8,
|
|
79
|
+
sideHintLeft: "",
|
|
80
|
+
sideHintRight: "",
|
|
81
|
+
sideHintLeftColor: "#26a69a",
|
|
82
|
+
sideHintRightColor: "#ef5350"
|
|
79
83
|
};
|
|
80
84
|
var DEFAULT_WATERMARK_OPTIONS = {
|
|
81
85
|
visible: false,
|
|
@@ -1003,6 +1007,7 @@ function createChart(element, options = {}) {
|
|
|
1003
1007
|
let drawingEditTextHandler = null;
|
|
1004
1008
|
let magnetMode = "none";
|
|
1005
1009
|
let magnetModifierActive = false;
|
|
1010
|
+
let shiftKeyActive = false;
|
|
1006
1011
|
const MAGNET_WEAK_THRESHOLD_PX = 16;
|
|
1007
1012
|
let drawingHoverHandler = null;
|
|
1008
1013
|
let lastHoveredDrawingId = null;
|
|
@@ -3017,6 +3022,25 @@ function createChart(element, options = {}) {
|
|
|
3017
3022
|
}
|
|
3018
3023
|
ctx.restore();
|
|
3019
3024
|
}
|
|
3025
|
+
if (crosshair.sideHintLeft || crosshair.sideHintRight) {
|
|
3026
|
+
ctx.save();
|
|
3027
|
+
ctx.font = `600 11px ${mergedOptions.fontFamily}`;
|
|
3028
|
+
ctx.textBaseline = "middle";
|
|
3029
|
+
ctx.setLineDash([]);
|
|
3030
|
+
const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
|
|
3031
|
+
const hintGap = 8;
|
|
3032
|
+
if (crosshair.sideHintLeft) {
|
|
3033
|
+
ctx.fillStyle = crosshair.sideHintLeftColor;
|
|
3034
|
+
ctx.textAlign = "right";
|
|
3035
|
+
ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
|
|
3036
|
+
}
|
|
3037
|
+
if (crosshair.sideHintRight) {
|
|
3038
|
+
ctx.fillStyle = crosshair.sideHintRightColor;
|
|
3039
|
+
ctx.textAlign = "left";
|
|
3040
|
+
ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
|
|
3041
|
+
}
|
|
3042
|
+
ctx.restore();
|
|
3043
|
+
}
|
|
3020
3044
|
}
|
|
3021
3045
|
ctx.restore();
|
|
3022
3046
|
const positionAxisGutter = Math.max(0, width - chartRight);
|
|
@@ -3895,6 +3919,23 @@ function createChart(element, options = {}) {
|
|
|
3895
3919
|
...time ? { time: time.toISOString() } : {}
|
|
3896
3920
|
};
|
|
3897
3921
|
};
|
|
3922
|
+
const constrainAngleFromAnchor = (anchor, x, y) => {
|
|
3923
|
+
const ax = canvasXFromDrawingPoint(anchor);
|
|
3924
|
+
const ay = canvasYFromDrawingPrice(anchor.price);
|
|
3925
|
+
const dx = x - ax;
|
|
3926
|
+
const dy = y - ay;
|
|
3927
|
+
if (dx === 0 && dy === 0) return drawingPointFromCanvas(x, y);
|
|
3928
|
+
const step = Math.PI / 4;
|
|
3929
|
+
const snappedAngle = Math.round(Math.atan2(dy, dx) / step) * step;
|
|
3930
|
+
const length = Math.hypot(dx, dy);
|
|
3931
|
+
const sx = ax + Math.cos(snappedAngle) * length;
|
|
3932
|
+
const sy = ay + Math.sin(snappedAngle) * length;
|
|
3933
|
+
const resolved = drawingPointFromCanvas(sx, sy);
|
|
3934
|
+
if (!resolved) return resolved;
|
|
3935
|
+
if (Math.abs(Math.sin(snappedAngle)) < 1e-6) return { ...resolved, price: anchor.price };
|
|
3936
|
+
if (Math.abs(Math.cos(snappedAngle)) < 1e-6) return { ...resolved, index: anchor.index };
|
|
3937
|
+
return resolved;
|
|
3938
|
+
};
|
|
3898
3939
|
const normalizeDrawingPoint = (index, price) => {
|
|
3899
3940
|
const roundedIndex = Math.round(index);
|
|
3900
3941
|
const time = getTimeForIndex(roundedIndex);
|
|
@@ -4198,9 +4239,11 @@ function createChart(element, options = {}) {
|
|
|
4198
4239
|
}
|
|
4199
4240
|
if (activeDrawingTool === "trendline") {
|
|
4200
4241
|
if (draftDrawing?.type === "trendline") {
|
|
4242
|
+
const anchor = draftDrawing.points[0];
|
|
4243
|
+
const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
|
|
4201
4244
|
const completed = normalizeDrawingState({
|
|
4202
4245
|
...serializeDrawing(draftDrawing),
|
|
4203
|
-
points: [
|
|
4246
|
+
points: [anchor, endPoint]
|
|
4204
4247
|
});
|
|
4205
4248
|
drawings.push(completed);
|
|
4206
4249
|
draftDrawing = null;
|
|
@@ -4222,9 +4265,11 @@ function createChart(element, options = {}) {
|
|
|
4222
4265
|
}
|
|
4223
4266
|
if (activeDrawingTool === "ray") {
|
|
4224
4267
|
if (draftDrawing?.type === "ray") {
|
|
4268
|
+
const anchor = draftDrawing.points[0];
|
|
4269
|
+
const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
|
|
4225
4270
|
const completed = normalizeDrawingState({
|
|
4226
4271
|
...serializeDrawing(draftDrawing),
|
|
4227
|
-
points: [
|
|
4272
|
+
points: [anchor, endPoint]
|
|
4228
4273
|
});
|
|
4229
4274
|
drawings.push(completed);
|
|
4230
4275
|
draftDrawing = null;
|
|
@@ -4421,6 +4466,17 @@ function createChart(element, options = {}) {
|
|
|
4421
4466
|
}
|
|
4422
4467
|
return { ...drawing, points: pts };
|
|
4423
4468
|
}
|
|
4469
|
+
if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray")) {
|
|
4470
|
+
const pointIndex = drawingDragState.pointIndex ?? 0;
|
|
4471
|
+
const anchor = drawing.points[pointIndex === 0 ? 1 : 0];
|
|
4472
|
+
const constrained = anchor ? constrainAngleFromAnchor(anchor, x, y) : null;
|
|
4473
|
+
if (constrained) {
|
|
4474
|
+
return {
|
|
4475
|
+
...drawing,
|
|
4476
|
+
points: drawing.points.map((point, index) => index === pointIndex ? constrained : point)
|
|
4477
|
+
};
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4424
4480
|
if (drawingDragState.target === "handle") {
|
|
4425
4481
|
return {
|
|
4426
4482
|
...drawing,
|
|
@@ -4509,6 +4565,7 @@ function createChart(element, options = {}) {
|
|
|
4509
4565
|
event.preventDefault();
|
|
4510
4566
|
}
|
|
4511
4567
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
4568
|
+
shiftKeyActive = event.shiftKey;
|
|
4512
4569
|
const point = getCanvasPoint(event);
|
|
4513
4570
|
if (event.pointerType === "touch") {
|
|
4514
4571
|
touchPointers.set(event.pointerId, point);
|
|
@@ -4632,6 +4689,7 @@ function createChart(element, options = {}) {
|
|
|
4632
4689
|
event.preventDefault();
|
|
4633
4690
|
}
|
|
4634
4691
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
4692
|
+
shiftKeyActive = event.shiftKey;
|
|
4635
4693
|
const point = getCanvasPoint(event);
|
|
4636
4694
|
if (event.pointerType === "touch" && touchPointers.has(event.pointerId)) {
|
|
4637
4695
|
touchPointers.set(event.pointerId, point);
|
|
@@ -4674,7 +4732,10 @@ function createChart(element, options = {}) {
|
|
|
4674
4732
|
return;
|
|
4675
4733
|
}
|
|
4676
4734
|
if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
|
|
4677
|
-
|
|
4735
|
+
let nextPoint = drawingPointFromCanvas(point.x, point.y);
|
|
4736
|
+
if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray") && draftDrawing.points[0]) {
|
|
4737
|
+
nextPoint = constrainAngleFromAnchor(draftDrawing.points[0], point.x, point.y) ?? nextPoint;
|
|
4738
|
+
}
|
|
4678
4739
|
if (nextPoint) {
|
|
4679
4740
|
draftDrawing = {
|
|
4680
4741
|
...draftDrawing,
|
|
@@ -4989,10 +5050,11 @@ function createChart(element, options = {}) {
|
|
|
4989
5050
|
canvas.addEventListener("contextmenu", onContextMenu);
|
|
4990
5051
|
const onModifierKeyChange = (event) => {
|
|
4991
5052
|
const active = event.metaKey || event.ctrlKey;
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
5053
|
+
const shift = event.shiftKey;
|
|
5054
|
+
const changed = active !== magnetModifierActive || shift !== shiftKeyActive;
|
|
5055
|
+
magnetModifierActive = active;
|
|
5056
|
+
shiftKeyActive = shift;
|
|
5057
|
+
if (changed && draftDrawing) draw();
|
|
4996
5058
|
};
|
|
4997
5059
|
const onWindowBlurMagnet = () => {
|
|
4998
5060
|
magnetModifierActive = false;
|
|
@@ -241,6 +241,10 @@ interface CrosshairOptions {
|
|
|
241
241
|
priceActionButtonGap?: number;
|
|
242
242
|
priceActionButtonRounded?: boolean;
|
|
243
243
|
priceActionButtonBorderRadius?: number;
|
|
244
|
+
sideHintLeft?: string;
|
|
245
|
+
sideHintRight?: string;
|
|
246
|
+
sideHintLeftColor?: string;
|
|
247
|
+
sideHintRightColor?: string;
|
|
244
248
|
}
|
|
245
249
|
interface WatermarkOptions {
|
|
246
250
|
visible?: boolean;
|
|
@@ -49,7 +49,11 @@ var DEFAULT_CROSSHAIR_OPTIONS = {
|
|
|
49
49
|
priceActionButtonSize: 16,
|
|
50
50
|
priceActionButtonGap: 4,
|
|
51
51
|
priceActionButtonRounded: true,
|
|
52
|
-
priceActionButtonBorderRadius: 8
|
|
52
|
+
priceActionButtonBorderRadius: 8,
|
|
53
|
+
sideHintLeft: "",
|
|
54
|
+
sideHintRight: "",
|
|
55
|
+
sideHintLeftColor: "#26a69a",
|
|
56
|
+
sideHintRightColor: "#ef5350"
|
|
53
57
|
};
|
|
54
58
|
var DEFAULT_WATERMARK_OPTIONS = {
|
|
55
59
|
visible: false,
|
|
@@ -977,6 +981,7 @@ function createChart(element, options = {}) {
|
|
|
977
981
|
let drawingEditTextHandler = null;
|
|
978
982
|
let magnetMode = "none";
|
|
979
983
|
let magnetModifierActive = false;
|
|
984
|
+
let shiftKeyActive = false;
|
|
980
985
|
const MAGNET_WEAK_THRESHOLD_PX = 16;
|
|
981
986
|
let drawingHoverHandler = null;
|
|
982
987
|
let lastHoveredDrawingId = null;
|
|
@@ -2991,6 +2996,25 @@ function createChart(element, options = {}) {
|
|
|
2991
2996
|
}
|
|
2992
2997
|
ctx.restore();
|
|
2993
2998
|
}
|
|
2999
|
+
if (crosshair.sideHintLeft || crosshair.sideHintRight) {
|
|
3000
|
+
ctx.save();
|
|
3001
|
+
ctx.font = `600 11px ${mergedOptions.fontFamily}`;
|
|
3002
|
+
ctx.textBaseline = "middle";
|
|
3003
|
+
ctx.setLineDash([]);
|
|
3004
|
+
const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
|
|
3005
|
+
const hintGap = 8;
|
|
3006
|
+
if (crosshair.sideHintLeft) {
|
|
3007
|
+
ctx.fillStyle = crosshair.sideHintLeftColor;
|
|
3008
|
+
ctx.textAlign = "right";
|
|
3009
|
+
ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
|
|
3010
|
+
}
|
|
3011
|
+
if (crosshair.sideHintRight) {
|
|
3012
|
+
ctx.fillStyle = crosshair.sideHintRightColor;
|
|
3013
|
+
ctx.textAlign = "left";
|
|
3014
|
+
ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
|
|
3015
|
+
}
|
|
3016
|
+
ctx.restore();
|
|
3017
|
+
}
|
|
2994
3018
|
}
|
|
2995
3019
|
ctx.restore();
|
|
2996
3020
|
const positionAxisGutter = Math.max(0, width - chartRight);
|
|
@@ -3869,6 +3893,23 @@ function createChart(element, options = {}) {
|
|
|
3869
3893
|
...time ? { time: time.toISOString() } : {}
|
|
3870
3894
|
};
|
|
3871
3895
|
};
|
|
3896
|
+
const constrainAngleFromAnchor = (anchor, x, y) => {
|
|
3897
|
+
const ax = canvasXFromDrawingPoint(anchor);
|
|
3898
|
+
const ay = canvasYFromDrawingPrice(anchor.price);
|
|
3899
|
+
const dx = x - ax;
|
|
3900
|
+
const dy = y - ay;
|
|
3901
|
+
if (dx === 0 && dy === 0) return drawingPointFromCanvas(x, y);
|
|
3902
|
+
const step = Math.PI / 4;
|
|
3903
|
+
const snappedAngle = Math.round(Math.atan2(dy, dx) / step) * step;
|
|
3904
|
+
const length = Math.hypot(dx, dy);
|
|
3905
|
+
const sx = ax + Math.cos(snappedAngle) * length;
|
|
3906
|
+
const sy = ay + Math.sin(snappedAngle) * length;
|
|
3907
|
+
const resolved = drawingPointFromCanvas(sx, sy);
|
|
3908
|
+
if (!resolved) return resolved;
|
|
3909
|
+
if (Math.abs(Math.sin(snappedAngle)) < 1e-6) return { ...resolved, price: anchor.price };
|
|
3910
|
+
if (Math.abs(Math.cos(snappedAngle)) < 1e-6) return { ...resolved, index: anchor.index };
|
|
3911
|
+
return resolved;
|
|
3912
|
+
};
|
|
3872
3913
|
const normalizeDrawingPoint = (index, price) => {
|
|
3873
3914
|
const roundedIndex = Math.round(index);
|
|
3874
3915
|
const time = getTimeForIndex(roundedIndex);
|
|
@@ -4172,9 +4213,11 @@ function createChart(element, options = {}) {
|
|
|
4172
4213
|
}
|
|
4173
4214
|
if (activeDrawingTool === "trendline") {
|
|
4174
4215
|
if (draftDrawing?.type === "trendline") {
|
|
4216
|
+
const anchor = draftDrawing.points[0];
|
|
4217
|
+
const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
|
|
4175
4218
|
const completed = normalizeDrawingState({
|
|
4176
4219
|
...serializeDrawing(draftDrawing),
|
|
4177
|
-
points: [
|
|
4220
|
+
points: [anchor, endPoint]
|
|
4178
4221
|
});
|
|
4179
4222
|
drawings.push(completed);
|
|
4180
4223
|
draftDrawing = null;
|
|
@@ -4196,9 +4239,11 @@ function createChart(element, options = {}) {
|
|
|
4196
4239
|
}
|
|
4197
4240
|
if (activeDrawingTool === "ray") {
|
|
4198
4241
|
if (draftDrawing?.type === "ray") {
|
|
4242
|
+
const anchor = draftDrawing.points[0];
|
|
4243
|
+
const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
|
|
4199
4244
|
const completed = normalizeDrawingState({
|
|
4200
4245
|
...serializeDrawing(draftDrawing),
|
|
4201
|
-
points: [
|
|
4246
|
+
points: [anchor, endPoint]
|
|
4202
4247
|
});
|
|
4203
4248
|
drawings.push(completed);
|
|
4204
4249
|
draftDrawing = null;
|
|
@@ -4395,6 +4440,17 @@ function createChart(element, options = {}) {
|
|
|
4395
4440
|
}
|
|
4396
4441
|
return { ...drawing, points: pts };
|
|
4397
4442
|
}
|
|
4443
|
+
if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray")) {
|
|
4444
|
+
const pointIndex = drawingDragState.pointIndex ?? 0;
|
|
4445
|
+
const anchor = drawing.points[pointIndex === 0 ? 1 : 0];
|
|
4446
|
+
const constrained = anchor ? constrainAngleFromAnchor(anchor, x, y) : null;
|
|
4447
|
+
if (constrained) {
|
|
4448
|
+
return {
|
|
4449
|
+
...drawing,
|
|
4450
|
+
points: drawing.points.map((point, index) => index === pointIndex ? constrained : point)
|
|
4451
|
+
};
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4398
4454
|
if (drawingDragState.target === "handle") {
|
|
4399
4455
|
return {
|
|
4400
4456
|
...drawing,
|
|
@@ -4483,6 +4539,7 @@ function createChart(element, options = {}) {
|
|
|
4483
4539
|
event.preventDefault();
|
|
4484
4540
|
}
|
|
4485
4541
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
4542
|
+
shiftKeyActive = event.shiftKey;
|
|
4486
4543
|
const point = getCanvasPoint(event);
|
|
4487
4544
|
if (event.pointerType === "touch") {
|
|
4488
4545
|
touchPointers.set(event.pointerId, point);
|
|
@@ -4606,6 +4663,7 @@ function createChart(element, options = {}) {
|
|
|
4606
4663
|
event.preventDefault();
|
|
4607
4664
|
}
|
|
4608
4665
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
4666
|
+
shiftKeyActive = event.shiftKey;
|
|
4609
4667
|
const point = getCanvasPoint(event);
|
|
4610
4668
|
if (event.pointerType === "touch" && touchPointers.has(event.pointerId)) {
|
|
4611
4669
|
touchPointers.set(event.pointerId, point);
|
|
@@ -4648,7 +4706,10 @@ function createChart(element, options = {}) {
|
|
|
4648
4706
|
return;
|
|
4649
4707
|
}
|
|
4650
4708
|
if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
|
|
4651
|
-
|
|
4709
|
+
let nextPoint = drawingPointFromCanvas(point.x, point.y);
|
|
4710
|
+
if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray") && draftDrawing.points[0]) {
|
|
4711
|
+
nextPoint = constrainAngleFromAnchor(draftDrawing.points[0], point.x, point.y) ?? nextPoint;
|
|
4712
|
+
}
|
|
4652
4713
|
if (nextPoint) {
|
|
4653
4714
|
draftDrawing = {
|
|
4654
4715
|
...draftDrawing,
|
|
@@ -4963,10 +5024,11 @@ function createChart(element, options = {}) {
|
|
|
4963
5024
|
canvas.addEventListener("contextmenu", onContextMenu);
|
|
4964
5025
|
const onModifierKeyChange = (event) => {
|
|
4965
5026
|
const active = event.metaKey || event.ctrlKey;
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
5027
|
+
const shift = event.shiftKey;
|
|
5028
|
+
const changed = active !== magnetModifierActive || shift !== shiftKeyActive;
|
|
5029
|
+
magnetModifierActive = active;
|
|
5030
|
+
shiftKeyActive = shift;
|
|
5031
|
+
if (changed && draftDrawing) draw();
|
|
4970
5032
|
};
|
|
4971
5033
|
const onWindowBlurMagnet = () => {
|
|
4972
5034
|
magnetModifierActive = false;
|
package/dist/index.cjs
CHANGED
|
@@ -75,7 +75,11 @@ var DEFAULT_CROSSHAIR_OPTIONS = {
|
|
|
75
75
|
priceActionButtonSize: 16,
|
|
76
76
|
priceActionButtonGap: 4,
|
|
77
77
|
priceActionButtonRounded: true,
|
|
78
|
-
priceActionButtonBorderRadius: 8
|
|
78
|
+
priceActionButtonBorderRadius: 8,
|
|
79
|
+
sideHintLeft: "",
|
|
80
|
+
sideHintRight: "",
|
|
81
|
+
sideHintLeftColor: "#26a69a",
|
|
82
|
+
sideHintRightColor: "#ef5350"
|
|
79
83
|
};
|
|
80
84
|
var DEFAULT_WATERMARK_OPTIONS = {
|
|
81
85
|
visible: false,
|
|
@@ -1003,6 +1007,7 @@ function createChart(element, options = {}) {
|
|
|
1003
1007
|
let drawingEditTextHandler = null;
|
|
1004
1008
|
let magnetMode = "none";
|
|
1005
1009
|
let magnetModifierActive = false;
|
|
1010
|
+
let shiftKeyActive = false;
|
|
1006
1011
|
const MAGNET_WEAK_THRESHOLD_PX = 16;
|
|
1007
1012
|
let drawingHoverHandler = null;
|
|
1008
1013
|
let lastHoveredDrawingId = null;
|
|
@@ -3017,6 +3022,25 @@ function createChart(element, options = {}) {
|
|
|
3017
3022
|
}
|
|
3018
3023
|
ctx.restore();
|
|
3019
3024
|
}
|
|
3025
|
+
if (crosshair.sideHintLeft || crosshair.sideHintRight) {
|
|
3026
|
+
ctx.save();
|
|
3027
|
+
ctx.font = `600 11px ${mergedOptions.fontFamily}`;
|
|
3028
|
+
ctx.textBaseline = "middle";
|
|
3029
|
+
ctx.setLineDash([]);
|
|
3030
|
+
const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
|
|
3031
|
+
const hintGap = 8;
|
|
3032
|
+
if (crosshair.sideHintLeft) {
|
|
3033
|
+
ctx.fillStyle = crosshair.sideHintLeftColor;
|
|
3034
|
+
ctx.textAlign = "right";
|
|
3035
|
+
ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
|
|
3036
|
+
}
|
|
3037
|
+
if (crosshair.sideHintRight) {
|
|
3038
|
+
ctx.fillStyle = crosshair.sideHintRightColor;
|
|
3039
|
+
ctx.textAlign = "left";
|
|
3040
|
+
ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
|
|
3041
|
+
}
|
|
3042
|
+
ctx.restore();
|
|
3043
|
+
}
|
|
3020
3044
|
}
|
|
3021
3045
|
ctx.restore();
|
|
3022
3046
|
const positionAxisGutter = Math.max(0, width - chartRight);
|
|
@@ -3895,6 +3919,23 @@ function createChart(element, options = {}) {
|
|
|
3895
3919
|
...time ? { time: time.toISOString() } : {}
|
|
3896
3920
|
};
|
|
3897
3921
|
};
|
|
3922
|
+
const constrainAngleFromAnchor = (anchor, x, y) => {
|
|
3923
|
+
const ax = canvasXFromDrawingPoint(anchor);
|
|
3924
|
+
const ay = canvasYFromDrawingPrice(anchor.price);
|
|
3925
|
+
const dx = x - ax;
|
|
3926
|
+
const dy = y - ay;
|
|
3927
|
+
if (dx === 0 && dy === 0) return drawingPointFromCanvas(x, y);
|
|
3928
|
+
const step = Math.PI / 4;
|
|
3929
|
+
const snappedAngle = Math.round(Math.atan2(dy, dx) / step) * step;
|
|
3930
|
+
const length = Math.hypot(dx, dy);
|
|
3931
|
+
const sx = ax + Math.cos(snappedAngle) * length;
|
|
3932
|
+
const sy = ay + Math.sin(snappedAngle) * length;
|
|
3933
|
+
const resolved = drawingPointFromCanvas(sx, sy);
|
|
3934
|
+
if (!resolved) return resolved;
|
|
3935
|
+
if (Math.abs(Math.sin(snappedAngle)) < 1e-6) return { ...resolved, price: anchor.price };
|
|
3936
|
+
if (Math.abs(Math.cos(snappedAngle)) < 1e-6) return { ...resolved, index: anchor.index };
|
|
3937
|
+
return resolved;
|
|
3938
|
+
};
|
|
3898
3939
|
const normalizeDrawingPoint = (index, price) => {
|
|
3899
3940
|
const roundedIndex = Math.round(index);
|
|
3900
3941
|
const time = getTimeForIndex(roundedIndex);
|
|
@@ -4198,9 +4239,11 @@ function createChart(element, options = {}) {
|
|
|
4198
4239
|
}
|
|
4199
4240
|
if (activeDrawingTool === "trendline") {
|
|
4200
4241
|
if (draftDrawing?.type === "trendline") {
|
|
4242
|
+
const anchor = draftDrawing.points[0];
|
|
4243
|
+
const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
|
|
4201
4244
|
const completed = normalizeDrawingState({
|
|
4202
4245
|
...serializeDrawing(draftDrawing),
|
|
4203
|
-
points: [
|
|
4246
|
+
points: [anchor, endPoint]
|
|
4204
4247
|
});
|
|
4205
4248
|
drawings.push(completed);
|
|
4206
4249
|
draftDrawing = null;
|
|
@@ -4222,9 +4265,11 @@ function createChart(element, options = {}) {
|
|
|
4222
4265
|
}
|
|
4223
4266
|
if (activeDrawingTool === "ray") {
|
|
4224
4267
|
if (draftDrawing?.type === "ray") {
|
|
4268
|
+
const anchor = draftDrawing.points[0];
|
|
4269
|
+
const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
|
|
4225
4270
|
const completed = normalizeDrawingState({
|
|
4226
4271
|
...serializeDrawing(draftDrawing),
|
|
4227
|
-
points: [
|
|
4272
|
+
points: [anchor, endPoint]
|
|
4228
4273
|
});
|
|
4229
4274
|
drawings.push(completed);
|
|
4230
4275
|
draftDrawing = null;
|
|
@@ -4421,6 +4466,17 @@ function createChart(element, options = {}) {
|
|
|
4421
4466
|
}
|
|
4422
4467
|
return { ...drawing, points: pts };
|
|
4423
4468
|
}
|
|
4469
|
+
if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray")) {
|
|
4470
|
+
const pointIndex = drawingDragState.pointIndex ?? 0;
|
|
4471
|
+
const anchor = drawing.points[pointIndex === 0 ? 1 : 0];
|
|
4472
|
+
const constrained = anchor ? constrainAngleFromAnchor(anchor, x, y) : null;
|
|
4473
|
+
if (constrained) {
|
|
4474
|
+
return {
|
|
4475
|
+
...drawing,
|
|
4476
|
+
points: drawing.points.map((point, index) => index === pointIndex ? constrained : point)
|
|
4477
|
+
};
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4424
4480
|
if (drawingDragState.target === "handle") {
|
|
4425
4481
|
return {
|
|
4426
4482
|
...drawing,
|
|
@@ -4509,6 +4565,7 @@ function createChart(element, options = {}) {
|
|
|
4509
4565
|
event.preventDefault();
|
|
4510
4566
|
}
|
|
4511
4567
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
4568
|
+
shiftKeyActive = event.shiftKey;
|
|
4512
4569
|
const point = getCanvasPoint(event);
|
|
4513
4570
|
if (event.pointerType === "touch") {
|
|
4514
4571
|
touchPointers.set(event.pointerId, point);
|
|
@@ -4632,6 +4689,7 @@ function createChart(element, options = {}) {
|
|
|
4632
4689
|
event.preventDefault();
|
|
4633
4690
|
}
|
|
4634
4691
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
4692
|
+
shiftKeyActive = event.shiftKey;
|
|
4635
4693
|
const point = getCanvasPoint(event);
|
|
4636
4694
|
if (event.pointerType === "touch" && touchPointers.has(event.pointerId)) {
|
|
4637
4695
|
touchPointers.set(event.pointerId, point);
|
|
@@ -4674,7 +4732,10 @@ function createChart(element, options = {}) {
|
|
|
4674
4732
|
return;
|
|
4675
4733
|
}
|
|
4676
4734
|
if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
|
|
4677
|
-
|
|
4735
|
+
let nextPoint = drawingPointFromCanvas(point.x, point.y);
|
|
4736
|
+
if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray") && draftDrawing.points[0]) {
|
|
4737
|
+
nextPoint = constrainAngleFromAnchor(draftDrawing.points[0], point.x, point.y) ?? nextPoint;
|
|
4738
|
+
}
|
|
4678
4739
|
if (nextPoint) {
|
|
4679
4740
|
draftDrawing = {
|
|
4680
4741
|
...draftDrawing,
|
|
@@ -4989,10 +5050,11 @@ function createChart(element, options = {}) {
|
|
|
4989
5050
|
canvas.addEventListener("contextmenu", onContextMenu);
|
|
4990
5051
|
const onModifierKeyChange = (event) => {
|
|
4991
5052
|
const active = event.metaKey || event.ctrlKey;
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
5053
|
+
const shift = event.shiftKey;
|
|
5054
|
+
const changed = active !== magnetModifierActive || shift !== shiftKeyActive;
|
|
5055
|
+
magnetModifierActive = active;
|
|
5056
|
+
shiftKeyActive = shift;
|
|
5057
|
+
if (changed && draftDrawing) draw();
|
|
4996
5058
|
};
|
|
4997
5059
|
const onWindowBlurMagnet = () => {
|
|
4998
5060
|
magnetModifierActive = false;
|
package/dist/index.d.cts
CHANGED
|
@@ -241,6 +241,10 @@ interface CrosshairOptions {
|
|
|
241
241
|
priceActionButtonGap?: number;
|
|
242
242
|
priceActionButtonRounded?: boolean;
|
|
243
243
|
priceActionButtonBorderRadius?: number;
|
|
244
|
+
sideHintLeft?: string;
|
|
245
|
+
sideHintRight?: string;
|
|
246
|
+
sideHintLeftColor?: string;
|
|
247
|
+
sideHintRightColor?: string;
|
|
244
248
|
}
|
|
245
249
|
interface WatermarkOptions {
|
|
246
250
|
visible?: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -241,6 +241,10 @@ interface CrosshairOptions {
|
|
|
241
241
|
priceActionButtonGap?: number;
|
|
242
242
|
priceActionButtonRounded?: boolean;
|
|
243
243
|
priceActionButtonBorderRadius?: number;
|
|
244
|
+
sideHintLeft?: string;
|
|
245
|
+
sideHintRight?: string;
|
|
246
|
+
sideHintLeftColor?: string;
|
|
247
|
+
sideHintRightColor?: string;
|
|
244
248
|
}
|
|
245
249
|
interface WatermarkOptions {
|
|
246
250
|
visible?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -49,7 +49,11 @@ var DEFAULT_CROSSHAIR_OPTIONS = {
|
|
|
49
49
|
priceActionButtonSize: 16,
|
|
50
50
|
priceActionButtonGap: 4,
|
|
51
51
|
priceActionButtonRounded: true,
|
|
52
|
-
priceActionButtonBorderRadius: 8
|
|
52
|
+
priceActionButtonBorderRadius: 8,
|
|
53
|
+
sideHintLeft: "",
|
|
54
|
+
sideHintRight: "",
|
|
55
|
+
sideHintLeftColor: "#26a69a",
|
|
56
|
+
sideHintRightColor: "#ef5350"
|
|
53
57
|
};
|
|
54
58
|
var DEFAULT_WATERMARK_OPTIONS = {
|
|
55
59
|
visible: false,
|
|
@@ -977,6 +981,7 @@ function createChart(element, options = {}) {
|
|
|
977
981
|
let drawingEditTextHandler = null;
|
|
978
982
|
let magnetMode = "none";
|
|
979
983
|
let magnetModifierActive = false;
|
|
984
|
+
let shiftKeyActive = false;
|
|
980
985
|
const MAGNET_WEAK_THRESHOLD_PX = 16;
|
|
981
986
|
let drawingHoverHandler = null;
|
|
982
987
|
let lastHoveredDrawingId = null;
|
|
@@ -2991,6 +2996,25 @@ function createChart(element, options = {}) {
|
|
|
2991
2996
|
}
|
|
2992
2997
|
ctx.restore();
|
|
2993
2998
|
}
|
|
2999
|
+
if (crosshair.sideHintLeft || crosshair.sideHintRight) {
|
|
3000
|
+
ctx.save();
|
|
3001
|
+
ctx.font = `600 11px ${mergedOptions.fontFamily}`;
|
|
3002
|
+
ctx.textBaseline = "middle";
|
|
3003
|
+
ctx.setLineDash([]);
|
|
3004
|
+
const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
|
|
3005
|
+
const hintGap = 8;
|
|
3006
|
+
if (crosshair.sideHintLeft) {
|
|
3007
|
+
ctx.fillStyle = crosshair.sideHintLeftColor;
|
|
3008
|
+
ctx.textAlign = "right";
|
|
3009
|
+
ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
|
|
3010
|
+
}
|
|
3011
|
+
if (crosshair.sideHintRight) {
|
|
3012
|
+
ctx.fillStyle = crosshair.sideHintRightColor;
|
|
3013
|
+
ctx.textAlign = "left";
|
|
3014
|
+
ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
|
|
3015
|
+
}
|
|
3016
|
+
ctx.restore();
|
|
3017
|
+
}
|
|
2994
3018
|
}
|
|
2995
3019
|
ctx.restore();
|
|
2996
3020
|
const positionAxisGutter = Math.max(0, width - chartRight);
|
|
@@ -3869,6 +3893,23 @@ function createChart(element, options = {}) {
|
|
|
3869
3893
|
...time ? { time: time.toISOString() } : {}
|
|
3870
3894
|
};
|
|
3871
3895
|
};
|
|
3896
|
+
const constrainAngleFromAnchor = (anchor, x, y) => {
|
|
3897
|
+
const ax = canvasXFromDrawingPoint(anchor);
|
|
3898
|
+
const ay = canvasYFromDrawingPrice(anchor.price);
|
|
3899
|
+
const dx = x - ax;
|
|
3900
|
+
const dy = y - ay;
|
|
3901
|
+
if (dx === 0 && dy === 0) return drawingPointFromCanvas(x, y);
|
|
3902
|
+
const step = Math.PI / 4;
|
|
3903
|
+
const snappedAngle = Math.round(Math.atan2(dy, dx) / step) * step;
|
|
3904
|
+
const length = Math.hypot(dx, dy);
|
|
3905
|
+
const sx = ax + Math.cos(snappedAngle) * length;
|
|
3906
|
+
const sy = ay + Math.sin(snappedAngle) * length;
|
|
3907
|
+
const resolved = drawingPointFromCanvas(sx, sy);
|
|
3908
|
+
if (!resolved) return resolved;
|
|
3909
|
+
if (Math.abs(Math.sin(snappedAngle)) < 1e-6) return { ...resolved, price: anchor.price };
|
|
3910
|
+
if (Math.abs(Math.cos(snappedAngle)) < 1e-6) return { ...resolved, index: anchor.index };
|
|
3911
|
+
return resolved;
|
|
3912
|
+
};
|
|
3872
3913
|
const normalizeDrawingPoint = (index, price) => {
|
|
3873
3914
|
const roundedIndex = Math.round(index);
|
|
3874
3915
|
const time = getTimeForIndex(roundedIndex);
|
|
@@ -4172,9 +4213,11 @@ function createChart(element, options = {}) {
|
|
|
4172
4213
|
}
|
|
4173
4214
|
if (activeDrawingTool === "trendline") {
|
|
4174
4215
|
if (draftDrawing?.type === "trendline") {
|
|
4216
|
+
const anchor = draftDrawing.points[0];
|
|
4217
|
+
const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
|
|
4175
4218
|
const completed = normalizeDrawingState({
|
|
4176
4219
|
...serializeDrawing(draftDrawing),
|
|
4177
|
-
points: [
|
|
4220
|
+
points: [anchor, endPoint]
|
|
4178
4221
|
});
|
|
4179
4222
|
drawings.push(completed);
|
|
4180
4223
|
draftDrawing = null;
|
|
@@ -4196,9 +4239,11 @@ function createChart(element, options = {}) {
|
|
|
4196
4239
|
}
|
|
4197
4240
|
if (activeDrawingTool === "ray") {
|
|
4198
4241
|
if (draftDrawing?.type === "ray") {
|
|
4242
|
+
const anchor = draftDrawing.points[0];
|
|
4243
|
+
const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
|
|
4199
4244
|
const completed = normalizeDrawingState({
|
|
4200
4245
|
...serializeDrawing(draftDrawing),
|
|
4201
|
-
points: [
|
|
4246
|
+
points: [anchor, endPoint]
|
|
4202
4247
|
});
|
|
4203
4248
|
drawings.push(completed);
|
|
4204
4249
|
draftDrawing = null;
|
|
@@ -4395,6 +4440,17 @@ function createChart(element, options = {}) {
|
|
|
4395
4440
|
}
|
|
4396
4441
|
return { ...drawing, points: pts };
|
|
4397
4442
|
}
|
|
4443
|
+
if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray")) {
|
|
4444
|
+
const pointIndex = drawingDragState.pointIndex ?? 0;
|
|
4445
|
+
const anchor = drawing.points[pointIndex === 0 ? 1 : 0];
|
|
4446
|
+
const constrained = anchor ? constrainAngleFromAnchor(anchor, x, y) : null;
|
|
4447
|
+
if (constrained) {
|
|
4448
|
+
return {
|
|
4449
|
+
...drawing,
|
|
4450
|
+
points: drawing.points.map((point, index) => index === pointIndex ? constrained : point)
|
|
4451
|
+
};
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4398
4454
|
if (drawingDragState.target === "handle") {
|
|
4399
4455
|
return {
|
|
4400
4456
|
...drawing,
|
|
@@ -4483,6 +4539,7 @@ function createChart(element, options = {}) {
|
|
|
4483
4539
|
event.preventDefault();
|
|
4484
4540
|
}
|
|
4485
4541
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
4542
|
+
shiftKeyActive = event.shiftKey;
|
|
4486
4543
|
const point = getCanvasPoint(event);
|
|
4487
4544
|
if (event.pointerType === "touch") {
|
|
4488
4545
|
touchPointers.set(event.pointerId, point);
|
|
@@ -4606,6 +4663,7 @@ function createChart(element, options = {}) {
|
|
|
4606
4663
|
event.preventDefault();
|
|
4607
4664
|
}
|
|
4608
4665
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
4666
|
+
shiftKeyActive = event.shiftKey;
|
|
4609
4667
|
const point = getCanvasPoint(event);
|
|
4610
4668
|
if (event.pointerType === "touch" && touchPointers.has(event.pointerId)) {
|
|
4611
4669
|
touchPointers.set(event.pointerId, point);
|
|
@@ -4648,7 +4706,10 @@ function createChart(element, options = {}) {
|
|
|
4648
4706
|
return;
|
|
4649
4707
|
}
|
|
4650
4708
|
if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
|
|
4651
|
-
|
|
4709
|
+
let nextPoint = drawingPointFromCanvas(point.x, point.y);
|
|
4710
|
+
if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray") && draftDrawing.points[0]) {
|
|
4711
|
+
nextPoint = constrainAngleFromAnchor(draftDrawing.points[0], point.x, point.y) ?? nextPoint;
|
|
4712
|
+
}
|
|
4652
4713
|
if (nextPoint) {
|
|
4653
4714
|
draftDrawing = {
|
|
4654
4715
|
...draftDrawing,
|
|
@@ -4963,10 +5024,11 @@ function createChart(element, options = {}) {
|
|
|
4963
5024
|
canvas.addEventListener("contextmenu", onContextMenu);
|
|
4964
5025
|
const onModifierKeyChange = (event) => {
|
|
4965
5026
|
const active = event.metaKey || event.ctrlKey;
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
5027
|
+
const shift = event.shiftKey;
|
|
5028
|
+
const changed = active !== magnetModifierActive || shift !== shiftKeyActive;
|
|
5029
|
+
magnetModifierActive = active;
|
|
5030
|
+
shiftKeyActive = shift;
|
|
5031
|
+
if (changed && draftDrawing) draw();
|
|
4970
5032
|
};
|
|
4971
5033
|
const onWindowBlurMagnet = () => {
|
|
4972
5034
|
magnetModifierActive = false;
|
package/docs/API.md
CHANGED
|
@@ -486,6 +486,7 @@ Use `getDrawings()` / `setDrawings()` for persistence.
|
|
|
486
486
|
- `onDrawingEditText(handler): void` (fires when a `text`/`note` tool is placed or double-clicked; use it to show an inline text editor at `{x, y}` and write the result back via `updateDrawing(id, { label })`). `text`/`note` drawings store their content in `label` and size in `fontSize`.
|
|
487
487
|
- `setTradeMarkers(markers: TradeMarkerOptions[]): void` — draw trade execution markers (arrow + `qty @ price` label) on the candle of each fill's `time`. Buy = arrow below the low pointing up; sell = arrow above the high pointing down. Each marker: `{ time (ms|ISO), price, side: "buy"|"sell", qty?, text?, color?, textColor? }` (defaults: buy `#2962ff`, sell `#f23645`, text `#d1d4dc`). Markers resolve to bars by time, so they persist across timeframe switches and scrolling. Pass `[]` to clear.
|
|
488
488
|
- `cancelDrawing(): boolean` — abort an in-progress multi-click drawing (between the first click and the final point, e.g. a half-drawn trendline/ray/fib). Returns `true` if a draft was cancelled. Wire it to Escape.
|
|
489
|
+
- Hold **Shift** while drawing or dragging an endpoint of a `trendline`/`ray` to constrain it to the nearest 0/45/90° angle (e.g. a perfectly horizontal line).
|
|
489
490
|
- `setMagnetMode(mode): void` / `getMagnetMode()` — magnet/snap for all drawing tools. `"none"` off, `"weak"` snaps to a candle's OHLC only when the cursor is near a value, `"strong"` always snaps to the nearest OHLC of the bar under the cursor. Holding Cmd/Ctrl while drawing/dragging forces strong snapping regardless of the mode.
|
|
490
491
|
- `setActiveDrawingTool(tool: DrawingToolType | null): void` (`DrawingToolType` = `"horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement"`)
|
|
491
492
|
- `getActiveDrawingTool(): DrawingToolType | null`
|