hyperprop-charting-library 0.1.83 → 0.1.84

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.
@@ -2561,6 +2561,80 @@ function createChart(element, options = {}) {
2561
2561
  }
2562
2562
  }
2563
2563
  }
2564
+ } else if (drawing.type === "price-range") {
2565
+ const p0 = drawing.points[0];
2566
+ const p1 = drawing.points[1];
2567
+ if (p0 && p1) {
2568
+ const px0 = xFromDrawingPoint(p0);
2569
+ const px1 = xFromDrawingPoint(p1);
2570
+ const leftX = Math.min(px0, px1);
2571
+ const rightX = Math.max(px0, px1);
2572
+ const boxW = Math.max(1, rightX - leftX);
2573
+ const y0 = yFromPrice(p0.price);
2574
+ const y1 = yFromPrice(p1.price);
2575
+ const topY = Math.min(y0, y1);
2576
+ const botY = Math.max(y0, y1);
2577
+ ctx.save();
2578
+ ctx.globalAlpha = draft ? 0.6 : 1;
2579
+ ctx.fillStyle = hexToRgba(drawing.color, 0.14);
2580
+ ctx.fillRect(leftX, topY, boxW, Math.max(0, botY - topY));
2581
+ ctx.setLineDash([]);
2582
+ ctx.lineWidth = Math.max(1, drawing.width);
2583
+ ctx.strokeStyle = drawing.color;
2584
+ ctx.beginPath();
2585
+ ctx.moveTo(crisp(leftX), crisp(topY));
2586
+ ctx.lineTo(crisp(rightX), crisp(topY));
2587
+ ctx.moveTo(crisp(leftX), crisp(botY));
2588
+ ctx.lineTo(crisp(rightX), crisp(botY));
2589
+ ctx.stroke();
2590
+ const midX = (leftX + rightX) / 2;
2591
+ const down = p1.price <= p0.price;
2592
+ const arrowToY = down ? botY : topY;
2593
+ ctx.beginPath();
2594
+ ctx.moveTo(crisp(midX), crisp(down ? topY : botY));
2595
+ ctx.lineTo(crisp(midX), crisp(arrowToY));
2596
+ ctx.stroke();
2597
+ const head = 6;
2598
+ ctx.beginPath();
2599
+ ctx.moveTo(midX, arrowToY);
2600
+ ctx.lineTo(midX - head, arrowToY + (down ? -head : head));
2601
+ ctx.moveTo(midX, arrowToY);
2602
+ ctx.lineTo(midX + head, arrowToY + (down ? -head : head));
2603
+ ctx.stroke();
2604
+ ctx.restore();
2605
+ handleAt(px0, y0, drawing.color);
2606
+ handleAt(px1, y1, drawing.color);
2607
+ const tick = getConfiguredTickSize();
2608
+ const diff = p1.price - p0.price;
2609
+ const base = Math.abs(p0.price) > 0 ? Math.abs(p0.price) : 1;
2610
+ const pct = diff / base * 100;
2611
+ const ticks = tick > 0 ? Math.round(diff / tick) : 0;
2612
+ const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
2613
+ const labelText = tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`;
2614
+ const prevFont = ctx.font;
2615
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2616
+ const padding = 6;
2617
+ const textW = ctx.measureText(labelText).width;
2618
+ const pillW = textW + padding * 2;
2619
+ const pillH = 18;
2620
+ const pillX = midX - pillW / 2;
2621
+ const pillY = botY + 6;
2622
+ ctx.fillStyle = mergedOptions.backgroundColor;
2623
+ fillRoundedRect(pillX, pillY, pillW, pillH, 4);
2624
+ ctx.save();
2625
+ ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
2626
+ ctx.lineWidth = 1;
2627
+ ctx.strokeRect(crisp(pillX), crisp(pillY), pillW, pillH);
2628
+ ctx.restore();
2629
+ ctx.fillStyle = drawing.color;
2630
+ ctx.textAlign = "center";
2631
+ ctx.textBaseline = "middle";
2632
+ ctx.fillText(labelText, midX, pillY + pillH / 2);
2633
+ ctx.font = prevFont;
2634
+ if (drawing.label) {
2635
+ drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
2636
+ }
2637
+ }
2564
2638
  }
2565
2639
  ctx.restore();
2566
2640
  };
@@ -3796,6 +3870,19 @@ function createChart(element, options = {}) {
3796
3870
  if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
3797
3871
  return { drawing, target: "line" };
3798
3872
  }
3873
+ } else if (drawing.type === "price-range") {
3874
+ const p0 = drawing.points[0];
3875
+ const p1 = drawing.points[1];
3876
+ if (!p0 || !p1) continue;
3877
+ const px0 = canvasXFromDrawingPoint(p0);
3878
+ const px1 = canvasXFromDrawingPoint(p1);
3879
+ const y0 = canvasYFromDrawingPrice(p0.price);
3880
+ const y1 = canvasYFromDrawingPrice(p1.price);
3881
+ if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
3882
+ if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
3883
+ if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
3884
+ return { drawing, target: "line" };
3885
+ }
3799
3886
  }
3800
3887
  }
3801
3888
  return null;
@@ -3996,6 +4083,25 @@ function createChart(element, options = {}) {
3996
4083
  draw();
3997
4084
  return true;
3998
4085
  }
4086
+ if (activeDrawingTool === "price-range") {
4087
+ const tick = getConfiguredTickSize();
4088
+ const visibleRange = drawState.yMax - drawState.yMin;
4089
+ const priceOffset = visibleRange > 0 ? visibleRange * 0.18 : tick > 0 ? tick * 80 : Math.abs(point.price) * 0.03 || 1;
4090
+ const width2 = Math.max(8, Math.round(xSpan * 0.2));
4091
+ const defaults = getDrawingToolDefaults(activeDrawingTool);
4092
+ drawings.push(
4093
+ normalizeDrawingState({
4094
+ type: activeDrawingTool,
4095
+ points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
4096
+ color: defaults.color ?? "#2962ff",
4097
+ style: defaults.style ?? "solid",
4098
+ width: defaults.width ?? 1
4099
+ })
4100
+ );
4101
+ emitDrawingsChange();
4102
+ draw();
4103
+ return true;
4104
+ }
3999
4105
  if (activeDrawingTool === "long-position" || activeDrawingTool === "short-position") {
4000
4106
  const isLong = activeDrawingTool === "long-position";
4001
4107
  const tick = getConfiguredTickSize();
@@ -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";
47
+ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range";
48
48
  interface DrawingPoint {
49
49
  index: number;
50
50
  price: number;
@@ -2535,6 +2535,80 @@ function createChart(element, options = {}) {
2535
2535
  }
2536
2536
  }
2537
2537
  }
2538
+ } else if (drawing.type === "price-range") {
2539
+ const p0 = drawing.points[0];
2540
+ const p1 = drawing.points[1];
2541
+ if (p0 && p1) {
2542
+ const px0 = xFromDrawingPoint(p0);
2543
+ const px1 = xFromDrawingPoint(p1);
2544
+ const leftX = Math.min(px0, px1);
2545
+ const rightX = Math.max(px0, px1);
2546
+ const boxW = Math.max(1, rightX - leftX);
2547
+ const y0 = yFromPrice(p0.price);
2548
+ const y1 = yFromPrice(p1.price);
2549
+ const topY = Math.min(y0, y1);
2550
+ const botY = Math.max(y0, y1);
2551
+ ctx.save();
2552
+ ctx.globalAlpha = draft ? 0.6 : 1;
2553
+ ctx.fillStyle = hexToRgba(drawing.color, 0.14);
2554
+ ctx.fillRect(leftX, topY, boxW, Math.max(0, botY - topY));
2555
+ ctx.setLineDash([]);
2556
+ ctx.lineWidth = Math.max(1, drawing.width);
2557
+ ctx.strokeStyle = drawing.color;
2558
+ ctx.beginPath();
2559
+ ctx.moveTo(crisp(leftX), crisp(topY));
2560
+ ctx.lineTo(crisp(rightX), crisp(topY));
2561
+ ctx.moveTo(crisp(leftX), crisp(botY));
2562
+ ctx.lineTo(crisp(rightX), crisp(botY));
2563
+ ctx.stroke();
2564
+ const midX = (leftX + rightX) / 2;
2565
+ const down = p1.price <= p0.price;
2566
+ const arrowToY = down ? botY : topY;
2567
+ ctx.beginPath();
2568
+ ctx.moveTo(crisp(midX), crisp(down ? topY : botY));
2569
+ ctx.lineTo(crisp(midX), crisp(arrowToY));
2570
+ ctx.stroke();
2571
+ const head = 6;
2572
+ ctx.beginPath();
2573
+ ctx.moveTo(midX, arrowToY);
2574
+ ctx.lineTo(midX - head, arrowToY + (down ? -head : head));
2575
+ ctx.moveTo(midX, arrowToY);
2576
+ ctx.lineTo(midX + head, arrowToY + (down ? -head : head));
2577
+ ctx.stroke();
2578
+ ctx.restore();
2579
+ handleAt(px0, y0, drawing.color);
2580
+ handleAt(px1, y1, drawing.color);
2581
+ const tick = getConfiguredTickSize();
2582
+ const diff = p1.price - p0.price;
2583
+ const base = Math.abs(p0.price) > 0 ? Math.abs(p0.price) : 1;
2584
+ const pct = diff / base * 100;
2585
+ const ticks = tick > 0 ? Math.round(diff / tick) : 0;
2586
+ const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
2587
+ const labelText = tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`;
2588
+ const prevFont = ctx.font;
2589
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2590
+ const padding = 6;
2591
+ const textW = ctx.measureText(labelText).width;
2592
+ const pillW = textW + padding * 2;
2593
+ const pillH = 18;
2594
+ const pillX = midX - pillW / 2;
2595
+ const pillY = botY + 6;
2596
+ ctx.fillStyle = mergedOptions.backgroundColor;
2597
+ fillRoundedRect(pillX, pillY, pillW, pillH, 4);
2598
+ ctx.save();
2599
+ ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
2600
+ ctx.lineWidth = 1;
2601
+ ctx.strokeRect(crisp(pillX), crisp(pillY), pillW, pillH);
2602
+ ctx.restore();
2603
+ ctx.fillStyle = drawing.color;
2604
+ ctx.textAlign = "center";
2605
+ ctx.textBaseline = "middle";
2606
+ ctx.fillText(labelText, midX, pillY + pillH / 2);
2607
+ ctx.font = prevFont;
2608
+ if (drawing.label) {
2609
+ drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
2610
+ }
2611
+ }
2538
2612
  }
