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