@pipsend/charts 2.3.12 → 2.3.14

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.12
3
+ * Pipsend Charts v2.3.14
4
4
  * Copyright (c) 2026 Pipsend
5
5
  * Licensed under MIT License
6
6
  * Built on TradingView Lightweight Charts™ (Apache 2.0)
@@ -14677,6 +14677,8 @@
14677
14677
  return size(originalSize, 0.7);
14678
14678
  case 'tag':
14679
14679
  return size(originalSize, 1);
14680
+ case 'xcross':
14681
+ return size(originalSize, 0.7);
14680
14682
  }
14681
14683
  }
14682
14684
  function calculateShapeHeight(barSpacing) {
@@ -14877,6 +14879,27 @@
14877
14879
  y >= textY - halfHeight && y <= textY + halfHeight;
14878
14880
  }
14879
14881
 
14882
+ function drawXCross(ctx, coords, size) {
14883
+ const crossSize = shapeSize('xcross', size);
14884
+ const halfSize = ((crossSize - 1) / 2) * coords._internal_pixelRatio;
14885
+ ctx.save();
14886
+ ctx.strokeStyle = ctx.fillStyle;
14887
+ ctx.lineWidth = Math.max(1, Math.round(1.5 * coords._internal_pixelRatio));
14888
+ ctx.lineCap = 'round';
14889
+ ctx.beginPath();
14890
+ ctx.moveTo(coords._internal_x - halfSize, coords._internal_y - halfSize);
14891
+ ctx.lineTo(coords._internal_x + halfSize, coords._internal_y + halfSize);
14892
+ ctx.moveTo(coords._internal_x + halfSize, coords._internal_y - halfSize);
14893
+ ctx.lineTo(coords._internal_x - halfSize, coords._internal_y + halfSize);
14894
+ ctx.stroke();
14895
+ ctx.restore();
14896
+ }
14897
+ function hitTestXCross(centerX, centerY, size, x, y) {
14898
+ const crossSize = shapeSize('xcross', size);
14899
+ const tolerance = 2 + crossSize / 2;
14900
+ return Math.abs(x - centerX) <= tolerance && Math.abs(y - centerY) <= tolerance;
14901
+ }
14902
+
14880
14903
  class SeriesMarkersRenderer {
14881
14904
  constructor() {
14882
14905
  this._private__data = null;
@@ -15020,6 +15043,9 @@
15020
15043
  case 'square':
15021
15044
  drawSquare(ctx, coordinates, item._internal_size);
15022
15045
  return;
15046
+ case 'xcross':
15047
+ drawXCross(ctx, coordinates, item._internal_size);
15048
+ return;
15023
15049
  case 'tag':
15024
15050
  // Handled by drawTagItem, which needs both pixel ratios and the text.
15025
15051
  return;
@@ -15048,6 +15074,8 @@
15048
15074
  return hitTestCircle(item._internal_x, item._internal_y, item._internal_size, x, y);
15049
15075
  case 'square':
15050
15076
  return hitTestSquare(item._internal_x, item._internal_y, item._internal_size, x, y);
15077
+ case 'xcross':
15078
+ return hitTestXCross(item._internal_x, item._internal_y, item._internal_size, x, y);
15051
15079
  case 'tag':
15052
15080
  return false;
15053
15081
  }
@@ -21616,6 +21644,13 @@
21616
21644
  series() {
21617
21645
  return this._series;
21618
21646
  }
21647
+ /**
21648
+ * Box the selection toolbar anchors to, in pane pixels. Null falls back to
21649
+ * the serialized points, which cannot place a line across the whole pane.
21650
+ */
21651
+ selectionBox() {
21652
+ return null;
21653
+ }
21619
21654
  getState() {
21620
21655
  return this._state;
21621
21656
  }
@@ -22292,6 +22327,53 @@
22292
22327
  return new TrendLineTool(context, options);
22293
22328
  }
22294
22329
 