2539
2613
  ctx.restore();
2540
2614
  };
@@ -3770,6 +3844,19 @@ function createChart(element, options = {}) {
3770
3844
  if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
3771
3845
  return { drawing, target: "line" };
3772
3846
  }
3847
+ } else if (drawing.type === "price-range") {
3848
+ const p0 = drawing.points[0];
3849
+ const p1 = drawing.points[1];
3850
+ if (!p0 || !p1) continue;
3851
+ const px0 = canvasXFromDrawingPoint(p0);
3852
+ const px1 = canvasXFromDrawingPoint(p1);
3853
+ const y0 = canvasYFromDrawingPrice(p0.price);
3854
+ const y1 = canvasYFromDrawingPrice(p1.price);
3855
+ if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
3856
+ if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
3857
+ if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
3858
+ return { drawing, target: "line" };
3859
+ }
3773
3860
  }
3774
3861
  }
3775
3862
  return null;
@@ -3970,6 +4057,25 @@ function createChart(element, options = {}) {
3970
4057
  draw();
3971
4058
  return true;
3972
4059
  }
4060
+ if (activeDrawingTool === "price-range") {
4061
+ const tick = getConfiguredTickSize();
4062
+ const visibleRange = drawState.yMax - drawState.yMin;
4063
+ const priceOffset = visibleRange > 0 ? visibleRange * 0.18 : tick > 0 ? tick * 80 : Math.abs(point.price) * 0.03 || 1;
4064
+ const width2 = Math.max(8, Math.round(xSpan * 0.2));
4065
+ const defaults = getDrawingToolDefaults(activeDrawingTool);
4066
+ drawings.push(
4067
+ normalizeDrawingState({
4068
+ type: activeDrawingTool,
4069
+ points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
4070
+ color: defaults.color ?? "#2962ff",
4071
+ style: defaults.style ?? "solid",
4072
+ width: defaults.width ?? 1
4073
+ })
4074
+ );
4075
+ emitDrawingsChange();
4076
+ draw();
4077
+ return true;
4078
+ }
3973
4079
  if (activeDrawingTool === "long-position" || activeDrawingTool === "short-position") {
3974
4080
  const isLong = activeDrawingTool === "long-position";
3975
4081
  const tick = getConfiguredTickSize();
package/dist/index.cjs CHANGED
@@ -2561,6 +2561,80 @@ function createChart(element, options = {}) {
2561
2561
  }
2562
2562
  }
