hyperprop-charting-library 0.1.146 → 0.1.148
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 +260 -93
- package/dist/hyperprop-charting-library.d.ts +13 -1
- package/dist/hyperprop-charting-library.js +260 -93
- package/dist/index.cjs +260 -93
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +260 -93
- package/docs/API.md +85 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2072,8 +2072,8 @@ var BUILTIN_DONCHIAN_INDICATOR = {
|
|
|
2072
2072
|
}
|
|
2073
2073
|
};
|
|
2074
2074
|
var computeVolumeProfile = (data, fromIndex, toIndex, rowCount, valueAreaPercent) => {
|
|
2075
|
-
const from = Math.max(0, fromIndex);
|
|
2076
|
-
const to = Math.min(data.length - 1, toIndex);
|
|
2075
|
+
const from = Math.max(0, Math.round(fromIndex));
|
|
2076
|
+
const to = Math.min(data.length - 1, Math.round(toIndex));
|
|
2077
2077
|
if (to < from) return null;
|
|
2078
2078
|
let low = Number.POSITIVE_INFINITY;
|
|
2079
2079
|
let high = Number.NEGATIVE_INFINITY;
|
|
@@ -3932,6 +3932,9 @@ function createChart(element, options = {}) {
|
|
|
3932
3932
|
pointValue: Number(drawing.pointValue) || 1,
|
|
3933
3933
|
qtyPrecision: Number.isFinite(drawing.qtyPrecision) ? Math.max(0, Math.floor(Number(drawing.qtyPrecision))) : 0,
|
|
3934
3934
|
fontSize: Math.max(6, Number(drawing.fontSize) || 14),
|
|
3935
|
+
profileRows: Math.max(4, Math.min(200, Math.round(Number(drawing.profileRows)) || 24)),
|
|
3936
|
+
profileValueAreaPercent: Math.max(10, Math.min(99, Number(drawing.profileValueAreaPercent) || 70)),
|
|
3937
|
+
profileWidthRatio: Math.max(0.1, Math.min(1, Number(drawing.profileWidthRatio) || 0.5)),
|
|
3935
3938
|
...drawing.label === void 0 ? {} : { label: drawing.label }
|
|
3936
3939
|
};
|
|
3937
3940
|
};
|
|
@@ -3965,6 +3968,9 @@ function createChart(element, options = {}) {
|
|
|
3965
3968
|
pointValue: drawing.pointValue,
|
|
3966
3969
|
qtyPrecision: drawing.qtyPrecision,
|
|
3967
3970
|
fontSize: drawing.fontSize,
|
|
3971
|
+
profileRows: drawing.profileRows,
|
|
3972
|
+
profileValueAreaPercent: drawing.profileValueAreaPercent,
|
|
3973
|
+
profileWidthRatio: drawing.profileWidthRatio,
|
|
3968
3974
|
...drawing.label === void 0 ? {} : { label: drawing.label }
|
|
3969
3975
|
});
|
|
3970
3976
|
let tradeMarkers = [];
|
|
@@ -4291,6 +4297,8 @@ function createChart(element, options = {}) {
|
|
|
4291
4297
|
let paneButtonRegions = [];
|
|
4292
4298
|
let hoveredPaneId = null;
|
|
4293
4299
|
let hoveredPaneButton = null;
|
|
4300
|
+
let legendRowRegions = [];
|
|
4301
|
+
let hoveredLegendId = null;
|
|
4294
4302
|
let paneDividerDrag = null;
|
|
4295
4303
|
let indicatorPaneActionHandler = null;
|
|
4296
4304
|
let indicatorPaneHeightChangeHandler = null;
|
|
@@ -5587,6 +5595,85 @@ function createChart(element, options = {}) {
|
|
|
5587
5595
|
ctx.closePath();
|
|
5588
5596
|
ctx.stroke();
|
|
5589
5597
|
};
|
|
5598
|
+
const INDICATOR_CONTROL_SIZE = 16;
|
|
5599
|
+
const INDICATOR_CONTROL_GAP = 4;
|
|
5600
|
+
const LEGEND_CONTROLS_WIDTH = (INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP) * 4 + 8;
|
|
5601
|
+
const drawIndicatorControls = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
|
|
5602
|
+
const labels = resolvedLabels;
|
|
5603
|
+
const actionList = [
|
|
5604
|
+
"visibility",
|
|
5605
|
+
"settings",
|
|
5606
|
+
"source",
|
|
5607
|
+
"remove"
|
|
5608
|
+
];
|
|
5609
|
+
let buttonX = startX;
|
|
5610
|
+
for (const action of actionList) {
|
|
5611
|
+
const buttonY = centerY - INDICATOR_CONTROL_SIZE / 2;
|
|
5612
|
+
const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
|
|
5613
|
+
const iconColor = labels.indicatorTextColor;
|
|
5614
|
+
ctx.save();
|
|
5615
|
+
ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
|
|
5616
|
+
fillRoundedRect(Math.round(buttonX), Math.round(buttonY), INDICATOR_CONTROL_SIZE, INDICATOR_CONTROL_SIZE, 4);
|
|
5617
|
+
ctx.strokeStyle = iconColor;
|
|
5618
|
+
ctx.fillStyle = iconColor;
|
|
5619
|
+
ctx.globalAlpha = hovered ? 1 : 0.75;
|
|
5620
|
+
ctx.lineWidth = 1.2;
|
|
5621
|
+
ctx.setLineDash([]);
|
|
5622
|
+
const cx = buttonX + INDICATOR_CONTROL_SIZE / 2;
|
|
5623
|
+
const cy = centerY;
|
|
5624
|
+
if (action === "visibility") {
|
|
5625
|
+
ctx.beginPath();
|
|
5626
|
+
ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
|
|
5627
|
+
ctx.stroke();
|
|
5628
|
+
ctx.beginPath();
|
|
5629
|
+
ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
|
|
5630
|
+
ctx.fill();
|
|
5631
|
+
if (!indicatorVisible) {
|
|
5632
|
+
ctx.beginPath();
|
|
5633
|
+
ctx.moveTo(cx - 5.2, cy + 4.6);
|
|
5634
|
+
ctx.lineTo(cx + 5.2, cy - 4.6);
|
|
5635
|
+
ctx.stroke();
|
|
5636
|
+
}
|
|
5637
|
+
} else if (action === "settings") {
|
|
5638
|
+
ctx.beginPath();
|
|
5639
|
+
ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
|
|
5640
|
+
ctx.stroke();
|
|
5641
|
+
for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
|
|
5642
|
+
const angle = Math.PI / 4 * toothIndex;
|
|
5643
|
+
ctx.beginPath();
|
|
5644
|
+
ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
|
|
5645
|
+
ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
|
|
5646
|
+
ctx.stroke();
|
|
5647
|
+
}
|
|
5648
|
+
ctx.beginPath();
|
|
5649
|
+
ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
|
|
5650
|
+
ctx.fill();
|
|
5651
|
+
} else if (action === "source") {
|
|
5652
|
+
const prevSourceFont = ctx.font;
|
|
5653
|
+
ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
|
|
5654
|
+
drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
|
|
5655
|
+
ctx.font = prevSourceFont;
|
|
5656
|
+
} else {
|
|
5657
|
+
ctx.beginPath();
|
|
5658
|
+
ctx.moveTo(cx - 3.4, cy - 3.4);
|
|
5659
|
+
ctx.lineTo(cx + 3.4, cy + 3.4);
|
|
5660
|
+
ctx.moveTo(cx + 3.4, cy - 3.4);
|
|
5661
|
+
ctx.lineTo(cx - 3.4, cy + 3.4);
|
|
5662
|
+
ctx.stroke();
|
|
5663
|
+
}
|
|
5664
|
+
ctx.restore();
|
|
5665
|
+
paneButtonRegions.push({
|
|
5666
|
+
id: indicatorId,
|
|
5667
|
+
type: indicatorType,
|
|
5668
|
+
action,
|
|
5669
|
+
x: buttonX,
|
|
5670
|
+
y: buttonY,
|
|
5671
|
+
width: INDICATOR_CONTROL_SIZE,
|
|
5672
|
+
height: INDICATOR_CONTROL_SIZE
|
|
5673
|
+
});
|
|
5674
|
+
buttonX += INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP;
|
|
5675
|
+
}
|
|
5676
|
+
};
|
|
5590
5677
|
const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
|
|
5591
5678
|
ctx.save();
|
|
5592
5679
|
ctx.fillStyle = color;
|
|
@@ -5667,6 +5754,8 @@ function createChart(element, options = {}) {
|
|
|
5667
5754
|
orderDragRegions = [];
|
|
5668
5755
|
alertRegions = [];
|
|
5669
5756
|
markRegions = [];
|
|
5757
|
+
paneButtonRegions = [];
|
|
5758
|
+
legendRowRegions = [];
|
|
5670
5759
|
crosshairPriceActionRegion = null;
|
|
5671
5760
|
const pixelRatio = getPixelRatio();
|
|
5672
5761
|
canvas.style.width = `${width}px`;
|
|
@@ -6671,6 +6760,81 @@ function createChart(element, options = {}) {
|
|
|
6671
6760
|
handleAt(ax, ay, drawing.color);
|
|
6672
6761
|
handleAt(bx, by, drawing.color);
|
|
6673
6762
|
}
|
|
6763
|
+
} else if (drawing.type === "fixed-range-volume-profile") {
|
|
6764
|
+
const p0 = drawing.points[0];
|
|
6765
|
+
const p1 = drawing.points[1];
|
|
6766
|
+
if (p0 && p1 && data.length > 0) {
|
|
6767
|
+
const fromIndex = Math.max(0, Math.round(Math.min(p0.index, p1.index)));
|
|
6768
|
+
const toIndex = Math.min(data.length - 1, Math.round(Math.max(p0.index, p1.index)));
|
|
6769
|
+
const leftX = xFromDrawingPoint(p0.index <= p1.index ? p0 : p1) - candleSpacing / 2;
|
|
6770
|
+
const rightX = xFromDrawingPoint(p0.index <= p1.index ? p1 : p0) + candleSpacing / 2;
|
|
6771
|
+
const rangeWidth = Math.max(1, rightX - leftX);
|
|
6772
|
+
const rows = Math.max(4, Math.min(200, Math.round(drawing.profileRows) || 24));
|
|
6773
|
+
const valueAreaPercent = Math.max(10, Math.min(99, Number(drawing.profileValueAreaPercent) || 70));
|
|
6774
|
+
const profile = toIndex >= fromIndex ? computeVolumeProfile(data, fromIndex, toIndex, rows, valueAreaPercent) : null;
|
|
6775
|
+
ctx.save();
|
|
6776
|
+
ctx.globalAlpha = draft ? 0.5 : 1;
|
|
6777
|
+
ctx.fillStyle = hexToRgba(drawing.color, 0.06);
|
|
6778
|
+
ctx.fillRect(leftX, chartTop, rangeWidth, chartBottom - chartTop);
|
|
6779
|
+
ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
|
|
6780
|
+
ctx.lineWidth = 1;
|
|
6781
|
+
ctx.setLineDash([4, 3]);
|
|
6782
|
+
ctx.beginPath();
|
|
6783
|
+
ctx.moveTo(crisp(leftX), chartTop);
|
|
6784
|
+
ctx.lineTo(crisp(leftX), chartBottom);
|
|
6785
|
+
ctx.moveTo(crisp(rightX), chartTop);
|
|
6786
|
+
ctx.lineTo(crisp(rightX), chartBottom);
|
|
6787
|
+
ctx.stroke();
|
|
6788
|
+
ctx.setLineDash([]);
|
|
6789
|
+
if (profile && profile.maxRowVolume > 0) {
|
|
6790
|
+
const widthRatio = Math.max(0.1, Math.min(1, Number(drawing.profileWidthRatio) || 0.5));
|
|
6791
|
+
const maxWidth = rangeWidth * widthRatio;
|
|
6792
|
+
for (let row = 0; row < profile.rows.length; row += 1) {
|
|
6793
|
+
const bucket = profile.rows[row];
|
|
6794
|
+
if (bucket.total <= 0) continue;
|
|
6795
|
+
const yTop = yFromPrice(bucket.high);
|
|
6796
|
+
const yBottom = yFromPrice(bucket.low);
|
|
6797
|
+
const top = Math.min(yTop, yBottom);
|
|
6798
|
+
const height2 = Math.max(1, Math.abs(yBottom - yTop) - 1);
|
|
6799
|
+
if (top > chartBottom || top + height2 < chartTop) continue;
|
|
6800
|
+
const inValueArea = row >= profile.valueAreaFrom && row <= profile.valueAreaTo;
|
|
6801
|
+
ctx.globalAlpha = (draft ? 0.5 : 1) * (inValueArea ? 0.5 : 0.2);
|
|
6802
|
+
const rowWidth = bucket.total / profile.maxRowVolume * maxWidth;
|
|
6803
|
+
const upWidth = rowWidth * (bucket.up / bucket.total);
|
|
6804
|
+
ctx.fillStyle = mergedOptions.upColor;
|
|
6805
|
+
ctx.fillRect(leftX, top, upWidth, height2);
|
|
6806
|
+
ctx.fillStyle = mergedOptions.downColor;
|
|
6807
|
+
ctx.fillRect(leftX + upWidth, top, rowWidth - upWidth, height2);
|
|
6808
|
+
}
|
|
6809
|
+
ctx.globalAlpha = draft ? 0.6 : 1;
|
|
6810
|
+
const poc = profile.rows[profile.pocIndex];
|
|
6811
|
+
const drawProfileLevel = (price, color, label, dashed) => {
|
|
6812
|
+
const y = yFromPrice(price);
|
|
6813
|
+
if (y < chartTop || y > chartBottom) return;
|
|
6814
|
+
ctx.strokeStyle = color;
|
|
6815
|
+
ctx.lineWidth = 1;
|
|
6816
|
+
ctx.setLineDash(dashed ? [4, 3] : []);
|
|
6817
|
+
ctx.beginPath();
|
|
6818
|
+
ctx.moveTo(leftX, crisp(y));
|
|
6819
|
+
ctx.lineTo(leftX + maxWidth, crisp(y));
|
|
6820
|
+
ctx.stroke();
|
|
6821
|
+
ctx.setLineDash([]);
|
|
6822
|
+
const prevFont = ctx.font;
|
|
6823
|
+
ctx.font = `10px ${mergedOptions.fontFamily}`;
|
|
6824
|
+
ctx.fillStyle = color;
|
|
6825
|
+
ctx.textAlign = "left";
|
|
6826
|
+
ctx.textBaseline = "bottom";
|
|
6827
|
+
ctx.fillText(label, leftX + 3, y - 1);
|
|
6828
|
+
ctx.font = prevFont;
|
|
6829
|
+
};
|
|
6830
|
+
drawProfileLevel(profile.rows[profile.valueAreaTo].high, "#7e8aa3", "VAH", true);
|
|
6831
|
+
drawProfileLevel(profile.rows[profile.valueAreaFrom].low, "#7e8aa3", "VAL", true);
|
|
6832
|
+
drawProfileLevel((poc.low + poc.high) / 2, "#f7a600", "POC", false);
|
|
6833
|
+
}
|
|
6834
|
+
ctx.restore();
|
|
6835
|
+
handleAt(leftX, yFromPrice(p0.index <= p1.index ? p0.price : p1.price), drawing.color);
|
|
6836
|
+
handleAt(rightX, yFromPrice(p0.index <= p1.index ? p1.price : p0.price), drawing.color);
|
|
6837
|
+
}
|
|
6674
6838
|
} else if (drawing.type === "price-range") {
|
|
6675
6839
|
const p0 = drawing.points[0];
|
|
6676
6840
|
const p1 = drawing.points[1];
|
|
@@ -7680,87 +7844,10 @@ function createChart(element, options = {}) {
|
|
|
7680
7844
|
}
|
|
7681
7845
|
paneLayoutFullChartHeight = fullChartHeight;
|
|
7682
7846
|
paneLayoutInfos = [];
|
|
7683
|
-
paneButtonRegions = [];
|
|
7684
7847
|
if (activeSeparateIndicators.length > 0) {
|
|
7685
7848
|
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
7686
7849
|
let paneTopCursor = chartBottom + paneGap;
|
|
7687
|
-
const
|
|
7688
|
-
const paneButtonGap = 4;
|
|
7689
|
-
const drawPaneButtons = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
|
|
7690
|
-
const actionList = [
|
|
7691
|
-
"visibility",
|
|
7692
|
-
"settings",
|
|
7693
|
-
"source",
|
|
7694
|
-
"remove"
|
|
7695
|
-
];
|
|
7696
|
-
let buttonX = startX;
|
|
7697
|
-
for (const action of actionList) {
|
|
7698
|
-
const buttonY = centerY - paneButtonSize / 2;
|
|
7699
|
-
const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
|
|
7700
|
-
const iconColor = labels.indicatorTextColor;
|
|
7701
|
-
ctx.save();
|
|
7702
|
-
ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
|
|
7703
|
-
fillRoundedRect(Math.round(buttonX), Math.round(buttonY), paneButtonSize, paneButtonSize, 4);
|
|
7704
|
-
ctx.strokeStyle = iconColor;
|
|
7705
|
-
ctx.fillStyle = iconColor;
|
|
7706
|
-
ctx.globalAlpha = hovered ? 1 : 0.75;
|
|
7707
|
-
ctx.lineWidth = 1.2;
|
|
7708
|
-
ctx.setLineDash([]);
|
|
7709
|
-
const cx = buttonX + paneButtonSize / 2;
|
|
7710
|
-
const cy = centerY;
|
|
7711
|
-
if (action === "visibility") {
|
|
7712
|
-
ctx.beginPath();
|
|
7713
|
-
ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
|
|
7714
|
-
ctx.stroke();
|
|
7715
|
-
ctx.beginPath();
|
|
7716
|
-
ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
|
|
7717
|
-
ctx.fill();
|
|
7718
|
-
if (!indicatorVisible) {
|
|
7719
|
-
ctx.beginPath();
|
|
7720
|
-
ctx.moveTo(cx - 5.2, cy + 4.6);
|
|
7721
|
-
ctx.lineTo(cx + 5.2, cy - 4.6);
|
|
7722
|
-
ctx.stroke();
|
|
7723
|
-
}
|
|
7724
|
-
} else if (action === "settings") {
|
|
7725
|
-
ctx.beginPath();
|
|
7726
|
-
ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
|
|
7727
|
-
ctx.stroke();
|
|
7728
|
-
for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
|
|
7729
|
-
const angle = Math.PI / 4 * toothIndex;
|
|
7730
|
-
ctx.beginPath();
|
|
7731
|
-
ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
|
|
7732
|
-
ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
|
|
7733
|
-
ctx.stroke();
|
|
7734
|
-
}
|
|
7735
|
-
ctx.beginPath();
|
|
7736
|
-
ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
|
|
7737
|
-
ctx.fill();
|
|
7738
|
-
} else if (action === "source") {
|
|
7739
|
-
const prevSourceFont = ctx.font;
|
|
7740
|
-
ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
|
|
7741
|
-
drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
|
|
7742
|
-
ctx.font = prevSourceFont;
|
|
7743
|
-
} else {
|
|
7744
|
-
ctx.beginPath();
|
|
7745
|
-
ctx.moveTo(cx - 3.4, cy - 3.4);
|
|
7746
|
-
ctx.lineTo(cx + 3.4, cy + 3.4);
|
|
7747
|
-
ctx.moveTo(cx + 3.4, cy - 3.4);
|
|
7748
|
-
ctx.lineTo(cx - 3.4, cy + 3.4);
|
|
7749
|
-
ctx.stroke();
|
|
7750
|
-
}
|
|
7751
|
-
ctx.restore();
|
|
7752
|
-
paneButtonRegions.push({
|
|
7753
|
-
id: indicatorId,
|
|
7754
|
-
type: indicatorType,
|
|
7755
|
-
action,
|
|
7756
|
-
x: buttonX,
|
|
7757
|
-
y: buttonY,
|
|
7758
|
-
width: paneButtonSize,
|
|
7759
|
-
height: paneButtonSize
|
|
7760
|
-
});
|
|
7761
|
-
buttonX += paneButtonSize + paneButtonGap;
|
|
7762
|
-
}
|
|
7763
|
-
};
|
|
7850
|
+
const drawPaneButtons = drawIndicatorControls;
|
|
7764
7851
|
activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
|
|
7765
7852
|
const paneHeight = separatePaneHeights[paneIndex] ?? 80;
|
|
7766
7853
|
const paneTop = paneTopCursor;
|
|
@@ -8262,26 +8349,56 @@ function createChart(element, options = {}) {
|
|
|
8262
8349
|
};
|
|
8263
8350
|
const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
|
|
8264
8351
|
const inputValues = Object.entries(indicator.inputs).filter(([key, value]) => !LEGEND_EXCLUDED_INPUT_KEYS.has(key) && typeof value !== "boolean" && isLegendInputValue(value)).slice(0, 2).map(([, value]) => formatLegendValue(value));
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
if (labels.showIndicatorNames) {
|
|
8269
|
-
return plugin.name;
|
|
8270
|
-
}
|
|
8271
|
-
return inputValues.join(" ");
|
|
8272
|
-
}).filter((entry) => entry.length > 0);
|
|
8352
|
+
const text = labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0 ? `${plugin.name} ${inputValues.join(" ")}` : labels.showIndicatorNames ? plugin.name : inputValues.join(" ");
|
|
8353
|
+
return { id: indicator.id, type: indicator.type, visible: indicator.visible, text };
|
|
8354
|
+
}).filter((entry) => entry.text.length > 0);
|
|
8273
8355
|
if (labelEntries.length > 0) {
|
|
8274
8356
|
const prevFont = ctx.font;
|
|
8275
|
-
|
|
8276
|
-
|
|
8357
|
+
const legendFontSize = Math.max(8, axis.fontSize);
|
|
8358
|
+
ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
|
|
8277
8359
|
const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
|
|
8278
8360
|
const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
|
|
8279
8361
|
const position = labels.indicatorLegendPosition;
|
|
8280
8362
|
const isRight = position === "top-right" || position === "bottom-right";
|
|
8281
8363
|
const isBottom = position === "bottom-left" || position === "bottom-right";
|
|
8282
8364
|
const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
|
|
8283
|
-
const
|
|
8284
|
-
|
|
8365
|
+
const legendTop = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
|
|
8366
|
+
const rowHeight = Math.round(legendFontSize * 1.55);
|
|
8367
|
+
labelEntries.forEach((entry, rowIndex) => {
|
|
8368
|
+
const rowTop = isBottom ? legendTop - (labelEntries.length - rowIndex) * rowHeight : legendTop + rowIndex * rowHeight;
|
|
8369
|
+
const rowCenter = rowTop + rowHeight / 2;
|
|
8370
|
+
const dimmed = entry.visible === false;
|
|
8371
|
+
ctx.save();
|
|
8372
|
+
if (dimmed) ctx.globalAlpha = 0.45;
|
|
8373
|
+
drawText(
|
|
8374
|
+
entry.text,
|
|
8375
|
+
legendX,
|
|
8376
|
+
rowCenter,
|
|
8377
|
+
isRight ? "right" : "left",
|
|
8378
|
+
"middle",
|
|
8379
|
+
labels.indicatorTextColor
|
|
8380
|
+
);
|
|
8381
|
+
ctx.restore();
|
|
8382
|
+
const textWidth = measureTextWidth(entry.text);
|
|
8383
|
+
const rowLeft = isRight ? legendX - textWidth : legendX;
|
|
8384
|
+
legendRowRegions.push({
|
|
8385
|
+
id: entry.id,
|
|
8386
|
+
type: entry.type,
|
|
8387
|
+
x: rowLeft,
|
|
8388
|
+
y: rowTop,
|
|
8389
|
+
width: textWidth + LEGEND_CONTROLS_WIDTH,
|
|
8390
|
+
height: rowHeight
|
|
8391
|
+
});
|
|
8392
|
+
if (hoveredLegendId === entry.id) {
|
|
8393
|
+
drawIndicatorControls(
|
|
8394
|
+
entry.id,
|
|
8395
|
+
entry.type,
|
|
8396
|
+
entry.visible !== false,
|
|
8397
|
+
isRight ? legendX + 6 : rowLeft + textWidth + 8,
|
|
8398
|
+
rowCenter
|
|
8399
|
+
);
|
|
8400
|
+
}
|
|
8401
|
+
});
|
|
8285
8402
|
ctx.font = prevFont;
|
|
8286
8403
|
}
|
|
8287
8404
|
}
|
|
@@ -8953,6 +9070,9 @@ function createChart(element, options = {}) {
|
|
|
8953
9070
|
if (hits.length === 0) return null;
|
|
8954
9071
|
return hits.find((region) => region.part === "remove") ?? hits[0];
|
|
8955
9072
|
};
|
|
9073
|
+
const getLegendRowAt = (x, y) => legendRowRegions.find(
|
|
9074
|
+
(region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
|
|
9075
|
+
) ?? null;
|
|
8956
9076
|
const getMarkAt = (x, y) => {
|
|
8957
9077
|
const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
|
|
8958
9078
|
for (let index = markRegions.length - 1; index >= 0; index -= 1) {
|
|
@@ -9443,6 +9563,19 @@ function createChart(element, options = {}) {
|
|
|
9443
9563
|
if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5 * hitTolerance) {
|
|
9444
9564
|
return { drawing, target: "line" };
|
|
9445
9565
|
}
|
|
9566
|
+
} else if (drawing.type === "fixed-range-volume-profile") {
|
|
9567
|
+
const p0 = drawing.points[0];
|
|
9568
|
+
const p1 = drawing.points[1];
|
|
9569
|
+
if (!p0 || !p1) continue;
|
|
9570
|
+
const px0 = canvasXFromDrawingPoint(p0);
|
|
9571
|
+
const px1 = canvasXFromDrawingPoint(p1);
|
|
9572
|
+
const y0 = canvasYFromDrawingPrice(p0.price);
|
|
9573
|
+
const y1 = canvasYFromDrawingPrice(p1.price);
|
|
9574
|
+
if (Math.hypot(x - px0, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
|
|
9575
|
+
if (Math.hypot(x - px1, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
|
|
9576
|
+
if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1)) {
|
|
9577
|
+
return { drawing, target: "line" };
|
|
9578
|
+
}
|
|
9446
9579
|
} else if (drawing.type === "price-range") {
|
|
9447
9580
|
const p0 = drawing.points[0];
|
|
9448
9581
|
const p1 = drawing.points[1];
|
|
@@ -9894,6 +10027,33 @@ function createChart(element, options = {}) {
|
|
|
9894
10027
|
scheduleDraw();
|
|
9895
10028
|
return true;
|
|
9896
10029
|
}
|
|
10030
|
+
if (activeDrawingTool === "fixed-range-volume-profile") {
|
|
10031
|
+
if (draftDrawing?.type === "fixed-range-volume-profile") {
|
|
10032
|
+
const completed = normalizeDrawingState({
|
|
10033
|
+
...serializeDrawing(draftDrawing),
|
|
10034
|
+
points: [draftDrawing.points[0], point]
|
|
10035
|
+
});
|
|
10036
|
+
drawings.push(completed);
|
|
10037
|
+
draftDrawing = null;
|
|
10038
|
+
activeDrawingTool = null;
|
|
10039
|
+
emitDrawingsChange();
|
|
10040
|
+
scheduleDraw();
|
|
10041
|
+
return true;
|
|
10042
|
+
}
|
|
10043
|
+
const defaults = getDrawingToolDefaults("fixed-range-volume-profile");
|
|
10044
|
+
draftDrawing = normalizeDrawingState({
|
|
10045
|
+
type: "fixed-range-volume-profile",
|
|
10046
|
+
points: [point, point],
|
|
10047
|
+
color: defaults.color ?? "#5b8def",
|
|
10048
|
+
style: defaults.style ?? "solid",
|
|
10049
|
+
width: defaults.width ?? 1,
|
|
10050
|
+
...defaults.profileRows === void 0 ? {} : { profileRows: defaults.profileRows },
|
|
10051
|
+
...defaults.profileValueAreaPercent === void 0 ? {} : { profileValueAreaPercent: defaults.profileValueAreaPercent },
|
|
10052
|
+
...defaults.profileWidthRatio === void 0 ? {} : { profileWidthRatio: defaults.profileWidthRatio }
|
|
10053
|
+
});
|
|
10054
|
+
scheduleDraw();
|
|
10055
|
+
return true;
|
|
10056
|
+
}
|
|
9897
10057
|
if (activeDrawingTool === "price-range") {
|
|
9898
10058
|
const tick = getConfiguredTickSize();
|
|
9899
10059
|
const visibleRange = drawState.yMax - drawState.yMin;
|
|
@@ -10662,6 +10822,12 @@ function createChart(element, options = {}) {
|
|
|
10662
10822
|
setCrosshairPoint(null);
|
|
10663
10823
|
return;
|
|
10664
10824
|
}
|
|
10825
|
+
const legendRow = getLegendRowAt(point.x, point.y);
|
|
10826
|
+
const nextLegendId = legendRow?.id ?? null;
|
|
10827
|
+
if (nextLegendId !== hoveredLegendId) {
|
|
10828
|
+
hoveredLegendId = nextLegendId;
|
|
10829
|
+
scheduleDraw();
|
|
10830
|
+
}
|
|
10665
10831
|
const hoveredPane = getPaneAt(point.x, point.y);
|
|
10666
10832
|
const nextHoveredPaneId = hoveredPane?.id ?? null;
|
|
10667
10833
|
if (nextHoveredPaneId !== hoveredPaneId) {
|
|
@@ -10880,10 +11046,11 @@ function createChart(element, options = {}) {
|
|
|
10880
11046
|
});
|
|
10881
11047
|
paneDividerDrag = null;
|
|
10882
11048
|
}
|
|
10883
|
-
if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null)) {
|
|
11049
|
+
if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null || hoveredLegendId !== null)) {
|
|
10884
11050
|
hoveredPaneId = null;
|
|
10885
11051
|
hoveredPaneButton = null;
|
|
10886
11052
|
hoveredAlertId = null;
|
|
11053
|
+
hoveredLegendId = null;
|
|
10887
11054
|
if (hoveredMarkKey !== null) {
|
|
10888
11055
|
hoveredMarkKey = null;
|
|
10889
11056
|
markHoverHandler?.(null);
|
package/dist/index.d.cts
CHANGED
|
@@ -120,6 +120,12 @@ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray"
|
|
|
120
120
|
| "brush"
|
|
121
121
|
/** Text box with a connector line pointing at an anchored bar/price. */
|
|
122
122
|
| "callout"
|
|
123
|
+
/**
|
|
124
|
+
* Volume profile over a bar range picked by two clicks (TradingView's
|
|
125
|
+
* fixed-range volume profile). Only the two anchors' bar indexes matter —
|
|
126
|
+
* the price extent comes from the bars they enclose.
|
|
127
|
+
*/
|
|
128
|
+
| "fixed-range-volume-profile"
|
|
123
129
|
/**
|
|
124
130
|
* TradingView-style ruler. Unlike every other tool it is transient: the
|
|
125
131
|
* measurement overlay is never added to the drawings list (no
|
|
@@ -157,6 +163,12 @@ interface DrawingObjectOptions {
|
|
|
157
163
|
pointValue?: number;
|
|
158
164
|
qtyPrecision?: number;
|
|
159
165
|
fontSize?: number;
|
|
166
|
+
/** Price buckets across the range. Default 24. */
|
|
167
|
+
profileRows?: number;
|
|
168
|
+
/** Share of volume enclosed by the value area. Default 70. */
|
|
169
|
+
profileValueAreaPercent?: number;
|
|
170
|
+
/** Profile width as a share of the selected range. Default 0.5. */
|
|
171
|
+
profileWidthRatio?: number;
|
|
160
172
|
}
|
|
161
173
|
/** Default colors for position tools: [profit, loss, label text]. */
|
|
162
174
|
declare const POSITION_DEFAULT_COLORS: string[];
|
|
@@ -220,7 +232,7 @@ interface DrawingHoverEvent {
|
|
|
220
232
|
x: number;
|
|
221
233
|
y: number;
|
|
222
234
|
}
|
|
223
|
-
type DrawingDefaults = Partial<Pick<DrawingObjectOptions, "color" | "colors" | "style" | "width" | "accountSize" | "lotSize" | "risk" | "riskMode" | "leverage" | "pointValue" | "qtyPrecision" | "fontSize">>;
|
|
235
|
+
type DrawingDefaults = Partial<Pick<DrawingObjectOptions, "color" | "colors" | "style" | "width" | "accountSize" | "lotSize" | "risk" | "riskMode" | "leverage" | "pointValue" | "qtyPrecision" | "fontSize" | "profileRows" | "profileValueAreaPercent" | "profileWidthRatio">>;
|
|
224
236
|
interface IndicatorInstanceOptions<TInputs extends Record<string, unknown> = Record<string, unknown>> {
|
|
225
237
|
id?: string;
|
|
226
238
|
type: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -120,6 +120,12 @@ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray"
|
|
|
120
120
|
| "brush"
|
|
121
121
|
/** Text box with a connector line pointing at an anchored bar/price. */
|
|
122
122
|
| "callout"
|
|
123
|
+
/**
|
|
124
|
+
* Volume profile over a bar range picked by two clicks (TradingView's
|
|
125
|
+
* fixed-range volume profile). Only the two anchors' bar indexes matter —
|
|
126
|
+
* the price extent comes from the bars they enclose.
|
|
127
|
+
*/
|
|
128
|
+
| "fixed-range-volume-profile"
|
|
123
129
|
/**
|
|
124
130
|
* TradingView-style ruler. Unlike every other tool it is transient: the
|
|
125
131
|
* measurement overlay is never added to the drawings list (no
|
|
@@ -157,6 +163,12 @@ interface DrawingObjectOptions {
|
|
|
157
163
|
pointValue?: number;
|
|
158
164
|
qtyPrecision?: number;
|
|
159
165
|
fontSize?: number;
|
|
166
|
+
/** Price buckets across the range. Default 24. */
|
|
167
|
+
profileRows?: number;
|
|
168
|
+
/** Share of volume enclosed by the value area. Default 70. */
|
|
169
|
+
profileValueAreaPercent?: number;
|
|
170
|
+
/** Profile width as a share of the selected range. Default 0.5. */
|
|
171
|
+
profileWidthRatio?: number;
|
|
160
172
|
}
|
|
161
173
|
/** Default colors for position tools: [profit, loss, label text]. */
|
|
162
174
|
declare const POSITION_DEFAULT_COLORS: string[];
|
|
@@ -220,7 +232,7 @@ interface DrawingHoverEvent {
|
|
|
220
232
|
x: number;
|
|
221
233
|
y: number;
|
|
222
234
|
}
|
|
223
|
-
type DrawingDefaults = Partial<Pick<DrawingObjectOptions, "color" | "colors" | "style" | "width" | "accountSize" | "lotSize" | "risk" | "riskMode" | "leverage" | "pointValue" | "qtyPrecision" | "fontSize">>;
|
|
235
|
+
type DrawingDefaults = Partial<Pick<DrawingObjectOptions, "color" | "colors" | "style" | "width" | "accountSize" | "lotSize" | "risk" | "riskMode" | "leverage" | "pointValue" | "qtyPrecision" | "fontSize" | "profileRows" | "profileValueAreaPercent" | "profileWidthRatio">>;
|
|
224
236
|
interface IndicatorInstanceOptions<TInputs extends Record<string, unknown> = Record<string, unknown>> {
|
|
225
237
|
id?: string;
|
|
226
238
|
type: string;
|