acttrader-charts 1.0.20 → 1.0.21

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.
package/dist/index.js CHANGED
@@ -11389,6 +11389,14 @@ var DrawingManager = class {
11389
11389
  this.onDrawingsChange = null;
11390
11390
  /** Called when a text-type drawing is first completed — ChartEngine shows the text input. */
11391
11391
  this.onStartTextEdit = null;
11392
+ /**
11393
+ * Called after drag/resize mutates a drawing's points so ChartEngine can
11394
+ * refresh each point's `timestamp` from the current dataStore. Required
11395
+ * because DrawingManager has no access to the data layer, and drag
11396
+ * reconstructs points from raw (barIndex, price) deltas that strip the
11397
+ * original point's timestamp.
11398
+ */
11399
+ this.onPointsMutated = null;
11392
11400
  /** Set to true by ChartEngine while the text input is focused — prevents Delete/Backspace from deleting the drawing. */
11393
11401
  this.editingText = false;
11394
11402
  // Drag state (move whole drawing)
@@ -11483,14 +11491,18 @@ var DrawingManager = class {
11483
11491
  const dBar = point.barIndex - this.dragStartMouse.barIndex;
11484
11492
  const dPrice = point.price - this.dragStartMouse.price;
11485
11493
  for (let i = 0; i < this.dragDrawing.points.length; i++) {
11494
+ const start = this.dragStartPoints[i];
11495
+ if (!start) continue;
11486
11496
  this.dragDrawing.points[i] = {
11487
- barIndex: this.dragStartPoints[i].barIndex + dBar,
11488
- price: this.dragStartPoints[i].price + dPrice
11497
+ barIndex: start.barIndex + dBar,
11498
+ price: start.price + dPrice
11489
11499
  };
11490
11500
  }
11501
+ this.onPointsMutated?.(this.dragDrawing);
11491
11502
  }
11492
11503
  if (this.resizeDrawing && this.resizePointIndex >= 0) {
11493
11504
  this.resizeDrawing.points[this.resizePointIndex] = { ...point };
11505
+ this.onPointsMutated?.(this.resizeDrawing);
11494
11506
  }
11495
11507
  }
11496
11508
  /**
@@ -15158,6 +15170,13 @@ var _ChartEngine = class _ChartEngine {
15158
15170
  /** Pending resize — committed only after mouse moves past DRAW_GESTURE_THRESHOLD. */
15159
15171
  this._pendingDrawResize = null;
15160
15172
  // px
15173
+ /**
15174
+ * Pending trade-level drag — committed only after mouse moves past TRADE_DRAG_PIXEL_THRESHOLD.
15175
+ * A mouseup before the threshold means it was a click: route to _openTradeLevelEditForm instead
15176
+ * of finishing a zero-delta drag (which would otherwise stage a spurious pending change).
15177
+ */
15178
+ this._pendingTradeDrag = null;
15179
+ // px
15161
15180
  // Pane resize
15162
15181
  this.paneHeightOverrides = /* @__PURE__ */ new Map();
15163
15182
  this.separatorDrag = null;
