@pipsend/charts 2.2.1 → 2.3.1

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.
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * Pipsend Charts v2.2.1
3
+ * Pipsend Charts v2.3.1
4
4
  * Copyright (c) 2026 Pipsend
5
5
  * Licensed under MIT License
6
6
  * Built on TradingView Lightweight Charts™ (Apache 2.0)
@@ -14641,6 +14641,8 @@
14641
14641
  return size(originalSize, 0.8);
14642
14642
  case 'square':
14643
14643
  return size(originalSize, 0.7);
14644
+ case 'tag':
14645
+ return size(originalSize, 1);
14644
14646
  }
14645
14647
  }
14646
14648
  function calculateShapeHeight(barSpacing) {
@@ -14750,6 +14752,85 @@
14750
14752
  y >= top && y <= top + squareSize;
14751
14753
  }
14752
14754
 
14755
+ const TAIL_HALF_WIDTH_RATIO = 0.62;
14756
+ /**
14757
+ * Lays the pointer into the edge the path is already travelling along, so the
14758
+ * bubble and its tail are a single outline. Two subpaths would leave a stroked
14759
+ * seam where they meet.
14760
+ */
14761
+ // eslint-disable-next-line max-params
14762
+ function tailPath(ctx, xCenter, edgeY, tailHeight, halfWidth, up) {
14763
+ // A tip with a small radius reads as drawn rather than as an artefact of the
14764
+ // rasteriser, which a bare triangle does at these sizes.
14765
+ const tipRadius = Math.min(halfWidth / 3, tailHeight / 3);
14766
+ const sign = up ? -1 : 1;
14767
+ const tipY = edgeY + sign * tailHeight;
14768
+ const shoulderY = tipY - sign * tipRadius;
14769
+ // The top edge is travelled left to right, the bottom edge right to left.
14770
+ const entry = up ? -halfWidth : halfWidth;
14771
+ ctx.lineTo(xCenter + entry, edgeY);
14772
+ ctx.lineTo(xCenter + (up ? -tipRadius : tipRadius), shoulderY);
14773
+ ctx.quadraticCurveTo(xCenter, tipY, xCenter + (up ? tipRadius : -tipRadius), shoulderY);
14774
+ ctx.lineTo(xCenter - entry, edgeY);
14775
+ }
14776
+ /**
14777
+ * Draws a rounded speech-bubble tag with an optional pointer towards its anchor.
14778
+ */
14779
+ function drawTag(ctx, params) {
14780
+ const hpr = params._internal_horizontalPixelRatio;
14781
+ const vpr = params._internal_verticalPixelRatio;
14782
+ const left = Math.round((params._internal_x - params._internal_width / 2) * hpr);
14783
+ const right = Math.round((params._internal_x + params._internal_width / 2) * hpr);
14784
+ const top = Math.round((params._internal_centerY - params._internal_height / 2) * vpr);
14785
+ const bottom = Math.round((params._internal_centerY + params._internal_height / 2) * vpr);
14786
+ const xCenter = Math.round(params._internal_x * hpr);
14787
+ const radius = Math.min(params._internal_radius * hpr, (right - left) / 2, (bottom - top) / 2);
14788
+ const tailHeight = params._internal_tailHeight * vpr;
14789
+ const tailHalfWidth = Math.min(tailHeight * TAIL_HALF_WIDTH_RATIO, Math.max(0, (right - left) / 2 - radius));
14790
+ const hasTail = params._internal_tailDirection !== 'none' && tailHalfWidth > 0 && tailHeight > 0;
14791
+ ctx.save();
14792
+ ctx.beginPath();
14793
+ if (!hasTail) {
14794
+ drawRoundRect(ctx, left, top, right - left, bottom - top, [radius, radius, radius, radius]);
14795
+ }
14796
+ else {
14797
+ ctx.moveTo(left + radius, top);
14798
+ if (params._internal_tailDirection === 'up') {
14799
+ tailPath(ctx, xCenter, top, tailHeight, tailHalfWidth, true);
14800
+ }
14801
+ ctx.lineTo(right - radius, top);
14802
+ ctx.arcTo(right, top, right, top + radius, radius);
14803
+ ctx.lineTo(right, bottom - radius);
14804
+ ctx.arcTo(right, bottom, right - radius, bottom, radius);
14805
+ if (params._internal_tailDirection === 'down') {
14806
+ tailPath(ctx, xCenter, bottom, tailHeight, tailHalfWidth, false);
14807
+ }
14808
+ ctx.lineTo(left + radius, bottom);
14809
+ ctx.arcTo(left, bottom, left, bottom - radius, radius);
14810
+ ctx.lineTo(left, top + radius);
14811
+ ctx.arcTo(left, top, left + radius, top, radius);
14812
+ }
14813
+ ctx.closePath();
14814
+ ctx.fillStyle = params._internal_fillColor;
14815
+ ctx.fill();
14816
+ if (params._internal_borderColor !== undefined && params._internal_borderWidth > 0) {
14817
+ ctx.lineWidth = Math.max(1, Math.round(params._internal_borderWidth * hpr));
14818
+ ctx.strokeStyle = params._internal_borderColor;
14819
+ ctx.stroke();
14820
+ }
14821
+ ctx.restore();
14822
+ }
14823
+ function hitTestTag(centerX, centerY, tag, x, y) {
14824
+ if (tag._internal_width <= 0) {
14825
+ return false;
14826
+ }
14827
+ const halfHeight = tag._internal_height / 2;
14828
+ const top = tag._internal_tailDirection === 'up' ? centerY - halfHeight - tag._internal_tailHeight : centerY - halfHeight;
14829
+ const bottom = tag._internal_tailDirection === 'down' ? centerY + halfHeight + tag._internal_tailHeight : centerY + halfHeight;
14830
+ const left = centerX - tag._internal_width / 2;
14831
+ return x >= left && x <= left + tag._internal_width && y >= top && y <= bottom;
14832
+ }
14833
+
14753
14834
  function drawText(ctx, text, x, y, horizontalPixelRatio, verticalPixelRatio) {
14754
14835
  ctx.save();
14755
14836
  ctx.scale(horizontalPixelRatio, verticalPixelRatio);
@@ -14766,9 +14847,11 @@
14766
14847
  constructor() {
14767
14848
  this._private__data = null;
14768
14849
  this._private__textWidthCache = new TextWidthCache();
14850
+ this._private__tagTextWidthCache = new TextWidthCache();
14769
14851
  this._private__fontSize = -1;
14770
14852
  this._private__fontFamily = '';
14771
14853
  this._private__font = '';
14854
+ this._private__tagFont = '';
14772
14855
  this._private__zOrder = 'normal';
14773
14856
  }
14774
14857
  _internal_setData(data) {
@@ -14779,7 +14862,11 @@
14779
14862
  this._private__fontSize = fontSize;
14780
14863
  this._private__fontFamily = fontFamily;
14781
14864
  this._private__font = makeFont(fontSize, fontFamily);
14865
+ // Tags carry a label rather than an annotation, so they get the heavier
14866
+ // weight — and their own cache, since widths differ per font.
14867
+ this._private__tagFont = makeFont(fontSize, fontFamily, '600');
14782
14868
  this._private__textWidthCache._internal_reset();
14869
+ this._private__tagTextWidthCache._internal_reset();
14783
14870
  }
14784
14871
  this._private__zOrder = zOrder;
14785
14872
  }
@@ -14819,14 +14906,21 @@
14819
14906
  return;
14820
14907
  }
14821
14908
  ctx.textBaseline = 'middle';
14822
- ctx.font = this._private__font;
14823
14909
  for (let index = this._private__data._internal_visibleRange.from; index < this._private__data._internal_visibleRange.to; index++) {
14824
14910
  const item = this._private__data._internal_items[index];
14911
+ const isTag = item._internal_tag !== undefined;
14912
+ ctx.font = isTag ? this._private__tagFont : this._private__font;
14825
14913
  if (item._internal_text !== undefined) {
14826
- item._internal_text._internal_width = this._private__textWidthCache._internal_measureText(ctx, item._internal_text._internal_content);
14914
+ const cache = isTag ? this._private__tagTextWidthCache : this._private__textWidthCache;
14915
+ item._internal_text._internal_width = cache._internal_measureText(ctx, item._internal_text._internal_content);
14827
14916
  item._internal_text._internal_height = this._private__fontSize;
14828
14917
  item._internal_text._internal_x = item._internal_x - item._internal_text._internal_width / 2;
14829
14918
  }
14919
+ if (item._internal_tag !== undefined) {
14920
+ const textWidth = item._internal_text !== undefined ? item._internal_text._internal_width : 0;
14921
+ item._internal_tag._internal_width = Math.max(textWidth + item._internal_tag._internal_paddingX * 2, item._internal_tag._internal_height);
14922
+ item._internal_tag._internal_left = item._internal_x - item._internal_tag._internal_width / 2;
14923
+ }
14830
14924
  drawItem(item, ctx, horizontalPixelRatio, verticalPixelRatio);
14831
14925
  }
14832
14926
  }
@@ -14841,12 +14935,40 @@
14841
14935
  };
14842
14936
  }
14843
14937
  function drawItem(item, ctx, horizontalPixelRatio, verticalPixelRatio) {
14938
+ if (item._internal_tag !== undefined) {
14939
+ drawTagItem(item, item._internal_tag, ctx, horizontalPixelRatio, verticalPixelRatio);
14940
+ return;
14941
+ }
14844
14942
  ctx.fillStyle = item._internal_color;
14845
14943
  if (item._internal_text !== undefined) {
14846
14944
  drawText(ctx, item._internal_text._internal_content, item._internal_text._internal_x, item._internal_text._internal_y, horizontalPixelRatio, verticalPixelRatio);
14847
14945
  }
14848
14946
  drawShape(item, ctx, bitmapShapeItemCoordinates(item, horizontalPixelRatio, verticalPixelRatio));
14849
14947
  }
