hyperprop-charting-library 0.1.114 → 0.1.116

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.
@@ -1239,7 +1239,15 @@ function createChart(element, options = {}) {
1239
1239
  const minSpan = minVisibleBars;
1240
1240
  const maxSpan = Math.min(maxVisibleBars, Math.max(minSpan, count + maxPanBars * 2));
1241
1241
  xSpan = clamp(xSpan, minSpan, maxSpan);
1242
- xCenter = clamp(xCenter, -maxPanBars, count + maxPanBars);
1242
+ const halfSpan = xSpan / 2;
1243
+ const minOverlap = Math.max(1, Math.min(count, Math.ceil(xSpan * 0.1)));
1244
+ const lowCenter = minOverlap - halfSpan;
1245
+ const highCenter = count - minOverlap + halfSpan;
1246
+ xCenter = clamp(
1247
+ xCenter,
1248
+ Math.max(lowCenter, -maxPanBars),
1249
+ Math.min(highCenter, count + maxPanBars)
1250
+ );
1243
1251
  };
1244
1252
  const fitXViewport = () => {
1245
1253
  const count = data.length;
@@ -2666,6 +2674,37 @@ function createChart(element, options = {}) {
2666
2674
  }
2667
2675
  }
2668
2676
  }
2677
+ } else if (drawing.type === "rectangle") {
2678
+ const p0 = drawing.points[0];
2679
+ const p1 = drawing.points[1];
2680
+ if (p0 && p1) {
2681
+ const px0 = xFromDrawingPoint(p0);
2682
+ const px1 = xFromDrawingPoint(p1);
2683
+ const leftX = Math.min(px0, px1);
2684
+ const rightX = Math.max(px0, px1);
2685
+ const y0 = yFromPrice(p0.price);
2686
+ const y1 = yFromPrice(p1.price);
2687
+ const topY = Math.min(y0, y1);
2688
+ const botY = Math.max(y0, y1);
2689
+ const boxW = Math.max(1, rightX - leftX);
2690
+ const boxH = Math.max(1, botY - topY);
2691
+ ctx.save();
2692
+ ctx.globalAlpha = draft ? 0.6 : 1;
2693
+ ctx.fillStyle = hexToRgba(drawing.color, 0.14);
2694
+ ctx.fillRect(leftX, topY, boxW, boxH);
2695
+ ctx.lineWidth = Math.max(1, drawing.width);
2696
+ ctx.strokeStyle = drawing.color;
2697
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2698
+ ctx.strokeRect(crisp(leftX), crisp(topY), crisp(rightX) - crisp(leftX), crisp(botY) - crisp(topY));
2699
+ ctx.restore();
2700
+ handleAt(px0, y0, drawing.color);
2701
+ handleAt(px1, y1, drawing.color);
2702
+ handleAt(px0, y1, drawing.color);
2703
+ handleAt(px1, y0, drawing.color);
2704
+ if (drawing.label) {
2705
+ drawDrawingLabel(drawing.label, (leftX + rightX) / 2, topY - 4, drawing.color);
2706
+ }
2707
+ }
2669
2708
  } else if (drawing.type === "price-range") {
2670
2709
  const p0 = drawing.points[0];
2671
2710
  const p1 = drawing.points[1];
@@ -4155,6 +4194,21 @@ function createChart(element, options = {}) {
4155
4194
  if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
4156
4195
  return { drawing, target: "line" };
4157
4196
  }
4197
+ } else if (drawing.type === "rectangle") {
4198
+ const p0 = drawing.points[0];
4199
+ const p1 = drawing.points[1];
4200
+ if (!p0 || !p1) continue;
4201
+ const px0 = canvasXFromDrawingPoint(p0);
4202
+ const px1 = canvasXFromDrawingPoint(p1);
4203
+ const y0 = canvasYFromDrawingPrice(p0.price);
4204
+ const y1 = canvasYFromDrawingPrice(p1.price);
4205
+ if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
4206
+ if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
4207
+ if (Math.hypot(x - px0, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 2 };
4208
+ if (Math.hypot(x - px1, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 3 };
4209
+ if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
4210
+ return { drawing, target: "line" };
4211
+ }
4158
4212
  } else if (drawing.type === "price-range") {
4159
4213
  const p0 = drawing.points[0];
4160
4214
  const p1 = drawing.points[1];
@@ -4411,6 +4465,30 @@ function createChart(element, options = {}) {
4411
4465
  drawingEditTextHandler?.({ drawing: serializeDrawing(created), target: "line", x, y });
4412
4466
  return true;
4413
4467
  }
4468
+ if (activeDrawingTool === "rectangle") {
4469
+ if (draftDrawing?.type === "rectangle") {
4470
+ const completed = normalizeDrawingState({
4471
+ ...serializeDrawing(draftDrawing),
4472
+ points: [draftDrawing.points[0], point]
4473
+ });
4474
+ drawings.push(completed);
4475
+ draftDrawing = null;
4476
+ activeDrawingTool = null;
4477
+ emitDrawingsChange();
4478
+ scheduleDraw();
4479
+ return true;
4480
+ }
4481
+ const defaults = getDrawingToolDefaults("rectangle");
4482
+ draftDrawing = normalizeDrawingState({
4483
+ type: "rectangle",
4484
+ points: [point, point],
4485
+ color: defaults.color ?? "#2962ff",
4486
+ style: defaults.style ?? "solid",
4487
+ width: defaults.width ?? 1
4488
+ });
4489
+ scheduleDraw();
4490
+ return true;
4491
+ }
4414
4492
  if (activeDrawingTool === "price-range") {
4415
4493
  const tick = getConfiguredTickSize();
4416
4494
  const visibleRange = drawState.yMax - drawState.yMin;
@@ -4512,6 +4590,25 @@ function createChart(element, options = {}) {
4512
4590
  }
4513
4591
  return { ...drawing, points: pts };
4514
4592
  }
4593
+ if (drawingDragState.target === "handle" && drawing.type === "rectangle") {
4594
+ const pts = drawing.points.map((point) => ({ ...point }));
4595
+ const p0 = pts[0];
4596
+ const p1 = pts[1];
4597
+ if (!p0 || !p1) return drawing;
4598
+ const pointIndex = drawingDragState.pointIndex ?? 0;
4599
+ if (pointIndex === 0) {
4600
+ pts[0] = normalizeDrawingPoint(currentPoint.index, currentPoint.price);
4601
+ } else if (pointIndex === 1) {
4602
+ pts[1] = normalizeDrawingPoint(currentPoint.index, currentPoint.price);
4603
+ } else if (pointIndex === 2) {
4604
+ pts[0] = normalizeDrawingPoint(currentPoint.index, p0.price);
4605
+ pts[1] = normalizeDrawingPoint(p1.index, currentPoint.price);
4606
+ } else if (pointIndex === 3) {
4607
+ pts[0] = normalizeDrawingPoint(p0.index, currentPoint.price);
4608
+ pts[1] = normalizeDrawingPoint(currentPoint.index, p1.price);
4609
+ }
4610
+ return { ...drawing, points: pts };
4611
+ }
4515
4612
  if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray")) {
4516
4613
  const pointIndex = drawingDragState.pointIndex ?? 0;
4517
4614
  const anchor = drawing.points[pointIndex === 0 ? 1 : 0];
@@ -4780,7 +4877,7 @@ function createChart(element, options = {}) {
4780
4877
  setCrosshairPoint(null);
4781
4878
  return;
4782
4879
  }
4783
- if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
4880
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "rectangle" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
4784
4881
  let nextPoint = drawingPointFromCanvas(point.x, point.y);
4785
4882
  if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray") && draftDrawing.points[0]) {
4786
4883
  nextPoint = constrainAngleFromAnchor(draftDrawing.points[0], point.x, point.y) ?? nextPoint;
@@ -44,7 +44,7 @@ interface ChartOptions {
44
44
  drawings?: DrawingObjectOptions[];
45
45
  }
46
46
  type IndicatorPane = "overlay" | "separate";
47
- type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range" | "text" | "note";
47
+ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range" | "rectangle" | "text" | "note";
48
48
  interface DrawingPoint {
49
49
  index: number;
50
50
  price: number;
@@ -1213,7 +1213,15 @@ function createChart(element, options = {}) {
1213
1213
  const minSpan = minVisibleBars;
1214
1214
  const maxSpan = Math.min(maxVisibleBars, Math.max(minSpan, count + maxPanBars * 2));
1215
1215
  xSpan = clamp(xSpan, minSpan, maxSpan);
1216
- xCenter = clamp(xCenter, -maxPanBars, count + maxPanBars);
1216
+ const halfSpan = xSpan / 2;
1217
+ const minOverlap = Math.max(1, Math.min(count, Math.ceil(xSpan * 0.1)));
1218
+ const lowCenter = minOverlap - halfSpan;
1219
+ const highCenter = count - minOverlap + halfSpan;
1220
+ xCenter = clamp(
1221
+ xCenter,
1222
+ Math.max(lowCenter, -maxPanBars),
1223
+ Math.min(highCenter, count + maxPanBars)
1224
+ );
1217
1225
  };
1218
1226
  const fitXViewport = () => {
1219
1227
  const count = data.length;
@@ -2640,6 +2648,37 @@ function createChart(element, options = {}) {
2640
2648
  }
2641
2649
  }
2642
2650
  }
2651
+ } else if (drawing.type === "rectangle") {
2652
+ const p0 = drawing.points[0];
2653
+ const p1 = drawing.points[1];
2654
+ if (p0 && p1) {
2655
+ const px0 = xFromDrawingPoint(p0);
2656
+ const px1 = xFromDrawingPoint(p1);
2657
+ const leftX = Math.min(px0, px1);
2658
+ const rightX = Math.max(px0, px1);
2659
+ const y0 = yFromPrice(p0.price);
2660
+ const y1 = yFromPrice(p1.price);
2661
+ const topY = Math.min(y0, y1);
2662
+ const botY = Math.max(y0, y1);
2663
+ const boxW = Math.max(1, rightX - leftX);
2664
+ const boxH = Math.max(1, botY - topY);
2665
+ ctx.save();
2666
+ ctx.globalAlpha = draft ? 0.6 : 1;
2667
+ ctx.fillStyle = hexToRgba(drawing.color, 0.14);
2668
+ ctx.fillRect(leftX, topY, boxW, boxH);
2669
+ ctx.lineWidth = Math.max(1, drawing.width);
2670
+ ctx.strokeStyle = drawing.color;
2671
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2672
+ ctx.strokeRect(crisp(leftX), crisp(topY), crisp(rightX) - crisp(leftX), crisp(botY) - crisp(topY));
2673
+ ctx.restore();
2674
+ handleAt(px0, y0, drawing.color);
2675
+ handleAt(px1, y1, drawing.color);
2676
+ handleAt(px0, y1, drawing.color);
2677
+ handleAt(px1, y0, drawing.color);
2678
+ if (drawing.label) {
2679
+ drawDrawingLabel(drawing.label, (leftX + rightX) / 2, topY - 4, drawing.color);
2680
+ }
2681
+ }
2643
2682
  } else if (drawing.type === "price-range") {
2644
2683
  const p0 = drawing.points[0];
2645
2684
  const p1 = drawing.points[1];
@@ -4129,6 +4168,21 @@ function createChart(element, options = {}) {
4129
4168
  if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
4130
4169
  return { drawing, target: "line" };
4131
4170
  }
4171
+ } else if (drawing.type === "rectangle") {
4172
+ const p0 = drawing.points[0];
4173
+ const p1 = drawing.points[1];
4174
+ if (!p0 || !p1) continue;
4175
+ const px0 = canvasXFromDrawingPoint(p0);
4176
+ const px1 = canvasXFromDrawingPoint(p1);
4177
+ const y0 = canvasYFromDrawingPrice(p0.price);
4178
+ const y1 = canvasYFromDrawingPrice(p1.price);
4179
+ if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
4180
+ if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
4181
+ if (Math.hypot(x - px0, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 2 };
4182
+ if (Math.hypot(x - px1, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 3 };
4183
+ if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
4184
+ return { drawing, target: "line" };
4185
+ }
4132
4186
  } else if (drawing.type === "price-range") {
4133
4187
  const p0 = drawing.points[0];
4134
4188
  const p1 = drawing.points[1];
@@ -4385,6 +4439,30 @@ function createChart(element, options = {}) {
4385
4439
  drawingEditTextHandler?.({ drawing: serializeDrawing(created), target: "line", x, y });
4386
4440
  return true;
4387
4441
  }
4442
+ if (activeDrawingTool === "rectangle") {
4443
+ if (draftDrawing?.type === "rectangle") {
4444
+ const completed = normalizeDrawingState({
4445
+ ...serializeDrawing(draftDrawing),
4446
+ points: [draftDrawing.points[0], point]
4447
+ });
4448
+ drawings.push(completed);
4449
+ draftDrawing = null;
4450
+ activeDrawingTool = null;
4451
+ emitDrawingsChange();
4452
+ scheduleDraw();
4453
+ return true;
4454
+ }
4455
+ const defaults = getDrawingToolDefaults("rectangle");
4456
+ draftDrawing = normalizeDrawingState({
4457
+ type: "rectangle",
4458
+ points: [point, point],
4459
+ color: defaults.color ?? "#2962ff",
4460
+ style: defaults.style ?? "solid",
4461
+ width: defaults.width ?? 1
4462
+ });
4463
+ scheduleDraw();
4464
+ return true;
4465
+ }
4388
4466
  if (activeDrawingTool === "price-range") {
4389
4467
  const tick = getConfiguredTickSize();
4390
4468
  const visibleRange = drawState.yMax - drawState.yMin;
@@ -4486,6 +4564,25 @@ function createChart(element, options = {}) {
4486
4564
  }
4487
4565
  return { ...drawing, points: pts };
4488
4566
  }
4567
+ if (drawingDragState.target === "handle" && drawing.type === "rectangle") {
4568
+ const pts = drawing.points.map((point) => ({ ...point }));
4569
+ const p0 = pts[0];
4570
+ const p1 = pts[1];
4571
+ if (!p0 || !p1) return drawing;
4572
+ const pointIndex = drawingDragState.pointIndex ?? 0;
4573
+ if (pointIndex === 0) {
4574
+ pts[0] = normalizeDrawingPoint(currentPoint.index, currentPoint.price);
4575
+ } else if (pointIndex === 1) {
4576
+ pts[1] = normalizeDrawingPoint(currentPoint.index, currentPoint.price);
4577
+ } else if (pointIndex === 2) {
4578
+ pts[0] = normalizeDrawingPoint(currentPoint.index, p0.price);
4579
+ pts[1] = normalizeDrawingPoint(p1.index, currentPoint.price);
4580
+ } else if (pointIndex === 3) {
4581
+ pts[0] = normalizeDrawingPoint(p0.index, currentPoint.price);
4582
+ pts[1] = normalizeDrawingPoint(currentPoint.index, p1.price);
4583
+ }
4584
+ return { ...drawing, points: pts };
4585
+ }
4489
4586
  if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray")) {
4490
4587
  const pointIndex = drawingDragState.pointIndex ?? 0;
4491
4588
  const anchor = drawing.points[pointIndex === 0 ? 1 : 0];
@@ -4754,7 +4851,7 @@ function createChart(element, options = {}) {
4754
4851
  setCrosshairPoint(null);
4755
4852
  return;
4756
4853
  }
4757
- if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
4854
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "rectangle" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
4758
4855
  let nextPoint = drawingPointFromCanvas(point.x, point.y);
4759
4856
  if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray") && draftDrawing.points[0]) {
4760
4857
  nextPoint = constrainAngleFromAnchor(draftDrawing.points[0], point.x, point.y) ?? nextPoint;
package/dist/index.cjs CHANGED
@@ -1239,7 +1239,15 @@ function createChart(element, options = {}) {
1239
1239
  const minSpan = minVisibleBars;
1240
1240
  const maxSpan = Math.min(maxVisibleBars, Math.max(minSpan, count + maxPanBars * 2));
1241
1241
  xSpan = clamp(xSpan, minSpan, maxSpan);
1242
- xCenter = clamp(xCenter, -maxPanBars, count + maxPanBars);
1242
+ const halfSpan = xSpan / 2;
1243
+ const minOverlap = Math.max(1, Math.min(count, Math.ceil(xSpan * 0.1)));
1244
+ const lowCenter = minOverlap - halfSpan;
1245
+ const highCenter = count - minOverlap + halfSpan;
1246
+ xCenter = clamp(
1247
+ xCenter,
1248
+ Math.max(lowCenter, -maxPanBars),
1249
+ Math.min(highCenter, count + maxPanBars)
1250
+ );
1243
1251
  };
1244
1252
  const fitXViewport = () => {
1245
1253
  const count = data.length;
@@ -2666,6 +2674,37 @@ function createChart(element, options = {}) {
2666
2674
  }
2667
2675
  }
2668
2676
  }
2677
+ } else if (drawing.type === "rectangle") {
2678
+ const p0 = drawing.points[0];
2679
+ const p1 = drawing.points[1];
2680
+ if (p0 && p1) {
2681
+ const px0 = xFromDrawingPoint(p0);
2682
+ const px1 = xFromDrawingPoint(p1);
2683
+ const leftX = Math.min(px0, px1);
2684
+ const rightX = Math.max(px0, px1);
2685
+ const y0 = yFromPrice(p0.price);
2686
+ const y1 = yFromPrice(p1.price);
2687
+ const topY = Math.min(y0, y1);
2688
+ const botY = Math.max(y0, y1);
2689
+ const boxW = Math.max(1, rightX - leftX);
2690
+ const boxH = Math.max(1, botY - topY);
2691
+ ctx.save();
2692
+ ctx.globalAlpha = draft ? 0.6 : 1;
2693
+ ctx.fillStyle = hexToRgba(drawing.color, 0.14);
2694
+ ctx.fillRect(leftX, topY, boxW, boxH);
2695
+ ctx.lineWidth = Math.max(1, drawing.width);
2696
+ ctx.strokeStyle = drawing.color;
2697
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2698
+ ctx.strokeRect(crisp(leftX), crisp(topY), crisp(rightX) - crisp(leftX), crisp(botY) - crisp(topY));
2699
+ ctx.restore();
2700
+ handleAt(px0, y0, drawing.color);
2701
+ handleAt(px1, y1, drawing.color);
2702
+ handleAt(px0, y1, drawing.color);
2703
+ handleAt(px1, y0, drawing.color);
2704
+ if (drawing.label) {
2705
+ drawDrawingLabel(drawing.label, (leftX + rightX) / 2, topY - 4, drawing.color);
2706
+ }
2707
+ }
2669
2708
  } else if (drawing.type === "price-range") {
2670
2709
  const p0 = drawing.points[0];
2671
2710
  const p1 = drawing.points[1];
@@ -4155,6 +4194,21 @@ function createChart(element, options = {}) {
4155
4194
  if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
4156
4195
  return { drawing, target: "line" };
4157
4196
  }
4197
+ } else if (drawing.type === "rectangle") {
4198
+ const p0 = drawing.points[0];
4199
+ const p1 = drawing.points[1];
4200
+ if (!p0 || !p1) continue;
4201
+ const px0 = canvasXFromDrawingPoint(p0);
4202
+ const px1 = canvasXFromDrawingPoint(p1);
4203
+ const y0 = canvasYFromDrawingPrice(p0.price);
4204
+ const y1 = canvasYFromDrawingPrice(p1.price);
4205
+ if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
4206
+ if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
4207
+ if (Math.hypot(x - px0, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 2 };
4208
+ if (Math.hypot(x - px1, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 3 };
4209
+ if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
4210
+ return { drawing, target: "line" };
4211
+ }
4158
4212
  } else if (drawing.type === "price-range") {
4159
4213
  const p0 = drawing.points[0];
4160
4214
  const p1 = drawing.points[1];
@@ -4411,6 +4465,30 @@ function createChart(element, options = {}) {
4411
4465
  drawingEditTextHandler?.({ drawing: serializeDrawing(created), target: "line", x, y });
4412
4466
  return true;
4413
4467
  }
4468
+ if (activeDrawingTool === "rectangle") {
4469
+ if (draftDrawing?.type === "rectangle") {
4470
+ const completed = normalizeDrawingState({
4471
+ ...serializeDrawing(draftDrawing),
4472
+ points: [draftDrawing.points[0], point]
4473
+ });
4474
+ drawings.push(completed);
4475
+ draftDrawing = null;
4476
+ activeDrawingTool = null;
4477
+ emitDrawingsChange();
4478
+ scheduleDraw();
4479
+ return true;
4480
+ }
4481
+ const defaults = getDrawingToolDefaults("rectangle");
4482
+ draftDrawing = normalizeDrawingState({
4483
+ type: "rectangle",
4484
+ points: [point, point],
4485
+ color: defaults.color ?? "#2962ff",
4486
+ style: defaults.style ?? "solid",
4487
+ width: defaults.width ?? 1
4488
+ });
4489
+ scheduleDraw();
4490
+ return true;
4491
+ }
4414
4492
  if (activeDrawingTool === "price-range") {
4415
4493
  const tick = getConfiguredTickSize();
4416
4494
  const visibleRange = drawState.yMax - drawState.yMin;
@@ -4512,6 +4590,25 @@ function createChart(element, options = {}) {
4512
4590
  }
4513
4591
  return { ...drawing, points: pts };
4514
4592
  }
4593
+ if (drawingDragState.target === "handle" && drawing.type === "rectangle") {
4594
+ const pts = drawing.points.map((point) => ({ ...point }));
4595
+ const p0 = pts[0];
4596
+ const p1 = pts[1];
4597
+ if (!p0 || !p1) return drawing;
4598
+ const pointIndex = drawingDragState.pointIndex ?? 0;
4599
+ if (pointIndex === 0) {
4600
+ pts[0] = normalizeDrawingPoint(currentPoint.index, currentPoint.price);
4601
+ } else if (pointIndex === 1) {
4602
+ pts[1] = normalizeDrawingPoint(currentPoint.index, currentPoint.price);
4603
+ } else if (pointIndex === 2) {
4604
+ pts[0] = normalizeDrawingPoint(currentPoint.index, p0.price);
4605
+ pts[1] = normalizeDrawingPoint(p1.index, currentPoint.price);
4606
+ } else if (pointIndex === 3) {
4607
+ pts[0] = normalizeDrawingPoint(p0.index, currentPoint.price);
4608
+ pts[1] = normalizeDrawingPoint(currentPoint.index, p1.price);
4609
+ }
4610
+ return { ...drawing, points: pts };
4611
+ }
4515
4612
  if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray")) {
4516
4613
  const pointIndex = drawingDragState.pointIndex ?? 0;
4517
4614
  const anchor = drawing.points[pointIndex === 0 ? 1 : 0];
@@ -4780,7 +4877,7 @@ function createChart(element, options = {}) {
4780
4877
  setCrosshairPoint(null);
4781
4878
  return;
4782
4879
  }
4783
- if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
4880
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "rectangle" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
4784
4881
  let nextPoint = drawingPointFromCanvas(point.x, point.y);
4785
4882
  if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray") && draftDrawing.points[0]) {
4786
4883
  nextPoint = constrainAngleFromAnchor(draftDrawing.points[0], point.x, point.y) ?? nextPoint;
package/dist/index.d.cts CHANGED
@@ -44,7 +44,7 @@ interface ChartOptions {
44
44
  drawings?: DrawingObjectOptions[];
45
45
  }
46
46
  type IndicatorPane = "overlay" | "separate";
47
- type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range" | "text" | "note";
47
+ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range" | "rectangle" | "text" | "note";
48
48
  interface DrawingPoint {
49
49
  index: number;
50
50
  price: number;
package/dist/index.d.ts CHANGED
@@ -44,7 +44,7 @@ interface ChartOptions {
44
44
  drawings?: DrawingObjectOptions[];
45
45
  }
46
46
  type IndicatorPane = "overlay" | "separate";
47
- type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range" | "text" | "note";
47
+ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range" | "rectangle" | "text" | "note";
48
48
  interface DrawingPoint {
49
49
  index: number;
50
50
  price: number;
package/dist/index.js CHANGED
@@ -1213,7 +1213,15 @@ function createChart(element, options = {}) {
1213
1213
  const minSpan = minVisibleBars;
1214
1214
  const maxSpan = Math.min(maxVisibleBars, Math.max(minSpan, count + maxPanBars * 2));
1215
1215
  xSpan = clamp(xSpan, minSpan, maxSpan);
1216
- xCenter = clamp(xCenter, -maxPanBars, count + maxPanBars);
1216
+ const halfSpan = xSpan / 2;
1217
+ const minOverlap = Math.max(1, Math.min(count, Math.ceil(xSpan * 0.1)));
1218
+ const lowCenter = minOverlap - halfSpan;
1219
+ const highCenter = count - minOverlap + halfSpan;
1220
+ xCenter = clamp(
1221
+ xCenter,
1222
+ Math.max(lowCenter, -maxPanBars),
1223
+ Math.min(highCenter, count + maxPanBars)
1224
+ );
1217
1225
  };
1218
1226
  const fitXViewport = () => {
1219
1227
  const count = data.length;
@@ -2640,6 +2648,37 @@ function createChart(element, options = {}) {
2640
2648
  }
2641
2649
  }
2642
2650
  }
2651
+ } else if (drawing.type === "rectangle") {
2652
+ const p0 = drawing.points[0];
2653
+ const p1 = drawing.points[1];
2654
+ if (p0 && p1) {
2655
+ const px0 = xFromDrawingPoint(p0);
2656
+ const px1 = xFromDrawingPoint(p1);
2657
+ const leftX = Math.min(px0, px1);
2658
+ const rightX = Math.max(px0, px1);
2659
+ const y0 = yFromPrice(p0.price);
2660
+ const y1 = yFromPrice(p1.price);
2661
+ const topY = Math.min(y0, y1);
2662
+ const botY = Math.max(y0, y1);
2663
+ const boxW = Math.max(1, rightX - leftX);
2664
+ const boxH = Math.max(1, botY - topY);
2665
+ ctx.save();
2666
+ ctx.globalAlpha = draft ? 0.6 : 1;
2667
+ ctx.fillStyle = hexToRgba(drawing.color, 0.14);
2668
+ ctx.fillRect(leftX, topY, boxW, boxH);
2669
+ ctx.lineWidth = Math.max(1, drawing.width);
2670
+ ctx.strokeStyle = drawing.color;
2671
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2672
+ ctx.strokeRect(crisp(leftX), crisp(topY), crisp(rightX) - crisp(leftX), crisp(botY) - crisp(topY));
2673
+ ctx.restore();
2674
+ handleAt(px0, y0, drawing.color);
2675
+ handleAt(px1, y1, drawing.color);
2676
+ handleAt(px0, y1, drawing.color);
2677
+ handleAt(px1, y0, drawing.color);
2678
+ if (drawing.label) {
2679
+ drawDrawingLabel(drawing.label, (leftX + rightX) / 2, topY - 4, drawing.color);
2680
+ }
2681
+ }
2643
2682
  } else if (drawing.type === "price-range") {
2644
2683
  const p0 = drawing.points[0];
2645
2684
  const p1 = drawing.points[1];
@@ -4129,6 +4168,21 @@ function createChart(element, options = {}) {
4129
4168
  if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
4130
4169
  return { drawing, target: "line" };
4131
4170
  }
4171
+ } else if (drawing.type === "rectangle") {
4172
+ const p0 = drawing.points[0];
4173
+ const p1 = drawing.points[1];
4174
+ if (!p0 || !p1) continue;
4175
+ const px0 = canvasXFromDrawingPoint(p0);
4176
+ const px1 = canvasXFromDrawingPoint(p1);
4177
+ const y0 = canvasYFromDrawingPrice(p0.price);
4178
+ const y1 = canvasYFromDrawingPrice(p1.price);
4179
+ if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
4180
+ if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
4181
+ if (Math.hypot(x - px0, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 2 };
4182
+ if (Math.hypot(x - px1, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 3 };
4183
+ if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
4184
+ return { drawing, target: "line" };
4185
+ }
4132
4186
  } else if (drawing.type === "price-range") {
4133
4187
  const p0 = drawing.points[0];
4134
4188
  const p1 = drawing.points[1];
@@ -4385,6 +4439,30 @@ function createChart(element, options = {}) {
4385
4439
  drawingEditTextHandler?.({ drawing: serializeDrawing(created), target: "line", x, y });
4386
4440
  return true;
4387
4441
  }
4442
+ if (activeDrawingTool === "rectangle") {
4443
+ if (draftDrawing?.type === "rectangle") {
4444
+ const completed = normalizeDrawingState({
4445
+ ...serializeDrawing(draftDrawing),
4446
+ points: [draftDrawing.points[0], point]
4447
+ });
4448
+ drawings.push(completed);
4449
+ draftDrawing = null;
4450
+ activeDrawingTool = null;
4451
+ emitDrawingsChange();
4452
+ scheduleDraw();
4453
+ return true;
4454
+ }
4455
+ const defaults = getDrawingToolDefaults("rectangle");
4456
+ draftDrawing = normalizeDrawingState({
4457
+ type: "rectangle",
4458
+ points: [point, point],
4459
+ color: defaults.color ?? "#2962ff",
4460
+ style: defaults.style ?? "solid",
4461
+ width: defaults.width ?? 1
4462
+ });
4463
+ scheduleDraw();
4464
+ return true;
4465
+ }
4388
4466
  if (activeDrawingTool === "price-range") {
4389
4467
  const tick = getConfiguredTickSize();
4390
4468
  const visibleRange = drawState.yMax - drawState.yMin;
@@ -4486,6 +4564,25 @@ function createChart(element, options = {}) {
4486
4564
  }
4487
4565
  return { ...drawing, points: pts };
4488
4566
  }
4567
+ if (drawingDragState.target === "handle" && drawing.type === "rectangle") {
4568
+ const pts = drawing.points.map((point) => ({ ...point }));
4569
+ const p0 = pts[0];
4570
+ const p1 = pts[1];
4571
+ if (!p0 || !p1) return drawing;
4572
+ const pointIndex = drawingDragState.pointIndex ?? 0;
4573
+ if (pointIndex === 0) {
4574
+ pts[0] = normalizeDrawingPoint(currentPoint.index, currentPoint.price);
4575
+ } else if (pointIndex === 1) {
4576
+ pts[1] = normalizeDrawingPoint(currentPoint.index, currentPoint.price);
4577
+ } else if (pointIndex === 2) {
4578
+ pts[0] = normalizeDrawingPoint(currentPoint.index, p0.price);
4579
+ pts[1] = normalizeDrawingPoint(p1.index, currentPoint.price);
4580
+ } else if (pointIndex === 3) {
4581
+ pts[0] = normalizeDrawingPoint(p0.index, currentPoint.price);
4582
+ pts[1] = normalizeDrawingPoint(currentPoint.index, p1.price);
4583
+ }
4584
+ return { ...drawing, points: pts };
4585
+ }
4489
4586
  if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray")) {
4490
4587
  const pointIndex = drawingDragState.pointIndex ?? 0;
4491
4588
  const anchor = drawing.points[pointIndex === 0 ? 1 : 0];
@@ -4754,7 +4851,7 @@ function createChart(element, options = {}) {
4754
4851
  setCrosshairPoint(null);
4755
4852
  return;
4756
4853
  }
4757
- if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
4854
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "rectangle" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
4758
4855
  let nextPoint = drawingPointFromCanvas(point.x, point.y);
4759
4856
  if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray") && draftDrawing.points[0]) {
4760
4857
  nextPoint = constrainAngleFromAnchor(draftDrawing.points[0], point.x, point.y) ?? nextPoint;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.114",
3
+ "version": "0.1.116",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",