@pipsend/charts 2.2.0 → 2.3.0

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.0
3
+ * Pipsend Charts v2.3.0-dev+202609220108
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,6 +19742,7 @@
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;
@@ -19596,8 +19800,12 @@
19596
19800
  if (this._private__dragging !== null) {
19597
19801
  return;
19598
19802
  }
19599
- const hovered = this._private__lineFromHit(param.hoveredObjectId);
19803
+ const hit = this._private__splitHit(param.hoveredObjectId);
19804
+ const hovered = hit === null ? null : this._private__lines.get(hit._internal_id) ?? null;
19600
19805
  this._private__setHovered(hovered ? hovered.id() : null);
19806
+ // The part is updated after the id, and separately from it: moving from a
19807
+ // label onto its close button keeps the same line hovered.
19808
+ this._private__setHoveredPart(hovered, hovered === null ? null : hit?._internal_part ?? null);
19601
19809
  this._private__applyCursor();
19602
19810
  }
19603
19811
  _private__onPointerDown(event) {
@@ -19701,24 +19909,43 @@
19701
19909
  if (line === null) {
19702
19910
  return null;
19703
19911
  }
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.
19912
+ // The pointer event is the gesture's own position, so it is the source of
19913
+ // truth. The crosshair is only a mirror of it: it stops updating as soon
19914
+ // as the pointer leaves the pane canvas — over the price axis, past the
19915
+ // chart's edge — and a drag driven from it freezes there while the user
19916
+ // is still moving.
19917
+ const pane = this._private__chart.panes()[line.paneIndex()];
19918
+ const paneElement = pane ? pane.getHTMLElement() : null;
19919
+ if (paneElement) {
19920
+ return line.priceAt(event.clientY - paneElement.getBoundingClientRect().top);
19921
+ }
19707
19922
  if (this._private__point !== null && this._private__paneIndex === line.paneIndex()) {
19708
19923
  return line.priceAt(this._private__point._internal_y);
19709
19924
  }
19710
- const pane = this._private__chart.panes()[line.paneIndex()];
19711
- const paneElement = pane ? pane.getHTMLElement() : null;
19712
- if (!paneElement) {
19925
+ return null;
19926
+ }
19927
+ /**
19928
+ * Hit ids are either a bare line id or `id#part`, which is how a line with
19929
+ * several targets reports which one was hit.
19930
+ */
19931
+ _private__splitHit(externalId) {
19932
+ if (typeof externalId !== 'string' || externalId.length === 0) {
19713
19933
  return null;
19714
19934
  }
19715
- return line.priceAt(event.clientY - paneElement.getBoundingClientRect().top);
19935
+ const separator = externalId.indexOf('#');
19936
+ if (separator === -1) {
19937
+ return { _internal_id: externalId, _internal_part: null };
19938
+ }
19939
+ return { _internal_id: externalId.slice(0, separator), _internal_part: externalId.slice(separator + 1) };
19716
19940
  }
19717
- _private__lineFromHit(externalId) {
19718
- if (typeof externalId !== 'string') {
19719
- return null;
19941
+ _private__setHoveredPart(line, part) {
19942
+ if (this._private__hoveredPart === part) {
19943
+ return;
19944
+ }
19945
+ this._private__hoveredPart = part;
19946
+ if (line && line.setHoveredPart) {
19947
+ line.setHoveredPart(part);
19720
19948
  }
19721
- return this._private__lines.get(externalId) ?? null;
19722
19949
  }
19723
19950
  _private__setHovered(id) {
19724
19951
  if (this._private__hoveredId === id) {
@@ -19728,6 +19955,7 @@
19728
19955
  this._private__lines.get(this._private__hoveredId)?.setHovered(false);
19729
19956
  }
19730
19957
  this._private__hoveredId = id;
19958
+ this._private__hoveredPart = null;
19731
19959
  if (id !== null) {
19732
19960
  this._private__lines.get(id)?.setHovered(true);
19733
19961
  }
@@ -20251,6 +20479,702 @@
20251
20479
  return new InteractiveLineManager(chart, series);
20252
20480
  }
20253
20481
 
20482
+ const positionLineDefaultColors = {
20483
+ entry: '#2962FF',
20484
+ takeProfit: '#26A17B',
20485
+ stopLoss: '#D9A23B',
20486
+ };
20487
+ const positionLineThemeDefaults = {
20488
+ badgeTextColor: '#FFFFFF',
20489
+ borderRadius: 4,
20490
+ borderWidth: 1,
20491
+ paddingX: 8,
20492
+ paddingY: 6,
20493
+ backgroundOpacity: 1,
20494
+ };
20495
+ const positionLineLevels = ['takeProfit', 'entry', 'stopLoss'];
20496
+
20497
+ class PositionLineRenderer {
20498
+ constructor() {
20499
+ this._private__data = null;
20500
+ this._private__layouts = [];
20501
+ this._private__connectorX = 0;
20502
+ this._private__font = '';
20503
+ this._private__colorParser = new ColorParser([]);
20504
+ this._private__textWidthCache = new TextWidthCache(50);
20505
+ }
20506
+ _internal_setData(data) {
20507
+ this._private__data = data;
20508
+ }
20509
+ _internal_hitTest(x, y) {
20510
+ // The close button sits on top of the label, which sits on top of the
20511
+ // line, so the narrowest target is tested first.
20512
+ for (const layout of this._private__layouts) {
20513
+ if (layout._internal_closable && x >= layout._internal_closeLeft && x <= layout._internal_closeRight &&
20514
+ Math.abs(y - layout._internal_y) <= layout._internal_height / 2) {
20515
+ return { _internal_level: layout._internal_level, _internal_part: 'close' };
20516
+ }
20517
+ }
20518
+ for (const layout of this._private__layouts) {
20519
+ if (x >= layout._internal_pillLeft && x <= layout._internal_pillRight && Math.abs(y - layout._internal_y) <= layout._internal_height / 2) {
20520
+ return { _internal_level: layout._internal_level, _internal_part: 'body' };
20521
+ }
20522
+ }
20523
+ for (const layout of this._private__layouts) {
20524
+ if (Math.abs(y - layout._internal_y) <= 8 /* Constants.LineHitTolerance */) {
20525
+ return { _internal_level: layout._internal_level, _internal_part: 'line' };
20526
+ }
20527
+ }
20528
+ return null;
20529
+ }
20530
+ draw(target) {
20531
+ target.useBitmapCoordinateSpace((scope) => this._private__drawImpl(scope));
20532
+ }
20533
+ _private__drawImpl(scope) {
20534
+ const data = this._private__data;
20535
+ this._private__layouts = [];
20536
+ if (data === null || data._internal_levels.length === 0) {
20537
+ return;
20538
+ }
20539
+ const { context: ctx, mediaSize } = scope;
20540
+ ctx.save();
20541
+ if (this._private__font !== data._internal_font) {
20542
+ this._private__font = data._internal_font;
20543
+ this._private__textWidthCache._internal_reset();
20544
+ }
20545
+ ctx.font = data._internal_font;
20546
+ ctx.textBaseline = 'middle';
20547
+ this._private__connectorX = Math.max(0, mediaSize.width - data._internal_rightOffset);
20548
+ const lineEnd = data._internal_connectorVisible ? this._private__connectorX : mediaSize.width;
20549
+ for (const level of data._internal_levels) {
20550
+ this._private__layouts.push(this._private__drawLevel(scope, data, level, lineEnd));
20551
+ }
20552
+ if (data._internal_connectorVisible && data._internal_levels.length > 1) {
20553
+ this._private__drawConnector(scope, data);
20554
+ }
20555
+ ctx.restore();
20556
+ }
20557
+ // eslint-disable-next-line max-params
20558
+ _private__drawLevel(scope, data, level, lineEnd) {
20559
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20560
+ const height = data._internal_fontSize + data._internal_paddingY * 2;
20561
+ const badgeWidth = level._internal_badge.length === 0
20562
+ ? 0
20563
+ : Math.max(this._private__textWidthCache._internal_measureText(ctx, level._internal_badge) + data._internal_paddingX * 2, height * 0.85 /* Constants.BadgeMinWidthRatio */);
20564
+ const labelWidth = this._private__textWidthCache._internal_measureText(ctx, level._internal_text) + data._internal_paddingX * 2;
20565
+ const closeWidth = level._internal_closable ? height : 0;
20566
+ const pillWidth = badgeWidth + labelWidth + closeWidth;
20567
+ const pillRight = Math.max(pillWidth, lineEnd - 8 /* Constants.LabelGap */);
20568
+ const pillLeft = pillRight - pillWidth;
20569
+ const top = level._internal_y - height / 2;
20570
+ this._private__drawLineSegments(scope, level, pillLeft, pillRight, lineEnd);
20571
+ this._private__drawPill(scope, data, level, { _internal_left: pillLeft, _internal_top: top, _internal_width: pillWidth, _internal_height: height });
20572
+ let cellLeft = pillLeft;
20573
+ if (badgeWidth > 0) {
20574
+ this._private__drawBadge(scope, data, level, { _internal_left: cellLeft, _internal_top: top, _internal_width: badgeWidth, _internal_height: height });
20575
+ cellLeft += badgeWidth;
20576
+ this._private__drawSeparator(ctx, level._internal_color, cellLeft, top, height, hpr, vpr);
20577
+ }
20578
+ ctx.save();
20579
+ ctx.scale(hpr, vpr);
20580
+ ctx.textAlign = 'center';
20581
+ ctx.fillStyle = data._internal_textColor ?? level._internal_color;
20582
+ ctx.fillText(level._internal_text, cellLeft + labelWidth / 2, level._internal_y);
20583
+ ctx.restore();
20584
+ cellLeft += labelWidth;
20585
+ if (level._internal_closable) {
20586
+ this._private__drawSeparator(ctx, level._internal_color, cellLeft, top, height, hpr, vpr);
20587
+ this._private__drawCloseButton(scope, data, level, { _internal_left: cellLeft, _internal_top: top, _internal_width: closeWidth, _internal_height: height });
20588
+ }
20589
+ return {
20590
+ _internal_level: level._internal_level,
20591
+ _internal_y: level._internal_y,
20592
+ _internal_height: height,
20593
+ _internal_pillLeft: pillLeft,
20594
+ _internal_pillRight: pillRight,
20595
+ _internal_closeLeft: cellLeft,
20596
+ _internal_closeRight: cellLeft + closeWidth,
20597
+ _internal_closable: level._internal_closable,
20598
+ };
20599
+ }
20600
+ // eslint-disable-next-line max-params
20601
+ _private__drawLineSegments(scope, level, pillLeft, pillRight, lineEnd) {
20602
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20603
+ const lineWidth = Math.max(1, Math.round(level._internal_lineWidth * vpr));
20604
+ const y = Math.round(level._internal_y * vpr) - Math.floor(lineWidth / 2);
20605
+ ctx.save();
20606
+ ctx.strokeStyle = level._internal_color;
20607
+ ctx.lineWidth = lineWidth;
20608
+ setLineStyle(ctx, level._internal_lineStyle);
20609
+ ctx.beginPath();
20610
+ ctx.moveTo(0, y);
20611
+ ctx.lineTo(Math.round(Math.max(0, pillLeft - 8 /* Constants.LabelGap */) * hpr), y);
20612
+ if (lineEnd > pillRight + 8 /* Constants.LabelGap */) {
20613
+ ctx.moveTo(Math.round((pillRight + 8 /* Constants.LabelGap */) * hpr), y);
20614
+ ctx.lineTo(Math.round(lineEnd * hpr), y);
20615
+ }
20616
+ ctx.stroke();
20617
+ ctx.restore();
20618
+ }
20619
+ // eslint-disable-next-line max-params
20620
+ _private__drawPill(scope, data, level, box) {
20621
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20622
+ const hovered = data._internal_hoveredLevel === level._internal_level;
20623
+ const borderWidth = Math.max(1, Math.round((hovered ? data._internal_borderWidth + 1 : data._internal_borderWidth) * hpr));
20624
+ ctx.save();
20625
+ ctx.setLineDash([]);
20626
+ bitmapRoundRect(ctx, box, data._internal_borderRadius, hpr, vpr);
20627
+ ctx.fillStyle = this._private__colorParser._internal_applyAlpha(data._internal_background, data._internal_backgroundOpacity);
20628
+ ctx.fill();
20629
+ ctx.lineWidth = borderWidth;
20630
+ ctx.strokeStyle = level._internal_color;
20631
+ ctx.stroke();
20632
+ ctx.restore();
20633
+ }
20634
+ // eslint-disable-next-line max-params
20635
+ _private__drawBadge(scope, data, level, box) {
20636
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20637
+ if (level._internal_badgeFilled) {
20638
+ // The badge shares the label's left corners, so its fill has to stop
20639
+ // short of the border instead of squaring it off.
20640
+ const inset = data._internal_borderWidth;
20641
+ ctx.save();
20642
+ 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');
20643
+ ctx.fillStyle = level._internal_color;
20644
+ ctx.fill();
20645
+ ctx.restore();
20646
+ }
20647
+ ctx.save();
20648
+ ctx.scale(hpr, vpr);
20649
+ ctx.textAlign = 'center';
20650
+ ctx.fillStyle = level._internal_badgeFilled ? data._internal_badgeTextColor : level._internal_color;
20651
+ ctx.fillText(level._internal_badge, box._internal_left + box._internal_width / 2, box._internal_top + box._internal_height / 2);
20652
+ ctx.restore();
20653
+ }
20654
+ // eslint-disable-next-line max-params
20655
+ _private__drawSeparator(ctx, color, x, top, height, hpr, vpr) {
20656
+ const width = Math.max(1, Math.floor(hpr));
20657
+ ctx.save();
20658
+ ctx.setLineDash([]);
20659
+ ctx.fillStyle = this._private__colorParser._internal_applyAlpha(color, 0.4 /* Constants.SeparatorAlpha */);
20660
+ ctx.fillRect(Math.round(x * hpr), Math.round(top * vpr), width, Math.round(height * vpr));
20661
+ ctx.restore();
20662
+ }
20663
+ // eslint-disable-next-line max-params
20664
+ _private__drawCloseButton(scope, data, level, box) {
20665
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20666
+ const hovered = data._internal_hoveredLevel === level._internal_level && data._internal_hoveredPart === 'close';
20667
+ if (hovered) {
20668
+ ctx.save();
20669
+ const inset = data._internal_borderWidth;
20670
+ 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');
20671
+ ctx.fillStyle = this._private__colorParser._internal_applyAlpha(level._internal_color, 0.25);
20672
+ ctx.fill();
20673
+ ctx.restore();
20674
+ }
20675
+ const centerX = (box._internal_left + box._internal_width / 2) * hpr;
20676
+ const centerY = (box._internal_top + box._internal_height / 2) * vpr;
20677
+ const arm = (7 /* Constants.CloseGlyphSize */ / 2) * hpr;
20678
+ ctx.save();
20679
+ ctx.setLineDash([]);
20680
+ ctx.lineCap = 'round';
20681
+ ctx.lineWidth = Math.max(1, Math.round(1.4 * hpr));
20682
+ ctx.strokeStyle = level._internal_color;
20683
+ ctx.beginPath();
20684
+ ctx.moveTo(centerX - arm, centerY - arm);
20685
+ ctx.lineTo(centerX + arm, centerY + arm);
20686
+ ctx.moveTo(centerX + arm, centerY - arm);
20687
+ ctx.lineTo(centerX - arm, centerY + arm);
20688
+ ctx.stroke();
20689
+ ctx.restore();
20690
+ }
20691
+ _private__drawConnector(scope, data) {
20692
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20693
+ const entry = data._internal_levels.find((level) => level._internal_level === 'entry') ?? data._internal_levels[0];
20694
+ const ys = data._internal_levels.map((level) => level._internal_y);
20695
+ const x = Math.round(this._private__connectorX * hpr);
20696
+ ctx.save();
20697
+ ctx.setLineDash([]);
20698
+ ctx.strokeStyle = entry._internal_color;
20699
+ ctx.lineWidth = Math.max(1, Math.floor(hpr));
20700
+ ctx.beginPath();
20701
+ ctx.moveTo(x, Math.round(Math.min(...ys) * vpr));
20702
+ ctx.lineTo(x, Math.round(Math.max(...ys) * vpr));
20703
+ ctx.stroke();
20704
+ for (const level of data._internal_levels) {
20705
+ ctx.beginPath();
20706
+ ctx.arc(x, Math.round(level._internal_y * vpr), 4.5 /* Constants.HandleRadius */ * hpr, 0, 2 * Math.PI);
20707
+ ctx.fillStyle = level._internal_level === 'entry'
20708
+ ? entry._internal_color
20709
+ : this._private__colorParser._internal_applyAlpha(data._internal_background, data._internal_backgroundOpacity);
20710
+ ctx.fill();
20711
+ ctx.lineWidth = Math.max(1, Math.round(1.5 * hpr));
20712
+ ctx.strokeStyle = level._internal_color;
20713
+ ctx.stroke();
20714
+ }
20715
+ ctx.restore();
20716
+ }
20717
+ }
20718
+ // eslint-disable-next-line max-params
20719
+ function bitmapRoundRect(ctx, box, radius, hpr, vpr, side = 'all') {
20720
+ const left = Math.round(box._internal_left * hpr);
20721
+ const top = Math.round(box._internal_top * vpr);
20722
+ const width = Math.round(box._internal_width * hpr);
20723
+ const height = Math.round(box._internal_height * vpr);
20724
+ const r = Math.min(radius * hpr, width / 2, height / 2);
20725
+ const radii = side === 'left'
20726
+ ? [r, 0, 0, r]
20727
+ : side === 'right'
20728
+ ? [0, r, r, 0]
20729
+ : [r, r, r, r];
20730
+ drawRoundRect(ctx, left, top, width, height, radii);
20731
+ }
20732
+
20733
+ class PositionLinePaneView {
20734
+ constructor() {
20735
+ this._private__renderer = new PositionLineRenderer();
20736
+ this._private__data = null;
20737
+ }
20738
+ _internal_update(data) {
20739
+ this._private__data = data;
20740
+ }
20741
+ renderer() {
20742
+ if (this._private__data === null) {
20743
+ return null;
20744
+ }
20745
+ this._private__renderer._internal_setData(this._private__data);
20746
+ return this._private__renderer;
20747
+ }
20748
+ zOrder() {
20749
+ return 'top';
20750
+ }
20751
+ _internal_hitTest(x, y) {
20752
+ return this._private__data === null ? null : this._private__renderer._internal_hitTest(x, y);
20753
+ }
20754
+ }
20755
+
20756
+ class PositionLineAxisView {
20757
+ constructor(source) {
20758
+ this._private__source = source;
20759
+ }
20760
+ coordinate() {
20761
+ // A hidden label is pushed far off the axis so it cannot reserve a gap
20762
+ // between the labels that are shown.
20763
+ return this._private__label()?._internal_coordinate ?? -1e3;
20764
+ }
20765
+ text() {
20766
+ return this._private__label()?._internal_text ?? '';
20767
+ }
20768
+ textColor() {
20769
+ return this._private__label()?._internal_textColor ?? '#FFFFFF';
20770
+ }
20771
+ backColor() {
20772
+ return this._private__label()?._internal_color ?? '#000000';
20773
+ }
20774
+ visible() {
20775
+ return this._private__label() !== null;
20776
+ }
20777
+ _private__label() {
20778
+ return this._private__source();
20779
+ }
20780
+ }
20781
+
20782
+ let nextPositionId = 0;
20783
+ const DEFAULT_RIGHT_OFFSET = 56;
20784
+ /**
20785
+ * A group of trading levels — entry, take profit and stop loss — drawn as one
20786
+ * unit: a line per level, a label carrying size and P&L, and a connector that
20787
+ * ties the three together.
20788
+ *
20789
+ * Input is not handled here. Every trading line on a chart shares a single
20790
+ * TradingLineController, which is what guarantees that one gesture moves one
20791
+ * level, and that a position line and a plain draggable price line on the same
20792
+ * chart do not fight over the pointer.
20793
+ */
20794
+ class PositionLine {
20795
+ constructor(chart, series, options) {
20796
+ this._private__paneView = new PositionLinePaneView();
20797
+ this._private__colorParser = new ColorParser([]);
20798
+ this._private__attached = false;
20799
+ this._private__hoveredLevel = null;
20800
+ this._private__hoveredPart = null;
20801
+ this._private__dragLevel = null;
20802
+ this._private__priceBeforeDrag = null;
20803
+ this._private__chart = chart;
20804
+ this._private__series = series;
20805
+ this._private__id = `pipsend-position-line-${nextPositionId++}`;
20806
+ this._private__options = options;
20807
+ this._private__theme = { ...positionLineThemeDefaults, ...options.theme };
20808
+ this._private__paneViews = [this._private__paneView];
20809
+ this._private__axisViews = positionLineLevels.map((level) => new PositionLineAxisView(() => this._private__axisLabel(level)));
20810
+ this._private__series.attachPrimitive(this);
20811
+ acquireController(this._private__chart)?._internal_register(this);
20812
+ }
20813
+ // ---- ISeriesPrimitive ----
20814
+ attached(param) {
20815
+ this._private__requestUpdate = param.requestUpdate;
20816
+ this._private__attached = true;
20817
+ this.requestUpdate();
20818
+ }
20819
+ detached() {
20820
+ this._private__attached = false;
20821
+ this._private__requestUpdate = undefined;
20822
+ }
20823
+ paneViews() {
20824
+ return this._private__paneViews;
20825
+ }
20826
+ priceAxisViews() {
20827
+ return this._private__axisViews;
20828
+ }
20829
+ updateAllViews() {
20830
+ this._private__paneView._internal_update(this._private__buildRendererData());
20831
+ }
20832
+ hitTest(x, y) {
20833
+ const hit = this._private__paneView._internal_hitTest(x, y);
20834
+ if (hit === null) {
20835
+ return null;
20836
+ }
20837
+ const level = this._private__levelOptions(hit._internal_level);
20838
+ if (level === null) {
20839
+ return null;
20840
+ }
20841
+ return {
20842
+ externalId: `${this._private__id}#${hit._internal_level}:${hit._internal_part}`,
20843
+ zOrder: 'top',
20844
+ cursorStyle: hit._internal_part === 'close'
20845
+ ? 'pointer'
20846
+ : (level.draggable ?? true) ? 'ns-resize' : undefined,
20847
+ };
20848
+ }
20849
+ // ---- ControllableLine ----
20850
+ id() {
20851
+ return this._private__id;
20852
+ }
20853
+ paneIndex() {
20854
+ try {
20855
+ return this._private__series.getPane().paneIndex();
20856
+ }
20857
+ catch (error) {
20858
+ // The engine asserts the series belongs to a pane, and that lookup can
20859
+ // come back empty while panes are being rebuilt. The main pane is the
20860
+ // right guess and keeps the gesture alive instead of throwing mid-drag.
20861
+ return 0;
20862
+ }
20863
+ }
20864
+ setHoveredPart(part) {
20865
+ const [level, name] = part === null ? [null, null] : part.split(':');
20866
+ const nextLevel = level ?? null;
20867
+ const nextPart = name ?? null;
20868
+ if (this._private__hoveredLevel === nextLevel && this._private__hoveredPart === nextPart) {
20869
+ return;
20870
+ }
20871
+ this._private__hoveredLevel = nextLevel;
20872
+ this._private__hoveredPart = nextPart;
20873
+ this.requestUpdate();
20874
+ }
20875
+ setHovered(hovered) {
20876
+ if (!hovered) {
20877
+ this.setHoveredPart(null);
20878
+ }
20879
+ }
20880
+ isDraggable() {
20881
+ if (this._private__hoveredLevel === null || this._private__hoveredPart === 'close') {
20882
+ return false;
20883
+ }
20884
+ return this._private__levelOptions(this._private__hoveredLevel)?.draggable ?? true;
20885
+ }
20886
+ isClickable() {
20887
+ if (this._private__hoveredPart === 'close') {
20888
+ return true;
20889
+ }
20890
+ return this._private__options.onClick !== undefined;
20891
+ }
20892
+ priceAt(paneY) {
20893
+ const price = this._private__series.coordinateToPrice(paneY);
20894
+ return price === null ? null : price;
20895
+ }
20896
+ beginDrag() {
20897
+ this._private__dragLevel = this._private__hoveredLevel;
20898
+ if (this._private__dragLevel === null) {
20899
+ return;
20900
+ }
20901
+ const price = this._private__levelOptions(this._private__dragLevel)?.price ?? null;
20902
+ this._private__priceBeforeDrag = price;
20903
+ if (price !== null && this._private__options.onDragStart) {
20904
+ this._private__options.onDragStart(this._private__dragLevel, price);
20905
+ }
20906
+ }
20907
+ dragTo(price) {
20908
+ if (this._private__dragLevel === null) {
20909
+ return;
20910
+ }
20911
+ this._private__applyPrice(this._private__dragLevel, price);
20912
+ if (this._private__options.onDragMove) {
20913
+ this._private__options.onDragMove(this._private__dragLevel, price);
20914
+ }
20915
+ }
20916
+ commitDrag() {
20917
+ const level = this._private__dragLevel;
20918
+ this._private__dragLevel = null;
20919
+ this._private__priceBeforeDrag = null;
20920
+ if (level === null) {
20921
+ return;
20922
+ }
20923
+ const price = this._private__levelOptions(level)?.price;
20924
+ if (price !== undefined && this._private__options.onDragEnd) {
20925
+ this._private__options.onDragEnd(level, price);
20926
+ }
20927
+ }
20928
+ cancelDrag() {
20929
+ const level = this._private__dragLevel;
20930
+ const original = this._private__priceBeforeDrag;
20931
+ this._private__dragLevel = null;
20932
+ this._private__priceBeforeDrag = null;
20933
+ if (level === null) {
20934
+ return;
20935
+ }
20936
+ // Reported even when the price did not move, so a caller that pauses work
20937
+ // on `onDragStart` always gets the matching end of the gesture.
20938
+ const restored = original !== null && original !== this._private__levelOptions(level)?.price;
20939
+ if (restored) {
20940
+ this._private__applyPrice(level, original);
20941
+ if (this._private__options.onDragMove) {
20942
+ this._private__options.onDragMove(level, original);
20943
+ }
20944
+ }
20945
+ if (this._private__options.onDragCancel) {
20946
+ this._private__options.onDragCancel(level, this._private__levelOptions(level)?.price ?? 0);
20947
+ }
20948
+ }
20949
+ click() {
20950
+ const level = this._private__hoveredLevel;
20951
+ if (level === null) {
20952
+ return;
20953
+ }
20954
+ if (this._private__hoveredPart === 'close') {
20955
+ if (this._private__options.onClose) {
20956
+ this._private__options.onClose(level);
20957
+ }
20958
+ return;
20959
+ }
20960
+ const price = this._private__levelOptions(level)?.price;
20961
+ if (price !== undefined && this._private__options.onClick) {
20962
+ this._private__options.onClick(level, price);
20963
+ }
20964
+ }
20965
+ // ---- public API ----
20966
+ /**
20967
+ * Updates the group. Any subset of options can be passed; the levels given
20968
+ * are merged into the current ones, and a level set to `undefined` is
20969
+ * removed.
20970
+ */
20971
+ applyOptions(options) {
20972
+ // A level under an active gesture owns its price. Callers feeding the
20973
+ // group from a socket re-send the old price several times a second, and
20974
+ // letting that through would pull the level out from under the cursor.
20975
+ const dragged = this._private__dragLevel;
20976
+ const draggedPrice = dragged === null ? undefined : this._private__levelOptions(dragged)?.price;
20977
+ const merged = { ...this._private__options, ...options };
20978
+ merged.entry = { ...this._private__options.entry, ...options.entry };
20979
+ merged.takeProfit = mergeLevel(this._private__options.takeProfit, options, 'takeProfit');
20980
+ merged.stopLoss = mergeLevel(this._private__options.stopLoss, options, 'stopLoss');
20981
+ this._private__options = merged;
20982
+ if (dragged !== null && draggedPrice !== undefined) {
20983
+ const level = this._private__levelOptions(dragged);
20984
+ if (level !== null) {
20985
+ level.price = draggedPrice;
20986
+ }
20987
+ }
20988
+ if (options.theme) {
20989
+ // Merged onto what is already applied, so two partial theme updates do
20990
+ // not undo each other.
20991
+ this._private__theme = { ...this._private__theme, ...options.theme };
20992
+ }
20993
+ this.requestUpdate();
20994
+ }
20995
+ /**
20996
+ * Moves one level to a new price, recomputing its text when the level has a
20997
+ * `formatText`.
20998
+ *
20999
+ * Ignored for a level the user is currently dragging — the gesture owns the
21000
+ * price until it reports back through
21001
+ * {@link PositionLineOptions.onDragEnd} or
21002
+ * {@link PositionLineOptions.onDragCancel}.
21003
+ */
21004
+ setPrice(level, price) {
21005
+ if (this._private__dragLevel === level) {
21006
+ return;
21007
+ }
21008
+ this._private__applyPrice(level, price);
21009
+ }
21010
+ /**
21011
+ * Replaces one level's label text. Use this for text that changes on its own
21012
+ * — a ticking P&L on a fixed entry — rather than with the price.
21013
+ */
21014
+ setText(level, text) {
21015
+ const options = this._private__levelOptions(level);
21016
+ if (options === null) {
21017
+ return;
21018
+ }
21019
+ options.text = text;
21020
+ this.requestUpdate();
21021
+ }
21022
+ /**
21023
+ * The current price of each level that is present.
21024
+ */
21025
+ prices() {
21026
+ const result = {};
21027
+ for (const level of positionLineLevels) {
21028
+ const options = this._private__levelOptions(level);
21029
+ if (options !== null) {
21030
+ result[level] = options.price;
21031
+ }
21032
+ }
21033
+ return result;
21034
+ }
21035
+ options() {
21036
+ return this._private__options;
21037
+ }
21038
+ /**
21039
+ * Removes the group from the chart and releases its share of the input
21040
+ * controller.
21041
+ */
21042
+ remove() {
21043
+ releaseController(this._private__chart, this);
21044
+ this._private__series.detachPrimitive(this);
21045
+ }
21046
+ requestUpdate() {
21047
+ if (this._private__requestUpdate) {
21048
+ this._private__requestUpdate();
21049
+ }
21050
+ }
21051
+ /** Reads back the label the price axis should draw for a level. */
21052
+ _private__axisLabel(level) {
21053
+ const options = this._private__levelOptions(level);
21054
+ if (options === null || !isFinite(options.price) ||
21055
+ this._private__options.visible === false || options.axisLabelVisible === false) {
21056
+ return null;
21057
+ }
21058
+ const y = this._private__series.priceToCoordinate(options.price);
21059
+ if (y === null) {
21060
+ return null;
21061
+ }
21062
+ const color = options.color ?? positionLineDefaultColors[level];
21063
+ return {
21064
+ _internal_coordinate: y,
21065
+ _internal_text: this._private__series.priceFormatter().format(options.price),
21066
+ _internal_color: color,
21067
+ _internal_textColor: this._private__colorParser._internal_generateContrastColors(color)._internal_foreground,
21068
+ };
21069
+ }
21070
+ _private__levelOptions(level) {
21071
+ const options = level === 'entry' ? this._private__options.entry : this._private__options[level];
21072
+ return options ?? null;
21073
+ }
21074
+ _private__applyPrice(level, price) {
21075
+ const options = this._private__levelOptions(level);
21076
+ if (options === null) {
21077
+ return;
21078
+ }
21079
+ options.price = price;
21080
+ if (options.formatText) {
21081
+ options.text = options.formatText(price);
21082
+ }
21083
+ this.requestUpdate();
21084
+ }
21085
+ _private__buildRendererData() {
21086
+ if (!this._private__attached || this._private__options.visible === false) {
21087
+ return null;
21088
+ }
21089
+ const levels = [];
21090
+ for (const level of positionLineLevels) {
21091
+ const options = this._private__levelOptions(level);
21092
+ // A level added through a patch can be missing its price until the
21093
+ // caller supplies one; drawing it would put it at NaN.
21094
+ if (options === null || !isFinite(options.price)) {
21095
+ continue;
21096
+ }
21097
+ const y = this._private__series.priceToCoordinate(options.price);
21098
+ if (y === null) {
21099
+ continue;
21100
+ }
21101
+ const isEntry = level === 'entry';
21102
+ levels.push({
21103
+ _internal_level: level,
21104
+ _internal_y: y,
21105
+ _internal_color: options.color ?? positionLineDefaultColors[level],
21106
+ _internal_text: options.formatText ? options.formatText(options.price) : options.text ?? '',
21107
+ _internal_badge: options.badge ?? '',
21108
+ _internal_badgeFilled: options.badgeFilled ?? isEntry,
21109
+ _internal_lineWidth: options.lineWidth ?? 1,
21110
+ _internal_lineStyle: options.lineStyle ?? (isEntry ? 0 /* LineStyle.Solid */ : 2 /* LineStyle.Dashed */),
21111
+ _internal_closable: options.closable ?? true,
21112
+ _internal_draggable: options.draggable ?? true,
21113
+ });
21114
+ }
21115
+ if (levels.length === 0) {
21116
+ return null;
21117
+ }
21118
+ const layout = this._private__chart.options()['layout'];
21119
+ const fontSize = this._private__theme.fontSize ?? layout.fontSize;
21120
+ return {
21121
+ _internal_levels: levels,
21122
+ _internal_background: this._private__theme.background ?? resolveBackgroundColor(layout.background),
21123
+ _internal_textColor: this._private__theme.textColor,
21124
+ _internal_badgeTextColor: this._private__theme.badgeTextColor,
21125
+ _internal_borderRadius: this._private__theme.borderRadius,
21126
+ _internal_borderWidth: this._private__theme.borderWidth,
21127
+ _internal_paddingX: this._private__theme.paddingX,
21128
+ _internal_paddingY: this._private__theme.paddingY,
21129
+ _internal_backgroundOpacity: this._private__theme.backgroundOpacity,
21130
+ _internal_font: makeFont(fontSize, this._private__theme.fontFamily ?? layout.fontFamily),
21131
+ _internal_fontSize: fontSize,
21132
+ _internal_connectorVisible: this._private__options.connectorVisible !== false && levels.length > 1,
21133
+ _internal_rightOffset: this._private__options.rightOffset ?? DEFAULT_RIGHT_OFFSET,
21134
+ _internal_hoveredLevel: this._private__hoveredLevel,
21135
+ _internal_hoveredPart: this._private__hoveredPart,
21136
+ };
21137
+ }
21138
+ }
21139
+ function mergeLevel(current, options, key) {
21140
+ if (!(key in options)) {
21141
+ return current;
21142
+ }
21143
+ const next = options[key];
21144
+ return next === undefined ? undefined : { ...current, ...next };
21145
+ }
21146
+ /**
21147
+ * Reads the chart's own background so the label sits on a solid surface. The
21148
+ * parameter is typed as the public {@link Background}: the production build
21149
+ * renames the members of structural types it does not see in the public API,
21150
+ * and an anonymous `{ color: string }` here resolved to nothing at runtime.
21151
+ */
21152
+ function resolveBackgroundColor(background) {
21153
+ return background.type === "solid" /* ColorType.Solid */ ? background.color : background.topColor;
21154
+ }
21155
+ /**
21156
+ * Creates a position line group: entry, take profit and stop loss drawn as one
21157
+ * interactive unit.
21158
+ *
21159
+ * @param series - The series the levels are priced against.
21160
+ * @param chart - The chart the series belongs to.
21161
+ * @param options - The levels and their style.
21162
+ *
21163
+ * @example
21164
+ * ```js
21165
+ * const position = createPositionLine(candleSeries, chart, {
21166
+ * entry: { price: 4306.79, badge: '1', formatText: () => '+ 2.32 USD' },
21167
+ * takeProfit: { price: 4349.58, badge: '1', text: '+ 42.79 USD' },
21168
+ * stopLoss: { price: 4266.06, badge: '1', text: '- 40.73 USD' },
21169
+ * onClose: (level) => console.log('close', level),
21170
+ * onDragEnd: (level, price) => console.log(level, 'moved to', price),
21171
+ * });
21172
+ * ```
21173
+ */
21174
+ function createPositionLine(series, chart, options) {
21175
+ return new PositionLine(chart, series, options);
21176
+ }
21177
+
20254
21178
  /** Hit parts that drag the whole object rather than a single handle. */
20255
21179
  const WHOLE_OBJECT_PARTS = new Set([
20256
21180
  'line',
@@ -20408,6 +21332,9 @@
20408
21332
  get type() {
20409
21333
  return this._type;
20410
21334
  }
21335
+ isComplete() {
21336
+ return true;
21337
+ }
20411
21338
  /**
20412
21339
  * Lets a tool hand the base its own options object, once, in the constructor.
20413
21340
  *
@@ -20735,6 +21662,7 @@
20735
21662
  this._private__dragStartPoint2 = null;
20736
21663
  // Interactive mode
20737
21664
  this._private__isComplete = false;
21665
+ this._private__interactive = null;
20738
21666
  this._private__previewPoint = null;
20739
21667
  this._private__trendOptions = {
20740
21668
  lineColor: '#2962ff',
@@ -20782,7 +21710,7 @@
20782
21710
  this._private__isComplete = true;
20783
21711
  }
20784
21712
  });
20785
- // Start the interactive drawing mode
21713
+ this._private__interactive = manager;
20786
21714
  manager._internal_startDrawing();
20787
21715
  }
20788
21716
  _private__createPrimitive() {
@@ -21112,20 +22040,20 @@
21112
22040
  _update() {
21113
22041
  this._private__requestUpdate();
21114
22042
  }
22043
+ isComplete() {
22044
+ return this._private__isComplete;
22045
+ }
21115
22046
  remove() {
22047
+ if (this._private__interactive) {
22048
+ this._private__interactive._internal_destroy();
22049
+ this._private__interactive = null;
22050
+ }
21116
22051
  if (this._private__primitive) {
21117
22052
  // Detach from series
21118
22053
  this._series.detachPrimitive(this._private__primitive);
21119
22054
  // Clean up references
21120
22055
  this._private__primitive = null;
21121
22056
  this._private__paneView = null;
21122
- // Force chart update by triggering a resize
21123
- const chartElement = this._chart.chartElement?.();
21124
- if (chartElement) {
21125
- const currentWidth = chartElement.clientWidth;
21126
- const currentHeight = chartElement.clientHeight;
21127
- this._chart.resize(currentWidth, currentHeight);
21128
- }
21129
22057
  }
21130
22058
  if (this._private__trendOptions.onRemove) {
21131
22059
  this._private__trendOptions.onRemove();
@@ -21398,12 +22326,6 @@
21398
22326
  this._series.detachPrimitive(this._private__primitive);
21399
22327
  this._private__primitive = null;
21400
22328
  this._private__paneView = null;
21401
- const chartElement = this._chart.chartElement?.();
21402
- if (chartElement) {
21403
- const currentWidth = chartElement.clientWidth;
21404
- const currentHeight = chartElement.clientHeight;
21405
- this._chart.resize(currentWidth, currentHeight);
21406
- }
21407
22329
  }
21408
22330
  if (this._private__hLineOptions.onRemove) {
21409
22331
  this._private__hLineOptions.onRemove();
@@ -22019,6 +22941,9 @@
22019
22941
  points() {
22020
22942
  return this._private__points;
22021
22943
  }
22944
+ isComplete() {
22945
+ return this._private__isComplete;
22946
+ }
22022
22947
  remove() {
22023
22948
  this._private__stopDrawing();
22024
22949
  if (this._private__primitive) {
@@ -22489,6 +23414,9 @@
22489
23414
  isHovered() {
22490
23415
  return this._private__isHovered;
22491
23416
  }
23417
+ isComplete() {
23418
+ return this._private__isComplete;
23419
+ }
22492
23420
  remove() {
22493
23421
  this._private__stopDrawing();
22494
23422
  if (this._private__primitive) {
@@ -22953,6 +23881,9 @@
22953
23881
  isHovered() {
22954
23882
  return this._private__isHovered;
22955
23883
  }
23884
+ isComplete() {
23885
+ return this._private__isComplete;
23886
+ }
22956
23887
  remove() {
22957
23888
  this._private__stopDrawing();
22958
23889
  if (this._private__primitive) {
@@ -23233,12 +24164,6 @@
23233
24164
  this._series.detachPrimitive(this._private__primitive);
23234
24165
  this._private__primitive = null;
23235
24166
  this._private__paneView = null;
23236
- const chartElement = this._chart.chartElement?.();
23237
- if (chartElement) {
23238
- const currentWidth = chartElement.clientWidth;
23239
- const currentHeight = chartElement.clientHeight;
23240
- this._chart.resize(currentWidth, currentHeight);
23241
- }
23242
24167
  }
23243
24168
  if (this._private__vLineOptions.onRemove) {
23244
24169
  this._private__vLineOptions.onRemove();
@@ -23883,17 +24808,14 @@
23883
24808
  }
23884
24809
  return false;
23885
24810
  }
24811
+ isComplete() {
24812
+ return this._private__isComplete;
24813
+ }
23886
24814
  remove() {
23887
24815
  if (this._private__primitive) {
23888
24816
  this._series.detachPrimitive(this._private__primitive);
23889
24817
  this._private__primitive = null;
23890
24818
  this._private__paneView = null;
23891
- const chartElement = this._chart.chartElement?.();
23892
- if (chartElement) {
23893
- const currentWidth = chartElement.clientWidth;
23894
- const currentHeight = chartElement.clientHeight;
23895
- this._chart.resize(currentWidth, currentHeight);
23896
- }
23897
24819
  }
23898
24820
  if (this._private__drawingManager) {
23899
24821
  this._private__drawingManager._internal_destroy();
@@ -23909,12 +24831,6 @@
23909
24831
  this._series.detachPrimitive(this._private__primitive);
23910
24832
  this._private__primitive = null;
23911
24833
  this._private__paneView = null;
23912
- const chartElement = this._chart.chartElement?.();
23913
- if (chartElement) {
23914
- const currentWidth = chartElement.clientWidth;
23915
- const currentHeight = chartElement.clientHeight;
23916
- this._chart.resize(currentWidth, currentHeight);
23917
- }
23918
24834
  }
23919
24835
  if (this._private__fibOptions.onRemove) {
23920
24836
  this._private__fibOptions.onRemove();
@@ -24342,12 +25258,6 @@
24342
25258
  this._series.detachPrimitive(this._private__primitive);
24343
25259
  this._private__primitive = null;
24344
25260
  this._private__paneView = null;
24345
- const chartElement = this._chart.chartElement?.();
24346
- if (chartElement) {
24347
- const currentWidth = chartElement.clientWidth;
24348
- const currentHeight = chartElement.clientHeight;
24349
- this._chart.resize(currentWidth, currentHeight);
24350
- }
24351
25261
  }
24352
25262
  }
24353
25263
  }
@@ -24773,6 +25683,9 @@
24773
25683
  _update() {
24774
25684
  this._private__requestUpdate();
24775
25685
  }
25686
+ isComplete() {
25687
+ return !this._private__isDrawing;
25688
+ }
24776
25689
  remove() {
24777
25690
  if (this._private__clickHandler) {
24778
25691
  this._chart.unsubscribeClick(this._private__clickHandler);
@@ -24786,12 +25699,6 @@
24786
25699
  this._series.detachPrimitive(this._private__primitive);
24787
25700
  this._private__primitive = null;
24788
25701
  this._private__paneView = null;
24789
- const chartElement = this._chart.chartElement?.();
24790
- if (chartElement) {
24791
- const currentWidth = chartElement.clientWidth;
24792
- const currentHeight = chartElement.clientHeight;
24793
- this._chart.resize(currentWidth, currentHeight);
24794
- }
24795
25702
  }
24796
25703
  if (this._private__fanOptions.onRemove) {
24797
25704
  this._private__fanOptions.onRemove();
@@ -25316,6 +26223,9 @@
25316
26223
  _update() {
25317
26224
  this._private__requestUpdate();
25318
26225
  }
26226
+ isComplete() {
26227
+ return !this._private__isDrawing;
26228
+ }
25319
26229
  remove() {
25320
26230
  if (this._private__clickHandler) {
25321
26231
  this._chart.unsubscribeClick(this._private__clickHandler);
@@ -25329,12 +26239,6 @@
25329
26239
  this._series.detachPrimitive(this._private__primitive);
25330
26240
  this._private__primitive = null;
25331
26241
  this._private__paneView = null;
25332
- const chartElement = this._chart.chartElement?.();
25333
- if (chartElement) {
25334
- const currentWidth = chartElement.clientWidth;
25335
- const currentHeight = chartElement.clientHeight;
25336
- this._chart.resize(currentWidth, currentHeight);
25337
- }
25338
26242
  }
25339
26243
  if (this._private__rectOptions.onRemove) {
25340
26244
  this._private__rectOptions.onRemove();
@@ -25779,6 +26683,9 @@
25779
26683
  _update() {
25780
26684
  this._private__requestUpdate();
25781
26685
  }
26686
+ isComplete() {
26687
+ return !this._private__isDrawing;
26688
+ }
25782
26689
  remove() {
25783
26690
  if (this._private__clickHandler) {
25784
26691
  this._chart.unsubscribeClick(this._private__clickHandler);
@@ -25792,12 +26699,6 @@
25792
26699
  this._series.detachPrimitive(this._private__primitive);
25793
26700
  this._private__primitive = null;
25794
26701
  this._private__paneView = null;
25795
- const chartElement = this._chart.chartElement?.();
25796
- if (chartElement) {
25797
- const currentWidth = chartElement.clientWidth;
25798
- const currentHeight = chartElement.clientHeight;
25799
- this._chart.resize(currentWidth, currentHeight);
25800
- }
25801
26702
  }
25802
26703
  if (this._private__circleOptions.onRemove) {
25803
26704
  this._private__circleOptions.onRemove();
@@ -26321,6 +27222,9 @@
26321
27222
  _update() {
26322
27223
  this._private__requestUpdate();
26323
27224
  }
27225
+ isComplete() {
27226
+ return !this._private__isDrawing;
27227
+ }
26324
27228
  remove() {
26325
27229
  // Clean up click handler
26326
27230
  if (this._private__clickHandler) {
@@ -26344,12 +27248,6 @@
26344
27248
  this._series.detachPrimitive(this._private__primitive);
26345
27249
  this._private__primitive = null;
26346
27250
  this._private__paneView = null;
26347
- const chartElement = this._chart.chartElement?.();
26348
- if (chartElement) {
26349
- const currentWidth = chartElement.clientWidth;
26350
- const currentHeight = chartElement.clientHeight;
26351
- this._chart.resize(currentWidth, currentHeight);
26352
- }
26353
27251
  }
26354
27252
  if (this._private__arrowOptions.onRemove) {
26355
27253
  this._private__arrowOptions.onRemove();
@@ -26887,7 +27785,7 @@
26887
27785
  }
26888
27786
  _private__setupClickOutsideHandler() {
26889
27787
  // Set up click handler to finish editing when clicking outside
26890
- this._private__clickOutsideHandler = this._chart.subscribeClick((param) => {
27788
+ const handler = (param) => {
26891
27789
  if (!this._private__isEditing)
26892
27790
  return;
26893
27791
  // Get click coordinates
@@ -26903,7 +27801,9 @@
26903
27801
  this.remove();
26904
27802
  }
26905
27803
  }
26906
- });
27804
+ };
27805
+ this._private__clickOutsideHandler = handler;
27806
+ this._chart.subscribeClick(handler);
26907
27807
  }
26908
27808
  _private__createPrimitive() {
26909
27809
  this._private__paneView = this._private__createPaneView();
@@ -27208,12 +28108,6 @@
27208
28108
  this._series.detachPrimitive(this._private__primitive);
27209
28109
  this._private__primitive = null;
27210
28110
  this._private__paneView = null;
27211
- const chartElement = this._chart.chartElement?.();
27212
- if (chartElement) {
27213
- const currentWidth = chartElement.clientWidth;
27214
- const currentHeight = chartElement.clientHeight;
27215
- this._chart.resize(currentWidth, currentHeight);
27216
- }
27217
28111
  }
27218
28112
  if (this._private__textOptions.onRemove) {
27219
28113
  this._private__textOptions.onRemove();
@@ -27796,12 +28690,6 @@
27796
28690
  this._series.detachPrimitive(this._private__primitive);
27797
28691
  this._private__primitive = null;
27798
28692
  this._private__paneView = null;
27799
- const chartElement = this._chart.chartElement?.();
27800
- if (chartElement) {
27801
- const currentWidth = chartElement.clientWidth;
27802
- const currentHeight = chartElement.clientHeight;
27803
- this._chart.resize(currentWidth, currentHeight);
27804
- }
27805
28693
  }
27806
28694
  if (this._private__drawingManager) {
27807
28695
  this._private__drawingManager._internal_destroy();
@@ -28657,12 +29545,6 @@
28657
29545
  this._series.detachPrimitive(this._private__primitive);
28658
29546
  this._private__primitive = null;
28659
29547
  this._private__paneView = null;
28660
- const chartElement = this._chart.chartElement?.();
28661
- if (chartElement) {
28662
- const currentWidth = chartElement.clientWidth;
28663
- const currentHeight = chartElement.clientHeight;
28664
- this._chart.resize(currentWidth, currentHeight);
28665
- }
28666
29548
  }
28667
29549
  if (this._private__positionOptions.onRemove) {
28668
29550
  this._private__positionOptions.onRemove();
@@ -29005,12 +29887,6 @@
29005
29887
  this._series.detachPrimitive(this._private__primitive);
29006
29888
  this._private__primitive = null;
29007
29889
  this._private__paneView = null;
29008
- const chartElement = this._chart.chartElement?.();
29009
- if (chartElement) {
29010
- const currentWidth = chartElement.clientWidth;
29011
- const currentHeight = chartElement.clientHeight;
29012
- this._chart.resize(currentWidth, currentHeight);
29013
- }
29014
29890
  }
29015
29891
  if (this._private__ganttOptions.onRemove) {
29016
29892
  this._private__ganttOptions.onRemove();
@@ -29021,6 +29897,8 @@
29021
29897
  return new GanttTool(context, options);
29022
29898
  }
29023
29899
 
29900
+ /** Undo steps kept, each a full snapshot of every drawing. */
29901
+ const UNDO_HISTORY_LIMIT = 100;
29024
29902
  /**
29025
29903
  * Central manager for all drawing tools - TradingView style
29026
29904
  * Handles multiple tools, selection, deletion, and keyboard shortcuts
@@ -29043,6 +29921,8 @@
29043
29921
  this._private__pointerDownHandler = null;
29044
29922
  this._private__pointerUpHandler = null;
29045
29923
  this._private__pointerCancelHandler = null;
29924
+ this._private__lostCaptureHandler = null;
29925
+ this._private__windowPointerUpHandler = null;
29046
29926
  this._private__pointerId = null;
29047
29927
  /** A press that grabbed a tool already selected it; its click must not undo that. */
29048
29928
  this._private__gestureHandled = false;
@@ -29052,6 +29932,9 @@
29052
29932
  this._private__selectionChanged = new Delegate();
29053
29933
  this._private__isDragging = false;
29054
29934
  this._private__draggedToolId = null;
29935
+ this._private__pendingToolIds = new Set();
29936
+ this._private__completionCheck = null;
29937
+ this._private__drawingCompleted = new Delegate();
29055
29938
  this._private__chart = chart;
29056
29939
  this._private__series = series;
29057
29940
  this._private__options = {
@@ -29090,9 +29973,63 @@
29090
29973
  addTool(tool, id) {
29091
29974
  const toolId = id ?? tool.id;
29092
29975
  this._private__tools.set(toolId, tool);
29976
+ if (!tool.isComplete()) {
29977
+ this._private__pendingToolIds.add(toolId);
29978
+ }
29093
29979
  this.recordChange();
29094
29980
  return toolId;
29095
29981
  }
29982
+ hasPendingDrawing() {
29983
+ return this._private__pendingToolIds.size > 0;
29984
+ }
29985
+ /** Removes every tool still waiting for points; returns how many were dropped. */
29986
+ cancelPendingDrawings() {
29987
+ if (this._private__pendingToolIds.size === 0) {
29988
+ return 0;
29989
+ }
29990
+ let cancelled = 0;
29991
+ for (const id of Array.from(this._private__pendingToolIds)) {
29992
+ const tool = this._private__tools.get(id);
29993
+ if (tool) {
29994
+ if (this._private__selectedToolId === id) {
29995
+ this._private__applySelection(null);
29996
+ }
29997
+ tool.remove();
29998
+ this._private__tools.delete(id);
29999
+ cancelled++;
30000
+ }
30001
+ }
30002
+ this._private__pendingToolIds.clear();
30003
+ if (cancelled > 0) {
30004
+ this.recordChange();
30005
+ }
30006
+ return cancelled;
30007
+ }
30008
+ subscribeDrawingCompleted(handler) {
30009
+ this._private__drawingCompleted._internal_subscribe(handler);
30010
+ }
30011
+ unsubscribeDrawingCompleted(handler) {
30012
+ this._private__drawingCompleted._internal_unsubscribe(handler);
30013
+ }
30014
+ _private__scheduleCompletionCheck() {
30015
+ if (this._private__pendingToolIds.size === 0 || this._private__completionCheck !== null) {
30016
+ return;
30017
+ }
30018
+ this._private__completionCheck = setTimeout(() => {
30019
+ this._private__completionCheck = null;
30020
+ for (const id of Array.from(this._private__pendingToolIds)) {
30021
+ const tool = this._private__tools.get(id);
30022
+ if (!tool) {
30023
+ this._private__pendingToolIds.delete(id);
30024
+ continue;
30025
+ }
30026
+ if (tool.isComplete()) {
30027
+ this._private__pendingToolIds.delete(id);
30028
+ this._private__drawingCompleted._internal_fire(id);
30029
+ }
30030
+ }
30031
+ }, 0);
30032
+ }
29096
30033
  /**
29097
30034
  * Remove a tool by ID
29098
30035
  */
@@ -29104,6 +30041,7 @@
29104
30041
  }
29105
30042
  tool.remove();
29106
30043
  this._private__tools.delete(id);
30044
+ this._private__pendingToolIds.delete(id);
29107
30045
  this.recordChange();
29108
30046
  return true;
29109
30047
  }
@@ -29150,6 +30088,9 @@
29150
30088
  return;
29151
30089
  }
29152
30090
  this._private__past.push(this._private__committed);
30091
+ if (this._private__past.length > UNDO_HISTORY_LIMIT) {
30092
+ this._private__past.shift();
30093
+ }
29153
30094
  this._private__future = [];
29154
30095
  this._private__committed = this.serializeAll();
29155
30096
  }
@@ -29303,6 +30244,7 @@
29303
30244
  this._private__applySelection(null);
29304
30245
  this._private__tools.forEach((tool) => tool.remove());
29305
30246
  this._private__tools.clear();
30247
+ this._private__pendingToolIds.clear();
29306
30248
  this.recordChange();
29307
30249
  }
29308
30250
  /**
@@ -29523,8 +30465,17 @@
29523
30465
  * Destroy the manager and cleanup
29524
30466
  */
29525
30467
  destroy() {
30468
+ if (this._private__completionCheck !== null) {
30469
+ clearTimeout(this._private__completionCheck);
30470
+ this._private__completionCheck = null;
30471
+ }
30472
+ this._private__drawingCompleted._internal_destroy();
30473
+ this._private__selectionChanged._internal_destroy();
29526
30474
  this.clearAll();
29527
30475
  this._private__detachEventHandlers();
30476
+ this._private__past = [];
30477
+ this._private__future = [];
30478
+ this._private__committed = [];
29528
30479
  }
29529
30480
  _private__attachEventHandlers() {
29530
30481
  // Click handler for selection
@@ -29542,11 +30493,17 @@
29542
30493
  const chartElement = this._private__chart.chartElement();
29543
30494
  if (chartElement) {
29544
30495
  this._private__pointerDownHandler = (event) => this._private__handlePointerDown(event);
29545
- this._private__pointerUpHandler = (event) => this._private__handlePointerUp();
29546
- this._private__pointerCancelHandler = (event) => this._private__handlePointerUp();
30496
+ this._private__pointerUpHandler = (event) => this._private__handlePointerUp(event);
30497
+ this._private__pointerCancelHandler = (event) => this._private__handlePointerUp(event);
30498
+ this._private__lostCaptureHandler = (event) => this._private__handleLostPointerCapture(event);
29547
30499
  chartElement.addEventListener('pointerdown', this._private__pointerDownHandler);
29548
30500
  chartElement.addEventListener('pointerup', this._private__pointerUpHandler);
29549
30501
  chartElement.addEventListener('pointercancel', this._private__pointerCancelHandler);
30502
+ chartElement.addEventListener('lostpointercapture', this._private__lostCaptureHandler);
30503
+ }
30504
+ if (isRunningOnClientSide) {
30505
+ this._private__windowPointerUpHandler = (event) => this._private__handlePointerUp(event);
30506
+ window.addEventListener('pointerup', this._private__windowPointerUpHandler);
29550
30507
  }
29551
30508
  // Keyboard handler for shortcuts
29552
30509
  if (this._private__options.enableKeyboardShortcuts) {
@@ -29579,6 +30536,14 @@
29579
30536
  chartElement.removeEventListener('pointercancel', this._private__pointerCancelHandler);
29580
30537
  this._private__pointerCancelHandler = null;
29581
30538
  }
30539
+ if (this._private__lostCaptureHandler) {
30540
+ chartElement.removeEventListener('lostpointercapture', this._private__lostCaptureHandler);
30541
+ this._private__lostCaptureHandler = null;
30542
+ }
30543
+ }
30544
+ if (this._private__windowPointerUpHandler) {
30545
+ window.removeEventListener('pointerup', this._private__windowPointerUpHandler);
30546
+ this._private__windowPointerUpHandler = null;
29582
30547
  }
29583
30548
  if (this._private__keyboardHandler) {
29584
30549
  document.removeEventListener('keydown', this._private__keyboardHandler);
@@ -29586,6 +30551,7 @@
29586
30551
  }
29587
30552
  }
