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

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,22 @@ 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: drag right to show more bars, left to show fewer |
2276
+ | One-finger drag on the price (y) axis | Zooms the price axis |
2277
+ | One-finger **double tap** on the chart or price axis | Zooms the price axis in one step around the tapped price |
2278
+ | 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) |
2279
+ | Long press | Shows the crosshair |
2280
+
2281
+ Once the view has moved away from its default, a round **Reset view** button
2282
+ appears just above the time axis. It is shown even when `showUI: false`, so a
2283
+ bare chart with no toolbar always has a way back. No configuration is needed.
2284
+
2269
2285
  ### Value tooltip on long press
2270
2286
 
2271
2287
  ```ts
package/dist/index.cjs CHANGED
@@ -37080,6 +37080,11 @@ 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;
37083
37088
  function lastIndexAtOrBefore(bars, time) {
37084
37089
  let lo = 0, hi = bars.length - 1, ans = -1;
37085
37090
  while (lo <= hi) {
@@ -37382,6 +37387,16 @@ var _ChartEngine = class _ChartEngine {
37382
37387
  */
37383
37388
  this._viewportPersistTimer = null;
37384
37389
  this.axisDrag = null;
37390
+ /**
37391
+ * One-finger double-tap zoom on the price scale (the TradingView app's
37392
+ * gesture). Second tap down arms it; dragging up/down zooms Y continuously,
37393
+ * and lifting without a drag steps one zoom-in around the tapped price.
37394
+ */
37395
+ this.tapZoom = null;
37396
+ /** Where and when the last quick one-finger tap ended — the first half of a double tap. */
37397
+ this._lastTap = null;
37398
+ /** Down position/time of the current one-finger touch, to tell a tap from a drag. */
37399
+ this._touchDown = null;
37385
37400
  /** Per-instance sub-pane Y-scale factors, keyed by ActiveIndicatorState.instanceId. */
37386
37401
  this.subPaneScaleFactors = /* @__PURE__ */ new Map();
37387
37402
  this._mobileCrosshairActive = false;
@@ -37999,6 +38014,16 @@ var _ChartEngine = class _ChartEngine {
37999
38014
  const y = touch.clientY - rect.top;
38000
38015
  const { chartW, priceH } = this.getChartLayout();
38001
38016
  const priceRange = this.getScaledPriceRange(this.viewport);
38017
+ const now = performance.now();
38018
+ const lastTap = this._lastTap;
38019
+ this._lastTap = null;
38020
+ this._touchDown = null;
38021
+ if (lastTap && now - lastTap.at < DOUBLE_TAP_MS && Math.hypot(x - lastTap.x, y - lastTap.y) < DOUBLE_TAP_SLOP_PX && y < priceH && !this._mobileCrosshairActive) {
38022
+ e.preventDefault();
38023
+ this.panHandler.cancel();
38024
+ this.startTapZoom(y, priceH);
38025
+ return;
38026
+ }
38002
38027
  const axisTapHit = hitTestAxisTagCluster(x, y, this.axisTagClusterAreas);
38003
38028
  if (axisTapHit) {
38004
38029
  e.preventDefault();
@@ -38015,6 +38040,7 @@ var _ChartEngine = class _ChartEngine {
38015
38040
  e.preventDefault();
38016
38041
  if (y <= priceH) {
38017
38042
  this.axisDrag = { axis: "price", startPos: y, startSize: this.priceScaleFactor };
38043
+ this._touchDown = { x, y, at: now };
38018
38044
  } else {
38019
38045
  const subPaneStates = this.activeIndicators.filter(
38020
38046
  (s) => s.indicator.pane === "sub"
@@ -38197,6 +38223,7 @@ var _ChartEngine = class _ChartEngine {
38197
38223
  }
38198
38224
  }
38199
38225
  }
38226
+ this._touchDown = { x, y, at: now };
38200
38227
  this.drawingManager.selectDrawing(null);
38201
38228
  this.hideMobileDrawingFlyout();
38202
38229
  if (this.expandedClusters.size > 0) {
@@ -38234,6 +38261,11 @@ var _ChartEngine = class _ChartEngine {
38234
38261
  const point = this.touchToDrawingPoint(e.touches[0]);
38235
38262
  const tp = this.touchToPixels(e.touches[0]);
38236
38263
  this._lastDrawingTouchPoint = point;
38264
+ if (this.tapZoom) {
38265
+ e.preventDefault();
38266
+ this.moveTapZoom(tp.y);
38267
+ return;
38268
+ }
38237
38269
  if (this.axisDrag) {
38238
38270
  e.preventDefault();
38239
38271
  const touch = e.touches[0];
@@ -38266,6 +38298,7 @@ var _ChartEngine = class _ChartEngine {
38266
38298
  startIndex: newStart,
38267
38299
  endIndex: newStart + newVisible
38268
38300
  };
38301
+ this._targetViewport = { ...this.viewport };
38269
38302
  }
38270
38303
  this.scheduleRender();
38271
38304
  return;
@@ -38409,6 +38442,9 @@ var _ChartEngine = class _ChartEngine {
38409
38442
  * back to its start; an in-progress price drag is also cancelled defensively.
38410
38443
  */
38411
38444
  this.onDrawingTouchCancel = (_e) => {
38445
+ this._touchDown = null;
38446
+ this._lastTap = null;
38447
+ if (this.tapZoom) this.endTapZoom();
38412
38448
  if (this._longPressTimer !== null) {
38413
38449
  clearTimeout(this._longPressTimer);
38414
38450
  this._longPressTimer = null;
@@ -38442,6 +38478,22 @@ var _ChartEngine = class _ChartEngine {
38442
38478
  this._longPressTimer = null;
38443
38479
  this._longPressPoint = null;
38444
38480
  }
38481
+ if (this.tapZoom) {
38482
+ this.endTapZoom();
38483
+ return;
38484
+ }
38485
+ const down = this._touchDown;
38486
+ this._touchDown = null;
38487
+ const lifted = _e.changedTouches[0];
38488
+ if (down && lifted && _e.touches.length === 0) {
38489
+ const rect = this._cachedDrawRect ?? this.drawCanvas.getBoundingClientRect();
38490
+ const ux = lifted.clientX - rect.left;
38491
+ const uy = lifted.clientY - rect.top;
38492
+ const now = performance.now();
38493
+ if (now - down.at < TAP_MAX_MS && Math.hypot(ux - down.x, uy - down.y) < TAP_MAX_MOVE_PX) {
38494
+ this._lastTap = { x: down.x, y: down.y, at: now };
38495
+ }
38496
+ }
38445
38497
  if (this._pendingTradeDrag) {
38446
38498
  const { level } = this._pendingTradeDrag;
38447
38499
  this._pendingTradeDrag = null;
@@ -39696,12 +39748,12 @@ var _ChartEngine = class _ChartEngine {
39696
39748
  this.applyScrollToEndBtnStyles();
39697
39749
  this.scrollToEndBtn.addEventListener("click", () => this.scrollToEnd());
39698
39750
  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
39751
  }
39752
+ this.resetViewBtn = document.createElement("button");
39753
+ this.resetViewBtn.title = "Reset view";
39754
+ this.applyResetViewBtnStyles();
39755
+ this.resetViewBtn.addEventListener("click", () => this.resetView());
39756
+ this.canvasWrap.appendChild(this.resetViewBtn);
39705
39757
  this.tradeDragHandler = new TradeDragHandler();
39706
39758
  this.orderTimeDragHandler = new OrderLineTimeDragHandler();
39707
39759
  if (config.onLevelDragEnd) {
@@ -39975,7 +40027,7 @@ var _ChartEngine = class _ChartEngine {
39975
40027
  },
39976
40028
  () => this.dataStore.length,
39977
40029
  () => 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,
40030
+ () => 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
40031
  (deltaY) => {
39980
40032
  if (this.priceScaleFactor === 1) return;
39981
40033
  const { priceH } = this.getChartLayout();
@@ -46886,6 +46938,55 @@ var _ChartEngine = class _ChartEngine {
46886
46938
  const all = this.dataStore.raw;
46887
46939
  return all[all.length - 1]?.close ?? 0;
46888
46940
  }
46941
+ /**
46942
+ * Arms a one-finger double-tap zoom at pane-local `y`. Locks the current
46943
+ * auto-fit range as the manual base so the zoom is relative to what is on
46944
+ * screen, and anchors on the tapped price so it stays under the finger.
46945
+ */
46946
+ startTapZoom(y, priceH) {
46947
+ if (this._longPressTimer !== null) {
46948
+ clearTimeout(this._longPressTimer);
46949
+ this._longPressTimer = null;
46950
+ this._longPressPoint = null;
46951
+ }
46952
+ const displayed = this._displayedPriceRange ?? this.getScaledPriceRange(this.viewport);
46953
+ if (!this._lockedAutoFitBase) this._lockedAutoFitBase = { ...displayed };
46954
+ const t = Math.max(0, Math.min(1, y / Math.max(1, priceH)));
46955
+ this.tapZoom = {
46956
+ startY: y,
46957
+ startFactor: this.priceScaleFactor,
46958
+ anchorPrice: displayed.max - t * (displayed.max - displayed.min),
46959
+ moved: false
46960
+ };
46961
+ }
46962
+ /** Sets the price-scale factor, keeping the tap-zoom anchor price at the same pixel. */
46963
+ applyTapZoomFactor(factor) {
46964
+ const tz = this.tapZoom;
46965
+ const base = this._lockedAutoFitBase;
46966
+ if (!tz || !base) return;
46967
+ const clamped = Math.max(0.1, Math.min(10, factor));
46968
+ const baseMid = (base.min + base.max) / 2;
46969
+ const mid = baseMid + this.pricePanOffset;
46970
+ const newMid = tz.anchorPrice + (mid - tz.anchorPrice) * (clamped / this.priceScaleFactor);
46971
+ this.priceScaleFactor = clamped;
46972
+ this.pricePanOffset = newMid - baseMid;
46973
+ this.scheduleRender();
46974
+ }
46975
+ moveTapZoom(y) {
46976
+ const tz = this.tapZoom;
46977
+ if (!tz) return;
46978
+ const dy = y - tz.startY;
46979
+ if (!tz.moved && Math.abs(dy) < TAP_MAX_MOVE_PX) return;
46980
+ tz.moved = true;
46981
+ this.applyTapZoomFactor(tz.startFactor * Math.pow(1.01, dy));
46982
+ }
46983
+ endTapZoom() {
46984
+ const tz = this.tapZoom;
46985
+ if (!tz) return;
46986
+ if (!tz.moved) this.applyTapZoomFactor(tz.startFactor * DOUBLE_TAP_ZOOM_STEP);
46987
+ this.tapZoom = null;
46988
+ this.scheduleViewportPersist();
46989
+ }
46889
46990
  resetPriceAxis() {
46890
46991
  this.priceScaleFactor = 1;
46891
46992
  this.pricePanOffset = 0;