acttrader-charts 1.4.0-beta.4 → 1.4.0-beta.6

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/README.md CHANGED
@@ -2266,6 +2266,23 @@ Two details worth knowing:
2266
2266
  in hand. It deletes via select-then-delete, so it shares the undo history with
2267
2267
  every other delete.
2268
2268
 
2269
+ ### Touch gestures (iOS / Android)
2270
+
2271
+ | Gesture | Effect |
2272
+ | --- | --- |
2273
+ | One-finger drag on the chart | Scrolls through time (with momentum) |
2274
+ | Pinch | Zooms the time axis |
2275
+ | One-finger drag along the time (x) axis | Zooms the time axis the way TradingView does: the right edge (normally the latest bar) stays put, drag right to zoom in and left to zoom out. Zooming out stops when the oldest bar reaches the left edge, so the candles always fill the full width. The mouse drag on the time axis behaves the same way |
2276
+ | One-finger drag on the price (y) axis | Zooms the price axis |
2277
+ | One-finger **double tap** on the chart | Zooms the price axis in one step around the tapped price |
2278
+ | One-finger **double tap** on the price (y) axis | Resets the view, the same as the Reset view button |
2279
+ | One-finger **double tap, then drag** up/down | Zooms the price axis continuously: up zooms in, down zooms out (the same direction as the price-axis drag) |
2280
+ | Long press | Shows the crosshair |
2281
+
2282
+ Once the view has moved away from its default, a round **Reset view** button
2283
+ appears just above the time axis. It is shown even when `showUI: false`, so a
2284
+ bare chart with no toolbar always has a way back. No configuration is needed.
2285
+
2269
2286
  ### Value tooltip on long press
2270
2287
 
