acttrader-charts 1.4.0-beta.3 → 1.4.0-beta.4
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 +12 -5
- package/dist/index.cjs +115 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -16
- package/dist/index.d.ts +42 -16
- package/dist/index.js +115 -59
- 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 |
|
|
@@ -2337,6 +2337,13 @@ Three consequences worth knowing:
|
|
|
2337
2337
|
different number of elements.
|
|
2338
2338
|
- **Market price still comes off the raw feed.** Trade levels and P&L price
|
|
2339
2339
|
against the real last close, never a brick top.
|
|
2340
|
+
- **Live ticks move the element count both ways.** The forming bar completes a
|
|
2341
|
+
brick, line or column on one tick and takes it back on the next. The view
|
|
2342
|
+
keeps tracking the live edge either way — same width, same room past the
|
|
2343
|
+
last element — and a view scrolled into history stays put. Box and reversal
|
|
2344
|
+
sizes are measured over **closed** bars, so a tick never re-grids the series;
|
|
2345
|
+
an ATR box changes once per completed bar. (Before 1.4.0-beta.4 the view only
|
|
2346
|
+
ever moved forward, so these types went blank after a while on a live feed.)
|
|
2340
2347
|
|
|
2341
2348
|
### Parameters
|
|
2342
2349
|
|
|
@@ -2362,12 +2369,12 @@ Renko leaves Kagi and P&F alone.
|
|
|
2362
2369
|
|
|
2363
2370
|
| `BoxSizeMode` | Meaning |
|
|
2364
2371
|
| --- | --- |
|
|
2365
|
-
| `{ kind: 'atr', length }` | Wilder ATR over the last `length` bars. Default `14`. |
|
|
2372
|
+
| `{ kind: 'atr', length }` | Wilder ATR over the last `length` closed bars (the forming bar is excluded). Default `14`. |
|
|
2366
2373
|
| `{ kind: 'fixed', size }` | A fixed price distance. |
|
|
2367
|
-
| `{ kind: 'percent', percent }` | A percentage of the
|
|
2374
|
+
| `{ kind: 'percent', percent }` | A percentage of the last closed bar's close. |
|
|
2368
2375
|
|
|
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
|
|
2376
|
+
When a box cannot be sized — fewer than `length + 1` closed bars for the ATR, or
|
|
2377
|
+
a non-positive size — the transform returns **no elements** rather than guessing. A
|
|
2371
2378
|
wrong box size does not look wrong; it looks like a different market.
|
|
2372
2379
|
|
|
2373
2380
|
### 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];
|
|
@@ -38805,7 +38841,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
38805
38841
|
};
|
|
38806
38842
|
all[all.length - 1] = updated;
|
|
38807
38843
|
this.dataStore.invalidate();
|
|
38808
|
-
this.
|
|
38844
|
+
this.syncViewportToData(before);
|
|
38809
38845
|
} else {
|
|
38810
38846
|
const completedBar = last;
|
|
38811
38847
|
const newBarTime = Math.floor(tick.time / intervalMs) * intervalMs;
|
|
@@ -38824,7 +38860,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
38824
38860
|
);
|
|
38825
38861
|
}
|
|
38826
38862
|
const openingBar = all[all.length - 1];
|
|
38827
|
-
this.
|
|
38863
|
+
this.syncViewportToData(before);
|
|
38828
38864
|
this.emitter.emit("newBar", { completedBar, openingBar, intervalMs });
|
|
38829
38865
|
this.correctCompletedBar(completedBar.time, intervalMs);
|
|
38830
38866
|
if (newBarTime > completedBar.time + intervalMs) {
|
|
@@ -40224,6 +40260,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
40224
40260
|
this.updateCompareRange();
|
|
40225
40261
|
this.scheduleRender();
|
|
40226
40262
|
}
|
|
40263
|
+
const clamped = clampViewportToData(this._targetViewport, this.dataStore.length);
|
|
40264
|
+
if (clamped !== this._targetViewport) {
|
|
40265
|
+
this._targetViewport = clamped;
|
|
40266
|
+
this.viewport = { ...clamped };
|
|
40267
|
+
this.scheduleRender();
|
|
40268
|
+
}
|
|
40227
40269
|
this.isFetching = false;
|
|
40228
40270
|
return this;
|
|
40229
40271
|
}
|
|
@@ -40410,15 +40452,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
40410
40452
|
const before = this.dataStore.length;
|
|
40411
40453
|
this.dataStore.raw.push({ ...bar });
|
|
40412
40454
|
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
|
-
}
|
|
40455
|
+
this.syncViewportToData(before);
|
|
40456
|
+
this.viewport = { ...this._targetViewport };
|
|
40422
40457
|
r.time = bar.time;
|
|
40423
40458
|
this.recomputeIndicators();
|
|
40424
40459
|
this.updateReplayBar();
|
|
@@ -45136,6 +45171,10 @@ var _ChartEngine = class _ChartEngine {
|
|
|
45136
45171
|
}
|
|
45137
45172
|
render() {
|
|
45138
45173
|
if (this._isDestroyed) return;
|
|
45174
|
+
{
|
|
45175
|
+
const clamped = clampViewportToData(this._targetViewport, this.dataStore.length);
|
|
45176
|
+
if (clamped !== this._targetViewport) this._targetViewport = clamped;
|
|
45177
|
+
}
|
|
45139
45178
|
const { ctx, canvas, theme, viewport } = this;
|
|
45140
45179
|
const dpr = window.devicePixelRatio || 1;
|
|
45141
45180
|
const totalW = canvas.width;
|
|
@@ -45150,6 +45189,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
45150
45189
|
const absBars = this.dataStore.getVisible(viewport);
|
|
45151
45190
|
if (absBars.length === 0) {
|
|
45152
45191
|
this.drawCtx.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height);
|
|
45192
|
+
const target = this._targetViewport;
|
|
45193
|
+
if (!this._isPanning && (viewport.startIndex !== target.startIndex || viewport.endIndex !== target.endIndex)) {
|
|
45194
|
+
this.viewport = { ...target };
|
|
45195
|
+
this.scheduleRender();
|
|
45196
|
+
}
|
|
45153
45197
|
return;
|
|
45154
45198
|
}
|
|
45155
45199
|
let bars = absBars;
|
|
@@ -46595,20 +46639,31 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46595
46639
|
}, this.tickActivityMs);
|
|
46596
46640
|
}
|
|
46597
46641
|
/**
|
|
46598
|
-
*
|
|
46599
|
-
*
|
|
46600
|
-
*
|
|
46601
|
-
*
|
|
46602
|
-
*
|
|
46642
|
+
* Re-aligns the view after a raw-bar mutation changed the number of
|
|
46643
|
+
* displayed elements. `before` is `dataStore.length` captured before the
|
|
46644
|
+
* mutation. The rule lives in `core/viewportSync.ts`: a view tracking the
|
|
46645
|
+
* live edge keeps tracking it whether the count grew or shrank; a view
|
|
46646
|
+
* scrolled into history is only clamped so it cannot sit past the data.
|
|
46647
|
+
*
|
|
46648
|
+
* The old rule shifted the view forward by the growth and did nothing on
|
|
46649
|
+
* a shrink. A time-based series never shrinks, but under Renko / Line
|
|
46650
|
+
* Break / Kagi / P&F the forming bar completes an element on one tick and
|
|
46651
|
+
* takes it back on the next, so the view ratcheted ahead one element per
|
|
46652
|
+
* oscillation until it sat past the data — a blank chart "after a while".
|
|
46653
|
+
*
|
|
46654
|
+
* Under a price transform the elements are rebuilt rather than shifted,
|
|
46655
|
+
* so there is nothing for the per-frame lerp to animate between and the
|
|
46656
|
+
* view snaps. A time-based series keeps its one-bar slide.
|
|
46603
46657
|
*/
|
|
46604
|
-
|
|
46605
|
-
const
|
|
46606
|
-
|
|
46607
|
-
|
|
46608
|
-
|
|
46609
|
-
|
|
46610
|
-
|
|
46611
|
-
|
|
46658
|
+
syncViewportToData(before) {
|
|
46659
|
+
const synced = syncViewportToElementCount(
|
|
46660
|
+
this._targetViewport,
|
|
46661
|
+
before,
|
|
46662
|
+
this.dataStore.length
|
|
46663
|
+
);
|
|
46664
|
+
if (synced === this._targetViewport) return;
|
|
46665
|
+
this._targetViewport = synced;
|
|
46666
|
+
if (isPriceTransformSeries(this.series)) this.viewport = { ...synced };
|
|
46612
46667
|
}
|
|
46613
46668
|
/** Fetch bars that fall in a gap between the last historical bar and the first live bar.
|
|
46614
46669
|
* This happens when the system clock was behind feed time during the initial data load. */
|
|
@@ -46629,12 +46684,14 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46629
46684
|
const liveBar = all[all.length - 1];
|
|
46630
46685
|
const before = this.dataStore.length;
|
|
46631
46686
|
let insertCount = 0;
|
|
46687
|
+
let replaceCount = 0;
|
|
46632
46688
|
for (const bar of bars) {
|
|
46633
46689
|
if (bar.time >= liveBar.time) continue;
|
|
46634
46690
|
const existingIdx = all.findIndex((b) => b.time === bar.time);
|
|
46635
46691
|
if (existingIdx !== -1) {
|
|
46636
46692
|
all[existingIdx] = bar;
|
|
46637
46693
|
this.dataStore.invalidate();
|
|
46694
|
+
replaceCount++;
|
|
46638
46695
|
} else {
|
|
46639
46696
|
const insertAt = all.findIndex((b) => b.time > bar.time);
|
|
46640
46697
|
all.splice(insertAt === -1 ? all.length - 1 : insertAt, 0, bar);
|
|
@@ -46642,13 +46699,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46642
46699
|
insertCount++;
|
|
46643
46700
|
}
|
|
46644
46701
|
}
|
|
46645
|
-
|
|
46646
|
-
|
|
46647
|
-
this.viewport = {
|
|
46648
|
-
...this.viewport,
|
|
46649
|
-
startIndex: this.viewport.startIndex + grew,
|
|
46650
|
-
endIndex: this.viewport.endIndex + grew
|
|
46651
|
-
};
|
|
46702
|
+
if (insertCount > 0 || replaceCount > 0) {
|
|
46703
|
+
this.syncViewportToData(before);
|
|
46652
46704
|
this.reanchorDrawingsByTimestamp();
|
|
46653
46705
|
this.recomputeIndicators();
|
|
46654
46706
|
this.scheduleRender();
|
|
@@ -46669,6 +46721,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46669
46721
|
if (bars.length === 0) return;
|
|
46670
46722
|
const all = this.dataStore.raw;
|
|
46671
46723
|
const liveBarIndex = all.length - 1;
|
|
46724
|
+
const before = this.dataStore.length;
|
|
46672
46725
|
let changed = false;
|
|
46673
46726
|
for (const bar of bars) {
|
|
46674
46727
|
const idx = all.findIndex((b) => b.time === bar.time);
|
|
@@ -46679,6 +46732,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46679
46732
|
}
|
|
46680
46733
|
}
|
|
46681
46734
|
if (changed) {
|
|
46735
|
+
this.syncViewportToData(before);
|
|
46682
46736
|
this.recomputeIndicators();
|
|
46683
46737
|
this.scheduleRender();
|
|
46684
46738
|
}
|
|
@@ -46708,8 +46762,10 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46708
46762
|
const all = this.dataStore.raw;
|
|
46709
46763
|
const idx = all.findIndex((b) => b.time === barTime);
|
|
46710
46764
|
if (idx === -1) return;
|
|
46765
|
+
const before = this.dataStore.length;
|
|
46711
46766
|
all[idx] = bar;
|
|
46712
46767
|
this.dataStore.invalidate();
|
|
46768
|
+
this.syncViewportToData(before);
|
|
46713
46769
|
this.recomputeIndicators();
|
|
46714
46770
|
this.scheduleRender();
|
|
46715
46771
|
}
|