acttrader-charts 1.3.0-beta.29 → 1.3.0-beta.30

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
@@ -550,6 +550,8 @@ use `{symbol}` and `{count}`. The maths behind the pill is exported as
550
550
  | `hideSymbolAndTick` | | `false` | Hide the symbol name and tick-activity (streaming) dot in the top-left overlay. Does **not** affect the OHLC(V) strip — use `hideOHLCV` for that |
551
551
  | `hideOHLCV` | | `false` | Hide the OHLC(V) data strip (`O: H: L: C: V:`) in the top-left overlay. Independent of `hideSymbolAndTick` — set both to `true` to hide the entire overlay |
552
552
  | `showBottomBar` | | `false` | Show the bottom duration-selector bar |
553
+ | `features.enableScaleControls` | | `false` | Bottom-bar **Go to date**, **timezone** and **%** / **log** / **auto** cluster, left of the duration buttons. Needs `showBottomBar: true`. Off by default — see [Bottom Bar & Price Scale](#bottom-bar--price-scale) |
554
+ | `features.enableSidePanels` | | `false` | Right-hand docked **Data Window** / **Objects** panel (232px out of the plot). Off by default; toggle at runtime with `setSidePanelVisible()` — see [Side Panels](#side-panels) |
553
555
  | `hideQtyButton` | | `false` | Hide the floating Qty input overlay on draft orders |
554
556
  | `showQuantityField` | | `false` | Render an editable QTY pill at the left of the draft order info box. Clicking the pill opens a flyout input to edit the quantity before submitting |
555
557
  | `quantityFieldConfig` | | — | Constraints for the draft order QTY field (only relevant when `showQuantityField` is `true`). Object: `{ minLots?: number, maxLots?: number }`. `minLots` also sets the initial quantity and the step value for the flyout input (default: `1`). `maxLots` caps the input (default: `100`) |
@@ -3006,3 +3008,22 @@ On Android and iOS the same controls are bridge commands — `openReplay`,
3006
3008
  mobile header gets its own Replay button. See the `charts-android` and
3007
3009
  `charts-ios` READMEs for the Kotlin / Swift calls.
3008
3010
 
3011
+ ## Two-finger measure (mobile)
3012
+
3013
+ Hold two fingers on the chart and it shows what the TradingView app shows: a
3014
+ pill over the range with the **close and date under each finger** and, between
3015
+ them, the **change in price and percent**, coloured by direction; a vertical
3016
+ line and a dot on the close mark each finger and the range is tinted. Moving
3017
+ the fingers moves the measurement; lifting either finger ends it. Pinch still
3018
+ zooms — the measure only starts once both fingers have rested for ~0.3 s.
3019
+
3020
+ ```ts
3021
+ new ChartEngine({ container, enableTwoFingerMeasure: true });
3022
+
3023
+ chart.on('twoFingerMeasure', ({ phase, startTime, endTime, startClose, endClose, change, changePercent, bars }) => {
3024
+ // phase: 'start' | 'update' | 'end'
3025
+ });
3026
+ ```
3027
+
3028
+ Touch-only, so desktop is unaffected. Default `false`. On Android / iOS set
3029
+ `enableTwoFingerMeasure` on `init`; the event arrives through the bridge.
@@ -1880,6 +1880,14 @@ interface ChartConfig {
1880
1880
  * Default `false`.
1881
1881
  */
1882
1882
  enableReplay?: boolean;
1883
+ /**
1884
+ * **Two-finger measure** (mobile) — hold two fingers on the chart and it shows
1885
+ * the close and date under each finger and the change between them in price
1886
+ * and percent, with the range tinted, as the TradingView app does. Pinch
1887
+ * still zooms: the measure starts only once both fingers have rested for
1888
+ * about 0.3 s. Touch-only, so desktop is unaffected. Default `false`.
1889
+ */
1890
+ enableTwoFingerMeasure?: boolean;
1883
1891
  /**
1884
1892
  * Show the **Templates** section at the foot of the indicators flyout —
1885
1893
  * named, reusable indicator sets the user can save from the current chart
@@ -3008,6 +3016,17 @@ type ChartEventMap = {
3008
3016
  };
3009
3017
  /** Bar Replay ended and the live chart is back. */
3010
3018
  replayExit: Record<string, never>;
3019
+ /** Two-finger measure (mobile) started, moved or ended. Only with `enableTwoFingerMeasure`. */
3020
+ twoFingerMeasure: {
3021
+ phase: 'start' | 'update' | 'end';
3022
+ startTime: number;
3023
+ endTime: number;
3024
+ startClose: number;
3025
+ endClose: number;
3026
+ change: number;
3027
+ changePercent: number;
3028
+ bars: number;
3029
+ };
3011
3030
  snapshot: {
3012
3031
  /** Full PNG data URL of the composited chart canvas. */
3013
3032
  dataUrl: string;
@@ -1880,6 +1880,14 @@ interface ChartConfig {
1880
1880
  * Default `false`.
1881
1881
  */
1882
1882
  enableReplay?: boolean;
1883
+ /**
1884
+ * **Two-finger measure** (mobile) — hold two fingers on the chart and it shows
1885
+ * the close and date under each finger and the change between them in price
1886
+ * and percent, with the range tinted, as the TradingView app does. Pinch
1887
+ * still zooms: the measure starts only once both fingers have rested for
1888
+ * about 0.3 s. Touch-only, so desktop is unaffected. Default `false`.
1889
+ */
1890
+ enableTwoFingerMeasure?: boolean;
1883
1891
  /**
1884
1892
  * Show the **Templates** section at the foot of the indicators flyout —
1885
1893
  * named, reusable indicator sets the user can save from the current chart
@@ -3008,6 +3016,17 @@ type ChartEventMap = {
3008
3016
  };
3009
3017
  /** Bar Replay ended and the live chart is back. */
3010
3018
  replayExit: Record<string, never>;
3019
+ /** Two-finger measure (mobile) started, moved or ended. Only with `enableTwoFingerMeasure`. */
3020
+ twoFingerMeasure: {
3021
+ phase: 'start' | 'update' | 'end';
3022
+ startTime: number;
3023
+ endTime: number;
3024
+ startClose: number;
3025
+ endClose: number;
3026
+ change: number;
3027
+ changePercent: number;
3028
+ bars: number;
3029
+ };
3011
3030
  snapshot: {
3012
3031
  /** Full PNG data URL of the composited chart canvas. */
3013
3032
  dataUrl: string;
package/dist/index.cjs CHANGED
@@ -1578,6 +1578,8 @@ var ZoomHandler = class {
1578
1578
  this._pinchStartDist = 0;
1579
1579
  this._pinchStartRatio = 0;
1580
1580
  this._pinchStartViewport = null;
1581
+ /** While this returns true, pinch moves are ignored — the two-finger measure owns the gesture. */
1582
+ this.isSuppressed = () => false;
1581
1583
  this.onWheel = (e) => {
1582
1584
  e.preventDefault();
1583
1585
  const vp = this.getViewport();
@@ -1623,6 +1625,7 @@ var ZoomHandler = class {
1623
1625
  this.onTouchMove = (e) => {
1624
1626
  if (e.touches.length !== 2) return;
1625
1627
  e.preventDefault();
1628
+ if (this.isSuppressed()) return;
1626
1629
  const currentDist = this.pinchDist(e.touches[0], e.touches[1]);
1627
1630
  if (this._pinchStartDist === 0 || !this._pinchStartViewport) {
1628
1631
  this._pinchStartDist = currentDist;
@@ -28150,6 +28153,177 @@ var ReplayCutOverlay = class {
28150
28153
  }
28151
28154
  };
28152
28155
 
28156
+ // src/ui/TwoFingerMeasure.ts
28157
+ var TwoFingerMeasure = class {
28158
+ constructor(theme) {
28159
+ this.band = document.createElement("div");
28160
+ this.line1 = document.createElement("div");
28161
+ this.line2 = document.createElement("div");
28162
+ this.dot1 = document.createElement("div");
28163
+ this.dot2 = document.createElement("div");
28164
+ this.pills = document.createElement("div");
28165
+ this.startPill = document.createElement("div");
28166
+ this.midPill = document.createElement("div");
28167
+ this.endPill = document.createElement("div");
28168
+ this.theme = theme;
28169
+ this.el = document.createElement("div");
28170
+ this.el.dataset.testid = "two-finger-measure";
28171
+ Object.assign(this.el.style, {
28172
+ position: "absolute",
28173
+ left: "0",
28174
+ top: "0",
28175
+ display: "none",
28176
+ pointerEvents: "none",
28177
+ overflow: "hidden",
28178
+ zIndex: "6",
28179
+ fontFamily: "'Inter', system-ui, sans-serif"
28180
+ });
28181
+ Object.assign(this.band.style, { position: "absolute", top: "0", bottom: "0" });
28182
+ for (const l of [this.line1, this.line2]) {
28183
+ Object.assign(l.style, { position: "absolute", top: "0", bottom: "0", width: "1px" });
28184
+ }
28185
+ for (const d of [this.dot1, this.dot2]) {
28186
+ Object.assign(d.style, {
28187
+ position: "absolute",
28188
+ width: "9px",
28189
+ height: "9px",
28190
+ marginLeft: "-4.5px",
28191
+ marginTop: "-4.5px",
28192
+ borderRadius: "50%",
28193
+ boxSizing: "border-box"
28194
+ });
28195
+ }
28196
+ Object.assign(this.pills.style, {
28197
+ position: "absolute",
28198
+ top: "8px",
28199
+ display: "flex",
28200
+ gap: "6px",
28201
+ alignItems: "stretch",
28202
+ whiteSpace: "nowrap"
28203
+ });
28204
+ for (const p of [this.startPill, this.midPill, this.endPill]) {
28205
+ Object.assign(p.style, {
28206
+ display: "flex",
28207
+ flexDirection: "column",
28208
+ alignItems: "center",
28209
+ justifyContent: "center",
28210
+ gap: "1px",
28211
+ padding: "4px 8px",
28212
+ borderRadius: "6px",
28213
+ fontSize: "11px",
28214
+ lineHeight: "14px",
28215
+ fontWeight: "600",
28216
+ boxSizing: "border-box"
28217
+ });
28218
+ }
28219
+ this.startPill.dataset.testid = "tfm-start";
28220
+ this.midPill.dataset.testid = "tfm-change";
28221
+ this.endPill.dataset.testid = "tfm-end";
28222
+ this.pills.append(this.startPill, this.midPill, this.endPill);
28223
+ this.el.append(this.band, this.line1, this.line2, this.dot1, this.dot2, this.pills);
28224
+ this.setTheme(theme);
28225
+ }
28226
+ setTheme(theme) {
28227
+ this.theme = theme;
28228
+ const lineColor = withAlpha4(theme.axisText, 0.55);
28229
+ this.line1.style.background = lineColor;
28230
+ this.line2.style.background = lineColor;
28231
+ for (const p of [this.startPill, this.endPill]) {
28232
+ Object.assign(p.style, {
28233
+ background: theme.tooltip.background,
28234
+ color: theme.tooltip.text,
28235
+ border: `1px solid ${theme.tooltip.border}`
28236
+ });
28237
+ }
28238
+ }
28239
+ show(v) {
28240
+ const t = this.theme;
28241
+ const color = v.up ? t.candle.up : t.candle.down;
28242
+ Object.assign(this.el.style, { display: "block", width: `${v.width}px`, height: `${v.height}px` });
28243
+ const left = Math.min(v.x1, v.x2);
28244
+ const right = Math.max(v.x1, v.x2);
28245
+ Object.assign(this.band.style, {
28246
+ left: `${left}px`,
28247
+ width: `${Math.max(0, right - left)}px`,
28248
+ background: withAlpha4(color, 0.12)
28249
+ });
28250
+ this.line1.style.left = `${v.x1}px`;
28251
+ this.line2.style.left = `${v.x2}px`;
28252
+ for (const [dot, x, y] of [[this.dot1, v.x1, v.y1], [this.dot2, v.x2, v.y2]]) {
28253
+ Object.assign(dot.style, {
28254
+ left: `${x}px`,
28255
+ top: `${y}px`,
28256
+ background: color,
28257
+ border: `2px solid ${t.background}`
28258
+ });
28259
+ }
28260
+ const muted = `color:${t.axisText};font-weight:500;font-size:10px`;
28261
+ this.startPill.innerHTML = `<span><span style="${muted}">C</span> ${v.startPrice}</span><span style="${muted}">${v.startDate}</span>`;
28262
+ this.endPill.innerHTML = `<span><span style="${muted}">C</span> ${v.endPrice}</span><span style="${muted}">${v.endDate}</span>`;
28263
+ this.midPill.innerHTML = `<span>${v.change}</span><span>${v.changePercent}</span>`;
28264
+ Object.assign(this.midPill.style, {
28265
+ background: withAlpha4(color, 0.16),
28266
+ color,
28267
+ border: `1px solid ${withAlpha4(color, 0.45)}`
28268
+ });
28269
+ this.pills.style.top = `${v.pillsTop ?? 8}px`;
28270
+ const w = this.pills.offsetWidth;
28271
+ const mid = (v.x1 + v.x2) / 2;
28272
+ this.pills.style.left = `${Math.max(4, Math.min(v.width - w - 4, mid - w / 2))}px`;
28273
+ }
28274
+ hide() {
28275
+ this.el.style.display = "none";
28276
+ }
28277
+ destroy() {
28278
+ this.el.remove();
28279
+ }
28280
+ };
28281
+
28282
+ // src/utils/rangeMeasure.ts
28283
+ function computeRangeMeasure(start, end, startIndex, endIndex) {
28284
+ const change2 = end.close - start.close;
28285
+ const changePercent = start.close !== 0 ? change2 / start.close * 100 : 0;
28286
+ return {
28287
+ change: change2,
28288
+ changePercent,
28289
+ bars: Math.abs(endIndex - startIndex),
28290
+ durationMs: Math.abs(end.time - start.time),
28291
+ up: change2 >= 0
28292
+ };
28293
+ }
28294
+ function formatMeasurePrice(price, precision) {
28295
+ return price.toLocaleString("en-US", {
28296
+ minimumFractionDigits: precision,
28297
+ maximumFractionDigits: precision
28298
+ });
28299
+ }
28300
+ function formatSigned(value, precision) {
28301
+ return (value < 0 ? "-" : "+") + formatMeasurePrice(Math.abs(value), precision);
28302
+ }
28303
+ function formatSignedPercent(value) {
28304
+ return (value < 0 ? "-" : "+") + Math.abs(value).toFixed(2) + "%";
28305
+ }
28306
+ var SUB_DAILY2 = /^\d+[mh]$/;
28307
+ function formatMeasureDate(time, timeframe, timezone) {
28308
+ const d = new Date(time);
28309
+ const pick = (parts, type) => parts.find((x) => x.type === type)?.value ?? "";
28310
+ const dp = new Intl.DateTimeFormat("en-US", {
28311
+ day: "2-digit",
28312
+ month: "short",
28313
+ year: "numeric",
28314
+ timeZone: timezone
28315
+ }).formatToParts(d);
28316
+ const date = `${pick(dp, "day")} ${pick(dp, "month")} ${pick(dp, "year")}`;
28317
+ if (!SUB_DAILY2.test(timeframe)) return date;
28318
+ const cp = new Intl.DateTimeFormat("en-US", {
28319
+ hour: "2-digit",
28320
+ minute: "2-digit",
28321
+ hourCycle: "h23",
28322
+ timeZone: timezone
28323
+ }).formatToParts(d);
28324
+ return `${date} ${pick(cp, "hour")}:${pick(cp, "minute")}`;
28325
+ }
28326
+
28153
28327
  // src/core/templateStore.ts
28154
28328
  function slugify(name) {
28155
28329
  return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -35555,7 +35729,7 @@ function isPriceTransformSeries(series) {
35555
35729
  }
35556
35730
 
35557
35731
  // src/ui/ValueTooltip.ts
35558
- var SUB_DAILY2 = [
35732
+ var SUB_DAILY3 = [
35559
35733
  "1m",
35560
35734
  "3m",
35561
35735
  "4m",
@@ -35645,7 +35819,7 @@ var ValueTooltip = class {
35645
35819
  year: "numeric",
35646
35820
  timeZone: timezone
35647
35821
  });
35648
- const stamp = SUB_DAILY2.includes(timeframe) ? `${date} ${d.toLocaleTimeString("en-US", {
35822
+ const stamp = SUB_DAILY3.includes(timeframe) ? `${date} ${d.toLocaleTimeString("en-US", {
35649
35823
  hour: "2-digit",
35650
35824
  minute: "2-digit",
35651
35825
  hour12: false,
@@ -36540,6 +36714,14 @@ var _ChartEngine = class _ChartEngine {
36540
36714
  this._replayLastStepAt = 0;
36541
36715
  this._replayDown = null;
36542
36716
  this._replayLastTick = null;
36717
+ // ── Two-finger measure (`enableTwoFingerMeasure`) ──
36718
+ this.twoFingerEnabled = false;
36719
+ this.twoFinger = null;
36720
+ this._tfTimer = null;
36721
+ this._tfStart = null;
36722
+ this._tfLast = null;
36723
+ this._tfActive = false;
36724
+ this._tfPayload = null;
36543
36725
  this.quickSearch = null;
36544
36726
  /** Host-registered commands, merged into the built-in list each time it opens. */
36545
36727
  this.hostCommands = [];
@@ -36944,6 +37126,53 @@ var _ChartEngine = class _ChartEngine {
36944
37126
  this.onReplayPointerLeave = () => {
36945
37127
  this.replayCut?.hide();
36946
37128
  };
37129
+ this.onTfTouchStart = (e) => {
37130
+ if (!this.twoFinger) return;
37131
+ if (e.touches.length !== 2) {
37132
+ if (e.touches.length > 2) this.cancelTwoFinger();
37133
+ return;
37134
+ }
37135
+ if (this._longPressTimer !== null) {
37136
+ clearTimeout(this._longPressTimer);
37137
+ this._longPressTimer = null;
37138
+ }
37139
+ this.valueTooltip?.hide();
37140
+ const a = this.tfPoint(e.touches[0]);
37141
+ const b = this.tfPoint(e.touches[1]);
37142
+ this._tfStart = { a, b };
37143
+ this._tfLast = { a, b };
37144
+ if (this._tfTimer !== null) clearTimeout(this._tfTimer);
37145
+ this._tfTimer = setTimeout(() => {
37146
+ this._tfTimer = null;
37147
+ if (!this._tfStart || !this._tfLast) return;
37148
+ this._tfActive = true;
37149
+ this.updateTwoFinger(this._tfLast.a, this._tfLast.b, "start");
37150
+ }, _ChartEngine.TF_HOLD_MS);
37151
+ };
37152
+ this.onTfTouchMove = (e) => {
37153
+ if (!this.twoFinger || e.touches.length !== 2) return;
37154
+ const a = this.tfPoint(e.touches[0]);
37155
+ const b = this.tfPoint(e.touches[1]);
37156
+ if (this._tfActive) {
37157
+ e.preventDefault();
37158
+ this.updateTwoFinger(a, b, "update");
37159
+ return;
37160
+ }
37161
+ if (!this._tfStart) return;
37162
+ const moved = Math.max(
37163
+ Math.hypot(a.x - this._tfStart.a.x, a.y - this._tfStart.a.y),
37164
+ Math.hypot(b.x - this._tfStart.b.x, b.y - this._tfStart.b.y)
37165
+ );
37166
+ if (moved > _ChartEngine.TF_MOVE_TOLERANCE) {
37167
+ this.cancelTwoFinger();
37168
+ return;
37169
+ }
37170
+ this._tfLast = { a, b };
37171
+ };
37172
+ this.onTfTouchEnd = (e) => {
37173
+ if (!this.twoFinger || e.touches.length >= 2) return;
37174
+ this.cancelTwoFinger();
37175
+ };
36947
37176
  // ── State persistence ─────────────────────────────────────────────────────
36948
37177
  this.isRestoringState = false;
36949
37178
  this.onPointerEnter = () => {
@@ -38208,6 +38437,7 @@ var _ChartEngine = class _ChartEngine {
38208
38437
  this.valueTooltipEnabled = config.valueTooltip === true;
38209
38438
  this.quickSearchEnabled = config.enableQuickSearch === true;
38210
38439
  this.replayEnabled = config.enableReplay === true;
38440
+ this.twoFingerEnabled = config.enableTwoFingerMeasure === true;
38211
38441
  this.quickSearchShowButton = this.quickSearchEnabled && config.quickSearch?.showButton === true;
38212
38442
  this.drawingToolsEnabled = config.showDrawingTools !== false;
38213
38443
  this.settingsEnabled = config.showSettings !== false;
@@ -38746,6 +38976,10 @@ var _ChartEngine = class _ChartEngine {
38746
38976
  this.drawCanvas.style["webkitTouchCallout"] = "none";
38747
38977
  this.canvasWrap.appendChild(this.drawCanvas);
38748
38978
  this.applyCursorCss();
38979
+ if (this.twoFingerEnabled) {
38980
+ this.twoFinger = new TwoFingerMeasure(this.chromeTheme);
38981
+ this.canvasWrap.appendChild(this.twoFinger.el);
38982
+ }
38749
38983
  if (this.valueTooltipEnabled) {
38750
38984
  this.valueTooltip = new ValueTooltip(this.chromeTheme, this.labels.ohlc);
38751
38985
  this.canvasWrap.appendChild(this.valueTooltip.el);
@@ -39309,6 +39543,11 @@ var _ChartEngine = class _ChartEngine {
39309
39543
  this.drawCanvas.addEventListener("touchmove", this.onDrawingTouchMove, { passive: false });
39310
39544
  this.drawCanvas.addEventListener("touchend", this.onDrawingTouchEnd, { passive: false });
39311
39545
  this.drawCanvas.addEventListener("touchcancel", this.onDrawingTouchCancel, { passive: false });
39546
+ this.canvasWrap.addEventListener("touchstart", this.onTfTouchStart, { passive: true });
39547
+ this.canvasWrap.addEventListener("touchmove", this.onTfTouchMove, { passive: false });
39548
+ this.canvasWrap.addEventListener("touchend", this.onTfTouchEnd);
39549
+ this.canvasWrap.addEventListener("touchcancel", this.onTfTouchEnd);
39550
+ this.zoomHandler.isSuppressed = () => this._tfActive || this._tfStart !== null;
39312
39551
  this.drawCanvas.addEventListener("contextmenu", (e) => e.preventDefault());
39313
39552
  document.addEventListener("keydown", this.onKeyDown);
39314
39553
  const onFullscreenChange = () => {
@@ -39748,6 +39987,76 @@ var _ChartEngine = class _ChartEngine {
39748
39987
  const raw = this.scaleManager.pixelToBarIndex(x, this.viewport, chartW);
39749
39988
  return Math.max(1, Math.min(n - 1, Math.round(raw)));
39750
39989
  }
39990
+ tfPoint(t) {
39991
+ const rect = this._cachedWrapRect ?? this.canvasWrap.getBoundingClientRect();
39992
+ return { x: t.clientX - rect.left, y: t.clientY - rect.top };
39993
+ }
39994
+ /** Drops a pending or showing measure; emits `end` if one was showing. */
39995
+ cancelTwoFinger() {
39996
+ if (this._tfTimer !== null) {
39997
+ clearTimeout(this._tfTimer);
39998
+ this._tfTimer = null;
39999
+ }
40000
+ this._tfStart = null;
40001
+ this._tfLast = null;
40002
+ if (this._tfActive) {
40003
+ this._tfActive = false;
40004
+ this.twoFinger?.hide();
40005
+ if (this._tfPayload) this.emitter.emit("twoFingerMeasure", { ...this._tfPayload, phase: "end" });
40006
+ }
40007
+ this._tfPayload = null;
40008
+ }
40009
+ updateTwoFinger(a, b, phase) {
40010
+ const view = this.twoFinger;
40011
+ if (!view) return;
40012
+ const all = this.dataStore.all;
40013
+ const n = all.length;
40014
+ if (n === 0) return;
40015
+ const { chartW, priceH } = this.getChartLayout();
40016
+ const idx = (x) => Math.max(0, Math.min(n - 1, Math.round(this.scaleManager.pixelToBarIndex(x, this.viewport, chartW))));
40017
+ let s = idx(a.x);
40018
+ let e = idx(b.x);
40019
+ if (s > e) [s, e] = [e, s];
40020
+ const bs = all[s];
40021
+ const be = all[e];
40022
+ const m = computeRangeMeasure(bs, be, s, e);
40023
+ const range = this._displayedPriceRange ?? this.getScaledPriceRange(this.viewport);
40024
+ const p = this.pricePrecision();
40025
+ let pillsTop = 8;
40026
+ const wrapTop = (this._cachedWrapRect ?? this.canvasWrap.getBoundingClientRect()).top;
40027
+ for (const el of [this.siEl, this.indicatorOverlay?.el]) {
40028
+ if (!el || el.offsetParent === null) continue;
40029
+ const r = el.getBoundingClientRect();
40030
+ if (r.height > 0) pillsTop = Math.max(pillsTop, r.bottom - wrapTop + 6);
40031
+ }
40032
+ view.show({
40033
+ pillsTop,
40034
+ x1: this.scaleManager.xToPixel(s, this.viewport, chartW),
40035
+ x2: this.scaleManager.xToPixel(e, this.viewport, chartW),
40036
+ y1: this.scaleManager.yToPixel(bs.close, range, priceH),
40037
+ y2: this.scaleManager.yToPixel(be.close, range, priceH),
40038
+ width: chartW,
40039
+ height: priceH,
40040
+ startPrice: formatMeasurePrice(bs.close, p),
40041
+ startDate: formatMeasureDate(bs.time, this.timeframe, this.timezone),
40042
+ endPrice: formatMeasurePrice(be.close, p),
40043
+ endDate: formatMeasureDate(be.time, this.timeframe, this.timezone),
40044
+ change: formatSigned(m.change, p),
40045
+ changePercent: formatSignedPercent(m.changePercent),
40046
+ up: m.up
40047
+ });
40048
+ this._tfPayload = {
40049
+ phase,
40050
+ startTime: bs.time,
40051
+ endTime: be.time,
40052
+ startClose: bs.close,
40053
+ endClose: be.close,
40054
+ change: m.change,
40055
+ changePercent: m.changePercent,
40056
+ bars: m.bars
40057
+ };
40058
+ this.emitter.emit("twoFingerMeasure", this._tfPayload);
40059
+ }
39751
40060
  /** Shows or hides the loading overlay. */
39752
40061
  setLoading(loading) {
39753
40062
  if (this.loadingOverlayEl) {
@@ -40446,6 +40755,7 @@ var _ChartEngine = class _ChartEngine {
40446
40755
  this.bottomBar?.setTheme(this.chromeTheme);
40447
40756
  this.replayBar?.setTheme(this.chromeTheme);
40448
40757
  this.replayCut?.setTheme(this.chromeTheme);
40758
+ this.twoFinger?.setTheme(this.chromeTheme);
40449
40759
  this.leftBar?.setTheme(this.chromeTheme);
40450
40760
  this.tradePopover?.setTheme(this.chromeTheme);
40451
40761
  this.tradeActionBar?.setTheme(this.chromeTheme);
@@ -43050,6 +43360,8 @@ var _ChartEngine = class _ChartEngine {
43050
43360
  this.pauseReplay();
43051
43361
  this.replayBar?.destroy();
43052
43362
  this.replayCut?.destroy();
43363
+ this.cancelTwoFinger();
43364
+ this.twoFinger?.destroy();
43053
43365
  this.panHandler.destroy();
43054
43366
  this.zoomHandler.destroy();
43055
43367
  this.resizeObserver.disconnect();
@@ -46086,6 +46398,9 @@ _ChartEngine.TRADE_DRAG_PIXEL_THRESHOLD = 3;
46086
46398
  // px
46087
46399
  /** Movement a dual-axis badge grab needs before it commits to time vs price. */
46088
46400
  _ChartEngine.AXIS_LOCK_PIXEL_THRESHOLD = 6;
46401
+ // ── Two-finger measure (mobile) ───────────────────────────────────────────
46402
+ _ChartEngine.TF_HOLD_MS = 280;
46403
+ _ChartEngine.TF_MOVE_TOLERANCE = 12;
46089
46404
  var ChartEngine = _ChartEngine;
46090
46405
 
46091
46406
  // src/core/ChartGroup.ts