acttrader-charts 1.4.0-beta.3 → 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 +28 -5
- package/dist/index.cjs +222 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -16
- package/dist/index.d.ts +62 -16
- package/dist/index.js +222 -65
- package/dist/index.js.map +1 -1
- package/dist/webview/chart.html +97 -97
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1203,7 +1203,7 @@ Sub-pane heights are user-resizable by dragging the separator between panes.
|
|
|
1203
1203
|
| `horizontalLine` | Horizontal Line | Single click |
|
|
1204
1204
|
| `verticalLine` | Vertical Line | Single click |
|
|
1205
1205
|
| `trendLine` | Trend Line | 2 clicks |
|
|
1206
|
-
| `trendAngle` | Trend Angle | 2 clicks. The angle is the drawing's own property: measured when a point is placed or dragged, kept while zooming, and saved as `angle` on the serialized drawing — so a synced drawing shows the same degrees on web, Android and iOS. |
|
|
1206
|
+
| `trendAngle` | Trend Angle | 2 clicks. The angle is the drawing's own property: measured when a point is placed or dragged, kept while zooming, and saved as `angle` on the serialized drawing — so a synced drawing shows the same degrees on web, Android and iOS. The line is drawn from the first point at that angle, as far as the second point projects; the stored points are never rewritten by rendering (before 1.4.0-beta.4 the second point was re-anchored onto the line on every render, which lengthened the line on every zoom or tick until it ran off the pane). |
|
|
1207
1207
|
| `crossLine` | Cross Line | Single click (H+V) |
|
|
1208
1208
|
| `parallelChannel` | Parallel Channel | 3 clicks |
|
|
1209
1209
|
| `flatChannel` | Flat Channel | 2 clicks |
|
|
@@ -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
|
|
@@ -2337,6 +2353,13 @@ Three consequences worth knowing:
|
|
|
2337
2353
|
different number of elements.
|
|
2338
2354
|
- **Market price still comes off the raw feed.** Trade levels and P&L price
|
|
2339
2355
|
against the real last close, never a brick top.
|
|
2356
|
+
- **Live ticks move the element count both ways.** The forming bar completes a
|
|
2357
|
+
brick, line or column on one tick and takes it back on the next. The view
|
|
2358
|
+
keeps tracking the live edge either way — same width, same room past the
|
|
2359
|
+
last element — and a view scrolled into history stays put. Box and reversal
|
|
2360
|
+
sizes are measured over **closed** bars, so a tick never re-grids the series;
|
|
2361
|
+
an ATR box changes once per completed bar. (Before 1.4.0-beta.4 the view only
|
|
2362
|
+
ever moved forward, so these types went blank after a while on a live feed.)
|
|
2340
2363
|
|
|
2341
2364
|
### Parameters
|
|
2342
2365
|
|
|
@@ -2362,12 +2385,12 @@ Renko leaves Kagi and P&F alone.
|
|
|
2362
2385
|
|
|
2363
2386
|
| `BoxSizeMode` | Meaning |
|
|
2364
2387
|
| --- | --- |
|
|
2365
|
-
| `{ kind: 'atr', length }` | Wilder ATR over the last `length` bars. Default `14`. |
|
|
2388
|
+
| `{ kind: 'atr', length }` | Wilder ATR over the last `length` closed bars (the forming bar is excluded). Default `14`. |
|
|
2366
2389
|
| `{ kind: 'fixed', size }` | A fixed price distance. |
|
|
2367
|
-
| `{ kind: 'percent', percent }` | A percentage of the
|
|
2390
|
+
| `{ kind: 'percent', percent }` | A percentage of the last closed bar's close. |
|
|
2368
2391
|
|
|
2369
|
-
When a box cannot be sized — fewer than `length + 1` bars for the ATR, or
|
|
2370
|
-
non-positive size — the transform returns **no elements** rather than guessing. A
|
|
2392
|
+
When a box cannot be sized — fewer than `length + 1` closed bars for the ATR, or
|
|
2393
|
+
a non-positive size — the transform returns **no elements** rather than guessing. A
|
|
2371
2394
|
wrong box size does not look wrong; it looks like a different market.
|
|
2372
2395
|
|
|
2373
2396
|
### The rules each type implements
|
package/dist/index.cjs
CHANGED
|
@@ -159,6 +159,28 @@ var DataStore = class {
|
|
|
159
159
|
}
|
|
160
160
|
};
|
|
161
161
|
|
|
162
|
+
// src/core/viewportSync.ts
|
|
163
|
+
function maxViewportStart(total, width) {
|
|
164
|
+
return Math.max(0, total - Math.floor(width / 2));
|
|
165
|
+
}
|
|
166
|
+
function syncViewportToElementCount(viewport, before, after) {
|
|
167
|
+
if (after === before || after <= 0) return viewport;
|
|
168
|
+
const width = viewport.endIndex - viewport.startIndex;
|
|
169
|
+
if (viewport.endIndex >= before) {
|
|
170
|
+
const slack = viewport.endIndex - before;
|
|
171
|
+
const endIndex = after + slack;
|
|
172
|
+
return { ...viewport, startIndex: endIndex - width, endIndex };
|
|
173
|
+
}
|
|
174
|
+
return clampViewportToData(viewport, after);
|
|
175
|
+
}
|
|
176
|
+
function clampViewportToData(viewport, total) {
|
|
177
|
+
if (total <= 0) return viewport;
|
|
178
|
+
const width = viewport.endIndex - viewport.startIndex;
|
|
179
|
+
const maxStart = maxViewportStart(total, width);
|
|
180
|
+
if (viewport.startIndex <= maxStart) return viewport;
|
|
181
|
+
return { ...viewport, startIndex: maxStart, endIndex: maxStart + width };
|
|
182
|
+
}
|
|
183
|
+
|
|
162
184
|
// src/core/ScaleManager.ts
|
|
163
185
|
var _ScaleManager = class _ScaleManager {
|
|
164
186
|
// ── X axis: bar index → canvas pixel ──────────────────────────────────────
|
|
@@ -15725,41 +15747,54 @@ var TrendAngle = class {
|
|
|
15725
15747
|
onPointsEdited() {
|
|
15726
15748
|
this.pendingMeasure = true;
|
|
15727
15749
|
}
|
|
15750
|
+
/** Whether the angle is (re)measured from the points rather than applied to them. */
|
|
15751
|
+
get measuring() {
|
|
15752
|
+
return this.angle === void 0 || this.pendingMeasure || !this.complete;
|
|
15753
|
+
}
|
|
15728
15754
|
/**
|
|
15729
|
-
*
|
|
15755
|
+
* The pixel geometry of the line on the given scales — a pure projection
|
|
15756
|
+
* of the stored points, with no side effects, shared by rendering and
|
|
15757
|
+
* hit-testing so a tap lands on what was drawn.
|
|
15730
15758
|
*
|
|
15731
15759
|
* - no stored angle, the drawing is still being placed, or the user has
|
|
15732
|
-
* just moved a point:
|
|
15733
|
-
*
|
|
15734
|
-
*
|
|
15735
|
-
*
|
|
15736
|
-
* length is whatever the stored point projects to on this screen.
|
|
15760
|
+
* just moved a point: the line joins the points as they are, and
|
|
15761
|
+
* `measured` is the angle that run makes on this screen;
|
|
15762
|
+
* - otherwise the line leaves the first point at the stored angle and
|
|
15763
|
+
* runs as far as the second point projects on this screen.
|
|
15737
15764
|
*/
|
|
15738
|
-
|
|
15765
|
+
geometry(scale, viewport, priceRange, chartW, chartH) {
|
|
15739
15766
|
const [p1, p2] = this.points;
|
|
15740
15767
|
const x1 = scale.xToPixel(p1.barIndex, viewport, chartW);
|
|
15741
15768
|
const y1 = scale.yToPixel(p1.price, priceRange, chartH);
|
|
15742
15769
|
const px2 = scale.xToPixel(p2.barIndex, viewport, chartW);
|
|
15743
15770
|
const py2 = scale.yToPixel(p2.price, priceRange, chartH);
|
|
15744
15771
|
const measured = Math.atan2(-(py2 - y1), px2 - x1) * 180 / Math.PI;
|
|
15745
|
-
if (this.angle === void 0
|
|
15746
|
-
|
|
15747
|
-
this.angle = measured;
|
|
15748
|
-
this.pendingMeasure = false;
|
|
15749
|
-
}
|
|
15750
|
-
return { x1, y1, x2: px2, y2: py2, angleDeg: this.angle ?? measured };
|
|
15772
|
+
if (this.measuring || this.angle === void 0) {
|
|
15773
|
+
return { x1, y1, x2: px2, y2: py2, angleDeg: this.angle ?? measured, measured };
|
|
15751
15774
|
}
|
|
15752
15775
|
const len = Math.hypot(px2 - x1, py2 - y1);
|
|
15753
15776
|
const rad = this.angle * Math.PI / 180;
|
|
15754
15777
|
const x22 = x1 + len * Math.cos(rad);
|
|
15755
15778
|
const y22 = y1 - len * Math.sin(rad);
|
|
15756
|
-
if (Number.isFinite(x22)
|
|
15757
|
-
this.
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
|
|
15779
|
+
if (!Number.isFinite(x22) || !Number.isFinite(y22)) {
|
|
15780
|
+
return { x1, y1, x2: px2, y2: py2, angleDeg: this.angle, measured };
|
|
15781
|
+
}
|
|
15782
|
+
return { x1, y1, x2: x22, y2: y22, angleDeg: this.angle, measured };
|
|
15783
|
+
}
|
|
15784
|
+
/**
|
|
15785
|
+
* The geometry to draw, recording the angle when it is due to be measured.
|
|
15786
|
+
* The stored points are left exactly as the user placed them.
|
|
15787
|
+
*/
|
|
15788
|
+
resolve(scale, viewport, priceRange, chartW, chartH) {
|
|
15789
|
+
const g = this.geometry(scale, viewport, priceRange, chartW, chartH);
|
|
15790
|
+
if (this.measuring) {
|
|
15791
|
+
if (Number.isFinite(g.measured) && chartW > 0 && chartH > 0) {
|
|
15792
|
+
this.angle = g.measured;
|
|
15793
|
+
this.pendingMeasure = false;
|
|
15794
|
+
}
|
|
15795
|
+
return { x1: g.x1, y1: g.y1, x2: g.x2, y2: g.y2, angleDeg: this.angle ?? g.measured };
|
|
15761
15796
|
}
|
|
15762
|
-
return
|
|
15797
|
+
return g;
|
|
15763
15798
|
}
|
|
15764
15799
|
render(ctx, scale, viewport, priceRange, chartW, chartH, isSelected, _pricePrecision, theme) {
|
|
15765
15800
|
if (this.points.length < 2) return;
|
|
@@ -15803,18 +15838,15 @@ var TrendAngle = class {
|
|
|
15803
15838
|
}
|
|
15804
15839
|
hitTest(x, y, scale, viewport, priceRange, chartW, chartH) {
|
|
15805
15840
|
if (this.points.length < 2) return false;
|
|
15806
|
-
const
|
|
15807
|
-
const x1 = scale.xToPixel(p1.barIndex, viewport, chartW);
|
|
15808
|
-
const y1 = scale.yToPixel(p1.price, priceRange, chartH);
|
|
15809
|
-
const x22 = scale.xToPixel(p2.barIndex, viewport, chartW);
|
|
15810
|
-
const y22 = scale.yToPixel(p2.price, priceRange, chartH);
|
|
15841
|
+
const { x1, y1, x2: x22, y2: y22 } = this.geometry(scale, viewport, priceRange, chartW, chartH);
|
|
15811
15842
|
return pointToSegmentDist3(x, y, x1, y1, x22, y22) <= HIT_TOLERANCE24;
|
|
15812
15843
|
}
|
|
15813
15844
|
getHandle(x, y, scale, viewport, priceRange, chartW, chartH) {
|
|
15814
15845
|
if (this.points.length < 2) return null;
|
|
15846
|
+
const { x1, y1, x2: x22, y2: y22 } = this.geometry(scale, viewport, priceRange, chartW, chartH);
|
|
15815
15847
|
const pts = [
|
|
15816
|
-
{ px:
|
|
15817
|
-
{ px:
|
|
15848
|
+
{ px: x1, py: y1 },
|
|
15849
|
+
{ px: x22, py: y22 }
|
|
15818
15850
|
];
|
|
15819
15851
|
for (let i = 0; i < pts.length; i++) {
|
|
15820
15852
|
if (Math.hypot(x - pts[i].px, y - pts[i].py) <= HIT_TOLERANCE24 + 2) {
|
|
@@ -27448,15 +27480,19 @@ function resolveBoxSize(bars, mode) {
|
|
|
27448
27480
|
case "fixed":
|
|
27449
27481
|
return mode.size > 0 ? mode.size : null;
|
|
27450
27482
|
case "percent": {
|
|
27451
|
-
const
|
|
27483
|
+
const closed = closedBars(bars);
|
|
27484
|
+
const last = closed[closed.length - 1];
|
|
27452
27485
|
if (!last || mode.percent <= 0) return null;
|
|
27453
27486
|
const size = last.close * mode.percent / 100;
|
|
27454
27487
|
return size > 0 ? size : null;
|
|
27455
27488
|
}
|
|
27456
27489
|
case "atr":
|
|
27457
|
-
return atrValue(bars, mode.length);
|
|
27490
|
+
return atrValue(closedBars(bars), mode.length);
|
|
27458
27491
|
}
|
|
27459
27492
|
}
|
|
27493
|
+
function closedBars(bars) {
|
|
27494
|
+
return bars.length > 1 ? bars.slice(0, -1) : bars;
|
|
27495
|
+
}
|
|
27460
27496
|
function enforceIncreasingTimes(bars) {
|
|
27461
27497
|
for (let i = 1; i < bars.length; i++) {
|
|
27462
27498
|
const prev = bars[i - 1];
|
|
@@ -37044,6 +37080,11 @@ function initialVisibleBars(mobileBarDivisor = 2, forceApply = false, defaultBar
|
|
|
37044
37080
|
}
|
|
37045
37081
|
var LOOKAHEAD = 4;
|
|
37046
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;
|
|
37047
37088
|
function lastIndexAtOrBefore(bars, time) {
|
|
37048
37089
|
let lo = 0, hi = bars.length - 1, ans = -1;
|
|
37049
37090
|
while (lo <= hi) {
|
|
@@ -37346,6 +37387,16 @@ var _ChartEngine = class _ChartEngine {
|
|
|
37346
37387
|
*/
|
|
37347
37388
|
this._viewportPersistTimer = null;
|
|
37348
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;
|
|
37349
37400
|
/** Per-instance sub-pane Y-scale factors, keyed by ActiveIndicatorState.instanceId. */
|
|
37350
37401
|
this.subPaneScaleFactors = /* @__PURE__ */ new Map();
|
|
37351
37402
|
this._mobileCrosshairActive = false;
|
|
@@ -37963,6 +38014,16 @@ var _ChartEngine = class _ChartEngine {
|
|
|
37963
38014
|
const y = touch.clientY - rect.top;
|
|
37964
38015
|
const { chartW, priceH } = this.getChartLayout();
|
|
37965
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
|
+
}
|
|
37966
38027
|
const axisTapHit = hitTestAxisTagCluster(x, y, this.axisTagClusterAreas);
|
|
37967
38028
|
if (axisTapHit) {
|
|
37968
38029
|
e.preventDefault();
|
|
@@ -37979,6 +38040,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
37979
38040
|
e.preventDefault();
|
|
37980
38041
|
if (y <= priceH) {
|
|
37981
38042
|
this.axisDrag = { axis: "price", startPos: y, startSize: this.priceScaleFactor };
|
|
38043
|
+
this._touchDown = { x, y, at: now };
|
|
37982
38044
|
} else {
|
|
37983
38045
|
const subPaneStates = this.activeIndicators.filter(
|
|
37984
38046
|
(s) => s.indicator.pane === "sub"
|
|
@@ -38161,6 +38223,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
38161
38223
|
}
|
|
38162
38224
|
}
|
|
38163
38225
|
}
|
|
38226
|
+
this._touchDown = { x, y, at: now };
|
|
38164
38227
|
this.drawingManager.selectDrawing(null);
|
|
38165
38228
|
this.hideMobileDrawingFlyout();
|
|
38166
38229
|
if (this.expandedClusters.size > 0) {
|
|
@@ -38198,6 +38261,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
38198
38261
|
const point = this.touchToDrawingPoint(e.touches[0]);
|
|
38199
38262
|
const tp = this.touchToPixels(e.touches[0]);
|
|
38200
38263
|
this._lastDrawingTouchPoint = point;
|
|
38264
|
+
if (this.tapZoom) {
|
|
38265
|
+
e.preventDefault();
|
|
38266
|
+
this.moveTapZoom(tp.y);
|
|
38267
|
+
return;
|
|
38268
|
+
}
|
|
38201
38269
|
if (this.axisDrag) {
|
|
38202
38270
|
e.preventDefault();
|
|
38203
38271
|
const touch = e.touches[0];
|
|
@@ -38230,6 +38298,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
38230
38298
|
startIndex: newStart,
|
|
38231
38299
|
endIndex: newStart + newVisible
|
|
38232
38300
|
};
|
|
38301
|
+
this._targetViewport = { ...this.viewport };
|
|
38233
38302
|
}
|
|
38234
38303
|
this.scheduleRender();
|
|
38235
38304
|
return;
|
|
@@ -38373,6 +38442,9 @@ var _ChartEngine = class _ChartEngine {
|
|
|
38373
38442
|
* back to its start; an in-progress price drag is also cancelled defensively.
|
|
38374
38443
|
*/
|
|
38375
38444
|
this.onDrawingTouchCancel = (_e) => {
|
|
38445
|
+
this._touchDown = null;
|
|
38446
|
+
this._lastTap = null;
|
|
38447
|
+
if (this.tapZoom) this.endTapZoom();
|
|
38376
38448
|
if (this._longPressTimer !== null) {
|
|
38377
38449
|
clearTimeout(this._longPressTimer);
|
|
38378
38450
|
this._longPressTimer = null;
|
|
@@ -38406,6 +38478,22 @@ var _ChartEngine = class _ChartEngine {
|
|
|
38406
38478
|
this._longPressTimer = null;
|
|
38407
38479
|
this._longPressPoint = null;
|
|
38408
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
|
+
}
|
|
38409
38497
|
if (this._pendingTradeDrag) {
|
|
38410
38498
|
const { level } = this._pendingTradeDrag;
|
|
38411
38499
|
this._pendingTradeDrag = null;
|
|
@@ -38805,7 +38893,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
38805
38893
|
};
|
|
38806
38894
|
all[all.length - 1] = updated;
|
|
38807
38895
|
this.dataStore.invalidate();
|
|
38808
|
-
this.
|
|
38896
|
+
this.syncViewportToData(before);
|
|
38809
38897
|
} else {
|
|
38810
38898
|
const completedBar = last;
|
|
38811
38899
|
const newBarTime = Math.floor(tick.time / intervalMs) * intervalMs;
|
|
@@ -38824,7 +38912,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
38824
38912
|
);
|
|
38825
38913
|
}
|
|
38826
38914
|
const openingBar = all[all.length - 1];
|
|
38827
|
-
this.
|
|
38915
|
+
this.syncViewportToData(before);
|
|
38828
38916
|
this.emitter.emit("newBar", { completedBar, openingBar, intervalMs });
|
|
38829
38917
|
this.correctCompletedBar(completedBar.time, intervalMs);
|
|
38830
38918
|
if (newBarTime > completedBar.time + intervalMs) {
|
|
@@ -39660,12 +39748,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
39660
39748
|
this.applyScrollToEndBtnStyles();
|
|
39661
39749
|
this.scrollToEndBtn.addEventListener("click", () => this.scrollToEnd());
|
|
39662
39750
|
this.canvasWrap.appendChild(this.scrollToEndBtn);
|
|
39663
|
-
this.resetViewBtn = document.createElement("button");
|
|
39664
|
-
this.resetViewBtn.title = "Reset view";
|
|
39665
|
-
this.applyResetViewBtnStyles();
|
|
39666
|
-
this.resetViewBtn.addEventListener("click", () => this.resetView());
|
|
39667
|
-
this.canvasWrap.appendChild(this.resetViewBtn);
|
|
39668
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);
|
|
39669
39757
|
this.tradeDragHandler = new TradeDragHandler();
|
|
39670
39758
|
this.orderTimeDragHandler = new OrderLineTimeDragHandler();
|
|
39671
39759
|
if (config.onLevelDragEnd) {
|
|
@@ -39939,7 +40027,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
39939
40027
|
},
|
|
39940
40028
|
() => this.dataStore.length,
|
|
39941
40029
|
() => this.scheduleRender(),
|
|
39942
|
-
() => 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,
|
|
39943
40031
|
(deltaY) => {
|
|
39944
40032
|
if (this.priceScaleFactor === 1) return;
|
|
39945
40033
|
const { priceH } = this.getChartLayout();
|
|
@@ -40224,6 +40312,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
40224
40312
|
this.updateCompareRange();
|
|
40225
40313
|
this.scheduleRender();
|
|
40226
40314
|
}
|
|
40315
|
+
const clamped = clampViewportToData(this._targetViewport, this.dataStore.length);
|
|
40316
|
+
if (clamped !== this._targetViewport) {
|
|
40317
|
+
this._targetViewport = clamped;
|
|
40318
|
+
this.viewport = { ...clamped };
|
|
40319
|
+
this.scheduleRender();
|
|
40320
|
+
}
|
|
40227
40321
|
this.isFetching = false;
|
|
40228
40322
|
return this;
|
|
40229
40323
|
}
|
|
@@ -40410,15 +40504,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
40410
40504
|
const before = this.dataStore.length;
|
|
40411
40505
|
this.dataStore.raw.push({ ...bar });
|
|
40412
40506
|
this.dataStore.invalidate();
|
|
40413
|
-
|
|
40414
|
-
|
|
40415
|
-
this.viewport = {
|
|
40416
|
-
...this.viewport,
|
|
40417
|
-
startIndex: this.viewport.startIndex + grew,
|
|
40418
|
-
endIndex: this.viewport.endIndex + grew
|
|
40419
|
-
};
|
|
40420
|
-
this._targetViewport = { ...this.viewport };
|
|
40421
|
-
}
|
|
40507
|
+
this.syncViewportToData(before);
|
|
40508
|
+
this.viewport = { ...this._targetViewport };
|
|
40422
40509
|
r.time = bar.time;
|
|
40423
40510
|
this.recomputeIndicators();
|
|
40424
40511
|
this.updateReplayBar();
|
|
@@ -45136,6 +45223,10 @@ var _ChartEngine = class _ChartEngine {
|
|
|
45136
45223
|
}
|
|
45137
45224
|
render() {
|
|
45138
45225
|
if (this._isDestroyed) return;
|
|
45226
|
+
{
|
|
45227
|
+
const clamped = clampViewportToData(this._targetViewport, this.dataStore.length);
|
|
45228
|
+
if (clamped !== this._targetViewport) this._targetViewport = clamped;
|
|
45229
|
+
}
|
|
45139
45230
|
const { ctx, canvas, theme, viewport } = this;
|
|
45140
45231
|
const dpr = window.devicePixelRatio || 1;
|
|
45141
45232
|
const totalW = canvas.width;
|
|
@@ -45150,6 +45241,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
45150
45241
|
const absBars = this.dataStore.getVisible(viewport);
|
|
45151
45242
|
if (absBars.length === 0) {
|
|
45152
45243
|
this.drawCtx.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height);
|
|
45244
|
+
const target = this._targetViewport;
|
|
45245
|
+
if (!this._isPanning && (viewport.startIndex !== target.startIndex || viewport.endIndex !== target.endIndex)) {
|
|
45246
|
+
this.viewport = { ...target };
|
|
45247
|
+
this.scheduleRender();
|
|
45248
|
+
}
|
|
45153
45249
|
return;
|
|
45154
45250
|
}
|
|
45155
45251
|
let bars = absBars;
|
|
@@ -46595,20 +46691,31 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46595
46691
|
}, this.tickActivityMs);
|
|
46596
46692
|
}
|
|
46597
46693
|
/**
|
|
46598
|
-
*
|
|
46599
|
-
*
|
|
46600
|
-
*
|
|
46601
|
-
*
|
|
46602
|
-
*
|
|
46694
|
+
* Re-aligns the view after a raw-bar mutation changed the number of
|
|
46695
|
+
* displayed elements. `before` is `dataStore.length` captured before the
|
|
46696
|
+
* mutation. The rule lives in `core/viewportSync.ts`: a view tracking the
|
|
46697
|
+
* live edge keeps tracking it whether the count grew or shrank; a view
|
|
46698
|
+
* scrolled into history is only clamped so it cannot sit past the data.
|
|
46699
|
+
*
|
|
46700
|
+
* The old rule shifted the view forward by the growth and did nothing on
|
|
46701
|
+
* a shrink. A time-based series never shrinks, but under Renko / Line
|
|
46702
|
+
* Break / Kagi / P&F the forming bar completes an element on one tick and
|
|
46703
|
+
* takes it back on the next, so the view ratcheted ahead one element per
|
|
46704
|
+
* oscillation until it sat past the data — a blank chart "after a while".
|
|
46705
|
+
*
|
|
46706
|
+
* Under a price transform the elements are rebuilt rather than shifted,
|
|
46707
|
+
* so there is nothing for the per-frame lerp to animate between and the
|
|
46708
|
+
* view snaps. A time-based series keeps its one-bar slide.
|
|
46603
46709
|
*/
|
|
46604
|
-
|
|
46605
|
-
const
|
|
46606
|
-
|
|
46607
|
-
|
|
46608
|
-
|
|
46609
|
-
|
|
46610
|
-
|
|
46611
|
-
|
|
46710
|
+
syncViewportToData(before) {
|
|
46711
|
+
const synced = syncViewportToElementCount(
|
|
46712
|
+
this._targetViewport,
|
|
46713
|
+
before,
|
|
46714
|
+
this.dataStore.length
|
|
46715
|
+
);
|
|
46716
|
+
if (synced === this._targetViewport) return;
|
|
46717
|
+
this._targetViewport = synced;
|
|
46718
|
+
if (isPriceTransformSeries(this.series)) this.viewport = { ...synced };
|
|
46612
46719
|
}
|
|
46613
46720
|
/** Fetch bars that fall in a gap between the last historical bar and the first live bar.
|
|
46614
46721
|
* This happens when the system clock was behind feed time during the initial data load. */
|
|
@@ -46629,12 +46736,14 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46629
46736
|
const liveBar = all[all.length - 1];
|
|
46630
46737
|
const before = this.dataStore.length;
|
|
46631
46738
|
let insertCount = 0;
|
|
46739
|
+
let replaceCount = 0;
|
|
46632
46740
|
for (const bar of bars) {
|
|
46633
46741
|
if (bar.time >= liveBar.time) continue;
|
|
46634
46742
|
const existingIdx = all.findIndex((b) => b.time === bar.time);
|
|
46635
46743
|
if (existingIdx !== -1) {
|
|
46636
46744
|
all[existingIdx] = bar;
|
|
46637
46745
|
this.dataStore.invalidate();
|
|
46746
|
+
replaceCount++;
|
|
46638
46747
|
} else {
|
|
46639
46748
|
const insertAt = all.findIndex((b) => b.time > bar.time);
|
|
46640
46749
|
all.splice(insertAt === -1 ? all.length - 1 : insertAt, 0, bar);
|
|
@@ -46642,13 +46751,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46642
46751
|
insertCount++;
|
|
46643
46752
|
}
|
|
46644
46753
|
}
|
|
46645
|
-
|
|
46646
|
-
|
|
46647
|
-
this.viewport = {
|
|
46648
|
-
...this.viewport,
|
|
46649
|
-
startIndex: this.viewport.startIndex + grew,
|
|
46650
|
-
endIndex: this.viewport.endIndex + grew
|
|
46651
|
-
};
|
|
46754
|
+
if (insertCount > 0 || replaceCount > 0) {
|
|
46755
|
+
this.syncViewportToData(before);
|
|
46652
46756
|
this.reanchorDrawingsByTimestamp();
|
|
46653
46757
|
this.recomputeIndicators();
|
|
46654
46758
|
this.scheduleRender();
|
|
@@ -46669,6 +46773,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46669
46773
|
if (bars.length === 0) return;
|
|
46670
46774
|
const all = this.dataStore.raw;
|
|
46671
46775
|
const liveBarIndex = all.length - 1;
|
|
46776
|
+
const before = this.dataStore.length;
|
|
46672
46777
|
let changed = false;
|
|
46673
46778
|
for (const bar of bars) {
|
|
46674
46779
|
const idx = all.findIndex((b) => b.time === bar.time);
|
|
@@ -46679,6 +46784,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46679
46784
|
}
|
|
46680
46785
|
}
|
|
46681
46786
|
if (changed) {
|
|
46787
|
+
this.syncViewportToData(before);
|
|
46682
46788
|
this.recomputeIndicators();
|
|
46683
46789
|
this.scheduleRender();
|
|
46684
46790
|
}
|
|
@@ -46708,8 +46814,10 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46708
46814
|
const all = this.dataStore.raw;
|
|
46709
46815
|
const idx = all.findIndex((b) => b.time === barTime);
|
|
46710
46816
|
if (idx === -1) return;
|
|
46817
|
+
const before = this.dataStore.length;
|
|
46711
46818
|
all[idx] = bar;
|
|
46712
46819
|
this.dataStore.invalidate();
|
|
46820
|
+
this.syncViewportToData(before);
|
|
46713
46821
|
this.recomputeIndicators();
|
|
46714
46822
|
this.scheduleRender();
|
|
46715
46823
|
}
|
|
@@ -46830,6 +46938,55 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46830
46938
|
const all = this.dataStore.raw;
|
|
46831
46939
|
return all[all.length - 1]?.close ?? 0;
|
|
46832
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
|
+
}
|
|
46833
46990
|
resetPriceAxis() {
|
|
46834
46991
|
this.priceScaleFactor = 1;
|
|
46835
46992
|
this.pricePanOffset = 0;
|