29588
30553
  _private__handleClick(param) {
30554
+ this._private__scheduleCompletionCheck();
29589
30555
  if (!param.point) {
29590
30556
  return;
29591
30557
  }
@@ -29665,6 +30631,9 @@
29665
30631
  return null;
29666
30632
  }
29667
30633
  _private__handlePointerDown(event) {
30634
+ if (this._private__isDragging) {
30635
+ return;
30636
+ }
29668
30637
  // Cleared for every press, then raised only if this one grabs a drawing
29669
30638
  setInputClaim(this._private__chart, false);
29670
30639
  const chartElement = this._private__chart.chartElement();
@@ -29700,28 +30669,51 @@
29700
30669
  this._private__gestureHandled = true;
29701
30670
  this._private__draggedToolId = toolId;
29702
30671
  this._private__pointerId = event.pointerId;
29703
- chartElement.setPointerCapture(event.pointerId);
30672
+ try {
30673
+ chartElement.setPointerCapture(event.pointerId);
30674
+ }
30675
+ catch (error) {
30676
+ this._private__pointerId = null;
30677
+ }
29704
30678
  tool.startDrag(endpoint, anchor);
29705
30679
  // Panning is stopped by disabling it for the gesture, not by preventing
29706
30680
  // the event: preventDefault on pointerdown suppresses the compatibility
29707
30681
  // mouse events, and those are what drive the crosshair the drag reads.
29708
30682
  this._private__disableChartInteractions();
29709
30683
  }
