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.js
CHANGED
|
@@ -2036,8 +2036,8 @@ var BUILTIN_DONCHIAN_INDICATOR = {
|
|
|
2036
2036
|
}
|
|
2037
2037
|
};
|
|
2038
2038
|
var computeVolumeProfile = (data, fromIndex, toIndex, rowCount, valueAreaPercent) => {
|
|
2039
|
-
const from = Math.max(0, fromIndex);
|
|
2040
|
-
const to = Math.min(data.length - 1, toIndex);
|
|
2039
|
+
const from = Math.max(0, Math.round(fromIndex));
|
|
2040
|
+
const to = Math.min(data.length - 1, Math.round(toIndex));
|
|
2041
2041
|
if (to < from) return null;
|
|
2042
2042
|
let low = Number.POSITIVE_INFINITY;
|
|
2043
2043
|
let high = Number.NEGATIVE_INFINITY;
|
|
@@ -3896,6 +3896,9 @@ function createChart(element, options = {}) {
|
|
|
3896
3896
|
pointValue: Number(drawing.pointValue) || 1,
|
|
3897
3897
|
qtyPrecision: Number.isFinite(drawing.qtyPrecision) ? Math.max(0, Math.floor(Number(drawing.qtyPrecision))) : 0,
|
|
3898
3898
|
fontSize: Math.max(6, Number(drawing.fontSize) || 14),
|
|
3899
|
+
profileRows: Math.max(4, Math.min(200, Math.round(Number(drawing.profileRows)) || 24)),
|
|
3900
|
+
profileValueAreaPercent: Math.max(10, Math.min(99, Number(drawing.profileValueAreaPercent) || 70)),
|
|
3901
|
+
profileWidthRatio: Math.max(0.1, Math.min(1, Number(drawing.profileWidthRatio) || 0.5)),
|
|
3899
3902
|
...drawing.label === void 0 ? {} : { label: drawing.label }
|
|
3900
3903
|
};
|
|
3901
3904
|
};
|
|
@@ -3929,6 +3932,9 @@ function createChart(element, options = {}) {
|
|
|
3929
3932
|
pointValue: drawing.pointValue,
|
|
3930
3933
|
qtyPrecision: drawing.qtyPrecision,
|
|
3931
3934
|
fontSize: drawing.fontSize,
|
|
3935
|
+
profileRows: drawing.profileRows,
|
|
3936
|
+
profileValueAreaPercent: drawing.profileValueAreaPercent,
|
|
3937
|
+
profileWidthRatio: drawing.profileWidthRatio,
|
|
3932
3938
|
...drawing.label === void 0 ? {} : { label: drawing.label }
|
|
3933
3939
|
});
|
|
3934
3940
|
let tradeMarkers = [];
|
|
@@ -4255,6 +4261,8 @@ function createChart(element, options = {}) {
|
|
|
4255
4261
|
let paneButtonRegions = [];
|
|
4256
4262
|
let hoveredPaneId = null;
|
|
4257
4263
|
let hoveredPaneButton = null;
|
|
4264
|
+
let legendRowRegions = [];
|
|
4265
|
+
let hoveredLegendId = null;
|
|
4258
4266
|
let paneDividerDrag = null;
|
|
4259
4267
|
let indicatorPaneActionHandler = null;
|
|
4260
4268
|
let indicatorPaneHeightChangeHandler = null;
|
|
@@ -5551,6 +5559,85 @@ function createChart(element, options = {}) {
|
|
|
5551
5559
|
ctx.closePath();
|
|
5552
5560
|
ctx.stroke();
|
|
5553
5561
|
};
|
|
5562
|
+
const INDICATOR_CONTROL_SIZE = 16;
|
|
5563
|
+
const INDICATOR_CONTROL_GAP = 4;
|
|
5564
|
+
const LEGEND_CONTROLS_WIDTH = (INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP) * 4 + 8;
|
|
5565
|
+
const drawIndicatorControls = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
|
|
5566
|
+
const labels = resolvedLabels;
|
|
5567
|
+
const actionList = [
|
|
5568
|
+
"visibility",
|
|
5569
|
+
"settings",
|
|
5570
|
+
"source",
|
|
5571
|
+
"remove"
|
|
5572
|
+
];
|
|
5573
|
+
let buttonX = startX;
|
|
5574
|
+
for (const action of actionList) {
|
|
5575
|
+
const buttonY = centerY - INDICATOR_CONTROL_SIZE / 2;
|
|
5576
|
+
const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
|
|
5577
|
+
const iconColor = labels.indicatorTextColor;
|
|
5578
|
+
ctx.save();
|
|
5579
|
+
ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
|
|
5580
|
+
fillRoundedRect(Math.round(buttonX), Math.round(buttonY), INDICATOR_CONTROL_SIZE, INDICATOR_CONTROL_SIZE, 4);
|
|
5581
|
+
ctx.strokeStyle = iconColor;
|
|
5582
|
+
ctx.fillStyle = iconColor;
|
|
5583
|
+
ctx.globalAlpha = hovered ? 1 : 0.75;
|
|
5584
|
+
ctx.lineWidth = 1.2;
|
|
5585
|
+
ctx.setLineDash([]);
|
|
5586
|
+
const cx = buttonX + INDICATOR_CONTROL_SIZE / 2;
|
|
5587
|
+
const cy = centerY;
|
|
5588
|
+
if (action === "visibility") {
|
|
5589
|
+
ctx.beginPath();
|
|
5590
|
+
ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
|
|
5591
|
+
ctx.stroke();
|
|
5592
|
+
ctx.beginPath();
|
|
5593
|
+
ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
|
|
5594
|
+
ctx.fill();
|
|
5595
|
+
if (!indicatorVisible) {
|
|
5596
|
+
ctx.beginPath();
|
|
5597
|
+
ctx.moveTo(cx - 5.2, cy + 4.6);
|
|
5598
|
+
ctx.lineTo(cx + 5.2, cy - 4.6);
|
|
5599
|
+
ctx.stroke();
|
|
5600
|
+
}
|
|
5601
|
+
} else if (action === "settings") {
|
|
5602
|
+
ctx.beginPath();
|
|
5603
|
+
ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
|
|
5604
|
+
ctx.stroke();
|
|
5605
|
+
for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
|
|
5606
|
+
const angle = Math.PI / 4 * toothIndex;
|
|
5607
|
+
ctx.beginPath();
|
|
5608
|
+
ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
|
|
5609
|
+
ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
|
|
5610
|
+
ctx.stroke();
|
|
5611
|
+
}
|
|
5612
|
+
ctx.beginPath();
|
|
5613
|
+
ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
|
|
5614
|
+
ctx.fill();
|
|
5615
|
+
} else if (action === "source") {
|
|
5616
|
+
const prevSourceFont = ctx.font;
|
|
5617
|
+
ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
|
|
5618
|
+
drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
|
|
5619
|
+
ctx.font = prevSourceFont;
|
|
5620
|
+
} else {
|
|
5621
|
+
ctx.beginPath();
|
|
5622
|
+
ctx.moveTo(cx - 3.4, cy - 3.4);
|
|
5623
|
+
ctx.lineTo(cx + 3.4, cy + 3.4);
|
|
5624
|
+
ctx.moveTo(cx + 3.4, cy - 3.4);
|
|
5625
|
+
ctx.lineTo(cx - 3.4, cy + 3.4);
|
|
5626
|
+
ctx.stroke();
|
|
5627
|
+
}
|
|
5628
|
+
ctx.restore();
|
|
5629
|
+
paneButtonRegions.push({
|
|
5630
|
+
id: indicatorId,
|
|
5631
|
+
type: indicatorType,
|
|
5632
|
+
action,
|
|
5633
|
+
x: buttonX,
|
|
5634
|
+
y: buttonY,
|
|
5635
|
+
width: INDICATOR_CONTROL_SIZE,
|
|
5636
|
+
height: INDICATOR_CONTROL_SIZE
|
|
5637
|
+
});
|
|
5638
|
+
buttonX += INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP;
|
|
5639
|
+
}
|
|
5640
|
+
};
|
|
5554
5641
|
const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
|
|
5555
5642
|
ctx.save();
|
|
5556
5643
|
ctx.fillStyle = color;
|
|
@@ -5631,6 +5718,8 @@ function createChart(element, options = {}) {
|
|
|
5631
5718
|
orderDragRegions = [];
|
|
5632
5719
|
alertRegions = [];
|
|
5633
5720
|
markRegions = [];
|
|
5721
|
+
paneButtonRegions = [];
|
|
5722
|
+
legendRowRegions = [];
|
|
5634
5723
|
crosshairPriceActionRegion = null;
|
|
5635
5724
|
const pixelRatio = getPixelRatio();
|
|
5636
5725
|
canvas.style.width = `${width}px`;
|
|
@@ -6635,6 +6724,81 @@ function createChart(element, options = {}) {
|
|
|
6635
6724
|
handleAt(ax, ay, drawing.color);
|
|
6636
6725
|
handleAt(bx, by, drawing.color);
|
|
6637
6726
|
}
|
|
6727
|
+
} else if (drawing.type === "fixed-range-volume-profile") {
|
|
6728
|
+
const p0 = drawing.points[0];
|
|
6729
|
+
const p1 = drawing.points[1];
|
|
6730
|
+
if (p0 && p1 && data.length > 0) {
|
|
6731
|
+
const fromIndex = Math.max(0, Math.round(Math.min(p0.index, p1.index)));
|
|
6732
|
+
const toIndex = Math.min(data.length - 1, Math.round(Math.max(p0.index, p1.index)));
|
|
6733
|
+
const leftX = xFromDrawingPoint(p0.index <= p1.index ? p0 : p1) - candleSpacing / 2;
|
|
6734
|
+
const rightX = xFromDrawingPoint(p0.index <= p1.index ? p1 : p0) + candleSpacing / 2;
|
|
6735
|
+
const rangeWidth = Math.max(1, rightX - leftX);
|
|
6736
|
+
const rows = Math.max(4, Math.min(200, Math.round(drawing.profileRows) || 24));
|
|
6737
|
+
const valueAreaPercent = Math.max(10, Math.min(99, Number(drawing.profileValueAreaPercent) || 70));
|
|
6738
|
+
const profile = toIndex >= fromIndex ? computeVolumeProfile(data, fromIndex, toIndex, rows, valueAreaPercent) : null;
|
|
6739
|
+
ctx.save();
|
|
6740
|
+
ctx.globalAlpha = draft ? 0.5 : 1;
|
|
6741
|
+
ctx.fillStyle = hexToRgba(drawing.color, 0.06);
|
|
6742
|
+
ctx.fillRect(leftX, chartTop, rangeWidth, chartBottom - chartTop);
|
|
6743
|
+
ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
|
|
6744
|
+
ctx.lineWidth = 1;
|
|
6745
|
+
ctx.setLineDash([4, 3]);
|
|
6746
|
+
ctx.beginPath();
|
|
6747
|
+
ctx.moveTo(crisp(leftX), chartTop);
|
|
6748
|
+
ctx.lineTo(crisp(leftX), chartBottom);
|
|
6749
|
+
ctx.moveTo(crisp(rightX), chartTop);
|
|
6750
|
+
ctx.lineTo(crisp(rightX), chartBottom);
|
|
6751
|
+
ctx.stroke();
|
|
6752
|
+
ctx.setLineDash([]);
|
|
6753
|
+
if (profile && profile.maxRowVolume > 0) {
|
|
6754
|
+
const widthRatio = Math.max(0.1, Math.min(1, Number(drawing.profileWidthRatio) || 0.5));
|
|
6755
|
+
const maxWidth = rangeWidth * widthRatio;
|
|
6756
|
+
for (let row = 0; row < profile.rows.length; row += 1) {
|
|
6757
|
+
const bucket = profile.rows[row];
|
|
6758
|
+
if (bucket.total <= 0) continue;
|
|
6759
|
+
const yTop = yFromPrice(bucket.high);
|
|
6760
|
+
const yBottom = yFromPrice(bucket.low);
|
|
6761
|
+
const top = Math.min(yTop, yBottom);
|
|
6762
|
+
const height2 = Math.max(1, Math.abs(yBottom - yTop) - 1);
|
|
6763
|
+
if (top > chartBottom || top + height2 < chartTop) continue;
|
|
6764
|
+
const inValueArea = row >= profile.valueAreaFrom && row <= profile.valueAreaTo;
|
|
6765
|
+
ctx.globalAlpha = (draft ? 0.5 : 1) * (inValueArea ? 0.5 : 0.2);
|
|
6766
|
+
const rowWidth = bucket.total / profile.maxRowVolume * maxWidth;
|
|
6767
|
+
const upWidth = rowWidth * (bucket.up / bucket.total);
|
|
6768
|
+
ctx.fillStyle = mergedOptions.upColor;
|
|
6769
|
+
ctx.fillRect(leftX, top, upWidth, height2);
|
|
6770
|
+
ctx.fillStyle = mergedOptions.downColor;
|
|
6771
|
+
ctx.fillRect(leftX + upWidth, top, rowWidth - upWidth, height2);
|
|
6772
|
+
}
|
|
6773
|
+
ctx.globalAlpha = draft ? 0.6 : 1;
|
|
6774
|
+
const poc = profile.rows[profile.pocIndex];
|
|
6775
|
+
const drawProfileLevel = (price, color, label, dashed) => {
|
|
6776
|
+
const y = yFromPrice(price);
|
|
6777
|
+
if (y < chartTop || y > chartBottom) return;
|
|
6778
|
+
ctx.strokeStyle = color;
|
|
6779
|
+
ctx.lineWidth = 1;
|
|
6780
|
+
ctx.setLineDash(dashed ? [4, 3] : []);
|
|
6781
|
+
ctx.beginPath();
|
|
6782
|
+
ctx.moveTo(leftX, crisp(y));
|
|
6783
|
+
ctx.lineTo(leftX + maxWidth, crisp(y));
|
|
6784
|
+
ctx.stroke();
|
|
6785
|
+
ctx.setLineDash([]);
|
|
6786
|
+
const prevFont = ctx.font;
|
|
6787
|
+
ctx.font = `10px ${mergedOptions.fontFamily}`;
|
|
6788
|
+
ctx.fillStyle = color;
|
|
6789
|
+
ctx.textAlign = "left";
|
|
6790
|
+
ctx.textBaseline = "bottom";
|
|
6791
|
+
ctx.fillText(label, leftX + 3, y - 1);
|
|
6792
|
+
ctx.font = prevFont;
|
|
6793
|
+
};
|
|
6794
|
+
drawProfileLevel(profile.rows[profile.valueAreaTo].high, "#7e8aa3", "VAH", true);
|
|
6795
|
+
drawProfileLevel(profile.rows[profile.valueAreaFrom].low, "#7e8aa3", "VAL", true);
|
|
6796
|
+
drawProfileLevel((poc.low + poc.high) / 2, "#f7a600", "POC", false);
|
|
6797
|
+
}
|
|
6798
|
+
ctx.restore();
|
|
6799
|
+
handleAt(leftX, yFromPrice(p0.index <= p1.index ? p0.price : p1.price), drawing.color);
|
|
6800
|
+
handleAt(rightX, yFromPrice(p0.index <= p1.index ? p1.price : p0.price), drawing.color);
|
|
6801
|
+
}
|
|
6638
6802
|
} else if (drawing.type === "price-range") {
|
|
6639
6803
|
const p0 = drawing.points[0];
|
|
6640
6804
|
const p1 = drawing.points[1];
|
|
@@ -7644,87 +7808,10 @@ function createChart(element, options = {}) {
|
|
|
7644
7808
|
}
|
|
7645
7809
|
paneLayoutFullChartHeight = fullChartHeight;
|
|
7646
7810
|
paneLayoutInfos = [];
|
|
7647
|
-
paneButtonRegions = [];
|
|
7648
7811
|
if (activeSeparateIndicators.length > 0) {
|
|
7649
7812
|
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
7650
7813
|
let paneTopCursor = chartBottom + paneGap;
|
|
7651
|
-
const
|
|
7652
|
-
const paneButtonGap = 4;
|
|
7653
|
-
const drawPaneButtons = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
|
|
7654
|
-
const actionList = [
|
|
7655
|
-
"visibility",
|
|
7656
|
-
"settings",
|
|
7657
|
-
"source",
|
|
7658
|
-
"remove"
|
|
7659
|
-
];
|
|
7660
|
-
let buttonX = startX;
|
|
7661
|
-
for (const action of actionList) {
|
|
7662
|
-
const buttonY = centerY - paneButtonSize / 2;
|
|
7663
|
-
const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
|
|
7664
|
-
const iconColor = labels.indicatorTextColor;
|
|
7665
|
-
ctx.save();
|
|
7666
|
-
ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
|
|
7667
|
-
fillRoundedRect(Math.round(buttonX), Math.round(buttonY), paneButtonSize, paneButtonSize, 4);
|
|
7668
|
-
ctx.strokeStyle = iconColor;
|
|
7669
|
-
ctx.fillStyle = iconColor;
|
|
7670
|
-
ctx.globalAlpha = hovered ? 1 : 0.75;
|
|
7671
|
-
ctx.lineWidth = 1.2;
|
|
7672
|
-
ctx.setLineDash([]);
|
|
7673
|
-
const cx = buttonX + paneButtonSize / 2;
|
|
7674
|
-
const cy = centerY;
|
|
7675
|
-
if (action === "visibility") {
|
|
7676
|
-
ctx.beginPath();
|
|
7677
|
-
ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
|
|
7678
|
-
ctx.stroke();
|
|
7679
|
-
ctx.beginPath();
|
|
7680
|
-
ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
|
|
7681
|
-
ctx.fill();
|
|
7682
|
-
if (!indicatorVisible) {
|
|
7683
|
-
ctx.beginPath();
|
|
7684
|
-
ctx.moveTo(cx - 5.2, cy + 4.6);
|
|
7685
|
-
ctx.lineTo(cx + 5.2, cy - 4.6);
|
|
7686
|
-
ctx.stroke();
|
|
7687
|
-
}
|
|
7688
|
-
} else if (action === "settings") {
|
|
7689
|
-
ctx.beginPath();
|
|
7690
|
-
ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
|
|
7691
|
-
ctx.stroke();
|
|
7692
|
-
for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
|
|
7693
|
-
const angle = Math.PI / 4 * toothIndex;
|
|
7694
|
-
ctx.beginPath();
|
|
7695
|
-
ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
|
|
7696
|
-
ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
|
|
7697
|
-
ctx.stroke();
|
|
7698
|
-
}
|
|
7699
|
-
ctx.beginPath();
|
|
7700
|
-
ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
|
|
7701
|
-
ctx.fill();
|
|
7702
|
-
} else if (action === "source") {
|
|
7703
|
-
const prevSourceFont = ctx.font;
|
|
7704
|
-
ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
|
|
7705
|
-
drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
|
|
7706
|
-
ctx.font = prevSourceFont;
|
|
7707
|
-
} else {
|
|
7708
|
-
ctx.beginPath();
|
|
7709
|
-
ctx.moveTo(cx - 3.4, cy - 3.4);
|
|
7710
|
-
ctx.lineTo(cx + 3.4, cy + 3.4);
|
|
7711
|
-
ctx.moveTo(cx + 3.4, cy - 3.4);
|
|
7712
|
-
ctx.lineTo(cx - 3.4, cy + 3.4);
|
|
7713
|
-
ctx.stroke();
|
|
7714
|
-
}
|
|
7715
|
-
ctx.restore();
|
|
7716
|
-
paneButtonRegions.push({
|
|
7717
|
-
id: indicatorId,
|
|
7718
|
-
type: indicatorType,
|
|
7719
|
-
action,
|
|
7720
|
-
x: buttonX,
|
|
7721
|
-
y: buttonY,
|
|
7722
|
-
width: paneButtonSize,
|
|
7723
|
-
height: paneButtonSize
|
|
7724
|
-
});
|
|
7725
|
-
buttonX += paneButtonSize + paneButtonGap;
|
|
7726
|
-
}
|
|
7727
|
-
};
|
|
7814
|
+
const drawPaneButtons = drawIndicatorControls;
|
|
7728
7815
|
activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
|
|
7729
7816
|
const paneHeight = separatePaneHeights[paneIndex] ?? 80;
|
|
7730
7817
|
const paneTop = paneTopCursor;
|
|
@@ -8226,26 +8313,56 @@ function createChart(element, options = {}) {
|
|
|
8226
8313
|
};
|
|
8227
8314
|
const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
|
|
8228
8315
|
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));
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
if (labels.showIndicatorNames) {
|
|
8233
|
-
return plugin.name;
|
|
8234
|
-
}
|
|
8235
|
-
return inputValues.join(" ");
|
|
8236
|
-
}).filter((entry) => entry.length > 0);
|
|
8316
|
+
const text = labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0 ? `${plugin.name} ${inputValues.join(" ")}` : labels.showIndicatorNames ? plugin.name : inputValues.join(" ");
|
|
8317
|
+
return { id: indicator.id, type: indicator.type, visible: indicator.visible, text };
|
|
8318
|
+
}).filter((entry) => entry.text.length > 0);
|
|
8237
8319
|
if (labelEntries.length > 0) {
|
|
8238
8320
|
const prevFont = ctx.font;
|
|
8239
|
-
|
|
8240
|
-
|
|
8321
|
+
const legendFontSize = Math.max(8, axis.fontSize);
|
|
8322
|
+
ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
|
|
8241
8323
|
const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
|
|
8242
8324
|
const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
|
|
8243
8325
|
const position = labels.indicatorLegendPosition;
|
|
8244
8326
|
const isRight = position === "top-right" || position === "bottom-right";
|
|
8245
8327
|
const isBottom = position === "bottom-left" || position === "bottom-right";
|
|
8246
8328
|
const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
|
|
8247
|
-
const
|
|
8248
|
-
|
|
8329
|
+
const legendTop = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
|
|
8330
|
+
const rowHeight = Math.round(legendFontSize * 1.55);
|
|
8331
|
+
labelEntries.forEach((entry, rowIndex) => {
|
|
8332
|
+
const rowTop = isBottom ? legendTop - (labelEntries.length - rowIndex) * rowHeight : legendTop + rowIndex * rowHeight;
|
|
8333
|
+
const rowCenter = rowTop + rowHeight / 2;
|
|
8334
|
+
const dimmed = entry.visible === false;
|
|
8335
|
+
ctx.save();
|
|
8336
|
+
if (dimmed) ctx.globalAlpha = 0.45;
|
|
8337
|
+
drawText(
|
|
8338
|
+
entry.text,
|
|
8339
|
+
legendX,
|
|
8340
|
+
rowCenter,
|
|
8341
|
+
isRight ? "right" : "left",
|
|
8342
|
+
"middle",
|
|
8343
|
+
labels.indicatorTextColor
|
|
8344
|
+
);
|
|
8345
|
+
ctx.restore();
|
|
8346
|
+
const textWidth = measureTextWidth(entry.text);
|
|
8347
|
+
const rowLeft = isRight ? legendX - textWidth : legendX;
|
|
8348
|
+
legendRowRegions.push({
|
|
8349
|
+
id: entry.id,
|
|
8350
|
+
type: entry.type,
|
|
8351
|
+
x: rowLeft,
|
|
8352
|
+
y: rowTop,
|
|
8353
|
+
width: textWidth + LEGEND_CONTROLS_WIDTH,
|
|
8354
|
+
height: rowHeight
|
|
8355
|
+
});
|
|
8356
|
+
if (hoveredLegendId === entry.id) {
|
|
8357
|
+
drawIndicatorControls(
|
|
8358
|
+
entry.id,
|
|
8359
|
+
entry.type,
|
|
8360
|
+
entry.visible !== false,
|
|
8361
|
+
isRight ? legendX + 6 : rowLeft + textWidth + 8,
|
|
8362
|
+
rowCenter
|
|
8363
|
+
);
|
|
8364
|
+
}
|
|
8365
|
+
});
|
|
8249
8366
|
ctx.font = prevFont;
|
|
8250
8367
|
}
|
|
8251
8368
|
}
|
|
@@ -8917,6 +9034,9 @@ function createChart(element, options = {}) {
|
|
|
8917
9034
|
if (hits.length === 0) return null;
|
|
8918
9035
|
return hits.find((region) => region.part === "remove") ?? hits[0];
|
|
8919
9036
|
};
|
|
9037
|
+
const getLegendRowAt = (x, y) => legendRowRegions.find(
|
|
9038
|
+
(region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
|
|
9039
|
+
) ?? null;
|
|
8920
9040
|
const getMarkAt = (x, y) => {
|
|
8921
9041
|
const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
|
|
8922
9042
|
for (let index = markRegions.length - 1; index >= 0; index -= 1) {
|
|
@@ -9407,6 +9527,19 @@ function createChart(element, options = {}) {
|
|
|
9407
9527
|
if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5 * hitTolerance) {
|
|
9408
9528
|
return { drawing, target: "line" };
|
|
9409
9529
|
}
|
|
9530
|
+
} else if (drawing.type === "fixed-range-volume-profile") {
|
|
9531
|
+
const p0 = drawing.points[0];
|
|
9532
|
+
const p1 = drawing.points[1];
|
|
9533
|
+
if (!p0 || !p1) continue;
|
|
9534
|
+
const px0 = canvasXFromDrawingPoint(p0);
|
|
9535
|
+
const px1 = canvasXFromDrawingPoint(p1);
|
|
9536
|
+
const y0 = canvasYFromDrawingPrice(p0.price);
|
|
9537
|
+
const y1 = canvasYFromDrawingPrice(p1.price);
|
|
9538
|
+
if (Math.hypot(x - px0, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
|
|
9539
|
+
if (Math.hypot(x - px1, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
|
|
9540
|
+
if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1)) {
|
|
9541
|
+
return { drawing, target: "line" };
|
|
9542
|
+
}
|
|
9410
9543
|
} else if (drawing.type === "price-range") {
|
|
9411
9544
|
const p0 = drawing.points[0];
|
|
9412
9545
|
const p1 = drawing.points[1];
|
|
@@ -9858,6 +9991,33 @@ function createChart(element, options = {}) {
|
|
|
9858
9991
|
scheduleDraw();
|
|
9859
9992
|
return true;
|
|
9860
9993
|
}
|
|
9994
|
+
if (activeDrawingTool === "fixed-range-volume-profile") {
|
|
9995
|
+
if (draftDrawing?.type === "fixed-range-volume-profile") {
|
|
9996
|
+
const completed = normalizeDrawingState({
|
|
9997
|
+
...serializeDrawing(draftDrawing),
|
|
9998
|
+
points: [draftDrawing.points[0], point]
|
|
9999
|
+
});
|
|
10000
|
+
drawings.push(completed);
|
|
10001
|
+
draftDrawing = null;
|
|
10002
|
+
activeDrawingTool = null;
|
|
10003
|
+
emitDrawingsChange();
|
|
10004
|
+
scheduleDraw();
|
|
10005
|
+
return true;
|
|
10006
|
+
}
|
|
10007
|
+
const defaults = getDrawingToolDefaults("fixed-range-volume-profile");
|
|
10008
|
+
draftDrawing = normalizeDrawingState({
|
|
10009
|
+
type: "fixed-range-volume-profile",
|
|
10010
|
+
points: [point, point],
|
|
10011
|
+
color: defaults.color ?? "#5b8def",
|
|
10012
|
+
style: defaults.style ?? "solid",
|
|
10013
|
+
width: defaults.width ?? 1,
|
|
10014
|
+
...defaults.profileRows === void 0 ? {} : { profileRows: defaults.profileRows },
|
|
10015
|
+
...defaults.profileValueAreaPercent === void 0 ? {} : { profileValueAreaPercent: defaults.profileValueAreaPercent },
|
|
10016
|
+
...defaults.profileWidthRatio === void 0 ? {} : { profileWidthRatio: defaults.profileWidthRatio }
|
|
10017
|
+
});
|
|
10018
|
+
scheduleDraw();
|
|
10019
|
+
return true;
|
|
10020
|
+
}
|
|
9861
10021
|
if (activeDrawingTool === "price-range") {
|
|
9862
10022
|
const tick = getConfiguredTickSize();
|
|
9863
10023
|
const visibleRange = drawState.yMax - drawState.yMin;
|
|
@@ -10626,6 +10786,12 @@ function createChart(element, options = {}) {
|
|
|
10626
10786
|
setCrosshairPoint(null);
|
|
10627
10787
|
return;
|
|
10628
10788
|
}
|
|
10789
|
+
const legendRow = getLegendRowAt(point.x, point.y);
|
|
10790
|
+
const nextLegendId = legendRow?.id ?? null;
|
|
10791
|
+
if (nextLegendId !== hoveredLegendId) {
|
|
10792
|
+
hoveredLegendId = nextLegendId;
|
|
10793
|
+
scheduleDraw();
|
|
10794
|
+
}
|
|
10629
10795
|
const hoveredPane = getPaneAt(point.x, point.y);
|
|
10630
10796
|
const nextHoveredPaneId = hoveredPane?.id ?? null;
|
|
10631
10797
|
if (nextHoveredPaneId !== hoveredPaneId) {
|
|
@@ -10844,10 +11010,11 @@ function createChart(element, options = {}) {
|
|
|
10844
11010
|
});
|
|
10845
11011
|
paneDividerDrag = null;
|
|
10846
11012
|
}
|
|
10847
|
-
if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null)) {
|
|
11013
|
+
if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null || hoveredLegendId !== null)) {
|
|
10848
11014
|
hoveredPaneId = null;
|
|
10849
11015
|
hoveredPaneButton = null;
|
|
10850
11016
|
hoveredAlertId = null;
|
|
11017
|
+
hoveredLegendId = null;
|
|
10851
11018
|
if (hoveredMarkKey !== null) {
|
|
10852
11019
|
hoveredMarkKey = null;
|
|
10853
11020
|
markHoverHandler?.(null);
|
package/docs/API.md
CHANGED
|
@@ -645,6 +645,91 @@ link.click();
|
|
|
645
645
|
|
|
646
646
|
---
|
|
647
647
|
|
|
648
|
+
## Futures desk indicators
|
|
649
|
+
|
|
650
|
+
Three built-ins aimed at intraday futures work. All three are overlays on the
|
|
651
|
+
main pane, and the last two need a session spec (`setSession`) — without one
|
|
652
|
+
they have no notion of a trading day and render nothing.
|
|
653
|
+
|
|
654
|
+
### `volume-profile`
|
|
655
|
+
|
|
656
|
+
Volume distribution across price, with point of control and value area.
|
|
657
|
+
|
|
658
|
+
```ts
|
|
659
|
+
chart.addIndicator("volume-profile", { scope: "visible", rows: 40, widthPercent: 28 });
|
|
660
|
+
chart.addIndicator("volume-profile", { scope: "session" }); // one per trading day
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
`scope: "visible"` profiles whatever is on screen and follows pan/zoom, drawn
|
|
664
|
+
against the price axis; `scope: "session"` draws a separate profile inside each
|
|
665
|
+
trading day's bars, skipping any session too narrow to read. Rows are split
|
|
666
|
+
into buy and sell volume by each bar's own direction, shaded brighter inside
|
|
667
|
+
the value area. Inputs: `rows`, `widthPercent`, `placement`
|
|
668
|
+
(`"right" | "left"`, visible scope only), `valueAreaPercent`, `upColor`,
|
|
669
|
+
`downColor`, `valueAreaOpacity`, `outsideOpacity`, `showPoc`, `pocColor`,
|
|
670
|
+
`showValueArea`, `valueAreaColor`, `showLabels`.
|
|
671
|
+
|
|
672
|
+
OHLCV bars carry no intrabar detail, so each bar's volume is spread evenly over
|
|
673
|
+
the rows its high–low range touches — the same approximation TradingView makes
|
|
674
|
+
when building a profile from candles rather than ticks.
|
|
675
|
+
|
|
676
|
+
### `session-levels`
|
|
677
|
+
|
|
678
|
+
The reference prices marked up before the open: prior-day high/low/close
|
|
679
|
+
(PDH/PDL/PDC), overnight high/low (ONH/ONL), the opening range (ORH/ORL) and
|
|
680
|
+
optionally the initial balance (IBH/IBL, first hour).
|
|
681
|
+
|
|
682
|
+
```ts
|
|
683
|
+
chart.setSession({ spec: "cme-futures" });
|
|
684
|
+
chart.addIndicator("session-levels", { openingRangeMinutes: 30, showInitialBalance: true });
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
Cash-session bounds are inputs (`rthStart` / `rthEnd`, default 09:30–16:00)
|
|
688
|
+
rather than being read from the chart's session spec: a CME futures session
|
|
689
|
+
runs 18:00–17:00, but the levels traders mark are relative to the 09:30 equity
|
|
690
|
+
open inside it. Everything in the current session outside those hours counts as
|
|
691
|
+
overnight.
|
|
692
|
+
|
|
693
|
+
### Fixed-range volume profile (drawing tool)
|
|
694
|
+
|
|
695
|
+
The third profile variant is a *drawing* rather than an indicator, because it
|
|
696
|
+
needs a range: arm `fixed-range-volume-profile` and click twice to bracket the
|
|
697
|
+
bars. Only the anchors' bar indexes matter — the price extent comes from the
|
|
698
|
+
bars they enclose.
|
|
699
|
+
|
|
700
|
+
```ts
|
|
701
|
+
chart.setActiveDrawingTool("fixed-range-volume-profile");
|
|
702
|
+
chart.setDrawingDefaults("fixed-range-volume-profile", {
|
|
703
|
+
color: "#5b8def",
|
|
704
|
+
profileRows: 24,
|
|
705
|
+
profileValueAreaPercent: 70,
|
|
706
|
+
profileWidthRatio: 0.5
|
|
707
|
+
});
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
It draws the selected span, the volume rows (buy/sell split, brighter inside
|
|
711
|
+
the value area) growing right from the range start, and POC/VAH/VAL. Being a
|
|
712
|
+
drawing, it is draggable, undoable and part of `saveState()` like any other.
|
|
713
|
+
|
|
714
|
+
### `anchored-vwap`
|
|
715
|
+
|
|
716
|
+
VWAP measured from a bar you choose rather than the session open.
|
|
717
|
+
|
|
718
|
+
```ts
|
|
719
|
+
chart.addIndicator("anchored-vwap", {
|
|
720
|
+
anchorTime: "2026-07-14T13:30:00Z", // epoch ms or ISO
|
|
721
|
+
showBands: true,
|
|
722
|
+
bandMultiplier: 2
|
|
723
|
+
});
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
Without an `anchorTime` it anchors to the most recent session start, which is
|
|
727
|
+
just the session VWAP. Add one instance per anchor; hosts typically expose this
|
|
728
|
+
as a "start here" action on the bar under the cursor. Volume-less feeds fall
|
|
729
|
+
back to an unweighted running average rather than flatlining.
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
648
733
|
## Time zones and trading sessions
|
|
649
734
|
|
|
650
735
|
Timestamps are rendered in whatever zone you choose, and the chart can be told
|