2563
2563
  }
2564
+ } else if (drawing.type === "price-range") {
2565
+ const p0 = drawing.points[0];
2566
+ const p1 = drawing.points[1];
2567
+ if (p0 && p1) {
2568
+ const px0 = xFromDrawingPoint(p0);
2569
+ const px1 = xFromDrawingPoint(p1);
2570
+ const leftX = Math.min(px0, px1);
2571
+ const rightX = Math.max(px0, px1);
2572
+ const boxW = Math.max(1, rightX - leftX);
2573
+ const y0 = yFromPrice(p0.price);
2574
+ const y1 = yFromPrice(p1.price);
2575
+ const topY = Math.min(y0, y1);
2576
+ const botY = Math.max(y0, y1);
2577
+ ctx.save();
2578
+ ctx.globalAlpha = draft ? 0.6 : 1;
2579
+ ctx.fillStyle = hexToRgba(drawing.color, 0.14);
2580
+ ctx.fillRect(leftX, topY, boxW, Math.max(0, botY - topY));
2581
+ ctx.setLineDash([]);
2582
+ ctx.lineWidth = Math.max(1, drawing.width);
2583
+ ctx.strokeStyle = drawing.color;
2584
+ ctx.beginPath();
2585
+ ctx.moveTo(crisp(leftX), crisp(topY));
2586
+ ctx.lineTo(crisp(rightX), crisp(topY));
2587
+ ctx.moveTo(crisp(leftX), crisp(botY));
2588
+ ctx.lineTo(crisp(rightX), crisp(botY));
2589
+ ctx.stroke();
2590
+ const midX = (leftX + rightX) / 2;
2591
+ const down = p1.price <= p0.price;
2592
+ const arrowToY = down ? botY : topY;
2593
+ ctx.beginPath();
2594
+ ctx.moveTo(crisp(midX), crisp(down ? topY : botY));
2595
+ ctx.lineTo(crisp(midX), crisp(arrowToY));
2596
+ ctx.stroke();
2597
+ const head = 6;
2598
+ ctx.beginPath();
2599
+ ctx.moveTo(midX, arrowToY);
2600
+ ctx.lineTo(midX - head, arrowToY + (down ? -head : head));
2601
+ ctx.moveTo(midX, arrowToY);
2602
+ ctx.lineTo(midX + head, arrowToY + (down ? -head : head));
2603
+ ctx.stroke();
2604
+ ctx.restore();
2605
+ handleAt(px0, y0, drawing.color);
2606
+ handleAt(px1, y1, drawing.color);
2607
+ const tick = getConfiguredTickSize();
2608
+ const diff = p1.price - p0.price;
2609
+ const base = Math.abs(p0.price) > 0 ? Math.abs(p0.price) : 1;
2610
+ const pct = diff / base * 100;
2611
+ const ticks = tick > 0 ? Math.round(diff / tick) : 0;
2612
+ const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
2613
+ const labelText = tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`;
2614
+ const prevFont = ctx.font;
2615
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2616
+ const padding = 6;
2617
+ const textW = ctx.measureText(labelText).width;
2618
+ const pillW = textW + padding * 2;
2619
+ const pillH = 18;
2620
+ const pillX = midX - pillW / 2;
2621
+ const pillY = botY + 6;
2622
+ ctx.fillStyle = mergedOptions.backgroundColor;
2623
+ fillRoundedRect(pillX, pillY, pillW, pillH, 4);
2624
+ ctx.save();
2625
+ ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
2626
+ ctx.lineWidth = 1;
2627
+ ctx.strokeRect(crisp(pillX), crisp(pillY), pillW, pillH);
2628
+ ctx.restore();
2629
+ ctx.fillStyle = drawing.color;
2630
+ ctx.textAlign = "center";
2631
+ ctx.textBaseline = "middle";
2632
+ ctx.fillText(labelText, midX, pillY + pillH / 2);
2633
+ ctx.font = prevFont;
2634
+ if (drawing.label) {
2635
+ drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
2636
+ }
2637
+ }
2564
2638
  }
2565
2639
  ctx.restore();
2566
2640
  };
@@ -3796,6 +3870,19 @@ function createChart(element, options = {}) {
3796
3870
  if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
3797
3871
  return { drawing, target: "line" };
3798
3872
  }
3873
+ } else if (drawing.type === "price-range") {
3874
+ const p0 = drawing.points[0];
3875
+ const p1 = drawing.points[1];
3876
+ if (!p0 || !p1) continue;
3877
+ const px0 = canvasXFromDrawingPoint(p0);
3878
+ const px1 = canvasXFromDrawingPoint(p1);
3879
+ const y0 = canvasYFromDrawingPrice(p0.price);
3880
+ const y1 = canvasYFromDrawingPrice(p1.price);
3881
+ if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
3882
+ if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
3883
+ if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
3884
+ return { drawing, target: "line" };
3885
+ }
3799
3886
  }
3800
3887
  }
3801
3888
  return null;
@@ -3996,6 +4083,25 @@ function createChart(element, options = {}) {
3996
4083
  draw();
3997
4084
  return true;
3998
4085
  }
4086
+ if (activeDrawingTool === "price-range") {
4087
+ const tick = getConfiguredTickSize();
4088
+ const visibleRange = drawState.yMax - drawState.yMin;
4089
+ const priceOffset = visibleRange > 0 ? visibleRange * 0.18 : tick > 0 ? tick * 80 : Math.abs(point.price) * 0.03 || 1;
4090
+ const width2 = Math.max(8, Math.round(xSpan * 0.2));
4091
+ const defaults = getDrawingToolDefaults(activeDrawingTool);
4092
+ drawings.push(
4093
+ normalizeDrawingState({
4094
+ type: activeDrawingTool,
4095
+ points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
4096
+ color: defaults.color ?? "#2962ff",
4097
+ style: defaults.style ?? "solid",
4098
+ width: defaults.width ?? 1
4099
+ })
4100
+ );
4101
+ emitDrawingsChange();
4102
+ draw();
4103
+ return true;
4104
+ }
3999
4105
  if (activeDrawingTool === "long-position" || activeDrawingTool === "short-position") {
4000
4106
  const isLong = activeDrawingTool === "long-position";
4001
4107
  const tick = getConfiguredTickSize();
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";
47
+ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range";
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";
47
+ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range";
48
48
  interface DrawingPoint {
49
49
  index: number;
50
50
  price: number;
package/dist/index.js CHANGED
@@ -2535,6 +2535,80 @@ function createChart(element, options = {}) {
2535
2535
  }
2536
2536
  }
2537
2537
  }
2538
+ } else if (drawing.type === "price-range") {
2539
+ const p0 = drawing.points[0];
2540
+ const p1 = drawing.points[1];
2541
+ if (p0 && p1) {
2542
+ const px0 = xFromDrawingPoint(p0);
2543
+ const px1 = xFromDrawingPoint(p1);
2544
+ const leftX = Math.min(px0, px1);
2545
+ const rightX = Math.max(px0, px1);
2546
+ const boxW = Math.max(1, rightX - leftX);
2547
+ const y0 = yFromPrice(p0.price);
2548
+ const y1 = yFromPrice(p1.price);
2549
+ const topY = Math.min(y0, y1);
2550
+ const botY = Math.max(y0, y1);
2551
+ ctx.save();
2552
+ ctx.globalAlpha = draft ? 0.6 : 1;
2553
+ ctx.fillStyle = hexToRgba(drawing.color, 0.14);
2554
+ ctx.fillRect(leftX, topY, boxW, Math.max(0, botY - topY));
2555
+ ctx.setLineDash([]);
2556
+ ctx.lineWidth = Math.max(1, drawing.width);
2557
+ ctx.strokeStyle = drawing.color;
2558
+ ctx.beginPath();
2559
+ ctx.moveTo(crisp(leftX), crisp(topY));
2560
+ ctx.lineTo(crisp(rightX), crisp(topY));
2561
+ ctx.moveTo(crisp(leftX), crisp(botY));
2562
+ ctx.lineTo(crisp(rightX), crisp(botY));
2563
+ ctx.stroke();
2564
+ const midX = (leftX + rightX) / 2;
2565
+ const down = p1.price <= p0.price;
2566
+ const arrowToY = down ? botY : topY;
2567
+ ctx.beginPath();
2568
+ ctx.moveTo(crisp(midX), crisp(down ? topY : botY));
2569
+ ctx.lineTo(crisp(midX), crisp(arrowToY));
2570
+ ctx.stroke();
2571
+ const head = 6;
2572
+ ctx.beginPath();
2573
+ ctx.moveTo(midX, arrowToY);
2574
+ ctx.lineTo(midX - head, arrowToY + (down ? -head : head));
2575
+ ctx.moveTo(midX, arrowToY);
2576
+ ctx.lineTo(midX + head, arrowToY + (down ? -head : head));
2577
+ ctx.stroke();
2578
+ ctx.restore();
2579
+ handleAt(px0, y0, drawing.color);
2580
+ handleAt(px1, y1, drawing.color);
2581
+ const tick = getConfiguredTickSize();
2582
+ const diff = p1.price - p0.price;
2583
+ const base = Math.abs(p0.price) > 0 ? Math.abs(p0.price) : 1;
2584
+ const pct = diff / base * 100;
2585
+ const ticks = tick > 0 ? Math.round(diff / tick) : 0;
2586
+ const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
2587
+ const labelText = tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`;
2588
+ const prevFont = ctx.font;
2589
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2590
+ const padding = 6;
2591
+ const textW = ctx.measureText(labelText).width;
2592
+ const pillW = textW + padding * 2;
2593
+ const pillH = 18;
2594
+ const pillX = midX - pillW / 2;
2595
+ const pillY = botY + 6;
2596
+ ctx.fillStyle = mergedOptions.backgroundColor;
2597
+ fillRoundedRect(pillX, pillY, pillW, pillH, 4);
2598
+ ctx.save();
2599
+ ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
2600
+ ctx.lineWidth = 1;
2601
+ ctx.strokeRect(crisp(pillX), crisp(pillY), pillW, pillH);
2602
+ ctx.restore();
2603
+ ctx.fillStyle = drawing.color;
2604
+ ctx.textAlign = "center";
2605
+ ctx.textBaseline = "middle";
2606
+ ctx.fillText(labelText, midX, pillY + pillH / 2);
2607
+ ctx.font = prevFont;
2608
+ if (drawing.label) {
2609
+ drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
2610
+ }
2611
+ }
2538
2612
  }