29710
- _private__handlePointerUp() {
30684
+ _private__handlePointerUp(event) {
29711
30685
  if (!this._private__isDragging || this._private__draggedToolId === null) {
29712
30686
  return;
29713
30687
  }
29714
- const tool = this._private__tools.get(this._private__draggedToolId);
29715
- if (tool) {
29716
- tool.stopDrag();
30688
+ if (event && this._private__pointerId !== null && event.pointerId !== this._private__pointerId) {
30689
+ return;
29717
30690
  }
29718
- this._private__releasePointer();
29719
- this._private__enableChartInteractions();
30691
+ const draggedToolId = this._private__draggedToolId;
30692
+ // Cleared first: anything below can throw, and a half-finished release
30693
+ // would leave the manager dragging forever.
29720
30694
  this._private__isDragging = false;
29721
30695
  this._private__draggedToolId = null;
30696
+ try {
30697
+ const tool = this._private__tools.get(draggedToolId);
30698
+ if (tool) {
30699
+ tool.stopDrag();
30700
+ }
30701
+ }
30702
+ finally {
30703
+ this._private__releasePointer();
30704
+ this._private__enableChartInteractions();
30705
+ }
29722
30706
  // A finished drag is one step, not one per frame
29723
30707
  this.recordChange();
29724
30708
  }
30709
+ /** Ends the drag when capture is lost without a pointerup ever arriving. */
30710
+ _private__handleLostPointerCapture(event) {
30711
+ if (this._private__pointerId !== null && event.pointerId !== this._private__pointerId) {
30712
+ return;
30713
+ }
30714
+ this._private__pointerId = null;
30715
+ this._private__handlePointerUp();
30716
+ }
29725
30717
  /**
29726
30718
  * Pane element of a tool's series. The engine asserts the series has a pane,
29727
30719
  * so the lookup is guarded.
@@ -29768,10 +30760,15 @@
29768
30760
  this._private__pointerId = null;
29769
30761
  }
29770
30762
  _private__disableChartInteractions() {
30763
+ if (this._private__interactionsBeforeDrag !== null) {
30764
+ return;
30765
+ }
29771
30766
  const options = this._private__chart.options();
30767
+ // `options()` returns the live object and `applyOptions` merges into it
30768
+ // in place, so a snapshot by reference restores all-false.
29772
30769
  this._private__interactionsBeforeDrag = {
29773
- _internal_handleScroll: options.handleScroll,
29774
- _internal_handleScale: options.handleScale,
30770
+ _internal_handleScroll: clone(options.handleScroll),
30771
+ _internal_handleScale: clone(options.handleScale),
29775
30772
  };
29776
30773
  this._private__chart.applyOptions({
29777
30774
  handleScroll: false,
@@ -29792,6 +30789,14 @@
29792
30789
  });
29793
30790
  }
29794
30791
  _private__handleKeyboard(event) {
30792
+ const target = event.target;
30793
+ const typing = !!target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable);
30794
+ if (typing) {
30795
+ return;
30796
+ }
30797
+ if (event.key === 'Enter') {
30798
+ this._private__scheduleCompletionCheck();
30799
+ }
29795
30800
  // Delete or Backspace to remove selected tool
29796
30801
  if (event.key === 'Delete' || event.key === 'Backspace') {
29797
30802
  if (this.deleteSelected()) {
@@ -29812,8 +30817,8 @@
29812
30817
  }
29813
30818
  return;
29814
30819
  }
29815
- // Escape to deselect
29816
30820
  if (event.key === 'Escape') {
30821
+ this.cancelPendingDrawings();
29817
30822
  this._private__handlePointerUp();
29818
30823
  this._private__deselectAll();
29819
30824
  this.stopDrawing();
@@ -29849,7 +30854,7 @@
29849
30854
  * Returns the current version as a string. For example `'1.0.0'`.
29850
30855
  */
29851
30856
  function version() {
29852
- return "2.2.0";
30857
+ return "2.3.0-dev+202609220108";
29853
30858
  }
29854
30859
 
29855
30860
  var PipsendChartsModule = /*#__PURE__*/Object.freeze({
@@ -29885,6 +30890,7 @@
29885
30890
  PitchforkTool: PitchforkTool,
29886
30891
  PolygonTool: PolygonTool,
29887
30892
  PositionCalculator: PositionCalculator,
30893
+ PositionLine: PositionLine,
29888
30894
  PositionMarkersManager: PositionMarkersManager,
29889
30895
  PositionTool: PositionTool,
29890
30896
  get PriceLineSource () { return PriceLineSource; },
@@ -29942,6 +30948,7 @@
29942
30948
  createParallelChannelTool: createParallelChannelTool,
29943
30949
  createPitchforkTool: createPitchforkTool,
29944
30950
  createPolygonTool: createPolygonTool,
30951
+ createPositionLine: createPositionLine,
29945
30952
  createPositionMarkers: createPositionMarkers,
29946
30953
  createPositionTool: createPositionTool,
29947
30954
  createRectangleTool: createRectangleTool,