22330
+ const END_HANDLE_MARGIN = 10;
22331
+ /**
22332
+ * Hands the next `clicks` clicks on the chart to `onClick`, past the last bar
22333
+ * too. A press that grabbed another drawing does not count.
22334
+ */
22335
+ function placeByClicks(chart, series, clicks, onClick) {
22336
+ let placed = 0;
22337
+ let handler = null;
22338
+ const stop = () => {
22339
+ if (handler !== null) {
22340
+ chart.unsubscribeClick(handler);
22341
+ handler = null;
22342
+ }
22343
+ };
22344
+ handler = (param) => {
22345
+ if (!param.point || consumeInputClaim(chart)) {
22346
+ return;
22347
+ }
22348
+ const price = series.coordinateToPrice(param.point.y);
22349
+ const time = timeFromCoordinate(chart, series, param.point.x);
22350
+ if (price === null || time === null) {
22351
+ return;
22352
+ }
22353
+ placed++;
22354
+ if (placed >= clicks) {
22355
+ stop();
22356
+ }
22357
+ onClick({ _internal_time: time, _internal_price: price, _internal_x: param.point.x, _internal_y: param.point.y }, placed - 1);
22358
+ };
22359
+ chart.subscribeClick(handler);
22360
+ return {
22361
+ _internal_pending: () => handler !== null,
22362
+ _internal_stop: stop,
22363
+ };
22364
+ }
22365
+ /** A fifth of the pane, and never less than a bar. */
22366
+ function defaultWidth(chart) {
22367
+ const timeScale = chart.timeScale();
22368
+ return Math.max(timeScale.width() / 5, timeScale.options().barSpacing);
22369
+ }
22370
+ /** Kept inside the pane: past it, the handles on the end hide under the price axis. */
22371
+ function placedEndX(chart, x) {
22372
+ const timeScale = chart.timeScale();
22373
+ const onScreen = Math.min(x + defaultWidth(chart), timeScale.width() - END_HANDLE_MARGIN);
22374
+ return Math.max(onScreen, x + timeScale.options().barSpacing);
22375
+ }
22376
+
22295
22377
  class HorizontalLinePaneRenderer {
22296
22378
  constructor(tool) {
22297
22379
  this._private__tool = tool;
@@ -22332,6 +22414,7 @@
22332
22414
  this._private__isDragging = false;
22333
22415
  this._private__grab = null;
22334
22416
  this._private__moveStartPrice = 0;
22417
+ this._private__placement = null;
22335
22418
  this._private__hLineOptions = {
22336
22419
  lineColor: '#2962ff',
22337
22420
  lineWidth: 2,
@@ -22344,6 +22427,26 @@
22344
22427
  };
22345
22428
  this._registerOptions(this._private__hLineOptions);
22346
22429
  this._private__createPrimitive();
22430
+ if (this._private__hLineOptions.interactive) {
22431
+ this._private__placement = placeByClicks(this._chart, this._series, 1, (point) => this._private__placeAt(point));
22432
+ }
22433
+ }
22434
+ _private__placeAt(point) {
22435
+ this._private__hLineOptions.price = point._internal_price;
22436
+ if (this._private__hLineOptions.startTime !== undefined) {
22437
+ this._private__hLineOptions.startTime = point._internal_time;
22438
+ }
22439
+ this._private__requestUpdate();
22440
+ }
22441
+ isComplete() {
22442
+ return !this._private__placement?._internal_pending();
22443
+ }
22444
+ selectionBox() {
22445
+ const y = this._private__hLineOptions.price === undefined ? null : this._series.priceToCoordinate(this._private__hLineOptions.price);
22446
+ if (y === null) {
22447
+ return null;
22448
+ }
22449
+ return { left: this._private__startX(), right: this._chart.timeScale().width(), top: y, bottom: y };
22347
22450
  }
22348
22451
  _private__createPrimitive() {
22349
22452
  this._private__paneView = this._private__createPaneView();
@@ -22381,7 +22484,7 @@
22381
22484
  return coordinateFromTime(this._chart, this._series, this._private__hLineOptions.startTime) ?? 0;
22382
22485
  }
22383
22486
  drawToCanvas(ctx, horizontalPixelRatio, verticalPixelRatio) {
22384
- if (this._private__hLineOptions.price === undefined)
22487
+ if (!this.isComplete() || this._private__hLineOptions.price === undefined)
22385
22488
  return;
22386
22489
  const y = (this._series.priceToCoordinate(this._private__hLineOptions.price) ?? 0) * verticalPixelRatio;
22387
22490
  const chartWidth = this._chart.chartElement().clientWidth * horizontalPixelRatio;
@@ -22475,7 +22578,7 @@
22475
22578
  return this.hitTest(x, y) ? toolHoveredItem(this.id, 'body', false) : null;
22476
22579
  }
22477
22580
  hitTest(x, y) {
22478
- if (this._private__hLineOptions.price === undefined) {
22581
+ if (!this.isComplete() || this._private__hLineOptions.price === undefined) {
22479
22582
  return false;
22480
22583
  }
22481
22584
  const lineY = this._series.priceToCoordinate(this._private__hLineOptions.price) ?? 0;
@@ -22550,6 +22653,7 @@
22550
22653
  this._private__requestUpdate();
22551
22654
  }
22552
22655
  remove() {
22656
+ this._private__placement?._internal_stop();
22553
22657
  if (this._private__primitive) {
22554
22658
  this._series.detachPrimitive(this._private__primitive);
22555
22659
  this._private__primitive = null;
@@ -22609,6 +22713,7 @@
22609
22713
  this._private__grab = null;
22610
22714
  this._private__moveStartTime = 0;
22611
22715
  this._private__moveStartPrice = 0;
22716
+ this._private__placement = null;
22612
22717
  this._private__crossOptions = {
22613
22718
  lineColor: '#787b86',
22614
22719
  lineWidth: 1,
@@ -22618,6 +22723,16 @@
22618
22723
  };
22619
22724
  this._registerOptions(this._private__crossOptions);
22620
22725
  this._private__createPrimitive();
22726
+ if (this._private__crossOptions.interactive) {
22727
+ this._private__placement = placeByClicks(this._chart, this._series, 1, (point) => {
22728
+ this._private__crossOptions.time = point._internal_time;
22729
+ this._private__crossOptions.price = point._internal_price;
22730
+ this._private__requestUpdate();
22731
+ });
22732
+ }
22733
+ }
22734
+ isComplete() {
22735
+ return !this._private__placement?._internal_pending();
22621
22736
  }
22622
22737
  _private__createPrimitive() {
22623
22738
  this._private__paneView = new CrossLinePaneView(this);
@@ -22645,7 +22760,7 @@
22645
22760
  this._private__requestUpdate();
22646
22761
  }
22647
22762
  _private__coordinates() {
22648
- if (this._private__crossOptions.time === undefined || this._private__crossOptions.price === undefined) {
22763
+ if (!this.isComplete() || this._private__crossOptions.time === undefined || this._private__crossOptions.price === undefined) {
22649
22764
  return null;
22650
22765
  }
22651
22766
  const x = coordinateFromTime(this._chart, this._series, this._private__crossOptions.time);
@@ -22768,6 +22883,7 @@
22768
22883
  return this._private__isHovered;
22769
22884
  }
22770
22885
  remove() {
22886
+ this._private__placement?._internal_stop();
22771
22887
  if (this._private__primitive) {
22772
22888
  try {
22773
22889
  this._series.detachPrimitive(this._private__primitive);
@@ -24190,6 +24306,7 @@
24190
24306
  this._private__isDragging = false;
24191
24307
  this._private__grab = null;
24192
24308
  this._private__moveStartTime = 0;
24309
+ this._private__placement = null;
24193
24310
  this._private__vLineOptions = {
24194
24311
  lineColor: '#787b86',
24195
24312
  lineWidth: 1,
@@ -24201,6 +24318,29 @@
24201
24318
  };
24202
24319
  this._registerOptions(this._private__vLineOptions);
24203
24320
  this._private__createPrimitive();
24321
+ if (this._private__vLineOptions.interactive) {
24322
+ this._private__placement = placeByClicks(this._chart, this._series, 1, (point) => {
24323
+ this._private__vLineOptions.time = point._internal_time;
24324
+ this._private__requestUpdate();
24325
+ });
24326
+ }
24327
+ }
24328
+ isComplete() {
24329
+ return !this._private__placement?._internal_pending();
24330
+ }
24331
+ selectionBox() {
24332
+ const x = coordinateFromTime(this._chart, this._series, this._private__vLineOptions.time);
24333
+ if (x === null) {
24334
+ return null;
24335
+ }
24336
+ let middle;
24337
+ try {
24338
+ middle = this._series.getPane().getHeight() / 2;
24339
+ }
24340
+ catch (error) {
24341
+ return null;
24342
+ }
24343
+ return { left: x, right: x, top: middle, bottom: middle };
24204
24344
  }
24205
24345
  _private__createPrimitive() {
24206
24346
  this._private__paneView = this._private__createPaneView();
@@ -24231,7 +24371,7 @@
24231
24371
  return new VerticalLinePaneView(this);
24232
24372
  }
24233
24373
  drawToCanvas(ctx, horizontalPixelRatio, verticalPixelRatio) {
24234
- if (this._private__vLineOptions.time === undefined)
24374
+ if (!this.isComplete() || this._private__vLineOptions.time === undefined)
24235
24375
  return;
24236
24376
  const x = (coordinateFromTime(this._chart, this._series, this._private__vLineOptions.time) ?? 0) * horizontalPixelRatio;
24237
24377
  const chartHeight = this._chart.chartElement().clientHeight * verticalPixelRatio;
@@ -24318,7 +24458,7 @@
24318
24458
  return this.hitTest(x, y) ? toolHoveredItem(this.id, 'body', false) : null;
24319
24459
  }
24320
24460
  hitTest(x, y) {
24321
- if (this._private__vLineOptions.time === undefined) {
24461
+ if (!this.isComplete() || this._private__vLineOptions.time === undefined) {
24322
24462
  return false;
24323
24463
  }
24324
24464
  const lineX = coordinateFromTime(this._chart, this._series, this._private__vLineOptions.time) ?? 0;
@@ -24388,6 +24528,7 @@
24388
24528
  this._private__requestUpdate();
24389
24529
  }
24390
24530
  remove() {
24531
+ this._private__placement?._internal_stop();
24391
24532
  if (this._private__primitive) {
24392
24533
  this._series.detachPrimitive(this._private__primitive);
24393
24534
  this._private__primitive = null;
@@ -25109,6 +25250,7 @@
25109
25250
  this._private__isHovered = false;
25110
25251
  this._private__isDragging = false;
25111
25252
  this._private__dragTarget = null;
25253
+ this._private__placement = null;
25112
25254
  this._private__fibOptions = {
25113
25255
  levels: [
25114
25256
  { level: 0, color: '#787B86', label: '0%' },
@@ -25124,6 +25266,20 @@
25124
25266
  ...options,
25125
25267
  };
25126
25268
  this._private__createPrimitive();
25269
+ if (this._private__fibOptions.interactive) {
25270
+ this._private__placement = placeByClicks(this._chart, this._series, 3, (point, index) => {
25271
+ const placed = { time: point._internal_time, price: point._internal_price };
25272
+ if (index === 0) {
25273
+ this.setPoint1(placed);
25274
+ }
25275
+ else if (index === 1) {
25276
+ this.setPoint2(placed);
25277
+ }
25278
+ else {
25279
+ this.setPoint3(placed);
25280
+ }
25281
+ });
25282
+ }
25127
25283
  }
25128
25284
  _private__createPrimitive() {
25129
25285
  this._private__paneView = this._private__createPaneView();
@@ -25181,7 +25337,7 @@
25181
25337
  this._private__requestUpdate();
25182
25338
  }
25183
25339
  isComplete() {
25184
- return !!(this._private__fibOptions.point1 && this._private__fibOptions.point2 && this._private__fibOptions.point3);
25340
+ return !this._private__placement?._internal_pending() && !!(this._private__fibOptions.point1 && this._private__fibOptions.point2 && this._private__fibOptions.point3);
25185
25341
  }
25186
25342
  setColor(color) {
25187
25343
  this._private__fibOptions.lineColor = color;
@@ -25476,6 +25632,7 @@
25476
25632
  this._private__requestUpdate();
25477
25633
  }
25478
25634
  remove() {
25635
+ this._private__placement?._internal_stop();
25479
25636
  if (this._private__primitive) {
25480
25637
  this._series.detachPrimitive(this._private__primitive);
25481
25638
  this._private__primitive = null;
@@ -26127,17 +26284,19 @@
26127
26284
  // Draw border
26128
26285
  const borderColor = this._private__rectOptions.borderColor;
26129
26286
  const borderWidth = this._private__isSelected ? this._private__rectOptions.borderWidth + 1 : this._private__rectOptions.borderWidth;
26130
- ctx.strokeStyle = borderColor;
26131
- ctx.lineWidth = borderWidth * horizontalPixelRatio;
26132
- // Set border style
26133
- if (this._private__rectOptions.borderStyle === 'dashed') {
26134
- ctx.setLineDash([10 * horizontalPixelRatio, 5 * horizontalPixelRatio]);
26135
- }
26136
- else if (this._private__rectOptions.borderStyle === 'dotted') {
26137
- ctx.setLineDash([2 * horizontalPixelRatio, 3 * horizontalPixelRatio]);
26287
+ if (borderWidth > 0) {
26288
+ ctx.strokeStyle = borderColor;
26289
+ ctx.lineWidth = borderWidth * horizontalPixelRatio;
26290
+ // Set border style
26291
+ if (this._private__rectOptions.borderStyle === 'dashed') {
26292
+ ctx.setLineDash([10 * horizontalPixelRatio, 5 * horizontalPixelRatio]);
26293
+ }
26294
+ else if (this._private__rectOptions.borderStyle === 'dotted') {
26295
+ ctx.setLineDash([2 * horizontalPixelRatio, 3 * horizontalPixelRatio]);
26296
+ }
26297
+ ctx.strokeRect(minX, minY, width, height);
26298
+ ctx.setLineDash([]);
26138
26299
  }
26139
- ctx.strokeRect(minX, minY, width, height);
26140
- ctx.setLineDash([]);
26141
26300
  // Draw corner handles if selected
26142
26301
  if ((this._private__isSelected || this._private__isHovered) && !this._private__isDrawing && !this.isLocked()) {
26143
26302
  this._private__drawCornerHandles(ctx, minX, minY, maxX, maxY, horizontalPixelRatio, borderColor);
@@ -26165,17 +26324,20 @@
26165
26324
  }
26166
26325
  _private__drawLabel(ctx, minX, minY, maxX, maxY, horizontalPixelRatio, verticalPixelRatio, color) {
26167
26326
  const padding = 6 * horizontalPixelRatio;
26168
- const fontSize = 12 * horizontalPixelRatio;
26327
+ const fontSize = (this._private__rectOptions.labelFontSize ?? 12) * horizontalPixelRatio;
26169
26328
  ctx.font = `${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
26170
26329
  const textMetrics = ctx.measureText(this._private__rectOptions.labelText);
26171
26330
  const textWidth = textMetrics.width;
26172
26331
  const textHeight = fontSize;
26332
+ const middleY = (minY + maxY) / 2 - textHeight / 2 - padding;
26333
+ // A box extended far past the pane keeps right-hand and centred labels on screen.
26334
+ const visibleMaxX = Math.min(maxX, ctx.canvas.width);
26173
26335
  // Calculate label position based on labelPosition option
26174
26336
  let labelX = minX + padding;
26175
26337
  let labelY = minY + padding;
26176
26338
  switch (this._private__rectOptions.labelPosition) {
26177
26339
  case 'top-right':
26178
- labelX = maxX - textWidth - padding * 2;
26340
+ labelX = visibleMaxX - textWidth - padding * 2;
26179
26341
  labelY = minY + padding;
26180
26342
  break;
26181
26343
  case 'bottom-left':
@@ -26183,19 +26345,27 @@
26183
26345
  labelY = maxY - textHeight - padding * 2;
26184
26346
  break;
26185
26347
  case 'bottom-right':
26186
- labelX = maxX - textWidth - padding * 2;
26348
+ labelX = visibleMaxX - textWidth - padding * 2;
26187
26349
  labelY = maxY - textHeight - padding * 2;
26188
26350
  break;
26189
26351
  case 'center':
26190
- labelX = (minX + maxX) / 2 - textWidth / 2 - padding;
26191
- labelY = (minY + maxY) / 2 - textHeight / 2 - padding;
26352
+ labelX = (Math.max(minX, 0) + visibleMaxX) / 2 - textWidth / 2 - padding;
26353
+ labelY = middleY;
26354
+ break;
26355
+ case 'left':
26356
+ labelX = minX + padding;
26357
+ labelY = middleY;
26358
+ break;
26359
+ case 'right':
26360
+ labelX = visibleMaxX - textWidth - padding * 2;
26361
+ labelY = middleY;
26192
26362
  break;
26193
26363
  }
26194
26364
  // Draw background
26195
- ctx.fillStyle = color;
26365
+ ctx.fillStyle = this._private__rectOptions.labelBackgroundColor ?? color;
26196
26366
  ctx.fillRect(labelX, labelY, textWidth + padding * 2, textHeight + padding * 2);
26197
26367
  // Draw text
26198
- ctx.fillStyle = '#ffffff';
26368
+ ctx.fillStyle = this._private__rectOptions.labelColor ?? '#ffffff';
26199
26369
  ctx.textBaseline = 'top';
26200
26370
  ctx.fillText(this._private__rectOptions.labelText, labelX + padding, labelY + padding);
26201
26371
  }
@@ -26433,6 +26603,9 @@
26433
26603
  showLabel: this._private__rectOptions.showLabel,
26434
26604
  labelText: this._private__rectOptions.labelText,
26435
26605
  labelPosition: this._private__rectOptions.labelPosition,
26606
+ labelColor: this._private__rectOptions.labelColor,
26607
+ labelBackgroundColor: this._private__rectOptions.labelBackgroundColor,
26608
+ labelFontSize: this._private__rectOptions.labelFontSize,
26436
26609
  },
26437
26610
  };
26438
26611
  }
@@ -28025,6 +28198,7 @@
28025
28198
  this._private__moveStart = null;
28026
28199
  this._private__isEditing = false;
28027
28200
  this._private__clickOutsideHandler = null;
28201
+ this._private__placement = null;
28028
28202
  this._private__textOptions = {
28029
28203
  text: 'Double-click to edit',
28030
28204
  fontSize: 14,
@@ -28037,11 +28211,22 @@
28037
28211
  borderWidth: 1,
28038
28212
  textAlign: 'left',
28039
28213
  maxWidth: 200,
28214
+ anchor: 'top-left',
28215
+ pointer: false,
28216
+ borderRadius: 0,
28040
28217
  ...options,
28041
28218
  };
28042
28219
  this._registerOptions(this._private__textOptions);
28043
28220
  this._private__createPrimitive();
28044
28221
  this._private__setupClickOutsideHandler();
28222
+ if (this._private__textOptions.interactive) {
28223
+ this._private__placement = placeByClicks(this._chart, this._series, 1, (point) => {
28224
+ this.setPosition(point._internal_time, point._internal_price);
28225
+ if (this._private__textOptions.onComplete) {
28226
+ this._private__textOptions.onComplete();
28227
+ }
28228
+ });
28229
+ }
28045
28230
  }
28046
28231
  _private__setupClickOutsideHandler() {
28047
28232
  // Set up click handler to finish editing when clicking outside
@@ -28127,7 +28312,7 @@
28127
28312
  this._private__requestUpdate();
28128
28313
  }
28129
28314
  isComplete() {
28130
- return !!(this._private__textOptions.time && this._private__textOptions.price !== undefined);
28315
+ return !this._private__placement?._internal_pending() && !!(this._private__textOptions.time && this._private__textOptions.price !== undefined);
28131
28316
  }
28132
28317
  setColor(color) {
28133
28318
  this._private__textOptions.textColor = color;
@@ -28188,18 +28373,59 @@
28188
28373
  const py = this._series.priceToCoordinate(price) ?? 0;
28189
28374
  // Calculate text box dimensions
28190
28375
  const fontSize = this._private__textOptions.fontSize || 14;
28191
- const padding = this._private__textOptions.padding || 8;
28376
+ const padding = this._private__textOptions.padding ?? 8;
28192
28377
  const text = this._private__textOptions.text || '';
28193
28378
  // Rough estimate of text width (will be more accurate in canvas)
28194
28379
  const charWidth = fontSize * 0.6;
28195
28380
  const textWidth = Math.min(text.length * charWidth, this._private__textOptions.maxWidth || 200);
28196
- const textHeight = fontSize + padding * 2;
28197
- // Check if click is within text box bounds
28198
- const left = px;
28199
- const right = px + textWidth + padding * 2;
28200
- const top = py;
28201
- const bottom = py + textHeight;
28202
- return x >= left && x <= right && y >= top && y <= bottom;
28381
+ const boxWidth = textWidth + padding * 2;
28382
+ const boxHeight = fontSize + padding * 2;
28383
+ const tail = this._private__tailSize(boxHeight);
28384
+ const box = this._private__boxOrigin(px, py, boxWidth, boxHeight, tail);
28385
+ const left = box._internal_left - (this._private__textOptions.anchor === 'left' ? tail : 0);
28386
+ const right = box._internal_left + boxWidth + (this._private__textOptions.anchor === 'right' ? tail : 0);
28387
+ return x >= left && x <= right && y >= box._internal_top && y <= box._internal_top + boxHeight;
28388
+ }
28389
+ _private__tailSize(boxHeight) {
28390
+ const anchor = this._private__textOptions.anchor;
28391
+ return this._private__textOptions.pointer && (anchor === 'left' || anchor === 'right') ? Math.round(boxHeight * 0.35) : 0;
28392
+ }
28393
+ _private__boxOrigin(x, y, width, height, tail) {
28394
+ switch (this._private__textOptions.anchor) {
28395
+ case 'left':
28396
+ return { _internal_left: x + tail, _internal_top: y - height / 2 };
28397
+ case 'right':
28398
+ return { _internal_left: x - tail - width, _internal_top: y - height / 2 };
28399
+ case 'center':
28400
+ return { _internal_left: x - width / 2, _internal_top: y - height / 2 };
28401
+ default:
28402
+ return { _internal_left: x, _internal_top: y };
28403
+ }
28404
+ }
28405
+ // eslint-disable-next-line max-params
28406
+ _private__traceBox(ctx, left, top, width, height, tail, anchorX, anchorY, radius) {
28407
+ const r = Math.max(0, Math.min(radius, width / 2, height / 2));
28408
+ ctx.beginPath();
28409
+ ctx.moveTo(left + r, top);
28410
+ ctx.lineTo(left + width - r, top);
28411
+ ctx.arcTo(left + width, top, left + width, top + r, r);
28412
+ if (tail > 0 && this._private__textOptions.anchor === 'right') {
28413
+ ctx.lineTo(left + width, anchorY - tail);
28414
+ ctx.lineTo(anchorX, anchorY);
28415
+ ctx.lineTo(left + width, anchorY + tail);
28416
+ }
28417
+ ctx.lineTo(left + width, top + height - r);
28418
+ ctx.arcTo(left + width, top + height, left + width - r, top + height, r);
28419
+ ctx.lineTo(left + r, top + height);
28420
+ ctx.arcTo(left, top + height, left, top + height - r, r);
28421
+ if (tail > 0 && this._private__textOptions.anchor === 'left') {
28422
+ ctx.lineTo(left, anchorY + tail);
28423
+ ctx.lineTo(anchorX, anchorY);
28424
+ ctx.lineTo(left, anchorY - tail);
28425
+ }
28426
+ ctx.lineTo(left, top + r);
28427
+ ctx.arcTo(left, top, left + r, top, r);
28428
+ ctx.closePath();
28203
28429
  }
28204
28430
  hitTestEndpoint(x, y) {
28205
28431
  if (!this.isComplete())
@@ -28251,7 +28477,7 @@
28251
28477
  const x = (coordinateFromTime(this._chart, this._series, time) ?? 0) * horizontalPixelRatio;
28252
28478
  const y = (this._series.priceToCoordinate(price) ?? 0) * verticalPixelRatio;
28253
28479
  const fontSize = (this._private__textOptions.fontSize || 14) * horizontalPixelRatio;
28254
- const padding = (this._private__textOptions.padding || 8) * horizontalPixelRatio;
28480
+ const padding = (this._private__textOptions.padding ?? 8) * horizontalPixelRatio;
28255
28481
  const maxWidth = (this._private__textOptions.maxWidth || 200) * horizontalPixelRatio;
28256
28482
  // Set font
28257
28483
  ctx.font = `${fontSize}px ${this._private__textOptions.fontFamily || 'Arial'}`;
@@ -28283,32 +28509,39 @@
28283
28509
  const boxWidth = maxLineWidth + padding * 2;
28284
28510
  const lineHeight = fontSize * 1.2;
28285
28511
  const boxHeight = lines.length * lineHeight + padding * 2;
28512
+ const tail = this._private__tailSize(boxHeight);
28513
+ const { _internal_left: left, _internal_top: top } = this._private__boxOrigin(x, y, boxWidth, boxHeight, tail);
28514
+ const radius = (this._private__textOptions.borderRadius ?? 0) * horizontalPixelRatio;
28286
28515
  // Draw background
28287
- const bgOpacity = this._private__textOptions.backgroundOpacity || 0.8;
28516
+ const bgOpacity = this._private__textOptions.backgroundOpacity ?? 0.8;
28288
28517
  const bgColor = this._private__textOptions.backgroundColor || '#2962FF';
28289
- ctx.fillStyle = this._private__hexToRgba(bgColor, bgOpacity);
28290
- ctx.fillRect(x, y, boxWidth, boxHeight);
28518
+ if (bgOpacity > 0) {
28519
+ ctx.fillStyle = this._private__hexToRgba(bgColor, bgOpacity);
28520
+ this._private__traceBox(ctx, left, top, boxWidth, boxHeight, tail, x, y, radius);
28521
+ ctx.fill();
28522
+ }
28291
28523
  // Draw border
28292
28524
  if (this._private__isSelected || this._private__isHovered) {
28293
28525
  ctx.strokeStyle = this._private__textOptions.borderColor ?? '#FFFFFF';
28294
28526
  ctx.lineWidth = (this._private__textOptions.borderWidth || 1) * horizontalPixelRatio * (this._private__isSelected ? 2 : 1);
28295
- ctx.strokeRect(x, y, boxWidth, boxHeight);
28527
+ this._private__traceBox(ctx, left, top, boxWidth, boxHeight, tail, x, y, radius);
28528
+ ctx.stroke();
28296
28529
  }
28297
28530
  // Draw text
28298
28531
  ctx.fillStyle = this._private__textOptions.textColor || '#FFFFFF';
28299
28532
  for (let i = 0; i < lines.length; i++) {
28300
28533
  const line = lines[i];
28301
- let textX = x + padding;
28534
+ let textX = left + padding;
28302
28535
  // Handle text alignment
28303
28536
  if (this._private__textOptions.textAlign === 'center') {
28304
28537
  const metrics = ctx.measureText(line);
28305
- textX = x + (boxWidth - metrics.width) / 2;
28538
+ textX = left + (boxWidth - metrics.width) / 2;
28306
28539
  }
28307
28540
  else if (this._private__textOptions.textAlign === 'right') {
28308
28541
  const metrics = ctx.measureText(line);
28309
- textX = x + boxWidth - metrics.width - padding;
28542
+ textX = left + boxWidth - metrics.width - padding;
28310
28543
  }
28311
- const textY = y + padding + i * lineHeight;
28544
+ const textY = top + padding + i * lineHeight;
28312
28545
  ctx.fillText(line, textX, textY);
28313
28546
  }
28314
28547
  // Draw anchor point if selected
@@ -28352,6 +28585,9 @@
28352
28585
  borderWidth: this._private__textOptions.borderWidth,
28353
28586
  textAlign: this._private__textOptions.textAlign,
28354
28587
  maxWidth: this._private__textOptions.maxWidth,
28588
+ anchor: this._private__textOptions.anchor,
28589
+ pointer: this._private__textOptions.pointer,
28590
+ borderRadius: this._private__textOptions.borderRadius,
28355
28591
  },
28356
28592
  extra: {
28357
28593
  text: this._private__textOptions.text,
@@ -28359,6 +28595,7 @@
28359
28595
  };
28360
28596
  }
28361
28597
  remove() {
28598
+ this._private__placement?._internal_stop();
28362
28599
  // Clean up click handler
28363
28600
  if (this._private__clickOutsideHandler) {
28364
28601
  this._chart.unsubscribeClick(this._private__clickOutsideHandler);
@@ -29217,6 +29454,7 @@
29217
29454
  this._private__paneView = null;
29218
29455
  this._private__isSelected = false;
29219
29456
  this._private__isHovered = false;
29457
+ this._private__placement = null;
29220
29458
  this._private__isDragging = false;
29221
29459
  this._private__dragTarget = null;
29222
29460
  this._private__grab = null;
@@ -29239,6 +29477,32 @@
29239
29477
  // Create primitive and setup
29240
29478
  this._private__createPrimitive();
29241
29479
  this._private__updateMetrics();
29480
+ if (this._private__positionOptions.interactive) {
29481
+ this._private__placement = placeByClicks(this._chart, this._series, 1, (point) => this._private__placeAt(point));
29482
+ }
29483
+ }
29484
+ _private__placeAt(point) {
29485
+ const options = this._private__positionOptions;
29486
+ const shift = point._internal_price - options.entryPrice;
29487
+ options.entryPrice = point._internal_price;
29488
+ options.entryTime = point._internal_time;
29489
+ if (options.stopLoss !== undefined) {
29490
+ options.stopLoss += shift;
29491
+ }
29492
+ if (options.takeProfit !== undefined) {
29493
+ options.takeProfit += shift;
29494
+ }
29495
+ options.endTime = timeFromCoordinate(this._chart, this._series, placedEndX(this._chart, point._internal_x)) ?? undefined;
29496
+ this._private__updateMetrics();
29497
+ this._private__requestUpdate();
29498
+ }
29499
+ isComplete() {
29500
+ return !this._private__placement?._internal_pending();
29501
+ }
29502
+ _private__span() {
29503
+ const entryX = coordinateFromTime(this._chart, this._series, this._private__positionOptions.entryTime) ?? 0;
29504
+ const endX = coordinateFromTime(this._chart, this._series, this._private__positionOptions.endTime) ?? entryX + defaultWidth(this._chart);
29505
+ return { _internal_entryX: entryX, _internal_endX: endX };
29242
29506
  }
29243
29507
  _private__createPrimitive() {
29244
29508
  // Create and cache the paneView once
@@ -29270,6 +29534,9 @@
29270
29534
  return new PositionPaneView(this);
29271
29535
  }
29272
29536
  drawToCanvas(ctx, horizontalPixelRatio, verticalPixelRatio) {
29537
+ if (!this.isComplete()) {
29538
+ return;
29539
+ }
29273
29540
  // Draw profit rectangle (green)
29274
29541
  if (this._private__profitRect && this._private__positionOptions.takeProfit) {
29275
29542
  const timeScale = this._chart.timeScale();
@@ -29303,16 +29570,9 @@
29303
29570
  }
29304
29571
  }
29305
29572
  _private__drawRectangle(ctx, rect, horizontalPixelRatio, verticalPixelRatio, timeScale) {
29306
- const entryX = (coordinateFromTime(this._chart, this._series, this._private__positionOptions.entryTime) ?? 0) * horizontalPixelRatio;
29307
- // Use endTime if specified, otherwise extend to chart edge
29308
- let endX;
29309
- if (this._private__positionOptions.endTime) {
29310
- endX = (coordinateFromTime(this._chart, this._series, this._private__positionOptions.endTime) ?? 0) * horizontalPixelRatio;
29311
- }
29312
- else {
29313
- const chartWidth = this._chart.chartElement().clientWidth * horizontalPixelRatio;
29314
- endX = chartWidth;
29315
- }
29573
+ const span = this._private__span();
29574
+ const entryX = span._internal_entryX * horizontalPixelRatio;
29575
+ const endX = span._internal_endX * horizontalPixelRatio;
29316
29576
  const y1 = (this._series.priceToCoordinate(rect._internal_price1) ?? 0) * verticalPixelRatio;
29317
29577
  const y2 = (this._series.priceToCoordinate(rect._internal_price2) ?? 0) * verticalPixelRatio;
29318
29578
  // Draw filled rectangle with semi-transparent fill
@@ -29327,16 +29587,10 @@
29327
29587
  ctx.stroke();
29328
29588
  }
29329
29589
  _private__drawEntryLine(ctx, horizontalPixelRatio, verticalPixelRatio, timeScale) {
29330
- const entryX = (coordinateFromTime(this._chart, this._series, this._private__positionOptions.entryTime) ?? 0) * horizontalPixelRatio;
29590
+ const span = this._private__span();
29591
+ const entryX = span._internal_entryX * horizontalPixelRatio;
29592
+ const endX = span._internal_endX * horizontalPixelRatio;
29331
29593
  const entryY = (this._series.priceToCoordinate(this._private__positionOptions.entryPrice) ?? 0) * verticalPixelRatio;
29332
- let endX;
29333
- if (this._private__positionOptions.endTime) {
29334
- endX = (coordinateFromTime(this._chart, this._series, this._private__positionOptions.endTime) ?? 0) * horizontalPixelRatio;
29335
- }
29336
- else {
29337
- const chartWidth = this._chart.chartElement().clientWidth * horizontalPixelRatio;
29338
- endX = chartWidth;
29339
- }
29340
29594
  // Draw horizontal entry line (divider)
29341
29595
  const lineColor = this._private__positionOptions.entryLineColor;
29342
29596
  ctx.strokeStyle = lineColor;
@@ -29450,9 +29704,11 @@
29450
29704
  const padding = 6 * horizontalPixelRatio;
29451
29705
  const labelWidth = textMetrics.width + padding * 2;
29452
29706
  const labelHeight = 20 * horizontalPixelRatio;
29453
- // Position label at the start of the line
29707
+ // Outside the box, so a label wider than it does not cover the handles on its edge
29708
+ const entryY = (this._series.priceToCoordinate(this._private__positionOptions.entryPrice) ?? 0) * verticalPixelRatio;
29709
+ const gap = 3 * verticalPixelRatio;
29454
29710
  const labelX = entryX + 8 * horizontalPixelRatio;
29455
- const labelY = y - labelHeight / 2;
29711
+ const labelY = y <= entryY ? y - labelHeight - gap : y + gap;
29456
29712
  // Draw label background
29457
29713
  ctx.fillStyle = rect._internal_color;
29458
29714
  ctx.fillRect(labelX, labelY, labelWidth, labelHeight);
@@ -29460,10 +29716,12 @@
29460
29716
  ctx.fillStyle = '#ffffff';
29461
29717
  ctx.textAlign = 'left';
29462
29718
  ctx.textBaseline = 'middle';
29463
- ctx.fillText(labelText, labelX + padding, y);
29719
+ ctx.fillText(labelText, labelX + padding, labelY + labelHeight / 2);
29464
29720
  }
29465
29721
  _private__drawEndpoint(ctx, price, color, horizontalPixelRatio, verticalPixelRatio, timeScale) {
29466
- const entryX = (coordinateFromTime(this._chart, this._series, this._private__positionOptions.entryTime) ?? 0) * horizontalPixelRatio;
29722
+ const span = this._private__span();
29723
+ const entryX = span._internal_entryX * horizontalPixelRatio;
29724
+ const endX = span._internal_endX * horizontalPixelRatio;
29467
29725
  const y = (this._series.priceToCoordinate(price) ?? 0) * verticalPixelRatio;
29468
29726
  // Draw circle endpoint at entry
29469
29727
  const circleColor = color;
@@ -29475,14 +29733,6 @@
29475
29733
  ctx.fill();
29476
29734
  ctx.stroke();
29477
29735
  // Draw square endpoint at the right edge
29478
- let endX;
29479
- if (this._private__positionOptions.endTime) {
29480
- endX = (coordinateFromTime(this._chart, this._series, this._private__positionOptions.endTime) ?? 0) * horizontalPixelRatio;
29481
- }
29482
- else {
29483
- const chartWidth = this._chart.chartElement().clientWidth * horizontalPixelRatio;
29484
- endX = chartWidth - 10 * horizontalPixelRatio;
29485
- }
29486
29736
  const size = 8 * horizontalPixelRatio;
29487
29737
  const squareColor = color;
29488
29738
  ctx.fillStyle = squareColor;
@@ -29603,18 +29853,12 @@
29603
29853
  return this.hitTest(x, y) ? toolHoveredItem(this.id, 'body', false) : null;
29604
29854
  }
29605
29855
  hitTest(x, y) {
29606
- if (!this._private__positionOptions.stopLoss && !this._private__positionOptions.takeProfit) {
29856
+ if (!this.isComplete() || (!this._private__positionOptions.stopLoss && !this._private__positionOptions.takeProfit)) {
29607
29857
  return false;
29608
29858
  }
29609
- const entryX = coordinateFromTime(this._chart, this._series, this._private__positionOptions.entryTime) ?? 0;
29610
- let endX;
29611
- if (this._private__positionOptions.endTime) {
29612
- endX = coordinateFromTime(this._chart, this._series, this._private__positionOptions.endTime) ?? 0;
29613
- }
29614
- else {
29615
- const chartWidth = this._chart.chartElement().clientWidth;
29616
- endX = chartWidth;
29617
- }
29859
+ const { _internal_entryX: entryX, _internal_endX: endX } = this._private__span();
29860
+ const left = Math.min(entryX, endX);
29861
+ const right = Math.max(entryX, endX);
29618
29862
  // Check if inside the entire Position Tool area (rectangles)
29619
29863
  if (this._private__positionOptions.takeProfit && this._private__positionOptions.stopLoss) {
29620
29864
  const tpY = this._series.priceToCoordinate(this._private__positionOptions.takeProfit) ?? 0;
@@ -29622,7 +29866,7 @@
29622
29866
  const minY = Math.min(tpY, slY);
29623
29867
  const maxY = Math.max(tpY, slY);
29624
29868
  // Check if inside the rectangle bounds (entire tool area)
29625
- if (x >= entryX && x <= endX && y >= minY && y <= maxY) {
29869
+ if (x >= left && x <= right && y >= minY && y <= maxY) {
29626
29870
  return true;
29627
29871
  }
29628
29872
  // Also check entry line area
@@ -29634,15 +29878,12 @@
29634
29878
  return false;
29635
29879
  }
29636
29880
  hitTestEndpoint(x, y) {
29637
- const entryX = coordinateFromTime(this._chart, this._series, this._private__positionOptions.entryTime) ?? 0;
29638
- let endX;
29639
- if (this._private__positionOptions.endTime) {
29640
- endX = coordinateFromTime(this._chart, this._series, this._private__positionOptions.endTime) ?? 0;
29641
- }
29642
- else {
29643
- const chartWidth = this._chart.chartElement().clientWidth;
29644
- endX = chartWidth;
29881
+ if (!this.isComplete()) {
29882
+ return null;
29645
29883
  }
29884
+ const { _internal_entryX: entryX, _internal_endX: endX } = this._private__span();
29885
+ const left = Math.min(entryX, endX);
29886
+ const right = Math.max(entryX, endX);
29646
29887
  const tolerance = 10;
29647
29888
  // Check TP left endpoint (circle)
29648
29889
  if (this._private__positionOptions.takeProfit) {
@@ -29679,7 +29920,7 @@
29679
29920
  // Check entry line (middle divider line) - higher priority than 'move'
29680
29921
  const entryY = this._series.priceToCoordinate(this._private__positionOptions.entryPrice) ?? 0;
29681
29922
  const lineTolerance = 8;
29682
- if (x >= entryX && x <= endX && Math.abs(y - entryY) <= lineTolerance) {
29923
+ if (x >= left && x <= right && Math.abs(y - entryY) <= lineTolerance) {
29683
29924
  return 'entry';
29684
29925
  }
29685
29926
  // Check if clicking on the body (rectangles) for moving entire tool
@@ -29689,7 +29930,7 @@
29689
29930
  const minY = Math.min(tpY, slY);
29690
29931
  const maxY = Math.max(tpY, slY);
29691
29932
  // Check if inside the rectangle bounds
29692
- if (x >= entryX && x <= endX && y >= minY && y <= maxY) {
29933
+ if (x >= left && x <= right && y >= minY && y <= maxY) {
29693
29934
  return 'move';
29694
29935
  }
29695
29936
  }
@@ -29789,6 +30030,7 @@
29789
30030
  this._private__requestUpdate();
29790
30031
  }
29791
30032
  remove() {
30033
+ this._private__placement?._internal_stop();
29792
30034
  if (this._private__primitive) {
29793
30035
  this._series.detachPrimitive(this._private__primitive);
29794
30036
  this._private__primitive = null;
@@ -29835,6 +30077,7 @@
29835
30077
  }
29836
30078
  }
29837
30079
 
30080
+ const TOP_PADDING = 50;
29838
30081
  class GanttTool extends DrawingToolBase {
29839
30082
  constructor(context, options) {
29840
30083
  super(context, 'gantt', options);
@@ -29848,6 +30091,7 @@
29848
30091
  this._private__grab = null;
29849
30092
  this._private__moveStart = null;
29850
30093
  this._private__hoveredSegmentIndex = -1;
30094
+ this._private__placement = null;
29851
30095
  this._private__ganttOptions = {
29852
30096
  segments: [],
29853
30097
  rowHeight: 40,
@@ -29860,6 +30104,43 @@
29860
30104
  };
29861
30105
  this._registerOptions(this._private__ganttOptions);
29862
30106
  this._private__createPrimitive();
30107
+ if (this._private__ganttOptions.interactive) {
30108
+ this._private__placement = placeByClicks(this._chart, this._series, 1, (point) => this._private__placeAt(point));
30109
+ }
30110
+ }
30111
+ _private__placeAt(point) {
30112
+ const segments = this._private__ganttOptions.segments ?? [];
30113
+ const endTime = timeFromCoordinate(this._chart, this._series, placedEndX(this._chart, point._internal_x)) ?? point._internal_time;
30114
+ const rowPitch = this._private__ganttOptions.rowHeight + this._private__ganttOptions.rowSpacing;
30115
+ const row = Math.max(0, Math.floor((point._internal_y - TOP_PADDING) / rowPitch));
30116
+ const shift = segments.length > 0 ? row - segments[0].level : 0;
30117
+ for (const segment of segments) {
30118
+ segment.startTime = point._internal_time;
30119
+ segment.endTime = endTime;
30120
+ segment.level = Math.max(0, segment.level + shift);
30121
+ }
30122
+ this._private__requestUpdate();
30123
+ }
30124
+ isComplete() {
30125
+ return !this._private__placement?._internal_pending();
30126
+ }
30127
+ selectionBox() {
30128
+ const segments = this._private__ganttOptions.segments ?? [];
30129
+ if (segments.length === 0) {
30130
+ return null;
30131
+ }
30132
+ const rowHeight = this._private__ganttOptions.rowHeight;
30133
+ const pitch = rowHeight + this._private__ganttOptions.rowSpacing;
30134
+ // Typed as the public shape: an inferred one gets its keys renamed by the build
30135
+ const box = { left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity };
30136
+ for (const segment of segments) {
30137
+ const top = TOP_PADDING + segment.level * pitch;
30138
+ box.left = Math.min(box.left, coordinateFromTime(this._chart, this._series, segment.startTime) ?? 0);
30139
+ box.right = Math.max(box.right, coordinateFromTime(this._chart, this._series, segment.endTime) ?? 0);
30140
+ box.top = Math.min(box.top, top);
30141
+ box.bottom = Math.max(box.bottom, top + rowHeight);
30142
+ }
30143
+ return box;
29863
30144
  }
29864
30145
  _private__createPrimitive() {
29865
30146
  this._private__paneView = this._private__createPaneView();
@@ -29890,11 +30171,10 @@
29890
30171
  return new GanttPaneView(this);
29891
30172
  }
29892
30173
  drawToCanvas(ctx, horizontalPixelRatio, verticalPixelRatio) {
29893
- if (!this._private__ganttOptions.segments || this._private__ganttOptions.segments.length === 0)
30174
+ if (!this.isComplete() || !this._private__ganttOptions.segments || this._private__ganttOptions.segments.length === 0)
29894
30175
  return;
29895
30176
  // Position gantt at top of chart with padding
29896
- const topPadding = 50; // pixels from top
29897
- const baseY = topPadding * verticalPixelRatio;
30177
+ const baseY = TOP_PADDING * verticalPixelRatio;
29898
30178
  const rowHeight = this._private__ganttOptions.rowHeight * verticalPixelRatio;
29899
30179
  const rowSpacing = this._private__ganttOptions.rowSpacing * verticalPixelRatio;
29900
30180
  // Draw grid lines if enabled
@@ -30073,11 +30353,10 @@
30073
30353
  this._private__moveStart = null;
30074
30354
  }
30075
30355
  hitTest(x, y) {
30076
- if (!this._private__ganttOptions.segments || this._private__ganttOptions.segments.length === 0) {
30356
+ if (!this.isComplete() || !this._private__ganttOptions.segments || this._private__ganttOptions.segments.length === 0) {
30077
30357
  return false;
30078
30358
  }
30079
- const topPadding = 50;
30080
- const baseY = topPadding;
30359
+ const baseY = TOP_PADDING;
30081
30360
  const rowHeight = this._private__ganttOptions.rowHeight;
30082
30361
  const rowSpacing = this._private__ganttOptions.rowSpacing;
30083
30362
  // Check each segment
@@ -30131,6 +30410,7 @@
30131
30410
  this._private__requestUpdate();
30132
30411
  }
30133
30412
  remove() {
30413
+ this._private__placement?._internal_stop();
30134
30414
  if (this._private__primitive) {
30135
30415
  this._series.detachPrimitive(this._private__primitive);
30136
30416
  this._private__primitive = null;
@@ -30266,6 +30546,7 @@
30266
30546
  }
30267
30547
  this._private__completionCheck = setTimeout(() => {
30268
30548
  this._private__completionCheck = null;
30549
+ const completed = [];
30269
30550
  for (const id of Array.from(this._private__pendingToolIds)) {
30270
30551
  const tool = this._private__tools.get(id);
30271
30552
  if (!tool) {
@@ -30274,9 +30555,15 @@
30274
30555
  }
30275
30556
  if (tool.isComplete()) {
30276
30557
  this._private__pendingToolIds.delete(id);
30277
- this._private__drawingCompleted._internal_fire(id);
30558
+ completed.push(id);
30278
30559
  }
30279
30560
  }
30561
+ if (completed.length > 0) {
30562
+ this.recordChange();
30563
+ }
30564
+ for (const id of completed) {
30565
+ this._private__drawingCompleted._internal_fire(id);
30566
+ }
30280
30567
  }, 0);
30281
30568
  }
30282
30569
  /**
@@ -30336,12 +30623,16 @@
30336
30623
  if (this._private__restoring) {
30337
30624
  return;
30338
30625
  }
30626
+ const next = this.serializeAll();
30627
+ if (JSON.stringify(next) === JSON.stringify(this._private__committed)) {
30628
+ return;
30629
+ }
30339
30630
  this._private__past.push(this._private__committed);
30340
30631
  if (this._private__past.length > UNDO_HISTORY_LIMIT) {
30341
30632
  this._private__past.shift();
30342
30633
  }
30343
30634
  this._private__future = [];
30344
- this._private__committed = this.serializeAll();
30635
+ this._private__committed = next;
30345
30636
  }
30346
30637
  canUndo() {
30347
30638
  return this._private__past.length > 0;
@@ -30397,11 +30688,25 @@
30397
30688
  const tool = this._private__tools.get(this._private__selectedToolId);
30398
30689
  return tool ? this._private__boundsOf(tool) : null;
30399
30690
  }
30691
+ /** Relative to the chart element; the tool's box is relative to its pane. */
30692
+ _private__boundsOf(tool) {
30693
+ const box = tool.selectionBox() ?? this._private__boxFromPoints(tool);
30694
+ if (box === null) {
30695
+ return null;
30696
+ }
30697
+ const offset = this._private__paneOffsetOf(tool);
30698
+ return {
30699
+ left: box.left + offset._internal_x,
30700
+ right: box.right + offset._internal_x,
30701
+ top: box.top + offset._internal_y,
30702
+ bottom: box.bottom + offset._internal_y,
30703
+ };
30704
+ }
30400
30705
  /**
30401
30706
  * Bounds come from the tool's own serialized points, so this works for every
30402
30707
  * tool without each one having to report a box.
30403
30708
  */
30404
- _private__boundsOf(tool) {
30709
+ _private__boxFromPoints(tool) {
30405
30710
  const series = tool.series();
30406
30711
  const points = tool.serialize().points;
30407
30712
  let left = Number.POSITIVE_INFINITY;
@@ -30424,13 +30729,7 @@
30424
30729
  if (!found) {
30425
30730
  return null;
30426
30731
  }
30427
- const offset = this._private__paneOffsetOf(tool);
30428
- return {
30429
- left: left + offset._internal_x,
30430
- right: right + offset._internal_x,
30431
- top: top + offset._internal_y,
30432
- bottom: bottom + offset._internal_y,
30433
- };
30732
+ return { left, top, right, bottom };
30434
30733
  }
30435
30734
  /** Panes below the first one are offset inside the chart element. */
30436
30735
  _private__paneOffsetOf(tool) {
@@ -30514,7 +30813,10 @@
30514
30813
  serializeAll() {
30515
30814
  const result = [];
30516
30815
  for (const tool of this._private__tools.values()) {
30517
- result.push(tool.serialize());
30816
+ // Not drawn yet: undo would bring it back as a finished drawing
30817
+ if (tool.isComplete()) {
30818
+ result.push(tool.serialize());
30819
+ }
30518
30820
  }
30519
30821
  return result;
30520
30822
  }
@@ -31139,7 +31441,7 @@
31139
31441
  * Returns the current version as a string. For example `'1.0.0'`.
31140
31442
  */
31141
31443
  function version() {
31142
- return "2.3.12";
31444
+ return "2.3.14";
31143
31445
  }
31144
31446
 
31145
31447
  var PipsendChartsModule = /*#__PURE__*/Object.freeze({