hyperprop-charting-library 0.1.147 → 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 +137 -91
- package/dist/hyperprop-charting-library.js +137 -91
- package/dist/index.cjs +137 -91
- package/dist/index.js +137 -91
- package/docs/API.md +21 -0
- package/package.json +1 -1
|
@@ -4297,6 +4297,8 @@ function createChart(element, options = {}) {
|
|
|
4297
4297
|
let paneButtonRegions = [];
|
|
4298
4298
|
let hoveredPaneId = null;
|
|
4299
4299
|
let hoveredPaneButton = null;
|
|
4300
|
+
let legendRowRegions = [];
|
|
4301
|
+
let hoveredLegendId = null;
|
|
4300
4302
|
let paneDividerDrag = null;
|
|
4301
4303
|
let indicatorPaneActionHandler = null;
|
|
4302
4304
|
let indicatorPaneHeightChangeHandler = null;
|
|
@@ -5593,6 +5595,85 @@ function createChart(element, options = {}) {
|
|
|
5593
5595
|
ctx.closePath();
|
|
5594
5596
|
ctx.stroke();
|
|
5595
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
|
+
};
|
|
5596
5677
|
const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
|
|
5597
5678
|
ctx.save();
|
|
5598
5679
|
ctx.fillStyle = color;
|
|
@@ -5673,6 +5754,8 @@ function createChart(element, options = {}) {
|
|
|
5673
5754
|
orderDragRegions = [];
|
|
5674
5755
|
alertRegions = [];
|
|
5675
5756
|
markRegions = [];
|
|
5757
|
+
paneButtonRegions = [];
|
|
5758
|
+
legendRowRegions = [];
|
|
5676
5759
|
crosshairPriceActionRegion = null;
|
|
5677
5760
|
const pixelRatio = getPixelRatio();
|
|
5678
5761
|
canvas.style.width = `${width}px`;
|
|
@@ -7761,87 +7844,10 @@ function createChart(element, options = {}) {
|
|
|
7761
7844
|
}
|
|
7762
7845
|
paneLayoutFullChartHeight = fullChartHeight;
|
|
7763
7846
|
paneLayoutInfos = [];
|
|
7764
|
-
paneButtonRegions = [];
|
|
7765
7847
|
if (activeSeparateIndicators.length > 0) {
|
|
7766
7848
|
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
7767
7849
|
let paneTopCursor = chartBottom + paneGap;
|
|
7768
|
-
const
|
|
7769
|
-
const paneButtonGap = 4;
|
|
7770
|
-
const drawPaneButtons = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
|
|
7771
|
-
const actionList = [
|
|
7772
|
-
"visibility",
|
|
7773
|
-
"settings",
|
|
7774
|
-
"source",
|
|
7775
|
-
"remove"
|
|
7776
|
-
];
|
|
7777
|
-
let buttonX = startX;
|
|
7778
|
-
for (const action of actionList) {
|
|
7779
|
-
const buttonY = centerY - paneButtonSize / 2;
|
|
7780
|
-
const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
|
|
7781
|
-
const iconColor = labels.indicatorTextColor;
|
|
7782
|
-
ctx.save();
|
|
7783
|
-
ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
|
|
7784
|
-
fillRoundedRect(Math.round(buttonX), Math.round(buttonY), paneButtonSize, paneButtonSize, 4);
|
|
7785
|
-
ctx.strokeStyle = iconColor;
|
|
7786
|
-
ctx.fillStyle = iconColor;
|
|
7787
|
-
ctx.globalAlpha = hovered ? 1 : 0.75;
|
|
7788
|
-
ctx.lineWidth = 1.2;
|
|
7789
|
-
ctx.setLineDash([]);
|
|
7790
|
-
const cx = buttonX + paneButtonSize / 2;
|
|
7791
|
-
const cy = centerY;
|
|
7792
|
-
if (action === "visibility") {
|
|
7793
|
-
ctx.beginPath();
|
|
7794
|
-
ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
|
|
7795
|
-
ctx.stroke();
|
|
7796
|
-
ctx.beginPath();
|
|
7797
|
-
ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
|
|
7798
|
-
ctx.fill();
|
|
7799
|
-
if (!indicatorVisible) {
|
|
7800
|
-
ctx.beginPath();
|
|
7801
|
-
ctx.moveTo(cx - 5.2, cy + 4.6);
|
|
7802
|
-
ctx.lineTo(cx + 5.2, cy - 4.6);
|
|
7803
|
-
ctx.stroke();
|
|
7804
|
-
}
|
|
7805
|
-
} else if (action === "settings") {
|
|
7806
|
-
ctx.beginPath();
|
|
7807
|
-
ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
|
|
7808
|
-
ctx.stroke();
|
|
7809
|
-
for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
|
|
7810
|
-
const angle = Math.PI / 4 * toothIndex;
|
|
7811
|
-
ctx.beginPath();
|
|
7812
|
-
ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
|
|
7813
|
-
ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
|
|
7814
|
-
ctx.stroke();
|
|
7815
|
-
}
|
|
7816
|
-
ctx.beginPath();
|
|
7817
|
-
ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
|
|
7818
|
-
ctx.fill();
|
|
7819
|
-
} else if (action === "source") {
|
|
7820
|
-
const prevSourceFont = ctx.font;
|
|
7821
|
-
ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
|
|
7822
|
-
drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
|
|
7823
|
-
ctx.font = prevSourceFont;
|
|
7824
|
-
} else {
|
|
7825
|
-
ctx.beginPath();
|
|
7826
|
-
ctx.moveTo(cx - 3.4, cy - 3.4);
|
|
7827
|
-
ctx.lineTo(cx + 3.4, cy + 3.4);
|
|
7828
|
-
ctx.moveTo(cx + 3.4, cy - 3.4);
|
|
7829
|
-
ctx.lineTo(cx - 3.4, cy + 3.4);
|
|
7830
|
-
ctx.stroke();
|
|
7831
|
-
}
|
|
7832
|
-
ctx.restore();
|
|
7833
|
-
paneButtonRegions.push({
|
|
7834
|
-
id: indicatorId,
|
|
7835
|
-
type: indicatorType,
|
|
7836
|
-
action,
|
|
7837
|
-
x: buttonX,
|
|
7838
|
-
y: buttonY,
|
|
7839
|
-
width: paneButtonSize,
|
|
7840
|
-
height: paneButtonSize
|
|
7841
|
-
});
|
|
7842
|
-
buttonX += paneButtonSize + paneButtonGap;
|
|
7843
|
-
}
|
|
7844
|
-
};
|
|
7850
|
+
const drawPaneButtons = drawIndicatorControls;
|
|
7845
7851
|
activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
|
|
7846
7852
|
const paneHeight = separatePaneHeights[paneIndex] ?? 80;
|
|
7847
7853
|
const paneTop = paneTopCursor;
|
|
@@ -8343,26 +8349,56 @@ function createChart(element, options = {}) {
|
|
|
8343
8349
|
};
|
|
8344
8350
|
const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
|
|
8345
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));
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
if (labels.showIndicatorNames) {
|
|
8350
|
-
return plugin.name;
|
|
8351
|
-
}
|
|
8352
|
-
return inputValues.join(" ");
|
|
8353
|
-
}).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);
|
|
8354
8355
|
if (labelEntries.length > 0) {
|
|
8355
8356
|
const prevFont = ctx.font;
|
|
8356
|
-
|
|
8357
|
-
|
|
8357
|
+
const legendFontSize = Math.max(8, axis.fontSize);
|
|
8358
|
+
ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
|
|
8358
8359
|
const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
|
|
8359
8360
|
const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
|
|
8360
8361
|
const position = labels.indicatorLegendPosition;
|
|
8361
8362
|
const isRight = position === "top-right" || position === "bottom-right";
|
|
8362
8363
|
const isBottom = position === "bottom-left" || position === "bottom-right";
|
|
8363
8364
|
const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
|
|
8364
|
-
const
|
|
8365
|
-
|
|
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
|
+
});
|
|
8366
8402
|
ctx.font = prevFont;
|
|
8367
8403
|
}
|
|
8368
8404
|
}
|
|
@@ -9034,6 +9070,9 @@ function createChart(element, options = {}) {
|
|
|
9034
9070
|
if (hits.length === 0) return null;
|
|
9035
9071
|
return hits.find((region) => region.part === "remove") ?? hits[0];
|
|
9036
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;
|
|
9037
9076
|
const getMarkAt = (x, y) => {
|
|
9038
9077
|
const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
|
|
9039
9078
|
for (let index = markRegions.length - 1; index >= 0; index -= 1) {
|
|
@@ -10783,6 +10822,12 @@ function createChart(element, options = {}) {
|
|
|
10783
10822
|
setCrosshairPoint(null);
|
|
10784
10823
|
return;
|
|
10785
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
|
+
}
|
|
10786
10831
|
const hoveredPane = getPaneAt(point.x, point.y);
|
|
10787
10832
|
const nextHoveredPaneId = hoveredPane?.id ?? null;
|
|
10788
10833
|
if (nextHoveredPaneId !== hoveredPaneId) {
|
|
@@ -11001,10 +11046,11 @@ function createChart(element, options = {}) {
|
|
|
11001
11046
|
});
|
|
11002
11047
|
paneDividerDrag = null;
|
|
11003
11048
|
}
|
|
11004
|
-
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)) {
|
|
11005
11050
|
hoveredPaneId = null;
|
|
11006
11051
|
hoveredPaneButton = null;
|
|
11007
11052
|
hoveredAlertId = null;
|
|
11053
|
+
hoveredLegendId = null;
|
|
11008
11054
|
if (hoveredMarkKey !== null) {
|
|
11009
11055
|
hoveredMarkKey = null;
|
|
11010
11056
|
markHoverHandler?.(null);
|
|
@@ -4261,6 +4261,8 @@ function createChart(element, options = {}) {
|
|
|
4261
4261
|
let paneButtonRegions = [];
|
|
4262
4262
|
let hoveredPaneId = null;
|
|
4263
4263
|
let hoveredPaneButton = null;
|
|
4264
|
+
let legendRowRegions = [];
|
|
4265
|
+
let hoveredLegendId = null;
|
|
4264
4266
|
let paneDividerDrag = null;
|
|
4265
4267
|
let indicatorPaneActionHandler = null;
|
|
4266
4268
|
let indicatorPaneHeightChangeHandler = null;
|
|
@@ -5557,6 +5559,85 @@ function createChart(element, options = {}) {
|
|
|
5557
5559
|
ctx.closePath();
|
|
5558
5560
|
ctx.stroke();
|
|
5559
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
|
+
};
|
|
5560
5641
|
const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
|
|
5561
5642
|
ctx.save();
|
|
5562
5643
|
ctx.fillStyle = color;
|
|
@@ -5637,6 +5718,8 @@ function createChart(element, options = {}) {
|
|
|
5637
5718
|
orderDragRegions = [];
|
|
5638
5719
|
alertRegions = [];
|
|
5639
5720
|
markRegions = [];
|
|
5721
|
+
paneButtonRegions = [];
|
|
5722
|
+
legendRowRegions = [];
|
|
5640
5723
|
crosshairPriceActionRegion = null;
|
|
5641
5724
|
const pixelRatio = getPixelRatio();
|
|
5642
5725
|
canvas.style.width = `${width}px`;
|
|
@@ -7725,87 +7808,10 @@ function createChart(element, options = {}) {
|
|
|
7725
7808
|
}
|
|
7726
7809
|
paneLayoutFullChartHeight = fullChartHeight;
|
|
7727
7810
|
paneLayoutInfos = [];
|
|
7728
|
-
paneButtonRegions = [];
|
|
7729
7811
|
if (activeSeparateIndicators.length > 0) {
|
|
7730
7812
|
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
7731
7813
|
let paneTopCursor = chartBottom + paneGap;
|
|
7732
|
-
const
|
|
7733
|
-
const paneButtonGap = 4;
|
|
7734
|
-
const drawPaneButtons = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
|
|
7735
|
-
const actionList = [
|
|
7736
|
-
"visibility",
|
|
7737
|
-
"settings",
|
|
7738
|
-
"source",
|
|
7739
|
-
"remove"
|
|
7740
|
-
];
|
|
7741
|
-
let buttonX = startX;
|
|
7742
|
-
for (const action of actionList) {
|
|
7743
|
-
const buttonY = centerY - paneButtonSize / 2;
|
|
7744
|
-
const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
|
|
7745
|
-
const iconColor = labels.indicatorTextColor;
|
|
7746
|
-
ctx.save();
|
|
7747
|
-
ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
|
|
7748
|
-
fillRoundedRect(Math.round(buttonX), Math.round(buttonY), paneButtonSize, paneButtonSize, 4);
|
|
7749
|
-
ctx.strokeStyle = iconColor;
|
|
7750
|
-
ctx.fillStyle = iconColor;
|
|
7751
|
-
ctx.globalAlpha = hovered ? 1 : 0.75;
|
|
7752
|
-
ctx.lineWidth = 1.2;
|
|
7753
|
-
ctx.setLineDash([]);
|
|
7754
|
-
const cx = buttonX + paneButtonSize / 2;
|
|
7755
|
-
const cy = centerY;
|
|
7756
|
-
if (action === "visibility") {
|
|
7757
|
-
ctx.beginPath();
|
|
7758
|
-
ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
|
|
7759
|
-
ctx.stroke();
|
|
7760
|
-
ctx.beginPath();
|
|
7761
|
-
ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
|
|
7762
|
-
ctx.fill();
|
|
7763
|
-
if (!indicatorVisible) {
|
|
7764
|
-
ctx.beginPath();
|
|
7765
|
-
ctx.moveTo(cx - 5.2, cy + 4.6);
|
|
7766
|
-
ctx.lineTo(cx + 5.2, cy - 4.6);
|
|
7767
|
-
ctx.stroke();
|
|
7768
|
-
}
|
|
7769
|
-
} else if (action === "settings") {
|
|
7770
|
-
ctx.beginPath();
|
|
7771
|
-
ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
|
|
7772
|
-
ctx.stroke();
|
|
7773
|
-
for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
|
|
7774
|
-
const angle = Math.PI / 4 * toothIndex;
|
|
7775
|
-
ctx.beginPath();
|
|
7776
|
-
ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
|
|
7777
|
-
ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
|
|
7778
|
-
ctx.stroke();
|
|
7779
|
-
}
|
|
7780
|
-
ctx.beginPath();
|
|
7781
|
-
ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
|
|
7782
|
-
ctx.fill();
|
|
7783
|
-
} else if (action === "source") {
|
|
7784
|
-
const prevSourceFont = ctx.font;
|
|
7785
|
-
ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
|
|
7786
|
-
drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
|
|
7787
|
-
ctx.font = prevSourceFont;
|
|
7788
|
-
} else {
|
|
7789
|
-
ctx.beginPath();
|
|
7790
|
-
ctx.moveTo(cx - 3.4, cy - 3.4);
|
|
7791
|
-
ctx.lineTo(cx + 3.4, cy + 3.4);
|
|
7792
|
-
ctx.moveTo(cx + 3.4, cy - 3.4);
|
|
7793
|
-
ctx.lineTo(cx - 3.4, cy + 3.4);
|
|
7794
|
-
ctx.stroke();
|
|
7795
|
-
}
|
|
7796
|
-
ctx.restore();
|
|
7797
|
-
paneButtonRegions.push({
|
|
7798
|
-
id: indicatorId,
|
|
7799
|
-
type: indicatorType,
|
|
7800
|
-
action,
|
|
7801
|
-
x: buttonX,
|
|
7802
|
-
y: buttonY,
|
|
7803
|
-
width: paneButtonSize,
|
|
7804
|
-
height: paneButtonSize
|
|
7805
|
-
});
|
|
7806
|
-
buttonX += paneButtonSize + paneButtonGap;
|
|
7807
|
-
}
|
|
7808
|
-
};
|
|
7814
|
+
const drawPaneButtons = drawIndicatorControls;
|
|
7809
7815
|
activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
|
|
7810
7816
|
const paneHeight = separatePaneHeights[paneIndex] ?? 80;
|
|
7811
7817
|
const paneTop = paneTopCursor;
|
|
@@ -8307,26 +8313,56 @@ function createChart(element, options = {}) {
|
|
|
8307
8313
|
};
|
|
8308
8314
|
const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
|
|
8309
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));
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
if (labels.showIndicatorNames) {
|
|
8314
|
-
return plugin.name;
|
|
8315
|
-
}
|
|
8316
|
-
return inputValues.join(" ");
|
|
8317
|
-
}).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);
|
|
8318
8319
|
if (labelEntries.length > 0) {
|
|
8319
8320
|
const prevFont = ctx.font;
|
|
8320
|
-
|
|
8321
|
-
|
|
8321
|
+
const legendFontSize = Math.max(8, axis.fontSize);
|
|
8322
|
+
ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
|
|
8322
8323
|
const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
|
|
8323
8324
|
const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
|
|
8324
8325
|
const position = labels.indicatorLegendPosition;
|
|
8325
8326
|
const isRight = position === "top-right" || position === "bottom-right";
|
|
8326
8327
|
const isBottom = position === "bottom-left" || position === "bottom-right";
|
|
8327
8328
|
const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
|
|
8328
|
-
const
|
|
8329
|
-
|
|
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
|
+
});
|
|
8330
8366
|
ctx.font = prevFont;
|
|
8331
8367
|
}
|
|
8332
8368
|
}
|
|
@@ -8998,6 +9034,9 @@ function createChart(element, options = {}) {
|
|
|
8998
9034
|
if (hits.length === 0) return null;
|
|
8999
9035
|
return hits.find((region) => region.part === "remove") ?? hits[0];
|
|
9000
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;
|
|
9001
9040
|
const getMarkAt = (x, y) => {
|
|
9002
9041
|
const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
|
|
9003
9042
|
for (let index = markRegions.length - 1; index >= 0; index -= 1) {
|
|
@@ -10747,6 +10786,12 @@ function createChart(element, options = {}) {
|
|
|
10747
10786
|
setCrosshairPoint(null);
|
|
10748
10787
|
return;
|
|
10749
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
|
+
}
|
|
10750
10795
|
const hoveredPane = getPaneAt(point.x, point.y);
|
|
10751
10796
|
const nextHoveredPaneId = hoveredPane?.id ?? null;
|
|
10752
10797
|
if (nextHoveredPaneId !== hoveredPaneId) {
|
|
@@ -10965,10 +11010,11 @@ function createChart(element, options = {}) {
|
|
|
10965
11010
|
});
|
|
10966
11011
|
paneDividerDrag = null;
|
|
10967
11012
|
}
|
|
10968
|
-
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)) {
|
|
10969
11014
|
hoveredPaneId = null;
|
|
10970
11015
|
hoveredPaneButton = null;
|
|
10971
11016
|
hoveredAlertId = null;
|
|
11017
|
+
hoveredLegendId = null;
|
|
10972
11018
|
if (hoveredMarkKey !== null) {
|
|
10973
11019
|
hoveredMarkKey = null;
|
|
10974
11020
|
markHoverHandler?.(null);
|
package/dist/index.cjs
CHANGED
|
@@ -4297,6 +4297,8 @@ function createChart(element, options = {}) {
|
|
|
4297
4297
|
let paneButtonRegions = [];
|
|
4298
4298
|
let hoveredPaneId = null;
|
|
4299
4299
|
let hoveredPaneButton = null;
|
|
4300
|
+
let legendRowRegions = [];
|
|
4301
|
+
let hoveredLegendId = null;
|
|
4300
4302
|
let paneDividerDrag = null;
|
|
4301
4303
|
let indicatorPaneActionHandler = null;
|
|
4302
4304
|
let indicatorPaneHeightChangeHandler = null;
|
|
@@ -5593,6 +5595,85 @@ function createChart(element, options = {}) {
|
|
|
5593
5595
|
ctx.closePath();
|
|
5594
5596
|
ctx.stroke();
|
|
5595
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
|
+
};
|
|
5596
5677
|
const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
|
|
5597
5678
|
ctx.save();
|
|
5598
5679
|
ctx.fillStyle = color;
|
|
@@ -5673,6 +5754,8 @@ function createChart(element, options = {}) {
|
|
|
5673
5754
|
orderDragRegions = [];
|
|
5674
5755
|
alertRegions = [];
|
|
5675
5756
|
markRegions = [];
|
|
5757
|
+
paneButtonRegions = [];
|
|
5758
|
+
legendRowRegions = [];
|
|
5676
5759
|
crosshairPriceActionRegion = null;
|
|
5677
5760
|
const pixelRatio = getPixelRatio();
|
|
5678
5761
|
canvas.style.width = `${width}px`;
|
|
@@ -7761,87 +7844,10 @@ function createChart(element, options = {}) {
|
|
|
7761
7844
|
}
|
|
7762
7845
|
paneLayoutFullChartHeight = fullChartHeight;
|
|
7763
7846
|
paneLayoutInfos = [];
|
|
7764
|
-
paneButtonRegions = [];
|
|
7765
7847
|
if (activeSeparateIndicators.length > 0) {
|
|
7766
7848
|
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
7767
7849
|
let paneTopCursor = chartBottom + paneGap;
|
|
7768
|
-
const
|
|
7769
|
-
const paneButtonGap = 4;
|
|
7770
|
-
const drawPaneButtons = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
|
|
7771
|
-
const actionList = [
|
|
7772
|
-
"visibility",
|
|
7773
|
-
"settings",
|
|
7774
|
-
"source",
|
|
7775
|
-
"remove"
|
|
7776
|
-
];
|
|
7777
|
-
let buttonX = startX;
|
|
7778
|
-
for (const action of actionList) {
|
|
7779
|
-
const buttonY = centerY - paneButtonSize / 2;
|
|
7780
|
-
const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
|
|
7781
|
-
const iconColor = labels.indicatorTextColor;
|
|
7782
|
-
ctx.save();
|
|
7783
|
-
ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
|
|
7784
|
-
fillRoundedRect(Math.round(buttonX), Math.round(buttonY), paneButtonSize, paneButtonSize, 4);
|
|
7785
|
-
ctx.strokeStyle = iconColor;
|
|
7786
|
-
ctx.fillStyle = iconColor;
|
|
7787
|
-
ctx.globalAlpha = hovered ? 1 : 0.75;
|
|
7788
|
-
ctx.lineWidth = 1.2;
|
|
7789
|
-
ctx.setLineDash([]);
|
|
7790
|
-
const cx = buttonX + paneButtonSize / 2;
|
|
7791
|
-
const cy = centerY;
|
|
7792
|
-
if (action === "visibility") {
|
|
7793
|
-
ctx.beginPath();
|
|
7794
|
-
ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
|
|
7795
|
-
ctx.stroke();
|
|
7796
|
-
ctx.beginPath();
|
|
7797
|
-
ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
|
|
7798
|
-
ctx.fill();
|
|
7799
|
-
if (!indicatorVisible) {
|
|
7800
|
-
ctx.beginPath();
|
|
7801
|
-
ctx.moveTo(cx - 5.2, cy + 4.6);
|
|
7802
|
-
ctx.lineTo(cx + 5.2, cy - 4.6);
|
|
7803
|
-
ctx.stroke();
|
|
7804
|
-
}
|
|
7805
|
-
} else if (action === "settings") {
|
|
7806
|
-
ctx.beginPath();
|
|
7807
|
-
ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
|
|
7808
|
-
ctx.stroke();
|
|
7809
|
-
for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
|
|
7810
|
-
const angle = Math.PI / 4 * toothIndex;
|
|
7811
|
-
ctx.beginPath();
|
|
7812
|
-
ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
|
|
7813
|
-
ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
|
|
7814
|
-
ctx.stroke();
|
|
7815
|
-
}
|
|
7816
|
-
ctx.beginPath();
|
|
7817
|
-
ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
|
|
7818
|
-
ctx.fill();
|
|
7819
|
-
} else if (action === "source") {
|
|
7820
|
-
const prevSourceFont = ctx.font;
|
|
7821
|
-
ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
|
|
7822
|
-
drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
|
|
7823
|
-
ctx.font = prevSourceFont;
|
|
7824
|
-
} else {
|
|
7825
|
-
ctx.beginPath();
|
|
7826
|
-
ctx.moveTo(cx - 3.4, cy - 3.4);
|
|
7827
|
-
ctx.lineTo(cx + 3.4, cy + 3.4);
|
|
7828
|
-
ctx.moveTo(cx + 3.4, cy - 3.4);
|
|
7829
|
-
ctx.lineTo(cx - 3.4, cy + 3.4);
|
|
7830
|
-
ctx.stroke();
|
|
7831
|
-
}
|
|
7832
|
-
ctx.restore();
|
|
7833
|
-
paneButtonRegions.push({
|
|
7834
|
-
id: indicatorId,
|
|
7835
|
-
type: indicatorType,
|
|
7836
|
-
action,
|
|
7837
|
-
x: buttonX,
|
|
7838
|
-
y: buttonY,
|
|
7839
|
-
width: paneButtonSize,
|
|
7840
|
-
height: paneButtonSize
|
|
7841
|
-
});
|
|
7842
|
-
buttonX += paneButtonSize + paneButtonGap;
|
|
7843
|
-
}
|
|
7844
|
-
};
|
|
7850
|
+
const drawPaneButtons = drawIndicatorControls;
|
|
7845
7851
|
activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
|
|
7846
7852
|
const paneHeight = separatePaneHeights[paneIndex] ?? 80;
|
|
7847
7853
|
const paneTop = paneTopCursor;
|
|
@@ -8343,26 +8349,56 @@ function createChart(element, options = {}) {
|
|
|
8343
8349
|
};
|
|
8344
8350
|
const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
|
|
8345
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));
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
if (labels.showIndicatorNames) {
|
|
8350
|
-
return plugin.name;
|
|
8351
|
-
}
|
|
8352
|
-
return inputValues.join(" ");
|
|
8353
|
-
}).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);
|
|
8354
8355
|
if (labelEntries.length > 0) {
|
|
8355
8356
|
const prevFont = ctx.font;
|
|
8356
|
-
|
|
8357
|
-
|
|
8357
|
+
const legendFontSize = Math.max(8, axis.fontSize);
|
|
8358
|
+
ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
|
|
8358
8359
|
const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
|
|
8359
8360
|
const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
|
|
8360
8361
|
const position = labels.indicatorLegendPosition;
|
|
8361
8362
|
const isRight = position === "top-right" || position === "bottom-right";
|
|
8362
8363
|
const isBottom = position === "bottom-left" || position === "bottom-right";
|
|
8363
8364
|
const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
|
|
8364
|
-
const
|
|
8365
|
-
|
|
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
|
+
});
|
|
8366
8402
|
ctx.font = prevFont;
|
|
8367
8403
|
}
|
|
8368
8404
|
}
|
|
@@ -9034,6 +9070,9 @@ function createChart(element, options = {}) {
|
|
|
9034
9070
|
if (hits.length === 0) return null;
|
|
9035
9071
|
return hits.find((region) => region.part === "remove") ?? hits[0];
|
|
9036
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;
|
|
9037
9076
|
const getMarkAt = (x, y) => {
|
|
9038
9077
|
const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
|
|
9039
9078
|
for (let index = markRegions.length - 1; index >= 0; index -= 1) {
|
|
@@ -10783,6 +10822,12 @@ function createChart(element, options = {}) {
|
|
|
10783
10822
|
setCrosshairPoint(null);
|
|
10784
10823
|
return;
|
|
10785
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
|
+
}
|
|
10786
10831
|
const hoveredPane = getPaneAt(point.x, point.y);
|
|
10787
10832
|
const nextHoveredPaneId = hoveredPane?.id ?? null;
|
|
10788
10833
|
if (nextHoveredPaneId !== hoveredPaneId) {
|
|
@@ -11001,10 +11046,11 @@ function createChart(element, options = {}) {
|
|
|
11001
11046
|
});
|
|
11002
11047
|
paneDividerDrag = null;
|
|
11003
11048
|
}
|
|
11004
|
-
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)) {
|
|
11005
11050
|
hoveredPaneId = null;
|
|
11006
11051
|
hoveredPaneButton = null;
|
|
11007
11052
|
hoveredAlertId = null;
|
|
11053
|
+
hoveredLegendId = null;
|
|
11008
11054
|
if (hoveredMarkKey !== null) {
|
|
11009
11055
|
hoveredMarkKey = null;
|
|
11010
11056
|
markHoverHandler?.(null);
|
package/dist/index.js
CHANGED
|
@@ -4261,6 +4261,8 @@ function createChart(element, options = {}) {
|
|
|
4261
4261
|
let paneButtonRegions = [];
|
|
4262
4262
|
let hoveredPaneId = null;
|
|
4263
4263
|
let hoveredPaneButton = null;
|
|
4264
|
+
let legendRowRegions = [];
|
|
4265
|
+
let hoveredLegendId = null;
|
|
4264
4266
|
let paneDividerDrag = null;
|
|
4265
4267
|
let indicatorPaneActionHandler = null;
|
|
4266
4268
|
let indicatorPaneHeightChangeHandler = null;
|
|
@@ -5557,6 +5559,85 @@ function createChart(element, options = {}) {
|
|
|
5557
5559
|
ctx.closePath();
|
|
5558
5560
|
ctx.stroke();
|
|
5559
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
|
+
};
|
|
5560
5641
|
const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
|
|
5561
5642
|
ctx.save();
|
|
5562
5643
|
ctx.fillStyle = color;
|
|
@@ -5637,6 +5718,8 @@ function createChart(element, options = {}) {
|
|
|
5637
5718
|
orderDragRegions = [];
|
|
5638
5719
|
alertRegions = [];
|
|
5639
5720
|
markRegions = [];
|
|
5721
|
+
paneButtonRegions = [];
|
|
5722
|
+
legendRowRegions = [];
|
|
5640
5723
|
crosshairPriceActionRegion = null;
|
|
5641
5724
|
const pixelRatio = getPixelRatio();
|
|
5642
5725
|
canvas.style.width = `${width}px`;
|
|
@@ -7725,87 +7808,10 @@ function createChart(element, options = {}) {
|
|
|
7725
7808
|
}
|
|
7726
7809
|
paneLayoutFullChartHeight = fullChartHeight;
|
|
7727
7810
|
paneLayoutInfos = [];
|
|
7728
|
-
paneButtonRegions = [];
|
|
7729
7811
|
if (activeSeparateIndicators.length > 0) {
|
|
7730
7812
|
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
7731
7813
|
let paneTopCursor = chartBottom + paneGap;
|
|
7732
|
-
const
|
|
7733
|
-
const paneButtonGap = 4;
|
|
7734
|
-
const drawPaneButtons = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
|
|
7735
|
-
const actionList = [
|
|
7736
|
-
"visibility",
|
|
7737
|
-
"settings",
|
|
7738
|
-
"source",
|
|
7739
|
-
"remove"
|
|
7740
|
-
];
|
|
7741
|
-
let buttonX = startX;
|
|
7742
|
-
for (const action of actionList) {
|
|
7743
|
-
const buttonY = centerY - paneButtonSize / 2;
|
|
7744
|
-
const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
|
|
7745
|
-
const iconColor = labels.indicatorTextColor;
|
|
7746
|
-
ctx.save();
|
|
7747
|
-
ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
|
|
7748
|
-
fillRoundedRect(Math.round(buttonX), Math.round(buttonY), paneButtonSize, paneButtonSize, 4);
|
|
7749
|
-
ctx.strokeStyle = iconColor;
|
|
7750
|
-
ctx.fillStyle = iconColor;
|
|
7751
|
-
ctx.globalAlpha = hovered ? 1 : 0.75;
|
|
7752
|
-
ctx.lineWidth = 1.2;
|
|
7753
|
-
ctx.setLineDash([]);
|
|
7754
|
-
const cx = buttonX + paneButtonSize / 2;
|
|
7755
|
-
const cy = centerY;
|
|
7756
|
-
if (action === "visibility") {
|
|
7757
|
-
ctx.beginPath();
|
|
7758
|
-
ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
|
|
7759
|
-
ctx.stroke();
|
|
7760
|
-
ctx.beginPath();
|
|
7761
|
-
ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
|
|
7762
|
-
ctx.fill();
|
|
7763
|
-
if (!indicatorVisible) {
|
|
7764
|
-
ctx.beginPath();
|
|
7765
|
-
ctx.moveTo(cx - 5.2, cy + 4.6);
|
|
7766
|
-
ctx.lineTo(cx + 5.2, cy - 4.6);
|
|
7767
|
-
ctx.stroke();
|
|
7768
|
-
}
|
|
7769
|
-
} else if (action === "settings") {
|
|
7770
|
-
ctx.beginPath();
|
|
7771
|
-
ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
|
|
7772
|
-
ctx.stroke();
|
|
7773
|
-
for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
|
|
7774
|
-
const angle = Math.PI / 4 * toothIndex;
|
|
7775
|
-
ctx.beginPath();
|
|
7776
|
-
ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
|
|
7777
|
-
ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
|
|
7778
|
-
ctx.stroke();
|
|
7779
|
-
}
|
|
7780
|
-
ctx.beginPath();
|
|
7781
|
-
ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
|
|
7782
|
-
ctx.fill();
|
|
7783
|
-
} else if (action === "source") {
|
|
7784
|
-
const prevSourceFont = ctx.font;
|
|
7785
|
-
ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
|
|
7786
|
-
drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
|
|
7787
|
-
ctx.font = prevSourceFont;
|
|
7788
|
-
} else {
|
|
7789
|
-
ctx.beginPath();
|
|
7790
|
-
ctx.moveTo(cx - 3.4, cy - 3.4);
|
|
7791
|
-
ctx.lineTo(cx + 3.4, cy + 3.4);
|
|
7792
|
-
ctx.moveTo(cx + 3.4, cy - 3.4);
|
|
7793
|
-
ctx.lineTo(cx - 3.4, cy + 3.4);
|
|
7794
|
-
ctx.stroke();
|
|
7795
|
-
}
|
|
7796
|
-
ctx.restore();
|
|
7797
|
-
paneButtonRegions.push({
|
|
7798
|
-
id: indicatorId,
|
|
7799
|
-
type: indicatorType,
|
|
7800
|
-
action,
|
|
7801
|
-
x: buttonX,
|
|
7802
|
-
y: buttonY,
|
|
7803
|
-
width: paneButtonSize,
|
|
7804
|
-
height: paneButtonSize
|
|
7805
|
-
});
|
|
7806
|
-
buttonX += paneButtonSize + paneButtonGap;
|
|
7807
|
-
}
|
|
7808
|
-
};
|
|
7814
|
+
const drawPaneButtons = drawIndicatorControls;
|
|
7809
7815
|
activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
|
|
7810
7816
|
const paneHeight = separatePaneHeights[paneIndex] ?? 80;
|
|
7811
7817
|
const paneTop = paneTopCursor;
|
|
@@ -8307,26 +8313,56 @@ function createChart(element, options = {}) {
|
|
|
8307
8313
|
};
|
|
8308
8314
|
const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
|
|
8309
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));
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
if (labels.showIndicatorNames) {
|
|
8314
|
-
return plugin.name;
|
|
8315
|
-
}
|
|
8316
|
-
return inputValues.join(" ");
|
|
8317
|
-
}).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);
|
|
8318
8319
|
if (labelEntries.length > 0) {
|
|
8319
8320
|
const prevFont = ctx.font;
|
|
8320
|
-
|
|
8321
|
-
|
|
8321
|
+
const legendFontSize = Math.max(8, axis.fontSize);
|
|
8322
|
+
ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
|
|
8322
8323
|
const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
|
|
8323
8324
|
const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
|
|
8324
8325
|
const position = labels.indicatorLegendPosition;
|
|
8325
8326
|
const isRight = position === "top-right" || position === "bottom-right";
|
|
8326
8327
|
const isBottom = position === "bottom-left" || position === "bottom-right";
|
|
8327
8328
|
const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
|
|
8328
|
-
const
|
|
8329
|
-
|
|
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
|
+
});
|
|
8330
8366
|
ctx.font = prevFont;
|
|
8331
8367
|
}
|
|
8332
8368
|
}
|
|
@@ -8998,6 +9034,9 @@ function createChart(element, options = {}) {
|
|
|
8998
9034
|
if (hits.length === 0) return null;
|
|
8999
9035
|
return hits.find((region) => region.part === "remove") ?? hits[0];
|
|
9000
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;
|
|
9001
9040
|
const getMarkAt = (x, y) => {
|
|
9002
9041
|
const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
|
|
9003
9042
|
for (let index = markRegions.length - 1; index >= 0; index -= 1) {
|
|
@@ -10747,6 +10786,12 @@ function createChart(element, options = {}) {
|
|
|
10747
10786
|
setCrosshairPoint(null);
|
|
10748
10787
|
return;
|
|
10749
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
|
+
}
|
|
10750
10795
|
const hoveredPane = getPaneAt(point.x, point.y);
|
|
10751
10796
|
const nextHoveredPaneId = hoveredPane?.id ?? null;
|
|
10752
10797
|
if (nextHoveredPaneId !== hoveredPaneId) {
|
|
@@ -10965,10 +11010,11 @@ function createChart(element, options = {}) {
|
|
|
10965
11010
|
});
|
|
10966
11011
|
paneDividerDrag = null;
|
|
10967
11012
|
}
|
|
10968
|
-
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)) {
|
|
10969
11014
|
hoveredPaneId = null;
|
|
10970
11015
|
hoveredPaneButton = null;
|
|
10971
11016
|
hoveredAlertId = null;
|
|
11017
|
+
hoveredLegendId = null;
|
|
10972
11018
|
if (hoveredMarkKey !== null) {
|
|
10973
11019
|
hoveredMarkKey = null;
|
|
10974
11020
|
markHoverHandler?.(null);
|
package/docs/API.md
CHANGED
|
@@ -690,6 +690,27 @@ runs 18:00–17:00, but the levels traders mark are relative to the 09:30 equity
|
|
|
690
690
|
open inside it. Everything in the current session outside those hours counts as
|
|
691
691
|
overnight.
|
|
692
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
|
+
|
|
693
714
|
### `anchored-vwap`
|
|
694
715
|
|
|
695
716
|
VWAP measured from a bar you choose rather than the session open.
|