hyperprop-charting-library 0.1.105 → 0.1.107

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.
@@ -1007,6 +1007,7 @@ function createChart(element, options = {}) {
1007
1007
  let drawingEditTextHandler = null;
1008
1008
  let magnetMode = "none";
1009
1009
  let magnetModifierActive = false;
1010
+ let shiftKeyActive = false;
1010
1011
  const MAGNET_WEAK_THRESHOLD_PX = 16;
1011
1012
  let drawingHoverHandler = null;
1012
1013
  let lastHoveredDrawingId = null;
@@ -3918,6 +3919,23 @@ function createChart(element, options = {}) {
3918
3919
  ...time ? { time: time.toISOString() } : {}
3919
3920
  };
3920
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
+ };
3921
3939
  const normalizeDrawingPoint = (index, price) => {
3922
3940
  const roundedIndex = Math.round(index);
3923
3941
  const time = getTimeForIndex(roundedIndex);
@@ -4221,9 +4239,11 @@ function createChart(element, options = {}) {
4221
4239
  }
4222
4240
  if (activeDrawingTool === "trendline") {
4223
4241
  if (draftDrawing?.type === "trendline") {
4242
+ const anchor = draftDrawing.points[0];
4243
+ const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
4224
4244
  const completed = normalizeDrawingState({
4225
4245
  ...serializeDrawing(draftDrawing),
4226
- points: [draftDrawing.points[0], point]
4246
+ points: [anchor, endPoint]
4227
4247
  });
4228
4248
  drawings.push(completed);
4229
4249
  draftDrawing = null;
@@ -4245,9 +4265,11 @@ function createChart(element, options = {}) {
4245
4265
  }
4246
4266
  if (activeDrawingTool === "ray") {
4247
4267
  if (draftDrawing?.type === "ray") {
4268
+ const anchor = draftDrawing.points[0];
4269
+ const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
4248
4270
  const completed = normalizeDrawingState({
4249
4271
  ...serializeDrawing(draftDrawing),
4250
- points: [draftDrawing.points[0], point]
4272
+ points: [anchor, endPoint]
4251
4273
  });
4252
4274
  drawings.push(completed);
4253
4275
  draftDrawing = null;
@@ -4444,6 +4466,17 @@ function createChart(element, options = {}) {
4444
4466
  }
4445
4467
  return { ...drawing, points: pts };
4446
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
+ }
4447
4480
  if (drawingDragState.target === "handle") {
4448
4481
  return {
4449
4482
  ...drawing,
@@ -4532,6 +4565,7 @@ function createChart(element, options = {}) {
4532
4565
  event.preventDefault();
4533
4566
  }
4534
4567
  magnetModifierActive = event.metaKey || event.ctrlKey;
4568
+ shiftKeyActive = event.shiftKey;
4535
4569
  const point = getCanvasPoint(event);
4536
4570
  if (event.pointerType === "touch") {
4537
4571
  touchPointers.set(event.pointerId, point);
@@ -4631,7 +4665,7 @@ function createChart(element, options = {}) {
4631
4665
  isDragging = true;
4632
4666
  dragMode = region;
4633
4667
  activePointerId = event.pointerId;
4634
- const crosshairDrag = (event.pointerType === "touch" || event.pointerType === "pen") && region === "plot";
4668
+ const crosshairDrag = false;
4635
4669
  pointerDownInfo = {
4636
4670
  pointerId: event.pointerId,
4637
4671
  pointerType: event.pointerType,
@@ -4655,6 +4689,7 @@ function createChart(element, options = {}) {
4655
4689
  event.preventDefault();
4656
4690
  }
4657
4691
  magnetModifierActive = event.metaKey || event.ctrlKey;
4692
+ shiftKeyActive = event.shiftKey;
4658
4693
  const point = getCanvasPoint(event);
4659
4694
  if (event.pointerType === "touch" && touchPointers.has(event.pointerId)) {
4660
4695
  touchPointers.set(event.pointerId, point);
@@ -4697,7 +4732,10 @@ function createChart(element, options = {}) {
4697
4732
  return;
4698
4733
  }
4699
4734
  if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
4700
- 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
+ }
4701
4739
  if (nextPoint) {
4702
4740
  draftDrawing = {
4703
4741
  ...draftDrawing,
@@ -5012,10 +5050,11 @@ function createChart(element, options = {}) {
5012
5050
  canvas.addEventListener("contextmenu", onContextMenu);
5013
5051
  const onModifierKeyChange = (event) => {
5014
5052
  const active = event.metaKey || event.ctrlKey;
5015
- if (active !== magnetModifierActive) {
5016
- magnetModifierActive = active;
5017
- if (draftDrawing) draw();
5018
- }
5053
+ const shift = event.shiftKey;
5054
+ const changed = active !== magnetModifierActive || shift !== shiftKeyActive;
5055
+ magnetModifierActive = active;
5056
+ shiftKeyActive = shift;
5057
+ if (changed && draftDrawing) draw();
5019
5058
  };
5020
5059
  const onWindowBlurMagnet = () => {
5021
5060
  magnetModifierActive = false;
@@ -981,6 +981,7 @@ function createChart(element, options = {}) {
981
981
  let drawingEditTextHandler = null;
982
982
  let magnetMode = "none";
983
983
  let magnetModifierActive = false;
984
+ let shiftKeyActive = false;
984
985
  const MAGNET_WEAK_THRESHOLD_PX = 16;
985
986
  let drawingHoverHandler = null;
986
987
  let lastHoveredDrawingId = null;
@@ -3892,6 +3893,23 @@ function createChart(element, options = {}) {
3892
3893
  ...time ? { time: time.toISOString() } : {}
3893
3894
  };
3894
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
+ };
3895
3913
  const normalizeDrawingPoint = (index, price) => {
3896
3914
  const roundedIndex = Math.round(index);
3897
3915
  const time = getTimeForIndex(roundedIndex);
@@ -4195,9 +4213,11 @@ function createChart(element, options = {}) {
4195
4213
  }
4196
4214
  if (activeDrawingTool === "trendline") {
4197
4215
  if (draftDrawing?.type === "trendline") {
4216
+ const anchor = draftDrawing.points[0];
4217
+ const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
4198
4218
  const completed = normalizeDrawingState({
4199
4219
  ...serializeDrawing(draftDrawing),
4200
- points: [draftDrawing.points[0], point]
4220
+ points: [anchor, endPoint]
4201
4221
  });
4202
4222
  drawings.push(completed);
4203
4223
  draftDrawing = null;
@@ -4219,9 +4239,11 @@ function createChart(element, options = {}) {
4219
4239
  }
4220
4240
  if (activeDrawingTool === "ray") {
4221
4241
  if (draftDrawing?.type === "ray") {
4242
+ const anchor = draftDrawing.points[0];
4243
+ const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
4222
4244
  const completed = normalizeDrawingState({
4223
4245
  ...serializeDrawing(draftDrawing),
4224
- points: [draftDrawing.points[0], point]
4246
+ points: [anchor, endPoint]
4225
4247
  });
4226
4248
  drawings.push(completed);
4227
4249
  draftDrawing = null;
@@ -4418,6 +4440,17 @@ function createChart(element, options = {}) {
4418
4440
  }
4419
4441
  return { ...drawing, points: pts };
4420
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
+ }
4421
4454
  if (drawingDragState.target === "handle") {
4422
4455
  return {
4423
4456
  ...drawing,
@@ -4506,6 +4539,7 @@ function createChart(element, options = {}) {
4506
4539
  event.preventDefault();
4507
4540
  }
4508
4541
  magnetModifierActive = event.metaKey || event.ctrlKey;
4542
+ shiftKeyActive = event.shiftKey;
4509
4543
  const point = getCanvasPoint(event);
4510
4544
  if (event.pointerType === "touch") {
4511
4545
  touchPointers.set(event.pointerId, point);
@@ -4605,7 +4639,7 @@ function createChart(element, options = {}) {
4605
4639
  isDragging = true;
4606
4640
  dragMode = region;
4607
4641
  activePointerId = event.pointerId;
4608
- const crosshairDrag = (event.pointerType === "touch" || event.pointerType === "pen") && region === "plot";
4642
+ const crosshairDrag = false;
4609
4643
  pointerDownInfo = {
4610
4644
  pointerId: event.pointerId,
4611
4645
  pointerType: event.pointerType,
@@ -4629,6 +4663,7 @@ function createChart(element, options = {}) {
4629
4663
  event.preventDefault();
4630
4664
  }
4631
4665
  magnetModifierActive = event.metaKey || event.ctrlKey;
4666
+ shiftKeyActive = event.shiftKey;
4632
4667
  const point = getCanvasPoint(event);
4633
4668
  if (event.pointerType === "touch" && touchPointers.has(event.pointerId)) {
4634
4669
  touchPointers.set(event.pointerId, point);
@@ -4671,7 +4706,10 @@ function createChart(element, options = {}) {
4671
4706
  return;
4672
4707
  }
4673
4708
  if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
4674
- 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
+ }
4675
4713
  if (nextPoint) {
4676
4714
  draftDrawing = {
4677
4715
  ...draftDrawing,
@@ -4986,10 +5024,11 @@ function createChart(element, options = {}) {
4986
5024
  canvas.addEventListener("contextmenu", onContextMenu);
4987
5025
  const onModifierKeyChange = (event) => {
4988
5026
  const active = event.metaKey || event.ctrlKey;
4989
- if (active !== magnetModifierActive) {
4990
- magnetModifierActive = active;
4991
- if (draftDrawing) draw();
4992
- }
5027
+ const shift = event.shiftKey;
5028
+ const changed = active !== magnetModifierActive || shift !== shiftKeyActive;
5029
+ magnetModifierActive = active;
5030
+ shiftKeyActive = shift;
5031
+ if (changed && draftDrawing) draw();
4993
5032
  };
4994
5033
  const onWindowBlurMagnet = () => {
4995
5034
  magnetModifierActive = false;
package/dist/index.cjs CHANGED
@@ -1007,6 +1007,7 @@ function createChart(element, options = {}) {
1007
1007
  let drawingEditTextHandler = null;
1008
1008
  let magnetMode = "none";
1009
1009
  let magnetModifierActive = false;
1010
+ let shiftKeyActive = false;
1010
1011
  const MAGNET_WEAK_THRESHOLD_PX = 16;
1011
1012
  let drawingHoverHandler = null;
1012
1013
  let lastHoveredDrawingId = null;
@@ -3918,6 +3919,23 @@ function createChart(element, options = {}) {
3918
3919
  ...time ? { time: time.toISOString() } : {}
3919
3920
  };
3920
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
+ };
3921
3939
  const normalizeDrawingPoint = (index, price) => {
3922
3940
  const roundedIndex = Math.round(index);
3923
3941
  const time = getTimeForIndex(roundedIndex);
@@ -4221,9 +4239,11 @@ function createChart(element, options = {}) {
4221
4239
  }
4222
4240
  if (activeDrawingTool === "trendline") {
4223
4241
  if (draftDrawing?.type === "trendline") {
4242
+ const anchor = draftDrawing.points[0];
4243
+ const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
4224
4244
  const completed = normalizeDrawingState({
4225
4245
  ...serializeDrawing(draftDrawing),
4226
- points: [draftDrawing.points[0], point]
4246
+ points: [anchor, endPoint]
4227
4247
  });
4228
4248
  drawings.push(completed);
4229
4249
  draftDrawing = null;
@@ -4245,9 +4265,11 @@ function createChart(element, options = {}) {
4245
4265
  }
4246
4266
  if (activeDrawingTool === "ray") {
4247
4267
  if (draftDrawing?.type === "ray") {
4268
+ const anchor = draftDrawing.points[0];
4269
+ const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
4248
4270
  const completed = normalizeDrawingState({
4249
4271
  ...serializeDrawing(draftDrawing),
4250
- points: [draftDrawing.points[0], point]
4272
+ points: [anchor, endPoint]
4251
4273
  });
4252
4274
  drawings.push(completed);
4253
4275
  draftDrawing = null;
@@ -4444,6 +4466,17 @@ function createChart(element, options = {}) {
4444
4466
  }
4445
4467
  return { ...drawing, points: pts };
4446
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
+ }
4447
4480
  if (drawingDragState.target === "handle") {
4448
4481
  return {
4449
4482
  ...drawing,
@@ -4532,6 +4565,7 @@ function createChart(element, options = {}) {
4532
4565
  event.preventDefault();
4533
4566
  }
4534
4567
  magnetModifierActive = event.metaKey || event.ctrlKey;
4568
+ shiftKeyActive = event.shiftKey;
4535
4569
  const point = getCanvasPoint(event);
4536
4570
  if (event.pointerType === "touch") {
4537
4571
  touchPointers.set(event.pointerId, point);
@@ -4631,7 +4665,7 @@ function createChart(element, options = {}) {
4631
4665
  isDragging = true;
4632
4666
  dragMode = region;
4633
4667
  activePointerId = event.pointerId;
4634
- const crosshairDrag = (event.pointerType === "touch" || event.pointerType === "pen") && region === "plot";
4668
+ const crosshairDrag = false;
4635
4669
  pointerDownInfo = {
4636
4670
  pointerId: event.pointerId,
4637
4671
  pointerType: event.pointerType,
@@ -4655,6 +4689,7 @@ function createChart(element, options = {}) {
4655
4689
  event.preventDefault();
4656
4690
  }
4657
4691
  magnetModifierActive = event.metaKey || event.ctrlKey;
4692
+ shiftKeyActive = event.shiftKey;
4658
4693
  const point = getCanvasPoint(event);
4659
4694
  if (event.pointerType === "touch" && touchPointers.has(event.pointerId)) {
4660
4695
  touchPointers.set(event.pointerId, point);
@@ -4697,7 +4732,10 @@ function createChart(element, options = {}) {
4697
4732
  return;
4698
4733
  }
4699
4734
  if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
4700
- 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
+ }
4701
4739
  if (nextPoint) {
4702
4740
  draftDrawing = {
4703
4741
  ...draftDrawing,
@@ -5012,10 +5050,11 @@ function createChart(element, options = {}) {
5012
5050
  canvas.addEventListener("contextmenu", onContextMenu);
5013
5051
  const onModifierKeyChange = (event) => {
5014
5052
  const active = event.metaKey || event.ctrlKey;
5015
- if (active !== magnetModifierActive) {
5016
- magnetModifierActive = active;
5017
- if (draftDrawing) draw();
5018
- }
5053
+ const shift = event.shiftKey;
5054
+ const changed = active !== magnetModifierActive || shift !== shiftKeyActive;
5055
+ magnetModifierActive = active;
5056
+ shiftKeyActive = shift;
5057
+ if (changed && draftDrawing) draw();
5019
5058
  };
5020
5059
  const onWindowBlurMagnet = () => {
5021
5060
  magnetModifierActive = false;
package/dist/index.js CHANGED
@@ -981,6 +981,7 @@ function createChart(element, options = {}) {
981
981
  let drawingEditTextHandler = null;
982
982
  let magnetMode = "none";
983
983
  let magnetModifierActive = false;
984
+ let shiftKeyActive = false;
984
985
  const MAGNET_WEAK_THRESHOLD_PX = 16;
985
986
  let drawingHoverHandler = null;
986
987
  let lastHoveredDrawingId = null;
@@ -3892,6 +3893,23 @@ function createChart(element, options = {}) {
3892
3893
  ...time ? { time: time.toISOString() } : {}
3893
3894
  };
3894
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
+ };
3895
3913
  const normalizeDrawingPoint = (index, price) => {
3896
3914
  const roundedIndex = Math.round(index);
3897
3915
  const time = getTimeForIndex(roundedIndex);
@@ -4195,9 +4213,11 @@ function createChart(element, options = {}) {
4195
4213
  }
4196
4214
  if (activeDrawingTool === "trendline") {
4197
4215
  if (draftDrawing?.type === "trendline") {
4216
+ const anchor = draftDrawing.points[0];
4217
+ const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
4198
4218
  const completed = normalizeDrawingState({
4199
4219
  ...serializeDrawing(draftDrawing),
4200
- points: [draftDrawing.points[0], point]
4220
+ points: [anchor, endPoint]
4201
4221
  });
4202
4222
  drawings.push(completed);
4203
4223
  draftDrawing = null;
@@ -4219,9 +4239,11 @@ function createChart(element, options = {}) {
4219
4239
  }
4220
4240
  if (activeDrawingTool === "ray") {
4221
4241
  if (draftDrawing?.type === "ray") {
4242
+ const anchor = draftDrawing.points[0];
4243
+ const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
4222
4244
  const completed = normalizeDrawingState({
4223
4245
  ...serializeDrawing(draftDrawing),
4224
- points: [draftDrawing.points[0], point]
4246
+ points: [anchor, endPoint]
4225
4247
  });
4226
4248
  drawings.push(completed);
4227
4249
  draftDrawing = null;
@@ -4418,6 +4440,17 @@ function createChart(element, options = {}) {
4418
4440
  }
4419
4441
  return { ...drawing, points: pts };
4420
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
+ }
4421
4454
  if (drawingDragState.target === "handle") {
4422
4455
  return {
4423
4456
  ...drawing,
@@ -4506,6 +4539,7 @@ function createChart(element, options = {}) {
4506
4539
  event.preventDefault();
4507
4540
  }
4508
4541
  magnetModifierActive = event.metaKey || event.ctrlKey;
4542
+ shiftKeyActive = event.shiftKey;
4509
4543
  const point = getCanvasPoint(event);
4510
4544
  if (event.pointerType === "touch") {
4511
4545
  touchPointers.set(event.pointerId, point);
@@ -4605,7 +4639,7 @@ function createChart(element, options = {}) {
4605
4639
  isDragging = true;
4606
4640
  dragMode = region;
4607
4641
  activePointerId = event.pointerId;
4608
- const crosshairDrag = (event.pointerType === "touch" || event.pointerType === "pen") && region === "plot";
4642
+ const crosshairDrag = false;
4609
4643
  pointerDownInfo = {
4610
4644
  pointerId: event.pointerId,
4611
4645
  pointerType: event.pointerType,
@@ -4629,6 +4663,7 @@ function createChart(element, options = {}) {
4629
4663
  event.preventDefault();
4630
4664
  }
4631
4665
  magnetModifierActive = event.metaKey || event.ctrlKey;
4666
+ shiftKeyActive = event.shiftKey;
4632
4667
  const point = getCanvasPoint(event);
4633
4668
  if (event.pointerType === "touch" && touchPointers.has(event.pointerId)) {
4634
4669
  touchPointers.set(event.pointerId, point);
@@ -4671,7 +4706,10 @@ function createChart(element, options = {}) {
4671
4706
  return;
4672
4707
  }
4673
4708
  if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
4674
- 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
+ }
4675
4713
  if (nextPoint) {
4676
4714
  draftDrawing = {
4677
4715
  ...draftDrawing,
@@ -4986,10 +5024,11 @@ function createChart(element, options = {}) {
4986
5024
  canvas.addEventListener("contextmenu", onContextMenu);
4987
5025
  const onModifierKeyChange = (event) => {
4988
5026
  const active = event.metaKey || event.ctrlKey;
4989
- if (active !== magnetModifierActive) {
4990
- magnetModifierActive = active;
4991
- if (draftDrawing) draw();
4992
- }
5027
+ const shift = event.shiftKey;
5028
+ const changed = active !== magnetModifierActive || shift !== shiftKeyActive;
5029
+ magnetModifierActive = active;
5030
+ shiftKeyActive = shift;
5031
+ if (changed && draftDrawing) draw();
4993
5032
  };
4994
5033
  const onWindowBlurMagnet = () => {
4995
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.105",
3
+ "version": "0.1.107",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",