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.
@@ -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 labelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
1354
- const labelX = chartRight + 4;
1355
- const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
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 labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
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(labelY), mainWidgetWidth, labelHeight, borderRadius);
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(labelY), mainWidgetWidth, labelHeight, borderRadius);
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 = labelY + actionPadding;
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, labelY + labelHeight / 2, "left", "middle", textColor);
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), labelY + 4);
1553
- ctx.lineTo(crisp(cursorX), labelY + labelHeight - 4);
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, labelY + labelHeight / 2, "left", "middle", textColor);
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), labelY + 4);
1562
- ctx.lineTo(crisp(cursorX), labelY + labelHeight - 4);
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, labelY + labelHeight / 2, "center", "middle", textColor);
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: labelY,
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 + 4;
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(labelY), priceWidth, labelHeight, borderRadius);
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(labelY), priceWidth, labelHeight, borderRadius);
1601
- drawText(priceText, priceX + pricePaddingX, labelY + labelHeight / 2, "left", "middle", textColor);
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 - margin.right;
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
- const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
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: labelTextWidth,
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
- const labelX = chartRight + 4;
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
- drawText(legendText, chartLeft + 10, chartTop + 10, "left", "top", labels.indicatorTextColor);
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 + 4;
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 };