@pipsend/charts 2.2.1 → 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.1
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',
@@ -21130,13 +22054,6 @@
21130
22054
  // Clean up references
21131
22055
  this._private__primitive = null;
21132
22056
  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
22057
  }
21141
22058
  if (this._private__trendOptions.onRemove) {
21142
22059
  this._private__trendOptions.onRemove();
@@ -21409,12 +22326,6 @@
21409
22326
  this._series.detachPrimitive(this._private__primitive);
21410
22327
  this._private__primitive = null;
21411
22328
  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
22329
  }
21419
22330
  if (this._private__hLineOptions.onRemove) {
21420
22331
  this._private__hLineOptions.onRemove();
@@ -23253,12 +24164,6 @@
23253
24164
  this._series.detachPrimitive(this._private__primitive);
23254
24165
  this._private__primitive = null;
23255
24166
  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
24167
  }
23263
24168
  if (this._private__vLineOptions.onRemove) {
23264
24169
  this._private__vLineOptions.onRemove();
@@ -23911,12 +24816,6 @@
23911
24816
  this._series.detachPrimitive(this._private__primitive);
23912
24817
  this._private__primitive = null;
23913
24818
  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
24819
  }
23921
24820
  if (this._private__drawingManager) {
23922
24821
  this._private__drawingManager._internal_destroy();
@@ -23932,12 +24831,6 @@
23932
24831
  this._series.detachPrimitive(this._private__primitive);
23933
24832
  this._private__primitive = null;
23934
24833
  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
24834
  }
23942
24835
  if (this._private__fibOptions.onRemove) {
23943
24836
  this._private__fibOptions.onRemove();
@@ -24365,12 +25258,6 @@
24365
25258
  this._series.detachPrimitive(this._private__primitive);
24366
25259
  this._private__primitive = null;
24367
25260
  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
25261
  }
24375
25262
  }
24376
25263
  }
@@ -24812,12 +25699,6 @@
24812
25699
  this._series.detachPrimitive(this._private__primitive);
24813
25700
  this._private__primitive = null;
24814
25701
  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
25702
  }
24822
25703
  if (this._private__fanOptions.onRemove) {
24823
25704
  this._private__fanOptions.onRemove();
@@ -25358,12 +26239,6 @@
25358
26239
  this._series.detachPrimitive(this._private__primitive);
25359
26240
  this._private__primitive = null;
25360
26241
  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
26242
  }
25368
26243
  if (this._private__rectOptions.onRemove) {
25369
26244
  this._private__rectOptions.onRemove();
@@ -25824,12 +26699,6 @@
25824
26699
  this._series.detachPrimitive(this._private__primitive);
25825
26700
  this._private__primitive = null;
25826
26701
  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
26702
  }
25834
26703
  if (this._private__circleOptions.onRemove) {
25835
26704
  this._private__circleOptions.onRemove();
@@ -26379,12 +27248,6 @@
26379
27248
  this._series.detachPrimitive(this._private__primitive);
26380
27249
  this._private__primitive = null;
26381
27250
  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
27251
  }
26389
27252
  if (this._private__arrowOptions.onRemove) {
26390
27253
  this._private__arrowOptions.onRemove();
@@ -27245,12 +28108,6 @@
27245
28108
  this._series.detachPrimitive(this._private__primitive);
27246
28109
  this._private__primitive = null;
27247
28110
  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
28111
  }
27255
28112
  if (this._private__textOptions.onRemove) {
27256
28113
  this._private__textOptions.onRemove();
@@ -27833,12 +28690,6 @@
27833
28690
  this._series.detachPrimitive(this._private__primitive);
27834
28691
  this._private__primitive = null;
27835
28692
  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
28693
  }
27843
28694
  if (this._private__drawingManager) {
27844
28695
  this._private__drawingManager._internal_destroy();
@@ -28694,12 +29545,6 @@
28694
29545
  this._series.detachPrimitive(this._private__primitive);
28695
29546
  this._private__primitive = null;
28696
29547
  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
29548
  }
28704
29549
  if (this._private__positionOptions.onRemove) {
28705
29550
  this._private__positionOptions.onRemove();
@@ -29042,12 +29887,6 @@
29042
29887
  this._series.detachPrimitive(this._private__primitive);
29043
29888
  this._private__primitive = null;
29044
29889
  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
29890
  }
29052
29891
  if (this._private__ganttOptions.onRemove) {
29053
29892
  this._private__ganttOptions.onRemove();
@@ -29058,6 +29897,8 @@
29058
29897
  return new GanttTool(context, options);
29059
29898
  }
29060
29899
 
29900
+ /** Undo steps kept, each a full snapshot of every drawing. */
29901
+ const UNDO_HISTORY_LIMIT = 100;
29061
29902
  /**
29062
29903
  * Central manager for all drawing tools - TradingView style
29063
29904
  * Handles multiple tools, selection, deletion, and keyboard shortcuts
@@ -29080,6 +29921,8 @@
29080
29921
  this._private__pointerDownHandler = null;
29081
29922
  this._private__pointerUpHandler = null;
29082
29923
  this._private__pointerCancelHandler = null;
29924
+ this._private__lostCaptureHandler = null;
29925
+ this._private__windowPointerUpHandler = null;
29083
29926
  this._private__pointerId = null;
29084
29927
  /** A press that grabbed a tool already selected it; its click must not undo that. */
