hyperprop-charting-library 0.1.48 → 0.1.50
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 +256 -34
- package/dist/hyperprop-charting-library.d.ts +38 -2
- package/dist/hyperprop-charting-library.js +256 -34
- package/dist/index.cjs +256 -34
- package/dist/index.d.cts +38 -2
- package/dist/index.d.ts +38 -2
- package/dist/index.js +256 -34
- package/docs/API.md +45 -3
- package/docs/RECIPES.md +100 -0
- package/package.json +1 -1
|
@@ -98,6 +98,9 @@ var DEFAULT_LABELS_OPTIONS = {
|
|
|
98
98
|
askPrice: Number.NaN,
|
|
99
99
|
showIndicatorNames: false,
|
|
100
100
|
showIndicatorValues: false,
|
|
101
|
+
indicatorLegendPosition: "top-left",
|
|
102
|
+
indicatorLegendOffsetX: 10,
|
|
103
|
+
indicatorLegendOffsetY: 10,
|
|
101
104
|
showCountdownToBarClose: false,
|
|
102
105
|
noOverlapping: true,
|
|
103
106
|
backgroundColor: "#0b1220",
|
|
@@ -531,7 +534,7 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
|
531
534
|
}
|
|
532
535
|
ctx.restore();
|
|
533
536
|
};
|
|
534
|
-
var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines) => {
|
|
537
|
+
var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
|
|
535
538
|
const visible = [];
|
|
536
539
|
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
537
540
|
const value = values[index];
|
|
@@ -539,7 +542,7 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
|
|
|
539
542
|
visible.push(value);
|
|
540
543
|
}
|
|
541
544
|
}
|
|
542
|
-
if (visible.length === 0) return;
|
|
545
|
+
if (visible.length === 0) return void 0;
|
|
543
546
|
const minValue = minOverride ?? Math.min(...visible);
|
|
544
547
|
const maxValue = maxOverride ?? Math.max(...visible);
|
|
545
548
|
const range = maxValue - minValue || 1;
|
|
@@ -588,6 +591,51 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
|
|
|
588
591
|
}
|
|
589
592
|
}
|
|
590
593
|
ctx.restore();
|
|
594
|
+
let latestValue = null;
|
|
595
|
+
for (let index = values.length - 1; index >= 0; index -= 1) {
|
|
596
|
+
const value = values[index];
|
|
597
|
+
if (Number.isFinite(value ?? Number.NaN)) {
|
|
598
|
+
latestValue = value;
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const decimals = options.decimals ?? 2;
|
|
603
|
+
const formatValue = (value) => value.toFixed(decimals);
|
|
604
|
+
const axisTicks = options.axisTicks ?? guideLines;
|
|
605
|
+
const paneInfo = {
|
|
606
|
+
...options.title ? { title: options.title } : {},
|
|
607
|
+
axis: {
|
|
608
|
+
min: minValue,
|
|
609
|
+
max: maxValue,
|
|
610
|
+
...axisTicks ? { ticks: axisTicks } : {},
|
|
611
|
+
decimals
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
if (guideLines) {
|
|
615
|
+
paneInfo.guideLines = guideLines.map((value) => ({ value, label: formatValue(value), style: "dotted" }));
|
|
616
|
+
}
|
|
617
|
+
if (latestValue !== null) {
|
|
618
|
+
paneInfo.legendValues = [
|
|
619
|
+
{
|
|
620
|
+
...options.legendLabel ? { label: options.legendLabel } : {},
|
|
621
|
+
value: latestValue,
|
|
622
|
+
text: formatValue(latestValue),
|
|
623
|
+
color
|
|
624
|
+
}
|
|
625
|
+
];
|
|
626
|
+
}
|
|
627
|
+
if (options.valueLabel !== false && latestValue !== null) {
|
|
628
|
+
paneInfo.valueLabels = [
|
|
629
|
+
{
|
|
630
|
+
value: latestValue,
|
|
631
|
+
text: formatValue(latestValue),
|
|
632
|
+
color,
|
|
633
|
+
backgroundColor: color,
|
|
634
|
+
textColor: "#0f172a"
|
|
635
|
+
}
|
|
636
|
+
];
|
|
637
|
+
}
|
|
638
|
+
return paneInfo;
|
|
591
639
|
};
|
|
592
640
|
var getPercentileValue = (values, percentile) => {
|
|
593
641
|
if (values.length === 0) {
|
|
@@ -760,7 +808,10 @@ var BUILTIN_STDDEV_INDICATOR = {
|
|
|
760
808
|
renderContext.data,
|
|
761
809
|
() => computeStdDevSeries(renderContext.data, length, inputs.source ?? "close")
|
|
762
810
|
);
|
|
763
|
-
drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2
|
|
811
|
+
return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2, void 0, void 0, void 0, {
|
|
812
|
+
title: `StdDev ${length}`,
|
|
813
|
+
decimals: 2
|
|
814
|
+
});
|
|
764
815
|
}
|
|
765
816
|
};
|
|
766
817
|
var BUILTIN_ATR_INDICATOR = {
|
|
@@ -772,7 +823,10 @@ var BUILTIN_ATR_INDICATOR = {
|
|
|
772
823
|
draw: (ctx, renderContext, inputs) => {
|
|
773
824
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
774
825
|
const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
|
|
775
|
-
drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2
|
|
826
|
+
return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2, void 0, void 0, void 0, {
|
|
827
|
+
title: `ATR ${length}`,
|
|
828
|
+
decimals: 2
|
|
829
|
+
});
|
|
776
830
|
}
|
|
777
831
|
};
|
|
778
832
|
var BUILTIN_RSI_INDICATOR = {
|
|
@@ -784,7 +838,7 @@ var BUILTIN_RSI_INDICATOR = {
|
|
|
784
838
|
draw: (ctx, renderContext, inputs) => {
|
|
785
839
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
786
840
|
const values = withCachedSeries(`rsi|${length}`, renderContext.data, () => computeRsiSeries(renderContext.data, length));
|
|
787
|
-
drawSeparateSeries(
|
|
841
|
+
return drawSeparateSeries(
|
|
788
842
|
ctx,
|
|
789
843
|
renderContext,
|
|
790
844
|
values,
|
|
@@ -792,7 +846,12 @@ var BUILTIN_RSI_INDICATOR = {
|
|
|
792
846
|
Number(inputs.width) || 2,
|
|
793
847
|
0,
|
|
794
848
|
100,
|
|
795
|
-
[30, 50, 70]
|
|
849
|
+
[30, 50, 70],
|
|
850
|
+
{
|
|
851
|
+
title: `RSI ${length}`,
|
|
852
|
+
axisTicks: [0, 30, 50, 70, 100],
|
|
853
|
+
decimals: 2
|
|
854
|
+
}
|
|
796
855
|
);
|
|
797
856
|
}
|
|
798
857
|
};
|
|
@@ -891,6 +950,9 @@ function createChart(element, options = {}) {
|
|
|
891
950
|
let crosshairPoint = null;
|
|
892
951
|
let doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
893
952
|
let doubleClickAction = mergedOptions.doubleClickAction;
|
|
953
|
+
let noOverlappingLineLabels = DEFAULT_LABELS_OPTIONS.noOverlapping;
|
|
954
|
+
let rightAxisLabelSlots = [];
|
|
955
|
+
let plotLabelSlots = [];
|
|
894
956
|
let smoothedTickerPrice = null;
|
|
895
957
|
let tickerPriceTarget = null;
|
|
896
958
|
let smoothedTickerVolume = null;
|
|
@@ -987,6 +1049,33 @@ function createChart(element, options = {}) {
|
|
|
987
1049
|
const clamp = (value, min, max) => {
|
|
988
1050
|
return Math.min(max, Math.max(min, value));
|
|
989
1051
|
};
|
|
1052
|
+
const resetLabelSlots = (enabled) => {
|
|
1053
|
+
noOverlappingLineLabels = enabled;
|
|
1054
|
+
rightAxisLabelSlots = [];
|
|
1055
|
+
plotLabelSlots = [];
|
|
1056
|
+
};
|
|
1057
|
+
const placeLabelSlot = (targetY, height2, minY, maxY, slots, gap = 2) => {
|
|
1058
|
+
const safeMaxY = Math.max(minY, maxY);
|
|
1059
|
+
const clampedTarget = clamp(targetY, minY, safeMaxY);
|
|
1060
|
+
if (!noOverlappingLineLabels || height2 <= 0) {
|
|
1061
|
+
return clampedTarget;
|
|
1062
|
+
}
|
|
1063
|
+
const overlaps = (y) => slots.some((slot) => y < slot.y + slot.height + gap && y + height2 + gap > slot.y);
|
|
1064
|
+
const candidates = [clampedTarget];
|
|
1065
|
+
for (const slot of slots) {
|
|
1066
|
+
candidates.push(slot.y - height2 - gap, slot.y + slot.height + gap);
|
|
1067
|
+
}
|
|
1068
|
+
const placedY = candidates.map((candidate) => clamp(candidate, minY, safeMaxY)).sort((a, b) => Math.abs(a - clampedTarget) - Math.abs(b - clampedTarget)).find((candidate) => !overlaps(candidate)) ?? clampedTarget;
|
|
1069
|
+
slots.push({ y: placedY, height: height2 });
|
|
1070
|
+
slots.sort((a, b) => a.y - b.y);
|
|
1071
|
+
return placedY;
|
|
1072
|
+
};
|
|
1073
|
+
const placeRightAxisLabel = (targetY, height2, minY, maxY) => {
|
|
1074
|
+
return placeLabelSlot(targetY, height2, minY, maxY, rightAxisLabelSlots);
|
|
1075
|
+
};
|
|
1076
|
+
const placePlotLabel = (targetY, height2, minY, maxY) => {
|
|
1077
|
+
return placeLabelSlot(targetY, height2, minY, maxY, plotLabelSlots);
|
|
1078
|
+
};
|
|
990
1079
|
const dashPatterns = {
|
|
991
1080
|
dotted: mergedOptions.dashPatterns.dotted ?? DEFAULT_DASH_PATTERNS.dotted,
|
|
992
1081
|
dashed: mergedOptions.dashPatterns.dashed ?? DEFAULT_DASH_PATTERNS.dashed,
|
|
@@ -1326,6 +1415,11 @@ function createChart(element, options = {}) {
|
|
|
1326
1415
|
const pad = (value) => String(value).padStart(2, "0");
|
|
1327
1416
|
return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
|
|
1328
1417
|
};
|
|
1418
|
+
const getRightAxisLabelX = (chartRight) => chartRight + 1;
|
|
1419
|
+
const getRightAxisLabelWidth = (chartRight, contentWidth) => {
|
|
1420
|
+
const scaleWidth = Math.max(0, Math.floor(width - getRightAxisLabelX(chartRight)));
|
|
1421
|
+
return Math.max(contentWidth, scaleWidth);
|
|
1422
|
+
};
|
|
1329
1423
|
const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
|
|
1330
1424
|
const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
|
|
1331
1425
|
if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
|
|
@@ -1350,9 +1444,10 @@ function createChart(element, options = {}) {
|
|
|
1350
1444
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1351
1445
|
const labelPaddingX = 8;
|
|
1352
1446
|
const labelHeight = 20;
|
|
1353
|
-
const
|
|
1354
|
-
const
|
|
1355
|
-
const
|
|
1447
|
+
const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
1448
|
+
const labelWidth = getRightAxisLabelWidth(chartRight, measuredLabelWidth);
|
|
1449
|
+
const labelX = getRightAxisLabelX(chartRight);
|
|
1450
|
+
const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
|
|
1356
1451
|
ctx.fillStyle = mergedLine.labelBackgroundColor;
|
|
1357
1452
|
fillRoundedRect(
|
|
1358
1453
|
Math.round(labelX),
|
|
@@ -1485,17 +1580,18 @@ function createChart(element, options = {}) {
|
|
|
1485
1580
|
leftWidgetX = maxWidgetX;
|
|
1486
1581
|
}
|
|
1487
1582
|
leftWidgetX = clamp(leftWidgetX, leftWidgetXBase, maxWidgetX);
|
|
1488
|
-
const
|
|
1583
|
+
const targetLabelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1584
|
+
const widgetY = placePlotLabel(targetLabelY, labelHeight, chartTop, chartBottom - labelHeight);
|
|
1489
1585
|
const borderRadius = Math.max(0, mergedLine.labelBorderRadius);
|
|
1490
1586
|
const widgetBackground = mergedOptions.backgroundColor;
|
|
1491
1587
|
const widgetBorder = color;
|
|
1492
1588
|
const textColor = line.labelTextColor ?? (mergedLine.type === "takeProfit" ? "#5eead4" : mergedLine.type === "stop" ? "#fbbf24" : mergedLine.labelTextColor);
|
|
1493
1589
|
const mainWidgetX = leftWidgetX + actionButtonsTotalWidth + actionButtonsGap;
|
|
1494
1590
|
ctx.fillStyle = widgetBackground;
|
|
1495
|
-
fillRoundedRect(Math.round(mainWidgetX), Math.round(
|
|
1591
|
+
fillRoundedRect(Math.round(mainWidgetX), Math.round(widgetY), mainWidgetWidth, labelHeight, borderRadius);
|
|
1496
1592
|
ctx.strokeStyle = widgetBorder;
|
|
1497
1593
|
ctx.lineWidth = 1;
|
|
1498
|
-
strokeRoundedRect(Math.round(mainWidgetX), Math.round(
|
|
1594
|
+
strokeRoundedRect(Math.round(mainWidgetX), Math.round(widgetY), mainWidgetWidth, labelHeight, borderRadius);
|
|
1499
1595
|
let cursorX = mainWidgetX;
|
|
1500
1596
|
const separatorColor = "rgba(148,163,184,0.45)";
|
|
1501
1597
|
if (actionButtonMetrics.length > 0) {
|
|
@@ -1505,7 +1601,7 @@ function createChart(element, options = {}) {
|
|
|
1505
1601
|
const fullHeight = button.fullHeight ?? mergedLine.actionButtonFullHeight;
|
|
1506
1602
|
const actionPadding = fullHeight ? 0 : 2;
|
|
1507
1603
|
const actionX = actionCursorX + actionPadding;
|
|
1508
|
-
const actionY =
|
|
1604
|
+
const actionY = widgetY + actionPadding;
|
|
1509
1605
|
const actionH = labelHeight - actionPadding * 2;
|
|
1510
1606
|
const actionW = Math.max(8, width2 - actionPadding * 2);
|
|
1511
1607
|
const actionRadius = Math.max(1, button.borderRadius ?? mergedLine.actionButtonBorderRadius);
|
|
@@ -1545,29 +1641,29 @@ function createChart(element, options = {}) {
|
|
|
1545
1641
|
});
|
|
1546
1642
|
}
|
|
1547
1643
|
if (qtyWidth > 0) {
|
|
1548
|
-
drawText(qtyText, cursorX + segmentPaddingX,
|
|
1644
|
+
drawText(qtyText, cursorX + segmentPaddingX, widgetY + labelHeight / 2, "left", "middle", textColor);
|
|
1549
1645
|
cursorX += qtyWidth;
|
|
1550
1646
|
ctx.strokeStyle = separatorColor;
|
|
1551
1647
|
ctx.beginPath();
|
|
1552
|
-
ctx.moveTo(crisp(cursorX),
|
|
1553
|
-
ctx.lineTo(crisp(cursorX),
|
|
1648
|
+
ctx.moveTo(crisp(cursorX), widgetY + 4);
|
|
1649
|
+
ctx.lineTo(crisp(cursorX), widgetY + labelHeight - 4);
|
|
1554
1650
|
ctx.stroke();
|
|
1555
1651
|
}
|
|
1556
|
-
drawText(centerText, cursorX + segmentPaddingX,
|
|
1652
|
+
drawText(centerText, cursorX + segmentPaddingX, widgetY + labelHeight / 2, "left", "middle", textColor);
|
|
1557
1653
|
cursorX += centerWidth;
|
|
1558
1654
|
if (showCloseButton) {
|
|
1559
1655
|
ctx.strokeStyle = separatorColor;
|
|
1560
1656
|
ctx.beginPath();
|
|
1561
|
-
ctx.moveTo(crisp(cursorX),
|
|
1562
|
-
ctx.lineTo(crisp(cursorX),
|
|
1657
|
+
ctx.moveTo(crisp(cursorX), widgetY + 4);
|
|
1658
|
+
ctx.lineTo(crisp(cursorX), widgetY + labelHeight - 4);
|
|
1563
1659
|
ctx.stroke();
|
|
1564
|
-
drawText("x", cursorX + closeWidth / 2,
|
|
1660
|
+
drawText("x", cursorX + closeWidth / 2, widgetY + labelHeight / 2, "center", "middle", textColor);
|
|
1565
1661
|
if (mergedLine.id) {
|
|
1566
1662
|
orderActionRegions.push({
|
|
1567
1663
|
orderId: mergedLine.id,
|
|
1568
1664
|
action: closeAction,
|
|
1569
1665
|
x: cursorX,
|
|
1570
|
-
y:
|
|
1666
|
+
y: widgetY,
|
|
1571
1667
|
width: closeWidth,
|
|
1572
1668
|
height: labelHeight,
|
|
1573
1669
|
line: mergedLine
|
|
@@ -1588,17 +1684,18 @@ function createChart(element, options = {}) {
|
|
|
1588
1684
|
const priceText = formatPrice(renderPrice);
|
|
1589
1685
|
const pricePaddingX = 8;
|
|
1590
1686
|
const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
|
|
1591
|
-
const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
|
|
1687
|
+
const priceWidth = mergedLine.id === void 0 ? getRightAxisLabelWidth(chartRight, measuredPriceWidth) : getRightAxisLabelWidth(chartRight, Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0));
|
|
1592
1688
|
if (mergedLine.id) {
|
|
1593
1689
|
orderPriceTagWidthById.set(mergedLine.id, priceWidth);
|
|
1594
1690
|
}
|
|
1595
|
-
const priceX = chartRight
|
|
1691
|
+
const priceX = getRightAxisLabelX(chartRight);
|
|
1692
|
+
const priceY = placeRightAxisLabel(targetLabelY, labelHeight, chartTop, chartBottom - labelHeight);
|
|
1596
1693
|
ctx.fillStyle = mergedLine.labelBackgroundColor ?? color;
|
|
1597
|
-
fillRoundedRect(Math.round(priceX), Math.round(
|
|
1694
|
+
fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, borderRadius);
|
|
1598
1695
|
ctx.strokeStyle = widgetBorder;
|
|
1599
1696
|
ctx.lineWidth = 1;
|
|
1600
|
-
strokeRoundedRect(Math.round(priceX), Math.round(
|
|
1601
|
-
drawText(priceText, priceX + pricePaddingX,
|
|
1697
|
+
strokeRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, borderRadius);
|
|
1698
|
+
drawText(priceText, priceX + pricePaddingX, priceY + labelHeight / 2, "left", "middle", textColor);
|
|
1602
1699
|
};
|
|
1603
1700
|
const fillRoundedRect = (x, y, widthValue, heightValue, radiusValue) => {
|
|
1604
1701
|
const maxRadius = Math.min(widthValue, heightValue) / 2;
|
|
@@ -1666,9 +1763,63 @@ function createChart(element, options = {}) {
|
|
|
1666
1763
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1667
1764
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
1668
1765
|
ctx.fillRect(0, 0, width, height);
|
|
1766
|
+
const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
1767
|
+
const estimateRightMargin = () => {
|
|
1768
|
+
const paddingX = Math.max(4, labels.labelPaddingX);
|
|
1769
|
+
const measure = (text) => Math.ceil(ctx.measureText(text).width) + paddingX * 2;
|
|
1770
|
+
let required = margin.right - 1;
|
|
1771
|
+
const include = (text) => {
|
|
1772
|
+
const normalized = text?.trim();
|
|
1773
|
+
if (normalized) {
|
|
1774
|
+
required = Math.max(required, measure(normalized));
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
const ticker2 = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
1778
|
+
const lastPoint2 = data[data.length - 1];
|
|
1779
|
+
if ((ticker2.visible ?? true) && labels.showLastPrice && lastPoint2) {
|
|
1780
|
+
include(formatPrice(lastPoint2.c));
|
|
1781
|
+
if (ticker2.labelSubtext) include(String(ticker2.labelSubtext));
|
|
1782
|
+
for (const subtext of ticker2.labelSubtexts ?? []) {
|
|
1783
|
+
include(String(subtext));
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if (labels.showSymbolName) {
|
|
1787
|
+
include(labels.symbolName);
|
|
1788
|
+
}
|
|
1789
|
+
if (labels.showPreviousClose) {
|
|
1790
|
+
const previousCloseCandidate = Number.isFinite(labels.previousClosePrice) ? labels.previousClosePrice : data.length > 1 ? data[data.length - 2]?.c : Number.NaN;
|
|
1791
|
+
if (previousCloseCandidate !== void 0 && Number.isFinite(previousCloseCandidate)) {
|
|
1792
|
+
include(`PDC ${formatPrice(previousCloseCandidate)}`);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
if (labels.showHighLow && data.length > 0) {
|
|
1796
|
+
include(`H ${formatPrice(Math.max(...data.map((point) => point.h)))}`);
|
|
1797
|
+
include(`L ${formatPrice(Math.min(...data.map((point) => point.l)))}`);
|
|
1798
|
+
}
|
|
1799
|
+
if (labels.showBidAsk) {
|
|
1800
|
+
if (Number.isFinite(labels.bidPrice)) include(`B ${formatPrice(labels.bidPrice)}`);
|
|
1801
|
+
if (Number.isFinite(labels.askPrice)) include(`A ${formatPrice(labels.askPrice)}`);
|
|
1802
|
+
}
|
|
1803
|
+
for (const line of priceLines) {
|
|
1804
|
+
const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
|
|
1805
|
+
if (mergedLine.visible && Number.isFinite(mergedLine.price)) {
|
|
1806
|
+
include(mergedLine.label ?? formatPrice(mergedLine.price));
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
for (const line of orderLines) {
|
|
1810
|
+
const mergedLine = { ...DEFAULT_ORDER_LINE_OPTIONS, ...line };
|
|
1811
|
+
const renderPrice = mergedLine.behavior === "follow" && Number.isFinite(mergedLine.followPrice) ? mergedLine.followPrice : mergedLine.price;
|
|
1812
|
+
if (mergedLine.visible && Number.isFinite(renderPrice)) {
|
|
1813
|
+
include(formatPrice(renderPrice));
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
const maxRightMargin = Math.max(margin.right, width - margin.left - 160);
|
|
1817
|
+
return Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
|
|
1818
|
+
};
|
|
1819
|
+
const rightMargin = estimateRightMargin();
|
|
1669
1820
|
const chartLeft = margin.left;
|
|
1670
1821
|
const chartTop = margin.top;
|
|
1671
|
-
const chartWidth = width - margin.left -
|
|
1822
|
+
const chartWidth = width - margin.left - rightMargin;
|
|
1672
1823
|
const fullChartHeight = height - margin.top - margin.bottom;
|
|
1673
1824
|
const fullChartBottom = chartTop + fullChartHeight;
|
|
1674
1825
|
const chartRight = chartLeft + chartWidth;
|
|
@@ -2021,7 +2172,7 @@ function createChart(element, options = {}) {
|
|
|
2021
2172
|
ctx.beginPath();
|
|
2022
2173
|
ctx.rect(chartLeft + 1, paneTop + 1, Math.max(0, chartWidth - 2), Math.max(0, paneHeight - 2));
|
|
2023
2174
|
ctx.clip();
|
|
2024
|
-
plugin.draw(
|
|
2175
|
+
const paneInfo = plugin.draw(
|
|
2025
2176
|
ctx,
|
|
2026
2177
|
{
|
|
2027
2178
|
data,
|
|
@@ -2054,6 +2205,60 @@ function createChart(element, options = {}) {
|
|
|
2054
2205
|
ctx.lineTo(crisp(chartRight), crisp(paneTop));
|
|
2055
2206
|
ctx.stroke();
|
|
2056
2207
|
ctx.restore();
|
|
2208
|
+
const axisInfo = paneInfo?.axis;
|
|
2209
|
+
if (axisInfo && Number.isFinite(axisInfo.min) && Number.isFinite(axisInfo.max) && axisInfo.max !== axisInfo.min) {
|
|
2210
|
+
const paneRange = axisInfo.max - axisInfo.min;
|
|
2211
|
+
const yFromPaneValue = (value) => {
|
|
2212
|
+
const ratio = (value - axisInfo.min) / paneRange;
|
|
2213
|
+
return paneBottom - ratio * paneHeight;
|
|
2214
|
+
};
|
|
2215
|
+
const formatPaneValue = (value) => {
|
|
2216
|
+
if (axisInfo.format) {
|
|
2217
|
+
return axisInfo.format(value);
|
|
2218
|
+
}
|
|
2219
|
+
const decimals = axisInfo.decimals ?? (Math.abs(paneRange) <= 2 ? 2 : Math.abs(paneRange) <= 20 ? 1 : 0);
|
|
2220
|
+
return value.toFixed(Math.max(0, Math.min(8, Math.round(decimals))));
|
|
2221
|
+
};
|
|
2222
|
+
const axisTicks = axisInfo.ticks && axisInfo.ticks.length > 0 ? axisInfo.ticks : [axisInfo.min, axisInfo.min + paneRange / 2, axisInfo.max];
|
|
2223
|
+
const uniqueTicks = Array.from(new Set(axisTicks.filter((tick) => Number.isFinite(tick))));
|
|
2224
|
+
ctx.save();
|
|
2225
|
+
ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
2226
|
+
for (const tick of uniqueTicks) {
|
|
2227
|
+
if (tick < axisInfo.min || tick > axisInfo.max) {
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
drawText(formatPaneValue(tick), chartRight + 6, yFromPaneValue(tick), "left", "middle", yAxis.textColor);
|
|
2231
|
+
}
|
|
2232
|
+
ctx.restore();
|
|
2233
|
+
if (labels.visible) {
|
|
2234
|
+
const prevFont = ctx.font;
|
|
2235
|
+
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
2236
|
+
const legendTitle = paneInfo.title ?? plugin.name;
|
|
2237
|
+
const legendValues = paneInfo.legendValues ?? [];
|
|
2238
|
+
const legendParts = [
|
|
2239
|
+
legendTitle,
|
|
2240
|
+
...legendValues.map((value) => value.text ?? (value.value === void 0 ? "" : formatPaneValue(value.value))).filter(Boolean)
|
|
2241
|
+
].filter(Boolean);
|
|
2242
|
+
if (legendParts.length > 0) {
|
|
2243
|
+
drawText(legendParts.join(" "), chartLeft + 10, paneTop + 8, "left", "top", labels.indicatorTextColor);
|
|
2244
|
+
}
|
|
2245
|
+
for (const label of paneInfo.valueLabels ?? []) {
|
|
2246
|
+
if (!Number.isFinite(label.value) || label.value < axisInfo.min || label.value > axisInfo.max) {
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
const text = label.text ?? formatPaneValue(label.value);
|
|
2250
|
+
const labelPaddingX = 7;
|
|
2251
|
+
const labelHeight = Math.max(16, yAxisFontSize + 8);
|
|
2252
|
+
const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(ctx.measureText(text).width) + labelPaddingX * 2);
|
|
2253
|
+
const labelX = getRightAxisLabelX(chartRight);
|
|
2254
|
+
const labelY = clamp(yFromPaneValue(label.value) - labelHeight / 2, paneTop + 2, paneBottom - labelHeight - 2);
|
|
2255
|
+
ctx.fillStyle = label.backgroundColor ?? label.color ?? labels.backgroundColor;
|
|
2256
|
+
fillRoundedRect(Math.round(labelX), Math.round(labelY), labelWidth, labelHeight, Math.max(0, labels.borderRadius));
|
|
2257
|
+
drawText(text, labelX + labelPaddingX, labelY + labelHeight / 2, "left", "middle", label.textColor ?? labels.textColor);
|
|
2258
|
+
}
|
|
2259
|
+
ctx.font = prevFont;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2057
2262
|
});
|
|
2058
2263
|
}
|
|
2059
2264
|
if (crosshair.visible && crosshairPoint && crosshair.showVertical) {
|
|
@@ -2085,7 +2290,7 @@ function createChart(element, options = {}) {
|
|
|
2085
2290
|
drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
|
|
2086
2291
|
ctx.font = prevFont;
|
|
2087
2292
|
}
|
|
2088
|
-
|
|
2293
|
+
resetLabelSlots(labels.noOverlapping);
|
|
2089
2294
|
const priceAxisLabels = [];
|
|
2090
2295
|
const addPriceAxisLabel = (label) => {
|
|
2091
2296
|
if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
|
|
@@ -2248,12 +2453,13 @@ function createChart(element, options = {}) {
|
|
|
2248
2453
|
ctx.font = baseFont;
|
|
2249
2454
|
}
|
|
2250
2455
|
const labelTextWidth = Math.max(primaryWidth, subtextWidth);
|
|
2456
|
+
const labelWidth = getRightAxisLabelWidth(chartRight, labelTextWidth);
|
|
2251
2457
|
return {
|
|
2252
2458
|
...label,
|
|
2253
2459
|
subtexts,
|
|
2254
2460
|
subtextFontSize,
|
|
2255
2461
|
height: labelHeight,
|
|
2256
|
-
width:
|
|
2462
|
+
width: labelWidth,
|
|
2257
2463
|
targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
|
|
2258
2464
|
y: 0
|
|
2259
2465
|
};
|
|
@@ -2274,7 +2480,16 @@ function createChart(element, options = {}) {
|
|
|
2274
2480
|
}
|
|
2275
2481
|
}
|
|
2276
2482
|
}
|
|
2277
|
-
|
|
2483
|
+
if (labels.noOverlapping) {
|
|
2484
|
+
rightAxisLabelSlots.push(
|
|
2485
|
+
...positionedLabels.map((label) => ({
|
|
2486
|
+
y: label.y,
|
|
2487
|
+
height: label.height
|
|
2488
|
+
}))
|
|
2489
|
+
);
|
|
2490
|
+
rightAxisLabelSlots.sort((a, b) => a.y - b.y);
|
|
2491
|
+
}
|
|
2492
|
+
const labelX = getRightAxisLabelX(chartRight);
|
|
2278
2493
|
for (const label of positionedLabels) {
|
|
2279
2494
|
ctx.fillStyle = label.backgroundColor;
|
|
2280
2495
|
fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width, label.height, labelRadius);
|
|
@@ -2319,7 +2534,14 @@ function createChart(element, options = {}) {
|
|
|
2319
2534
|
const prevFont = ctx.font;
|
|
2320
2535
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
2321
2536
|
const legendText = labelEntries.join(" ");
|
|
2322
|
-
|
|
2537
|
+
const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
|
|
2538
|
+
const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
|
|
2539
|
+
const position = labels.indicatorLegendPosition;
|
|
2540
|
+
const isRight = position === "top-right" || position === "bottom-right";
|
|
2541
|
+
const isBottom = position === "bottom-left" || position === "bottom-right";
|
|
2542
|
+
const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
|
|
2543
|
+
const legendY = isBottom ? chartBottom - offsetY : chartTop + offsetY;
|
|
2544
|
+
drawText(legendText, legendX, legendY, isRight ? "right" : "left", isBottom ? "bottom" : "top", labels.indicatorTextColor);
|
|
2323
2545
|
ctx.font = prevFont;
|
|
2324
2546
|
}
|
|
2325
2547
|
}
|
|
@@ -2377,8 +2599,8 @@ function createChart(element, options = {}) {
|
|
|
2377
2599
|
if (crosshair.showPriceLabel) {
|
|
2378
2600
|
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
2379
2601
|
const priceText = formatPrice(hoverPrice);
|
|
2380
|
-
const priceWidth = getPriceLabelWidth(priceText, labelPaddingX);
|
|
2381
|
-
const priceX = chartRight
|
|
2602
|
+
const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
|
|
2603
|
+
const priceX = getRightAxisLabelX(chartRight);
|
|
2382
2604
|
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
2383
2605
|
ctx.fillStyle = labelBackground;
|
|
2384
2606
|
fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
|
|
@@ -80,13 +80,46 @@ interface IndicatorRenderContext {
|
|
|
80
80
|
upColor: string;
|
|
81
81
|
downColor: string;
|
|
82
82
|
}
|
|
83
|
+
interface IndicatorPaneAxisOptions {
|
|
84
|
+
min: number;
|
|
85
|
+
max: number;
|
|
86
|
+
ticks?: number[];
|
|
87
|
+
decimals?: number;
|
|
88
|
+
format?: (value: number) => string;
|
|
89
|
+
}
|
|
90
|
+
interface IndicatorPaneGuideLine {
|
|
91
|
+
value: number;
|
|
92
|
+
label?: string;
|
|
93
|
+
color?: string;
|
|
94
|
+
style?: "solid" | "dotted" | "dashed";
|
|
95
|
+
}
|
|
96
|
+
interface IndicatorPaneValue {
|
|
97
|
+
label?: string;
|
|
98
|
+
value?: number;
|
|
99
|
+
text?: string;
|
|
100
|
+
color?: string;
|
|
101
|
+
}
|
|
102
|
+
interface IndicatorPaneValueLabel {
|
|
103
|
+
value: number;
|
|
104
|
+
text?: string;
|
|
105
|
+
color?: string;
|
|
106
|
+
backgroundColor?: string;
|
|
107
|
+
textColor?: string;
|
|
108
|
+
}
|
|
109
|
+
interface IndicatorPaneRenderInfo {
|
|
110
|
+
title?: string;
|
|
111
|
+
axis?: IndicatorPaneAxisOptions;
|
|
112
|
+
guideLines?: IndicatorPaneGuideLine[];
|
|
113
|
+
legendValues?: IndicatorPaneValue[];
|
|
114
|
+
valueLabels?: IndicatorPaneValueLabel[];
|
|
115
|
+
}
|
|
83
116
|
interface IndicatorPlugin<TInputs extends Record<string, unknown> = Record<string, unknown>> {
|
|
84
117
|
id: string;
|
|
85
118
|
name: string;
|
|
86
119
|
pane?: IndicatorPane;
|
|
87
120
|
paneHeightRatio?: number;
|
|
88
121
|
defaultInputs?: TInputs;
|
|
89
|
-
draw: (ctx: CanvasRenderingContext2D, renderContext: IndicatorRenderContext, inputs: TInputs) => void;
|
|
122
|
+
draw: (ctx: CanvasRenderingContext2D, renderContext: IndicatorRenderContext, inputs: TInputs) => void | IndicatorPaneRenderInfo;
|
|
90
123
|
}
|
|
91
124
|
interface BuiltInIndicatorInfo {
|
|
92
125
|
id: string;
|
|
@@ -282,6 +315,9 @@ interface LabelsOptions {
|
|
|
282
315
|
askPrice?: number;
|
|
283
316
|
showIndicatorNames?: boolean;
|
|
284
317
|
showIndicatorValues?: boolean;
|
|
318
|
+
indicatorLegendPosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
319
|
+
indicatorLegendOffsetX?: number;
|
|
320
|
+
indicatorLegendOffsetY?: number;
|
|
285
321
|
showCountdownToBarClose?: boolean;
|
|
286
322
|
noOverlapping?: boolean;
|
|
287
323
|
backgroundColor?: string;
|
|
@@ -361,4 +397,4 @@ interface ViewportState {
|
|
|
361
397
|
}
|
|
362
398
|
declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
|
|
363
399
|
|
|
364
|
-
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type ViewportState, type WatermarkOptions, createChart };
|
|
400
|
+
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type ViewportState, type WatermarkOptions, createChart };
|