@pipsend/charts 2.2.1 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * Pipsend Charts v2.2.1
3
+ * Pipsend Charts v2.3.1
4
4
  * Copyright (c) 2026 Pipsend
5
5
  * Licensed under MIT License
6
6
  * Built on TradingView Lightweight Charts™ (Apache 2.0)
@@ -14245,6 +14245,8 @@ function shapeSize(shape, originalSize) {
14245
14245
  return size(originalSize, 0.8);
14246
14246
  case 'square':
14247
14247
  return size(originalSize, 0.7);
14248
+ case 'tag':
14249
+ return size(originalSize, 1);
14248
14250
  }
14249
14251
  }
14250
14252
  function calculateShapeHeight(barSpacing) {
@@ -14354,6 +14356,85 @@ function hitTestSquare(centerX, centerY, size, x, y) {
14354
14356
  y >= top && y <= top + squareSize;
14355
14357
  }
14356
14358
 
14359
+ const TAIL_HALF_WIDTH_RATIO = 0.62;
14360
+ /**
14361
+ * Lays the pointer into the edge the path is already travelling along, so the
14362
+ * bubble and its tail are a single outline. Two subpaths would leave a stroked
14363
+ * seam where they meet.
14364
+ */
14365
+ // eslint-disable-next-line max-params
14366
+ function tailPath(ctx, xCenter, edgeY, tailHeight, halfWidth, up) {
14367
+ // A tip with a small radius reads as drawn rather than as an artefact of the
14368
+ // rasteriser, which a bare triangle does at these sizes.
14369
+ const tipRadius = Math.min(halfWidth / 3, tailHeight / 3);
14370
+ const sign = up ? -1 : 1;
14371
+ const tipY = edgeY + sign * tailHeight;
14372
+ const shoulderY = tipY - sign * tipRadius;
14373
+ // The top edge is travelled left to right, the bottom edge right to left.
14374
+ const entry = up ? -halfWidth : halfWidth;
14375
+ ctx.lineTo(xCenter + entry, edgeY);
14376
+ ctx.lineTo(xCenter + (up ? -tipRadius : tipRadius), shoulderY);
14377
+ ctx.quadraticCurveTo(xCenter, tipY, xCenter + (up ? tipRadius : -tipRadius), shoulderY);
14378
+ ctx.lineTo(xCenter - entry, edgeY);
14379
+ }
14380
+ /**
14381
+ * Draws a rounded speech-bubble tag with an optional pointer towards its anchor.
14382
+ */
14383
+ function drawTag(ctx, params) {
14384
+ const hpr = params._internal_horizontalPixelRatio;
14385
+ const vpr = params._internal_verticalPixelRatio;
14386
+ const left = Math.round((params._internal_x - params._internal_width / 2) * hpr);
14387
+ const right = Math.round((params._internal_x + params._internal_width / 2) * hpr);
14388
+ const top = Math.round((params._internal_centerY - params._internal_height / 2) * vpr);
14389
+ const bottom = Math.round((params._internal_centerY + params._internal_height / 2) * vpr);
14390
+ const xCenter = Math.round(params._internal_x * hpr);
14391
+ const radius = Math.min(params._internal_radius * hpr, (right - left) / 2, (bottom - top) / 2);
14392
+ const tailHeight = params._internal_tailHeight * vpr;
14393
+ const tailHalfWidth = Math.min(tailHeight * TAIL_HALF_WIDTH_RATIO, Math.max(0, (right - left) / 2 - radius));
14394
+ const hasTail = params._internal_tailDirection !== 'none' && tailHalfWidth > 0 && tailHeight > 0;
14395
+ ctx.save();
14396
+ ctx.beginPath();
14397
+ if (!hasTail) {
14398
+ drawRoundRect(ctx, left, top, right - left, bottom - top, [radius, radius, radius, radius]);
14399
+ }
14400
+ else {
14401
+ ctx.moveTo(left + radius, top);
14402
+ if (params._internal_tailDirection === 'up') {
14403
+ tailPath(ctx, xCenter, top, tailHeight, tailHalfWidth, true);
14404
+ }
14405
+ ctx.lineTo(right - radius, top);
14406
+ ctx.arcTo(right, top, right, top + radius, radius);
14407
+ ctx.lineTo(right, bottom - radius);
14408
+ ctx.arcTo(right, bottom, right - radius, bottom, radius);
14409
+ if (params._internal_tailDirection === 'down') {
14410
+ tailPath(ctx, xCenter, bottom, tailHeight, tailHalfWidth, false);
14411
+ }
14412
+ ctx.lineTo(left + radius, bottom);
14413
+ ctx.arcTo(left, bottom, left, bottom - radius, radius);
14414
+ ctx.lineTo(left, top + radius);
14415
+ ctx.arcTo(left, top, left + radius, top, radius);
14416
+ }
14417
+ ctx.closePath();
14418
+ ctx.fillStyle = params._internal_fillColor;
14419
+ ctx.fill();
14420
+ if (params._internal_borderColor !== undefined && params._internal_borderWidth > 0) {
14421
+ ctx.lineWidth = Math.max(1, Math.round(params._internal_borderWidth * hpr));
14422
+ ctx.strokeStyle = params._internal_borderColor;
14423
+ ctx.stroke();
14424
+ }
14425
+ ctx.restore();
14426
+ }
14427
+ function hitTestTag(centerX, centerY, tag, x, y) {
14428
+ if (tag._internal_width <= 0) {
14429
+ return false;
14430
+ }
14431
+ const halfHeight = tag._internal_height / 2;
14432
+ const top = tag._internal_tailDirection === 'up' ? centerY - halfHeight - tag._internal_tailHeight : centerY - halfHeight;
14433
+ const bottom = tag._internal_tailDirection === 'down' ? centerY + halfHeight + tag._internal_tailHeight : centerY + halfHeight;
14434
+ const left = centerX - tag._internal_width / 2;
14435
+ return x >= left && x <= left + tag._internal_width && y >= top && y <= bottom;
14436
+ }
14437
+
14357
14438
  function drawText(ctx, text, x, y, horizontalPixelRatio, verticalPixelRatio) {
14358
14439
  ctx.save();
14359
14440
  ctx.scale(horizontalPixelRatio, verticalPixelRatio);
@@ -14370,9 +14451,11 @@ class SeriesMarkersRenderer {
14370
14451
  constructor() {
14371
14452
  this._private__data = null;
14372
14453
  this._private__textWidthCache = new TextWidthCache();
14454
+ this._private__tagTextWidthCache = new TextWidthCache();
14373
14455
  this._private__fontSize = -1;
14374
14456
  this._private__fontFamily = '';
14375
14457
  this._private__font = '';
14458
+ this._private__tagFont = '';
14376
14459
  this._private__zOrder = 'normal';
14377
14460
  }
14378
14461
  _internal_setData(data) {
@@ -14383,7 +14466,11 @@ class SeriesMarkersRenderer {
14383
14466
  this._private__fontSize = fontSize;
14384
14467
  this._private__fontFamily = fontFamily;
14385
14468
  this._private__font = makeFont(fontSize, fontFamily);
14469
+ // Tags carry a label rather than an annotation, so they get the heavier
14470
+ // weight — and their own cache, since widths differ per font.
14471
+ this._private__tagFont = makeFont(fontSize, fontFamily, '600');
14386
14472
  this._private__textWidthCache._internal_reset();
14473
+ this._private__tagTextWidthCache._internal_reset();
14387
14474
  }
14388
14475
  this._private__zOrder = zOrder;
14389
14476
  }
@@ -14423,14 +14510,21 @@ class SeriesMarkersRenderer {
14423
14510
  return;
14424
14511
  }
14425
14512
  ctx.textBaseline = 'middle';
14426
- ctx.font = this._private__font;
14427
14513
  for (let index = this._private__data._internal_visibleRange.from; index < this._private__data._internal_visibleRange.to; index++) {
14428
14514
  const item = this._private__data._internal_items[index];
14515
+ const isTag = item._internal_tag !== undefined;
14516
+ ctx.font = isTag ? this._private__tagFont : this._private__font;
14429
14517
  if (item._internal_text !== undefined) {
14430
- item._internal_text._internal_width = this._private__textWidthCache._internal_measureText(ctx, item._internal_text._internal_content);
14518
+ const cache = isTag ? this._private__tagTextWidthCache : this._private__textWidthCache;
14519
+ item._internal_text._internal_width = cache._internal_measureText(ctx, item._internal_text._internal_content);
14431
14520
  item._internal_text._internal_height = this._private__fontSize;
14432
14521
  item._internal_text._internal_x = item._internal_x - item._internal_text._internal_width / 2;
14433
14522
  }
14523
+ if (item._internal_tag !== undefined) {
14524
+ const textWidth = item._internal_text !== undefined ? item._internal_text._internal_width : 0;
14525
+ item._internal_tag._internal_width = Math.max(textWidth + item._internal_tag._internal_paddingX * 2, item._internal_tag._internal_height);
14526
+ item._internal_tag._internal_left = item._internal_x - item._internal_tag._internal_width / 2;
14527
+ }
14434
14528
  drawItem(item, ctx, horizontalPixelRatio, verticalPixelRatio);
14435
14529
  }
14436
14530
  }
@@ -14445,12 +14539,40 @@ function bitmapShapeItemCoordinates(item, horizontalPixelRatio, verticalPixelRat
14445
14539
  };
14446
14540
  }
14447
14541
  function drawItem(item, ctx, horizontalPixelRatio, verticalPixelRatio) {
14542
+ if (item._internal_tag !== undefined) {
14543
+ drawTagItem(item, item._internal_tag, ctx, horizontalPixelRatio, verticalPixelRatio);
14544
+ return;
14545
+ }
14448
14546
  ctx.fillStyle = item._internal_color;
14449
14547
  if (item._internal_text !== undefined) {
14450
14548
  drawText(ctx, item._internal_text._internal_content, item._internal_text._internal_x, item._internal_text._internal_y, horizontalPixelRatio, verticalPixelRatio);
14451
14549
  }
14452
14550
  drawShape(item, ctx, bitmapShapeItemCoordinates(item, horizontalPixelRatio, verticalPixelRatio));
14453
14551
  }
14552
+ // eslint-disable-next-line max-params
14553
+ function drawTagItem(item, tag, ctx, horizontalPixelRatio, verticalPixelRatio) {
14554
+ if (item._internal_size === 0) {
14555
+ return;
14556
+ }
14557
+ drawTag(ctx, {
14558
+ _internal_x: item._internal_x,
14559
+ _internal_centerY: item._internal_y,
14560
+ _internal_width: tag._internal_width,
14561
+ _internal_height: tag._internal_height,
14562
+ _internal_tailHeight: tag._internal_tailHeight,
14563
+ _internal_tailDirection: tag._internal_tailDirection,
14564
+ _internal_radius: tag._internal_radius,
14565
+ _internal_fillColor: item._internal_color,
14566
+ _internal_borderColor: item._internal_borderColor,
14567
+ _internal_borderWidth: item._internal_borderWidth ?? 1,
14568
+ _internal_horizontalPixelRatio: horizontalPixelRatio,
14569
+ _internal_verticalPixelRatio: verticalPixelRatio,
14570
+ });
14571
+ if (item._internal_text !== undefined) {
14572
+ ctx.fillStyle = item._internal_textColor ?? '#FFFFFF';
14573
+ drawText(ctx, item._internal_text._internal_content, item._internal_text._internal_x, item._internal_text._internal_y, horizontalPixelRatio, verticalPixelRatio);
14574
+ }
14575
+ }
14454
14576
  function drawShape(item, ctx, coordinates) {
14455
14577
  if (item._internal_size === 0) {
14456
14578
  return;
@@ -14468,10 +14590,16 @@ function drawShape(item, ctx, coordinates) {
14468
14590
  case 'square':
14469
14591
  drawSquare(ctx, coordinates, item._internal_size);
14470
14592
  return;
14593
+ case 'tag':
14594
+ // Handled by drawTagItem, which needs both pixel ratios and the text.
14595
+ return;
14471
14596
  }
14472
14597
  ensureNever(item._internal_shape);
14473
14598
  }
14474
14599
  function hitTestItem(item, x, y) {
14600
+ if (item._internal_tag !== undefined) {
14601
+ return item._internal_size !== 0 && hitTestTag(item._internal_x, item._internal_y, item._internal_tag, x, y);
14602
+ }
14475
14603
  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)) {
14476
14604
  return true;
14477
14605
  }
@@ -14490,6 +14618,8 @@ function hitTestShape(item, x, y) {
14490
14618
  return hitTestCircle(item._internal_x, item._internal_y, item._internal_size, x, y);
14491
14619
  case 'square':
14492
14620
  return hitTestSquare(item._internal_x, item._internal_y, item._internal_size, x, y);
14621
+ case 'tag':
14622
+ return false;
14493
14623
  }
14494
14624
  }