14948
+ // eslint-disable-next-line max-params
14949
+ function drawTagItem(item, tag, ctx, horizontalPixelRatio, verticalPixelRatio) {
14950
+ if (item._internal_size === 0) {
14951
+ return;
14952
+ }
14953
+ drawTag(ctx, {
14954
+ _internal_x: item._internal_x,
14955
+ _internal_centerY: item._internal_y,
14956
+ _internal_width: tag._internal_width,
14957
+ _internal_height: tag._internal_height,
14958
+ _internal_tailHeight: tag._internal_tailHeight,
14959
+ _internal_tailDirection: tag._internal_tailDirection,
14960
+ _internal_radius: tag._internal_radius,
14961
+ _internal_fillColor: item._internal_color,
14962
+ _internal_borderColor: item._internal_borderColor,
14963
+ _internal_borderWidth: item._internal_borderWidth ?? 1,
14964
+ _internal_horizontalPixelRatio: horizontalPixelRatio,
14965
+ _internal_verticalPixelRatio: verticalPixelRatio,
14966
+ });
14967
+ if (item._internal_text !== undefined) {
14968
+ ctx.fillStyle = item._internal_textColor ?? '#FFFFFF';
14969
+ drawText(ctx, item._internal_text._internal_content, item._internal_text._internal_x, item._internal_text._internal_y, horizontalPixelRatio, verticalPixelRatio);
14970
+ }
14971
+ }
14850
14972
  function drawShape(item, ctx, coordinates) {
14851
14973
  if (item._internal_size === 0) {
14852
14974
  return;
@@ -14864,10 +14986,16 @@
14864
14986
  case 'square':
14865
14987
  drawSquare(ctx, coordinates, item._internal_size);
14866
14988
  return;
14989
+ case 'tag':
14990
+ // Handled by drawTagItem, which needs both pixel ratios and the text.
14991
+ return;
14867
14992
  }
14868
14993
  ensureNever(item._internal_shape);
14869
14994
  }
14870
14995
  function hitTestItem(item, x, y) {
14996
+ if (item._internal_tag !== undefined) {
14997
+ return item._internal_size !== 0 && hitTestTag(item._internal_x, item._internal_y, item._internal_tag, x, y);
14998
+ }
14871
14999
  if (item._internal_text !== undefined && hitTestText(item._internal_text._internal_x, item._internal_text._internal_y, item._internal_text._internal_width, item._internal_text._internal_height, x, y)) {
14872
15000
  return true;
14873
15001
  }
@@ -14886,6 +15014,8 @@
14886
15014
  return hitTestCircle(item._internal_x, item._internal_y, item._internal_size, x, y);
14887
15015
  case 'square':
14888
15016
  return hitTestSquare(item._internal_x, item._internal_y, item._internal_size, x, y);
15017
+ case 'tag':
15018
+ return false;
14889
15019
  }
14890
15020
  }
14891
15021
 
@@ -14918,15 +15048,71 @@
14918
15048
  }
14919
15049
  return;
14920
15050
  }
15051
+ /**
15052
+ * Lays out a `tag` marker: the bubble carries the text, so it is measured from
15053
+ * the font rather than from the bar spacing, and its pointer is aimed back at
15054
+ * the anchor price.
15055
+ */
15056
+ // eslint-disable-next-line max-params
15057
+ function fillTagSizeAndY(rendererItem, marker, anchorY, offsets, textHeight, shapeMargin, sizeMultiplier) {
15058
+ const height = Math.round((textHeight + 5 /* Constants.TagPaddingY */ * 2) * sizeMultiplier);
15059
+ const tailHeight = Math.round(7 /* Constants.TagTailHeight */ * sizeMultiplier);
15060
+ const ignoreOffset = isPriceMarker(marker.position);
15061
+ const halfHeight = height / 2;
15062
+ let tailDirection = 'none';
15063
+ let centerY = anchorY;
15064
+ switch (marker.position) {
15065
+ case 'aboveBar':
15066
+ case 'atPriceTop': {
15067
+ const offset = ignoreOffset ? 0 : offsets._internal_aboveBar;
15068
+ tailDirection = 'down';
15069
+ centerY = (anchorY - offset - tailHeight - halfHeight);
15070
+ if (!ignoreOffset) {
15071
+ offsets._internal_aboveBar += height + tailHeight + shapeMargin;
15072
+ }
15073
+ break;
15074
+ }
15075
+ case 'belowBar':
15076
+ case 'atPriceBottom': {
15077
+ const offset = ignoreOffset ? 0 : offsets._internal_belowBar;
15078
+ tailDirection = 'up';
15079
+ centerY = (anchorY + offset + tailHeight + halfHeight);
15080
+ if (!ignoreOffset) {
15081
+ offsets._internal_belowBar += height + tailHeight + shapeMargin;
15082
+ }
15083
+ break;
15084
+ }
15085
+ }
15086
+ const tag = {
15087
+ _internal_height: height,
15088
+ _internal_tailHeight: tailHeight,
15089
+ _internal_tailDirection: tailDirection,
15090
+ _internal_radius: marker.borderRadius ?? 6 /* Constants.TagRadius */,
15091
+ _internal_paddingX: 9 /* Constants.TagPaddingX */ * sizeMultiplier,
15092
+ _internal_left: 0,
15093
+ _internal_width: 0,
15094
+ };
15095
+ rendererItem._internal_size = height;
15096
+ rendererItem._internal_y = centerY;
15097
+ rendererItem._internal_tag = tag;
15098
+ if (rendererItem._internal_text !== undefined) {
15099
+ rendererItem._internal_text._internal_y = centerY;
15100
+ }
15101
+ }
14921
15102
  // eslint-disable-next-line max-params, complexity
14922
- function fillSizeAndY(rendererItem, marker, seriesData, offsets, textHeight, shapeMargin, series, chart) {
14923
- const price = getPrice(seriesData, marker, series.priceScale().options().invertScale);
15103
+ function fillSizeAndY(rendererItem, marker, seriesData, offsets, textHeight, shapeMargin, series, chart, invertScale) {
15104
+ const price = getPrice(seriesData, marker, invertScale);
14924
15105
  if (price === undefined) {
14925
15106
  return;
14926
15107
  }
14927
15108
  const ignoreOffset = isPriceMarker(marker.position);
14928
15109
  const timeScale = chart.timeScale();
14929
15110
  const sizeMultiplier = isNumber(marker.size) ? Math.max(marker.size, 0) : 1;
15111
+ if (marker.shape === 'tag') {
15112
+ const anchorY = ensureNotNull(series.priceToCoordinate(price));
15113
+ fillTagSizeAndY(rendererItem, marker, anchorY, offsets, textHeight, shapeMargin, sizeMultiplier);
15114
+ return;
15115
+ }
14930
15116
  const shapeSize = calculateShapeHeight(timeScale.options().barSpacing) * sizeMultiplier;
14931
15117
  const halfSize = shapeSize / 2;
14932
15118
  rendererItem._internal_size = shapeSize;
@@ -15033,6 +15219,10 @@
15033
15219
  _internal_externalId: marker.id,
15034
15220
  _internal_internalId: marker._internal_internalId,
15035
15221
  _internal_text: undefined,
15222
+ _internal_textColor: marker.textColor,
15223
+ _internal_borderColor: marker.borderColor,
15224
+ _internal_borderWidth: marker.borderWidth,
15225
+ _internal_tag: undefined,
15036
15226
  }));
15037
15227
  this._private__dataInvalidated = false;
15038
15228
  }
@@ -15043,13 +15233,22 @@
15043
15233
  return;
15044
15234
  }
15045
15235
  const visibleBarsRange = new RangeImpl(Math.floor(visibleBars.from), Math.ceil(visibleBars.to));
15046
- const firstValue = this._private__series.data()[0];
15236
+ const firstValue = this._private__series.dataByIndex(0, 1 /* MismatchDirection.NearestRight */);
15047
15237
  if (firstValue === null) {
15048
15238
  return;
15049
15239
  }
15050
15240
  if (this._private__data._internal_items.length === 0) {
15051
15241
  return;
15052
15242
  }
15243
+ // This runs inside the hit-test and paint loops, where a throw costs the
15244
+ // rest of the frame.
15245
+ let invertScale = false;
15246
+ try {
15247
+ invertScale = this._private__series.priceScale().options().invertScale;
15248
+ }
15249
+ catch (error) {
15250
+ invertScale = false;
15251
+ }
15053
15252
  let prevTimeIndex = NaN;
15054
15253
  const shapeMargin$1 = shapeMargin(timeScale.options().barSpacing);
15055
15254
  const offsets = {
@@ -15080,7 +15279,7 @@
15080
15279
  if (dataAt === null) {
15081
15280
  continue;
15082
15281
  }
15083
- fillSizeAndY(rendererItem, marker, dataAt, offsets, layoutOptions.fontSize, shapeMargin$1, this._private__series, this._private__chart);
15282
+ fillSizeAndY(rendererItem, marker, dataAt, offsets, layoutOptions.fontSize, shapeMargin$1, this._private__series, this._private__chart, invertScale);
15084
15283
  }
15085
15284
  this._private__invalidated = false;
15086
15285
  }
@@ -15238,6 +15437,10 @@
15238
15437
  text: marker.text,
15239
15438
  size: marker.size,
15240
15439
  price: marker.price,
15440
+ textColor: marker.textColor,
15441
+ borderColor: marker.borderColor,
15442
+ borderWidth: marker.borderWidth,
15443
+ borderRadius: marker.borderRadius,
15241
15444
  _internal_originalTime: marker.time,
15242
15445
  };