2271
2288
  ```ts
package/dist/index.cjs CHANGED
@@ -37080,6 +37080,14 @@ function initialVisibleBars(mobileBarDivisor = 2, forceApply = false, defaultBar
37080
37080
  }
37081
37081
  var LOOKAHEAD = 4;
37082
37082
  var LONG_PRESS_CANCEL_ARM_MS = 300;
37083
+ var DOUBLE_TAP_MS = 300;
37084
+ var DOUBLE_TAP_SLOP_PX = 30;
37085
+ var TAP_MAX_MS = 250;
37086
+ var TAP_MAX_MOVE_PX = 10;
37087
+ var DOUBLE_TAP_ZOOM_STEP = 0.7;
37088
+ var TIME_AXIS_DRAG_SPEED = 2.5;
37089
+ var MIN_VISIBLE_BARS_AXIS = 10;
37090
+ var MAX_VISIBLE_BARS_AXIS = 500;
37083
37091
  function lastIndexAtOrBefore(bars, time) {
37084
37092
  let lo = 0, hi = bars.length - 1, ans = -1;
37085
37093
  while (lo <= hi) {
@@ -37382,6 +37390,16 @@ var _ChartEngine = class _ChartEngine {
37382
37390
  */
37383
37391
  this._viewportPersistTimer = null;
37384
37392
  this.axisDrag = null;
37393
+ /**
37394
+ * One-finger double-tap zoom on the price scale (the TradingView app's
37395
+ * gesture). Second tap down arms it; dragging up/down zooms Y continuously,
37396
+ * and lifting without a drag steps one zoom-in around the tapped price.
37397
+ */
37398
+ this.tapZoom = null;
37399
+ /** Where and when the last quick one-finger tap ended — the first half of a double tap. */
37400
+ this._lastTap = null;
37401
+ /** Down position/time of the current one-finger touch, to tell a tap from a drag. */
37402
+ this._touchDown = null;
37385
37403
  /** Per-instance sub-pane Y-scale factors, keyed by ActiveIndicatorState.instanceId. */
37386
37404
  this.subPaneScaleFactors = /* @__PURE__ */ new Map();
37387
37405
  this._mobileCrosshairActive = false;
@@ -37738,19 +37756,7 @@ var _ChartEngine = class _ChartEngine {
37738
37756
  );
37739
37757
  this.subPaneScaleFactors.set(this.axisDrag.instanceId, newFactor);
37740
37758
  } else {
37741
- const delta = x - this.axisDrag.startPos;
37742
- const newVisible = Math.max(
37743
- 10,
37744
- Math.min(500, Math.round(this.axisDrag.startSize + delta * 0.5))
37745
- );
37746
- const mid = (this.viewport.startIndex + this.viewport.endIndex) / 2;
37747
- const newStart = Math.max(0, mid - newVisible / 2);
37748
- this.viewport = {
37749
- ...this.viewport,
37750
- startIndex: newStart,
37751
- endIndex: newStart + newVisible
37752
- };
37753
- this._targetViewport = { ...this.viewport };
37759
+ this.applyTimeAxisDrag(x);
37754
37760
  }
37755
37761
  this.drawCanvas.style.cursor = this.axisDrag.axis === "time" ? "ew-resize" : "ns-resize";
37756
37762
  this.scheduleRender();
@@ -37999,6 +38005,22 @@ var _ChartEngine = class _ChartEngine {
37999
38005
  const y = touch.clientY - rect.top;
38000
38006
  const { chartW, priceH } = this.getChartLayout();
38001
38007
  const priceRange = this.getScaledPriceRange(this.viewport);
38008
+ const now = performance.now();
38009
+ const lastTap = this._lastTap;
38010
+ this._lastTap = null;
38011
+ this._touchDown = null;
38012
+ if (lastTap && now - lastTap.at < DOUBLE_TAP_MS && Math.hypot(x - lastTap.x, y - lastTap.y) < DOUBLE_TAP_SLOP_PX && y < priceH && !this._mobileCrosshairActive) {
38013
+ e.preventDefault();
38014
+ this.panHandler.cancel();
38015
+ this.axisDrag = null;
38016
+ if (x >= chartW) {
38017
+ this.resetView();
38018
+ this.scheduleViewportPersist();
38019
+ } else {
38020
+ this.startTapZoom(y, priceH);
38021
+ }
38022
+ return;
38023
+ }
38002
38024
  const axisTapHit = hitTestAxisTagCluster(x, y, this.axisTagClusterAreas);
38003
38025
  if (axisTapHit) {
38004
38026
  e.preventDefault();
@@ -38015,6 +38037,7 @@ var _ChartEngine = class _ChartEngine {
38015
38037
  e.preventDefault();
38016
38038
  if (y <= priceH) {
38017
38039
  this.axisDrag = { axis: "price", startPos: y, startSize: this.priceScaleFactor };
38040
+ this._touchDown = { x, y, at: now };
38018
38041
  } else {
38019
38042
  const subPaneStates = this.activeIndicators.filter(
38020
38043
  (s) => s.indicator.pane === "sub"
@@ -38043,7 +38066,8 @@ var _ChartEngine = class _ChartEngine {
38043
38066
  this.axisDrag = {
38044
38067
  axis: "time",
38045
38068
  startPos: x,
38046
- startSize: this.viewport.endIndex - this.viewport.startIndex
38069
+ startSize: this.viewport.endIndex - this.viewport.startIndex,
38070
+ startEnd: this.viewport.endIndex
38047
38071
  };
38048
38072
  this.scheduleRender();
38049
38073
  return;
@@ -38197,6 +38221,7 @@ var _ChartEngine = class _ChartEngine {
38197
38221
  }
38198
38222
  }
38199
38223
  }
38224
+ this._touchDown = { x, y, at: now };
38200
38225
  this.drawingManager.selectDrawing(null);
38201
38226
  this.hideMobileDrawingFlyout();
38202
38227
  if (this.expandedClusters.size > 0) {
@@ -38234,6 +38259,11 @@ var _ChartEngine = class _ChartEngine {
38234
38259
  const point = this.touchToDrawingPoint(e.touches[0]);
38235
38260
  const tp = this.touchToPixels(e.touches[0]);
38236
38261
  this._lastDrawingTouchPoint = point;
38262
+ if (this.tapZoom) {
38263
+ e.preventDefault();
38264
+ this.moveTapZoom(tp.y);
38265
+ return;
38266
+ }
38237
38267
  if (this.axisDrag) {
38238
38268
  e.preventDefault();
38239
38269
  const touch = e.touches[0];
@@ -38254,18 +38284,7 @@ var _ChartEngine = class _ChartEngine {
38254
38284
  );
38255
38285
  this.subPaneScaleFactors.set(this.axisDrag.instanceId, newFactor);
38256
38286
  } else {
38257
- const delta = tx - this.axisDrag.startPos;
38258
- const newVisible = Math.max(
38259
- 10,
38260
- Math.min(500, Math.round(this.axisDrag.startSize + delta * 0.5))
38261
- );
38262
- const mid = (this.viewport.startIndex + this.viewport.endIndex) / 2;
38263
- const newStart = Math.max(0, mid - newVisible / 2);
38264
- this.viewport = {
38265
- ...this.viewport,
38266
- startIndex: newStart,
38267
- endIndex: newStart + newVisible
38268
- };
38287
+ this.applyTimeAxisDrag(tx);
38269
38288
  }
38270
38289
  this.scheduleRender();
38271
38290
  return;
@@ -38409,6 +38428,9 @@ var _ChartEngine = class _ChartEngine {
38409
38428
  * back to its start; an in-progress price drag is also cancelled defensively.
38410
38429
  */
38411
38430
  this.onDrawingTouchCancel = (_e) => {
38431
+ this._touchDown = null;
38432
+ this._lastTap = null;
38433
+ if (this.tapZoom) this.endTapZoom();
38412
38434
  if (this._longPressTimer !== null) {
38413
38435
  clearTimeout(this._longPressTimer);
38414
38436
  this._longPressTimer = null;
@@ -38442,6 +38464,22 @@ var _ChartEngine = class _ChartEngine {
38442
38464
  this._longPressTimer = null;
38443
38465
  this._longPressPoint = null;
38444
38466
  }
38467
+ if (this.tapZoom) {
38468
+ this.endTapZoom();
38469
+ return;
38470
+ }
38471
+ const down = this._touchDown;
38472
+ this._touchDown = null;
38473
+ const lifted = _e.changedTouches[0];
38474
+ if (down && lifted && _e.touches.length === 0) {
38475
+ const rect = this._cachedDrawRect ?? this.drawCanvas.getBoundingClientRect();
38476
+ const ux = lifted.clientX - rect.left;
38477
+ const uy = lifted.clientY - rect.top;
38478
+ const now = performance.now();
38479
+ if (now - down.at < TAP_MAX_MS && Math.hypot(ux - down.x, uy - down.y) < TAP_MAX_MOVE_PX) {
38480
+ this._lastTap = { x: down.x, y: down.y, at: now };
38481
+ }
38482
+ }
38445
38483
  if (this._pendingTradeDrag) {
38446
38484
  const { level } = this._pendingTradeDrag;
38447
38485
  this._pendingTradeDrag = null;
@@ -38664,7 +38702,8 @@ var _ChartEngine = class _ChartEngine {
38664
38702
  this.axisDrag = {
38665
38703
  axis: "time",
38666
38704
  startPos: x,
38667
- startSize: this.viewport.endIndex - this.viewport.startIndex
38705
+ startSize: this.viewport.endIndex - this.viewport.startIndex,
38706
+ startEnd: this.viewport.endIndex
38668
38707
  };
38669
38708
  this.scheduleRender();
38670
38709
  return;
@@ -39696,12 +39735,12 @@ var _ChartEngine = class _ChartEngine {
39696
39735
  this.applyScrollToEndBtnStyles();
39697
39736
  this.scrollToEndBtn.addEventListener("click", () => this.scrollToEnd());
39698
39737
  this.canvasWrap.appendChild(this.scrollToEndBtn);
39699
- this.resetViewBtn = document.createElement("button");
39700
- this.resetViewBtn.title = "Reset view";
39701
- this.applyResetViewBtnStyles();
39702
- this.resetViewBtn.addEventListener("click", () => this.resetView());
39703
- this.canvasWrap.appendChild(this.resetViewBtn);
39704
39738
  }
39739
+ this.resetViewBtn = document.createElement("button");
39740
+ this.resetViewBtn.title = "Reset view";
39741
+ this.applyResetViewBtnStyles();
39742
+ this.resetViewBtn.addEventListener("click", () => this.resetView());
39743
+ this.canvasWrap.appendChild(this.resetViewBtn);
39705
39744
  this.tradeDragHandler = new TradeDragHandler();
39706
39745
  this.orderTimeDragHandler = new OrderLineTimeDragHandler();
39707
39746
  if (config.onLevelDragEnd) {
@@ -39975,7 +40014,7 @@ var _ChartEngine = class _ChartEngine {
39975
40014
  },
39976
40015
  () => this.dataStore.length,
39977
40016
  () => this.scheduleRender(),
39978
- () => this.drawingManager.getActiveTool() !== null || this.drawingManager.isDraggingDrawing() || this.drawingManager.isResizingDrawing() || this.separatorDrag !== null || this.axisDrag !== null || (this.tradeDragHandler?.active ?? false) || (this.orderTimeDragHandler?.active ?? false) || this._pendingTradeDrag !== null || this._mobileCrosshairActive,
40017
+ () => this.drawingManager.getActiveTool() !== null || this.drawingManager.isDraggingDrawing() || this.drawingManager.isResizingDrawing() || this.separatorDrag !== null || this.axisDrag !== null || this.tapZoom !== null || (this.tradeDragHandler?.active ?? false) || (this.orderTimeDragHandler?.active ?? false) || this._pendingTradeDrag !== null || this._mobileCrosshairActive,
39979
40018
  (deltaY) => {
39980
40019
  if (this.priceScaleFactor === 1) return;
39981
40020
  const { priceH } = this.getChartLayout();
@@ -40254,6 +40293,7 @@ var _ChartEngine = class _ChartEngine {
40254
40293
  endIndex: this.viewport.endIndex + n
40255
40294
  };
40256
40295
  this._targetViewport = { ...this.viewport };
40296
+ if (this.axisDrag?.startEnd !== void 0) this.axisDrag.startEnd += n;
40257
40297
  this.drawingManager.shiftBarIndices(n);
40258
40298
  this.reanchorDrawingsByTimestamp();
40259
40299
  this.recomputeIndicators();
@@ -46886,6 +46926,79 @@ var _ChartEngine = class _ChartEngine {
46886
46926
  const all = this.dataStore.raw;
46887
46927
  return all[all.length - 1]?.close ?? 0;
46888
46928
  }
46929
+ /**
46930
+ * Time-axis drag (mouse or touch), the way TradingView does it: the right
46931
+ * edge — normally the latest bar — stays where it was, dragging right
46932
+ * spreads the bars (zoom in) and left packs more in (zoom out). The scale is
46933
+ * proportional to the drag, and zooming out stops once the oldest bar
46934
+ * reaches the left edge, so the candles always fill the full width.
46935
+ */
46936
+ applyTimeAxisDrag(x) {
46937
+ const drag = this.axisDrag;
46938
+ if (!drag || drag.axis !== "time") return;
46939
+ const { chartW } = this.getChartLayout();
46940
+ const end = drag.startEnd ?? this.viewport.endIndex;
46941
+ const delta = x - drag.startPos;
46942
+ const wanted = drag.startSize * Math.exp(-delta / Math.max(1, chartW) * TIME_AXIS_DRAG_SPEED);
46943
+ const maxVisible = Math.max(MIN_VISIBLE_BARS_AXIS, Math.min(MAX_VISIBLE_BARS_AXIS, end));
46944
+ const newVisible = Math.max(MIN_VISIBLE_BARS_AXIS, Math.min(maxVisible, wanted));
46945
+ this.viewport = {
46946
+ ...this.viewport,
46947
+ startIndex: end - newVisible,
46948
+ endIndex: end
46949
+ };
46950
+ this._targetViewport = { ...this.viewport };
46951
+ this.checkNeedMoreData();
46952
+ }
46953
+ /**
46954
+ * Arms a one-finger double-tap zoom at pane-local `y`. Locks the current
46955
+ * auto-fit range as the manual base so the zoom is relative to what is on
46956
+ * screen, and anchors on the tapped price so it stays under the finger.
46957
+ */
46958
+ startTapZoom(y, priceH) {
46959
+ if (this._longPressTimer !== null) {
46960
+ clearTimeout(this._longPressTimer);
46961
+ this._longPressTimer = null;
46962
+ this._longPressPoint = null;
46963
+ }
46964
+ const displayed = this._displayedPriceRange ?? this.getScaledPriceRange(this.viewport);
46965
+ if (!this._lockedAutoFitBase) this._lockedAutoFitBase = { ...displayed };
46966
+ const t = Math.max(0, Math.min(1, y / Math.max(1, priceH)));
46967
+ this.tapZoom = {
46968
+ startY: y,
46969
+ startFactor: this.priceScaleFactor,
46970
+ anchorPrice: displayed.max - t * (displayed.max - displayed.min),
46971
+ moved: false
46972
+ };
46973
+ }
46974
+ /** Sets the price-scale factor, keeping the tap-zoom anchor price at the same pixel. */
46975
+ applyTapZoomFactor(factor) {
46976
+ const tz = this.tapZoom;
46977
+ const base = this._lockedAutoFitBase;
46978
+ if (!tz || !base) return;
46979
+ const clamped = Math.max(0.1, Math.min(10, factor));
46980
+ const baseMid = (base.min + base.max) / 2;
46981
+ const mid = baseMid + this.pricePanOffset;
46982
+ const newMid = tz.anchorPrice + (mid - tz.anchorPrice) * (clamped / this.priceScaleFactor);
46983
+ this.priceScaleFactor = clamped;
46984
+ this.pricePanOffset = newMid - baseMid;
46985
+ this.scheduleRender();
46986
+ }
46987
+ moveTapZoom(y) {
46988
+ const tz = this.tapZoom;
46989
+ if (!tz) return;
46990
+ const dy = y - tz.startY;
46991
+ if (!tz.moved && Math.abs(dy) < TAP_MAX_MOVE_PX) return;
46992
+ tz.moved = true;
46993
+ this.applyTapZoomFactor(tz.startFactor * Math.pow(1.01, dy));
46994
+ }
46995
+ endTapZoom() {
46996
+ const tz = this.tapZoom;
46997
+ if (!tz) return;
46998
+ if (!tz.moved) this.applyTapZoomFactor(tz.startFactor * DOUBLE_TAP_ZOOM_STEP);
46999
+ this.tapZoom = null;
47000
+ this.scheduleViewportPersist();
47001
+ }
46889
47002
  resetPriceAxis() {
46890
47003
  this.priceScaleFactor = 1;
46891
47004
  this.pricePanOffset = 0;