@@ -15173,6 +15192,21 @@ var _ChartEngine = class _ChartEngine {
15173
15192
  // lerp target; null = snap on next render
15174
15193
  this._lockedAutoFitBase = null;
15175
15194
  // non-null = user has manually adjusted Y-axis
15195
+ /**
15196
+ * When setState() restores a persisted viewport but data isn't loaded yet,
15197
+ * we stash the desired time window here and apply it from loadData() once
15198
+ * bars are available. Cleared after one application.
15199
+ */
15200
+ this._pendingViewportRestore = null;
15201
+ /**
15202
+ * Debounces viewport/priceScale stateChange emissions during continuous
15203
+ * gestures (pan, wheel-zoom, price-scale drag). Pan/zoom don't emit
15204
+ * stateChange directly because doing so per-frame would be wasteful; this
15205
+ * timer coalesces bursts so the persisted viewport catches up a few ms
15206
+ * after the user stops interacting. Paired with the consumer's own
15207
+ * debounce (e.g. CRM's 800ms save) for minimal traffic.
15208
+ */
15209
+ this._viewportPersistTimer = null;
15176
15210
  this.axisDrag = null;
15177
15211
  this.subPaneScaleFactors = /* @__PURE__ */ new Map();
15178
15212
  this._mobileCrosshairActive = false;
@@ -15333,14 +15367,14 @@ var _ChartEngine = class _ChartEngine {
15333
15367
  const dx = x - this._pendingDrawDrag.startX;
15334
15368
  const dy = y - this._pendingDrawDrag.startY;
15335
15369
  if (Math.sqrt(dx * dx + dy * dy) > _ChartEngine.DRAW_GESTURE_THRESHOLD) {
15336
- this.drawingManager.startDrag(this._pendingDrawDrag.drawing, {
15337
- barIndex: rawBarIndex,
15338
- price
15339
- });
15370
+ this.drawingManager.startDrag(
15371
+ this._pendingDrawDrag.drawing,
15372
+ this.makeDrawingPoint(rawBarIndex, price)
15373
+ );
15340
15374
  this._pendingDrawDrag = null;
15341
15375
  }
15342
15376
  }
15343
- this.drawingManager.onMouseMove({ barIndex: rawBarIndex, price });
15377
+ this.drawingManager.onMouseMove(this.makeDrawingPoint(rawBarIndex, price));
15344
15378
  if (this.axisDrag) {
15345
15379
  if (this.axisDrag.axis === "price") {
15346
15380
  const delta = y - this.axisDrag.startPos;
@@ -15382,6 +15416,18 @@ var _ChartEngine = class _ChartEngine {
15382
15416
  this.scheduleRender();
15383
15417
  return;
15384
15418
  }
15419
+ if (this._pendingTradeDrag) {
15420
+ const dx = x - this._pendingTradeDrag.startX;
15421
+ const dy = y - this._pendingTradeDrag.startY;
15422
+ if (Math.sqrt(dx * dx + dy * dy) > _ChartEngine.TRADE_DRAG_PIXEL_THRESHOLD) {
15423
+ const { dragHit, level, hitY } = this._pendingTradeDrag;
15424
+ this._pendingTradeDrag = null;
15425
+ this._startTradeDragFromHit(dragHit, level, hitY);
15426
+ this.drawCanvas.style.cursor = "ns-resize";
15427
+ } else {
15428
+ return;
15429
+ }
15430
+ }
15385
15431
  if (this.tradeDragHandler?.active) {
15386
15432
  const moveResult = this.tradeDragHandler.onMouseMove(
15387
15433
  y,
@@ -15422,6 +15468,10 @@ var _ChartEngine = class _ChartEngine {
15422
15468
  const dragHover = hitTestTradeLevelDrag(x, y, this.tradeLevelDragAreas);
15423
15469
  if (dragHover) {
15424
15470
  this.drawCanvas.style.cursor = "ns-resize";
15471
+ if (dragHover.label !== this.hoveredTradeLabel) {
15472
+ this.hoveredTradeLabel = dragHover.label;
15473
+ this.renderTradeLayer();
15474
+ }
15425
15475
  this.scheduleRender();
15426
15476
  return;
15427
15477
  }
@@ -15491,6 +15541,16 @@ var _ChartEngine = class _ChartEngine {
15491
15541
  this.scheduleRender();
15492
15542
  };
15493
15543
  this.onMouseUp = () => {
15544
+ if (this._pendingTradeDrag) {
15545
+ const { level } = this._pendingTradeDrag;
15546
+ this._pendingTradeDrag = null;
15547
+ if (this._editOpenLabel === level.label && !this.pendingLevelDrags.has(level.label)) {
15548
+ this._cancelEditAndDeselect(level.label);
15549
+ return;
15550
+ }
15551
+ this._openTradeLevelEditForm(level);
15552
+ return;
15553
+ }
15494
15554
  if (this.tradeDragHandler?.active) {
15495
15555
  const result = this.tradeDragHandler.endDrag();
15496
15556
  if (result) {
@@ -15507,9 +15567,11 @@ var _ChartEngine = class _ChartEngine {
15507
15567
  return;
15508
15568
  }
15509
15569
  if (this.axisDrag) {
15570
+ const wasPriceAxis = this.axisDrag.axis === "price" || this.axisDrag.axis === "time";
15510
15571
  this.axisDrag = null;
15511
15572
  this.drawCanvas.style.cursor = "default";
15512
15573
  this.scheduleRender();
15574
+ if (wasPriceAxis) this.scheduleViewportPersist();
15513
15575
  }
15514
15576
  if (this.separatorDrag) {
15515
15577
  this.separatorDrag = null;
@@ -15671,22 +15733,7 @@ var _ChartEngine = class _ChartEngine {
15671
15733
  if (editHit) {
15672
15734
  e.preventDefault();
15673
15735
  const level = this.levels.find((l) => l.label === editHit.label);
15674
- if (level) {
15675
- this.selectedTradeLabel = level.label;
15676
- this.renderTradeLayer();
15677
- const { slPrice: _slE, tpPrice: _tpE } = this._resolveBracketPrices(level);
15678
- this._editOpenLabel = level.label;
15679
- this.emitter.emit("tradeLevelEditOpen", {
15680
- label: level.label,
15681
- type: level.type,
15682
- data: level.data,
15683
- price: level.price,
15684
- side: level.side,
15685
- stopLossPrice: _slE,
15686
- takeProfitPrice: _tpE,
15687
- isFullscreen: this.isFullscreen
15688
- });
15689
- }
15736
+ if (level) this._openTradeLevelEditForm(level);
15690
15737
  return;
15691
15738
  }
15692
15739
  }
@@ -15701,23 +15748,10 @@ var _ChartEngine = class _ChartEngine {
15701
15748
  if (this.hideLevelConfirmCancel && !isMobileDraggable) {
15702
15749
  const hasPending = this.pendingLevelDrags.has(level.label);
15703
15750
  if (this.selectedTradeLabel === level.label && !hasPending) {
15704
- this.selectedTradeLabel = null;
15705
- } else {
15706
- this.selectedTradeLabel = level.label;
15751
+ this._cancelEditAndDeselect(level.label);
15752
+ return;
15707
15753
  }
15708
- this.renderTradeLayer();
15709
- const { slPrice: _sl1, tpPrice: _tp1 } = this._resolveBracketPrices(level);
15710
- this._editOpenLabel = level.label;
15711
- this.emitter.emit("tradeLevelEditOpen", {
15712
- label: level.label,
15713
- type: level.type,
15714
- data: level.data,
15715
- price: level.price,
15716
- side: level.side,
15717
- stopLossPrice: _sl1,
15718
- takeProfitPrice: _tp1,
15719
- isFullscreen: this.isFullscreen
15720
- });
15754
+ this._openTradeLevelEditForm(level);
15721
15755
  return;
15722
15756
  }
15723
15757
  this._startTradeDragFromHit(dragHit, level, y);
@@ -15741,23 +15775,10 @@ var _ChartEngine = class _ChartEngine {
15741
15775
  if (level) {
15742
15776
  const hasPending = this.pendingLevelDrags.has(hoverHit.label);
15743
15777
  if (this.selectedTradeLabel === hoverHit.label && !hasPending) {
15744
- this.selectedTradeLabel = null;
15745
- } else {
15746
- this.selectedTradeLabel = hoverHit.label;
15778
+ this._cancelEditAndDeselect(hoverHit.label);
15779
+ return;
15747
15780
  }
15748
- const { slPrice: _slH, tpPrice: _tpH } = this._resolveBracketPrices(level);
15749
- this._editOpenLabel = level.label;
15750
- this.renderTradeLayer();
15751
- this.emitter.emit("tradeLevelEditOpen", {
15752
- label: level.label,
15753
- type: level.type,
15754
- data: level.data,
15755
- price: level.price,
15756
- side: level.side,
15757
- stopLossPrice: _slH,
15758
- takeProfitPrice: _tpH,
15759
- isFullscreen: this.isFullscreen
15760
- });
15781
+ this._openTradeLevelEditForm(level);
15761
15782
  return;
15762
15783
  }
15763
15784
  }
@@ -15956,8 +15977,10 @@ var _ChartEngine = class _ChartEngine {
15956
15977
  return;
15957
15978
  }
15958
15979
  if (this.axisDrag) {
15980
+ const wasPriceAxis = this.axisDrag.axis === "price" || this.axisDrag.axis === "time";
15959
15981
  this.axisDrag = null;
15960
15982
  this.scheduleRender();
15983
+ if (wasPriceAxis) this.scheduleViewportPersist();
15961
15984
  return;
15962
15985
  }
15963
15986
  const wasAnchorTouch = this._drawTouchAnchorActive;
@@ -16042,22 +16065,7 @@ var _ChartEngine = class _ChartEngine {
16042
16065
  const editHit = hitTestTradeLevelEdit(x, y, this.tradeLevelEditAreas);
16043
16066
  if (editHit) {
16044
16067
  const level = this.levels.find((l) => l.label === editHit.label);
16045
- if (level) {
16046
- this.selectedTradeLabel = level.label;
16047
- this.renderTradeLayer();
16048
- const { slPrice: _sl2, tpPrice: _tp2 } = this._resolveBracketPrices(level);
16049
- this._editOpenLabel = level.label;
16050
- this.emitter.emit("tradeLevelEditOpen", {
16051
- label: level.label,
16052
- type: level.type,
16053
- data: level.data,
16054
- price: level.price,
16055
- side: level.side,
16056
- stopLossPrice: _sl2,
16057
- takeProfitPrice: _tp2,
16058
- isFullscreen: this.isFullscreen
16059
- });
16060
- }
16068
+ if (level) this._openTradeLevelEditForm(level);
16061
16069
  return;
16062
16070
  }
16063
16071
  }
@@ -16075,8 +16083,7 @@ var _ChartEngine = class _ChartEngine {
16075
16083
  const level = this.levels.find((l) => l.label === dragHit.label);
16076
16084
  if (level) {
16077
16085
  if (!(this.hideLevelConfirmCancel && !dragHit.isBracket)) {
16078
- this._startTradeDragFromHit(dragHit, level, y);
16079
- this.drawCanvas.style.cursor = "ns-resize";
16086
+ this._pendingTradeDrag = { dragHit, level, hitY: y, startX: x, startY: y };
16080
16087
  return;
16081
16088
  }
16082
16089
  }
@@ -16086,49 +16093,24 @@ var _ChartEngine = class _ChartEngine {
16086
16093
  if (hoverHit) {
16087
16094
  const hasPending = this.pendingLevelDrags.has(hoverHit.label);
16088
16095
  if (this.selectedTradeLabel === hoverHit.label && !hasPending) {
16089
- this.selectedTradeLabel = null;
16096
+ this._cancelEditAndDeselect(hoverHit.label);
16097
+ return;
16098
+ }
16099
+ const level = this.levels.find((l) => l.label === hoverHit.label);
16100
+ if (level) {
16101
+ this._openTradeLevelEditForm(level);
16090
16102
  } else {
16091
16103
  this.selectedTradeLabel = hoverHit.label;
16092
- }
16093
- this.renderTradeLayer();
16094
- if (this.hideLevelConfirmCancel) {
16095
- const level = this.levels.find((l) => l.label === hoverHit.label);
16096
- if (level) {
16097
- const { slPrice: _sl3, tpPrice: _tp3 } = this._resolveBracketPrices(level);
16098
- this._editOpenLabel = level.label;
16099
- this.emitter.emit("tradeLevelEditOpen", {
16100
- label: level.label,
16101
- type: level.type,
16102
- data: level.data,
16103
- price: level.price,
16104
- side: level.side,
16105
- stopLossPrice: _sl3,
16106
- takeProfitPrice: _tp3,
16107
- isFullscreen: this.isFullscreen
16108
- });
16109
- }
16104
+ this.renderTradeLayer();
16110
16105
  }
16111
16106
  return;
16112
16107
  } else if (this.selectedTradeLabel !== null) {
16113
16108
  if (!this.hideLevelConfirmCancel) {
16114
16109
  const lbl = this.selectedTradeLabel;
16115
16110
  const pending = this.pendingLevelDrags.get(lbl);
16116
- const isEditFormOpen = this._editOpenLabel === lbl;
16117
16111
  if (!pending || pending.length === 0) {
16118
- if (isEditFormOpen) {
16119
- const lvl = this.levels.find((l) => l.label === lbl);
16120
- this._editOpenLabel = null;
16121
- if (lvl) {
16122
- this.emitter.emit("tradeLevelEditCancelled", {
16123
- label: lbl,
16124
- type: lvl.type,
16125
- isFullscreen: this.isFullscreen
16126
- });
16127
- }
16128
- }
16129
- this.selectedTradeLabel = null;
16130
- this.renderTradeLayer();
16131
- } else if (pending.every((c) => c.isNew)) {
16112
+ this._cancelEditAndDeselect(lbl);
16113
+ } else {
16132
16114
  this.revertPendingChanges(lbl);
16133
16115
  }
16134
16116
  }
@@ -16183,10 +16165,9 @@ var _ChartEngine = class _ChartEngine {
16183
16165
  this.scheduleRender();
16184
16166
  return;
16185
16167
  }
16186
- const consumed = this.drawingManager.onMouseDown({
16187
- barIndex: rawBarIndex,
16188
- price
16189
- });
16168
+ const consumed = this.drawingManager.onMouseDown(
16169
+ this.makeDrawingPoint(rawBarIndex, price)
16170
+ );
16190
16171
  if (!consumed) {
16191
16172
  const handleHit = this.drawingManager.getHandleAtPoint(
16192
16173
  x,
@@ -16885,6 +16866,12 @@ var _ChartEngine = class _ChartEngine {
16885
16866
  this.drawCanvas.style.cursor = "default";
16886
16867
  });
16887
16868
  this.drawingManager.onDrawingsChange = () => this.emitStateChange();
16869
+ this.drawingManager.onPointsMutated = (drawing) => {
16870
+ for (const pt of drawing.points) {
16871
+ const t = this.barIndexToTimestamp(pt.barIndex);
16872
+ if (t != null) pt.timestamp = t;
16873
+ }
16874
+ };
16888
16875
  this.drawingManager.onStartTextEdit = (drawing) => this.showTextInput(drawing);
16889
16876
  this.panHandler = new PanHandler(
16890
16877
  this.drawCanvas,
@@ -16894,6 +16881,7 @@ var _ChartEngine = class _ChartEngine {
16894
16881
  this.clearDuration();
16895
16882
  this.checkNeedMoreData();
16896
16883
  this.scheduleRender();
16884
+ this.scheduleViewportPersist();
16897
16885
  },
16898
16886
  () => this.dataStore.length,
16899
16887
  () => this.scheduleRender(),
@@ -16905,6 +16893,7 @@ var _ChartEngine = class _ChartEngine {
16905
16893
  const pricePerPixel = (currentRange.max - currentRange.min) / priceH;
16906
16894
  this.pricePanOffset += deltaY * pricePerPixel;
16907
16895
  this.scheduleRender();
16896
+ this.scheduleViewportPersist();
16908
16897
  },
16909
16898
  () => {
16910
16899
  this._isPanning = true;
@@ -16927,6 +16916,7 @@ var _ChartEngine = class _ChartEngine {
16927
16916
  this.clearDuration();
16928
16917
  this.checkNeedMoreData();
16929
16918
  this.scheduleRender();
16919
+ this.scheduleViewportPersist();
16930
16920
  },
16931
16921
  () => this.dataStore.length,
16932
16922
  () => this.scheduleRender(),
@@ -16947,6 +16937,7 @@ var _ChartEngine = class _ChartEngine {
16947
16937
  this.clearDuration();
16948
16938
  this.checkNeedMoreData();
16949
16939
  this.scheduleRender();
16940
+ this.scheduleViewportPersist();
16950
16941
  }
16951
16942
  );
16952
16943
  this.drawCanvas.addEventListener("mousemove", this.onMouseMove);
@@ -16984,6 +16975,15 @@ var _ChartEngine = class _ChartEngine {
16984
16975
  return true;
16985
16976
  }).map((s) => s.indicator);
16986
16977
  }
16978
+ /** Schedules a stateChange emission so viewport/priceScale changes made by
16979
+ * pan/zoom/price-scale drag reach persistence. Coalesces rapid calls. */
16980
+ scheduleViewportPersist() {
16981
+ if (this._viewportPersistTimer) clearTimeout(this._viewportPersistTimer);
16982
+ this._viewportPersistTimer = setTimeout(() => {
16983
+ this._viewportPersistTimer = null;
16984
+ this.emitStateChange();
16985
+ }, 500);
16986
+ }
16987
16987
  get isFullscreen() {
16988
16988
  return this._isCssFullscreen || !!document.fullscreenElement;
16989
16989
  }
@@ -17075,6 +17075,22 @@ var _ChartEngine = class _ChartEngine {
17075
17075
  }
17076
17076
  this.recomputeIndicators();
17077
17077
  this.reanchorDrawingsByTimestamp();
17078
+ if (this._pendingViewportRestore) {
17079
+ const pending = this._pendingViewportRestore;
17080
+ this._pendingViewportRestore = null;
17081
+ if (pending.priceScale) {
17082
+ this.priceScaleFactor = pending.priceScale.factor;
17083
+ this.pricePanOffset = pending.priceScale.panOffset;
17084
+ this._displayedPriceRange = null;
17085
+ this._lockedAutoFitBase = null;
17086
+ }
17087
+ if (pending.viewport) {
17088
+ this.applyViewportFromTimestamps(
17089
+ pending.viewport.startTime,
17090
+ pending.viewport.endTime
17091
+ );
17092
+ }
17093
+ }
17078
17094
  this.updateCountdownState();
17079
17095
  this.scheduleRender();
17080
17096
  return this;
@@ -17095,6 +17111,7 @@ var _ChartEngine = class _ChartEngine {
17095
17111
  };
17096
17112
  this._targetViewport = { ...this.viewport };
17097
17113
  this.drawingManager.shiftBarIndices(n);
17114
+ this.reanchorDrawingsByTimestamp();
17098
17115
  this.recomputeIndicators();
17099
17116
  this.scheduleRender();
17100
17117
  }
@@ -17618,15 +17635,44 @@ var _ChartEngine = class _ChartEngine {
17618
17635
  ...d,
17619
17636
  points: d.points.map((p) => ({
17620
17637
  ...p,
17621
- timestamp: this.dataStore.all[Math.floor(p.barIndex)]?.time
17638
+ // Prefer the point's own timestamp (captured at placement / drag /
17639
+ // resize); only derive from current bars as a fallback for legacy
17640
+ // points. Re-deriving on every export would otherwise corrupt the
17641
+ // anchor after a timeframe change has staled the cached barIndex.
17642
+ timestamp: p.timestamp ?? this.barIndexToTimestamp(p.barIndex)
17622
17643
  }))
17623
17644
  })),
17624
17645
  positionRenderStyle: this.positionRenderStyle,
17625
17646
  tradeDisplayFilter: this.tradeDisplayFilter,
17626
17647
  canvasColors: Object.keys(this.settingsCanvasColors).length > 0 ? { ...this.settingsCanvasColors } : void 0,
17627
- tfcEnabled: this.tfcActive
17648
+ tfcEnabled: this.tfcActive,
17649
+ viewport: this.getViewportTimestamps(),
17650
+ priceScale: this.priceScaleFactor !== 1 || this.pricePanOffset !== 0 ? { factor: this.priceScaleFactor, panOffset: this.pricePanOffset } : void 0
17628
17651
  };
17629
17652
  }
17653
+ /**
17654
+ * Captures the current viewport as a {startTime, endTime} timestamp pair,
17655
+ * preserving sub-bar fractional position. Returns undefined only when no
17656
+ * bars are loaded (viewport has no meaningful time anchor).
17657
+ */
17658
+ getViewportTimestamps() {
17659
+ if (this.dataStore.all.length === 0) return void 0;
17660
+ const startTime = this.barIndexToTimestamp(this.viewport.startIndex);
17661
+ const endTime = this.barIndexToTimestamp(this.viewport.endIndex);
17662
+ if (startTime == null || endTime == null) return void 0;
17663
+ return { startTime, endTime };
17664
+ }
17665
+ /**
17666
+ * Restores the viewport from a timestamp pair, converting each edge back
17667
+ * into a fractional bar index relative to the current dataStore. Callers
17668
+ * must ensure bars are loaded before invoking.
17669
+ */
17670
+ applyViewportFromTimestamps(startTime, endTime) {
17671
+ const startIndex = this.timestampToBarIndex(startTime);
17672
+ const endIndex = this.timestampToBarIndex(endTime);
17673
+ this.viewport = { ...this.viewport, startIndex, endIndex };
17674
+ this._targetViewport = { ...this.viewport };
17675
+ }
17630
17676
  /**
17631
17677
  * Restores chart configuration from a previously captured `ChartState`.
17632
17678
  * Call this after construction to hydrate a chart from persisted state.
@@ -17673,18 +17719,11 @@ var _ChartEngine = class _ChartEngine {
17673
17719
  }
17674
17720
  }
17675
17721
  }
17676
- const bars = this.dataStore.all;
17677
17722
  const resolvedDrawings = state.drawings.map((d) => ({
17678
17723
  ...d,
17679
17724
  points: d.points.map((p) => {
17680
- if (!p.timestamp || bars.length === 0) return p;
17681
- let lo = 0, hi = bars.length - 1;
17682
- while (lo < hi) {
17683
- const mid = lo + hi >> 1;
17684
- if ((bars[mid]?.time ?? 0) < p.timestamp) lo = mid + 1;
17685
- else hi = mid;
17686
- }
17687
- return { ...p, barIndex: lo + (p.barIndex - Math.floor(p.barIndex)) };
17725
+ if (p.timestamp == null || this.dataStore.all.length === 0) return p;
17726
+ return { ...p, barIndex: this.timestampToBarIndex(p.timestamp) };
17688
17727
  })
17689
17728
  }));
17690
17729
  this.drawingManager.loadDrawings(resolvedDrawings);
@@ -17702,6 +17741,25 @@ var _ChartEngine = class _ChartEngine {
17702
17741
  this.tfcActive = state.tfcEnabled;
17703
17742
  this.topBar?.setTfcActive(state.tfcEnabled);
17704
17743
  }
17744
+ if (state.priceScale) {
17745
+ this.priceScaleFactor = state.priceScale.factor;
17746
+ this.pricePanOffset = state.priceScale.panOffset;
17747
+ this._displayedPriceRange = null;
17748
+ this._lockedAutoFitBase = null;
17749
+ }
17750
+ if (state.viewport) {
17751
+ if (this.dataStore.all.length > 0) {
17752
+ this.applyViewportFromTimestamps(
17753
+ state.viewport.startTime,
17754
+ state.viewport.endTime
17755
+ );
17756
+ } else {
17757
+ this._pendingViewportRestore = {
17758
+ viewport: state.viewport,
17759
+ priceScale: state.priceScale
17760
+ };
17761
+ }
17762
+ }
17705
17763
  } finally {
17706
17764
  this.isRestoringState = false;
17707
17765
  }
@@ -17710,23 +17768,120 @@ var _ChartEngine = class _ChartEngine {
17710
17768
  return this;
17711
17769
  }
17712
17770
  /**
17713
- * Re-anchors all loaded drawings to the current dataset using their stored timestamps.
17714
- * Called after loadData() to fix drawings restored via setState() before data was
17715
- * available (bars.length === 0 at restore time causes the raw barIndex fallback).
17771
+ * Converts a fractional barIndex into an absolute unix-ms timestamp by
17772
+ * interpolating across the containing bar. Extrapolates linearly for indexes
17773
+ * outside [0, bars.length-1] using the adjacent bar's width.
17774
+ *
17775
+ * This is the inverse of `timestampToBarIndex` and the authoritative way to
17776
+ * capture "where in time" a drawing point sits at placement, drag, or resize.
17777
+ * Returns undefined only when no bars are loaded.
17778
+ */
17779
+ barIndexToTimestamp(idx) {
17780
+ const bars = this.dataStore.all;
17781
+ const n = bars.length;
17782
+ if (n === 0) return void 0;
17783
+ const first = bars[0];
17784
+ if (!first) return void 0;
17785
+ if (n === 1) return first.time;
17786
+ const floorIdx = Math.floor(idx);
17787
+ const frac = idx - floorIdx;
17788
+ if (floorIdx < 0) {
17789
+ const second = bars[1];
17790
+ if (!second) return first.time;
17791
+ return first.time + idx * (second.time - first.time);
17792
+ }
17793
+ if (floorIdx >= n - 1) {
17794
+ const last = bars[n - 1];
17795
+ const prev = bars[n - 2];
17796
+ if (!last || !prev) return first.time;
17797
+ return last.time + (idx - (n - 1)) * (last.time - prev.time);
17798
+ }
17799
+ const lo = bars[floorIdx];
17800
+ const hi = bars[floorIdx + 1];
17801
+ if (!lo || !hi) return first.time;
17802
+ return lo.time + frac * (hi.time - lo.time);
17803
+ }
17804
+ /**
17805
+ * Converts a unix-ms timestamp into a fractional barIndex using floor-bar
17806
+ * containment: the result is `floorBar + (ts - floorBar.time) / (nextBar.time - floorBar.time)`.
17807
+ * Extrapolates linearly for timestamps outside the loaded range using the edge
17808
+ * bar width, so drawings outside the visible data still render at the correct
17809
+ * relative offset rather than snapping to an edge.
17810
+ *
17811
+ * Matches TradingView's floor-bar semantics and preserves sub-bar time
17812
+ * precision across timeframe changes.
17813
+ */
17814
+ timestampToBarIndex(ts) {
17815
+ const bars = this.dataStore.all;
17816
+ const n = bars.length;
17817
+ if (n === 0) return 0;
17818
+ const first = bars[0];
17819
+ if (!first) return 0;
17820
+ if (n === 1) return 0;
17821
+ if (ts <= first.time) {
17822
+ const second = bars[1];
17823
+ if (!second) return 0;
17824
+ const barMs2 = second.time - first.time;
17825
+ return barMs2 > 0 ? (ts - first.time) / barMs2 : 0;
17826
+ }
17827
+ const lastIdx = n - 1;
17828
+ const lastBar = bars[lastIdx];
17829
+ const prevBar = bars[lastIdx - 1];
17830
+ if (!lastBar || !prevBar) return 0;
17831
+ if (ts >= lastBar.time) {
17832
+ const barMs2 = lastBar.time - prevBar.time;
17833
+ return lastIdx + (barMs2 > 0 ? (ts - lastBar.time) / barMs2 : 0);
17834
+ }
17835
+ let lo = 0, hi = lastIdx;
17836
+ while (lo < hi) {
17837
+ const mid = lo + hi + 1 >> 1;
17838
+ if ((bars[mid]?.time ?? 0) <= ts) lo = mid;
17839
+ else hi = mid - 1;
17840
+ }
17841
+ const loBar = bars[lo];
17842
+ const hiBar = bars[lo + 1];
17843
+ if (!loBar || !hiBar) return lo;
17844
+ const barMs = hiBar.time - loBar.time;
17845
+ return lo + (barMs > 0 ? (ts - loBar.time) / barMs : 0);
17846
+ }
17847
+ /**
17848
+ * Re-anchors all loaded drawings to the current dataset. For points with a
17849
+ * stored timestamp, barIndex is recomputed via `timestampToBarIndex`; for
17850
+ * points missing a timestamp (legacy saves or freshly placed points whose
17851
+ * timestamp capture was skipped), the timestamp is backfilled from the
17852
+ * current barIndex so subsequent data reloads remain accurate.
17853
+ *
17854
+ * Called after loadData(), prependData(), and whenever the underlying bars
17855
+ * change such that cached barIndex values would otherwise drift.
17716
17856
  */
17717
17857
  reanchorDrawingsByTimestamp() {
17718
17858
  const bars = this.dataStore.all;
17719
17859
  if (bars.length === 0) return;
17720
17860
  for (const drawing of this.drawingManager.getDrawings()) {
17721
17861
  for (const pt of drawing.points) {
17722
- if (!pt.timestamp) continue;
17723
- let lo = 0, hi = bars.length - 1;
17724
- while (lo < hi) {
17725
- const mid = lo + hi >> 1;
17726
- if ((bars[mid]?.time ?? 0) < pt.timestamp) lo = mid + 1;
17727
- else hi = mid;
17862
+ if (pt.timestamp == null) {
17863
+ const t = this.barIndexToTimestamp(pt.barIndex);
17864
+ if (t != null) pt.timestamp = t;
17865
+ continue;
17728
17866
  }
17729
- pt.barIndex = lo + (pt.barIndex - Math.floor(pt.barIndex));
17867
+ pt.barIndex = this.timestampToBarIndex(pt.timestamp);
17868
+ }
17869
+ }
17870
+ }
17871
+ /**
17872
+ * Captures a timestamp on any drawing point that is missing one, using the
17873
+ * current bars. Call this BEFORE mutating `dataStore.all` (insert/splice)
17874
+ * so that points still pointing at a stale barIndex get frozen to the
17875
+ * correct time before the mutation invalidates the index. Safe no-op for
17876
+ * points that already have a timestamp.
17877
+ */
17878
+ backfillDrawingTimestamps() {
17879
+ if (this.dataStore.all.length === 0) return;
17880
+ for (const drawing of this.drawingManager.getDrawings()) {
17881
+ for (const pt of drawing.points) {
17882
+ if (pt.timestamp != null) continue;
17883
+ const t = this.barIndexToTimestamp(pt.barIndex);
17884
+ if (t != null) pt.timestamp = t;
17730
17885
  }
17731
17886
  }
17732
17887
  }
@@ -18764,7 +18919,7 @@ var _ChartEngine = class _ChartEngine {
18764
18919
  return;
18765
18920
  }
18766
18921
  const { chartW, priceH } = this.getChartLayout();
18767
- const priceRange = this.getScaledPriceRange(viewport);
18922
+ const priceRange = this._displayedPriceRange ?? this.getScaledPriceRange(viewport);
18768
18923
  const bracketOrders = this.levels.filter(
18769
18924
  (l) => l.type === "pending" && l.ToClose != null
18770
18925
  );
@@ -18829,7 +18984,12 @@ var _ChartEngine = class _ChartEngine {
18829
18984
  this.tradeDragHandler?.dragLabel ?? null,
18830
18985
  this.tradeDragNewPrice,
18831
18986
  this.selectedTradeLabel,
18832
- new Set(this.pendingLevelDrags.keys()),
18987
+ // Include _editOpenLabel so confirm/cancel buttons appear while the edit form is open
18988
+ // even before any change is staged — e.g. editing a position with no SL/TP.
18989
+ /* @__PURE__ */ new Set([
18990
+ ...this.pendingLevelDrags.keys(),
18991
+ ...this._editOpenLabel !== null ? [this._editOpenLabel] : []
18992
+ ]),
18833
18993
  this.positionRenderStyle,
18834
18994
  this.hideLevelConfirmCancel,
18835
18995
  this.draftOrderLabel,
@@ -19032,6 +19192,11 @@ var _ChartEngine = class _ChartEngine {
19032
19192
  }
19033
19193
  }
19034
19194
  }
19195
+ if (result.newPrice === oldPrice) {
19196
+ this.tradeDragNewPrice = null;
19197
+ this.renderTradeLayer();
19198
+ return;
19199
+ }
19035
19200
  const field = result.bracketType ?? "main";
19036
19201
  const isNew = this._isBracketNew;
19037
19202
  this._isBracketNew = false;
@@ -19137,7 +19302,22 @@ var _ChartEngine = class _ChartEngine {
19137
19302
  }
19138
19303
  applyPendingChanges(label) {
19139
19304
  const changes = this.pendingLevelDrags.get(label);
19140
- if (!changes) return;
19305
+ if (!changes) {
19306
+ if (this._editOpenLabel === label) {
19307
+ const level2 = this.levels.find((l) => l.label === label);
19308
+ this._editOpenLabel = null;
19309
+ this.selectedTradeLabel = null;
19310
+ this.renderTradeLayer();
19311
+ if (level2) {
19312
+ this.emitter.emit("tradeLevelConfirmed", {
19313
+ label,
19314
+ type: level2.type,
19315
+ isFullscreen: this.isFullscreen
19316
+ });
19317
+ }
19318
+ }
19319
+ return;
19320
+ }
19141
19321
  const level = this.levels.find((l) => l.label === label);
19142
19322
  if (!level) return;
19143
19323
  if (this._editOpenLabel === label) this._editOpenLabel = null;
@@ -19216,6 +19396,10 @@ var _ChartEngine = class _ChartEngine {
19216
19396
  if (!label && this.draftOrderLabel) {
19217
19397
  this.removeDraftOrder();
19218
19398
  }
19399
+ if (label && !this.pendingLevelDrags.has(label) && this._editOpenLabel === label) {
19400
+ this._cancelEditAndDeselect(label);
19401
+ return;
19402
+ }
19219
19403
  const toRevert = label ? this.pendingLevelDrags.has(label) ? [[label, this.pendingLevelDrags.get(label) ?? []]] : [] : [...this.pendingLevelDrags.entries()];
19220
19404
  for (const [lbl] of toRevert) {
19221
19405
  this.undoIsNewBracketMutations(lbl);
@@ -19879,10 +20063,21 @@ var _ChartEngine = class _ChartEngine {
19879
20063
  const y = touch.clientY - rect.top;
19880
20064
  const { chartW, priceH } = this.getChartLayout();
19881
20065
  const priceRange = this._displayedPriceRange ?? this.getScaledPriceRange(this.viewport);
19882
- return {
19883
- barIndex: this.scaleManager.pixelToBarIndex(x, this.viewport, chartW),
19884
- price: this.scaleManager.pixelToPrice(y, priceRange, priceH)
19885
- };
20066
+ return this.makeDrawingPoint(
20067
+ this.scaleManager.pixelToBarIndex(x, this.viewport, chartW),
20068
+ this.scaleManager.pixelToPrice(y, priceRange, priceH)
20069
+ );
20070
+ }
20071
+ /**
20072
+ * Builds a fully-populated DrawingPoint from raw float coordinates. Captures
20073
+ * the absolute timestamp at this fractional barIndex so the anchor survives
20074
+ * timeframe changes, data reloads, and serialization round-trips.
20075
+ */
20076
+ makeDrawingPoint(barIndex, price) {
20077
+ const point = { barIndex, price };
20078
+ const t = this.barIndexToTimestamp(barIndex);
20079
+ if (t != null) point.timestamp = t;
20080
+ return point;
19886
20081
  }
19887
20082
  /** Shared close-button handler used by both mouse click and touch tap. */
19888
20083
  _handleTradeLevelClose(closedLevel) {
@@ -19992,6 +20187,49 @@ var _ChartEngine = class _ChartEngine {
19992
20187
  }
19993
20188
  this.scheduleRender();
19994
20189
  }
20190
+ /**
20191
+ * Closes the edit form, clears selection, and emits `tradeLevelEditCancelled` when
20192
+ * the edit form was actually open for this label. Shared between click-outside,
20193
+ * repeat-click toggle, and the ✗ button so all exit paths produce the same payload.
20194
+ */
20195
+ _cancelEditAndDeselect(label) {
20196
+ const wasEditFormOpen = this._editOpenLabel === label;
20197
+ const level = this.levels.find((l) => l.label === label);
20198
+ this._editOpenLabel = null;
20199
+ this.selectedTradeLabel = null;
20200
+ this.draftBracketPnl = {};
20201
+ this.renderTradeLayer();
20202
+ if (wasEditFormOpen && level) {
20203
+ this.emitter.emit("tradeLevelEditCancelled", {
20204
+ label,
20205
+ type: level.type,
20206
+ isFullscreen: this.isFullscreen
20207
+ });
20208
+ }
20209
+ }
20210
+ /**
20211
+ * Opens the external edit form for a trade level by emitting `tradeLevelEditOpen`.
20212
+ * Shared between pencil click, bare box click, and drag start so every entry
20213
+ * point fires the same payload. No-op when the form is already open for this
20214
+ * label, so repeat opens (e.g. pencil click followed by drag) don't spam the event.
20215
+ */
20216
+ _openTradeLevelEditForm(level) {
20217
+ if (this._editOpenLabel === level.label) return;
20218
+ this.selectedTradeLabel = level.label;
20219
+ this._editOpenLabel = level.label;
20220
+ const { slPrice, tpPrice } = this._resolveBracketPrices(level);
20221
+ this.renderTradeLayer();
20222
+ this.emitter.emit("tradeLevelEditOpen", {
20223
+ label: level.label,
20224
+ type: level.type,
20225
+ data: level.data,
20226
+ price: level.price,
20227
+ side: level.side,
20228
+ stopLossPrice: slPrice,
20229
+ takeProfitPrice: tpPrice,
20230
+ isFullscreen: this.isFullscreen
20231
+ });
20232
+ }
19995
20233
  /** Shared helper: initiate a trade level main-line drag from a hit area. */
19996
20234
  _startTradeDragFromHit(dragHit, level, hitY) {
19997
20235
  if (!this.tradeDragHandler) return;
@@ -20067,6 +20305,7 @@ var _ChartEngine = class _ChartEngine {
20067
20305
  );
20068
20306
  }
20069
20307
  }
20308
+ this._openTradeLevelEditForm(level);
20070
20309
  }
20071
20310
  /** Shared helper: initiate a +SL/+TP add-bracket drag from a hit area. */
20072
20311
  _startAddBracketFromHit(addHit, hitY) {
@@ -20180,6 +20419,7 @@ var _ChartEngine = class _ChartEngine {
20180
20419
  this.dataLoader({ start: new Date(fromTime), end: new Date(toTime), interval }).then((rawBars) => {
20181
20420
  const bars = this.maybeAggregate(rawBars);
20182
20421
  if (bars.length === 0) return;
20422
+ this.backfillDrawingTimestamps();
20183
20423
  const all = this.dataStore.all;
20184
20424
  const liveBar = all[all.length - 1];
20185
20425
  let insertCount = 0;
@@ -20200,6 +20440,7 @@ var _ChartEngine = class _ChartEngine {
20200
20440
  startIndex: this.viewport.startIndex + insertCount,
20201
20441
  endIndex: this.viewport.endIndex + insertCount
20202
20442
  };
20443
+ this.reanchorDrawingsByTimestamp();
20203
20444
  this.recomputeIndicators();
20204
20445
  this.scheduleRender();
20205
20446
  }
@@ -20394,6 +20635,7 @@ var _ChartEngine = class _ChartEngine {
20394
20635
  }
20395
20636
  };
20396
20637
  _ChartEngine.DRAW_GESTURE_THRESHOLD = 4;
20638
+ _ChartEngine.TRADE_DRAG_PIXEL_THRESHOLD = 3;
20397
20639
  var ChartEngine = _ChartEngine;
20398
20640
 
20399
20641
  // src/data/WebSocketAdapter.ts