29085
29928
  this._private__gestureHandled = false;
@@ -29245,6 +30088,9 @@
29245
30088
  return;
29246
30089
  }
29247
30090
  this._private__past.push(this._private__committed);
30091
+ if (this._private__past.length > UNDO_HISTORY_LIMIT) {
30092
+ this._private__past.shift();
30093
+ }
29248
30094
  this._private__future = [];
29249
30095
  this._private__committed = this.serializeAll();
29250
30096
  }
@@ -29624,8 +30470,12 @@
29624
30470
  this._private__completionCheck = null;
29625
30471
  }
29626
30472
  this._private__drawingCompleted._internal_destroy();
30473
+ this._private__selectionChanged._internal_destroy();
29627
30474
  this.clearAll();
29628
30475
  this._private__detachEventHandlers();
30476
+ this._private__past = [];
30477
+ this._private__future = [];
30478
+ this._private__committed = [];
29629
30479
  }
29630
30480
  _private__attachEventHandlers() {
29631
30481
  // Click handler for selection
@@ -29645,9 +30495,15 @@
29645
30495
  this._private__pointerDownHandler = (event) => this._private__handlePointerDown(event);
29646
30496
  this._private__pointerUpHandler = (event) => this._private__handlePointerUp(event);
29647
30497
  this._private__pointerCancelHandler = (event) => this._private__handlePointerUp(event);
30498
+ this._private__lostCaptureHandler = (event) => this._private__handleLostPointerCapture(event);
29648
30499
  chartElement.addEventListener('pointerdown', this._private__pointerDownHandler);
29649
30500
  chartElement.addEventListener('pointerup', this._private__pointerUpHandler);
29650
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);
29651
30507
  }
29652
30508
  // Keyboard handler for shortcuts
29653
30509
  if (this._private__options.enableKeyboardShortcuts) {
@@ -29680,6 +30536,14 @@
29680
30536
  chartElement.removeEventListener('pointercancel', this._private__pointerCancelHandler);
29681
30537
  this._private__pointerCancelHandler = null;
29682
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;
29683
30547
  }
29684
30548
  if (this._private__keyboardHandler) {
29685
30549
  document.removeEventListener('keydown', this._private__keyboardHandler);
@@ -29805,7 +30669,12 @@
29805
30669
  this._private__gestureHandled = true;
29806
30670
  this._private__draggedToolId = toolId;
29807
30671
  this._private__pointerId = event.pointerId;
29808
- chartElement.setPointerCapture(event.pointerId);
30672
+ try {
30673
+ chartElement.setPointerCapture(event.pointerId);
30674
+ }
30675
+ catch (error) {
30676
+ this._private__pointerId = null;
30677
+ }
29809
30678
  tool.startDrag(endpoint, anchor);
29810
30679
  // Panning is stopped by disabling it for the gesture, not by preventing
29811
30680
  // the event: preventDefault on pointerdown suppresses the compatibility
@@ -29819,17 +30688,32 @@
29819
30688
  if (event && this._private__pointerId !== null && event.pointerId !== this._private__pointerId) {
29820
30689
  return;
29821
30690
  }
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();
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.
29828
30694
  this._private__isDragging = false;
29829
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
+ }
29830
30706
  // A finished drag is one step, not one per frame
29831
30707
  this.recordChange();
29832
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
+ }
29833
30717
  /**
29834
30718
  * Pane element of a tool's series. The engine asserts the series has a pane,
29835
30719
  * so the lookup is guarded.
@@ -29880,9 +30764,11 @@
29880
30764
  return;
29881
30765
  }
29882
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.
29883
30769
  this._private__interactionsBeforeDrag = {
29884
- _internal_handleScroll: options.handleScroll,
29885
- _internal_handleScale: options.handleScale,
30770
+ _internal_handleScroll: clone(options.handleScroll),
30771
+ _internal_handleScale: clone(options.handleScale),
29886
30772
  };
29887
30773
  this._private__chart.applyOptions({
29888
30774
  handleScroll: false,
@@ -29968,7 +30854,7 @@
29968
30854
  * Returns the current version as a string. For example `'1.0.0'`.
29969
30855
  */
29970
30856
  function version() {
29971
- return "2.2.1";
30857
+ return "2.3.0-dev+202609220108";
29972
30858
  }
29973
30859
 
29974
30860
  var PipsendChartsModule = /*#__PURE__*/Object.freeze({
@@ -30004,6 +30890,7 @@
30004
30890
  PitchforkTool: PitchforkTool,
30005
30891
  PolygonTool: PolygonTool,
30006
30892
  PositionCalculator: PositionCalculator,
30893
+ PositionLine: PositionLine,
30007
30894
  PositionMarkersManager: PositionMarkersManager,
30008
30895
  PositionTool: PositionTool,
30009
30896
  get PriceLineSource () { return PriceLineSource; },
@@ -30061,6 +30948,7 @@
30061
30948
  createParallelChannelTool: createParallelChannelTool,
30062
30949
  createPitchforkTool: createPitchforkTool,
30063
30950
  createPolygonTool: createPolygonTool,
30951
+ createPositionLine: createPositionLine,
30064
30952
  createPositionMarkers: createPositionMarkers,
30065
30953
  createPositionTool: createPositionTool,
30066
30954
  createRectangleTool: createRectangleTool,