@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)
@@ -19351,6 +19351,8 @@ class TradingLineController {
19351
19351
  this._private__pointerId = null;
19352
19352
  this._private__pressOrigin = null;
19353
19353
  this._private__pressedLine = null;
19354
+ /** Distance from the pointer to the dragged line when the press landed. */
19355
+ this._private__grabOffset = 0;
19354
19356
  /** Latest pointer position in pane coordinates, or null when off the panes. */
19355
19357
  this._private__point = null;
19356
19358
  this._private__paneIndex = undefined;
@@ -19428,6 +19430,7 @@ class TradingLineController {
19428
19430
  this._private__pointerId = event.pointerId;
19429
19431
  this._private__element.setPointerCapture(event.pointerId);
19430
19432
  line.beginDrag();
19433
+ this._private__grabOffset = this._private__measureGrabOffset(line, event);
19431
19434
  this._private__applyCursor();
19432
19435
  event.preventDefault();
19433
19436
  event.stopPropagation();
@@ -19462,6 +19465,7 @@ class TradingLineController {
19462
19465
  Math.abs(event.clientY - origin._internal_y) > CLICK_TOLERANCE_PX);
19463
19466
  if (dragged !== null) {
19464
19467
  this._private__dragging = null;
19468
+ this._private__grabOffset = 0;
19465
19469
  if (moved) {
19466
19470
  dragged.commitDrag();
19467
19471
  }
@@ -19489,6 +19493,7 @@ class TradingLineController {
19489
19493
  }
19490
19494
  const line = this._private__dragging;
19491
19495
  this._private__dragging = null;
19496
+ this._private__grabOffset = 0;
19492
19497
  this._private__releasePointer();
19493
19498
  line.cancelDrag();
19494
19499
  this._private__applyCursor();
@@ -19518,16 +19523,38 @@ class TradingLineController {
19518
19523
  // as the pointer leaves the pane canvas — over the price axis, past the
19519
19524
  // chart's edge — and a drag driven from it freezes there while the user
19520
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) {
19521
19531
  const pane = this._private__chart.panes()[line.paneIndex()];
19522
19532
  const paneElement = pane ? pane.getHTMLElement() : null;
19523
19533
  if (paneElement) {
19524
- return line.priceAt(event.clientY - paneElement.getBoundingClientRect().top);
19534
+ return event.clientY - paneElement.getBoundingClientRect().top;
19525
19535
  }
19526
19536
  if (this._private__point !== null && this._private__paneIndex === line.paneIndex()) {
19527
- return line.priceAt(this._private__point._internal_y);
19537
+ return this._private__point._internal_y;
19528
19538
  }
19529
19539
  return null;
19530
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;
19554
+ }
19555
+ const offset = paneY - anchor;
19556
+ return Math.abs(offset) > 40 /* Constants.MaxGrabOffsetPx */ ? 0 : offset;
19557
+ }
19531
19558
  /**
19532
19559
  * Hit ids are either a bare line id or `id#part`, which is how a line with
19533
19560
  * several targets reports which one was hit.
@@ -20083,6 +20110,126 @@ function createInteractiveLineManager(chart, series) {
20083
20110
  return new InteractiveLineManager(chart, series);
20084
20111
  }
20085
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
+
20086
20233
  const positionLineDefaultColors = {
20087
20234
  entry: '#2962FF',
20088
20235
  takeProfit: '#26A17B',
@@ -20115,12 +20262,12 @@ class PositionLineRenderer {
20115
20262
  // line, so the narrowest target is tested first.
20116
20263
  for (const layout of this._private__layouts) {
20117
20264
  if (layout._internal_closable && x >= layout._internal_closeLeft && x <= layout._internal_closeRight &&
20118
- Math.abs(y - layout._internal_y) <= layout._internal_height / 2) {
20265
+ Math.abs(y - layout._internal_labelY) <= layout._internal_height / 2) {
20119
20266
  return { _internal_level: layout._internal_level, _internal_part: 'close' };
20120
20267
  }
20121
20268
  }
20122
20269
  for (const layout of this._private__layouts) {
20123
- if (x >= layout._internal_pillLeft && x <= layout._internal_pillRight && Math.abs(y - layout._internal_y) <= layout._internal_height / 2) {
20270
+ if (x >= layout._internal_pillLeft && x <= layout._internal_pillRight && Math.abs(y - layout._internal_labelY) <= layout._internal_height / 2) {
20124
20271
  return { _internal_level: layout._internal_level, _internal_part: 'body' };
20125
20272
  }
20126
20273
  }
@@ -20170,7 +20317,8 @@ class PositionLineRenderer {
20170
20317
  const pillWidth = badgeWidth + labelWidth + closeWidth;
20171
20318
  const pillRight = Math.max(pillWidth, lineEnd - 8 /* Constants.LabelGap */);
20172
20319
  const pillLeft = pillRight - pillWidth;
20173
- const top = level._internal_y - height / 2;
20320
+ const labelY = level._internal_y + level._internal_labelOffsetY;
20321
+ const top = labelY - height / 2;
20174
20322
  this._private__drawLineSegments(scope, level, pillLeft, pillRight, lineEnd);
20175
20323
  this._private__drawPill(scope, data, level, { _internal_left: pillLeft, _internal_top: top, _internal_width: pillWidth, _internal_height: height });
20176
20324
  let cellLeft = pillLeft;
@@ -20183,7 +20331,7 @@ class PositionLineRenderer {
20183
20331
  ctx.scale(hpr, vpr);
20184
20332
  ctx.textAlign = 'center';
20185
20333
  ctx.fillStyle = data._internal_textColor ?? level._internal_color;
20186
- ctx.fillText(level._internal_text, cellLeft + labelWidth / 2, level._internal_y);
20334
+ ctx.fillText(level._internal_text, cellLeft + labelWidth / 2, labelY);
20187
20335
  ctx.restore();
20188
20336
  cellLeft += labelWidth;
20189
20337
  if (level._internal_closable) {
@@ -20193,6 +20341,7 @@ class PositionLineRenderer {
20193
20341
  return {
20194
20342
  _internal_level: level._internal_level,
20195
20343
  _internal_y: level._internal_y,
20344
+ _internal_labelY: labelY,
20196
20345
  _internal_height: height,
20197
20346
  _internal_pillLeft: pillLeft,
20198
20347
  _internal_pillRight: pillRight,
@@ -20211,12 +20360,20 @@ class PositionLineRenderer {
20211
20360
  ctx.lineWidth = lineWidth;
20212
20361
  setLineStyle(ctx, level._internal_lineStyle);
20213
20362
  ctx.beginPath();
20214
- ctx.moveTo(0, y);
20215
- ctx.lineTo(Math.round(Math.max(0, pillLeft - 8 /* Constants.LabelGap */) * hpr), y);
20216
- if (lineEnd > pillRight + 8 /* Constants.LabelGap */) {
20217
- ctx.moveTo(Math.round((pillRight + 8 /* Constants.LabelGap */) * hpr), y);
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);
20218
20367
  ctx.lineTo(Math.round(lineEnd * hpr), y);
20219
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
+ }
20220
20377
  ctx.stroke();
20221
20378
  ctx.restore();
20222
20379
  }
@@ -20411,6 +20568,7 @@ class PositionLine {
20411
20568
  this._private__theme = { ...positionLineThemeDefaults, ...options.theme };
20412
20569
  this._private__paneViews = [this._private__paneView];
20413
20570
  this._private__axisViews = positionLineLevels.map((level) => new PositionLineAxisView(() => this._private__axisLabel(level)));
20571
+ this._private__labelLayout = acquireLabelLayout(this._private__chart, this._private__id);
20414
20572
  this._private__series.attachPrimitive(this);
20415
20573
  acquireController(this._private__chart)?._internal_register(this);
20416
20574
  }
@@ -20497,6 +20655,23 @@ class PositionLine {
20497
20655
  const price = this._private__series.coordinateToPrice(paneY);
20498
20656
  return price === null ? null : price;
20499
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
+ }
20500
20675
  beginDrag() {
20501
20676
  this._private__dragLevel = this._private__hoveredLevel;
20502
20677
  if (this._private__dragLevel === null) {
@@ -20645,6 +20820,7 @@ class PositionLine {
20645
20820
  */
20646
20821
  remove() {
20647
20822
  releaseController(this._private__chart, this);
20823
+ releaseLabelLayout(this._private__chart, this._private__id);
20648
20824
  this._private__series.detachPrimitive(this);
20649
20825
  }
20650
20826
  requestUpdate() {
@@ -20690,7 +20866,11 @@ class PositionLine {
20690
20866
  if (!this._private__attached || this._private__options.visible === false) {
20691
20867
  return null;
20692
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;
20693
20872
  const levels = [];
20873
+ const claims = [];
20694
20874
  for (const level of positionLineLevels) {
20695
20875
  const options = this._private__levelOptions(level);
20696
20876
  // A level added through a patch can be missing its price until the
@@ -20703,9 +20883,11 @@ class PositionLine {
20703
20883
  continue;
20704
20884
  }
20705
20885
  const isEntry = level === 'entry';
20886
+ claims.push({ _internal_level: level, _internal_y: y, _internal_height: labelHeight });
20706
20887
  levels.push({
20707
20888
  _internal_level: level,
20708
20889
  _internal_y: y,
20890
+ _internal_labelOffsetY: 0,
20709
20891
  _internal_color: options.color ?? positionLineDefaultColors[level],
20710
20892
  _internal_text: options.formatText ? options.formatText(options.price) : options.text ?? '',
20711
20893
  _internal_badge: options.badge ?? '',
@@ -20717,10 +20899,15 @@ class PositionLine {
20717
20899
  });
20718
20900
  }
20719
20901
  if (levels.length === 0) {
20902
+ this._private__labelLayout._internal_setClaims(this._private__id, []);
20720
20903
  return null;
20721
20904
  }
20722
- const layout = this._private__chart.options()['layout'];
20723
- const fontSize = this._private__theme.fontSize ?? layout.fontSize;
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
+ }
20724
20911
  return {
20725
20912
  _internal_levels: levels,
20726
20913
  _internal_background: this._private__theme.background ?? resolveBackgroundColor(layout.background),
@@ -30458,7 +30645,7 @@ const customSeriesDefaultOptions = {
30458
30645
  * Returns the current version as a string. For example `'1.0.0'`.
30459
30646
  */
30460
30647
  function version() {
30461
- return "2.3.0-dev+202609220108";
30648
+ return "2.3.1";
30462
30649
  }
30463
30650
 
30464
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 };