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.
@@ -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: [draftDrawing.points[0], point]
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: [draftDrawing.points[0], point]
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
- const nextPoint = drawingPointFromCanvas(point.x, point.y);
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
- if (active !== magnetModifierActive) {
4993
- magnetModifierActive = active;
4994
- if (draftDrawing) draw();
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: [draftDrawing.points[0], point]
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: [draftDrawing.points[0], point]
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
- const nextPoint = drawingPointFromCanvas(point.x, point.y);
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
- if (active !== magnetModifierActive) {
4967
- magnetModifierActive = active;
4968
- if (draftDrawing) draw();
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: [draftDrawing.points[0], point]
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: [draftDrawing.points[0], point]
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
- const nextPoint = drawingPointFromCanvas(point.x, point.y);
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
- if (active !== magnetModifierActive) {
4993
- magnetModifierActive = active;
4994
- if (draftDrawing) draw();
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: [draftDrawing.points[0], point]
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: [draftDrawing.points[0], point]
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
- const nextPoint = drawingPointFromCanvas(point.x, point.y);
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
- if (active !== magnetModifierActive) {
4967
- magnetModifierActive = active;
4968
- if (draftDrawing) draw();
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`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.104",
3
+ "version": "0.1.106",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",