15243
15446
  if (marker.position === 'atPriceTop' ||
@@ -19539,10 +19742,13 @@
19539
19742
  constructor(chart, element) {
19540
19743
  this._private__lines = new Map();
19541
19744
  this._private__hoveredId = null;
19745
+ this._private__hoveredPart = null;
19542
19746
  this._private__dragging = null;
19543
19747
  this._private__pointerId = null;
19544
19748
  this._private__pressOrigin = null;
19545
19749
  this._private__pressedLine = null;
19750
+ /** Distance from the pointer to the dragged line when the press landed. */
19751
+ this._private__grabOffset = 0;
19546
19752
  /** Latest pointer position in pane coordinates, or null when off the panes. */
19547
19753
  this._private__point = null;
19548
19754
  this._private__paneIndex = undefined;
@@ -19596,8 +19802,12 @@
19596
19802
  if (this._private__dragging !== null) {
19597
19803
  return;
19598
19804
  }
19599
- const hovered = this._private__lineFromHit(param.hoveredObjectId);
19805
+ const hit = this._private__splitHit(param.hoveredObjectId);
19806
+ const hovered = hit === null ? null : this._private__lines.get(hit._internal_id) ?? null;
19600
19807
  this._private__setHovered(hovered ? hovered.id() : null);
19808
+ // The part is updated after the id, and separately from it: moving from a
19809
+ // label onto its close button keeps the same line hovered.
19810
+ this._private__setHoveredPart(hovered, hovered === null ? null : hit?._internal_part ?? null);
19601
19811
  this._private__applyCursor();
19602
19812
  }
19603
19813
  _private__onPointerDown(event) {
@@ -19616,6 +19826,7 @@
19616
19826
  this._private__pointerId = event.pointerId;
19617
19827
  this._private__element.setPointerCapture(event.pointerId);
19618
19828
  line.beginDrag();
19829
+ this._private__grabOffset = this._private__measureGrabOffset(line, event);
19619
19830
  this._private__applyCursor();
19620
19831
  event.preventDefault();
19621
19832
  event.stopPropagation();
@@ -19650,6 +19861,7 @@
19650
19861
  Math.abs(event.clientY - origin._internal_y) > CLICK_TOLERANCE_PX);
19651
19862
  if (dragged !== null) {
19652
19863
  this._private__dragging = null;
19864
+ this._private__grabOffset = 0;
19653
19865
  if (moved) {
19654
19866
  dragged.commitDrag();
19655
19867
  }
@@ -19677,6 +19889,7 @@
19677
19889
  }
19678
19890
  const line = this._private__dragging;
19679
19891
  this._private__dragging = null;
19892
+ this._private__grabOffset = 0;
19680
19893
  this._private__releasePointer();
19681
19894
  line.cancelDrag();
19682
19895
  this._private__applyCursor();
@@ -19701,24 +19914,65 @@
19701
19914
  if (line === null) {
19702
19915
  return null;
19703
19916
  }
19704
- // While the pointer is over the panes the engine gives pane coordinates.
19705
- // Once it leaves them — during a captured drag — fall back to the pane
19706
- // element, which still excludes the price axis.
19707
- if (this._private__point !== null && this._private__paneIndex === line.paneIndex()) {
19708
- return line.priceAt(this._private__point._internal_y);
19709
- }
19917
+ // The pointer event is the gesture's own position, so it is the source of
19918
+ // truth. The crosshair is only a mirror of it: it stops updating as soon
19919
+ // as the pointer leaves the pane canvas — over the price axis, past the
19920
+ // chart's edge — and a drag driven from it freezes there while the user
19921
+ // is still moving.
19922
+ const paneY = this._private__paneY(line, event);
19923
+ return paneY === null ? null : line.priceAt(paneY - this._private__grabOffset);
19924
+ }
19925
+ /** The pointer's position inside the line's pane, in pane coordinates. */
19926
+ _private__paneY(line, event) {
19710
19927
  const pane = this._private__chart.panes()[line.paneIndex()];
19711
19928
  const paneElement = pane ? pane.getHTMLElement() : null;
19712
- if (!paneElement) {
19713
- return null;
19929
+ if (paneElement) {
19930
+ return event.clientY - paneElement.getBoundingClientRect().top;
19931
+ }
19932
+ if (this._private__point !== null && this._private__paneIndex === line.paneIndex()) {
19933
+ return this._private__point._internal_y;
19934
+ }
19935
+ return null;
19936
+ }
19937
+ /**
19938
+ * Keeps the grab point: a line picked up by the edge of its label moves with
19939
+ * the pointer rather than centring itself on it. Ignored past a sane
19940
+ * distance, so a bad reading cannot send the line off on the first move.
19941
+ */
19942
+ _private__measureGrabOffset(line, event) {
19943
+ if (!line.dragAnchorY) {
19944
+ return 0;
19945
+ }
19946
+ const anchor = line.dragAnchorY();
19947
+ const paneY = this._private__paneY(line, event);
19948
+ if (anchor === null || paneY === null) {
19949
+ return 0;
19714
19950
  }
19715
- return line.priceAt(event.clientY - paneElement.getBoundingClientRect().top);
19951
+ const offset = paneY - anchor;
19952
+ return Math.abs(offset) > 40 /* Constants.MaxGrabOffsetPx */ ? 0 : offset;
19716
19953
  }
19717
- _private__lineFromHit(externalId) {
19718
- if (typeof externalId !== 'string') {
19954
+ /**
19955
+ * Hit ids are either a bare line id or `id#part`, which is how a line with
19956
+ * several targets reports which one was hit.
19957
+ */
19958
+ _private__splitHit(externalId) {
19959
+ if (typeof externalId !== 'string' || externalId.length === 0) {
19719
19960
  return null;
19720
19961
  }
19721
- return this._private__lines.get(externalId) ?? null;
19962
+ const separator = externalId.indexOf('#');
19963
+ if (separator === -1) {
19964
+ return { _internal_id: externalId, _internal_part: null };
19965
+ }
19966
+ return { _internal_id: externalId.slice(0, separator), _internal_part: externalId.slice(separator + 1) };
19967
+ }
19968
+ _private__setHoveredPart(line, part) {
19969
+ if (this._private__hoveredPart === part) {
19970
+ return;
19971
+ }
19972
+ this._private__hoveredPart = part;
19973
+ if (line && line.setHoveredPart) {
19974
+ line.setHoveredPart(part);
19975
+ }
19722
19976
  }
19723
19977
  _private__setHovered(id) {
19724
19978
  if (this._private__hoveredId === id) {
@@ -19728,6 +19982,7 @@
19728
19982
  this._private__lines.get(this._private__hoveredId)?.setHovered(false);
19729
19983
  }
19730
19984
  this._private__hoveredId = id;
19985
+ this._private__hoveredPart = null;
19731
19986
  if (id !== null) {
19732
19987
  this._private__lines.get(id)?.setHovered(true);
19733
19988
  }
@@ -20251,6 +20506,862 @@
20251
20506
  return new InteractiveLineManager(chart, series);
20252
20507
  }
20253
20508
 
20509
+ /**
20510
+ * Fans out labels that land on top of each other.
20511
+ *
20512
+ * Every position line on a chart shares one of these. Two positions opened at
20513
+ * the same price draw their labels at the same coordinate, and the one behind
20514
+ * becomes unreachable — the engine resolves the hit to whichever primitive it
20515
+ * reaches first, and that is always the same one. Nudging each collision down
20516
+ * leaves every label a strip of its own to be grabbed by.
20517
+ *
20518
+ * The line itself never moves: it stays at the price it belongs to, and only
20519
+ * the label is offset.
20520
+ */
20521
+ class PositionLineLabelLayout {
20522
+ constructor() {
20523
+ /** Registration order. It is what keeps the fan from reshuffling per frame. */
20524
+ this._private__order = [];
20525
+ this._private__claims = new Map();
20526
+ this._private__offsets = new Map();
20527
+ this._private__invalidated = true;
20528
+ }
20529
+ _internal_register(ownerId) {
20530
+ if (!this._private__claims.has(ownerId)) {
20531
+ this._private__order.push(ownerId);
20532
+ this._private__claims.set(ownerId, []);
20533
+ this._private__invalidated = true;
20534
+ }
20535
+ }
20536
+ _internal_unregister(ownerId) {
20537
+ const index = this._private__order.indexOf(ownerId);
20538
+ if (index !== -1) {
20539
+ this._private__order.splice(index, 1);
20540
+ }
20541
+ this._private__claims.delete(ownerId);
20542
+ this._private__invalidated = true;
20543
+ }
20544
+ _internal_isEmpty() {
20545
+ return this._private__order.length === 0;
20546
+ }
20547
+ /**
20548
+ * Publishes where an owner's labels want to sit. Called while each line
20549
+ * rebuilds its renderer data, so a fan can be one frame behind while prices
20550
+ * move — which is invisible, and cheaper than forcing a second pass.
20551
+ */
20552
+ _internal_setClaims(ownerId, claims) {
20553
+ this._private__claims.set(ownerId, claims);
20554
+ this._private__invalidated = true;
20555
+ }
20556
+ /** How far down a label has to move to clear the ones already placed. */
20557
+ _internal_offsetFor(ownerId, level) {
20558
+ if (this._private__invalidated) {
20559
+ this._private__recalculate();
20560
+ }
20561
+ return this._private__offsets.get(key(ownerId, level)) ?? 0;
20562
+ }
20563
+ _private__recalculate() {
20564
+ const offsets = new Map();
20565
+ const placed = [];
20566
+ for (const ownerId of this._private__order) {
20567
+ for (const claim of this._private__claims.get(ownerId) ?? []) {
20568
+ const gap = minGap(claim._internal_height);
20569
+ const maxOffset = claim._internal_height;
20570
+ // Moved down to the first coordinate that clears everything already
20571
+ // placed, and no further. Two levels whose own prices nearly part
20572
+ // them only need the few pixels they are short of — stepping by a
20573
+ // full gap each time is what made the fan look flung open.
20574
+ let y = claim._internal_y;
20575
+ for (const other of [...placed].sort(byY)) {
20576
+ if (Math.abs(y - other._internal_y) < gap) {
20577
+ y = other._internal_y + gap;
20578
+ }
20579
+ }
20580
+ let offset = y - claim._internal_y;
20581
+ if (offset > maxOffset) {
20582
+ // More labels than there is room to fan. The rest stack, which is
20583
+ // no worse than before and keeps the pile from running off.
20584
+ offset = 0;
20585
+ }
20586
+ placed.push({ _internal_y: claim._internal_y + offset, _internal_height: claim._internal_height });
20587
+ offsets.set(key(ownerId, claim._internal_level), offset);
20588
+ }
20589
+ }
20590
+ this._private__offsets = offsets;
20591
+ this._private__invalidated = false;
20592
+ }
20593
+ }
20594
+ function key(ownerId, level) {
20595
+ return `${ownerId}:${level}`;
20596
+ }
20597
+ /**
20598
+ * The strip a label keeps to itself. Comfortably clickable, and no wider —
20599
+ * every pixel here is a pixel the label sits away from its own line.
20600
+ */
20601
+ function minGap(height) {
20602
+ return Math.max(9, Math.round(height * 0.4));
20603
+ }
20604
+ function byY(a, b) {
20605
+ return a._internal_y - b._internal_y;
20606
+ }
20607
+ const layouts = new WeakMap();
20608
+ /** The layout is shared by every position line on the same chart. */
20609
+ function acquireLabelLayout(chart, ownerId) {
20610
+ let layout = layouts.get(chart);
20611
+ if (!layout) {
20612
+ layout = new PositionLineLabelLayout();
20613
+ layouts.set(chart, layout);
20614
+ }
20615
+ layout._internal_register(ownerId);
20616
+ return layout;
20617
+ }
20618
+ function releaseLabelLayout(chart, ownerId) {
20619
+ const layout = layouts.get(chart);
20620
+ if (!layout) {
20621
+ return;
20622
+ }
20623
+ layout._internal_unregister(ownerId);
20624
+ if (layout._internal_isEmpty()) {
20625
+ layouts.delete(chart);
20626
+ }
20627
+ }
20628
+
20629
+ const positionLineDefaultColors = {
20630
+ entry: '#2962FF',
20631
+ takeProfit: '#26A17B',
20632
+ stopLoss: '#D9A23B',
20633
+ };
20634
+ const positionLineThemeDefaults = {
20635
+ badgeTextColor: '#FFFFFF',
20636
+ borderRadius: 4,
20637
+ borderWidth: 1,
20638
+ paddingX: 8,
20639
+ paddingY: 6,
20640
+ backgroundOpacity: 1,
20641
+ };
20642
+ const positionLineLevels = ['takeProfit', 'entry', 'stopLoss'];
20643
+
20644
+ class PositionLineRenderer {
20645
+ constructor() {
20646
+ this._private__data = null;
20647
+ this._private__layouts = [];
20648
+ this._private__connectorX = 0;
20649
+ this._private__font = '';
20650
+ this._private__colorParser = new ColorParser([]);
20651
+ this._private__textWidthCache = new TextWidthCache(50);
20652
+ }
20653
+ _internal_setData(data) {
20654
+ this._private__data = data;
20655
+ }
20656
+ _internal_hitTest(x, y) {
20657
+ // The close button sits on top of the label, which sits on top of the
20658
+ // line, so the narrowest target is tested first.
20659
+ for (const layout of this._private__layouts) {
20660
+ if (layout._internal_closable && x >= layout._internal_closeLeft && x <= layout._internal_closeRight &&
20661
+ Math.abs(y - layout._internal_labelY) <= layout._internal_height / 2) {
20662
+ return { _internal_level: layout._internal_level, _internal_part: 'close' };
20663
+ }
20664
+ }
20665
+ for (const layout of this._private__layouts) {
20666
+ if (x >= layout._internal_pillLeft && x <= layout._internal_pillRight && Math.abs(y - layout._internal_labelY) <= layout._internal_height / 2) {
20667
+ return { _internal_level: layout._internal_level, _internal_part: 'body' };
20668
+ }
20669
+ }
20670
+ for (const layout of this._private__layouts) {
20671
+ if (Math.abs(y - layout._internal_y) <= 8 /* Constants.LineHitTolerance */) {
20672
+ return { _internal_level: layout._internal_level, _internal_part: 'line' };
20673
+ }
20674
+ }
20675
+ return null;
20676
+ }
20677
+ draw(target) {
20678
+ target.useBitmapCoordinateSpace((scope) => this._private__drawImpl(scope));
20679
+ }
20680
+ _private__drawImpl(scope) {
20681
+ const data = this._private__data;
20682
+ this._private__layouts = [];
20683
+ if (data === null || data._internal_levels.length === 0) {
20684
+ return;
20685
+ }
20686
+ const { context: ctx, mediaSize } = scope;
20687
+ ctx.save();
20688
+ if (this._private__font !== data._internal_font) {
20689
+ this._private__font = data._internal_font;
20690
+ this._private__textWidthCache._internal_reset();
20691
+ }
20692
+ ctx.font = data._internal_font;
20693
+ ctx.textBaseline = 'middle';
20694
+ this._private__connectorX = Math.max(0, mediaSize.width - data._internal_rightOffset);
20695
+ const lineEnd = data._internal_connectorVisible ? this._private__connectorX : mediaSize.width;
20696
+ for (const level of data._internal_levels) {
20697
+ this._private__layouts.push(this._private__drawLevel(scope, data, level, lineEnd));
20698
+ }
20699
+ if (data._internal_connectorVisible && data._internal_levels.length > 1) {
20700
+ this._private__drawConnector(scope, data);
20701
+ }
20702
+ ctx.restore();
20703
+ }
20704
+ // eslint-disable-next-line max-params
20705
+ _private__drawLevel(scope, data, level, lineEnd) {
20706
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20707
+ const height = data._internal_fontSize + data._internal_paddingY * 2;
20708
+ const badgeWidth = level._internal_badge.length === 0
20709
+ ? 0
20710
+ : Math.max(this._private__textWidthCache._internal_measureText(ctx, level._internal_badge) + data._internal_paddingX * 2, height * 0.85 /* Constants.BadgeMinWidthRatio */);
20711
+ const labelWidth = this._private__textWidthCache._internal_measureText(ctx, level._internal_text) + data._internal_paddingX * 2;
20712
+ const closeWidth = level._internal_closable ? height : 0;
20713
+ const pillWidth = badgeWidth + labelWidth + closeWidth;
20714
+ const pillRight = Math.max(pillWidth, lineEnd - 8 /* Constants.LabelGap */);
20715
+ const pillLeft = pillRight - pillWidth;
20716
+ const labelY = level._internal_y + level._internal_labelOffsetY;
20717
+ const top = labelY - height / 2;
20718
+ this._private__drawLineSegments(scope, level, pillLeft, pillRight, lineEnd);
20719
+ this._private__drawPill(scope, data, level, { _internal_left: pillLeft, _internal_top: top, _internal_width: pillWidth, _internal_height: height });
20720
+ let cellLeft = pillLeft;
20721
+ if (badgeWidth > 0) {
20722
+ this._private__drawBadge(scope, data, level, { _internal_left: cellLeft, _internal_top: top, _internal_width: badgeWidth, _internal_height: height });
20723
+ cellLeft += badgeWidth;
20724
+ this._private__drawSeparator(ctx, level._internal_color, cellLeft, top, height, hpr, vpr);
20725
+ }
20726
+ ctx.save();
20727
+ ctx.scale(hpr, vpr);
20728
+ ctx.textAlign = 'center';
20729
+ ctx.fillStyle = data._internal_textColor ?? level._internal_color;
20730
+ ctx.fillText(level._internal_text, cellLeft + labelWidth / 2, labelY);
20731
+ ctx.restore();
20732
+ cellLeft += labelWidth;
20733
+ if (level._internal_closable) {
20734
+ this._private__drawSeparator(ctx, level._internal_color, cellLeft, top, height, hpr, vpr);
20735
+ this._private__drawCloseButton(scope, data, level, { _internal_left: cellLeft, _internal_top: top, _internal_width: closeWidth, _internal_height: height });
20736
+ }
20737
+ return {
20738
+ _internal_level: level._internal_level,
20739
+ _internal_y: level._internal_y,
20740
+ _internal_labelY: labelY,
20741
+ _internal_height: height,
20742
+ _internal_pillLeft: pillLeft,
20743
+ _internal_pillRight: pillRight,
20744
+ _internal_closeLeft: cellLeft,
20745
+ _internal_closeRight: cellLeft + closeWidth,
20746
+ _internal_closable: level._internal_closable,
20747
+ };
20748
+ }
20749
+ // eslint-disable-next-line max-params
20750
+ _private__drawLineSegments(scope, level, pillLeft, pillRight, lineEnd) {
20751
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20752
+ const lineWidth = Math.max(1, Math.round(level._internal_lineWidth * vpr));
20753
+ const y = Math.round(level._internal_y * vpr) - Math.floor(lineWidth / 2);
20754
+ ctx.save();
20755
+ ctx.strokeStyle = level._internal_color;
20756
+ ctx.lineWidth = lineWidth;
20757
+ setLineStyle(ctx, level._internal_lineStyle);
20758
+ ctx.beginPath();
20759
+ if (level._internal_labelOffsetY !== 0) {
20760
+ // The label has been fanned off its line, so breaking the line for it
20761
+ // would leave a gap with nothing in it.
20762
+ ctx.moveTo(0, y);
20763
+ ctx.lineTo(Math.round(lineEnd * hpr), y);
20764
+ }
20765
+ else {
20766
+ ctx.moveTo(0, y);
20767
+ ctx.lineTo(Math.round(Math.max(0, pillLeft - 8 /* Constants.LabelGap */) * hpr), y);
20768
+ if (lineEnd > pillRight + 8 /* Constants.LabelGap */) {
20769
+ ctx.moveTo(Math.round((pillRight + 8 /* Constants.LabelGap */) * hpr), y);
20770
+ ctx.lineTo(Math.round(lineEnd * hpr), y);
20771
+ }
20772
+ }
20773
+ ctx.stroke();
20774
+ ctx.restore();
20775
+ }
20776
+ // eslint-disable-next-line max-params
20777
+ _private__drawPill(scope, data, level, box) {
20778
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20779
+ const hovered = data._internal_hoveredLevel === level._internal_level;
20780
+ const borderWidth = Math.max(1, Math.round((hovered ? data._internal_borderWidth + 1 : data._internal_borderWidth) * hpr));
20781
+ ctx.save();
20782
+ ctx.setLineDash([]);
20783
+ bitmapRoundRect(ctx, box, data._internal_borderRadius, hpr, vpr);
20784
+ ctx.fillStyle = this._private__colorParser._internal_applyAlpha(data._internal_background, data._internal_backgroundOpacity);
20785
+ ctx.fill();
20786
+ ctx.lineWidth = borderWidth;
20787
+ ctx.strokeStyle = level._internal_color;
20788
+ ctx.stroke();
20789
+ ctx.restore();
20790
+ }
20791
+ // eslint-disable-next-line max-params
20792
+ _private__drawBadge(scope, data, level, box) {
20793
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20794
+ if (level._internal_badgeFilled) {
20795
+ // The badge shares the label's left corners, so its fill has to stop
20796
+ // short of the border instead of squaring it off.
20797
+ const inset = data._internal_borderWidth;
20798
+ ctx.save();
20799
+ bitmapRoundRect(ctx, { _internal_left: box._internal_left + inset, _internal_top: box._internal_top + inset, _internal_width: box._internal_width - inset, _internal_height: box._internal_height - inset * 2 }, Math.max(0, data._internal_borderRadius - inset), hpr, vpr, 'left');
20800
+ ctx.fillStyle = level._internal_color;
20801
+ ctx.fill();
20802
+ ctx.restore();
20803
+ }
20804
+ ctx.save();
20805
+ ctx.scale(hpr, vpr);
20806
+ ctx.textAlign = 'center';
20807
+ ctx.fillStyle = level._internal_badgeFilled ? data._internal_badgeTextColor : level._internal_color;
20808
+ ctx.fillText(level._internal_badge, box._internal_left + box._internal_width / 2, box._internal_top + box._internal_height / 2);
20809
+ ctx.restore();
20810
+ }
20811
+ // eslint-disable-next-line max-params
20812
+ _private__drawSeparator(ctx, color, x, top, height, hpr, vpr) {
20813
+ const width = Math.max(1, Math.floor(hpr));
20814
+ ctx.save();
20815
+ ctx.setLineDash([]);
20816
+ ctx.fillStyle = this._private__colorParser._internal_applyAlpha(color, 0.4 /* Constants.SeparatorAlpha */);
20817
+ ctx.fillRect(Math.round(x * hpr), Math.round(top * vpr), width, Math.round(height * vpr));
20818
+ ctx.restore();
20819
+ }
20820
+ // eslint-disable-next-line max-params
20821
+ _private__drawCloseButton(scope, data, level, box) {
20822
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20823
+ const hovered = data._internal_hoveredLevel === level._internal_level && data._internal_hoveredPart === 'close';
20824
+ if (hovered) {
20825
+ ctx.save();
20826
+ const inset = data._internal_borderWidth;
20827
+ bitmapRoundRect(ctx, { _internal_left: box._internal_left, _internal_top: box._internal_top + inset, _internal_width: box._internal_width - inset, _internal_height: box._internal_height - inset * 2 }, Math.max(0, data._internal_borderRadius - inset), hpr, vpr, 'right');
20828
+ ctx.fillStyle = this._private__colorParser._internal_applyAlpha(level._internal_color, 0.25);
20829
+ ctx.fill();
20830
+ ctx.restore();
20831
+ }
20832
+ const centerX = (box._internal_left + box._internal_width / 2) * hpr;
20833
+ const centerY = (box._internal_top + box._internal_height / 2) * vpr;
20834
+ const arm = (7 /* Constants.CloseGlyphSize */ / 2) * hpr;
20835
+ ctx.save();
20836
+ ctx.setLineDash([]);
20837
+ ctx.lineCap = 'round';
20838
+ ctx.lineWidth = Math.max(1, Math.round(1.4 * hpr));
20839
+ ctx.strokeStyle = level._internal_color;
20840
+ ctx.beginPath();
20841
+ ctx.moveTo(centerX - arm, centerY - arm);
20842
+ ctx.lineTo(centerX + arm, centerY + arm);
20843
+ ctx.moveTo(centerX + arm, centerY - arm);
20844
+ ctx.lineTo(centerX - arm, centerY + arm);
20845
+ ctx.stroke();
20846
+ ctx.restore();
20847
+ }
20848
+ _private__drawConnector(scope, data) {
20849
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20850
+ const entry = data._internal_levels.find((level) => level._internal_level === 'entry') ?? data._internal_levels[0];
20851
+ const ys = data._internal_levels.map((level) => level._internal_y);
20852
+ const x = Math.round(this._private__connectorX * hpr);
20853
+ ctx.save();
20854
+ ctx.setLineDash([]);
20855
+ ctx.strokeStyle = entry._internal_color;
20856
+ ctx.lineWidth = Math.max(1, Math.floor(hpr));
20857
+ ctx.beginPath();
20858
+ ctx.moveTo(x, Math.round(Math.min(...ys) * vpr));
20859
+ ctx.lineTo(x, Math.round(Math.max(...ys) * vpr));
20860
+ ctx.stroke();
20861
+ for (const level of data._internal_levels) {
20862
+ ctx.beginPath();
20863
+ ctx.arc(x, Math.round(level._internal_y * vpr), 4.5 /* Constants.HandleRadius */ * hpr, 0, 2 * Math.PI);
20864
+ ctx.fillStyle = level._internal_level === 'entry'
20865
+ ? entry._internal_color
20866
+ : this._private__colorParser._internal_applyAlpha(data._internal_background, data._internal_backgroundOpacity);
20867
+ ctx.fill();
20868
+ ctx.lineWidth = Math.max(1, Math.round(1.5 * hpr));
20869
+ ctx.strokeStyle = level._internal_color;
20870
+ ctx.stroke();
20871
+ }
20872
+ ctx.restore();
20873
+ }
20874
+ }
20875
+ // eslint-disable-next-line max-params
20876
+ function bitmapRoundRect(ctx, box, radius, hpr, vpr, side = 'all') {
20877
+ const left = Math.round(box._internal_left * hpr);
20878
+ const top = Math.round(box._internal_top * vpr);
20879
+ const width = Math.round(box._internal_width * hpr);
20880
+ const height = Math.round(box._internal_height * vpr);
20881
+ const r = Math.min(radius * hpr, width / 2, height / 2);
20882
+ const radii = side === 'left'
20883
+ ? [r, 0, 0, r]
20884
+ : side === 'right'
20885
+ ? [0, r, r, 0]
20886
+ : [r, r, r, r];
20887
+ drawRoundRect(ctx, left, top, width, height, radii);
20888
+ }
20889
+
20890
+ class PositionLinePaneView {
20891
+ constructor() {
20892
+ this._private__renderer = new PositionLineRenderer();
20893
+ this._private__data = null;
20894
+ }
20895
+ _internal_update(data) {
20896
+ this._private__data = data;
20897
+ }
20898
+ renderer() {
20899
+ if (this._private__data === null) {
20900
+ return null;
20901
+ }
20902
+ this._private__renderer._internal_setData(this._private__data);
20903
+ return this._private__renderer;
20904
+ }
20905
+ zOrder() {
20906
+ return 'top';
20907
+ }
20908
+ _internal_hitTest(x, y) {
20909
+ return this._private__data === null ? null : this._private__renderer._internal_hitTest(x, y);
20910
+ }
20911
+ }
20912
+
20913
+ class PositionLineAxisView {
20914
+ constructor(source) {
20915
+ this._private__source = source;
20916
+ }
20917
+ coordinate() {
20918
+ // A hidden label is pushed far off the axis so it cannot reserve a gap
20919
+ // between the labels that are shown.
20920
+ return this._private__label()?._internal_coordinate ?? -1e3;
20921
+ }
20922
+ text() {
20923
+ return this._private__label()?._internal_text ?? '';
20924
+ }
20925
+ textColor() {
20926
+ return this._private__label()?._internal_textColor ?? '#FFFFFF';
20927
+ }
20928
+ backColor() {
20929
+ return this._private__label()?._internal_color ?? '#000000';
20930
+ }
20931
+ visible() {
20932
+ return this._private__label() !== null;
20933
+ }
20934
+ _private__label() {
20935
+ return this._private__source();
20936
+ }
20937
+ }
20938
+
20939
+ let nextPositionId = 0;
20940
+ const DEFAULT_RIGHT_OFFSET = 56;
20941
+ /**
20942
+ * A group of trading levels — entry, take profit and stop loss — drawn as one
20943
+ * unit: a line per level, a label carrying size and P&L, and a connector that
20944
+ * ties the three together.
20945
+ *
20946
+ * Input is not handled here. Every trading line on a chart shares a single
20947
+ * TradingLineController, which is what guarantees that one gesture moves one
20948
+ * level, and that a position line and a plain draggable price line on the same
20949
+ * chart do not fight over the pointer.
20950
+ */
20951
+ class PositionLine {
20952
+ constructor(chart, series, options) {
20953
+ this._private__paneView = new PositionLinePaneView();
20954
+ this._private__colorParser = new ColorParser([]);
20955
+ this._private__attached = false;
20956
+ this._private__hoveredLevel = null;
20957
+ this._private__hoveredPart = null;
20958
+ this._private__dragLevel = null;
20959
+ this._private__priceBeforeDrag = null;
20960
+ this._private__chart = chart;
20961
+ this._private__series = series;
20962
+ this._private__id = `pipsend-position-line-${nextPositionId++}`;
20963
+ this._private__options = options;
20964
+ this._private__theme = { ...positionLineThemeDefaults, ...options.theme };
20965
+ this._private__paneViews = [this._private__paneView];
20966
+ this._private__axisViews = positionLineLevels.map((level) => new PositionLineAxisView(() => this._private__axisLabel(level)));
20967
+ this._private__labelLayout = acquireLabelLayout(this._private__chart, this._private__id);
20968
+ this._private__series.attachPrimitive(this);
20969
+ acquireController(this._private__chart)?._internal_register(this);
20970
+ }
20971
+ // ---- ISeriesPrimitive ----
20972
+ attached(param) {
20973
+ this._private__requestUpdate = param.requestUpdate;
20974
+ this._private__attached = true;
20975
+ this.requestUpdate();
20976
+ }
20977
+ detached() {
20978
+ this._private__attached = false;
20979
+ this._private__requestUpdate = undefined;
20980
+ }
20981
+ paneViews() {
20982
+ return this._private__paneViews;
20983
+ }
20984
+ priceAxisViews() {
20985
+ return this._private__axisViews;
20986
+ }
20987
+ updateAllViews() {
20988
+ this._private__paneView._internal_update(this._private__buildRendererData());
20989
+ }
20990
+ hitTest(x, y) {
20991
+ const hit = this._private__paneView._internal_hitTest(x, y);
20992
+ if (hit === null) {
20993
+ return null;
20994
+ }
20995
+ const level = this._private__levelOptions(hit._internal_level);
20996
+ if (level === null) {
20997
+ return null;
20998
+ }
20999
+ return {
21000
+ externalId: `${this._private__id}#${hit._internal_level}:${hit._internal_part}`,
21001
+ zOrder: 'top',
21002
+ cursorStyle: hit._internal_part === 'close'
21003
+ ? 'pointer'
21004
+ : (level.draggable ?? true) ? 'ns-resize' : undefined,
21005
+ };
21006
+ }
21007
+ // ---- ControllableLine ----
21008
+ id() {
21009
+ return this._private__id;
21010
+ }
21011
+ paneIndex() {
21012
+ try {
21013
+ return this._private__series.getPane().paneIndex();
21014
+ }
21015
+ catch (error) {
21016
+ // The engine asserts the series belongs to a pane, and that lookup can
21017
+ // come back empty while panes are being rebuilt. The main pane is the
21018
+ // right guess and keeps the gesture alive instead of throwing mid-drag.
21019
+ return 0;
21020
+ }
21021
+ }
21022
+ setHoveredPart(part) {
21023
+ const [level, name] = part === null ? [null, null] : part.split(':');
21024
+ const nextLevel = level ?? null;
21025
+ const nextPart = name ?? null;
21026
+ if (this._private__hoveredLevel === nextLevel && this._private__hoveredPart === nextPart) {
21027
+ return;
21028
+ }
21029
+ this._private__hoveredLevel = nextLevel;
21030
+ this._private__hoveredPart = nextPart;
21031
+ this.requestUpdate();
21032
+ }
21033
+ setHovered(hovered) {
21034
+ if (!hovered) {
21035
+ this.setHoveredPart(null);
21036
+ }
21037
+ }
21038
+ isDraggable() {
21039
+ if (this._private__hoveredLevel === null || this._private__hoveredPart === 'close') {
21040
+ return false;
21041
+ }
21042
+ return this._private__levelOptions(this._private__hoveredLevel)?.draggable ?? true;
21043
+ }
21044
+ isClickable() {
21045
+ if (this._private__hoveredPart === 'close') {
21046
+ return true;
21047
+ }
21048
+ return this._private__options.onClick !== undefined;
21049
+ }
21050
+ priceAt(paneY) {
21051
+ const price = this._private__series.coordinateToPrice(paneY);
21052
+ return price === null ? null : price;
21053
+ }
21054
+ /**
21055
+ * Where the level being dragged actually sits. The controller measures the
21056
+ * pointer against it so the level moves by what the pointer moved, instead
21057
+ * of jumping to sit under it — which matters most when the label has been
21058
+ * fanned away from its own line.
21059
+ */
21060
+ dragAnchorY() {
21061
+ if (this._private__dragLevel === null) {
21062
+ return null;
21063
+ }
21064
+ const price = this._private__levelOptions(this._private__dragLevel)?.price;
21065
+ if (price === undefined || !isFinite(price)) {
21066
+ return null;
21067
+ }
21068
+ const y = this._private__series.priceToCoordinate(price);
21069
+ return y === null ? null : y;
21070
+ }
21071
+ beginDrag() {
21072
+ this._private__dragLevel = this._private__hoveredLevel;
21073
+ if (this._private__dragLevel === null) {
21074
+ return;
21075
+ }
21076
+ const price = this._private__levelOptions(this._private__dragLevel)?.price ?? null;
21077
+ this._private__priceBeforeDrag = price;
21078
+ if (price !== null && this._private__options.onDragStart) {
21079
+ this._private__options.onDragStart(this._private__dragLevel, price);
21080
+ }
21081
+ }
21082
+ dragTo(price) {
21083
+ if (this._private__dragLevel === null) {
21084
+ return;
21085
+ }
21086
+ this._private__applyPrice(this._private__dragLevel, price);
21087
+ if (this._private__options.onDragMove) {
21088
+ this._private__options.onDragMove(this._private__dragLevel, price);
21089
+ }
21090
+ }
21091
+ commitDrag() {
21092
+ const level = this._private__dragLevel;
21093
+ this._private__dragLevel = null;
21094
+ this._private__priceBeforeDrag = null;
21095
+ if (level === null) {
21096
+ return;
21097
+ }
21098
+ const price = this._private__levelOptions(level)?.price;
21099
+ if (price !== undefined && this._private__options.onDragEnd) {
21100
+ this._private__options.onDragEnd(level, price);
21101
+ }
21102
+ }
21103
+ cancelDrag() {
21104
+ const level = this._private__dragLevel;
21105
+ const original = this._private__priceBeforeDrag;
21106
+ this._private__dragLevel = null;
21107
+ this._private__priceBeforeDrag = null;
21108
+ if (level === null) {
21109
+ return;
21110
+ }
21111
+ // Reported even when the price did not move, so a caller that pauses work
21112
+ // on `onDragStart` always gets the matching end of the gesture.
21113
+ const restored = original !== null && original !== this._private__levelOptions(level)?.price;
21114
+ if (restored) {
21115
+ this._private__applyPrice(level, original);
21116
+ if (this._private__options.onDragMove) {
21117
+ this._private__options.onDragMove(level, original);
21118
+ }
21119
+ }
21120
+ if (this._private__options.onDragCancel) {
21121
+ this._private__options.onDragCancel(level, this._private__levelOptions(level)?.price ?? 0);
21122
+ }
21123
+ }
21124
+ click() {
21125
+ const level = this._private__hoveredLevel;
21126
+ if (level === null) {
21127
+ return;
21128
+ }
21129
+ if (this._private__hoveredPart === 'close') {
21130
+ if (this._private__options.onClose) {
21131
+ this._private__options.onClose(level);
21132
+ }
21133
+ return;
21134
+ }
21135
+ const price = this._private__levelOptions(level)?.price;
21136
+ if (price !== undefined && this._private__options.onClick) {
21137
+ this._private__options.onClick(level, price);
21138
+ }
21139
+ }
21140
+ // ---- public API ----
21141
+ /**
21142
+ * Updates the group. Any subset of options can be passed; the levels given
21143
+ * are merged into the current ones, and a level set to `undefined` is
21144
+ * removed.
21145
+ */
21146
+ applyOptions(options) {
21147
+ // A level under an active gesture owns its price. Callers feeding the
21148
+ // group from a socket re-send the old price several times a second, and
21149
+ // letting that through would pull the level out from under the cursor.
21150
+ const dragged = this._private__dragLevel;
21151
+ const draggedPrice = dragged === null ? undefined : this._private__levelOptions(dragged)?.price;
21152
+ const merged = { ...this._private__options, ...options };
21153
+ merged.entry = { ...this._private__options.entry, ...options.entry };
21154
+ merged.takeProfit = mergeLevel(this._private__options.takeProfit, options, 'takeProfit');
21155
+ merged.stopLoss = mergeLevel(this._private__options.stopLoss, options, 'stopLoss');
21156
+ this._private__options = merged;
21157
+ if (dragged !== null && draggedPrice !== undefined) {
21158
+ const level = this._private__levelOptions(dragged);
21159
+ if (level !== null) {
21160
+ level.price = draggedPrice;
21161
+ }
21162
+ }
21163
+ if (options.theme) {
21164
+ // Merged onto what is already applied, so two partial theme updates do
21165
+ // not undo each other.
21166
+ this._private__theme = { ...this._private__theme, ...options.theme };
21167
+ }
21168
+ this.requestUpdate();
21169
+ }
21170
+ /**
21171
+ * Moves one level to a new price, recomputing its text when the level has a
21172
+ * `formatText`.
21173
+ *
21174
+ * Ignored for a level the user is currently dragging — the gesture owns the
21175
+ * price until it reports back through
21176
+ * {@link PositionLineOptions.onDragEnd} or
21177
+ * {@link PositionLineOptions.onDragCancel}.
21178
+ */
21179
+ setPrice(level, price) {
21180
+ if (this._private__dragLevel === level) {
21181
+ return;
21182
+ }
21183
+ this._private__applyPrice(level, price);
21184
+ }
21185
+ /**
21186
+ * Replaces one level's label text. Use this for text that changes on its own
21187
+ * — a ticking P&L on a fixed entry — rather than with the price.
21188
+ */
21189
+ setText(level, text) {
21190
+ const options = this._private__levelOptions(level);
21191
+ if (options === null) {
21192
+ return;
21193
+ }
21194
+ options.text = text;
21195
+ this.requestUpdate();
21196
+ }
21197
+ /**
21198
+ * The current price of each level that is present.
21199
+ */
21200
+ prices() {
21201
+ const result = {};
21202
+ for (const level of positionLineLevels) {
21203
+ const options = this._private__levelOptions(level);
21204
+ if (options !== null) {
21205
+ result[level] = options.price;
21206
+ }
21207
+ }
21208
+ return result;
21209
+ }
21210
+ options() {
21211
+ return this._private__options;
21212
+ }
21213
+ /**
21214
+ * Removes the group from the chart and releases its share of the input
21215
+ * controller.
21216
+ */
21217
+ remove() {
21218
+ releaseController(this._private__chart, this);
21219
+ releaseLabelLayout(this._private__chart, this._private__id);
21220
+ this._private__series.detachPrimitive(this);
21221
+ }
21222
+ requestUpdate() {
21223
+ if (this._private__requestUpdate) {
21224
+ this._private__requestUpdate();
21225
+ }
21226
+ }
21227
+ /** Reads back the label the price axis should draw for a level. */
21228
+ _private__axisLabel(level) {
21229
+ const options = this._private__levelOptions(level);
21230
+ if (options === null || !isFinite(options.price) ||
21231
+ this._private__options.visible === false || options.axisLabelVisible === false) {
21232
+ return null;
21233
+ }
21234
+ const y = this._private__series.priceToCoordinate(options.price);
21235
+ if (y === null) {
21236
+ return null;
21237
+ }
21238
+ const color = options.color ?? positionLineDefaultColors[level];
21239
+ return {
21240
+ _internal_coordinate: y,
21241
+ _internal_text: this._private__series.priceFormatter().format(options.price),
21242
+ _internal_color: color,
21243
+ _internal_textColor: this._private__colorParser._internal_generateContrastColors(color)._internal_foreground,
21244
+ };
21245
+ }
21246
+ _private__levelOptions(level) {
21247
+ const options = level === 'entry' ? this._private__options.entry : this._private__options[level];
21248
+ return options ?? null;
21249
+ }
21250
+ _private__applyPrice(level, price) {
21251
+ const options = this._private__levelOptions(level);
21252
+ if (options === null) {
21253
+ return;
21254
+ }
21255
+ options.price = price;
21256
+ if (options.formatText) {
21257
+ options.text = options.formatText(price);
21258
+ }
21259
+ this.requestUpdate();
21260
+ }
21261
+ _private__buildRendererData() {
21262
+ if (!this._private__attached || this._private__options.visible === false) {
21263
+ return null;
21264
+ }
21265
+ const layout = this._private__chart.options()['layout'];
21266
+ const fontSize = this._private__theme.fontSize ?? layout.fontSize;
21267
+ const labelHeight = fontSize + this._private__theme.paddingY * 2;
21268
+ const levels = [];
21269
+ const claims = [];
21270
+ for (const level of positionLineLevels) {
21271
+ const options = this._private__levelOptions(level);
21272
+ // A level added through a patch can be missing its price until the
21273
+ // caller supplies one; drawing it would put it at NaN.
21274
+ if (options === null || !isFinite(options.price)) {
21275
+ continue;
21276
+ }
21277
+ const y = this._private__series.priceToCoordinate(options.price);
21278
+ if (y === null) {
21279
+ continue;
21280
+ }
21281
+ const isEntry = level === 'entry';
21282
+ claims.push({ _internal_level: level, _internal_y: y, _internal_height: labelHeight });
21283
+ levels.push({
21284
+ _internal_level: level,
21285
+ _internal_y: y,
21286
+ _internal_labelOffsetY: 0,
21287
+ _internal_color: options.color ?? positionLineDefaultColors[level],
21288
+ _internal_text: options.formatText ? options.formatText(options.price) : options.text ?? '',
21289
+ _internal_badge: options.badge ?? '',
21290
+ _internal_badgeFilled: options.badgeFilled ?? isEntry,
21291
+ _internal_lineWidth: options.lineWidth ?? 1,
21292
+ _internal_lineStyle: options.lineStyle ?? (isEntry ? 0 /* LineStyle.Solid */ : 2 /* LineStyle.Dashed */),
21293
+ _internal_closable: options.closable ?? true,
21294
+ _internal_draggable: options.draggable ?? true,
21295
+ });
21296
+ }
21297
+ if (levels.length === 0) {
21298
+ this._private__labelLayout._internal_setClaims(this._private__id, []);
21299
+ return null;
21300
+ }
21301
+ // Published before the offsets are read so every line on the chart —
21302
+ // positions, pending orders, alerts — is fanned against the same set.
21303
+ this._private__labelLayout._internal_setClaims(this._private__id, claims);
21304
+ for (const level of levels) {
21305
+ level._internal_labelOffsetY = this._private__labelLayout._internal_offsetFor(this._private__id, level._internal_level);
21306
+ }
21307
+ return {
21308
+ _internal_levels: levels,
21309
+ _internal_background: this._private__theme.background ?? resolveBackgroundColor(layout.background),
21310
+ _internal_textColor: this._private__theme.textColor,
21311
+ _internal_badgeTextColor: this._private__theme.badgeTextColor,
21312
+ _internal_borderRadius: this._private__theme.borderRadius,
21313
+ _internal_borderWidth: this._private__theme.borderWidth,
21314
+ _internal_paddingX: this._private__theme.paddingX,
21315
+ _internal_paddingY: this._private__theme.paddingY,
21316
+ _internal_backgroundOpacity: this._private__theme.backgroundOpacity,
21317
+ _internal_font: makeFont(fontSize, this._private__theme.fontFamily ?? layout.fontFamily),
21318
+ _internal_fontSize: fontSize,
21319
+ _internal_connectorVisible: this._private__options.connectorVisible !== false && levels.length > 1,
21320
+ _internal_rightOffset: this._private__options.rightOffset ?? DEFAULT_RIGHT_OFFSET,
21321
+ _internal_hoveredLevel: this._private__hoveredLevel,
21322
+ _internal_hoveredPart: this._private__hoveredPart,
21323
+ };
21324
+ }
21325
+ }
21326
+ function mergeLevel(current, options, key) {
21327
+ if (!(key in options)) {
21328
+ return current;
21329
+ }
21330
+ const next = options[key];
21331
+ return next === undefined ? undefined : { ...current, ...next };
21332
+ }
21333
+ /**
21334
+ * Reads the chart's own background so the label sits on a solid surface. The
21335
+ * parameter is typed as the public {@link Background}: the production build
21336
+ * renames the members of structural types it does not see in the public API,
21337
+ * and an anonymous `{ color: string }` here resolved to nothing at runtime.
21338
+ */
21339
+ function resolveBackgroundColor(background) {
21340
+ return background.type === "solid" /* ColorType.Solid */ ? background.color : background.topColor;
21341
+ }
21342
+ /**
21343
+ * Creates a position line group: entry, take profit and stop loss drawn as one
21344
+ * interactive unit.
21345
+ *
21346
+ * @param series - The series the levels are priced against.
21347
+ * @param chart - The chart the series belongs to.
21348
+ * @param options - The levels and their style.
21349
+ *
21350
+ * @example
21351
+ * ```js
21352
+ * const position = createPositionLine(candleSeries, chart, {
21353
+ * entry: { price: 4306.79, badge: '1', formatText: () => '+ 2.32 USD' },
21354
+ * takeProfit: { price: 4349.58, badge: '1', text: '+ 42.79 USD' },
21355
+ * stopLoss: { price: 4266.06, badge: '1', text: '- 40.73 USD' },
21356
+ * onClose: (level) => console.log('close', level),
21357
+ * onDragEnd: (level, price) => console.log(level, 'moved to', price),
21358
+ * });
21359
+ * ```
21360
+ */
21361
+ function createPositionLine(series, chart, options) {
21362
+ return new PositionLine(chart, series, options);
21363
+ }
21364
+
20254
21365
  /** Hit parts that drag the whole object rather than a single handle. */
20255
21366
  const WHOLE_OBJECT_PARTS = new Set([
20256
21367
  'line',
@@ -21130,13 +22241,6 @@
21130
22241
  // Clean up references
21131
22242
  this._private__primitive = null;
21132
22243
  this._private__paneView = null;
21133
- // Force chart update by triggering a resize
21134
- const chartElement = this._chart.chartElement?.();
21135
- if (chartElement) {
21136
- const currentWidth = chartElement.clientWidth;
21137
- const currentHeight = chartElement.clientHeight;
21138
- this._chart.resize(currentWidth, currentHeight);
21139
- }
21140
22244
  }
21141
22245
  if (this._private__trendOptions.onRemove) {
21142
22246
  this._private__trendOptions.onRemove();
@@ -21409,12 +22513,6 @@
21409
22513
  this._series.detachPrimitive(this._private__primitive);
21410
22514
  this._private__primitive = null;
21411
22515
  this._private__paneView = null;
21412
- const chartElement = this._chart.chartElement?.();
21413
- if (chartElement) {
21414
- const currentWidth = chartElement.clientWidth;
21415
- const currentHeight = chartElement.clientHeight;
21416
- this._chart.resize(currentWidth, currentHeight);
21417
- }
21418
22516
  }
21419
22517
  if (this._private__hLineOptions.onRemove) {
21420
22518
  this._private__hLineOptions.onRemove();
@@ -23253,12 +24351,6 @@
23253
24351
  this._series.detachPrimitive(this._private__primitive);
23254
24352
  this._private__primitive = null;
23255
24353
  this._private__paneView = null;
23256
- const chartElement = this._chart.chartElement?.();
23257
- if (chartElement) {
23258
- const currentWidth = chartElement.clientWidth;
23259
- const currentHeight = chartElement.clientHeight;
23260
- this._chart.resize(currentWidth, currentHeight);
23261
- }
23262
24354
  }
23263
24355
  if (this._private__vLineOptions.onRemove) {
23264
24356
  this._private__vLineOptions.onRemove();
@@ -23911,12 +25003,6 @@
23911
25003
  this._series.detachPrimitive(this._private__primitive);
23912
25004
  this._private__primitive = null;
23913
25005
  this._private__paneView = null;
23914
- const chartElement = this._chart.chartElement?.();
23915
- if (chartElement) {
23916
- const currentWidth = chartElement.clientWidth;
23917
- const currentHeight = chartElement.clientHeight;
23918
- this._chart.resize(currentWidth, currentHeight);
23919
- }
23920
25006
  }
23921
25007
  if (this._private__drawingManager) {
23922
25008
  this._private__drawingManager._internal_destroy();
@@ -23932,12 +25018,6 @@
23932
25018
  this._series.detachPrimitive(this._private__primitive);
23933
25019
  this._private__primitive = null;
23934
25020
  this._private__paneView = null;
23935
- const chartElement = this._chart.chartElement?.();
23936
- if (chartElement) {
23937
- const currentWidth = chartElement.clientWidth;
23938
- const currentHeight = chartElement.clientHeight;
23939
- this._chart.resize(currentWidth, currentHeight);
23940
- }
23941
25021
  }
23942
25022
  if (this._private__fibOptions.onRemove) {
23943
25023
  this._private__fibOptions.onRemove();
@@ -24365,12 +25445,6 @@
24365
25445
  this._series.detachPrimitive(this._private__primitive);
24366
25446
  this._private__primitive = null;
24367
25447
  this._private__paneView = null;
24368
- const chartElement = this._chart.chartElement?.();
24369
- if (chartElement) {
24370
- const currentWidth = chartElement.clientWidth;
24371
- const currentHeight = chartElement.clientHeight;
24372
- this._chart.resize(currentWidth, currentHeight);
24373
- }
24374
25448
  }
24375
25449
  }
24376
25450
  }
@@ -24812,12 +25886,6 @@
24812
25886
  this._series.detachPrimitive(this._private__primitive);
24813
25887
  this._private__primitive = null;
24814
25888
  this._private__paneView = null;
24815
- const chartElement = this._chart.chartElement?.();
24816
- if (chartElement) {
24817
- const currentWidth = chartElement.clientWidth;
24818
- const currentHeight = chartElement.clientHeight;
24819
- this._chart.resize(currentWidth, currentHeight);
24820
- }
24821
25889
  }
24822
25890
  if (this._private__fanOptions.onRemove) {
24823
25891
  this._private__fanOptions.onRemove();
@@ -25358,12 +26426,6 @@
25358
26426
  this._series.detachPrimitive(this._private__primitive);
25359
26427
  this._private__primitive = null;
25360
26428
  this._private__paneView = null;
25361
- const chartElement = this._chart.chartElement?.();
25362
- if (chartElement) {
25363
- const currentWidth = chartElement.clientWidth;
25364
- const currentHeight = chartElement.clientHeight;
25365
- this._chart.resize(currentWidth, currentHeight);
25366
- }
25367
26429
  }
25368
26430
  if (this._private__rectOptions.onRemove) {
25369
26431
  this._private__rectOptions.onRemove();
@@ -25824,12 +26886,6 @@
25824
26886
  this._series.detachPrimitive(this._private__primitive);
25825
26887
  this._private__primitive = null;
25826
26888
  this._private__paneView = null;
25827
- const chartElement = this._chart.chartElement?.();
25828
- if (chartElement) {
25829
- const currentWidth = chartElement.clientWidth;
25830
- const currentHeight = chartElement.clientHeight;
25831
- this._chart.resize(currentWidth, currentHeight);
25832
- }
25833
26889
  }
25834
26890
  if (this._private__circleOptions.onRemove) {
25835
26891
  this._private__circleOptions.onRemove();
@@ -26379,12 +27435,6 @@
26379
27435
  this._series.detachPrimitive(this._private__primitive);
26380
27436
  this._private__primitive = null;
26381
27437
  this._private__paneView = null;
26382
- const chartElement = this._chart.chartElement?.();
26383
- if (chartElement) {
26384
- const currentWidth = chartElement.clientWidth;
26385
- const currentHeight = chartElement.clientHeight;
26386
- this._chart.resize(currentWidth, currentHeight);
26387
- }
26388
27438
  }
26389
27439
  if (this._private__arrowOptions.onRemove) {
26390
27440
  this._private__arrowOptions.onRemove();
@@ -27245,12 +28295,6 @@
27245
28295
  this._series.detachPrimitive(this._private__primitive);
27246
28296
  this._private__primitive = null;
27247
28297
  this._private__paneView = null;
27248
- const chartElement = this._chart.chartElement?.();
27249
- if (chartElement) {
27250
- const currentWidth = chartElement.clientWidth;
27251
- const currentHeight = chartElement.clientHeight;
27252
- this._chart.resize(currentWidth, currentHeight);
27253
- }
27254
28298
  }
27255
28299
  if (this._private__textOptions.onRemove) {
27256
28300
  this._private__textOptions.onRemove();
@@ -27833,12 +28877,6 @@
27833
28877
  this._series.detachPrimitive(this._private__primitive);
27834
28878
  this._private__primitive = null;
27835
28879
  this._private__paneView = null;
27836
- const chartElement = this._chart.chartElement?.();
27837
- if (chartElement) {
27838
- const currentWidth = chartElement.clientWidth;
27839
- const currentHeight = chartElement.clientHeight;
27840
- this._chart.resize(currentWidth, currentHeight);
27841
- }
27842
28880
  }
27843
28881
  if (this._private__drawingManager) {
27844
28882
  this._private__drawingManager._internal_destroy();
@@ -28694,12 +29732,6 @@
28694
29732
  this._series.detachPrimitive(this._private__primitive);
28695
29733
  this._private__primitive = null;
28696
29734
  this._private__paneView = null;
28697
- const chartElement = this._chart.chartElement?.();
28698
- if (chartElement) {
28699
- const currentWidth = chartElement.clientWidth;
28700
- const currentHeight = chartElement.clientHeight;
28701
- this._chart.resize(currentWidth, currentHeight);
28702
- }
28703
29735
  }
28704
29736
  if (this._private__positionOptions.onRemove) {
28705
29737
  this._private__positionOptions.onRemove();
@@ -29042,12 +30074,6 @@
29042
30074
  this._series.detachPrimitive(this._private__primitive);
29043
30075
  this._private__primitive = null;
29044
30076
  this._private__paneView = null;
29045
- const chartElement = this._chart.chartElement?.();
29046
- if (chartElement) {
29047
- const currentWidth = chartElement.clientWidth;
29048
- const currentHeight = chartElement.clientHeight;
29049
- this._chart.resize(currentWidth, currentHeight);
29050
- }
29051
30077
  }
29052
30078
  if (this._private__ganttOptions.onRemove) {
29053
30079
  this._private__ganttOptions.onRemove();
@@ -29058,6 +30084,8 @@
29058
30084
  return new GanttTool(context, options);
29059
30085
  }
29060
30086
 
30087
+ /** Undo steps kept, each a full snapshot of every drawing. */
30088
+ const UNDO_HISTORY_LIMIT = 100;
29061
30089
  /**
29062
30090
  * Central manager for all drawing tools - TradingView style
29063
30091
  * Handles multiple tools, selection, deletion, and keyboard shortcuts
@@ -29080,6 +30108,8 @@
29080
30108
  this._private__pointerDownHandler = null;
29081
30109
  this._private__pointerUpHandler = null;
29082
30110
  this._private__pointerCancelHandler = null;
30111
+ this._private__lostCaptureHandler = null;
30112
+ this._private__windowPointerUpHandler = null;
29083
30113
  this._private__pointerId = null;
29084
30114
  /** A press that grabbed a tool already selected it; its click must not undo that. */
29085
30115
  this._private__gestureHandled = false;
@@ -29245,6 +30275,9 @@
29245
30275
  return;
29246
30276
  }