14495
14625
 
@@ -14522,15 +14652,71 @@ function getPrice(seriesData, marker, isInverted) {
14522
14652
  }
14523
14653
  return;
14524
14654
  }
14655
+ /**
14656
+ * Lays out a `tag` marker: the bubble carries the text, so it is measured from
14657
+ * the font rather than from the bar spacing, and its pointer is aimed back at
14658
+ * the anchor price.
14659
+ */
14660
+ // eslint-disable-next-line max-params
14661
+ function fillTagSizeAndY(rendererItem, marker, anchorY, offsets, textHeight, shapeMargin, sizeMultiplier) {
14662
+ const height = Math.round((textHeight + 5 /* Constants.TagPaddingY */ * 2) * sizeMultiplier);
14663
+ const tailHeight = Math.round(7 /* Constants.TagTailHeight */ * sizeMultiplier);
14664
+ const ignoreOffset = isPriceMarker(marker.position);
14665
+ const halfHeight = height / 2;
14666
+ let tailDirection = 'none';
14667
+ let centerY = anchorY;
14668
+ switch (marker.position) {
14669
+ case 'aboveBar':
14670
+ case 'atPriceTop': {
14671
+ const offset = ignoreOffset ? 0 : offsets._internal_aboveBar;
14672
+ tailDirection = 'down';
14673
+ centerY = (anchorY - offset - tailHeight - halfHeight);
14674
+ if (!ignoreOffset) {
14675
+ offsets._internal_aboveBar += height + tailHeight + shapeMargin;
14676
+ }
14677
+ break;
14678
+ }
14679
+ case 'belowBar':
14680
+ case 'atPriceBottom': {
14681
+ const offset = ignoreOffset ? 0 : offsets._internal_belowBar;
14682
+ tailDirection = 'up';
14683
+ centerY = (anchorY + offset + tailHeight + halfHeight);
14684
+ if (!ignoreOffset) {
14685
+ offsets._internal_belowBar += height + tailHeight + shapeMargin;
14686
+ }
14687
+ break;
14688
+ }
14689
+ }
14690
+ const tag = {
14691
+ _internal_height: height,
14692
+ _internal_tailHeight: tailHeight,
14693
+ _internal_tailDirection: tailDirection,
14694
+ _internal_radius: marker.borderRadius ?? 6 /* Constants.TagRadius */,
14695
+ _internal_paddingX: 9 /* Constants.TagPaddingX */ * sizeMultiplier,
14696
+ _internal_left: 0,
14697
+ _internal_width: 0,
14698
+ };
14699
+ rendererItem._internal_size = height;
14700
+ rendererItem._internal_y = centerY;
14701
+ rendererItem._internal_tag = tag;
14702
+ if (rendererItem._internal_text !== undefined) {
14703
+ rendererItem._internal_text._internal_y = centerY;
14704
+ }
14705
+ }
14525
14706
  // eslint-disable-next-line max-params, complexity
14526
- function fillSizeAndY(rendererItem, marker, seriesData, offsets, textHeight, shapeMargin, series, chart) {
14527
- const price = getPrice(seriesData, marker, series.priceScale().options().invertScale);
14707
+ function fillSizeAndY(rendererItem, marker, seriesData, offsets, textHeight, shapeMargin, series, chart, invertScale) {
14708
+ const price = getPrice(seriesData, marker, invertScale);
14528
14709
  if (price === undefined) {
14529
14710
  return;
14530
14711
  }
14531
14712
  const ignoreOffset = isPriceMarker(marker.position);
14532
14713
  const timeScale = chart.timeScale();
14533
14714
  const sizeMultiplier = isNumber(marker.size) ? Math.max(marker.size, 0) : 1;
14715
+ if (marker.shape === 'tag') {
14716
+ const anchorY = ensureNotNull(series.priceToCoordinate(price));
14717
+ fillTagSizeAndY(rendererItem, marker, anchorY, offsets, textHeight, shapeMargin, sizeMultiplier);
14718
+ return;
14719
+ }
14534
14720
  const shapeSize = calculateShapeHeight(timeScale.options().barSpacing) * sizeMultiplier;
14535
14721
  const halfSize = shapeSize / 2;
14536
14722
  rendererItem._internal_size = shapeSize;
@@ -14637,6 +14823,10 @@ class SeriesMarkersPaneView {
14637
14823
  _internal_externalId: marker.id,
14638
14824
  _internal_internalId: marker._internal_internalId,
14639
14825
  _internal_text: undefined,
14826
+ _internal_textColor: marker.textColor,
14827
+ _internal_borderColor: marker.borderColor,
14828
+ _internal_borderWidth: marker.borderWidth,
14829
+ _internal_tag: undefined,
14640
14830
  }));
14641
14831
  this._private__dataInvalidated = false;
14642
14832
  }
@@ -14647,13 +14837,22 @@ class SeriesMarkersPaneView {
14647
14837
  return;
14648
14838
  }
14649
14839
  const visibleBarsRange = new RangeImpl(Math.floor(visibleBars.from), Math.ceil(visibleBars.to));
14650
- const firstValue = this._private__series.data()[0];
14840
+ const firstValue = this._private__series.dataByIndex(0, 1 /* MismatchDirection.NearestRight */);
14651
14841
  if (firstValue === null) {
14652
14842
  return;
14653
14843
  }
14654
14844
  if (this._private__data._internal_items.length === 0) {
14655
14845
  return;
14656
14846
  }
14847
+ // This runs inside the hit-test and paint loops, where a throw costs the
14848
+ // rest of the frame.
14849
+ let invertScale = false;
14850
+ try {
14851
+ invertScale = this._private__series.priceScale().options().invertScale;
14852
+ }
14853
+ catch (error) {
14854
+ invertScale = false;
14855
+ }
14657
14856
  let prevTimeIndex = NaN;
14658
14857
  const shapeMargin$1 = shapeMargin(timeScale.options().barSpacing);
14659
14858
  const offsets = {
@@ -14684,7 +14883,7 @@ class SeriesMarkersPaneView {
14684
14883
  if (dataAt === null) {
14685
14884
  continue;
14686
14885
  }
14687
- fillSizeAndY(rendererItem, marker, dataAt, offsets, layoutOptions.fontSize, shapeMargin$1, this._private__series, this._private__chart);
14886
+ fillSizeAndY(rendererItem, marker, dataAt, offsets, layoutOptions.fontSize, shapeMargin$1, this._private__series, this._private__chart, invertScale);
14688
14887
  }
14689
14888
  this._private__invalidated = false;
14690
14889
  }
@@ -14842,6 +15041,10 @@ class SeriesMarkersPrimitive {
14842
15041
  text: marker.text,
14843
15042
  size: marker.size,
14844
15043
  price: marker.price,
15044
+ textColor: marker.textColor,
15045
+ borderColor: marker.borderColor,
15046
+ borderWidth: marker.borderWidth,
15047
+ borderRadius: marker.borderRadius,
14845
15048
  _internal_originalTime: marker.time,
14846
15049
  };
