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
package/dist/index.js
CHANGED
|
@@ -74,6 +74,9 @@ var DEFAULT_LABELS_OPTIONS = {
|
|
|
74
74
|
askPrice: Number.NaN,
|
|
75
75
|
showIndicatorNames: false,
|
|
76
76
|
showIndicatorValues: false,
|
|
77
|
+
indicatorLegendPosition: "top-left",
|
|
78
|
+
indicatorLegendOffsetX: 10,
|
|
79
|
+
indicatorLegendOffsetY: 10,
|
|
77
80
|
showCountdownToBarClose: false,
|
|
78
81
|
noOverlapping: true,
|
|
79
82
|
backgroundColor: "#0b1220",
|
|
@@ -507,7 +510,7 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
|
507
510
|
}
|
|
508
511
|
ctx.restore();
|
|
509
512
|
};
|
|
510
|
-
var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines) => {
|
|
513
|
+
var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
|
|
511
514
|
const visible = [];
|
|
512
515
|
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
513
516
|
const value = values[index];
|
|
@@ -515,7 +518,7 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
|
|
|
515
518
|
visible.push(value);
|
|
516
519
|
}
|
|
517
520
|
}
|
|
518
|
-
if (visible.length === 0) return;
|
|
521
|
+
if (visible.length === 0) return void 0;
|
|
519
522
|
const minValue = minOverride ?? Math.min(...visible);
|
|
520
523
|
const maxValue = maxOverride ?? Math.max(...visible);
|
|
521
524
|
const range = maxValue - minValue || 1;
|
|
@@ -564,6 +567,51 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
|
|
|
564
567
|
}
|
|
565
568
|
}
|
|
566
569
|
ctx.restore();
|
|
570
|
+
let latestValue = null;
|
|
571
|
+
for (let index = values.length - 1; index >= 0; index -= 1) {
|
|
572
|
+
const value = values[index];
|
|
573
|
+
if (Number.isFinite(value ?? Number.NaN)) {
|
|
574
|
+
latestValue = value;
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const decimals = options.decimals ?? 2;
|
|
579
|
+
const formatValue = (value) => value.toFixed(decimals);
|
|
580
|
+
const axisTicks = options.axisTicks ?? guideLines;
|
|
581
|
+
const paneInfo = {
|
|
582
|
+
...options.title ? { title: options.title } : {},
|
|
583
|
+
axis: {
|
|
584
|
+
min: minValue,
|
|
585
|
+
max: maxValue,
|
|
586
|
+
...axisTicks ? { ticks: axisTicks } : {},
|
|
587
|
+
decimals
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
if (guideLines) {
|
|
591
|
+
paneInfo.guideLines = guideLines.map((value) => ({ value, label: formatValue(value), style: "dotted" }));
|
|
592
|
+
}
|
|
593
|
+
if (latestValue !== null) {
|
|
594
|
+
paneInfo.legendValues = [
|
|
595
|
+
{
|
|
596
|
+
...options.legendLabel ? { label: options.legendLabel } : {},
|
|
597
|
+
value: latestValue,
|
|
598
|
+
text: formatValue(latestValue),
|
|
599
|
+
color
|
|
600
|
+
}
|
|
601
|
+
];
|
|
602
|
+
}
|
|
603
|
+
if (options.valueLabel !== false && latestValue !== null) {
|
|
604
|
+
paneInfo.valueLabels = [
|
|
605
|
+
{
|
|
606
|
+
value: latestValue,
|
|
607
|
+
text: formatValue(latestValue),
|
|
608
|
+
color,
|
|
609
|
+
backgroundColor: color,
|
|
610
|
+
textColor: "#0f172a"
|
|
611
|
+
}
|
|
612
|
+
];
|
|
613
|
+
}
|
|
614
|
+
return paneInfo;
|
|
567
615
|
};
|
|
568
616
|
var getPercentileValue = (values, percentile) => {
|
|
569
617
|
if (values.length === 0) {
|
|
@@ -736,7 +784,10 @@ var BUILTIN_STDDEV_INDICATOR = {
|
|
|
736
784
|
renderContext.data,
|
|
737
785
|
() => computeStdDevSeries(renderContext.data, length, inputs.source ?? "close")
|
|
738
786
|
);
|
|
739
|
-
drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2
|
|
787
|
+
return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2, void 0, void 0, void 0, {
|
|
788
|
+
title: `StdDev ${length}`,
|
|
789
|
+
decimals: 2
|
|
790
|
+
});
|
|
740
791
|
}
|
|
741
792
|
};
|
|
742
793
|
var BUILTIN_ATR_INDICATOR = {
|
|
@@ -748,7 +799,10 @@ var BUILTIN_ATR_INDICATOR = {
|
|
|
748
799
|
draw: (ctx, renderContext, inputs) => {
|
|
749
800
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
750
801
|
const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
|
|
751
|
-
drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2
|
|
802
|
+
return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2, void 0, void 0, void 0, {
|
|
803
|
+
title: `ATR ${length}`,
|
|
804
|
+
decimals: 2
|
|
805
|
+
});
|
|
752
806
|
}
|
|
753
807
|
};
|
|
754
808
|
var BUILTIN_RSI_INDICATOR = {
|
|
@@ -760,7 +814,7 @@ var BUILTIN_RSI_INDICATOR = {
|
|
|
760
814
|
draw: (ctx, renderContext, inputs) => {
|
|
761
815
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
762
816
|
const values = withCachedSeries(`rsi|${length}`, renderContext.data, () => computeRsiSeries(renderContext.data, length));
|
|
763
|
-
drawSeparateSeries(
|
|
817
|
+
return drawSeparateSeries(
|
|
764
818
|
ctx,
|
|
765
819
|
renderContext,
|
|
766
820
|
values,
|
|
@@ -768,7 +822,12 @@ var BUILTIN_RSI_INDICATOR = {
|
|
|
768
822
|
Number(inputs.width) || 2,
|
|
769
823
|
0,
|
|
770
824
|
100,
|
|
771
|
-
[30, 50, 70]
|
|
825
|
+
[30, 50, 70],
|
|
826
|
+
{
|
|
827
|
+
title: `RSI ${length}`,
|
|
828
|
+
axisTicks: [0, 30, 50, 70, 100],
|
|
829
|
+
decimals: 2
|
|
830
|
+
}
|
|
772
831
|
);
|
|
773
832
|
}
|
|
774
833
|
};
|
|
@@ -867,6 +926,9 @@ function createChart(element, options = {}) {
|
|
|
867
926
|
let crosshairPoint = null;
|
|
868
927
|
let doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
869
928
|
let doubleClickAction = mergedOptions.doubleClickAction;
|
|
929
|
+
let noOverlappingLineLabels = DEFAULT_LABELS_OPTIONS.noOverlapping;
|
|
930
|
+
let rightAxisLabelSlots = [];
|
|
931
|
+
let plotLabelSlots = [];
|
|
870
932
|
let smoothedTickerPrice = null;
|
|
871
933
|
let tickerPriceTarget = null;
|
|
872
934
|
let smoothedTickerVolume = null;
|
|
@@ -963,6 +1025,33 @@ function createChart(element, options = {}) {
|
|
|
963
1025
|
const clamp = (value, min, max) => {
|
|
964
1026
|
return Math.min(max, Math.max(min, value));
|
|
965
1027
|
};
|
|
1028
|
+
const resetLabelSlots = (enabled) => {
|
|
1029
|
+
noOverlappingLineLabels = enabled;
|
|
1030
|
+
rightAxisLabelSlots = [];
|
|
1031
|
+
plotLabelSlots = [];
|
|
1032
|
+
};
|
|
1033
|
+
const placeLabelSlot = (targetY, height2, minY, maxY, slots, gap = 2) => {
|
|
1034
|
+
const safeMaxY = Math.max(minY, maxY);
|
|
1035
|
+
const clampedTarget = clamp(targetY, minY, safeMaxY);
|
|
1036
|
+
if (!noOverlappingLineLabels || height2 <= 0) {
|
|
1037
|
+
return clampedTarget;
|
|
1038
|
+
}
|
|
1039
|
+
const overlaps = (y) => slots.some((slot) => y < slot.y + slot.height + gap && y + height2 + gap > slot.y);
|
|
1040
|
+
const candidates = [clampedTarget];
|
|
1041
|
+
for (const slot of slots) {
|
|
1042
|
+
candidates.push(slot.y - height2 - gap, slot.y + slot.height + gap);
|
|
1043
|
+
}
|
|
1044
|
+
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;
|
|
1045
|
+
slots.push({ y: placedY, height: height2 });
|
|
1046
|
+
slots.sort((a, b) => a.y - b.y);
|
|
1047
|
+
return placedY;
|
|
1048
|
+
};
|
|
1049
|
+
const placeRightAxisLabel = (targetY, height2, minY, maxY) => {
|
|
1050
|
+
return placeLabelSlot(targetY, height2, minY, maxY, rightAxisLabelSlots);
|
|
1051
|
+
};
|
|
1052
|
+
const placePlotLabel = (targetY, height2, minY, maxY) => {
|
|
1053
|
+
return placeLabelSlot(targetY, height2, minY, maxY, plotLabelSlots);
|
|
1054
|
+
};
|
|
966
1055
|
const dashPatterns = {
|
|
967
1056
|
dotted: mergedOptions.dashPatterns.dotted ?? DEFAULT_DASH_PATTERNS.dotted,
|
|
968
1057
|
dashed: mergedOptions.dashPatterns.dashed ?? DEFAULT_DASH_PATTERNS.dashed,
|
|
@@ -1302,6 +1391,11 @@ function createChart(element, options = {}) {
|
|
|
1302
1391
|
const pad = (value) => String(value).padStart(2, "0");
|
|
1303
1392
|
return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
|
|
1304
1393
|
};
|
|
1394
|
+
const getRightAxisLabelX = (chartRight) => chartRight + 1;
|
|
1395
|
+
const getRightAxisLabelWidth = (chartRight, contentWidth) => {
|
|
1396
|
+
const scaleWidth = Math.max(0, Math.floor(width - getRightAxisLabelX(chartRight)));
|
|
1397
|
+
return Math.max(contentWidth, scaleWidth);
|
|
1398
|
+
};
|
|
1305
1399
|
const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
|
|
1306
1400
|
const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
|
|
1307
1401
|
if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
|
|
@@ -1326,9 +1420,10 @@ function createChart(element, options = {}) {
|
|
|
1326
1420
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1327
1421
|
const labelPaddingX = 8;
|
|
1328
1422
|
const labelHeight = 20;
|
|
1329
|
-
const
|
|
1330
|
-
const
|
|
1331
|
-
const
|
|
1423
|
+
const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
1424
|
+
const labelWidth = getRightAxisLabelWidth(chartRight, measuredLabelWidth);
|
|
1425
|
+
const labelX = getRightAxisLabelX(chartRight);
|
|
1426
|
+
const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
|
|
1332
1427
|
ctx.fillStyle = mergedLine.labelBackgroundColor;
|
|
1333
1428
|
fillRoundedRect(
|
|
1334
1429
|
Math.round(labelX),
|
|
@@ -1461,17 +1556,18 @@ function createChart(element, options = {}) {
|
|
|
1461
1556
|
leftWidgetX = maxWidgetX;
|
|
1462
1557
|
}
|
|
1463
1558
|
leftWidgetX = clamp(leftWidgetX, leftWidgetXBase, maxWidgetX);
|
|
1464
|
-
const
|
|
1559
|
+
const targetLabelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1560
|
+
const widgetY = placePlotLabel(targetLabelY, labelHeight, chartTop, chartBottom - labelHeight);
|
|
1465
1561
|
const borderRadius = Math.max(0, mergedLine.labelBorderRadius);
|
|
1466
1562
|
const widgetBackground = mergedOptions.backgroundColor;
|
|
1467
1563
|
const widgetBorder = color;
|
|
1468
1564
|
const textColor = line.labelTextColor ?? (mergedLine.type === "takeProfit" ? "#5eead4" : mergedLine.type === "stop" ? "#fbbf24" : mergedLine.labelTextColor);
|
|
1469
1565
|
const mainWidgetX = leftWidgetX + actionButtonsTotalWidth + actionButtonsGap;
|
|
1470
1566
|
ctx.fillStyle = widgetBackground;
|
|
1471
|
-
fillRoundedRect(Math.round(mainWidgetX), Math.round(
|
|
1567
|
+
fillRoundedRect(Math.round(mainWidgetX), Math.round(widgetY), mainWidgetWidth, labelHeight, borderRadius);
|
|
1472
1568
|
ctx.strokeStyle = widgetBorder;
|
|
1473
1569
|
ctx.lineWidth = 1;
|
|
1474
|
-
strokeRoundedRect(Math.round(mainWidgetX), Math.round(
|
|
1570
|
+
strokeRoundedRect(Math.round(mainWidgetX), Math.round(widgetY), mainWidgetWidth, labelHeight, borderRadius);
|
|
1475
1571
|
let cursorX = mainWidgetX;
|
|
1476
1572
|
const separatorColor = "rgba(148,163,184,0.45)";
|
|
1477
1573
|
if (actionButtonMetrics.length > 0) {
|
|
@@ -1481,7 +1577,7 @@ function createChart(element, options = {}) {
|
|
|
1481
1577
|
const fullHeight = button.fullHeight ?? mergedLine.actionButtonFullHeight;
|
|
1482
1578
|
const actionPadding = fullHeight ? 0 : 2;
|
|
1483
1579
|
const actionX = actionCursorX + actionPadding;
|
|
1484
|
-
const actionY =
|
|
1580
|
+
const actionY = widgetY + actionPadding;
|
|
1485
1581
|
const actionH = labelHeight - actionPadding * 2;
|
|
1486
1582
|
const actionW = Math.max(8, width2 - actionPadding * 2);
|
|
1487
1583
|
const actionRadius = Math.max(1, button.borderRadius ?? mergedLine.actionButtonBorderRadius);
|
|
@@ -1521,29 +1617,29 @@ function createChart(element, options = {}) {
|
|
|
1521
1617
|
});
|
|
1522
1618
|
}
|
|
1523
1619
|
if (qtyWidth > 0) {
|
|
1524
|
-
drawText(qtyText, cursorX + segmentPaddingX,
|
|
1620
|
+
drawText(qtyText, cursorX + segmentPaddingX, widgetY + labelHeight / 2, "left", "middle", textColor);
|
|
1525
1621
|
cursorX += qtyWidth;
|
|
1526
1622
|
ctx.strokeStyle = separatorColor;
|
|
1527
1623
|
ctx.beginPath();
|
|
1528
|
-
ctx.moveTo(crisp(cursorX),
|
|
1529
|
-
ctx.lineTo(crisp(cursorX),
|
|
1624
|
+
ctx.moveTo(crisp(cursorX), widgetY + 4);
|
|
1625
|
+
ctx.lineTo(crisp(cursorX), widgetY + labelHeight - 4);
|
|
1530
1626
|
ctx.stroke();
|
|
1531
1627
|
}
|
|
1532
|
-
drawText(centerText, cursorX + segmentPaddingX,
|
|
1628
|
+
drawText(centerText, cursorX + segmentPaddingX, widgetY + labelHeight / 2, "left", "middle", textColor);
|
|
1533
1629
|
cursorX += centerWidth;
|
|
1534
1630
|
if (showCloseButton) {
|
|
1535
1631
|
ctx.strokeStyle = separatorColor;
|
|
1536
1632
|
ctx.beginPath();
|
|
1537
|
-
ctx.moveTo(crisp(cursorX),
|
|
1538
|
-
ctx.lineTo(crisp(cursorX),
|
|
1633
|
+
ctx.moveTo(crisp(cursorX), widgetY + 4);
|
|
1634
|
+
ctx.lineTo(crisp(cursorX), widgetY + labelHeight - 4);
|
|
1539
1635
|
ctx.stroke();
|
|
1540
|
-
drawText("x", cursorX + closeWidth / 2,
|
|
1636
|
+
drawText("x", cursorX + closeWidth / 2, widgetY + labelHeight / 2, "center", "middle", textColor);
|
|
1541
1637
|
if (mergedLine.id) {
|
|
1542
1638
|
orderActionRegions.push({
|
|
1543
1639
|
orderId: mergedLine.id,
|
|
1544
1640
|
action: closeAction,
|
|
1545
1641
|
x: cursorX,
|
|
1546
|
-
y:
|
|
1642
|
+
y: widgetY,
|
|
1547
1643
|
width: closeWidth,
|
|
1548
1644
|
height: labelHeight,
|
|
1549
1645
|
line: mergedLine
|
|
@@ -1564,17 +1660,18 @@ function createChart(element, options = {}) {
|
|
|
1564
1660
|
const priceText = formatPrice(renderPrice);
|
|
1565
1661
|
const pricePaddingX = 8;
|
|
1566
1662
|
const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
|
|
1567
|
-
const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
|
|
1663
|
+
const priceWidth = mergedLine.id === void 0 ? getRightAxisLabelWidth(chartRight, measuredPriceWidth) : getRightAxisLabelWidth(chartRight, Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0));
|
|
1568
1664
|
if (mergedLine.id) {
|
|
1569
1665
|
orderPriceTagWidthById.set(mergedLine.id, priceWidth);
|
|
1570
1666
|
}
|
|
1571
|
-
const priceX = chartRight
|
|
1667
|
+
const priceX = getRightAxisLabelX(chartRight);
|
|
1668
|
+
const priceY = placeRightAxisLabel(targetLabelY, labelHeight, chartTop, chartBottom - labelHeight);
|
|
1572
1669
|
ctx.fillStyle = mergedLine.labelBackgroundColor ?? color;
|
|
1573
|
-
fillRoundedRect(Math.round(priceX), Math.round(
|
|
1670
|
+
fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, borderRadius);
|
|
1574
1671
|
ctx.strokeStyle = widgetBorder;
|
|
1575
1672
|
ctx.lineWidth = 1;
|
|
1576
|
-
strokeRoundedRect(Math.round(priceX), Math.round(
|
|
1577
|
-
drawText(priceText, priceX + pricePaddingX,
|
|
1673
|
+
strokeRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, borderRadius);
|
|
1674
|
+
drawText(priceText, priceX + pricePaddingX, priceY + labelHeight / 2, "left", "middle", textColor);
|
|
1578
1675
|
};
|
|
1579
1676
|
const fillRoundedRect = (x, y, widthValue, heightValue, radiusValue) => {
|
|
1580
1677
|
const maxRadius = Math.min(widthValue, heightValue) / 2;
|
|
@@ -1642,9 +1739,63 @@ function createChart(element, options = {}) {
|
|
|
1642
1739
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1643
1740
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
1644
1741
|
ctx.fillRect(0, 0, width, height);
|
|
1742
|
+
const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
1743
|
+
const estimateRightMargin = () => {
|
|
1744
|
+
const paddingX = Math.max(4, labels.labelPaddingX);
|
|
1745
|
+
const measure = (text) => Math.ceil(ctx.measureText(text).width) + paddingX * 2;
|
|
1746
|
+
let required = margin.right - 1;
|
|
1747
|
+
const include = (text) => {
|
|
1748
|
+
const normalized = text?.trim();
|
|
1749
|
+
if (normalized) {
|
|
1750
|
+
required = Math.max(required, measure(normalized));
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
const ticker2 = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
1754
|
+
const lastPoint2 = data[data.length - 1];
|
|
1755
|
+
if ((ticker2.visible ?? true) && labels.showLastPrice && lastPoint2) {
|
|
1756
|
+
include(formatPrice(lastPoint2.c));
|
|
1757
|
+
if (ticker2.labelSubtext) include(String(ticker2.labelSubtext));
|
|
1758
|
+
for (const subtext of ticker2.labelSubtexts ?? []) {
|
|
1759
|
+
include(String(subtext));
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
if (labels.showSymbolName) {
|
|
1763
|
+
include(labels.symbolName);
|
|
1764
|
+
}
|
|
1765
|
+
if (labels.showPreviousClose) {
|
|
1766
|
+
const previousCloseCandidate = Number.isFinite(labels.previousClosePrice) ? labels.previousClosePrice : data.length > 1 ? data[data.length - 2]?.c : Number.NaN;
|
|
1767
|
+
if (previousCloseCandidate !== void 0 && Number.isFinite(previousCloseCandidate)) {
|
|
1768
|
+
include(`PDC ${formatPrice(previousCloseCandidate)}`);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
if (labels.showHighLow && data.length > 0) {
|
|
1772
|
+
include(`H ${formatPrice(Math.max(...data.map((point) => point.h)))}`);
|
|
1773
|
+
include(`L ${formatPrice(Math.min(...data.map((point) => point.l)))}`);
|
|
1774
|
+
}
|
|
1775
|
+
if (labels.showBidAsk) {
|
|
1776
|
+
if (Number.isFinite(labels.bidPrice)) include(`B ${formatPrice(labels.bidPrice)}`);
|
|
1777
|
+
if (Number.isFinite(labels.askPrice)) include(`A ${formatPrice(labels.askPrice)}`);
|
|
1778
|
+
}
|
|
1779
|
+
for (const line of priceLines) {
|
|
1780
|
+
const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
|
|
1781
|
+
if (mergedLine.visible && Number.isFinite(mergedLine.price)) {
|
|
1782
|
+
include(mergedLine.label ?? formatPrice(mergedLine.price));
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
for (const line of orderLines) {
|
|
1786
|
+
const mergedLine = { ...DEFAULT_ORDER_LINE_OPTIONS, ...line };
|
|
1787
|
+
const renderPrice = mergedLine.behavior === "follow" && Number.isFinite(mergedLine.followPrice) ? mergedLine.followPrice : mergedLine.price;
|
|
1788
|
+
if (mergedLine.visible && Number.isFinite(renderPrice)) {
|
|
1789
|
+
include(formatPrice(renderPrice));
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
const maxRightMargin = Math.max(margin.right, width - margin.left - 160);
|
|
1793
|
+
return Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
|
|
1794
|
+
};
|
|
1795
|
+
const rightMargin = estimateRightMargin();
|
|
1645
1796
|
const chartLeft = margin.left;
|
|
1646
1797
|
const chartTop = margin.top;
|
|
1647
|
-
const chartWidth = width - margin.left -
|
|
1798
|
+
const chartWidth = width - margin.left - rightMargin;
|
|
1648
1799
|
const fullChartHeight = height - margin.top - margin.bottom;
|
|
1649
1800
|
const fullChartBottom = chartTop + fullChartHeight;
|
|
1650
1801
|
const chartRight = chartLeft + chartWidth;
|
|
@@ -1997,7 +2148,7 @@ function createChart(element, options = {}) {
|
|
|
1997
2148
|
ctx.beginPath();
|
|
1998
2149
|
ctx.rect(chartLeft + 1, paneTop + 1, Math.max(0, chartWidth - 2), Math.max(0, paneHeight - 2));
|
|
1999
2150
|
ctx.clip();
|
|
2000
|
-
plugin.draw(
|
|
2151
|
+
const paneInfo = plugin.draw(
|
|
2001
2152
|
ctx,
|
|
2002
2153
|
{
|
|
2003
2154
|
data,
|
|
@@ -2030,6 +2181,60 @@ function createChart(element, options = {}) {
|
|
|
2030
2181
|
ctx.lineTo(crisp(chartRight), crisp(paneTop));
|
|
2031
2182
|
ctx.stroke();
|
|
2032
2183
|
ctx.restore();
|
|
2184
|
+
const axisInfo = paneInfo?.axis;
|
|
2185
|
+
if (axisInfo && Number.isFinite(axisInfo.min) && Number.isFinite(axisInfo.max) && axisInfo.max !== axisInfo.min) {
|
|
2186
|
+
const paneRange = axisInfo.max - axisInfo.min;
|
|
2187
|
+
const yFromPaneValue = (value) => {
|
|
2188
|
+
const ratio = (value - axisInfo.min) / paneRange;
|
|
2189
|
+
return paneBottom - ratio * paneHeight;
|
|
2190
|
+
};
|
|
2191
|
+
const formatPaneValue = (value) => {
|
|
2192
|
+
if (axisInfo.format) {
|
|
2193
|
+
return axisInfo.format(value);
|
|
2194
|
+
}
|
|
2195
|
+
const decimals = axisInfo.decimals ?? (Math.abs(paneRange) <= 2 ? 2 : Math.abs(paneRange) <= 20 ? 1 : 0);
|
|
2196
|
+
return value.toFixed(Math.max(0, Math.min(8, Math.round(decimals))));
|
|
2197
|
+
};
|
|
2198
|
+
const axisTicks = axisInfo.ticks && axisInfo.ticks.length > 0 ? axisInfo.ticks : [axisInfo.min, axisInfo.min + paneRange / 2, axisInfo.max];
|
|
2199
|
+
const uniqueTicks = Array.from(new Set(axisTicks.filter((tick) => Number.isFinite(tick))));
|
|
2200
|
+
ctx.save();
|
|
2201
|
+
ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
2202
|
+
for (const tick of uniqueTicks) {
|
|
2203
|
+
if (tick < axisInfo.min || tick > axisInfo.max) {
|
|
2204
|
+
continue;
|
|
2205
|
+
}
|
|
2206
|
+
drawText(formatPaneValue(tick), chartRight + 6, yFromPaneValue(tick), "left", "middle", yAxis.textColor);
|
|
2207
|
+
}
|
|
2208
|
+
ctx.restore();
|
|
2209
|
+
if (labels.visible) {
|
|
2210
|
+
const prevFont = ctx.font;
|
|
2211
|
+
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
2212
|
+
const legendTitle = paneInfo.title ?? plugin.name;
|
|
2213
|
+
const legendValues = paneInfo.legendValues ?? [];
|
|
2214
|
+
const legendParts = [
|
|
2215
|
+
legendTitle,
|
|
2216
|
+
...legendValues.map((value) => value.text ?? (value.value === void 0 ? "" : formatPaneValue(value.value))).filter(Boolean)
|
|
2217
|
+
].filter(Boolean);
|
|
2218
|
+
if (legendParts.length > 0) {
|
|
2219
|
+
drawText(legendParts.join(" "), chartLeft + 10, paneTop + 8, "left", "top", labels.indicatorTextColor);
|
|
2220
|
+
}
|
|
2221
|
+
for (const label of paneInfo.valueLabels ?? []) {
|
|
2222
|
+
if (!Number.isFinite(label.value) || label.value < axisInfo.min || label.value > axisInfo.max) {
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
const text = label.text ?? formatPaneValue(label.value);
|
|
2226
|
+
const labelPaddingX = 7;
|
|
2227
|
+
const labelHeight = Math.max(16, yAxisFontSize + 8);
|
|
2228
|
+
const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(ctx.measureText(text).width) + labelPaddingX * 2);
|
|
2229
|
+
const labelX = getRightAxisLabelX(chartRight);
|
|
2230
|
+
const labelY = clamp(yFromPaneValue(label.value) - labelHeight / 2, paneTop + 2, paneBottom - labelHeight - 2);
|
|
2231
|
+
ctx.fillStyle = label.backgroundColor ?? label.color ?? labels.backgroundColor;
|
|
2232
|
+
fillRoundedRect(Math.round(labelX), Math.round(labelY), labelWidth, labelHeight, Math.max(0, labels.borderRadius));
|
|
2233
|
+
drawText(text, labelX + labelPaddingX, labelY + labelHeight / 2, "left", "middle", label.textColor ?? labels.textColor);
|
|
2234
|
+
}
|
|
2235
|
+
ctx.font = prevFont;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2033
2238
|
});
|
|
2034
2239
|
}
|
|
2035
2240
|
if (crosshair.visible && crosshairPoint && crosshair.showVertical) {
|
|
@@ -2061,7 +2266,7 @@ function createChart(element, options = {}) {
|
|
|
2061
2266
|
drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
|
|
2062
2267
|
ctx.font = prevFont;
|
|
2063
2268
|
}
|
|
2064
|
-
|
|
2269
|
+
resetLabelSlots(labels.noOverlapping);
|
|
2065
2270
|
const priceAxisLabels = [];
|
|
2066
2271
|
const addPriceAxisLabel = (label) => {
|
|
2067
2272
|
if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
|
|
@@ -2224,12 +2429,13 @@ function createChart(element, options = {}) {
|
|
|
2224
2429
|
ctx.font = baseFont;
|
|
2225
2430
|
}
|
|
2226
2431
|
const labelTextWidth = Math.max(primaryWidth, subtextWidth);
|
|
2432
|
+
const labelWidth = getRightAxisLabelWidth(chartRight, labelTextWidth);
|
|
2227
2433
|
return {
|
|
2228
2434
|
...label,
|
|
2229
2435
|
subtexts,
|
|
2230
2436
|
subtextFontSize,
|
|
2231
2437
|
height: labelHeight,
|
|
2232
|
-
width:
|
|
2438
|
+
width: labelWidth,
|
|
2233
2439
|
targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
|
|
2234
2440
|
y: 0
|
|
2235
2441
|
};
|
|
@@ -2250,7 +2456,16 @@ function createChart(element, options = {}) {
|
|
|
2250
2456
|
}
|
|
2251
2457
|
}
|
|
2252
2458
|
}
|
|
2253
|
-
|
|
2459
|
+
if (labels.noOverlapping) {
|
|
2460
|
+
rightAxisLabelSlots.push(
|
|
2461
|
+
...positionedLabels.map((label) => ({
|
|
2462
|
+
y: label.y,
|
|
2463
|
+
height: label.height
|
|
2464
|
+
}))
|
|
2465
|
+
);
|
|
2466
|
+
rightAxisLabelSlots.sort((a, b) => a.y - b.y);
|
|
2467
|
+
}
|
|
2468
|
+
const labelX = getRightAxisLabelX(chartRight);
|
|
2254
2469
|
for (const label of positionedLabels) {
|
|
2255
2470
|
ctx.fillStyle = label.backgroundColor;
|
|
2256
2471
|
fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width, label.height, labelRadius);
|
|
@@ -2295,7 +2510,14 @@ function createChart(element, options = {}) {
|
|
|
2295
2510
|
const prevFont = ctx.font;
|
|
2296
2511
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
2297
2512
|
const legendText = labelEntries.join(" ");
|
|
2298
|
-
|
|
2513
|
+
const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
|
|
2514
|
+
const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
|
|
2515
|
+
const position = labels.indicatorLegendPosition;
|
|
2516
|
+
const isRight = position === "top-right" || position === "bottom-right";
|
|
2517
|
+
const isBottom = position === "bottom-left" || position === "bottom-right";
|
|
2518
|
+
const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
|
|
2519
|
+
const legendY = isBottom ? chartBottom - offsetY : chartTop + offsetY;
|
|
2520
|
+
drawText(legendText, legendX, legendY, isRight ? "right" : "left", isBottom ? "bottom" : "top", labels.indicatorTextColor);
|
|
2299
2521
|
ctx.font = prevFont;
|
|
2300
2522
|
}
|
|
2301
2523
|
}
|
|
@@ -2353,8 +2575,8 @@ function createChart(element, options = {}) {
|
|
|
2353
2575
|
if (crosshair.showPriceLabel) {
|
|
2354
2576
|
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
2355
2577
|
const priceText = formatPrice(hoverPrice);
|
|
2356
|
-
const priceWidth = getPriceLabelWidth(priceText, labelPaddingX);
|
|
2357
|
-
const priceX = chartRight
|
|
2578
|
+
const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
|
|
2579
|
+
const priceX = getRightAxisLabelX(chartRight);
|
|
2358
2580
|
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
2359
2581
|
ctx.fillStyle = labelBackground;
|
|
2360
2582
|
fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
|
package/docs/API.md
CHANGED
|
@@ -171,8 +171,11 @@ TradingView-style labels can be controlled from a single top-level object:
|
|
|
171
171
|
- `bidPrice`, `askPrice` (optional market data values for bid/ask labels)
|
|
172
172
|
- `showIndicatorNames` (default `false`; draws active indicator names in the chart)
|
|
173
173
|
- `showIndicatorValues` (default `false`; appends simple indicator input values)
|
|
174
|
+
- `indicatorLegendPosition` (`"top-left" | "top-right" | "bottom-left" | "bottom-right"`, default `"top-left"`)
|
|
175
|
+
- `indicatorLegendOffsetX` (default `10`)
|
|
176
|
+
- `indicatorLegendOffsetY` (default `10`; increase this if your frontend overlays a symbol/OHLC HUD in the top-left)
|
|
174
177
|
- `showCountdownToBarClose` (default `false`; draws a bottom-axis countdown based on candle time spacing)
|
|
175
|
-
- `noOverlapping` (default `true`; stacks price-scale labels so they do not cover each other)
|
|
178
|
+
- `noOverlapping` (default `true`; stacks price-scale labels, price-line tags, order/position price tags, and order widgets so they do not cover each other)
|
|
176
179
|
- Style fields: `backgroundColor`, `textColor`, `mutedTextColor`, `symbolNameBackgroundColor`, `symbolNameTextColor`, `previousCloseColor`, `highLowColor`, `bidColor`, `askColor`, `indicatorTextColor`, `borderRadius`, `labelHeight`, `labelPaddingX`
|
|
177
180
|
|
|
178
181
|
Example:
|
|
@@ -190,6 +193,8 @@ createChart(root, {
|
|
|
190
193
|
askPrice: 5235.0,
|
|
191
194
|
showIndicatorNames: true,
|
|
192
195
|
showIndicatorValues: true,
|
|
196
|
+
indicatorLegendPosition: "top-left",
|
|
197
|
+
indicatorLegendOffsetY: 34,
|
|
193
198
|
showCountdownToBarClose: true,
|
|
194
199
|
noOverlapping: true
|
|
195
200
|
}
|
|
@@ -311,7 +316,7 @@ Connector/fill visuals:
|
|
|
311
316
|
- `pane?: "overlay" | "separate"` (default `"overlay"`)
|
|
312
317
|
- `paneHeightRatio?: number` (for separate panes)
|
|
313
318
|
- `defaultInputs?: Record<string, unknown>`
|
|
314
|
-
- `draw(ctx, renderContext, inputs): void`
|
|
319
|
+
- `draw(ctx, renderContext, inputs): void | IndicatorPaneRenderInfo`
|
|
315
320
|
|
|
316
321
|
`IndicatorRenderContext` includes:
|
|
317
322
|
|
|
@@ -319,14 +324,51 @@ Connector/fill visuals:
|
|
|
319
324
|
- pane bounds (`chartLeft`, `chartRight`, `chartTop`, `chartBottom`, `chartWidth`, `chartHeight`)
|
|
320
325
|
- `xFromIndex(index)` helper
|
|
321
326
|
- `yFromPrice(price)` helper (available for overlay indicators, `null` for separate-pane indicators)
|
|
327
|
+
- `getCandleDirectionByIndex(index)` and `getVolumeByIndex(index)` helpers
|
|
322
328
|
- theme colors (`upColor`, `downColor`)
|
|
323
329
|
|
|
330
|
+
For separate-pane indicators, return `IndicatorPaneRenderInfo` from `draw()` when the core should render TradingView-style pane UI:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
type IndicatorPaneRenderInfo = {
|
|
334
|
+
title?: string;
|
|
335
|
+
axis?: {
|
|
336
|
+
min: number;
|
|
337
|
+
max: number;
|
|
338
|
+
ticks?: number[];
|
|
339
|
+
decimals?: number;
|
|
340
|
+
format?: (value: number) => string;
|
|
341
|
+
};
|
|
342
|
+
guideLines?: Array<{
|
|
343
|
+
value: number;
|
|
344
|
+
label?: string;
|
|
345
|
+
color?: string;
|
|
346
|
+
style?: "solid" | "dotted" | "dashed";
|
|
347
|
+
}>;
|
|
348
|
+
legendValues?: Array<{
|
|
349
|
+
label?: string;
|
|
350
|
+
value?: number;
|
|
351
|
+
text?: string;
|
|
352
|
+
color?: string;
|
|
353
|
+
}>;
|
|
354
|
+
valueLabels?: Array<{
|
|
355
|
+
value: number;
|
|
356
|
+
text?: string;
|
|
357
|
+
color?: string;
|
|
358
|
+
backgroundColor?: string;
|
|
359
|
+
textColor?: string;
|
|
360
|
+
}>;
|
|
361
|
+
};
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
The core uses this metadata to draw separate-pane right-side axis values, top-left pane legends, guide-level labels, and latest-value tags. Existing plugins can keep returning nothing.
|
|
365
|
+
|
|
324
366
|
Built-in:
|
|
325
367
|
|
|
326
368
|
- `"volume"`: overlay histogram by default (uses `OhlcDataPoint.v`; can be moved to separate pane)
|
|
327
369
|
- `"sma"`: Simple Moving Average (overlay)
|
|
328
370
|
- `"ema"`: Exponential Moving Average (overlay)
|
|
329
|
-
- `"rsi"`: Relative Strength Index (separate pane, 30/50/70 guides)
|
|
371
|
+
- `"rsi"`: Relative Strength Index (separate pane, 30/50/70 guides, 0/30/50/70/100 axis labels, latest-value tag)
|
|
330
372
|
- `"wma"`: Weighted Moving Average (overlay)
|
|
331
373
|
- `"vwma"`: Volume Weighted Moving Average (overlay, uses `OhlcDataPoint.v`)
|
|
332
374
|
- `"rma"`: Wilder's Moving Average (overlay)
|