29247
30277
  this._private__past.push(this._private__committed);
30278
+ if (this._private__past.length > UNDO_HISTORY_LIMIT) {
30279
+ this._private__past.shift();
30280
+ }
29248
30281
  this._private__future = [];
29249
30282
  this._private__committed = this.serializeAll();
29250
30283
  }
@@ -29624,8 +30657,12 @@
29624
30657
  this._private__completionCheck = null;
29625
30658
  }
29626
30659
  this._private__drawingCompleted._internal_destroy();
30660
+ this._private__selectionChanged._internal_destroy();
29627
30661
  this.clearAll();
29628
30662
  this._private__detachEventHandlers();
30663
+ this._private__past = [];
30664
+ this._private__future = [];
30665
+ this._private__committed = [];
29629
30666
  }
29630
30667
  _private__attachEventHandlers() {
29631
30668
  // Click handler for selection
@@ -29645,9 +30682,15 @@
29645
30682
  this._private__pointerDownHandler = (event) => this._private__handlePointerDown(event);
29646
30683
  this._private__pointerUpHandler = (event) => this._private__handlePointerUp(event);
29647
30684
  this._private__pointerCancelHandler = (event) => this._private__handlePointerUp(event);
30685
+ this._private__lostCaptureHandler = (event) => this._private__handleLostPointerCapture(event);
29648
30686
  chartElement.addEventListener('pointerdown', this._private__pointerDownHandler);
29649
30687
  chartElement.addEventListener('pointerup', this._private__pointerUpHandler);
29650
30688
  chartElement.addEventListener('pointercancel', this._private__pointerCancelHandler);
30689
+ chartElement.addEventListener('lostpointercapture', this._private__lostCaptureHandler);
30690
+ }
30691
+ if (isRunningOnClientSide) {
30692
+ this._private__windowPointerUpHandler = (event) => this._private__handlePointerUp(event);
30693
+ window.addEventListener('pointerup', this._private__windowPointerUpHandler);
29651
30694
  }
