hyperprop-charting-library 0.1.48 → 0.1.49

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.
@@ -891,6 +891,9 @@ function createChart(element, options = {}) {
891
891
  let crosshairPoint = null;
892
892
  let doubleClickEnabled = mergedOptions.doubleClickEnabled;
893
893
  let doubleClickAction = mergedOptions.doubleClickAction;
894
+ let noOverlappingLineLabels = DEFAULT_LABELS_OPTIONS.noOverlapping;
895
+ let rightAxisLabelSlots = [];
896
+ let plotLabelSlots = [];
894
897
  let smoothedTickerPrice = null;
895
898
  let tickerPriceTarget = null;
896
899
  let smoothedTickerVolume = null;
@@ -987,6 +990,33 @@ function createChart(element, options = {}) {
987
990
  const clamp = (value, min, max) => {
988
991
  return Math.min(max, Math.max(min, value));
989
992
  };
993
+ const resetLabelSlots = (enabled) => {
994
+ noOverlappingLineLabels = enabled;
995
+ rightAxisLabelSlots = [];
996
+ plotLabelSlots = [];
997
+ };
998
+ const placeLabelSlot = (targetY, height2, minY, maxY, slots, gap = 2) => {
999
+ const safeMaxY = Math.max(minY, maxY);
1000
+ const clampedTarget = clamp(targetY, minY, safeMaxY);
1001
+ if (!noOverlappingLineLabels || height2 <= 0) {
1002
+ return clampedTarget;
1003
+ }
1004
+ const overlaps = (y) => slots.some((slot) => y < slot.y + slot.height + gap && y + height2 + gap > slot.y);
1005
+ const candidates = [clampedTarget];
1006
+ for (const slot of slots) {
1007
+ candidates.push(slot.y - height2 - gap, slot.y + slot.height + gap);
1008
+ }
1009
+ 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;
1010
+ slots.push({ y: placedY, height: height2 });
1011
+ slots.sort((a, b) => a.y - b.y);
1012
+ return placedY;
1013
+ };
1014
+ const placeRightAxisLabel = (targetY, height2, minY, maxY) => {
1015
+ return placeLabelSlot(targetY, height2, minY, maxY, rightAxisLabelSlots);
1016
+ };
1017
+ const placePlotLabel = (targetY, height2, minY, maxY) => {
1018
+ return placeLabelSlot(targetY, height2, minY, maxY, plotLabelSlots);
1019
+ };
990
1020
  const dashPatterns = {
991
1021
  dotted: mergedOptions.dashPatterns.dotted ?? DEFAULT_DASH_PATTERNS.dotted,
992
1022
  dashed: mergedOptions.dashPatterns.dashed ?? DEFAULT_DASH_PATTERNS.dashed,
@@ -1352,7 +1382,7 @@ function createChart(element, options = {}) {
1352
1382
  const labelHeight = 20;
1353
1383
  const labelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
1354
1384
  const labelX = chartRight + 4;
1355
- const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
1385
+ const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
1356
1386
  ctx.fillStyle = mergedLine.labelBackgroundColor;
1357
1387
  fillRoundedRect(
1358
1388
  Math.round(labelX),
@@ -1485,17 +1515,18 @@ function createChart(element, options = {}) {
1485
1515
  leftWidgetX = maxWidgetX;
1486
1516
  }
1487
1517
  leftWidgetX = clamp(leftWidgetX, leftWidgetXBase, maxWidgetX);
1488
- const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
1518
+ const targetLabelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
1519
+ const widgetY = placePlotLabel(targetLabelY, labelHeight, chartTop, chartBottom - labelHeight);
1489
1520
  const borderRadius = Math.max(0, mergedLine.labelBorderRadius);
1490
1521
  const widgetBackground = mergedOptions.backgroundColor;
1491
1522
  const widgetBorder = color;
1492
1523
  const textColor = line.labelTextColor ?? (mergedLine.type === "takeProfit" ? "#5eead4" : mergedLine.type === "stop" ? "#fbbf24" : mergedLine.labelTextColor);
1493
1524
  const mainWidgetX = leftWidgetX + actionButtonsTotalWidth + actionButtonsGap;
1494
1525
  ctx.fillStyle = widgetBackground;
1495
- fillRoundedRect(Math.round(mainWidgetX), Math.round(labelY), mainWidgetWidth, labelHeight, borderRadius);
1526
+ fillRoundedRect(Math.round(mainWidgetX), Math.round(widgetY), mainWidgetWidth, labelHeight, borderRadius);
1496
1527
  ctx.strokeStyle = widgetBorder;
1497
1528
  ctx.lineWidth = 1;
1498
- strokeRoundedRect(Math.round(mainWidgetX), Math.round(labelY), mainWidgetWidth, labelHeight, borderRadius);
1529
+ strokeRoundedRect(Math.round(mainWidgetX), Math.round(widgetY), mainWidgetWidth, labelHeight, borderRadius);
1499
1530
  let cursorX = mainWidgetX;
1500
1531
  const separatorColor = "rgba(148,163,184,0.45)";
1501
1532
  if (actionButtonMetrics.length > 0) {
@@ -1505,7 +1536,7 @@ function createChart(element, options = {}) {
1505
1536
  const fullHeight = button.fullHeight ?? mergedLine.actionButtonFullHeight;
1506
1537
  const actionPadding = fullHeight ? 0 : 2;
1507
1538
  const actionX = actionCursorX + actionPadding;
1508
- const actionY = labelY + actionPadding;
1539
+ const actionY = widgetY + actionPadding;
1509
1540
  const actionH = labelHeight - actionPadding * 2;
1510
1541
  const actionW = Math.max(8, width2 - actionPadding * 2);
1511
1542
  const actionRadius = Math.max(1, button.borderRadius ?? mergedLine.actionButtonBorderRadius);
@@ -1545,29 +1576,29 @@ function createChart(element, options = {}) {
1545
1576
  });
1546
1577
  }
1547
1578
  if (qtyWidth > 0) {
1548
- drawText(qtyText, cursorX + segmentPaddingX, labelY + labelHeight / 2, "left", "middle", textColor);
1579
+ drawText(qtyText, cursorX + segmentPaddingX, widgetY + labelHeight / 2, "left", "middle", textColor);
1549
1580
  cursorX += qtyWidth;
1550
1581
  ctx.strokeStyle = separatorColor;
1551
1582
  ctx.beginPath();
1552
- ctx.moveTo(crisp(cursorX), labelY + 4);
1553
- ctx.lineTo(crisp(cursorX), labelY + labelHeight - 4);
1583
+ ctx.moveTo(crisp(cursorX), widgetY + 4);
1584
+ ctx.lineTo(crisp(cursorX), widgetY + labelHeight - 4);
1554
1585
  ctx.stroke();
1555
1586
  }
1556
- drawText(centerText, cursorX + segmentPaddingX, labelY + labelHeight / 2, "left", "middle", textColor);
1587
+ drawText(centerText, cursorX + segmentPaddingX, widgetY + labelHeight / 2, "left", "middle", textColor);
1557
1588
  cursorX += centerWidth;
1558
1589
  if (showCloseButton) {
1559
1590
  ctx.strokeStyle = separatorColor;
1560
1591
  ctx.beginPath();
1561
- ctx.moveTo(crisp(cursorX), labelY + 4);
1562
- ctx.lineTo(crisp(cursorX), labelY + labelHeight - 4);
1592
+ ctx.moveTo(crisp(cursorX), widgetY + 4);
1593
+ ctx.lineTo(crisp(cursorX), widgetY + labelHeight - 4);
1563
1594
  ctx.stroke();
1564
- drawText("x", cursorX + closeWidth / 2, labelY + labelHeight / 2, "center", "middle", textColor);
1595
+ drawText("x", cursorX + closeWidth / 2, widgetY + labelHeight / 2, "center", "middle", textColor);
1565
1596
  if (mergedLine.id) {
1566
1597
  orderActionRegions.push({
1567
1598
  orderId: mergedLine.id,
1568
1599
  action: closeAction,
1569
1600
  x: cursorX,
1570
- y: labelY,
1601
+ y: widgetY,
1571
1602
  width: closeWidth,
1572
1603
  height: labelHeight,
1573
1604
  line: mergedLine
@@ -1593,12 +1624,13 @@ function createChart(element, options = {}) {
1593
1624
  orderPriceTagWidthById.set(mergedLine.id, priceWidth);
1594
1625
  }
1595
1626
  const priceX = chartRight + 4;
1627
+ const priceY = placeRightAxisLabel(targetLabelY, labelHeight, chartTop, chartBottom - labelHeight);
1596
1628
  ctx.fillStyle = mergedLine.labelBackgroundColor ?? color;
1597
- fillRoundedRect(Math.round(priceX), Math.round(labelY), priceWidth, labelHeight, borderRadius);
1629
+ fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, borderRadius);
1598
1630
  ctx.strokeStyle = widgetBorder;
1599
1631
  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);
1632
+ strokeRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, borderRadius);
1633
+ drawText(priceText, priceX + pricePaddingX, priceY + labelHeight / 2, "left", "middle", textColor);
1602
1634
  };
1603
1635
  const fillRoundedRect = (x, y, widthValue, heightValue, radiusValue) => {
1604
1636
  const maxRadius = Math.min(widthValue, heightValue) / 2;
@@ -2086,6 +2118,7 @@ function createChart(element, options = {}) {
2086
2118
  ctx.font = prevFont;
2087
2119
  }
2088
2120
  const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
2121
+ resetLabelSlots(labels.noOverlapping);
2089
2122
  const priceAxisLabels = [];
2090
2123
  const addPriceAxisLabel = (label) => {
2091
2124
  if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
@@ -2274,6 +2307,15 @@ function createChart(element, options = {}) {
2274
2307
  }
2275
2308
  }
2276
2309
  }
2310
+ if (labels.noOverlapping) {
2311
+ rightAxisLabelSlots.push(
2312
+ ...positionedLabels.map((label) => ({
2313
+ y: label.y,
2314
+ height: label.height
2315
+ }))
2316
+ );
2317
+ rightAxisLabelSlots.sort((a, b) => a.y - b.y);
2318
+ }
2277
2319
  const labelX = chartRight + 4;
2278
2320
  for (const label of positionedLabels) {
2279
2321
  ctx.fillStyle = label.backgroundColor;
@@ -867,6 +867,9 @@ function createChart(element, options = {}) {
867
867
  let crosshairPoint = null;
868
868
  let doubleClickEnabled = mergedOptions.doubleClickEnabled;
869
869
  let doubleClickAction = mergedOptions.doubleClickAction;
870
+ let noOverlappingLineLabels = DEFAULT_LABELS_OPTIONS.noOverlapping;
871
+ let rightAxisLabelSlots = [];
872
+ let plotLabelSlots = [];
870
873
  let smoothedTickerPrice = null;
871
874
  let tickerPriceTarget = null;
872
875
  let smoothedTickerVolume = null;
@@ -963,6 +966,33 @@ function createChart(element, options = {}) {
963
966
  const clamp = (value, min, max) => {
964
967
  return Math.min(max, Math.max(min, value));
965
968
  };
969
+ const resetLabelSlots = (enabled) => {
970
+ noOverlappingLineLabels = enabled;
971
+ rightAxisLabelSlots = [];
972
+ plotLabelSlots = [];
973
+ };
974
+ const placeLabelSlot = (targetY, height2, minY, maxY, slots, gap = 2) => {
975
+ const safeMaxY = Math.max(minY, maxY);
976
+ const clampedTarget = clamp(targetY, minY, safeMaxY);
977
+ if (!noOverlappingLineLabels || height2 <= 0) {
978
+ return clampedTarget;
979
+ }
980
+ const overlaps = (y) => slots.some((slot) => y < slot.y + slot.height + gap && y + height2 + gap > slot.y);
981
+ const candidates = [clampedTarget];
982
+ for (const slot of slots) {
983
+ candidates.push(slot.y - height2 - gap, slot.y + slot.height + gap);
984
+ }
985
+ 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;
986
+ slots.push({ y: placedY, height: height2 });
987
+ slots.sort((a, b) => a.y - b.y);
988
+ return placedY;
989
+ };
990
+ const placeRightAxisLabel = (targetY, height2, minY, maxY) => {
991
+ return placeLabelSlot(targetY, height2, minY, maxY, rightAxisLabelSlots);
992
+ };
993
+ const placePlotLabel = (targetY, height2, minY, maxY) => {
994
+ return placeLabelSlot(targetY, height2, minY, maxY, plotLabelSlots);
995
+ };
966
996
  const dashPatterns = {
967
997
  dotted: mergedOptions.dashPatterns.dotted ?? DEFAULT_DASH_PATTERNS.dotted,
968
998
  dashed: mergedOptions.dashPatterns.dashed ?? DEFAULT_DASH_PATTERNS.dashed,
@@ -1328,7 +1358,7 @@ function createChart(element, options = {}) {
1328
1358
  const labelHeight = 20;
1329
1359
  const labelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
1330
1360
  const labelX = chartRight + 4;
1331
- const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
1361
+ const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
1332
1362
  ctx.fillStyle = mergedLine.labelBackgroundColor;
1333
1363
  fillRoundedRect(
1334
1364
  Math.round(labelX),
@@ -1461,17 +1491,18 @@ function createChart(element, options = {}) {
1461
1491
  leftWidgetX = maxWidgetX;
1462
1492
  }
1463
1493
  leftWidgetX = clamp(leftWidgetX, leftWidgetXBase, maxWidgetX);
1464
- const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
1494
+ const targetLabelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
1495
+ const widgetY = placePlotLabel(targetLabelY, labelHeight, chartTop, chartBottom - labelHeight);
1465
1496
  const borderRadius = Math.max(0, mergedLine.labelBorderRadius);
1466
1497
  const widgetBackground = mergedOptions.backgroundColor;
1467
1498
  const widgetBorder = color;
1468
1499
  const textColor = line.labelTextColor ?? (mergedLine.type === "takeProfit" ? "#5eead4" : mergedLine.type === "stop" ? "#fbbf24" : mergedLine.labelTextColor);
1469
1500
  const mainWidgetX = leftWidgetX + actionButtonsTotalWidth + actionButtonsGap;
1470
1501
  ctx.fillStyle = widgetBackground;
1471
- fillRoundedRect(Math.round(mainWidgetX), Math.round(labelY), mainWidgetWidth, labelHeight, borderRadius);
1502
+ fillRoundedRect(Math.round(mainWidgetX), Math.round(widgetY), mainWidgetWidth, labelHeight, borderRadius);
1472
1503
  ctx.strokeStyle = widgetBorder;
1473
1504
  ctx.lineWidth = 1;
1474
- strokeRoundedRect(Math.round(mainWidgetX), Math.round(labelY), mainWidgetWidth, labelHeight, borderRadius);
1505
+ strokeRoundedRect(Math.round(mainWidgetX), Math.round(widgetY), mainWidgetWidth, labelHeight, borderRadius);
1475
1506
  let cursorX = mainWidgetX;
1476
1507
  const separatorColor = "rgba(148,163,184,0.45)";
1477
1508
  if (actionButtonMetrics.length > 0) {
@@ -1481,7 +1512,7 @@ function createChart(element, options = {}) {
1481
1512
  const fullHeight = button.fullHeight ?? mergedLine.actionButtonFullHeight;
1482
1513
  const actionPadding = fullHeight ? 0 : 2;
1483
1514
  const actionX = actionCursorX + actionPadding;
1484
- const actionY = labelY + actionPadding;
1515
+ const actionY = widgetY + actionPadding;
1485
1516
  const actionH = labelHeight - actionPadding * 2;
1486
1517
  const actionW = Math.max(8, width2 - actionPadding * 2);
1487
1518
  const actionRadius = Math.max(1, button.borderRadius ?? mergedLine.actionButtonBorderRadius);
@@ -1521,29 +1552,29 @@ function createChart(element, options = {}) {
1521
1552
  });
1522
1553
  }
1523
1554
  if (qtyWidth > 0) {
1524
- drawText(qtyText, cursorX + segmentPaddingX, labelY + labelHeight / 2, "left", "middle", textColor);
1555
+ drawText(qtyText, cursorX + segmentPaddingX, widgetY + labelHeight / 2, "left", "middle", textColor);
1525
1556
  cursorX += qtyWidth;
1526
1557
  ctx.strokeStyle = separatorColor;
1527
1558
  ctx.beginPath();
1528
- ctx.moveTo(crisp(cursorX), labelY + 4);
1529
- ctx.lineTo(crisp(cursorX), labelY + labelHeight - 4);
1559
+ ctx.moveTo(crisp(cursorX), widgetY + 4);
1560
+ ctx.lineTo(crisp(cursorX), widgetY + labelHeight - 4);
1530
1561
  ctx.stroke();
1531
1562
  }
1532
- drawText(centerText, cursorX + segmentPaddingX, labelY + labelHeight / 2, "left", "middle", textColor);
1563
+ drawText(centerText, cursorX + segmentPaddingX, widgetY + labelHeight / 2, "left", "middle", textColor);
1533
1564
  cursorX += centerWidth;
1534
1565
  if (showCloseButton) {
1535
1566
  ctx.strokeStyle = separatorColor;
1536
1567
  ctx.beginPath();
1537
- ctx.moveTo(crisp(cursorX), labelY + 4);
1538
- ctx.lineTo(crisp(cursorX), labelY + labelHeight - 4);
1568
+ ctx.moveTo(crisp(cursorX), widgetY + 4);
1569
+ ctx.lineTo(crisp(cursorX), widgetY + labelHeight - 4);
1539
1570
  ctx.stroke();
1540
- drawText("x", cursorX + closeWidth / 2, labelY + labelHeight / 2, "center", "middle", textColor);
1571
+ drawText("x", cursorX + closeWidth / 2, widgetY + labelHeight / 2, "center", "middle", textColor);
1541
1572
  if (mergedLine.id) {
1542
1573
  orderActionRegions.push({
1543
1574
  orderId: mergedLine.id,
1544
1575
  action: closeAction,
1545
1576
  x: cursorX,
1546
- y: labelY,
1577
+ y: widgetY,
1547
1578
  width: closeWidth,
1548
1579
  height: labelHeight,
1549
1580
  line: mergedLine
@@ -1569,12 +1600,13 @@ function createChart(element, options = {}) {
1569
1600
  orderPriceTagWidthById.set(mergedLine.id, priceWidth);
1570
1601
  }
1571
1602
  const priceX = chartRight + 4;
1603
+ const priceY = placeRightAxisLabel(targetLabelY, labelHeight, chartTop, chartBottom - labelHeight);
1572
1604
  ctx.fillStyle = mergedLine.labelBackgroundColor ?? color;
1573
- fillRoundedRect(Math.round(priceX), Math.round(labelY), priceWidth, labelHeight, borderRadius);
1605
+ fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, borderRadius);
1574
1606
  ctx.strokeStyle = widgetBorder;
1575
1607
  ctx.lineWidth = 1;
1576
- strokeRoundedRect(Math.round(priceX), Math.round(labelY), priceWidth, labelHeight, borderRadius);
1577
- drawText(priceText, priceX + pricePaddingX, labelY + labelHeight / 2, "left", "middle", textColor);
1608
+ strokeRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, borderRadius);
1609
+ drawText(priceText, priceX + pricePaddingX, priceY + labelHeight / 2, "left", "middle", textColor);
1578
1610
  };
1579
1611
  const fillRoundedRect = (x, y, widthValue, heightValue, radiusValue) => {
1580
1612
  const maxRadius = Math.min(widthValue, heightValue) / 2;
@@ -2062,6 +2094,7 @@ function createChart(element, options = {}) {
2062
2094
  ctx.font = prevFont;
2063
2095
  }
2064
2096
  const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
2097
+ resetLabelSlots(labels.noOverlapping);
2065
2098
  const priceAxisLabels = [];
2066
2099
  const addPriceAxisLabel = (label) => {
2067
2100
  if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
@@ -2250,6 +2283,15 @@ function createChart(element, options = {}) {
2250
2283
  }
2251
2284
  }
2252
2285
  }
2286
+ if (labels.noOverlapping) {
2287
+ rightAxisLabelSlots.push(
2288
+ ...positionedLabels.map((label) => ({
2289
+ y: label.y,
2290
+ height: label.height
2291
+ }))
2292
+ );
2293
+ rightAxisLabelSlots.sort((a, b) => a.y - b.y);
2294
+ }
2253
2295
  const labelX = chartRight + 4;
2254
2296
  for (const label of positionedLabels) {
2255
2297
  ctx.fillStyle = label.backgroundColor;
package/dist/index.cjs CHANGED
@@ -891,6 +891,9 @@ function createChart(element, options = {}) {
891
891
  let crosshairPoint = null;
892
892
  let doubleClickEnabled = mergedOptions.doubleClickEnabled;
893
893
  let doubleClickAction = mergedOptions.doubleClickAction;
894
+ let noOverlappingLineLabels = DEFAULT_LABELS_OPTIONS.noOverlapping;
895
+ let rightAxisLabelSlots = [];
896
+ let plotLabelSlots = [];
894
897
  let smoothedTickerPrice = null;
895
898
  let tickerPriceTarget = null;
896
899
  let smoothedTickerVolume = null;
@@ -987,6 +990,33 @@ function createChart(element, options = {}) {
987
990
  const clamp = (value, min, max) => {
988
991
  return Math.min(max, Math.max(min, value));
989
992
  };
993
+ const resetLabelSlots = (enabled) => {
994
+ noOverlappingLineLabels = enabled;
995
+ rightAxisLabelSlots = [];
996
+ plotLabelSlots = [];
997
+ };
998
+ const placeLabelSlot = (targetY, height2, minY, maxY, slots, gap = 2) => {
999
+ const safeMaxY = Math.max(minY, maxY);
1000
+ const clampedTarget = clamp(targetY, minY, safeMaxY);
1001
+ if (!noOverlappingLineLabels || height2 <= 0) {
1002
+ return clampedTarget;
1003
+ }
1004
+ const overlaps = (y) => slots.some((slot) => y < slot.y + slot.height + gap && y + height2 + gap > slot.y);
1005
+ const candidates = [clampedTarget];
1006
+ for (const slot of slots) {
1007
+ candidates.push(slot.y - height2 - gap, slot.y + slot.height + gap);
1008
+ }
1009
+ 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;
1010
+ slots.push({ y: placedY, height: height2 });
1011
+ slots.sort((a, b) => a.y - b.y);
1012
+ return placedY;
1013
+ };
1014
+ const placeRightAxisLabel = (targetY, height2, minY, maxY) => {
1015
+ return placeLabelSlot(targetY, height2, minY, maxY, rightAxisLabelSlots);
1016
+ };
1017
+ const placePlotLabel = (targetY, height2, minY, maxY) => {
1018
+ return placeLabelSlot(targetY, height2, minY, maxY, plotLabelSlots);
1019
+ };
990
1020
  const dashPatterns = {
991
1021
  dotted: mergedOptions.dashPatterns.dotted ?? DEFAULT_DASH_PATTERNS.dotted,
992
1022
  dashed: mergedOptions.dashPatterns.dashed ?? DEFAULT_DASH_PATTERNS.dashed,
@@ -1352,7 +1382,7 @@ function createChart(element, options = {}) {
1352
1382
  const labelHeight = 20;
1353
1383
  const labelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
1354
1384
  const labelX = chartRight + 4;
1355
- const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
1385
+ const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
1356
1386
  ctx.fillStyle = mergedLine.labelBackgroundColor;
1357
1387
  fillRoundedRect(
1358
1388
  Math.round(labelX),
@@ -1485,17 +1515,18 @@ function createChart(element, options = {}) {
1485
1515
  leftWidgetX = maxWidgetX;
1486
1516
  }
1487
1517
  leftWidgetX = clamp(leftWidgetX, leftWidgetXBase, maxWidgetX);
1488
- const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
1518
+ const targetLabelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
1519
+ const widgetY = placePlotLabel(targetLabelY, labelHeight, chartTop, chartBottom - labelHeight);
1489
1520
  const borderRadius = Math.max(0, mergedLine.labelBorderRadius);
1490
1521
  const widgetBackground = mergedOptions.backgroundColor;
1491
1522
  const widgetBorder = color;
1492
1523
  const textColor = line.labelTextColor ?? (mergedLine.type === "takeProfit" ? "#5eead4" : mergedLine.type === "stop" ? "#fbbf24" : mergedLine.labelTextColor);
1493
1524
  const mainWidgetX = leftWidgetX + actionButtonsTotalWidth + actionButtonsGap;
1494
1525
  ctx.fillStyle = widgetBackground;
1495
- fillRoundedRect(Math.round(mainWidgetX), Math.round(labelY), mainWidgetWidth, labelHeight, borderRadius);
1526
+ fillRoundedRect(Math.round(mainWidgetX), Math.round(widgetY), mainWidgetWidth, labelHeight, borderRadius);
1496
1527
  ctx.strokeStyle = widgetBorder;
1497
1528
  ctx.lineWidth = 1;
1498
- strokeRoundedRect(Math.round(mainWidgetX), Math.round(labelY), mainWidgetWidth, labelHeight, borderRadius);
1529
+ strokeRoundedRect(Math.round(mainWidgetX), Math.round(widgetY), mainWidgetWidth, labelHeight, borderRadius);
1499
1530
  let cursorX = mainWidgetX;
1500
1531
  const separatorColor = "rgba(148,163,184,0.45)";
1501
1532
  if (actionButtonMetrics.length > 0) {
@@ -1505,7 +1536,7 @@ function createChart(element, options = {}) {
1505
1536
  const fullHeight = button.fullHeight ?? mergedLine.actionButtonFullHeight;
1506
1537
  const actionPadding = fullHeight ? 0 : 2;
1507
1538
  const actionX = actionCursorX + actionPadding;
1508
- const actionY = labelY + actionPadding;
1539
+ const actionY = widgetY + actionPadding;
1509
1540
  const actionH = labelHeight - actionPadding * 2;
1510
1541
  const actionW = Math.max(8, width2 - actionPadding * 2);
1511
1542
  const actionRadius = Math.max(1, button.borderRadius ?? mergedLine.actionButtonBorderRadius);
@@ -1545,29 +1576,29 @@ function createChart(element, options = {}) {
1545
1576
  });
1546
1577
  }
1547
1578
  if (qtyWidth > 0) {
1548
- drawText(qtyText, cursorX + segmentPaddingX, labelY + labelHeight / 2, "left", "middle", textColor);
1579
+ drawText(qtyText, cursorX + segmentPaddingX, widgetY + labelHeight / 2, "left", "middle", textColor);
1549
1580
  cursorX += qtyWidth;
1550
1581
  ctx.strokeStyle = separatorColor;
1551
1582
  ctx.beginPath();
1552
- ctx.moveTo(crisp(cursorX), labelY + 4);
1553
- ctx.lineTo(crisp(cursorX), labelY + labelHeight - 4);
1583
+ ctx.moveTo(crisp(cursorX), widgetY + 4);
1584
+ ctx.lineTo(crisp(cursorX), widgetY + labelHeight - 4);
1554
1585
  ctx.stroke();
1555
1586
  }
1556
- drawText(centerText, cursorX + segmentPaddingX, labelY + labelHeight / 2, "left", "middle", textColor);
1587
+ drawText(centerText, cursorX + segmentPaddingX, widgetY + labelHeight / 2, "left", "middle", textColor);
1557
1588
  cursorX += centerWidth;
1558
1589
  if (showCloseButton) {
1559
1590
  ctx.strokeStyle = separatorColor;
1560
1591
  ctx.beginPath();
1561
- ctx.moveTo(crisp(cursorX), labelY + 4);
1562
- ctx.lineTo(crisp(cursorX), labelY + labelHeight - 4);
1592
+ ctx.moveTo(crisp(cursorX), widgetY + 4);
1593
+ ctx.lineTo(crisp(cursorX), widgetY + labelHeight - 4);
1563
1594
  ctx.stroke();
1564
- drawText("x", cursorX + closeWidth / 2, labelY + labelHeight / 2, "center", "middle", textColor);
1595
+ drawText("x", cursorX + closeWidth / 2, widgetY + labelHeight / 2, "center", "middle", textColor);
1565
1596
  if (mergedLine.id) {
1566
1597
  orderActionRegions.push({
1567
1598
  orderId: mergedLine.id,
1568
1599
  action: closeAction,
1569
1600
  x: cursorX,
1570
- y: labelY,
1601
+ y: widgetY,
1571
1602
  width: closeWidth,
1572
1603
  height: labelHeight,
1573
1604
  line: mergedLine
@@ -1593,12 +1624,13 @@ function createChart(element, options = {}) {
1593
1624
  orderPriceTagWidthById.set(mergedLine.id, priceWidth);
1594
1625
  }
1595
1626
  const priceX = chartRight + 4;
1627
+ const priceY = placeRightAxisLabel(targetLabelY, labelHeight, chartTop, chartBottom - labelHeight);
1596
1628
  ctx.fillStyle = mergedLine.labelBackgroundColor ?? color;
1597
- fillRoundedRect(Math.round(priceX), Math.round(labelY), priceWidth, labelHeight, borderRadius);
1629
+ fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, borderRadius);
1598
1630
  ctx.strokeStyle = widgetBorder;
1599
1631
  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);
1632
+ strokeRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, borderRadius);
1633
+ drawText(priceText, priceX + pricePaddingX, priceY + labelHeight / 2, "left", "middle", textColor);
1602
1634
  };
1603
1635
  const fillRoundedRect = (x, y, widthValue, heightValue, radiusValue) => {
1604
1636
  const maxRadius = Math.min(widthValue, heightValue) / 2;
@@ -2086,6 +2118,7 @@ function createChart(element, options = {}) {
2086
2118
  ctx.font = prevFont;
2087
2119
  }
2088
2120
  const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
2121
+ resetLabelSlots(labels.noOverlapping);
2089
2122
  const priceAxisLabels = [];
2090
2123
  const addPriceAxisLabel = (label) => {
2091
2124
  if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
@@ -2274,6 +2307,15 @@ function createChart(element, options = {}) {
2274
2307
  }
2275
2308
  }
2276
2309
  }
2310
+ if (labels.noOverlapping) {
2311
+ rightAxisLabelSlots.push(
2312
+ ...positionedLabels.map((label) => ({
2313
+ y: label.y,
2314
+ height: label.height
2315
+ }))
2316
+ );
2317
+ rightAxisLabelSlots.sort((a, b) => a.y - b.y);
2318
+ }
2277
2319
  const labelX = chartRight + 4;
2278
2320
  for (const label of positionedLabels) {
2279
2321
  ctx.fillStyle = label.backgroundColor;
package/dist/index.js CHANGED
@@ -867,6 +867,9 @@ function createChart(element, options = {}) {
867
867
  let crosshairPoint = null;
868
868
  let doubleClickEnabled = mergedOptions.doubleClickEnabled;
869
869
  let doubleClickAction = mergedOptions.doubleClickAction;
870
+ let noOverlappingLineLabels = DEFAULT_LABELS_OPTIONS.noOverlapping;
871
+ let rightAxisLabelSlots = [];
872
+ let plotLabelSlots = [];
870
873
  let smoothedTickerPrice = null;
871
874
  let tickerPriceTarget = null;
872
875
  let smoothedTickerVolume = null;
@@ -963,6 +966,33 @@ function createChart(element, options = {}) {
963
966
  const clamp = (value, min, max) => {
964
967
  return Math.min(max, Math.max(min, value));
965
968
  };
969
+ const resetLabelSlots = (enabled) => {
970
+ noOverlappingLineLabels = enabled;
971
+ rightAxisLabelSlots = [];
972
+ plotLabelSlots = [];
973
+ };
974
+ const placeLabelSlot = (targetY, height2, minY, maxY, slots, gap = 2) => {
975
+ const safeMaxY = Math.max(minY, maxY);
976
+ const clampedTarget = clamp(targetY, minY, safeMaxY);
977
+ if (!noOverlappingLineLabels || height2 <= 0) {
978
+ return clampedTarget;
979
+ }
980
+ const overlaps = (y) => slots.some((slot) => y < slot.y + slot.height + gap && y + height2 + gap > slot.y);
981
+ const candidates = [clampedTarget];
982
+ for (const slot of slots) {
983
+ candidates.push(slot.y - height2 - gap, slot.y + slot.height + gap);
984
+ }
985
+ 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;
986
+ slots.push({ y: placedY, height: height2 });
987
+ slots.sort((a, b) => a.y - b.y);
988
+ return placedY;
989
+ };
990
+ const placeRightAxisLabel = (targetY, height2, minY, maxY) => {
991
+ return placeLabelSlot(targetY, height2, minY, maxY, rightAxisLabelSlots);
992
+ };
993
+ const placePlotLabel = (targetY, height2, minY, maxY) => {
994
+ return placeLabelSlot(targetY, height2, minY, maxY, plotLabelSlots);
995
+ };
966
996
  const dashPatterns = {
967
997
  dotted: mergedOptions.dashPatterns.dotted ?? DEFAULT_DASH_PATTERNS.dotted,
968
998
  dashed: mergedOptions.dashPatterns.dashed ?? DEFAULT_DASH_PATTERNS.dashed,
@@ -1328,7 +1358,7 @@ function createChart(element, options = {}) {
1328
1358
  const labelHeight = 20;
1329
1359
  const labelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
1330
1360
  const labelX = chartRight + 4;
1331
- const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
1361
+ const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
1332
1362
  ctx.fillStyle = mergedLine.labelBackgroundColor;
1333
1363
  fillRoundedRect(
1334
1364
  Math.round(labelX),
@@ -1461,17 +1491,18 @@ function createChart(element, options = {}) {
1461
1491
  leftWidgetX = maxWidgetX;
1462
1492
  }
1463
1493
  leftWidgetX = clamp(leftWidgetX, leftWidgetXBase, maxWidgetX);
1464
- const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
1494
+ const targetLabelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
1495
+ const widgetY = placePlotLabel(targetLabelY, labelHeight, chartTop, chartBottom - labelHeight);
1465
1496
  const borderRadius = Math.max(0, mergedLine.labelBorderRadius);
1466
1497
  const widgetBackground = mergedOptions.backgroundColor;
1467
1498
  const widgetBorder = color;
1468
1499
  const textColor = line.labelTextColor ?? (mergedLine.type === "takeProfit" ? "#5eead4" : mergedLine.type === "stop" ? "#fbbf24" : mergedLine.labelTextColor);
1469
1500
  const mainWidgetX = leftWidgetX + actionButtonsTotalWidth + actionButtonsGap;
1470
1501
  ctx.fillStyle = widgetBackground;
1471
- fillRoundedRect(Math.round(mainWidgetX), Math.round(labelY), mainWidgetWidth, labelHeight, borderRadius);
1502
+ fillRoundedRect(Math.round(mainWidgetX), Math.round(widgetY), mainWidgetWidth, labelHeight, borderRadius);
1472
1503
  ctx.strokeStyle = widgetBorder;
1473
1504
  ctx.lineWidth = 1;
1474
- strokeRoundedRect(Math.round(mainWidgetX), Math.round(labelY), mainWidgetWidth, labelHeight, borderRadius);
1505
+ strokeRoundedRect(Math.round(mainWidgetX), Math.round(widgetY), mainWidgetWidth, labelHeight, borderRadius);
1475
1506
  let cursorX = mainWidgetX;
1476
1507
  const separatorColor = "rgba(148,163,184,0.45)";
1477
1508
  if (actionButtonMetrics.length > 0) {
@@ -1481,7 +1512,7 @@ function createChart(element, options = {}) {
1481
1512
  const fullHeight = button.fullHeight ?? mergedLine.actionButtonFullHeight;
1482
1513
  const actionPadding = fullHeight ? 0 : 2;
1483
1514
  const actionX = actionCursorX + actionPadding;
1484
- const actionY = labelY + actionPadding;
1515
+ const actionY = widgetY + actionPadding;
1485
1516
  const actionH = labelHeight - actionPadding * 2;
1486
1517
  const actionW = Math.max(8, width2 - actionPadding * 2);
1487
1518
  const actionRadius = Math.max(1, button.borderRadius ?? mergedLine.actionButtonBorderRadius);
@@ -1521,29 +1552,29 @@ function createChart(element, options = {}) {
1521
1552
  });
1522
1553
  }
1523
1554
  if (qtyWidth > 0) {
1524
- drawText(qtyText, cursorX + segmentPaddingX, labelY + labelHeight / 2, "left", "middle", textColor);
1555
+ drawText(qtyText, cursorX + segmentPaddingX, widgetY + labelHeight / 2, "left", "middle", textColor);
1525
1556
  cursorX += qtyWidth;
1526
1557
  ctx.strokeStyle = separatorColor;
1527
1558
  ctx.beginPath();
1528
- ctx.moveTo(crisp(cursorX), labelY + 4);
1529
- ctx.lineTo(crisp(cursorX), labelY + labelHeight - 4);
1559
+ ctx.moveTo(crisp(cursorX), widgetY + 4);
1560
+ ctx.lineTo(crisp(cursorX), widgetY + labelHeight - 4);
1530
1561
  ctx.stroke();
1531
1562
  }
1532
- drawText(centerText, cursorX + segmentPaddingX, labelY + labelHeight / 2, "left", "middle", textColor);
1563
+ drawText(centerText, cursorX + segmentPaddingX, widgetY + labelHeight / 2, "left", "middle", textColor);
1533
1564
  cursorX += centerWidth;
1534
1565
  if (showCloseButton) {
1535
1566
  ctx.strokeStyle = separatorColor;
1536
1567
  ctx.beginPath();
1537
- ctx.moveTo(crisp(cursorX), labelY + 4);
1538
- ctx.lineTo(crisp(cursorX), labelY + labelHeight - 4);
1568
+ ctx.moveTo(crisp(cursorX), widgetY + 4);
1569
+ ctx.lineTo(crisp(cursorX), widgetY + labelHeight - 4);
1539
1570
  ctx.stroke();
1540
- drawText("x", cursorX + closeWidth / 2, labelY + labelHeight / 2, "center", "middle", textColor);
1571
+ drawText("x", cursorX + closeWidth / 2, widgetY + labelHeight / 2, "center", "middle", textColor);
1541
1572
  if (mergedLine.id) {
1542
1573
  orderActionRegions.push({
1543
1574
  orderId: mergedLine.id,
1544
1575
  action: closeAction,
1545
1576
  x: cursorX,
1546
- y: labelY,
1577
+ y: widgetY,
1547
1578
  width: closeWidth,
1548
1579
  height: labelHeight,
1549
1580
  line: mergedLine
@@ -1569,12 +1600,13 @@ function createChart(element, options = {}) {
1569
1600
  orderPriceTagWidthById.set(mergedLine.id, priceWidth);
1570
1601
  }
1571
1602
  const priceX = chartRight + 4;
1603
+ const priceY = placeRightAxisLabel(targetLabelY, labelHeight, chartTop, chartBottom - labelHeight);
1572
1604
  ctx.fillStyle = mergedLine.labelBackgroundColor ?? color;
1573
- fillRoundedRect(Math.round(priceX), Math.round(labelY), priceWidth, labelHeight, borderRadius);
1605
+ fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, borderRadius);
1574
1606
  ctx.strokeStyle = widgetBorder;
1575
1607
  ctx.lineWidth = 1;
1576
- strokeRoundedRect(Math.round(priceX), Math.round(labelY), priceWidth, labelHeight, borderRadius);
1577
- drawText(priceText, priceX + pricePaddingX, labelY + labelHeight / 2, "left", "middle", textColor);
1608
+ strokeRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, borderRadius);
1609
+ drawText(priceText, priceX + pricePaddingX, priceY + labelHeight / 2, "left", "middle", textColor);
1578
1610
  };
1579
1611
  const fillRoundedRect = (x, y, widthValue, heightValue, radiusValue) => {
1580
1612
  const maxRadius = Math.min(widthValue, heightValue) / 2;
@@ -2062,6 +2094,7 @@ function createChart(element, options = {}) {
2062
2094
  ctx.font = prevFont;
2063
2095
  }
2064
2096
  const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
2097
+ resetLabelSlots(labels.noOverlapping);
2065
2098
  const priceAxisLabels = [];
2066
2099
  const addPriceAxisLabel = (label) => {
2067
2100
  if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
@@ -2250,6 +2283,15 @@ function createChart(element, options = {}) {
2250
2283
  }
2251
2284
  }
2252
2285
  }
2286
+ if (labels.noOverlapping) {
2287
+ rightAxisLabelSlots.push(
2288
+ ...positionedLabels.map((label) => ({
2289
+ y: label.y,
2290
+ height: label.height
2291
+ }))
2292
+ );
2293
+ rightAxisLabelSlots.sort((a, b) => a.y - b.y);
2294
+ }
2253
2295
  const labelX = chartRight + 4;
2254
2296
  for (const label of positionedLabels) {
2255
2297
  ctx.fillStyle = label.backgroundColor;
package/docs/API.md CHANGED
@@ -172,7 +172,7 @@ TradingView-style labels can be controlled from a single top-level object:
172
172
  - `showIndicatorNames` (default `false`; draws active indicator names in the chart)
173
173
  - `showIndicatorValues` (default `false`; appends simple indicator input values)
174
174
  - `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)
175
+ - `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
176
  - Style fields: `backgroundColor`, `textColor`, `mutedTextColor`, `symbolNameBackgroundColor`, `symbolNameTextColor`, `previousCloseColor`, `highLowColor`, `bidColor`, `askColor`, `indicatorTextColor`, `borderRadius`, `labelHeight`, `labelPaddingX`
177
177
 
178
178
  Example:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.48",
3
+ "version": "0.1.49",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",