14847
15050
  if (marker.position === 'atPriceTop' ||
@@ -19143,10 +19346,13 @@ class TradingLineController {
19143
19346
  constructor(chart, element) {
19144
19347
  this._private__lines = new Map();
19145
19348
  this._private__hoveredId = null;
19349
+ this._private__hoveredPart = null;
19146
19350
  this._private__dragging = null;
19147
19351
  this._private__pointerId = null;
19148
19352
  this._private__pressOrigin = null;
19149
19353
  this._private__pressedLine = null;
19354
+ /** Distance from the pointer to the dragged line when the press landed. */
19355
+ this._private__grabOffset = 0;
19150
19356
  /** Latest pointer position in pane coordinates, or null when off the panes. */
19151
19357
  this._private__point = null;
19152
19358
  this._private__paneIndex = undefined;
@@ -19200,8 +19406,12 @@ class TradingLineController {
19200
19406
  if (this._private__dragging !== null) {
19201
19407
  return;
19202
19408
  }
19203
- const hovered = this._private__lineFromHit(param.hoveredObjectId);
19409
+ const hit = this._private__splitHit(param.hoveredObjectId);
19410
+ const hovered = hit === null ? null : this._private__lines.get(hit._internal_id) ?? null;
19204
19411
  this._private__setHovered(hovered ? hovered.id() : null);
19412
+ // The part is updated after the id, and separately from it: moving from a
19413
+ // label onto its close button keeps the same line hovered.
19414
+ this._private__setHoveredPart(hovered, hovered === null ? null : hit?._internal_part ?? null);
19205
19415
  this._private__applyCursor();
19206
19416
  }
19207
19417
  _private__onPointerDown(event) {
@@ -19220,6 +19430,7 @@ class TradingLineController {
19220
19430
  this._private__pointerId = event.pointerId;
19221
19431
  this._private__element.setPointerCapture(event.pointerId);
19222
19432
  line.beginDrag();
19433
+ this._private__grabOffset = this._private__measureGrabOffset(line, event);
19223
19434
  this._private__applyCursor();
19224
19435
  event.preventDefault();
19225
19436
  event.stopPropagation();
@@ -19254,6 +19465,7 @@ class TradingLineController {
19254
19465
  Math.abs(event.clientY - origin._internal_y) > CLICK_TOLERANCE_PX);
19255
19466
  if (dragged !== null) {
19256
19467
  this._private__dragging = null;
19468
+ this._private__grabOffset = 0;
19257
19469
  if (moved) {
19258
19470
  dragged.commitDrag();
19259
19471
  }
@@ -19281,6 +19493,7 @@ class TradingLineController {
19281
19493
  }
19282
19494
  const line = this._private__dragging;
19283
19495
  this._private__dragging = null;
19496
+ this._private__grabOffset = 0;
19284
19497
  this._private__releasePointer();
19285
19498
  line.cancelDrag();
19286
19499
  this._private__applyCursor();
@@ -19305,24 +19518,65 @@ class TradingLineController {
19305
19518
  if (line === null) {
19306
19519
  return null;
19307
19520
  }
19308
- // While the pointer is over the panes the engine gives pane coordinates.
19309
- // Once it leaves them — during a captured drag — fall back to the pane
19310
- // element, which still excludes the price axis.
19311
- if (this._private__point !== null && this._private__paneIndex === line.paneIndex()) {
19312
- return line.priceAt(this._private__point._internal_y);
19313
- }
19521
+ // The pointer event is the gesture's own position, so it is the source of
19522
+ // truth. The crosshair is only a mirror of it: it stops updating as soon
19523
+ // as the pointer leaves the pane canvas — over the price axis, past the
19524
+ // chart's edge — and a drag driven from it freezes there while the user
19525
+ // is still moving.
19526
+ const paneY = this._private__paneY(line, event);
19527
+ return paneY === null ? null : line.priceAt(paneY - this._private__grabOffset);
19528
+ }
19529
+ /** The pointer's position inside the line's pane, in pane coordinates. */
19530
+ _private__paneY(line, event) {
19314
19531
  const pane = this._private__chart.panes()[line.paneIndex()];
19315
19532
  const paneElement = pane ? pane.getHTMLElement() : null;
19316
- if (!paneElement) {
19317
- return null;
19533
+ if (paneElement) {
19534
+ return event.clientY - paneElement.getBoundingClientRect().top;
19535
+ }
19536
+ if (this._private__point !== null && this._private__paneIndex === line.paneIndex()) {
19537
+ return this._private__point._internal_y;
19538
+ }
19539
+ return null;
19540
+ }
19541
+ /**
19542
+ * Keeps the grab point: a line picked up by the edge of its label moves with
19543
+ * the pointer rather than centring itself on it. Ignored past a sane
19544
+ * distance, so a bad reading cannot send the line off on the first move.
19545
+ */
19546
+ _private__measureGrabOffset(line, event) {
19547
+ if (!line.dragAnchorY) {
19548
+ return 0;
19549
+ }
19550
+ const anchor = line.dragAnchorY();
19551
+ const paneY = this._private__paneY(line, event);
19552
+ if (anchor === null || paneY === null) {
19553
+ return 0;
19318
19554
  }
19319
- return line.priceAt(event.clientY - paneElement.getBoundingClientRect().top);
19555
+ const offset = paneY - anchor;
19556
+ return Math.abs(offset) > 40 /* Constants.MaxGrabOffsetPx */ ? 0 : offset;
19320
19557
  }
19321
- _private__lineFromHit(externalId) {
19322
- if (typeof externalId !== 'string') {
19558
+ /**
19559
+ * Hit ids are either a bare line id or `id#part`, which is how a line with
19560
+ * several targets reports which one was hit.
19561
+ */
19562
+ _private__splitHit(externalId) {
19563
+ if (typeof externalId !== 'string' || externalId.length === 0) {
19323
19564
  return null;
19324
19565
  }
19325
- return this._private__lines.get(externalId) ?? null;
19566
+ const separator = externalId.indexOf('#');
19567
+ if (separator === -1) {
19568
+ return { _internal_id: externalId, _internal_part: null };
19569
+ }
19570
+ return { _internal_id: externalId.slice(0, separator), _internal_part: externalId.slice(separator + 1) };
19571
+ }
19572
+ _private__setHoveredPart(line, part) {
19573
+ if (this._private__hoveredPart === part) {
19574
+ return;
19575
+ }
19576
+ this._private__hoveredPart = part;
19577
+ if (line && line.setHoveredPart) {
19578
+ line.setHoveredPart(part);
19579
+ }
19326
19580
  }
19327
19581
  _private__setHovered(id) {
19328
19582
  if (this._private__hoveredId === id) {
@@ -19332,6 +19586,7 @@ class TradingLineController {
19332
19586
  this._private__lines.get(this._private__hoveredId)?.setHovered(false);
19333
19587
  }
19334
19588
  this._private__hoveredId = id;
19589
+ this._private__hoveredPart = null;
19335
19590
  if (id !== null) {
19336
19591
  this._private__lines.get(id)?.setHovered(true);
19337
19592
  }
@@ -19855,6 +20110,862 @@ function createInteractiveLineManager(chart, series) {
19855
20110
  return new InteractiveLineManager(chart, series);
19856
20111
  }
19857
20112
 
20113
+ /**
20114
+ * Fans out labels that land on top of each other.
20115
+ *
20116
+ * Every position line on a chart shares one of these. Two positions opened at
20117
+ * the same price draw their labels at the same coordinate, and the one behind
20118
+ * becomes unreachable — the engine resolves the hit to whichever primitive it
20119
+ * reaches first, and that is always the same one. Nudging each collision down
20120
+ * leaves every label a strip of its own to be grabbed by.
20121
+ *
20122
+ * The line itself never moves: it stays at the price it belongs to, and only
20123
+ * the label is offset.
20124
+ */
20125
+ class PositionLineLabelLayout {
20126
+ constructor() {
20127
+ /** Registration order. It is what keeps the fan from reshuffling per frame. */
20128
+ this._private__order = [];
20129
+ this._private__claims = new Map();
20130
+ this._private__offsets = new Map();
20131
+ this._private__invalidated = true;
20132
+ }
20133
+ _internal_register(ownerId) {
20134
+ if (!this._private__claims.has(ownerId)) {
20135
+ this._private__order.push(ownerId);
20136
+ this._private__claims.set(ownerId, []);
20137
+ this._private__invalidated = true;
20138
+ }
20139
+ }
20140
+ _internal_unregister(ownerId) {
20141
+ const index = this._private__order.indexOf(ownerId);
20142
+ if (index !== -1) {
20143
+ this._private__order.splice(index, 1);
20144
+ }
20145
+ this._private__claims.delete(ownerId);
20146
+ this._private__invalidated = true;
20147
+ }
20148
+ _internal_isEmpty() {
20149
+ return this._private__order.length === 0;
20150
+ }
20151
+ /**
20152
+ * Publishes where an owner's labels want to sit. Called while each line
20153
+ * rebuilds its renderer data, so a fan can be one frame behind while prices
20154
+ * move — which is invisible, and cheaper than forcing a second pass.
20155
+ */
20156
+ _internal_setClaims(ownerId, claims) {
20157
+ this._private__claims.set(ownerId, claims);
20158
+ this._private__invalidated = true;
20159
+ }
20160
+ /** How far down a label has to move to clear the ones already placed. */
20161
+ _internal_offsetFor(ownerId, level) {
20162
+ if (this._private__invalidated) {
20163
+ this._private__recalculate();
20164
+ }
20165
+ return this._private__offsets.get(key(ownerId, level)) ?? 0;
20166
+ }
20167
+ _private__recalculate() {
20168
+ const offsets = new Map();
20169
+ const placed = [];
20170
+ for (const ownerId of this._private__order) {
20171
+ for (const claim of this._private__claims.get(ownerId) ?? []) {
20172
+ const gap = minGap(claim._internal_height);
20173
+ const maxOffset = claim._internal_height;
20174
+ // Moved down to the first coordinate that clears everything already
20175
+ // placed, and no further. Two levels whose own prices nearly part
20176
+ // them only need the few pixels they are short of — stepping by a
20177
+ // full gap each time is what made the fan look flung open.
20178
+ let y = claim._internal_y;
20179
+ for (const other of [...placed].sort(byY)) {
20180
+ if (Math.abs(y - other._internal_y) < gap) {
20181
+ y = other._internal_y + gap;
20182
+ }
20183
+ }
20184
+ let offset = y - claim._internal_y;
20185
+ if (offset > maxOffset) {
20186
+ // More labels than there is room to fan. The rest stack, which is
20187
+ // no worse than before and keeps the pile from running off.
20188
+ offset = 0;
20189
+ }
20190
+ placed.push({ _internal_y: claim._internal_y + offset, _internal_height: claim._internal_height });
20191
+ offsets.set(key(ownerId, claim._internal_level), offset);
20192
+ }
20193
+ }
20194
+ this._private__offsets = offsets;
20195
+ this._private__invalidated = false;
20196
+ }
20197
+ }
20198
+ function key(ownerId, level) {
20199
+ return `${ownerId}:${level}`;
20200
+ }
20201
+ /**
20202
+ * The strip a label keeps to itself. Comfortably clickable, and no wider —
20203
+ * every pixel here is a pixel the label sits away from its own line.
20204
+ */
20205
+ function minGap(height) {
20206
+ return Math.max(9, Math.round(height * 0.4));
20207
+ }
20208
+ function byY(a, b) {
20209
+ return a._internal_y - b._internal_y;
20210
+ }
20211
+ const layouts = new WeakMap();
20212
+ /** The layout is shared by every position line on the same chart. */
20213
+ function acquireLabelLayout(chart, ownerId) {
20214
+ let layout = layouts.get(chart);
20215
+ if (!layout) {
20216
+ layout = new PositionLineLabelLayout();
20217
+ layouts.set(chart, layout);
20218
+ }
20219
+ layout._internal_register(ownerId);
20220
+ return layout;
20221
+ }
20222
+ function releaseLabelLayout(chart, ownerId) {
20223
+ const layout = layouts.get(chart);
20224
+ if (!layout) {
20225
+ return;
20226
+ }
20227
+ layout._internal_unregister(ownerId);
20228
+ if (layout._internal_isEmpty()) {
20229
+ layouts.delete(chart);
20230
+ }
20231
+ }
20232
+
20233
+ const positionLineDefaultColors = {
20234
+ entry: '#2962FF',
20235
+ takeProfit: '#26A17B',
20236
+ stopLoss: '#D9A23B',
20237
+ };
20238
+ const positionLineThemeDefaults = {
20239
+ badgeTextColor: '#FFFFFF',
20240
+ borderRadius: 4,
20241
+ borderWidth: 1,
20242
+ paddingX: 8,
20243
+ paddingY: 6,
20244
+ backgroundOpacity: 1,
20245
+ };
20246
+ const positionLineLevels = ['takeProfit', 'entry', 'stopLoss'];
20247
+
20248
+ class PositionLineRenderer {
20249
+ constructor() {
20250
+ this._private__data = null;
20251
+ this._private__layouts = [];
20252
+ this._private__connectorX = 0;
20253
+ this._private__font = '';
20254
+ this._private__colorParser = new ColorParser([]);
20255
+ this._private__textWidthCache = new TextWidthCache(50);
20256
+ }
20257
+ _internal_setData(data) {
20258
+ this._private__data = data;
20259
+ }
20260
+ _internal_hitTest(x, y) {
20261
+ // The close button sits on top of the label, which sits on top of the
20262
+ // line, so the narrowest target is tested first.
20263
+ for (const layout of this._private__layouts) {
20264
+ if (layout._internal_closable && x >= layout._internal_closeLeft && x <= layout._internal_closeRight &&
20265
+ Math.abs(y - layout._internal_labelY) <= layout._internal_height / 2) {
20266
+ return { _internal_level: layout._internal_level, _internal_part: 'close' };
20267
+ }
20268
+ }
20269
+ for (const layout of this._private__layouts) {
20270
+ if (x >= layout._internal_pillLeft && x <= layout._internal_pillRight && Math.abs(y - layout._internal_labelY) <= layout._internal_height / 2) {
20271
+ return { _internal_level: layout._internal_level, _internal_part: 'body' };
20272
+ }
20273
+ }
20274
+ for (const layout of this._private__layouts) {
20275
+ if (Math.abs(y - layout._internal_y) <= 8 /* Constants.LineHitTolerance */) {
20276
+ return { _internal_level: layout._internal_level, _internal_part: 'line' };
20277
+ }
20278
+ }
20279
+ return null;
20280
+ }
20281
+ draw(target) {
20282
+ target.useBitmapCoordinateSpace((scope) => this._private__drawImpl(scope));
20283
+ }
20284
+ _private__drawImpl(scope) {
20285
+ const data = this._private__data;
20286
+ this._private__layouts = [];
20287
+ if (data === null || data._internal_levels.length === 0) {
20288
+ return;
20289
+ }
20290
+ const { context: ctx, mediaSize } = scope;
20291
+ ctx.save();
20292
+ if (this._private__font !== data._internal_font) {
20293
+ this._private__font = data._internal_font;
20294
+ this._private__textWidthCache._internal_reset();
20295
+ }
20296
+ ctx.font = data._internal_font;
20297
+ ctx.textBaseline = 'middle';
20298
+ this._private__connectorX = Math.max(0, mediaSize.width - data._internal_rightOffset);
20299
+ const lineEnd = data._internal_connectorVisible ? this._private__connectorX : mediaSize.width;
20300
+ for (const level of data._internal_levels) {
20301
+ this._private__layouts.push(this._private__drawLevel(scope, data, level, lineEnd));
20302
+ }
20303
+ if (data._internal_connectorVisible && data._internal_levels.length > 1) {
20304
+ this._private__drawConnector(scope, data);
20305
+ }
20306
+ ctx.restore();
20307
+ }
20308
+ // eslint-disable-next-line max-params
20309
+ _private__drawLevel(scope, data, level, lineEnd) {
20310
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20311
+ const height = data._internal_fontSize + data._internal_paddingY * 2;
20312
+ const badgeWidth = level._internal_badge.length === 0
20313
+ ? 0
20314
+ : Math.max(this._private__textWidthCache._internal_measureText(ctx, level._internal_badge) + data._internal_paddingX * 2, height * 0.85 /* Constants.BadgeMinWidthRatio */);
20315
+ const labelWidth = this._private__textWidthCache._internal_measureText(ctx, level._internal_text) + data._internal_paddingX * 2;
20316
+ const closeWidth = level._internal_closable ? height : 0;
20317
+ const pillWidth = badgeWidth + labelWidth + closeWidth;
20318
+ const pillRight = Math.max(pillWidth, lineEnd - 8 /* Constants.LabelGap */);
20319
+ const pillLeft = pillRight - pillWidth;
20320
+ const labelY = level._internal_y + level._internal_labelOffsetY;
20321
+ const top = labelY - height / 2;
20322
+ this._private__drawLineSegments(scope, level, pillLeft, pillRight, lineEnd);
20323
+ this._private__drawPill(scope, data, level, { _internal_left: pillLeft, _internal_top: top, _internal_width: pillWidth, _internal_height: height });
20324
+ let cellLeft = pillLeft;
20325
+ if (badgeWidth > 0) {
20326
+ this._private__drawBadge(scope, data, level, { _internal_left: cellLeft, _internal_top: top, _internal_width: badgeWidth, _internal_height: height });
20327
+ cellLeft += badgeWidth;
20328
+ this._private__drawSeparator(ctx, level._internal_color, cellLeft, top, height, hpr, vpr);
20329
+ }
20330
+ ctx.save();
20331
+ ctx.scale(hpr, vpr);
20332
+ ctx.textAlign = 'center';
20333
+ ctx.fillStyle = data._internal_textColor ?? level._internal_color;
20334
+ ctx.fillText(level._internal_text, cellLeft + labelWidth / 2, labelY);
20335
+ ctx.restore();
20336
+ cellLeft += labelWidth;
20337
+ if (level._internal_closable) {
20338
+ this._private__drawSeparator(ctx, level._internal_color, cellLeft, top, height, hpr, vpr);
20339
+ this._private__drawCloseButton(scope, data, level, { _internal_left: cellLeft, _internal_top: top, _internal_width: closeWidth, _internal_height: height });
20340
+ }
20341
+ return {
20342
+ _internal_level: level._internal_level,
20343
+ _internal_y: level._internal_y,
20344
+ _internal_labelY: labelY,
20345
+ _internal_height: height,
20346
+ _internal_pillLeft: pillLeft,
20347
+ _internal_pillRight: pillRight,
20348
+ _internal_closeLeft: cellLeft,
20349
+ _internal_closeRight: cellLeft + closeWidth,
20350
+ _internal_closable: level._internal_closable,
20351
+ };
20352
+ }
20353
+ // eslint-disable-next-line max-params
20354
+ _private__drawLineSegments(scope, level, pillLeft, pillRight, lineEnd) {
20355
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20356
+ const lineWidth = Math.max(1, Math.round(level._internal_lineWidth * vpr));
20357
+ const y = Math.round(level._internal_y * vpr) - Math.floor(lineWidth / 2);
20358
+ ctx.save();
20359
+ ctx.strokeStyle = level._internal_color;
20360
+ ctx.lineWidth = lineWidth;
20361
+ setLineStyle(ctx, level._internal_lineStyle);
20362
+ ctx.beginPath();
20363
+ if (level._internal_labelOffsetY !== 0) {
20364
+ // The label has been fanned off its line, so breaking the line for it
20365
+ // would leave a gap with nothing in it.
20366
+ ctx.moveTo(0, y);
20367
+ ctx.lineTo(Math.round(lineEnd * hpr), y);
20368
+ }
20369
+ else {
20370
+ ctx.moveTo(0, y);
20371
+ ctx.lineTo(Math.round(Math.max(0, pillLeft - 8 /* Constants.LabelGap */) * hpr), y);
20372
+ if (lineEnd > pillRight + 8 /* Constants.LabelGap */) {
20373
+ ctx.moveTo(Math.round((pillRight + 8 /* Constants.LabelGap */) * hpr), y);
20374
+ ctx.lineTo(Math.round(lineEnd * hpr), y);
20375
+ }
20376
+ }
20377
+ ctx.stroke();
20378
+ ctx.restore();
20379
+ }
20380
+ // eslint-disable-next-line max-params
20381
+ _private__drawPill(scope, data, level, box) {
20382
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20383
+ const hovered = data._internal_hoveredLevel === level._internal_level;
20384
+ const borderWidth = Math.max(1, Math.round((hovered ? data._internal_borderWidth + 1 : data._internal_borderWidth) * hpr));
20385
+ ctx.save();
20386
+ ctx.setLineDash([]);
20387
+ bitmapRoundRect(ctx, box, data._internal_borderRadius, hpr, vpr);
20388
+ ctx.fillStyle = this._private__colorParser._internal_applyAlpha(data._internal_background, data._internal_backgroundOpacity);
20389
+ ctx.fill();
20390
+ ctx.lineWidth = borderWidth;
20391
+ ctx.strokeStyle = level._internal_color;
20392
+ ctx.stroke();
20393
+ ctx.restore();
20394
+ }
20395
+ // eslint-disable-next-line max-params
20396
+ _private__drawBadge(scope, data, level, box) {
20397
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20398
+ if (level._internal_badgeFilled) {
20399
+ // The badge shares the label's left corners, so its fill has to stop
20400
+ // short of the border instead of squaring it off.
20401
+ const inset = data._internal_borderWidth;
20402
+ ctx.save();
20403
+ 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');
20404
+ ctx.fillStyle = level._internal_color;
20405
+ ctx.fill();
20406
+ ctx.restore();
20407
+ }
20408
+ ctx.save();
20409
+ ctx.scale(hpr, vpr);
20410
+ ctx.textAlign = 'center';
20411
+ ctx.fillStyle = level._internal_badgeFilled ? data._internal_badgeTextColor : level._internal_color;
20412
+ ctx.fillText(level._internal_badge, box._internal_left + box._internal_width / 2, box._internal_top + box._internal_height / 2);
20413
+ ctx.restore();
20414
+ }
20415
+ // eslint-disable-next-line max-params
20416
+ _private__drawSeparator(ctx, color, x, top, height, hpr, vpr) {
20417
+ const width = Math.max(1, Math.floor(hpr));
20418
+ ctx.save();
20419
+ ctx.setLineDash([]);
20420
+ ctx.fillStyle = this._private__colorParser._internal_applyAlpha(color, 0.4 /* Constants.SeparatorAlpha */);
20421
+ ctx.fillRect(Math.round(x * hpr), Math.round(top * vpr), width, Math.round(height * vpr));
20422
+ ctx.restore();
20423
+ }
20424
+ // eslint-disable-next-line max-params
20425
+ _private__drawCloseButton(scope, data, level, box) {
20426
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20427
+ const hovered = data._internal_hoveredLevel === level._internal_level && data._internal_hoveredPart === 'close';
20428
+ if (hovered) {
20429
+ ctx.save();
20430
+ const inset = data._internal_borderWidth;
20431
+ 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');
20432
+ ctx.fillStyle = this._private__colorParser._internal_applyAlpha(level._internal_color, 0.25);
20433
+ ctx.fill();
20434
+ ctx.restore();
20435
+ }
20436
+ const centerX = (box._internal_left + box._internal_width / 2) * hpr;
20437
+ const centerY = (box._internal_top + box._internal_height / 2) * vpr;
20438
+ const arm = (7 /* Constants.CloseGlyphSize */ / 2) * hpr;
20439
+ ctx.save();
20440
+ ctx.setLineDash([]);
20441
+ ctx.lineCap = 'round';
20442
+ ctx.lineWidth = Math.max(1, Math.round(1.4 * hpr));
20443
+ ctx.strokeStyle = level._internal_color;
20444
+ ctx.beginPath();
20445
+ ctx.moveTo(centerX - arm, centerY - arm);
20446
+ ctx.lineTo(centerX + arm, centerY + arm);
20447
+ ctx.moveTo(centerX + arm, centerY - arm);
20448
+ ctx.lineTo(centerX - arm, centerY + arm);
20449
+ ctx.stroke();
20450
+ ctx.restore();
20451
+ }
20452
+ _private__drawConnector(scope, data) {
20453
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20454
+ const entry = data._internal_levels.find((level) => level._internal_level === 'entry') ?? data._internal_levels[0];
20455
+ const ys = data._internal_levels.map((level) => level._internal_y);
20456
+ const x = Math.round(this._private__connectorX * hpr);
20457
+ ctx.save();
20458
+ ctx.setLineDash([]);
20459
+ ctx.strokeStyle = entry._internal_color;
20460
+ ctx.lineWidth = Math.max(1, Math.floor(hpr));
20461
+ ctx.beginPath();
20462
+ ctx.moveTo(x, Math.round(Math.min(...ys) * vpr));
20463
+ ctx.lineTo(x, Math.round(Math.max(...ys) * vpr));
20464
+ ctx.stroke();
20465
+ for (const level of data._internal_levels) {
20466
+ ctx.beginPath();
20467
+ ctx.arc(x, Math.round(level._internal_y * vpr), 4.5 /* Constants.HandleRadius */ * hpr, 0, 2 * Math.PI);
20468
+ ctx.fillStyle = level._internal_level === 'entry'
20469
+ ? entry._internal_color
20470
+ : this._private__colorParser._internal_applyAlpha(data._internal_background, data._internal_backgroundOpacity);
20471
+ ctx.fill();
20472
+ ctx.lineWidth = Math.max(1, Math.round(1.5 * hpr));
20473
+ ctx.strokeStyle = level._internal_color;
20474
+ ctx.stroke();
20475
+ }
20476
+ ctx.restore();
20477
+ }
20478
+ }
20479
+ // eslint-disable-next-line max-params
20480
+ function bitmapRoundRect(ctx, box, radius, hpr, vpr, side = 'all') {
20481
+ const left = Math.round(box._internal_left * hpr);
20482
+ const top = Math.round(box._internal_top * vpr);
20483
+ const width = Math.round(box._internal_width * hpr);
20484
+ const height = Math.round(box._internal_height * vpr);
20485
+ const r = Math.min(radius * hpr, width / 2, height / 2);
20486
+ const radii = side === 'left'
20487
+ ? [r, 0, 0, r]
20488
+ : side === 'right'
20489
+ ? [0, r, r, 0]
20490
+ : [r, r, r, r];
20491
+ drawRoundRect(ctx, left, top, width, height, radii);
20492
+ }
20493
+
20494
+ class PositionLinePaneView {
20495
+ constructor() {
20496
+ this._private__renderer = new PositionLineRenderer();
20497
+ this._private__data = null;
20498
+ }
20499
+ _internal_update(data) {
20500
+ this._private__data = data;
20501
+ }
20502
+ renderer() {
20503
+ if (this._private__data === null) {
20504
+ return null;
20505
+ }
20506
+ this._private__renderer._internal_setData(this._private__data);
20507
+ return this._private__renderer;
20508
+ }
20509
+ zOrder() {
20510
+ return 'top';
20511
+ }
20512
+ _internal_hitTest(x, y) {
20513
+ return this._private__data === null ? null : this._private__renderer._internal_hitTest(x, y);
20514
+ }
20515
+ }
20516
+
20517
+ class PositionLineAxisView {
20518
+ constructor(source) {
20519
+ this._private__source = source;
20520
+ }
20521
+ coordinate() {
20522
+ // A hidden label is pushed far off the axis so it cannot reserve a gap
20523
+ // between the labels that are shown.
20524
+ return this._private__label()?._internal_coordinate ?? -1e3;
20525
+ }
20526
+ text() {
20527
+ return this._private__label()?._internal_text ?? '';
20528
+ }
20529
+ textColor() {
20530
+ return this._private__label()?._internal_textColor ?? '#FFFFFF';
20531
+ }
20532
+ backColor() {
20533
+ return this._private__label()?._internal_color ?? '#000000';
20534
+ }
20535
+ visible() {
20536
+ return this._private__label() !== null;
20537
+ }
20538
+ _private__label() {
20539
+ return this._private__source();
20540
+ }
20541
+ }
20542
+
20543
+ let nextPositionId = 0;
20544
+ const DEFAULT_RIGHT_OFFSET = 56;
20545
+ /**
20546
+ * A group of trading levels — entry, take profit and stop loss — drawn as one
20547
+ * unit: a line per level, a label carrying size and P&L, and a connector that
20548
+ * ties the three together.
20549
+ *
20550
+ * Input is not handled here. Every trading line on a chart shares a single
20551
+ * TradingLineController, which is what guarantees that one gesture moves one
20552
+ * level, and that a position line and a plain draggable price line on the same
20553
+ * chart do not fight over the pointer.
20554
+ */
20555
+ class PositionLine {
20556
+ constructor(chart, series, options) {
20557
+ this._private__paneView = new PositionLinePaneView();
20558
+ this._private__colorParser = new ColorParser([]);
20559
+ this._private__attached = false;
20560
+ this._private__hoveredLevel = null;
20561
+ this._private__hoveredPart = null;
20562
+ this._private__dragLevel = null;
20563
+ this._private__priceBeforeDrag = null;
20564
+ this._private__chart = chart;
20565
+ this._private__series = series;
20566
+ this._private__id = `pipsend-position-line-${nextPositionId++}`;
20567
+ this._private__options = options;
20568
+ this._private__theme = { ...positionLineThemeDefaults, ...options.theme };
20569
+ this._private__paneViews = [this._private__paneView];
20570
+ this._private__axisViews = positionLineLevels.map((level) => new PositionLineAxisView(() => this._private__axisLabel(level)));
20571
+ this._private__labelLayout = acquireLabelLayout(this._private__chart, this._private__id);
20572
+ this._private__series.attachPrimitive(this);
20573
+ acquireController(this._private__chart)?._internal_register(this);
20574
+ }
20575
+ // ---- ISeriesPrimitive ----
20576
+ attached(param) {
20577
+ this._private__requestUpdate = param.requestUpdate;
20578
+ this._private__attached = true;
20579
+ this.requestUpdate();
20580
+ }
20581
+ detached() {
20582
+ this._private__attached = false;
20583
+ this._private__requestUpdate = undefined;
20584
+ }
20585
+ paneViews() {
20586
+ return this._private__paneViews;
20587
+ }
20588
+ priceAxisViews() {
20589
+ return this._private__axisViews;
20590
+ }
20591
+ updateAllViews() {
20592
+ this._private__paneView._internal_update(this._private__buildRendererData());
20593
+ }
20594
+ hitTest(x, y) {
20595
+ const hit = this._private__paneView._internal_hitTest(x, y);
20596
+ if (hit === null) {
20597
+ return null;
20598
+ }
20599
+ const level = this._private__levelOptions(hit._internal_level);
20600
+ if (level === null) {
20601
+ return null;
20602
+ }
20603
+ return {
20604
+ externalId: `${this._private__id}#${hit._internal_level}:${hit._internal_part}`,
20605
+ zOrder: 'top',
20606
+ cursorStyle: hit._internal_part === 'close'
20607
+ ? 'pointer'
20608
+ : (level.draggable ?? true) ? 'ns-resize' : undefined,
20609
+ };
20610
+ }
20611
+ // ---- ControllableLine ----
20612
+ id() {
20613
+ return this._private__id;
20614
+ }
20615
+ paneIndex() {
20616
+ try {
20617
+ return this._private__series.getPane().paneIndex();
20618
+ }
20619
+ catch (error) {
20620
+ // The engine asserts the series belongs to a pane, and that lookup can
20621
+ // come back empty while panes are being rebuilt. The main pane is the
20622
+ // right guess and keeps the gesture alive instead of throwing mid-drag.
20623
+ return 0;
20624
+ }
20625
+ }
20626
+ setHoveredPart(part) {
20627
+ const [level, name] = part === null ? [null, null] : part.split(':');
20628
+ const nextLevel = level ?? null;
20629
+ const nextPart = name ?? null;
20630
+ if (this._private__hoveredLevel === nextLevel && this._private__hoveredPart === nextPart) {
20631
+ return;
20632
+ }
20633
+ this._private__hoveredLevel = nextLevel;
20634
+ this._private__hoveredPart = nextPart;
20635
+ this.requestUpdate();
20636
+ }
20637
+ setHovered(hovered) {
20638
+ if (!hovered) {
20639
+ this.setHoveredPart(null);
20640
+ }
20641
+ }
20642
+ isDraggable() {
20643
+ if (this._private__hoveredLevel === null || this._private__hoveredPart === 'close') {
20644
+ return false;
20645
+ }
20646
+ return this._private__levelOptions(this._private__hoveredLevel)?.draggable ?? true;
20647
+ }
20648
+ isClickable() {
20649
+ if (this._private__hoveredPart === 'close') {
20650
+ return true;
20651
+ }
20652
+ return this._private__options.onClick !== undefined;
20653
+ }
20654
+ priceAt(paneY) {
20655
+ const price = this._private__series.coordinateToPrice(paneY);
20656
+ return price === null ? null : price;
20657
+ }
20658
+ /**
20659
+ * Where the level being dragged actually sits. The controller measures the
20660
+ * pointer against it so the level moves by what the pointer moved, instead
20661
+ * of jumping to sit under it — which matters most when the label has been
20662
+ * fanned away from its own line.
20663
+ */
20664
+ dragAnchorY() {
20665
+ if (this._private__dragLevel === null) {
20666
+ return null;
20667
+ }
20668
+ const price = this._private__levelOptions(this._private__dragLevel)?.price;
20669
+ if (price === undefined || !isFinite(price)) {
20670
+ return null;
20671
+ }
20672
+ const y = this._private__series.priceToCoordinate(price);
20673
+ return y === null ? null : y;
20674
+ }
20675
+ beginDrag() {
20676
+ this._private__dragLevel = this._private__hoveredLevel;
20677
+ if (this._private__dragLevel === null) {
20678
+ return;
20679
+ }
20680
+ const price = this._private__levelOptions(this._private__dragLevel)?.price ?? null;
20681
+ this._private__priceBeforeDrag = price;
20682
+ if (price !== null && this._private__options.onDragStart) {
20683
+ this._private__options.onDragStart(this._private__dragLevel, price);
20684
+ }
20685
+ }
20686
+ dragTo(price) {
20687
+ if (this._private__dragLevel === null) {
20688
+ return;
20689
+ }
20690
+ this._private__applyPrice(this._private__dragLevel, price);
20691
+ if (this._private__options.onDragMove) {
20692
+ this._private__options.onDragMove(this._private__dragLevel, price);
20693
+ }
20694
+ }
20695
+ commitDrag() {
20696
+ const level = this._private__dragLevel;
20697
+ this._private__dragLevel = null;
20698
+ this._private__priceBeforeDrag = null;
20699
+ if (level === null) {
20700
+ return;
20701
+ }
20702
+ const price = this._private__levelOptions(level)?.price;
20703
+ if (price !== undefined && this._private__options.onDragEnd) {
20704
+ this._private__options.onDragEnd(level, price);
20705
+ }
20706
+ }
20707
+ cancelDrag() {
20708
+ const level = this._private__dragLevel;
20709
+ const original = this._private__priceBeforeDrag;
20710
+ this._private__dragLevel = null;
20711
+ this._private__priceBeforeDrag = null;
20712
+ if (level === null) {
20713
+ return;
20714
+ }
20715
+ // Reported even when the price did not move, so a caller that pauses work
20716
+ // on `onDragStart` always gets the matching end of the gesture.
20717
+ const restored = original !== null && original !== this._private__levelOptions(level)?.price;
20718
+ if (restored) {
20719
+ this._private__applyPrice(level, original);
20720
+ if (this._private__options.onDragMove) {
20721
+ this._private__options.onDragMove(level, original);
20722
+ }
20723
+ }
20724
+ if (this._private__options.onDragCancel) {
20725
+ this._private__options.onDragCancel(level, this._private__levelOptions(level)?.price ?? 0);
20726
+ }
20727
+ }
20728
+ click() {
20729
+ const level = this._private__hoveredLevel;
20730
+ if (level === null) {
20731
+ return;
20732
+ }
20733
+ if (this._private__hoveredPart === 'close') {
20734
+ if (this._private__options.onClose) {
20735
+ this._private__options.onClose(level);
20736
+ }
20737
+ return;
20738
+ }
20739
+ const price = this._private__levelOptions(level)?.price;
20740
+ if (price !== undefined && this._private__options.onClick) {
20741
+ this._private__options.onClick(level, price);
20742
+ }
20743
+ }
20744
+ // ---- public API ----
20745
+ /**
20746
+ * Updates the group. Any subset of options can be passed; the levels given
20747
+ * are merged into the current ones, and a level set to `undefined` is
20748
+ * removed.
20749
+ */
20750
+ applyOptions(options) {
20751
+ // A level under an active gesture owns its price. Callers feeding the
20752
+ // group from a socket re-send the old price several times a second, and
20753
+ // letting that through would pull the level out from under the cursor.
20754
+ const dragged = this._private__dragLevel;
20755
+ const draggedPrice = dragged === null ? undefined : this._private__levelOptions(dragged)?.price;
20756
+ const merged = { ...this._private__options, ...options };
20757
+ merged.entry = { ...this._private__options.entry, ...options.entry };
20758
+ merged.takeProfit = mergeLevel(this._private__options.takeProfit, options, 'takeProfit');
20759
+ merged.stopLoss = mergeLevel(this._private__options.stopLoss, options, 'stopLoss');
20760
+ this._private__options = merged;
20761
+ if (dragged !== null && draggedPrice !== undefined) {
20762
+ const level = this._private__levelOptions(dragged);
20763
+ if (level !== null) {
20764
+ level.price = draggedPrice;
20765
+ }
20766
+ }
20767
+ if (options.theme) {
20768
+ // Merged onto what is already applied, so two partial theme updates do
20769
+ // not undo each other.
20770
+ this._private__theme = { ...this._private__theme, ...options.theme };
20771
+ }
20772
+ this.requestUpdate();
20773
+ }
20774
+ /**
20775
+ * Moves one level to a new price, recomputing its text when the level has a
20776
+ * `formatText`.
20777
+ *
20778
+ * Ignored for a level the user is currently dragging — the gesture owns the
20779
+ * price until it reports back through
20780
+ * {@link PositionLineOptions.onDragEnd} or
20781
+ * {@link PositionLineOptions.onDragCancel}.
20782
+ */
20783
+ setPrice(level, price) {
20784
+ if (this._private__dragLevel === level) {
20785
+ return;
20786
+ }
20787
+ this._private__applyPrice(level, price);
20788
+ }
20789
+ /**
20790
+ * Replaces one level's label text. Use this for text that changes on its own
20791
+ * — a ticking P&L on a fixed entry — rather than with the price.
20792
+ */
20793
+ setText(level, text) {
20794
+ const options = this._private__levelOptions(level);
20795
+ if (options === null) {
20796
+ return;
20797
+ }
20798
+ options.text = text;
20799
+ this.requestUpdate();
20800
+ }
20801
+ /**
20802
+ * The current price of each level that is present.
20803
+ */
20804
+ prices() {
20805
+ const result = {};
20806
+ for (const level of positionLineLevels) {
20807
+ const options = this._private__levelOptions(level);
20808
+ if (options !== null) {
20809
+ result[level] = options.price;
20810
+ }
20811
+ }
20812
+ return result;
20813
+ }
20814
+ options() {
20815
+ return this._private__options;
20816
+ }
20817
+ /**
20818
+ * Removes the group from the chart and releases its share of the input
20819
+ * controller.
20820
+ */
20821
+ remove() {
20822
+ releaseController(this._private__chart, this);
20823
+ releaseLabelLayout(this._private__chart, this._private__id);
20824
+ this._private__series.detachPrimitive(this);
20825
+ }
20826
+ requestUpdate() {
20827
+ if (this._private__requestUpdate) {
20828
+ this._private__requestUpdate();
20829
+ }
20830
+ }
20831
+ /** Reads back the label the price axis should draw for a level. */
20832
+ _private__axisLabel(level) {
20833
+ const options = this._private__levelOptions(level);
20834
+ if (options === null || !isFinite(options.price) ||
20835
+ this._private__options.visible === false || options.axisLabelVisible === false) {
20836
+ return null;
20837
+ }
20838
+ const y = this._private__series.priceToCoordinate(options.price);
20839
+ if (y === null) {
20840
+ return null;
20841
+ }
20842
+ const color = options.color ?? positionLineDefaultColors[level];
20843
+ return {
20844
+ _internal_coordinate: y,
20845
+ _internal_text: this._private__series.priceFormatter().format(options.price),
20846
+ _internal_color: color,
20847
+ _internal_textColor: this._private__colorParser._internal_generateContrastColors(color)._internal_foreground,
20848
+ };
20849
+ }
20850
+ _private__levelOptions(level) {
20851
+ const options = level === 'entry' ? this._private__options.entry : this._private__options[level];
20852
+ return options ?? null;
20853
+ }
20854
+ _private__applyPrice(level, price) {
20855
+ const options = this._private__levelOptions(level);
20856
+ if (options === null) {
20857
+ return;
20858
+ }
20859
+ options.price = price;
20860
+ if (options.formatText) {
20861
+ options.text = options.formatText(price);
20862
+ }
20863
+ this.requestUpdate();
20864
+ }
20865
+ _private__buildRendererData() {
20866
+ if (!this._private__attached || this._private__options.visible === false) {
20867
+ return null;
20868
+ }
20869
+ const layout = this._private__chart.options()['layout'];
20870
+ const fontSize = this._private__theme.fontSize ?? layout.fontSize;
20871
+ const labelHeight = fontSize + this._private__theme.paddingY * 2;
20872
+ const levels = [];
20873
+ const claims = [];
20874
+ for (const level of positionLineLevels) {
20875
+ const options = this._private__levelOptions(level);
20876
+ // A level added through a patch can be missing its price until the
20877
+ // caller supplies one; drawing it would put it at NaN.
20878
+ if (options === null || !isFinite(options.price)) {
20879
+ continue;
20880
+ }
20881
+ const y = this._private__series.priceToCoordinate(options.price);
20882
+ if (y === null) {
20883
+ continue;
20884
+ }
20885
+ const isEntry = level === 'entry';
20886
+ claims.push({ _internal_level: level, _internal_y: y, _internal_height: labelHeight });
20887
+ levels.push({
20888
+ _internal_level: level,
20889
+ _internal_y: y,
20890
+ _internal_labelOffsetY: 0,
20891
+ _internal_color: options.color ?? positionLineDefaultColors[level],
20892
+ _internal_text: options.formatText ? options.formatText(options.price) : options.text ?? '',
20893
+ _internal_badge: options.badge ?? '',
20894
+ _internal_badgeFilled: options.badgeFilled ?? isEntry,
20895
+ _internal_lineWidth: options.lineWidth ?? 1,
20896
+ _internal_lineStyle: options.lineStyle ?? (isEntry ? 0 /* LineStyle.Solid */ : 2 /* LineStyle.Dashed */),
20897
+ _internal_closable: options.closable ?? true,
20898
+ _internal_draggable: options.draggable ?? true,
20899
+ });
20900
+ }
20901
+ if (levels.length === 0) {
20902
+ this._private__labelLayout._internal_setClaims(this._private__id, []);
20903
+ return null;
20904
+ }
20905
+ // Published before the offsets are read so every line on the chart —
20906
+ // positions, pending orders, alerts — is fanned against the same set.
20907
+ this._private__labelLayout._internal_setClaims(this._private__id, claims);
20908
+ for (const level of levels) {
20909
+ level._internal_labelOffsetY = this._private__labelLayout._internal_offsetFor(this._private__id, level._internal_level);
20910
+ }
20911
+ return {
20912
+ _internal_levels: levels,
20913
+ _internal_background: this._private__theme.background ?? resolveBackgroundColor(layout.background),
20914
+ _internal_textColor: this._private__theme.textColor,
20915
+ _internal_badgeTextColor: this._private__theme.badgeTextColor,
20916
+ _internal_borderRadius: this._private__theme.borderRadius,
20917
+ _internal_borderWidth: this._private__theme.borderWidth,
20918
+ _internal_paddingX: this._private__theme.paddingX,
20919
+ _internal_paddingY: this._private__theme.paddingY,
20920
+ _internal_backgroundOpacity: this._private__theme.backgroundOpacity,
20921
+ _internal_font: makeFont(fontSize, this._private__theme.fontFamily ?? layout.fontFamily),
20922
+ _internal_fontSize: fontSize,
20923
+ _internal_connectorVisible: this._private__options.connectorVisible !== false && levels.length > 1,
20924
+ _internal_rightOffset: this._private__options.rightOffset ?? DEFAULT_RIGHT_OFFSET,
20925
+ _internal_hoveredLevel: this._private__hoveredLevel,
20926
+ _internal_hoveredPart: this._private__hoveredPart,
20927
+ };
20928
+ }
20929
+ }
20930
+ function mergeLevel(current, options, key) {
20931
+ if (!(key in options)) {
20932
+ return current;
20933
+ }
20934
+ const next = options[key];
20935
+ return next === undefined ? undefined : { ...current, ...next };
20936
+ }
20937
+ /**
20938
+ * Reads the chart's own background so the label sits on a solid surface. The
20939
+ * parameter is typed as the public {@link Background}: the production build
20940
+ * renames the members of structural types it does not see in the public API,
20941
+ * and an anonymous `{ color: string }` here resolved to nothing at runtime.
20942
+ */
20943
+ function resolveBackgroundColor(background) {
20944
+ return background.type === "solid" /* ColorType.Solid */ ? background.color : background.topColor;
20945
+ }
20946
+ /**
20947
+ * Creates a position line group: entry, take profit and stop loss drawn as one
20948
+ * interactive unit.
20949
+ *
20950
+ * @param series - The series the levels are priced against.
20951
+ * @param chart - The chart the series belongs to.
20952
+ * @param options - The levels and their style.
20953
+ *
20954
+ * @example
20955
+ * ```js
20956
+ * const position = createPositionLine(candleSeries, chart, {
20957
+ * entry: { price: 4306.79, badge: '1', formatText: () => '+ 2.32 USD' },
20958
+ * takeProfit: { price: 4349.58, badge: '1', text: '+ 42.79 USD' },
20959
+ * stopLoss: { price: 4266.06, badge: '1', text: '- 40.73 USD' },
20960
+ * onClose: (level) => console.log('close', level),
20961
+ * onDragEnd: (level, price) => console.log(level, 'moved to', price),
20962
+ * });
20963
+ * ```
20964
+ */
20965
+ function createPositionLine(series, chart, options) {
20966
+ return new PositionLine(chart, series, options);
20967
+ }
20968
+
19858
20969
  /** Hit parts that drag the whole object rather than a single handle. */
19859
20970
  const WHOLE_OBJECT_PARTS = new Set([
19860
20971
  'line',
@@ -20734,13 +21845,6 @@ class TrendLineTool extends DrawingToolBase {
20734
21845
  // Clean up references
20735
21846
  this._private__primitive = null;
20736
21847
  this._private__paneView = null;
20737
- // Force chart update by triggering a resize
20738
- const chartElement = this._chart.chartElement?.();
20739
- if (chartElement) {
20740
- const currentWidth = chartElement.clientWidth;
20741
- const currentHeight = chartElement.clientHeight;
20742
- this._chart.resize(currentWidth, currentHeight);
20743
- }
20744
21848
  }
20745
21849
  if (this._private__trendOptions.onRemove) {
20746
21850
  this._private__trendOptions.onRemove();
@@ -21013,12 +22117,6 @@ class HorizontalLineTool extends DrawingToolBase {
21013
22117
  this._series.detachPrimitive(this._private__primitive);
21014
22118
  this._private__primitive = null;
21015
22119
  this._private__paneView = null;
21016
- const chartElement = this._chart.chartElement?.();
21017
- if (chartElement) {
21018
- const currentWidth = chartElement.clientWidth;
21019
- const currentHeight = chartElement.clientHeight;
21020
- this._chart.resize(currentWidth, currentHeight);
21021
- }
21022
22120
  }
21023
22121
  if (this._private__hLineOptions.onRemove) {
21024
22122
  this._private__hLineOptions.onRemove();
@@ -22857,12 +23955,6 @@ class VerticalLineTool extends DrawingToolBase {
22857
23955
  this._series.detachPrimitive(this._private__primitive);
22858
23956
  this._private__primitive = null;
22859
23957
  this._private__paneView = null;
22860
- const chartElement = this._chart.chartElement?.();
22861
- if (chartElement) {
22862
- const currentWidth = chartElement.clientWidth;
22863
- const currentHeight = chartElement.clientHeight;
22864
- this._chart.resize(currentWidth, currentHeight);
22865
- }
22866
23958
  }
22867
23959
  if (this._private__vLineOptions.onRemove) {
22868
23960
  this._private__vLineOptions.onRemove();
@@ -23515,12 +24607,6 @@ class FibonacciTool extends DrawingToolBase {
23515
24607
  this._series.detachPrimitive(this._private__primitive);
23516
24608
  this._private__primitive = null;
23517
24609
  this._private__paneView = null;
23518
- const chartElement = this._chart.chartElement?.();
23519
- if (chartElement) {
23520
- const currentWidth = chartElement.clientWidth;
23521
- const currentHeight = chartElement.clientHeight;
23522
- this._chart.resize(currentWidth, currentHeight);
23523
- }
23524
24610
  }
23525
24611
  if (this._private__drawingManager) {
23526
24612
  this._private__drawingManager._internal_destroy();
@@ -23536,12 +24622,6 @@ class FibonacciTool extends DrawingToolBase {
23536
24622
  this._series.detachPrimitive(this._private__primitive);
23537
24623
  this._private__primitive = null;
23538
24624
  this._private__paneView = null;
23539
- const chartElement = this._chart.chartElement?.();
23540
- if (chartElement) {
23541
- const currentWidth = chartElement.clientWidth;
23542
- const currentHeight = chartElement.clientHeight;
23543
- this._chart.resize(currentWidth, currentHeight);
23544
- }
23545
24625
  }
23546
24626
  if (this._private__fibOptions.onRemove) {
23547
24627
  this._private__fibOptions.onRemove();
@@ -23969,12 +25049,6 @@ class FibonacciExtensionTool extends DrawingToolBase {
23969
25049
  this._series.detachPrimitive(this._private__primitive);
23970
25050
  this._private__primitive = null;
23971
25051
  this._private__paneView = null;
23972
- const chartElement = this._chart.chartElement?.();
23973
- if (chartElement) {
23974
- const currentWidth = chartElement.clientWidth;
23975
- const currentHeight = chartElement.clientHeight;
23976
- this._chart.resize(currentWidth, currentHeight);
23977
- }
23978
25052
  }
23979
25053
  }
23980
25054
  }
@@ -24416,12 +25490,6 @@ class FibonacciFanTool extends DrawingToolBase {
24416
25490
  this._series.detachPrimitive(this._private__primitive);
24417
25491
  this._private__primitive = null;
24418
25492
  this._private__paneView = null;
24419
- const chartElement = this._chart.chartElement?.();
24420
- if (chartElement) {
24421
- const currentWidth = chartElement.clientWidth;
24422
- const currentHeight = chartElement.clientHeight;
24423
- this._chart.resize(currentWidth, currentHeight);
24424
- }
24425
25493
  }
24426
25494
  if (this._private__fanOptions.onRemove) {
24427
25495
  this._private__fanOptions.onRemove();
@@ -24962,12 +26030,6 @@ class RectangleTool extends DrawingToolBase {
24962
26030
  this._series.detachPrimitive(this._private__primitive);
24963
26031
  this._private__primitive = null;
24964
26032
  this._private__paneView = null;
24965
- const chartElement = this._chart.chartElement?.();
24966
- if (chartElement) {
24967
- const currentWidth = chartElement.clientWidth;
24968
- const currentHeight = chartElement.clientHeight;
24969
- this._chart.resize(currentWidth, currentHeight);
24970
- }
24971
26033
  }
24972
26034
  if (this._private__rectOptions.onRemove) {
24973
26035
  this._private__rectOptions.onRemove();
@@ -25428,12 +26490,6 @@ class CircleTool extends DrawingToolBase {
25428
26490
  this._series.detachPrimitive(this._private__primitive);
25429
26491
  this._private__primitive = null;
25430
26492
  this._private__paneView = null;
25431
- const chartElement = this._chart.chartElement?.();
25432
- if (chartElement) {
25433
- const currentWidth = chartElement.clientWidth;
25434
- const currentHeight = chartElement.clientHeight;
25435
- this._chart.resize(currentWidth, currentHeight);
25436
- }
25437
26493
  }
25438
26494
  if (this._private__circleOptions.onRemove) {
25439
26495
  this._private__circleOptions.onRemove();
@@ -25983,12 +27039,6 @@ class ArrowTool extends DrawingToolBase {
25983
27039
  this._series.detachPrimitive(this._private__primitive);
25984
27040
  this._private__primitive = null;
25985
27041
  this._private__paneView = null;
25986
- const chartElement = this._chart.chartElement?.();
25987
- if (chartElement) {
25988
- const currentWidth = chartElement.clientWidth;
25989
- const currentHeight = chartElement.clientHeight;
25990
- this._chart.resize(currentWidth, currentHeight);
25991
- }
25992
27042
  }
25993
27043
  if (this._private__arrowOptions.onRemove) {
25994
27044
  this._private__arrowOptions.onRemove();
@@ -26849,12 +27899,6 @@ class TextTool extends DrawingToolBase {
26849
27899
  this._series.detachPrimitive(this._private__primitive);
26850
27900
  this._private__primitive = null;
26851
27901
  this._private__paneView = null;
26852
- const chartElement = this._chart.chartElement?.();
26853
- if (chartElement) {
26854
- const currentWidth = chartElement.clientWidth;
26855
- const currentHeight = chartElement.clientHeight;
26856
- this._chart.resize(currentWidth, currentHeight);
26857
- }
26858
27902
  }
26859
27903
  if (this._private__textOptions.onRemove) {
26860
27904
  this._private__textOptions.onRemove();
@@ -27437,12 +28481,6 @@ class RulerTool extends DrawingToolBase {
27437
28481
  this._series.detachPrimitive(this._private__primitive);
27438
28482
  this._private__primitive = null;
27439
28483
  this._private__paneView = null;
27440
- const chartElement = this._chart.chartElement?.();
27441
- if (chartElement) {
27442
- const currentWidth = chartElement.clientWidth;
27443
- const currentHeight = chartElement.clientHeight;
27444
- this._chart.resize(currentWidth, currentHeight);
27445
- }
27446
28484
  }
27447
28485
  if (this._private__drawingManager) {
27448
28486
  this._private__drawingManager._internal_destroy();
@@ -28298,12 +29336,6 @@ class PositionTool extends DrawingToolBase {
28298
29336
  this._series.detachPrimitive(this._private__primitive);
28299
29337
  this._private__primitive = null;
28300
29338
  this._private__paneView = null;
28301
- const chartElement = this._chart.chartElement?.();
28302
- if (chartElement) {
28303
- const currentWidth = chartElement.clientWidth;
28304
- const currentHeight = chartElement.clientHeight;
28305
- this._chart.resize(currentWidth, currentHeight);
28306
- }
28307
29339
  }
28308
29340
  if (this._private__positionOptions.onRemove) {
28309
29341
  this._private__positionOptions.onRemove();
@@ -28646,12 +29678,6 @@ class GanttTool extends DrawingToolBase {
28646
29678
  this._series.detachPrimitive(this._private__primitive);
28647
29679
  this._private__primitive = null;
28648
29680
  this._private__paneView = null;
28649
- const chartElement = this._chart.chartElement?.();
28650
- if (chartElement) {
28651
- const currentWidth = chartElement.clientWidth;
28652
- const currentHeight = chartElement.clientHeight;
28653
- this._chart.resize(currentWidth, currentHeight);
28654
- }
28655
29681
  }
28656
29682
  if (this._private__ganttOptions.onRemove) {
28657
29683
  this._private__ganttOptions.onRemove();
@@ -28662,6 +29688,8 @@ function createGanttTool(context, options) {
28662
29688
  return new GanttTool(context, options);
28663
29689
  }
28664
29690
 
29691
+ /** Undo steps kept, each a full snapshot of every drawing. */
29692
+ const UNDO_HISTORY_LIMIT = 100;
28665
29693
  /**
28666
29694
  * Central manager for all drawing tools - TradingView style
28667
29695
  * Handles multiple tools, selection, deletion, and keyboard shortcuts
@@ -28684,6 +29712,8 @@ class DrawingToolsManager {
28684
29712
  this._private__pointerDownHandler = null;
28685
29713
  this._private__pointerUpHandler = null;
28686
29714
  this._private__pointerCancelHandler = null;
29715
+ this._private__lostCaptureHandler = null;
29716
+ this._private__windowPointerUpHandler = null;
28687
29717
  this._private__pointerId = null;
28688
29718
  /** A press that grabbed a tool already selected it; its click must not undo that. */
28689
29719
  this._private__gestureHandled = false;
@@ -28849,6 +29879,9 @@ class DrawingToolsManager {
28849
29879
  return;
28850
29880
  }
28851
29881
  this._private__past.push(this._private__committed);
29882
+ if (this._private__past.length > UNDO_HISTORY_LIMIT) {
29883
+ this._private__past.shift();
29884
+ }
28852
29885
  this._private__future = [];
28853
29886
  this._private__committed = this.serializeAll();
28854
29887
  }
@@ -29228,8 +30261,12 @@ class DrawingToolsManager {
29228
30261
  this._private__completionCheck = null;
29229
30262
  }
29230
30263
  this._private__drawingCompleted._internal_destroy();
30264
+ this._private__selectionChanged._internal_destroy();
29231
30265
  this.clearAll();
29232
30266
  this._private__detachEventHandlers();
30267
+ this._private__past = [];
30268
+ this._private__future = [];
30269
+ this._private__committed = [];
29233
30270
  }
29234
30271
  _private__attachEventHandlers() {
29235
30272
  // Click handler for selection
@@ -29249,9 +30286,15 @@ class DrawingToolsManager {
29249
30286
  this._private__pointerDownHandler = (event) => this._private__handlePointerDown(event);
29250
30287
  this._private__pointerUpHandler = (event) => this._private__handlePointerUp(event);
29251
30288
  this._private__pointerCancelHandler = (event) => this._private__handlePointerUp(event);
30289
+ this._private__lostCaptureHandler = (event) => this._private__handleLostPointerCapture(event);
29252
30290
  chartElement.addEventListener('pointerdown', this._private__pointerDownHandler);
29253
30291
  chartElement.addEventListener('pointerup', this._private__pointerUpHandler);
29254
30292
  chartElement.addEventListener('pointercancel', this._private__pointerCancelHandler);
30293
+ chartElement.addEventListener('lostpointercapture', this._private__lostCaptureHandler);
30294
+ }
30295
+ if (isRunningOnClientSide) {
30296
+ this._private__windowPointerUpHandler = (event) => this._private__handlePointerUp(event);
30297
+ window.addEventListener('pointerup', this._private__windowPointerUpHandler);
29255
30298
  }
29256
30299
  // Keyboard handler for shortcuts
29257
30300
  if (this._private__options.enableKeyboardShortcuts) {
@@ -29284,6 +30327,14 @@ class DrawingToolsManager {
29284
30327
  chartElement.removeEventListener('pointercancel', this._private__pointerCancelHandler);
29285
30328
  this._private__pointerCancelHandler = null;
29286
30329
  }
30330
+ if (this._private__lostCaptureHandler) {
30331
+ chartElement.removeEventListener('lostpointercapture', this._private__lostCaptureHandler);
30332
+ this._private__lostCaptureHandler = null;
30333
+ }
30334
+ }
30335
+ if (this._private__windowPointerUpHandler) {
30336
+ window.removeEventListener('pointerup', this._private__windowPointerUpHandler);
30337
+ this._private__windowPointerUpHandler = null;
29287
30338
  }
29288
30339
  if (this._private__keyboardHandler) {
29289
30340
  document.removeEventListener('keydown', this._private__keyboardHandler);
@@ -29409,7 +30460,12 @@ class DrawingToolsManager {
29409
30460
  this._private__gestureHandled = true;
29410
30461
  this._private__draggedToolId = toolId;
29411
30462
  this._private__pointerId = event.pointerId;
29412
- chartElement.setPointerCapture(event.pointerId);
30463
+ try {
30464
+ chartElement.setPointerCapture(event.pointerId);
30465
+ }
30466
+ catch (error) {
30467
+ this._private__pointerId = null;
30468
+ }
29413
30469
  tool.startDrag(endpoint, anchor);
29414
30470
  // Panning is stopped by disabling it for the gesture, not by preventing
29415
30471
  // the event: preventDefault on pointerdown suppresses the compatibility
@@ -29423,17 +30479,32 @@ class DrawingToolsManager {
29423
30479
  if (event && this._private__pointerId !== null && event.pointerId !== this._private__pointerId) {
29424
30480
  return;
29425
30481
  }
29426
- const tool = this._private__tools.get(this._private__draggedToolId);
29427
- if (tool) {
29428
- tool.stopDrag();
29429
- }
29430
- this._private__releasePointer();
29431
- this._private__enableChartInteractions();
30482
+ const draggedToolId = this._private__draggedToolId;
30483
+ // Cleared first: anything below can throw, and a half-finished release
30484
+ // would leave the manager dragging forever.
29432
30485
  this._private__isDragging = false;
29433
30486
  this._private__draggedToolId = null;
30487
+ try {
30488
+ const tool = this._private__tools.get(draggedToolId);
30489
+ if (tool) {
30490
+ tool.stopDrag();
30491
+ }
30492
+ }
30493
+ finally {
30494
+ this._private__releasePointer();
30495
+ this._private__enableChartInteractions();
30496
+ }
29434
30497
  // A finished drag is one step, not one per frame
29435
30498
  this.recordChange();
29436
30499
  }
30500
+ /** Ends the drag when capture is lost without a pointerup ever arriving. */
30501
+ _private__handleLostPointerCapture(event) {
30502
+ if (this._private__pointerId !== null && event.pointerId !== this._private__pointerId) {
30503
+ return;
30504
+ }
30505
+ this._private__pointerId = null;
30506
+ this._private__handlePointerUp();
30507
+ }
29437
30508
  /**
29438
30509
  * Pane element of a tool's series. The engine asserts the series has a pane,
29439
30510
  * so the lookup is guarded.
@@ -29484,9 +30555,11 @@ class DrawingToolsManager {
29484
30555
  return;
29485
30556
  }
29486
30557
  const options = this._private__chart.options();
30558
+ // `options()` returns the live object and `applyOptions` merges into it
30559
+ // in place, so a snapshot by reference restores all-false.
29487
30560
  this._private__interactionsBeforeDrag = {
29488
- _internal_handleScroll: options.handleScroll,
29489
- _internal_handleScale: options.handleScale,
30561
+ _internal_handleScroll: clone(options.handleScroll),
30562
+ _internal_handleScale: clone(options.handleScale),
29490
30563
  };
29491
30564
  this._private__chart.applyOptions({
29492
30565
  handleScroll: false,
@@ -29572,7 +30645,7 @@ const customSeriesDefaultOptions = {
29572
30645
  * Returns the current version as a string. For example `'1.0.0'`.
29573
30646
  */
29574
30647
  function version() {
29575
- return "2.2.1";
30648
+ return "2.3.1";
29576
30649
  }
29577
30650
 
29578
- export { areaSeries as AreaSeries, ArrowTool, barSeries as BarSeries, baselineSeries as BaselineSeries, BrushTool, candlestickSeries as CandlestickSeries, CircleTool, ColorType, CrossLineTool, CrosshairMode, DEFAULT_SESSIONS, DraggablePriceLine, DrawingToolsManager, EXTENDED_FIBONACCI_RATIOS, FibonacciCalculator, FibonacciExtensionTool, FibonacciFanTool, FibonacciTool, GanttTool, histogramSeries as HistogramSeries, HorizontalLineTool, InteractiveLineManager, LastPriceAnimationMode, lineSeries as LineSeries, LineStyle, LineType, MismatchDirection, ParallelChannelTool, PitchforkTool, PolygonTool, PositionCalculator, PositionMarkersManager, PositionTool, PriceLineSource, PriceScaleMode, RectangleTool, RulerCalculator, RulerTool, STANDARD_FIBONACCI_RATIOS, TextTool, TickMarkType, TrackingModeExitMode, TradingSessionsIndicator, TrendLineTool, VerticalLineTool, VolumeProfileIndicator, applyADX, applyATR, applyBollingerBands, applyCCI, applyDonchianChannels, applyEMA, applyHeikinAshi, applyIchimoku, applyKeltnerChannels, applyMACD, applyMFI, applyOBV, applyParabolicSAR, applyPivotPoints, applyRSI, applySMA, applyStochastic, applySuperTrend, applyVWAP, applyVolume, applyWMA, applyWilliamsR, calculateHeikinAshi, calculatePipConfig, coordinateFromTime, createArrowTool, createBrushTool, createChart, createChartEx, createCircleTool, createCrossLineTool, createFibonacciExtensionTool, createFibonacciFanTool, createFibonacciTool, createGanttTool, createHorizontalLineTool, createImageWatermark, createInteractiveLineManager, createOptionsChart, createParallelChannelTool, createPitchforkTool, createPolygonTool, createPositionMarkers, createPositionTool, createRectangleTool, createRulerTool, createSeriesMarkers, createTextTool, createTextWatermark, createTradingLine, createTradingSessionsIndicator, createTrendLineTool, createUpDownMarkers, createVerticalLineTool, createVolumeProfileIndicator, createYieldCurveChart, customSeriesDefaultOptions, defaultHorzScaleBehavior, getPipConfig, hideSeriesPaint, isBusinessDay, isUTCTimestamp, setSeriesVolumes, setVolumeProfileData, timeFromCoordinate, version };
30651
+ export { areaSeries as AreaSeries, ArrowTool, barSeries as BarSeries, baselineSeries as BaselineSeries, BrushTool, candlestickSeries as CandlestickSeries, CircleTool, ColorType, CrossLineTool, CrosshairMode, DEFAULT_SESSIONS, DraggablePriceLine, DrawingToolsManager, EXTENDED_FIBONACCI_RATIOS, FibonacciCalculator, FibonacciExtensionTool, FibonacciFanTool, FibonacciTool, GanttTool, histogramSeries as HistogramSeries, HorizontalLineTool, InteractiveLineManager, LastPriceAnimationMode, lineSeries as LineSeries, LineStyle, LineType, MismatchDirection, ParallelChannelTool, PitchforkTool, PolygonTool, PositionCalculator, PositionLine, PositionMarkersManager, PositionTool, PriceLineSource, PriceScaleMode, RectangleTool, RulerCalculator, RulerTool, STANDARD_FIBONACCI_RATIOS, TextTool, TickMarkType, TrackingModeExitMode, TradingSessionsIndicator, TrendLineTool, VerticalLineTool, VolumeProfileIndicator, applyADX, applyATR, applyBollingerBands, applyCCI, applyDonchianChannels, applyEMA, applyHeikinAshi, applyIchimoku, applyKeltnerChannels, applyMACD, applyMFI, applyOBV, applyParabolicSAR, applyPivotPoints, applyRSI, applySMA, applyStochastic, applySuperTrend, applyVWAP, applyVolume, applyWMA, applyWilliamsR, calculateHeikinAshi, calculatePipConfig, coordinateFromTime, createArrowTool, createBrushTool, createChart, createChartEx, createCircleTool, createCrossLineTool, createFibonacciExtensionTool, createFibonacciFanTool, createFibonacciTool, createGanttTool, createHorizontalLineTool, createImageWatermark, createInteractiveLineManager, createOptionsChart, createParallelChannelTool, createPitchforkTool, createPolygonTool, createPositionLine, createPositionMarkers, createPositionTool, createRectangleTool, createRulerTool, createSeriesMarkers, createTextTool, createTextWatermark, createTradingLine, createTradingSessionsIndicator, createTrendLineTool, createUpDownMarkers, createVerticalLineTool, createVolumeProfileIndicator, createYieldCurveChart, customSeriesDefaultOptions, defaultHorzScaleBehavior, getPipConfig, hideSeriesPaint, isBusinessDay, isUTCTimestamp, setSeriesVolumes, setVolumeProfileData, timeFromCoordinate, version };