@pipsend/charts 2.3.0 → 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.3.0-dev+202609220108
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)
@@ -19744,6 +19744,8 @@ class TradingLineController {
19744
19744
  this._private__pointerId = null;
19745
19745
  this._private__pressOrigin = null;
19746
19746
  this._private__pressedLine = null;
19747
+ /** Distance from the pointer to the dragged line when the press landed. */
19748
+ this._private__grabOffset = 0;
19747
19749
  /** Latest pointer position in pane coordinates, or null when off the panes. */
19748
19750
  this._private__point = null;
19749
19751
  this._private__paneIndex = undefined;
@@ -19821,6 +19823,7 @@ class TradingLineController {
19821
19823
  this._private__pointerId = event.pointerId;
19822
19824
  this._private__element.setPointerCapture(event.pointerId);
19823
19825
  line.beginDrag();
19826
+ this._private__grabOffset = this._private__measureGrabOffset(line, event);
19824
19827
  this._private__applyCursor();
19825
19828
  event.preventDefault();
19826
19829
  event.stopPropagation();
@@ -19855,6 +19858,7 @@ class TradingLineController {
19855
19858
  Math.abs(event.clientY - origin._internal_y) > CLICK_TOLERANCE_PX);
19856
19859
  if (dragged !== null) {
19857
19860
  this._private__dragging = null;
19861
+ this._private__grabOffset = 0;
19858
19862
  if (moved) {
19859
19863
  dragged.commitDrag();
19860
19864
  }
@@ -19882,6 +19886,7 @@ class TradingLineController {
19882
19886
  }
19883
19887
  const line = this._private__dragging;
19884
19888
  this._private__dragging = null;
19889
+ this._private__grabOffset = 0;
19885
19890
  this._private__releasePointer();
19886
19891
  line.cancelDrag();
19887
19892
  this._private__applyCursor();
@@ -19911,16 +19916,38 @@ class TradingLineController {
19911
19916
  // as the pointer leaves the pane canvas — over the price axis, past the
19912
19917
  // chart's edge — and a drag driven from it freezes there while the user
19913
19918
  // is still moving.
19919
+ const paneY = this._private__paneY(line, event);
19920
+ return paneY === null ? null : line.priceAt(paneY - this._private__grabOffset);
19921
+ }
19922
+ /** The pointer's position inside the line's pane, in pane coordinates. */
19923
+ _private__paneY(line, event) {
19914
19924
  const pane = this._private__chart.panes()[line.paneIndex()];
19915
19925
  const paneElement = pane ? pane.getHTMLElement() : null;
19916
19926
  if (paneElement) {
19917
- return line.priceAt(event.clientY - paneElement.getBoundingClientRect().top);
19927
+ return event.clientY - paneElement.getBoundingClientRect().top;
19918
19928
  }
19919
19929
  if (this._private__point !== null && this._private__paneIndex === line.paneIndex()) {
19920
- return line.priceAt(this._private__point._internal_y);
19930
+ return this._private__point._internal_y;
19921
19931
  }
19922
19932
  return null;
19923
19933
  }
19934
+ /**
19935
+ * Keeps the grab point: a line picked up by the edge of its label moves with
19936
+ * the pointer rather than centring itself on it. Ignored past a sane
19937
+ * distance, so a bad reading cannot send the line off on the first move.
19938
+ */
19939
+ _private__measureGrabOffset(line, event) {
19940
+ if (!line.dragAnchorY) {
19941
+ return 0;
19942
+ }
19943
+ const anchor = line.dragAnchorY();
19944
+ const paneY = this._private__paneY(line, event);
19945
+ if (anchor === null || paneY === null) {
19946
+ return 0;
19947
+ }
19948
+ const offset = paneY - anchor;
19949
+ return Math.abs(offset) > 40 /* Constants.MaxGrabOffsetPx */ ? 0 : offset;
19950
+ }
19924
19951
  /**
19925
19952
  * Hit ids are either a bare line id or `id#part`, which is how a line with
19926
19953
  * several targets reports which one was hit.
@@ -20476,6 +20503,126 @@ function createInteractiveLineManager(chart, series) {
20476
20503
  return new InteractiveLineManager(chart, series);
20477
20504
  }
20478
20505
 
20506
+ /**
20507
+ * Fans out labels that land on top of each other.
20508
+ *
20509
+ * Every position line on a chart shares one of these. Two positions opened at
20510
+ * the same price draw their labels at the same coordinate, and the one behind
20511
+ * becomes unreachable — the engine resolves the hit to whichever primitive it
20512
+ * reaches first, and that is always the same one. Nudging each collision down
20513
+ * leaves every label a strip of its own to be grabbed by.
20514
+ *
20515
+ * The line itself never moves: it stays at the price it belongs to, and only
20516
+ * the label is offset.
20517
+ */
20518
+ class PositionLineLabelLayout {
20519
+ constructor() {
20520
+ /** Registration order. It is what keeps the fan from reshuffling per frame. */
20521
+ this._private__order = [];
20522
+ this._private__claims = new Map();
20523
+ this._private__offsets = new Map();
20524
+ this._private__invalidated = true;
20525
+ }
20526
+ _internal_register(ownerId) {
20527
+ if (!this._private__claims.has(ownerId)) {
20528
+ this._private__order.push(ownerId);
20529
+ this._private__claims.set(ownerId, []);
20530
+ this._private__invalidated = true;
20531
+ }
20532
+ }
20533
+ _internal_unregister(ownerId) {
20534
+ const index = this._private__order.indexOf(ownerId);
20535
+ if (index !== -1) {
20536
+ this._private__order.splice(index, 1);
20537
+ }
20538
+ this._private__claims.delete(ownerId);
20539
+ this._private__invalidated = true;
20540
+ }
20541
+ _internal_isEmpty() {
20542
+ return this._private__order.length === 0;
20543
+ }
20544
+ /**
20545
+ * Publishes where an owner's labels want to sit. Called while each line
20546
+ * rebuilds its renderer data, so a fan can be one frame behind while prices
20547
+ * move — which is invisible, and cheaper than forcing a second pass.
20548
+ */
20549
+ _internal_setClaims(ownerId, claims) {
20550
+ this._private__claims.set(ownerId, claims);
20551
+ this._private__invalidated = true;
20552
+ }
20553
+ /** How far down a label has to move to clear the ones already placed. */
20554
+ _internal_offsetFor(ownerId, level) {
20555
+ if (this._private__invalidated) {
20556
+ this._private__recalculate();
20557
+ }
20558
+ return this._private__offsets.get(key(ownerId, level)) ?? 0;
20559
+ }
20560
+ _private__recalculate() {
20561
+ const offsets = new Map();
20562
+ const placed = [];
20563
+ for (const ownerId of this._private__order) {
20564
+ for (const claim of this._private__claims.get(ownerId) ?? []) {
20565
+ const gap = minGap(claim._internal_height);
20566
+ const maxOffset = claim._internal_height;
20567
+ // Moved down to the first coordinate that clears everything already
20568
+ // placed, and no further. Two levels whose own prices nearly part
20569
+ // them only need the few pixels they are short of — stepping by a
20570
+ // full gap each time is what made the fan look flung open.
20571
+ let y = claim._internal_y;
20572
+ for (const other of [...placed].sort(byY)) {
20573
+ if (Math.abs(y - other._internal_y) < gap) {
20574
+ y = other._internal_y + gap;
20575
+ }
20576
+ }
20577
+ let offset = y - claim._internal_y;
20578
+ if (offset > maxOffset) {
20579
+ // More labels than there is room to fan. The rest stack, which is
20580
+ // no worse than before and keeps the pile from running off.
20581
+ offset = 0;
20582
+ }
20583
+ placed.push({ _internal_y: claim._internal_y + offset, _internal_height: claim._internal_height });
20584
+ offsets.set(key(ownerId, claim._internal_level), offset);
20585
+ }
20586
+ }
20587
+ this._private__offsets = offsets;
20588
+ this._private__invalidated = false;
20589
+ }
20590
+ }
20591
+ function key(ownerId, level) {
20592
+ return `${ownerId}:${level}`;
20593
+ }
20594
+ /**
20595
+ * The strip a label keeps to itself. Comfortably clickable, and no wider —
20596
+ * every pixel here is a pixel the label sits away from its own line.
20597
+ */
20598
+ function minGap(height) {
20599
+ return Math.max(9, Math.round(height * 0.4));
20600
+ }
20601
+ function byY(a, b) {
20602
+ return a._internal_y - b._internal_y;
20603
+ }
20604
+ const layouts = new WeakMap();
20605
+ /** The layout is shared by every position line on the same chart. */
20606
+ function acquireLabelLayout(chart, ownerId) {
20607
+ let layout = layouts.get(chart);
20608
+ if (!layout) {
20609
+ layout = new PositionLineLabelLayout();
20610
+ layouts.set(chart, layout);
20611
+ }
20612
+ layout._internal_register(ownerId);
20613
+ return layout;
20614
+ }
20615
+ function releaseLabelLayout(chart, ownerId) {
20616
+ const layout = layouts.get(chart);
20617
+ if (!layout) {
20618
+ return;
20619
+ }
20620
+ layout._internal_unregister(ownerId);
20621
+ if (layout._internal_isEmpty()) {
20622
+ layouts.delete(chart);
20623
+ }
20624
+ }
20625
+
20479
20626
  const positionLineDefaultColors = {
20480
20627
  entry: '#2962FF',
20481
20628
  takeProfit: '#26A17B',
@@ -20508,12 +20655,12 @@ class PositionLineRenderer {
20508
20655
  // line, so the narrowest target is tested first.
20509
20656
  for (const layout of this._private__layouts) {
20510
20657
  if (layout._internal_closable && x >= layout._internal_closeLeft && x <= layout._internal_closeRight &&
20511
- Math.abs(y - layout._internal_y) <= layout._internal_height / 2) {
20658
+ Math.abs(y - layout._internal_labelY) <= layout._internal_height / 2) {
20512
20659
  return { _internal_level: layout._internal_level, _internal_part: 'close' };
20513
20660
  }
20514
20661
  }
20515
20662
  for (const layout of this._private__layouts) {
20516
- if (x >= layout._internal_pillLeft && x <= layout._internal_pillRight && Math.abs(y - layout._internal_y) <= layout._internal_height / 2) {
20663
+ if (x >= layout._internal_pillLeft && x <= layout._internal_pillRight && Math.abs(y - layout._internal_labelY) <= layout._internal_height / 2) {
20517
20664
  return { _internal_level: layout._internal_level, _internal_part: 'body' };
20518
20665
  }
20519
20666
  }
@@ -20563,7 +20710,8 @@ class PositionLineRenderer {
20563
20710
  const pillWidth = badgeWidth + labelWidth + closeWidth;
20564
20711
  const pillRight = Math.max(pillWidth, lineEnd - 8 /* Constants.LabelGap */);
20565
20712
  const pillLeft = pillRight - pillWidth;
20566
- const top = level._internal_y - height / 2;
20713
+ const labelY = level._internal_y + level._internal_labelOffsetY;
20714
+ const top = labelY - height / 2;
20567
20715
  this._private__drawLineSegments(scope, level, pillLeft, pillRight, lineEnd);
20568
20716
  this._private__drawPill(scope, data, level, { _internal_left: pillLeft, _internal_top: top, _internal_width: pillWidth, _internal_height: height });
20569
20717
  let cellLeft = pillLeft;
@@ -20576,7 +20724,7 @@ class PositionLineRenderer {
20576
20724
  ctx.scale(hpr, vpr);
20577
20725
  ctx.textAlign = 'center';
20578
20726
  ctx.fillStyle = data._internal_textColor ?? level._internal_color;
20579
- ctx.fillText(level._internal_text, cellLeft + labelWidth / 2, level._internal_y);
20727
+ ctx.fillText(level._internal_text, cellLeft + labelWidth / 2, labelY);
20580
20728
  ctx.restore();
20581
20729
  cellLeft += labelWidth;
20582
20730
  if (level._internal_closable) {
@@ -20586,6 +20734,7 @@ class PositionLineRenderer {
20586
20734
  return {
20587
20735
  _internal_level: level._internal_level,
20588
20736
  _internal_y: level._internal_y,
20737
+ _internal_labelY: labelY,
20589
20738
  _internal_height: height,
20590
20739
  _internal_pillLeft: pillLeft,
20591
20740
  _internal_pillRight: pillRight,
@@ -20604,12 +20753,20 @@ class PositionLineRenderer {
20604
20753
  ctx.lineWidth = lineWidth;
20605
20754
  setLineStyle(ctx, level._internal_lineStyle);
20606
20755
  ctx.beginPath();
20607
- ctx.moveTo(0, y);
20608
- ctx.lineTo(Math.round(Math.max(0, pillLeft - 8 /* Constants.LabelGap */) * hpr), y);
20609
- if (lineEnd > pillRight + 8 /* Constants.LabelGap */) {
20610
- ctx.moveTo(Math.round((pillRight + 8 /* Constants.LabelGap */) * hpr), y);
20756
+ if (level._internal_labelOffsetY !== 0) {
20757
+ // The label has been fanned off its line, so breaking the line for it
20758
+ // would leave a gap with nothing in it.
20759
+ ctx.moveTo(0, y);
20611
20760
  ctx.lineTo(Math.round(lineEnd * hpr), y);
20612
20761
  }
20762
+ else {
20763
+ ctx.moveTo(0, y);
20764
+ ctx.lineTo(Math.round(Math.max(0, pillLeft - 8 /* Constants.LabelGap */) * hpr), y);
20765
+ if (lineEnd > pillRight + 8 /* Constants.LabelGap */) {
20766
+ ctx.moveTo(Math.round((pillRight + 8 /* Constants.LabelGap */) * hpr), y);
20767
+ ctx.lineTo(Math.round(lineEnd * hpr), y);
20768
+ }
20769
+ }
20613
20770
  ctx.stroke();
20614
20771
  ctx.restore();
20615
20772
  }
@@ -20804,6 +20961,7 @@ class PositionLine {
20804
20961
  this._private__theme = { ...positionLineThemeDefaults, ...options.theme };
20805
20962
  this._private__paneViews = [this._private__paneView];
20806
20963
  this._private__axisViews = positionLineLevels.map((level) => new PositionLineAxisView(() => this._private__axisLabel(level)));
20964
+ this._private__labelLayout = acquireLabelLayout(this._private__chart, this._private__id);
20807
20965
  this._private__series.attachPrimitive(this);
20808
20966
  acquireController(this._private__chart)?._internal_register(this);
20809
20967
  }
@@ -20890,6 +21048,23 @@ class PositionLine {
20890
21048
  const price = this._private__series.coordinateToPrice(paneY);
20891
21049
  return price === null ? null : price;
20892
21050
  }
21051
+ /**
21052
+ * Where the level being dragged actually sits. The controller measures the
21053
+ * pointer against it so the level moves by what the pointer moved, instead
21054
+ * of jumping to sit under it — which matters most when the label has been
21055
+ * fanned away from its own line.
21056
+ */
21057
+ dragAnchorY() {
21058
+ if (this._private__dragLevel === null) {
21059
+ return null;
21060
+ }
21061
+ const price = this._private__levelOptions(this._private__dragLevel)?.price;
21062
+ if (price === undefined || !isFinite(price)) {
21063
+ return null;
21064
+ }
21065
+ const y = this._private__series.priceToCoordinate(price);
21066
+ return y === null ? null : y;
21067
+ }
20893
21068
  beginDrag() {
20894
21069
  this._private__dragLevel = this._private__hoveredLevel;
20895
21070
  if (this._private__dragLevel === null) {
@@ -21038,6 +21213,7 @@ class PositionLine {
21038
21213
  */
21039
21214
  remove() {
21040
21215
  releaseController(this._private__chart, this);
21216
+ releaseLabelLayout(this._private__chart, this._private__id);
21041
21217
  this._private__series.detachPrimitive(this);
21042
21218
  }
21043
21219
  requestUpdate() {
@@ -21083,7 +21259,11 @@ class PositionLine {
21083
21259
  if (!this._private__attached || this._private__options.visible === false) {
21084
21260
  return null;
21085
21261
  }
21262
+ const layout = this._private__chart.options()['layout'];
21263
+ const fontSize = this._private__theme.fontSize ?? layout.fontSize;
21264
+ const labelHeight = fontSize + this._private__theme.paddingY * 2;
21086
21265
  const levels = [];
21266
+ const claims = [];
21087
21267
  for (const level of positionLineLevels) {
21088
21268
  const options = this._private__levelOptions(level);
21089
21269
  // A level added through a patch can be missing its price until the
@@ -21096,9 +21276,11 @@ class PositionLine {
21096
21276
  continue;
21097
21277
  }
21098
21278
  const isEntry = level === 'entry';
21279
+ claims.push({ _internal_level: level, _internal_y: y, _internal_height: labelHeight });
21099
21280
  levels.push({
21100
21281
  _internal_level: level,
21101
21282
  _internal_y: y,
21283
+ _internal_labelOffsetY: 0,
21102
21284
  _internal_color: options.color ?? positionLineDefaultColors[level],
21103
21285
  _internal_text: options.formatText ? options.formatText(options.price) : options.text ?? '',
21104
21286
  _internal_badge: options.badge ?? '',
@@ -21110,10 +21292,15 @@ class PositionLine {
21110
21292
  });
21111
21293
  }
21112
21294
  if (levels.length === 0) {
21295
+ this._private__labelLayout._internal_setClaims(this._private__id, []);
21113
21296
  return null;
21114
21297
  }
21115
- const layout = this._private__chart.options()['layout'];
21116
- const fontSize = this._private__theme.fontSize ?? layout.fontSize;
21298
+ // Published before the offsets are read so every line on the chart —
21299
+ // positions, pending orders, alerts — is fanned against the same set.
21300
+ this._private__labelLayout._internal_setClaims(this._private__id, claims);
21301
+ for (const level of levels) {
21302
+ level._internal_labelOffsetY = this._private__labelLayout._internal_offsetFor(this._private__id, level._internal_level);
21303
+ }
21117
21304
  return {
21118
21305
  _internal_levels: levels,
21119
21306
  _internal_background: this._private__theme.background ?? resolveBackgroundColor(layout.background),
@@ -30851,7 +31038,7 @@ const customSeriesDefaultOptions = {
30851
31038
  * Returns the current version as a string. For example `'1.0.0'`.
30852
31039
  */
30853
31040
  function version() {
30854
- return "2.3.0-dev+202609220108";
31041
+ return "2.3.1";
30855
31042
  }
30856
31043
 
30857
31044
  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 };