29652
30695
  // Keyboard handler for shortcuts
29653
30696
  if (this._private__options.enableKeyboardShortcuts) {
@@ -29680,6 +30723,14 @@
29680
30723
  chartElement.removeEventListener('pointercancel', this._private__pointerCancelHandler);
29681
30724
  this._private__pointerCancelHandler = null;
29682
30725
  }
30726
+ if (this._private__lostCaptureHandler) {
30727
+ chartElement.removeEventListener('lostpointercapture', this._private__lostCaptureHandler);
30728
+ this._private__lostCaptureHandler = null;
30729
+ }
30730
+ }
30731
+ if (this._private__windowPointerUpHandler) {
30732
+ window.removeEventListener('pointerup', this._private__windowPointerUpHandler);
30733
+ this._private__windowPointerUpHandler = null;
29683
30734
  }
29684
30735
  if (this._private__keyboardHandler) {
29685
30736
  document.removeEventListener('keydown', this._private__keyboardHandler);
@@ -29805,7 +30856,12 @@
29805
30856
  this._private__gestureHandled = true;
29806
30857
  this._private__draggedToolId = toolId;
29807
30858
  this._private__pointerId = event.pointerId;
29808
- chartElement.setPointerCapture(event.pointerId);
30859
+ try {
30860
+ chartElement.setPointerCapture(event.pointerId);
30861
+ }
30862
+ catch (error) {
30863
+ this._private__pointerId = null;
30864
+ }
29809
30865
  tool.startDrag(endpoint, anchor);
29810
30866
  // Panning is stopped by disabling it for the gesture, not by preventing
29811
30867
  // the event: preventDefault on pointerdown suppresses the compatibility
@@ -29819,17 +30875,32 @@
29819
30875
  if (event && this._private__pointerId !== null && event.pointerId !== this._private__pointerId) {
29820
30876
  return;
29821
30877
  }
29822
- const tool = this._private__tools.get(this._private__draggedToolId);
29823
- if (tool) {
29824
- tool.stopDrag();
29825
- }
29826
- this._private__releasePointer();
29827
- this._private__enableChartInteractions();
30878
+ const draggedToolId = this._private__draggedToolId;
30879
+ // Cleared first: anything below can throw, and a half-finished release
30880
+ // would leave the manager dragging forever.
29828
30881
  this._private__isDragging = false;
29829
30882
  this._private__draggedToolId = null;
30883
+ try {
30884
+ const tool = this._private__tools.get(draggedToolId);
30885
+ if (tool) {
30886
+ tool.stopDrag();
30887
+ }
30888
+ }
30889
+ finally {
30890
+ this._private__releasePointer();
30891
+ this._private__enableChartInteractions();
30892
+ }
29830
30893
  // A finished drag is one step, not one per frame
29831
30894
  this.recordChange();
29832
30895
  }
