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.
- package/dist/hyperprop-charting-library.cjs +106 -0
- package/dist/hyperprop-charting-library.d.ts +1 -1
- package/dist/hyperprop-charting-library.js +106 -0
- package/dist/index.cjs +106 -0
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +106 -0
- package/docs/API.md +1 -1
- package/package.json +1 -1
|
@@ -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`
|