2539
2613
  ctx.restore();
2540
2614
  };
@@ -3770,6 +3844,19 @@ function createChart(element, options = {}) {
3770
3844
  if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
3771
3845
  return { drawing, target: "line" };
3772
3846
  }
3847
+ } else if (drawing.type === "price-range") {
3848
+ const p0 = drawing.points[0];
3849
+ const p1 = drawing.points[1];
3850
+ if (!p0 || !p1) continue;
3851
+ const px0 = canvasXFromDrawingPoint(p0);
3852
+ const px1 = canvasXFromDrawingPoint(p1);
3853
+ const y0 = canvasYFromDrawingPrice(p0.price);
3854
+ const y1 = canvasYFromDrawingPrice(p1.price);
3855
+ if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
3856
+ if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
3857
+ if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
3858
+ return { drawing, target: "line" };
3859
+ }
3773
3860
  }
3774
3861
  }
3775
3862
  return null;
@@ -3970,6 +4057,25 @@ function createChart(element, options = {}) {
3970
4057
  draw();
3971
4058
  return true;
3972
4059
  }
4060
+ if (activeDrawingTool === "price-range") {
4061
+ const tick = getConfiguredTickSize();
4062
+ const visibleRange = drawState.yMax - drawState.yMin;
4063
+ const priceOffset = visibleRange > 0 ? visibleRange * 0.18 : tick > 0 ? tick * 80 : Math.abs(point.price) * 0.03 || 1;
4064
+ const width2 = Math.max(8, Math.round(xSpan * 0.2));
4065
+ const defaults = getDrawingToolDefaults(activeDrawingTool);
4066
+ drawings.push(
4067
+ normalizeDrawingState({
4068
+ type: activeDrawingTool,
4069
+ points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
4070
+ color: defaults.color ?? "#2962ff",
4071
+ style: defaults.style ?? "solid",
4072
+ width: defaults.width ?? 1
4073
+ })
4074
+ );
4075
+ emitDrawingsChange();
4076
+ draw();
4077
+ return true;
4078
+ }
3973
4079
  if (activeDrawingTool === "long-position" || activeDrawingTool === "short-position") {
3974
4080
  const isLong = activeDrawingTool === "long-position";
3975
4081
  const tick = getConfiguredTickSize();
package/docs/API.md CHANGED
@@ -422,7 +422,7 @@ Drawings are user-created chart tools, separate from indicators. They are intera
422
422
  - `ray`: two-point line that extends infinitely past the second point
423
423
  - `fib-retracement`: two-point retracement with levels/bands
424
424
  - `fib-extension`: trend-based three-point extension (click trend start, trend end, then the projection origin); levels project from the third point by the first→second move
425
- - `long-position` / `short-position`: risk/reward forecasting box (single click drops a default box). Points are `[entry, target, stop, rightEdge]`; anchors are target/entry/stop on the left and a time-width handle on the right. Labels show price, % move, ticks, and risk/reward ratio. `color` sets the entry line; `colors` is `[profitColor, lossColor, labelTextColor]` (defaults `POSITION_DEFAULT_COLORS`). Sizing inputs `accountSize`, `lotSize`, `risk`, `riskMode` (`"percent"|"amount"`), `leverage`, `pointValue`, `qtyPrecision` drive the Qty/Amount labels — set `pointValue` (contract $/point) from your app for real money values. The box also runs a trade simulation across the bars it covers: it shades the traversed region and draws a diagonal to the first bar that touches the target or stop, and the center label switches to "Closed P&L" (green if the target was hit first, red if the stop was hit first).
425
+ - `long-position` / `short-position`: risk/reward forecasting box (single click drops a default box). Points are `[entry, target, stop, rightEdge]`; anchors are target/entry/stop on the left and a time-width handle on the right. Labels show price, % move, ticks, and risk/reward ratio. `color` sets the entry line; `colors` is `[profitColor, lossColor, labelTextColor]` (defaults `POSITION_DEFAULT_COLORS`). Sizing inputs `accountSize`, `lotSize`, `risk`, `riskMode` (`"percent"|"amount"`), `leverage`, `pointValue`, `qtyPrecision` drive the Qty/Amount labels — set `pointValue` (contract $/point) from your app for real money values. A `price-range` tool (single click drops a default box; two diagonal anchors) measures a region and labels the signed price change, % move and ticks with a vertical arrow. The box also runs a trade simulation across the bars it covers: it shades the traversed region and draws a diagonal to the first bar that touches the target or stop, and the center label switches to "Closed P&L" (green if the target was hit first, red if the stop was hit first).
426
426
  - `points: DrawingPoint[]`
427
427
  - `visible?: boolean`
428
428
  - `color?: string`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.83",
3
+ "version": "0.1.84",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",