30896
+ /** Ends the drag when capture is lost without a pointerup ever arriving. */
30897
+ _private__handleLostPointerCapture(event) {
30898
+ if (this._private__pointerId !== null && event.pointerId !== this._private__pointerId) {
30899
+ return;
30900
+ }
30901
+ this._private__pointerId = null;
30902
+ this._private__handlePointerUp();
30903
+ }
29833
30904
  /**
29834
30905
  * Pane element of a tool's series. The engine asserts the series has a pane,
29835
30906
  * so the lookup is guarded.
@@ -29880,9 +30951,11 @@
29880
30951
  return;
29881
30952
  }
29882
30953
  const options = this._private__chart.options();
30954
+ // `options()` returns the live object and `applyOptions` merges into it
30955
+ // in place, so a snapshot by reference restores all-false.
29883
30956
  this._private__interactionsBeforeDrag = {
29884
- _internal_handleScroll: options.handleScroll,
29885
- _internal_handleScale: options.handleScale,
30957
+ _internal_handleScroll: clone(options.handleScroll),
30958
+ _internal_handleScale: clone(options.handleScale),
29886
30959
  };
29887
30960
  this._private__chart.applyOptions({
29888
30961
  handleScroll: false,
@@ -29968,7 +31041,7 @@
29968
31041
  * Returns the current version as a string. For example `'1.0.0'`.
29969
31042
  */
29970
31043
  function version() {
29971
- return "2.2.1";
31044
+ return "2.3.1";
29972
31045
  }
29973
31046
 
29974
31047
  var PipsendChartsModule = /*#__PURE__*/Object.freeze({
@@ -30004,6 +31077,7 @@
30004
31077
  PitchforkTool: PitchforkTool,
30005
31078
  PolygonTool: PolygonTool,
30006
31079
  PositionCalculator: PositionCalculator,
31080
+ PositionLine: PositionLine,
30007
31081
  PositionMarkersManager: PositionMarkersManager,
30008
31082
  PositionTool: PositionTool,
30009
31083
  get PriceLineSource () { return PriceLineSource; },
@@ -30061,6 +31135,7 @@
30061
31135
  createParallelChannelTool: createParallelChannelTool,
30062
31136
  createPitchforkTool: createPitchforkTool,
30063
31137
  createPolygonTool: createPolygonTool,
31138
+ createPositionLine: createPositionLine,
30064
31139
  createPositionMarkers: createPositionMarkers,
30065
31140
  createPositionTool: createPositionTool,
30066
31141
  createRectangleTool: createRectangleTool,