hyperprop-charting-library 0.1.65 → 0.1.67

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.
@@ -126,7 +126,8 @@ var DEFAULT_PRICE_LINE_OPTIONS = {
126
126
  labelBackgroundColor: "#0b1220",
127
127
  labelTextColor: "#60a5fa",
128
128
  labelBorderRadius: 3,
129
- showLabel: true
129
+ showLabel: true,
130
+ pinOutOfRange: false
130
131
  };
131
132
  var DEFAULT_ORDER_LINE_OPTIONS = {
132
133
  visible: true,
@@ -160,7 +161,8 @@ var DEFAULT_ORDER_LINE_OPTIONS = {
160
161
  connectorThickness: 1,
161
162
  connectorAnchorPaddingRight: 10,
162
163
  fillToPrice: Number.NaN,
163
- fillColor: "rgba(37,99,235,0.18)"
164
+ fillColor: "rgba(37,99,235,0.18)",
165
+ pinOutOfRange: false
164
166
  };
165
167
  var DEFAULT_OPTIONS = {
166
168
  width: 720,
@@ -193,6 +195,7 @@ var DEFAULT_OPTIONS = {
193
195
  candleColorEpsilon: -1,
194
196
  autoScaleSmoothing: 0.16,
195
197
  autoScaleIgnoreLatestCandle: true,
198
+ pinOutOfRangeLines: false,
196
199
  doubleClickEnabled: true,
197
200
  doubleClickAction: "reset",
198
201
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
@@ -1501,12 +1504,27 @@ function createChart(element, options = {}) {
1501
1504
  const scaleWidth = Math.max(0, Math.floor(width - getRightAxisLabelX(chartRight)));
1502
1505
  return Math.max(contentWidth, scaleWidth);
1503
1506
  };
1507
+ const getLineY = (price, yFromPrice, chartTop, chartBottom, pinOutOfRange = mergedOptions.pinOutOfRangeLines) => {
1508
+ const y = yFromPrice(price);
1509
+ if (!Number.isFinite(y)) {
1510
+ return null;
1511
+ }
1512
+ const minY = chartTop + 1;
1513
+ const maxY = chartBottom - 1;
1514
+ if (y < minY || y > maxY) {
1515
+ return pinOutOfRange ? clamp(y, minY, maxY) : null;
1516
+ }
1517
+ return y;
1518
+ };
1504
1519
  const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
1505
1520
  const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
1506
1521
  if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
1507
1522
  return;
1508
1523
  }
1509
- const lineY = clamp(yFromPrice(mergedLine.price), chartTop + 1, chartBottom - 1);
1524
+ const lineY = getLineY(mergedLine.price, yFromPrice, chartTop, chartBottom, mergedLine.pinOutOfRange);
1525
+ if (lineY === null) {
1526
+ return;
1527
+ }
1510
1528
  const color = mergedLine.color;
1511
1529
  ctx.save();
1512
1530
  ctx.strokeStyle = color;
@@ -1552,10 +1570,16 @@ function createChart(element, options = {}) {
1552
1570
  if (!mergedLine.visible || !Number.isFinite(renderPrice)) {
1553
1571
  return;
1554
1572
  }
1555
- const lineY = clamp(yFromPrice(renderPrice), chartTop + 1, chartBottom - 1);
1573
+ const lineY = getLineY(renderPrice, yFromPrice, chartTop, chartBottom, mergedLine.pinOutOfRange);
1574
+ if (lineY === null) {
1575
+ return;
1576
+ }
1556
1577
  const color = line.color ?? (mergedLine.type === "takeProfit" ? "rgba(45,212,191,0.86)" : mergedLine.type === "stop" ? "rgba(245,158,11,0.86)" : "rgba(59,130,246,0.8)");
1557
1578
  if (Number.isFinite(mergedLine.fillToPrice)) {
1558
- const fillY = clamp(yFromPrice(mergedLine.fillToPrice), chartTop + 1, chartBottom - 1);
1579
+ const fillY = getLineY(mergedLine.fillToPrice, yFromPrice, chartTop, chartBottom, true);
1580
+ if (fillY === null) {
1581
+ return;
1582
+ }
1559
1583
  const topY = Math.min(lineY, fillY);
1560
1584
  const heightY = Math.max(1, Math.abs(lineY - fillY));
1561
1585
  ctx.save();
@@ -1573,21 +1597,29 @@ function createChart(element, options = {}) {
1573
1597
  ctx.stroke();
1574
1598
  ctx.restore();
1575
1599
  if (Number.isFinite(mergedLine.connectorToPrice)) {
1576
- const connectorY = clamp(yFromPrice(mergedLine.connectorToPrice), chartTop + 1, chartBottom - 1);
1577
- const connectorX = chartRight - Math.max(6, mergedLine.connectorAnchorPaddingRight);
1578
- ctx.save();
1579
- ctx.strokeStyle = mergedLine.connectorColor ?? color;
1580
- ctx.lineWidth = Math.max(1, mergedLine.connectorThickness);
1581
- applyDashPattern(
1582
- mergedLine.connectorStyle,
1583
- dashPatterns.connectorDotted,
1584
- dashPatterns.connectorDashed
1600
+ const connectorY = getLineY(
1601
+ mergedLine.connectorToPrice,
1602
+ yFromPrice,
1603
+ chartTop,
1604
+ chartBottom,
1605
+ mergedLine.pinOutOfRange
1585
1606
  );
1586
- ctx.beginPath();
1587
- ctx.moveTo(crisp(connectorX), crisp(lineY));
1588
- ctx.lineTo(crisp(connectorX), crisp(connectorY));
1589
- ctx.stroke();
1590
- ctx.restore();
1607
+ if (connectorY !== null) {
1608
+ const connectorX = chartRight - Math.max(6, mergedLine.connectorAnchorPaddingRight);
1609
+ ctx.save();
1610
+ ctx.strokeStyle = mergedLine.connectorColor ?? color;
1611
+ ctx.lineWidth = Math.max(1, mergedLine.connectorThickness);
1612
+ applyDashPattern(
1613
+ mergedLine.connectorStyle,
1614
+ dashPatterns.connectorDotted,
1615
+ dashPatterns.connectorDashed
1616
+ );
1617
+ ctx.beginPath();
1618
+ ctx.moveTo(crisp(connectorX), crisp(lineY));
1619
+ ctx.lineTo(crisp(connectorX), crisp(connectorY));
1620
+ ctx.stroke();
1621
+ ctx.restore();
1622
+ }
1591
1623
  }
1592
1624
  const qtyText = mergedLine.qty === void 0 ? "" : String(mergedLine.qty);
1593
1625
  const typeTextMap = {
@@ -2120,7 +2152,11 @@ function createChart(element, options = {}) {
2120
2152
  if (drawing.type === "horizontal-line") {
2121
2153
  const point = drawing.points[0];
2122
2154
  if (point) {
2123
- const y = clamp(yFromPrice(point.price), chartTop + 1, chartBottom - 1);
2155
+ const y = getLineY(point.price, yFromPrice, chartTop, chartBottom);
2156
+ if (y === null) {
2157
+ ctx.restore();
2158
+ return;
2159
+ }
2124
2160
  const handleX = chartRight - 64;
2125
2161
  ctx.beginPath();
2126
2162
  ctx.moveTo(crisp(chartLeft), crisp(y));
@@ -2560,13 +2596,19 @@ function createChart(element, options = {}) {
2560
2596
  if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
2561
2597
  return;
2562
2598
  }
2599
+ if (getLineY(label.price, yFromPrice, chartTop, chartBottom, label.pinOutOfRange) === null) {
2600
+ return;
2601
+ }
2563
2602
  priceAxisLabels.push(label);
2564
2603
  };
2565
- const drawReferenceLine = (price, color, style = "dotted") => {
2604
+ const drawReferenceLine = (price, color, style = "dotted", pinOutOfRange = mergedOptions.pinOutOfRangeLines) => {
2566
2605
  if (!Number.isFinite(price)) {
2567
2606
  return;
2568
2607
  }
2569
- const y = clamp(yFromPrice(price), chartTop + 1, chartBottom - 1);
2608
+ const y = getLineY(price, yFromPrice, chartTop, chartBottom, pinOutOfRange);
2609
+ if (y === null) {
2610
+ return;
2611
+ }
2570
2612
  ctx.save();
2571
2613
  ctx.strokeStyle = color;
2572
2614
  ctx.lineWidth = 1;
@@ -2593,22 +2635,23 @@ function createChart(element, options = {}) {
2593
2635
  let tickerColor = null;
2594
2636
  if ((ticker.visible ?? true) && lastPoint) {
2595
2637
  tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
2596
- const tickerY = yFromPrice(tickerPrice);
2597
- const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
2638
+ const lineY = getLineY(tickerPrice, yFromPrice, chartTop, chartBottom);
2598
2639
  const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
2599
2640
  tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
2600
2641
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
2601
2642
  const tickerStyle = ticker.style ?? "solid";
2602
- ctx.save();
2603
- ctx.strokeStyle = tickerColor;
2604
- ctx.lineWidth = tickerThickness;
2605
- applyDashPattern(tickerStyle, dashPatterns.dotted, dashPatterns.dashed);
2606
- ctx.beginPath();
2607
- ctx.moveTo(crisp(chartLeft), crisp(lineY));
2608
- ctx.lineTo(crisp(chartRight), crisp(lineY));
2609
- ctx.stroke();
2610
- ctx.setLineDash([]);
2611
- ctx.restore();
2643
+ if (lineY !== null) {
2644
+ ctx.save();
2645
+ ctx.strokeStyle = tickerColor;
2646
+ ctx.lineWidth = tickerThickness;
2647
+ applyDashPattern(tickerStyle, dashPatterns.dotted, dashPatterns.dashed);
2648
+ ctx.beginPath();
2649
+ ctx.moveTo(crisp(chartLeft), crisp(lineY));
2650
+ ctx.lineTo(crisp(chartRight), crisp(lineY));
2651
+ ctx.stroke();
2652
+ ctx.setLineDash([]);
2653
+ ctx.restore();
2654
+ }
2612
2655
  }
2613
2656
  if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
2614
2657
  const tickerSubtexts = [
@@ -2718,13 +2761,14 @@ function createChart(element, options = {}) {
2718
2761
  }
2719
2762
  const labelTextWidth = Math.max(primaryWidth, subtextWidth);
2720
2763
  const labelWidth = getRightAxisLabelWidth(chartRight, labelTextWidth);
2764
+ const labelLineY = getLineY(label.price, yFromPrice, chartTop, chartBottom, label.pinOutOfRange);
2721
2765
  return {
2722
2766
  ...label,
2723
2767
  subtexts,
2724
2768
  subtextFontSize,
2725
2769
  height: labelHeight,
2726
2770
  width: labelWidth,
2727
- targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
2771
+ targetY: (labelLineY ?? yFromPrice(label.price)) - labelHeight / 2,
2728
2772
  y: 0
2729
2773
  };
2730
2774
  }).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
@@ -3522,15 +3566,10 @@ function createChart(element, options = {}) {
3522
3566
  }
3523
3567
  const midpoint = getMidpoint(first, second);
3524
3568
  const anchorRatio = clamp((midpoint.x - drawState.chartLeft) / drawState.chartWidth, 0, 1);
3525
- const startYMin = yMinOverride ?? drawState.yMin;
3526
- const startYMax = yMaxOverride ?? drawState.yMax;
3527
3569
  pinchZoomState = {
3528
3570
  startDistance: Math.max(1, getPointerDistance(first, second)),
3529
3571
  startSpan: xSpan,
3530
- anchorIndex: drawState.xStart + anchorRatio * xSpan,
3531
- startMidpoint: midpoint,
3532
- startYMin,
3533
- startYMax
3572
+ anchorIndex: drawState.xStart + anchorRatio * xSpan
3534
3573
  };
3535
3574
  isDragging = false;
3536
3575
  dragMode = null;
@@ -3559,11 +3598,6 @@ function createChart(element, options = {}) {
3559
3598
  xSpan = nextSpan;
3560
3599
  xCenter = nextStart + nextSpan / 2;
3561
3600
  clampXViewport();
3562
- const startYRange = pinchZoomState.startYMax - pinchZoomState.startYMin || 1;
3563
- const priceShift = (midpoint.y - pinchZoomState.startMidpoint.y) / drawState.chartHeight * startYRange;
3564
- const clampedY = clampYRange(pinchZoomState.startYMin + priceShift, pinchZoomState.startYMax + priceShift);
3565
- yMinOverride = clampedY.min;
3566
- yMaxOverride = clampedY.max;
3567
3601
  updateFollowLatest(false);
3568
3602
  emitViewportChange();
3569
3603
  draw();
@@ -29,6 +29,7 @@ interface ChartOptions {
29
29
  candleColorEpsilon?: number;
30
30
  autoScaleSmoothing?: number;
31
31
  autoScaleIgnoreLatestCandle?: boolean;
32
+ pinOutOfRangeLines?: boolean;
32
33
  doubleClickEnabled?: boolean;
33
34
  doubleClickAction?: "reset" | "placeLimitOrder";
34
35
  crosshair?: CrosshairOptions;
@@ -233,6 +234,7 @@ interface PriceLineOptions {
233
234
  labelTextColor?: string;
234
235
  labelBorderRadius?: number;
235
236
  showLabel?: boolean;
237
+ pinOutOfRange?: boolean;
236
238
  }
237
239
  interface OrderLineOptions {
238
240
  id?: string;
@@ -276,6 +278,7 @@ interface OrderLineOptions {
276
278
  connectorAnchorPaddingRight?: number;
277
279
  fillToPrice?: number;
278
280
  fillColor?: string;
281
+ pinOutOfRange?: boolean;
279
282
  }
280
283
  interface OrderActionEvent {
281
284
  orderId?: string;
@@ -102,7 +102,8 @@ var DEFAULT_PRICE_LINE_OPTIONS = {
102
102
  labelBackgroundColor: "#0b1220",
103
103
  labelTextColor: "#60a5fa",
104
104
  labelBorderRadius: 3,
105
- showLabel: true
105
+ showLabel: true,
106
+ pinOutOfRange: false
106
107
  };
107
108
  var DEFAULT_ORDER_LINE_OPTIONS = {
108
109
  visible: true,
@@ -136,7 +137,8 @@ var DEFAULT_ORDER_LINE_OPTIONS = {
136
137
  connectorThickness: 1,
137
138
  connectorAnchorPaddingRight: 10,
138
139
  fillToPrice: Number.NaN,
139
- fillColor: "rgba(37,99,235,0.18)"
140
+ fillColor: "rgba(37,99,235,0.18)",
141
+ pinOutOfRange: false
140
142
  };
141
143
  var DEFAULT_OPTIONS = {
142
144
  width: 720,
@@ -169,6 +171,7 @@ var DEFAULT_OPTIONS = {
169
171
  candleColorEpsilon: -1,
170
172
  autoScaleSmoothing: 0.16,
171
173
  autoScaleIgnoreLatestCandle: true,
174
+ pinOutOfRangeLines: false,
172
175
  doubleClickEnabled: true,
173
176
  doubleClickAction: "reset",
174
177
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
@@ -1477,12 +1480,27 @@ function createChart(element, options = {}) {
1477
1480
  const scaleWidth = Math.max(0, Math.floor(width - getRightAxisLabelX(chartRight)));
1478
1481
  return Math.max(contentWidth, scaleWidth);
1479
1482
  };
1483
+ const getLineY = (price, yFromPrice, chartTop, chartBottom, pinOutOfRange = mergedOptions.pinOutOfRangeLines) => {
1484
+ const y = yFromPrice(price);
1485
+ if (!Number.isFinite(y)) {
1486
+ return null;
1487
+ }
1488
+ const minY = chartTop + 1;
1489
+ const maxY = chartBottom - 1;
1490
+ if (y < minY || y > maxY) {
1491
+ return pinOutOfRange ? clamp(y, minY, maxY) : null;
1492
+ }
1493
+ return y;
1494
+ };
1480
1495
  const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
1481
1496
  const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
1482
1497
  if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
1483
1498
  return;
1484
1499
  }
1485
- const lineY = clamp(yFromPrice(mergedLine.price), chartTop + 1, chartBottom - 1);
1500
+ const lineY = getLineY(mergedLine.price, yFromPrice, chartTop, chartBottom, mergedLine.pinOutOfRange);
1501
+ if (lineY === null) {
1502
+ return;
1503
+ }
1486
1504
  const color = mergedLine.color;
1487
1505
  ctx.save();
1488
1506
  ctx.strokeStyle = color;
@@ -1528,10 +1546,16 @@ function createChart(element, options = {}) {
1528
1546
  if (!mergedLine.visible || !Number.isFinite(renderPrice)) {
1529
1547
  return;
1530
1548
  }
1531
- const lineY = clamp(yFromPrice(renderPrice), chartTop + 1, chartBottom - 1);
1549
+ const lineY = getLineY(renderPrice, yFromPrice, chartTop, chartBottom, mergedLine.pinOutOfRange);
1550
+ if (lineY === null) {
1551
+ return;
1552
+ }
1532
1553
  const color = line.color ?? (mergedLine.type === "takeProfit" ? "rgba(45,212,191,0.86)" : mergedLine.type === "stop" ? "rgba(245,158,11,0.86)" : "rgba(59,130,246,0.8)");
1533
1554
  if (Number.isFinite(mergedLine.fillToPrice)) {
1534
- const fillY = clamp(yFromPrice(mergedLine.fillToPrice), chartTop + 1, chartBottom - 1);
1555
+ const fillY = getLineY(mergedLine.fillToPrice, yFromPrice, chartTop, chartBottom, true);
1556
+ if (fillY === null) {
1557
+ return;
1558
+ }
1535
1559
  const topY = Math.min(lineY, fillY);
1536
1560
  const heightY = Math.max(1, Math.abs(lineY - fillY));
1537
1561
  ctx.save();
@@ -1549,21 +1573,29 @@ function createChart(element, options = {}) {
1549
1573
  ctx.stroke();
1550
1574
  ctx.restore();
1551
1575
  if (Number.isFinite(mergedLine.connectorToPrice)) {
1552
- const connectorY = clamp(yFromPrice(mergedLine.connectorToPrice), chartTop + 1, chartBottom - 1);
1553
- const connectorX = chartRight - Math.max(6, mergedLine.connectorAnchorPaddingRight);
1554
- ctx.save();
1555
- ctx.strokeStyle = mergedLine.connectorColor ?? color;
1556
- ctx.lineWidth = Math.max(1, mergedLine.connectorThickness);
1557
- applyDashPattern(
1558
- mergedLine.connectorStyle,
1559
- dashPatterns.connectorDotted,
1560
- dashPatterns.connectorDashed
1576
+ const connectorY = getLineY(
1577
+ mergedLine.connectorToPrice,
1578
+ yFromPrice,
1579
+ chartTop,
1580
+ chartBottom,
1581
+ mergedLine.pinOutOfRange
1561
1582
  );
1562
- ctx.beginPath();
1563
- ctx.moveTo(crisp(connectorX), crisp(lineY));
1564
- ctx.lineTo(crisp(connectorX), crisp(connectorY));
1565
- ctx.stroke();
1566
- ctx.restore();
1583
+ if (connectorY !== null) {
1584
+ const connectorX = chartRight - Math.max(6, mergedLine.connectorAnchorPaddingRight);
1585
+ ctx.save();
1586
+ ctx.strokeStyle = mergedLine.connectorColor ?? color;
1587
+ ctx.lineWidth = Math.max(1, mergedLine.connectorThickness);
1588
+ applyDashPattern(
1589
+ mergedLine.connectorStyle,
1590
+ dashPatterns.connectorDotted,
1591
+ dashPatterns.connectorDashed
1592
+ );
1593
+ ctx.beginPath();
1594
+ ctx.moveTo(crisp(connectorX), crisp(lineY));
1595
+ ctx.lineTo(crisp(connectorX), crisp(connectorY));
1596
+ ctx.stroke();
1597
+ ctx.restore();
1598
+ }
1567
1599
  }
1568
1600
  const qtyText = mergedLine.qty === void 0 ? "" : String(mergedLine.qty);
1569
1601
  const typeTextMap = {
@@ -2096,7 +2128,11 @@ function createChart(element, options = {}) {
2096
2128
  if (drawing.type === "horizontal-line") {
2097
2129
  const point = drawing.points[0];
2098
2130
  if (point) {
2099
- const y = clamp(yFromPrice(point.price), chartTop + 1, chartBottom - 1);
2131
+ const y = getLineY(point.price, yFromPrice, chartTop, chartBottom);
2132
+ if (y === null) {
2133
+ ctx.restore();
2134
+ return;
2135
+ }
2100
2136
  const handleX = chartRight - 64;
2101
2137
  ctx.beginPath();
2102
2138
  ctx.moveTo(crisp(chartLeft), crisp(y));
@@ -2536,13 +2572,19 @@ function createChart(element, options = {}) {
2536
2572
  if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
2537
2573
  return;
2538
2574
  }
2575
+ if (getLineY(label.price, yFromPrice, chartTop, chartBottom, label.pinOutOfRange) === null) {
2576
+ return;
2577
+ }
2539
2578
  priceAxisLabels.push(label);
2540
2579
  };
2541
- const drawReferenceLine = (price, color, style = "dotted") => {
2580
+ const drawReferenceLine = (price, color, style = "dotted", pinOutOfRange = mergedOptions.pinOutOfRangeLines) => {
2542
2581
  if (!Number.isFinite(price)) {
2543
2582
  return;
2544
2583
  }
2545
- const y = clamp(yFromPrice(price), chartTop + 1, chartBottom - 1);
2584
+ const y = getLineY(price, yFromPrice, chartTop, chartBottom, pinOutOfRange);
2585
+ if (y === null) {
2586
+ return;
2587
+ }
2546
2588
  ctx.save();
2547
2589
  ctx.strokeStyle = color;
2548
2590
  ctx.lineWidth = 1;
@@ -2569,22 +2611,23 @@ function createChart(element, options = {}) {
2569
2611
  let tickerColor = null;
2570
2612
  if ((ticker.visible ?? true) && lastPoint) {
2571
2613
  tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
2572
- const tickerY = yFromPrice(tickerPrice);
2573
- const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
2614
+ const lineY = getLineY(tickerPrice, yFromPrice, chartTop, chartBottom);
2574
2615
  const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
2575
2616
  tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
2576
2617
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
2577
2618
  const tickerStyle = ticker.style ?? "solid";
2578
- ctx.save();
2579
- ctx.strokeStyle = tickerColor;
2580
- ctx.lineWidth = tickerThickness;
2581
- applyDashPattern(tickerStyle, dashPatterns.dotted, dashPatterns.dashed);
2582
- ctx.beginPath();
2583
- ctx.moveTo(crisp(chartLeft), crisp(lineY));
2584
- ctx.lineTo(crisp(chartRight), crisp(lineY));
2585
- ctx.stroke();
2586
- ctx.setLineDash([]);
2587
- ctx.restore();
2619
+ if (lineY !== null) {
2620
+ ctx.save();
2621
+ ctx.strokeStyle = tickerColor;
2622
+ ctx.lineWidth = tickerThickness;
2623
+ applyDashPattern(tickerStyle, dashPatterns.dotted, dashPatterns.dashed);
2624
+ ctx.beginPath();
2625
+ ctx.moveTo(crisp(chartLeft), crisp(lineY));
2626
+ ctx.lineTo(crisp(chartRight), crisp(lineY));
2627
+ ctx.stroke();
2628
+ ctx.setLineDash([]);
2629
+ ctx.restore();
2630
+ }
2588
2631
  }
2589
2632
  if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
2590
2633
  const tickerSubtexts = [
@@ -2694,13 +2737,14 @@ function createChart(element, options = {}) {
2694
2737
  }
2695
2738
  const labelTextWidth = Math.max(primaryWidth, subtextWidth);
2696
2739
  const labelWidth = getRightAxisLabelWidth(chartRight, labelTextWidth);
2740
+ const labelLineY = getLineY(label.price, yFromPrice, chartTop, chartBottom, label.pinOutOfRange);
2697
2741
  return {
2698
2742
  ...label,
2699
2743
  subtexts,
2700
2744
  subtextFontSize,
2701
2745
  height: labelHeight,
2702
2746
  width: labelWidth,
2703
- targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
2747
+ targetY: (labelLineY ?? yFromPrice(label.price)) - labelHeight / 2,
2704
2748
  y: 0
2705
2749
  };
2706
2750
  }).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
@@ -3498,15 +3542,10 @@ function createChart(element, options = {}) {
3498
3542
  }
3499
3543
  const midpoint = getMidpoint(first, second);
3500
3544
  const anchorRatio = clamp((midpoint.x - drawState.chartLeft) / drawState.chartWidth, 0, 1);
3501
- const startYMin = yMinOverride ?? drawState.yMin;
3502
- const startYMax = yMaxOverride ?? drawState.yMax;
3503
3545
  pinchZoomState = {
3504
3546
  startDistance: Math.max(1, getPointerDistance(first, second)),
3505
3547
  startSpan: xSpan,
3506
- anchorIndex: drawState.xStart + anchorRatio * xSpan,
3507
- startMidpoint: midpoint,
3508
- startYMin,
3509
- startYMax
3548
+ anchorIndex: drawState.xStart + anchorRatio * xSpan
3510
3549
  };
3511
3550
  isDragging = false;
3512
3551
  dragMode = null;
@@ -3535,11 +3574,6 @@ function createChart(element, options = {}) {
3535
3574
  xSpan = nextSpan;
3536
3575
  xCenter = nextStart + nextSpan / 2;
3537
3576
  clampXViewport();
3538
- const startYRange = pinchZoomState.startYMax - pinchZoomState.startYMin || 1;
3539
- const priceShift = (midpoint.y - pinchZoomState.startMidpoint.y) / drawState.chartHeight * startYRange;
3540
- const clampedY = clampYRange(pinchZoomState.startYMin + priceShift, pinchZoomState.startYMax + priceShift);
3541
- yMinOverride = clampedY.min;
3542
- yMaxOverride = clampedY.max;
3543
3577
  updateFollowLatest(false);
3544
3578
  emitViewportChange();
3545
3579
  draw();
package/dist/index.cjs CHANGED
@@ -126,7 +126,8 @@ var DEFAULT_PRICE_LINE_OPTIONS = {
126
126
  labelBackgroundColor: "#0b1220",
127
127
  labelTextColor: "#60a5fa",
128
128
  labelBorderRadius: 3,
129
- showLabel: true
129
+ showLabel: true,
130
+ pinOutOfRange: false
130
131
  };
131
132
  var DEFAULT_ORDER_LINE_OPTIONS = {
132
133
  visible: true,
@@ -160,7 +161,8 @@ var DEFAULT_ORDER_LINE_OPTIONS = {
160
161
  connectorThickness: 1,
161
162
  connectorAnchorPaddingRight: 10,
162
163
  fillToPrice: Number.NaN,
163
- fillColor: "rgba(37,99,235,0.18)"
164
+ fillColor: "rgba(37,99,235,0.18)",
165
+ pinOutOfRange: false
164
166
  };
165
167
  var DEFAULT_OPTIONS = {
166
168
  width: 720,
@@ -193,6 +195,7 @@ var DEFAULT_OPTIONS = {
193
195
  candleColorEpsilon: -1,
194
196
  autoScaleSmoothing: 0.16,
195
197
  autoScaleIgnoreLatestCandle: true,
198
+ pinOutOfRangeLines: false,
196
199
  doubleClickEnabled: true,
197
200
  doubleClickAction: "reset",
198
201
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
@@ -1501,12 +1504,27 @@ function createChart(element, options = {}) {
1501
1504
  const scaleWidth = Math.max(0, Math.floor(width - getRightAxisLabelX(chartRight)));
1502
1505
  return Math.max(contentWidth, scaleWidth);
1503
1506
  };
1507
+ const getLineY = (price, yFromPrice, chartTop, chartBottom, pinOutOfRange = mergedOptions.pinOutOfRangeLines) => {
1508
+ const y = yFromPrice(price);
1509
+ if (!Number.isFinite(y)) {
1510
+ return null;
1511
+ }
1512
+ const minY = chartTop + 1;
1513
+ const maxY = chartBottom - 1;
1514
+ if (y < minY || y > maxY) {
1515
+ return pinOutOfRange ? clamp(y, minY, maxY) : null;
1516
+ }
1517
+ return y;
1518
+ };
1504
1519
  const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
1505
1520
  const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
1506
1521
  if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
1507
1522
  return;
1508
1523
  }
1509
- const lineY = clamp(yFromPrice(mergedLine.price), chartTop + 1, chartBottom - 1);
1524
+ const lineY = getLineY(mergedLine.price, yFromPrice, chartTop, chartBottom, mergedLine.pinOutOfRange);
1525
+ if (lineY === null) {
1526
+ return;
1527
+ }
1510
1528
  const color = mergedLine.color;
1511
1529
  ctx.save();
1512
1530
  ctx.strokeStyle = color;
@@ -1552,10 +1570,16 @@ function createChart(element, options = {}) {
1552
1570
  if (!mergedLine.visible || !Number.isFinite(renderPrice)) {
1553
1571
  return;
1554
1572
  }
1555
- const lineY = clamp(yFromPrice(renderPrice), chartTop + 1, chartBottom - 1);
1573
+ const lineY = getLineY(renderPrice, yFromPrice, chartTop, chartBottom, mergedLine.pinOutOfRange);
1574
+ if (lineY === null) {
1575
+ return;
1576
+ }
1556
1577
  const color = line.color ?? (mergedLine.type === "takeProfit" ? "rgba(45,212,191,0.86)" : mergedLine.type === "stop" ? "rgba(245,158,11,0.86)" : "rgba(59,130,246,0.8)");
1557
1578
  if (Number.isFinite(mergedLine.fillToPrice)) {
1558
- const fillY = clamp(yFromPrice(mergedLine.fillToPrice), chartTop + 1, chartBottom - 1);
1579
+ const fillY = getLineY(mergedLine.fillToPrice, yFromPrice, chartTop, chartBottom, true);
1580
+ if (fillY === null) {
1581
+ return;
1582
+ }
1559
1583
  const topY = Math.min(lineY, fillY);
1560
1584
  const heightY = Math.max(1, Math.abs(lineY - fillY));
1561
1585
  ctx.save();
@@ -1573,21 +1597,29 @@ function createChart(element, options = {}) {
1573
1597
  ctx.stroke();
1574
1598
  ctx.restore();
1575
1599
  if (Number.isFinite(mergedLine.connectorToPrice)) {
1576
- const connectorY = clamp(yFromPrice(mergedLine.connectorToPrice), chartTop + 1, chartBottom - 1);
1577
- const connectorX = chartRight - Math.max(6, mergedLine.connectorAnchorPaddingRight);
1578
- ctx.save();
1579
- ctx.strokeStyle = mergedLine.connectorColor ?? color;
1580
- ctx.lineWidth = Math.max(1, mergedLine.connectorThickness);
1581
- applyDashPattern(
1582
- mergedLine.connectorStyle,
1583
- dashPatterns.connectorDotted,
1584
- dashPatterns.connectorDashed
1600
+ const connectorY = getLineY(
1601
+ mergedLine.connectorToPrice,
1602
+ yFromPrice,
1603
+ chartTop,
1604
+ chartBottom,
1605
+ mergedLine.pinOutOfRange
1585
1606
  );
1586
- ctx.beginPath();
1587
- ctx.moveTo(crisp(connectorX), crisp(lineY));
1588
- ctx.lineTo(crisp(connectorX), crisp(connectorY));
1589
- ctx.stroke();
1590
- ctx.restore();
1607
+ if (connectorY !== null) {
1608
+ const connectorX = chartRight - Math.max(6, mergedLine.connectorAnchorPaddingRight);
1609
+ ctx.save();
1610
+ ctx.strokeStyle = mergedLine.connectorColor ?? color;
1611
+ ctx.lineWidth = Math.max(1, mergedLine.connectorThickness);
1612
+ applyDashPattern(
1613
+ mergedLine.connectorStyle,
1614
+ dashPatterns.connectorDotted,
1615
+ dashPatterns.connectorDashed
1616
+ );
1617
+ ctx.beginPath();
1618
+ ctx.moveTo(crisp(connectorX), crisp(lineY));
1619
+ ctx.lineTo(crisp(connectorX), crisp(connectorY));
1620
+ ctx.stroke();
1621
+ ctx.restore();
1622
+ }
1591
1623
  }
1592
1624
  const qtyText = mergedLine.qty === void 0 ? "" : String(mergedLine.qty);
1593
1625
  const typeTextMap = {
@@ -2120,7 +2152,11 @@ function createChart(element, options = {}) {
2120
2152
  if (drawing.type === "horizontal-line") {
2121
2153
  const point = drawing.points[0];
2122
2154
  if (point) {
2123
- const y = clamp(yFromPrice(point.price), chartTop + 1, chartBottom - 1);
2155
+ const y = getLineY(point.price, yFromPrice, chartTop, chartBottom);
2156
+ if (y === null) {
2157
+ ctx.restore();
2158
+ return;
2159
+ }
2124
2160
  const handleX = chartRight - 64;
2125
2161
  ctx.beginPath();
2126
2162
  ctx.moveTo(crisp(chartLeft), crisp(y));
@@ -2560,13 +2596,19 @@ function createChart(element, options = {}) {
2560
2596
  if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
2561
2597
  return;
2562
2598
  }
2599
+ if (getLineY(label.price, yFromPrice, chartTop, chartBottom, label.pinOutOfRange) === null) {
2600
+ return;
2601
+ }
2563
2602
  priceAxisLabels.push(label);
2564
2603
  };
2565
- const drawReferenceLine = (price, color, style = "dotted") => {
2604
+ const drawReferenceLine = (price, color, style = "dotted", pinOutOfRange = mergedOptions.pinOutOfRangeLines) => {
2566
2605
  if (!Number.isFinite(price)) {
2567
2606
  return;
2568
2607
  }
2569
- const y = clamp(yFromPrice(price), chartTop + 1, chartBottom - 1);
2608
+ const y = getLineY(price, yFromPrice, chartTop, chartBottom, pinOutOfRange);
2609
+ if (y === null) {
2610
+ return;
2611
+ }
2570
2612
  ctx.save();
2571
2613
  ctx.strokeStyle = color;
2572
2614
  ctx.lineWidth = 1;
@@ -2593,22 +2635,23 @@ function createChart(element, options = {}) {
2593
2635
  let tickerColor = null;
2594
2636
  if ((ticker.visible ?? true) && lastPoint) {
2595
2637
  tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
2596
- const tickerY = yFromPrice(tickerPrice);
2597
- const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
2638
+ const lineY = getLineY(tickerPrice, yFromPrice, chartTop, chartBottom);
2598
2639
  const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
2599
2640
  tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
2600
2641
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
2601
2642
  const tickerStyle = ticker.style ?? "solid";
2602
- ctx.save();
2603
- ctx.strokeStyle = tickerColor;
2604
- ctx.lineWidth = tickerThickness;
2605
- applyDashPattern(tickerStyle, dashPatterns.dotted, dashPatterns.dashed);
2606
- ctx.beginPath();
2607
- ctx.moveTo(crisp(chartLeft), crisp(lineY));
2608
- ctx.lineTo(crisp(chartRight), crisp(lineY));
2609
- ctx.stroke();
2610
- ctx.setLineDash([]);
2611
- ctx.restore();
2643
+ if (lineY !== null) {
2644
+ ctx.save();
2645
+ ctx.strokeStyle = tickerColor;
2646
+ ctx.lineWidth = tickerThickness;
2647
+ applyDashPattern(tickerStyle, dashPatterns.dotted, dashPatterns.dashed);
2648
+ ctx.beginPath();
2649
+ ctx.moveTo(crisp(chartLeft), crisp(lineY));
2650
+ ctx.lineTo(crisp(chartRight), crisp(lineY));
2651
+ ctx.stroke();
2652
+ ctx.setLineDash([]);
2653
+ ctx.restore();
2654
+ }
2612
2655
  }
2613
2656
  if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
2614
2657
  const tickerSubtexts = [
@@ -2718,13 +2761,14 @@ function createChart(element, options = {}) {
2718
2761
  }
2719
2762
  const labelTextWidth = Math.max(primaryWidth, subtextWidth);
2720
2763
  const labelWidth = getRightAxisLabelWidth(chartRight, labelTextWidth);
2764
+ const labelLineY = getLineY(label.price, yFromPrice, chartTop, chartBottom, label.pinOutOfRange);
2721
2765
  return {
2722
2766
  ...label,
2723
2767
  subtexts,
2724
2768
  subtextFontSize,
2725
2769
  height: labelHeight,
2726
2770
  width: labelWidth,
2727
- targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
2771
+ targetY: (labelLineY ?? yFromPrice(label.price)) - labelHeight / 2,
2728
2772
  y: 0
2729
2773
  };
2730
2774
  }).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
@@ -3522,15 +3566,10 @@ function createChart(element, options = {}) {
3522
3566
  }
3523
3567
  const midpoint = getMidpoint(first, second);
3524
3568
  const anchorRatio = clamp((midpoint.x - drawState.chartLeft) / drawState.chartWidth, 0, 1);
3525
- const startYMin = yMinOverride ?? drawState.yMin;
3526
- const startYMax = yMaxOverride ?? drawState.yMax;
3527
3569
  pinchZoomState = {
3528
3570
  startDistance: Math.max(1, getPointerDistance(first, second)),
3529
3571
  startSpan: xSpan,
3530
- anchorIndex: drawState.xStart + anchorRatio * xSpan,
3531
- startMidpoint: midpoint,
3532
- startYMin,
3533
- startYMax
3572
+ anchorIndex: drawState.xStart + anchorRatio * xSpan
3534
3573
  };
3535
3574
  isDragging = false;
3536
3575
  dragMode = null;
@@ -3559,11 +3598,6 @@ function createChart(element, options = {}) {
3559
3598
  xSpan = nextSpan;
3560
3599
  xCenter = nextStart + nextSpan / 2;
3561
3600
  clampXViewport();
3562
- const startYRange = pinchZoomState.startYMax - pinchZoomState.startYMin || 1;
3563
- const priceShift = (midpoint.y - pinchZoomState.startMidpoint.y) / drawState.chartHeight * startYRange;
3564
- const clampedY = clampYRange(pinchZoomState.startYMin + priceShift, pinchZoomState.startYMax + priceShift);
3565
- yMinOverride = clampedY.min;
3566
- yMaxOverride = clampedY.max;
3567
3601
  updateFollowLatest(false);
3568
3602
  emitViewportChange();
3569
3603
  draw();
package/dist/index.d.cts CHANGED
@@ -29,6 +29,7 @@ interface ChartOptions {
29
29
  candleColorEpsilon?: number;
30
30
  autoScaleSmoothing?: number;
31
31
  autoScaleIgnoreLatestCandle?: boolean;
32
+ pinOutOfRangeLines?: boolean;
32
33
  doubleClickEnabled?: boolean;
33
34
  doubleClickAction?: "reset" | "placeLimitOrder";
34
35
  crosshair?: CrosshairOptions;
@@ -233,6 +234,7 @@ interface PriceLineOptions {
233
234
  labelTextColor?: string;
234
235
  labelBorderRadius?: number;
235
236
  showLabel?: boolean;
237
+ pinOutOfRange?: boolean;
236
238
  }
237
239
  interface OrderLineOptions {
238
240
  id?: string;
@@ -276,6 +278,7 @@ interface OrderLineOptions {
276
278
  connectorAnchorPaddingRight?: number;
277
279
  fillToPrice?: number;
278
280
  fillColor?: string;
281
+ pinOutOfRange?: boolean;
279
282
  }
280
283
  interface OrderActionEvent {
281
284
  orderId?: string;
package/dist/index.d.ts CHANGED
@@ -29,6 +29,7 @@ interface ChartOptions {
29
29
  candleColorEpsilon?: number;
30
30
  autoScaleSmoothing?: number;
31
31
  autoScaleIgnoreLatestCandle?: boolean;
32
+ pinOutOfRangeLines?: boolean;
32
33
  doubleClickEnabled?: boolean;
33
34
  doubleClickAction?: "reset" | "placeLimitOrder";
34
35
  crosshair?: CrosshairOptions;
@@ -233,6 +234,7 @@ interface PriceLineOptions {
233
234
  labelTextColor?: string;
234
235
  labelBorderRadius?: number;
235
236
  showLabel?: boolean;
237
+ pinOutOfRange?: boolean;
236
238
  }
237
239
  interface OrderLineOptions {
238
240
  id?: string;
@@ -276,6 +278,7 @@ interface OrderLineOptions {
276
278
  connectorAnchorPaddingRight?: number;
277
279
  fillToPrice?: number;
278
280
  fillColor?: string;
281
+ pinOutOfRange?: boolean;
279
282
  }
280
283
  interface OrderActionEvent {
281
284
  orderId?: string;
package/dist/index.js CHANGED
@@ -102,7 +102,8 @@ var DEFAULT_PRICE_LINE_OPTIONS = {
102
102
  labelBackgroundColor: "#0b1220",
103
103
  labelTextColor: "#60a5fa",
104
104
  labelBorderRadius: 3,
105
- showLabel: true
105
+ showLabel: true,
106
+ pinOutOfRange: false
106
107
  };
107
108
  var DEFAULT_ORDER_LINE_OPTIONS = {
108
109
  visible: true,
@@ -136,7 +137,8 @@ var DEFAULT_ORDER_LINE_OPTIONS = {
136
137
  connectorThickness: 1,
137
138
  connectorAnchorPaddingRight: 10,
138
139
  fillToPrice: Number.NaN,
139
- fillColor: "rgba(37,99,235,0.18)"
140
+ fillColor: "rgba(37,99,235,0.18)",
141
+ pinOutOfRange: false
140
142
  };
141
143
  var DEFAULT_OPTIONS = {
142
144
  width: 720,
@@ -169,6 +171,7 @@ var DEFAULT_OPTIONS = {
169
171
  candleColorEpsilon: -1,
170
172
  autoScaleSmoothing: 0.16,
171
173
  autoScaleIgnoreLatestCandle: true,
174
+ pinOutOfRangeLines: false,
172
175
  doubleClickEnabled: true,
173
176
  doubleClickAction: "reset",
174
177
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
@@ -1477,12 +1480,27 @@ function createChart(element, options = {}) {
1477
1480
  const scaleWidth = Math.max(0, Math.floor(width - getRightAxisLabelX(chartRight)));
1478
1481
  return Math.max(contentWidth, scaleWidth);
1479
1482
  };
1483
+ const getLineY = (price, yFromPrice, chartTop, chartBottom, pinOutOfRange = mergedOptions.pinOutOfRangeLines) => {
1484
+ const y = yFromPrice(price);
1485
+ if (!Number.isFinite(y)) {
1486
+ return null;
1487
+ }
1488
+ const minY = chartTop + 1;
1489
+ const maxY = chartBottom - 1;
1490
+ if (y < minY || y > maxY) {
1491
+ return pinOutOfRange ? clamp(y, minY, maxY) : null;
1492
+ }
1493
+ return y;
1494
+ };
1480
1495
  const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
1481
1496
  const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
1482
1497
  if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
1483
1498
  return;
1484
1499
  }
1485
- const lineY = clamp(yFromPrice(mergedLine.price), chartTop + 1, chartBottom - 1);
1500
+ const lineY = getLineY(mergedLine.price, yFromPrice, chartTop, chartBottom, mergedLine.pinOutOfRange);
1501
+ if (lineY === null) {
1502
+ return;
1503
+ }
1486
1504
  const color = mergedLine.color;
1487
1505
  ctx.save();
1488
1506
  ctx.strokeStyle = color;
@@ -1528,10 +1546,16 @@ function createChart(element, options = {}) {
1528
1546
  if (!mergedLine.visible || !Number.isFinite(renderPrice)) {
1529
1547
  return;
1530
1548
  }
1531
- const lineY = clamp(yFromPrice(renderPrice), chartTop + 1, chartBottom - 1);
1549
+ const lineY = getLineY(renderPrice, yFromPrice, chartTop, chartBottom, mergedLine.pinOutOfRange);
1550
+ if (lineY === null) {
1551
+ return;
1552
+ }
1532
1553
  const color = line.color ?? (mergedLine.type === "takeProfit" ? "rgba(45,212,191,0.86)" : mergedLine.type === "stop" ? "rgba(245,158,11,0.86)" : "rgba(59,130,246,0.8)");
1533
1554
  if (Number.isFinite(mergedLine.fillToPrice)) {
1534
- const fillY = clamp(yFromPrice(mergedLine.fillToPrice), chartTop + 1, chartBottom - 1);
1555
+ const fillY = getLineY(mergedLine.fillToPrice, yFromPrice, chartTop, chartBottom, true);
1556
+ if (fillY === null) {
1557
+ return;
1558
+ }
1535
1559
  const topY = Math.min(lineY, fillY);
1536
1560
  const heightY = Math.max(1, Math.abs(lineY - fillY));
1537
1561
  ctx.save();
@@ -1549,21 +1573,29 @@ function createChart(element, options = {}) {
1549
1573
  ctx.stroke();
1550
1574
  ctx.restore();
1551
1575
  if (Number.isFinite(mergedLine.connectorToPrice)) {
1552
- const connectorY = clamp(yFromPrice(mergedLine.connectorToPrice), chartTop + 1, chartBottom - 1);
1553
- const connectorX = chartRight - Math.max(6, mergedLine.connectorAnchorPaddingRight);
1554
- ctx.save();
1555
- ctx.strokeStyle = mergedLine.connectorColor ?? color;
1556
- ctx.lineWidth = Math.max(1, mergedLine.connectorThickness);
1557
- applyDashPattern(
1558
- mergedLine.connectorStyle,
1559
- dashPatterns.connectorDotted,
1560
- dashPatterns.connectorDashed
1576
+ const connectorY = getLineY(
1577
+ mergedLine.connectorToPrice,
1578
+ yFromPrice,
1579
+ chartTop,
1580
+ chartBottom,
1581
+ mergedLine.pinOutOfRange
1561
1582
  );
1562
- ctx.beginPath();
1563
- ctx.moveTo(crisp(connectorX), crisp(lineY));
1564
- ctx.lineTo(crisp(connectorX), crisp(connectorY));
1565
- ctx.stroke();
1566
- ctx.restore();
1583
+ if (connectorY !== null) {
1584
+ const connectorX = chartRight - Math.max(6, mergedLine.connectorAnchorPaddingRight);
1585
+ ctx.save();
1586
+ ctx.strokeStyle = mergedLine.connectorColor ?? color;
1587
+ ctx.lineWidth = Math.max(1, mergedLine.connectorThickness);
1588
+ applyDashPattern(
1589
+ mergedLine.connectorStyle,
1590
+ dashPatterns.connectorDotted,
1591
+ dashPatterns.connectorDashed
1592
+ );
1593
+ ctx.beginPath();
1594
+ ctx.moveTo(crisp(connectorX), crisp(lineY));
1595
+ ctx.lineTo(crisp(connectorX), crisp(connectorY));
1596
+ ctx.stroke();
1597
+ ctx.restore();
1598
+ }
1567
1599
  }
1568
1600
  const qtyText = mergedLine.qty === void 0 ? "" : String(mergedLine.qty);
1569
1601
  const typeTextMap = {
@@ -2096,7 +2128,11 @@ function createChart(element, options = {}) {
2096
2128
  if (drawing.type === "horizontal-line") {
2097
2129
  const point = drawing.points[0];
2098
2130
  if (point) {
2099
- const y = clamp(yFromPrice(point.price), chartTop + 1, chartBottom - 1);
2131
+ const y = getLineY(point.price, yFromPrice, chartTop, chartBottom);
2132
+ if (y === null) {
2133
+ ctx.restore();
2134
+ return;
2135
+ }
2100
2136
  const handleX = chartRight - 64;
2101
2137
  ctx.beginPath();
2102
2138
  ctx.moveTo(crisp(chartLeft), crisp(y));
@@ -2536,13 +2572,19 @@ function createChart(element, options = {}) {
2536
2572
  if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
2537
2573
  return;
2538
2574
  }
2575
+ if (getLineY(label.price, yFromPrice, chartTop, chartBottom, label.pinOutOfRange) === null) {
2576
+ return;
2577
+ }
2539
2578
  priceAxisLabels.push(label);
2540
2579
  };
2541
- const drawReferenceLine = (price, color, style = "dotted") => {
2580
+ const drawReferenceLine = (price, color, style = "dotted", pinOutOfRange = mergedOptions.pinOutOfRangeLines) => {
2542
2581
  if (!Number.isFinite(price)) {
2543
2582
  return;
2544
2583
  }
2545
- const y = clamp(yFromPrice(price), chartTop + 1, chartBottom - 1);
2584
+ const y = getLineY(price, yFromPrice, chartTop, chartBottom, pinOutOfRange);
2585
+ if (y === null) {
2586
+ return;
2587
+ }
2546
2588
  ctx.save();
2547
2589
  ctx.strokeStyle = color;
2548
2590
  ctx.lineWidth = 1;
@@ -2569,22 +2611,23 @@ function createChart(element, options = {}) {
2569
2611
  let tickerColor = null;
2570
2612
  if ((ticker.visible ?? true) && lastPoint) {
2571
2613
  tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
2572
- const tickerY = yFromPrice(tickerPrice);
2573
- const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
2614
+ const lineY = getLineY(tickerPrice, yFromPrice, chartTop, chartBottom);
2574
2615
  const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
2575
2616
  tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
2576
2617
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
2577
2618
  const tickerStyle = ticker.style ?? "solid";
2578
- ctx.save();
2579
- ctx.strokeStyle = tickerColor;
2580
- ctx.lineWidth = tickerThickness;
2581
- applyDashPattern(tickerStyle, dashPatterns.dotted, dashPatterns.dashed);
2582
- ctx.beginPath();
2583
- ctx.moveTo(crisp(chartLeft), crisp(lineY));
2584
- ctx.lineTo(crisp(chartRight), crisp(lineY));
2585
- ctx.stroke();
2586
- ctx.setLineDash([]);
2587
- ctx.restore();
2619
+ if (lineY !== null) {
2620
+ ctx.save();
2621
+ ctx.strokeStyle = tickerColor;
2622
+ ctx.lineWidth = tickerThickness;
2623
+ applyDashPattern(tickerStyle, dashPatterns.dotted, dashPatterns.dashed);
2624
+ ctx.beginPath();
2625
+ ctx.moveTo(crisp(chartLeft), crisp(lineY));
2626
+ ctx.lineTo(crisp(chartRight), crisp(lineY));
2627
+ ctx.stroke();
2628
+ ctx.setLineDash([]);
2629
+ ctx.restore();
2630
+ }
2588
2631
  }
2589
2632
  if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
2590
2633
  const tickerSubtexts = [
@@ -2694,13 +2737,14 @@ function createChart(element, options = {}) {
2694
2737
  }
2695
2738
  const labelTextWidth = Math.max(primaryWidth, subtextWidth);
2696
2739
  const labelWidth = getRightAxisLabelWidth(chartRight, labelTextWidth);
2740
+ const labelLineY = getLineY(label.price, yFromPrice, chartTop, chartBottom, label.pinOutOfRange);
2697
2741
  return {
2698
2742
  ...label,
2699
2743
  subtexts,
2700
2744
  subtextFontSize,
2701
2745
  height: labelHeight,
2702
2746
  width: labelWidth,
2703
- targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
2747
+ targetY: (labelLineY ?? yFromPrice(label.price)) - labelHeight / 2,
2704
2748
  y: 0
2705
2749
  };
2706
2750
  }).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
@@ -3498,15 +3542,10 @@ function createChart(element, options = {}) {
3498
3542
  }
3499
3543
  const midpoint = getMidpoint(first, second);
3500
3544
  const anchorRatio = clamp((midpoint.x - drawState.chartLeft) / drawState.chartWidth, 0, 1);
3501
- const startYMin = yMinOverride ?? drawState.yMin;
3502
- const startYMax = yMaxOverride ?? drawState.yMax;
3503
3545
  pinchZoomState = {
3504
3546
  startDistance: Math.max(1, getPointerDistance(first, second)),
3505
3547
  startSpan: xSpan,
3506
- anchorIndex: drawState.xStart + anchorRatio * xSpan,
3507
- startMidpoint: midpoint,
3508
- startYMin,
3509
- startYMax
3548
+ anchorIndex: drawState.xStart + anchorRatio * xSpan
3510
3549
  };
3511
3550
  isDragging = false;
3512
3551
  dragMode = null;
@@ -3535,11 +3574,6 @@ function createChart(element, options = {}) {
3535
3574
  xSpan = nextSpan;
3536
3575
  xCenter = nextStart + nextSpan / 2;
3537
3576
  clampXViewport();
3538
- const startYRange = pinchZoomState.startYMax - pinchZoomState.startYMin || 1;
3539
- const priceShift = (midpoint.y - pinchZoomState.startMidpoint.y) / drawState.chartHeight * startYRange;
3540
- const clampedY = clampYRange(pinchZoomState.startYMin + priceShift, pinchZoomState.startYMax + priceShift);
3541
- yMinOverride = clampedY.min;
3542
- yMaxOverride = clampedY.max;
3543
3577
  updateFollowLatest(false);
3544
3578
  emitViewportChange();
3545
3579
  draw();
package/docs/API.md CHANGED
@@ -57,6 +57,7 @@ Top-level options:
57
57
  - `candleColorEpsilon` (default `-1` = auto from `priceDecimals`; set `0` to disable tolerance)
58
58
  - `autoScaleSmoothing` (default `0.16`)
59
59
  - `autoScaleIgnoreLatestCandle` (default `true`)
60
+ - `pinOutOfRangeLines` (default `false`; when `true`, horizontal price/order/reference lines outside the visible price range are pinned to the top/bottom chart edge)
60
61
  - `doubleClickEnabled` (default `true`)
61
62
  - `doubleClickAction` (`"reset"` | `"placeLimitOrder"`, default `"reset"`)
62
63
  - `crosshair?: CrosshairOptions`
@@ -226,6 +227,7 @@ createChart(root, {
226
227
  - `labelTextColor` (default `#0f172a`)
227
228
  - `labelBorderRadius` (default `3`)
228
229
  - `showLabel` (default `true`)
230
+ - `pinOutOfRange?: boolean` (default `false`; overrides `ChartOptions.pinOutOfRangeLines` for this line)
229
231
 
230
232
  ### `OrderActionButton`
231
233
 
@@ -269,6 +271,7 @@ Common optional fields:
269
271
  - `widgetPosition?: "left" | "center" | "right"` (default `"left"`)
270
272
  - `widgetPaddingRight?: number` (default `10`, extra right margin when `widgetPosition` is `"right"`)
271
273
  - `draggable?: boolean` (default `false`)
274
+ - `pinOutOfRange?: boolean` (default `false`; overrides `ChartOptions.pinOutOfRangeLines` for this order/position line)
272
275
 
273
276
  Legacy single action button:
274
277
 
package/docs/RECIPES.md CHANGED
@@ -85,6 +85,22 @@ chart.addPriceLine({
85
85
  });
86
86
  ```
87
87
 
88
+ ## Pin out-of-range lines to the chart edge
89
+
90
+ By default, horizontal price/order/drawing lines outside the visible price range are hidden instead of being pinned to the top or bottom edge. Opt in globally or per line when you want marker-style behavior for important levels.
91
+
92
+ ```ts
93
+ const chart = createChart(rootEl, {
94
+ pinOutOfRangeLines: true
95
+ });
96
+
97
+ chart.addPriceLine({
98
+ price: 6400,
99
+ label: "Liquidation",
100
+ pinOutOfRange: true
101
+ });
102
+ ```
103
+
88
104
  ## Add chart drawing tools
89
105
 
90
106
  Drawing tools are separate from indicators. Indicators compute/render data series; drawings are user-created objects that can be persisted.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.65",
3
+ "version": "0.1.67",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",