acttrader-charts 1.4.0-beta.2 → 1.4.0-beta.3
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 +9 -1
- package/dist/{MACD-qgMFBxWV.d.cts → MACD-CUfZXApv.d.cts} +16 -0
- package/dist/{MACD-qgMFBxWV.d.ts → MACD-CUfZXApv.d.ts} +16 -0
- package/dist/index.cjs +202 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -2
- package/dist/index.d.ts +78 -2
- package/dist/index.js +202 -28
- package/dist/index.js.map +1 -1
- package/dist/indicators/index.d.cts +2 -2
- package/dist/indicators/index.d.ts +2 -2
- package/dist/webview/chart.html +95 -95
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -983,6 +983,7 @@ chart.setVolume(show: boolean): this
|
|
|
983
983
|
|
|
984
984
|
```ts
|
|
985
985
|
chart.addIndicator(indicator: IIndicator, initialParams?: IndicatorParams): this
|
|
986
|
+
chart.addIndicatorByName(shortName: string, params?: IndicatorParams): void // params from a host are normalised to the study's schema first: "20" → 20 (clamped to min/max), "Close" → 'close', TradingView's `length` → `period` — so a native settings sheet draws the same line as the web
|
|
986
987
|
chart.addIndicatorByName(shortName: string, params?: IndicatorParams): void
|
|
987
988
|
chart.removeIndicator(idOrShortName: string): this
|
|
988
989
|
```
|
|
@@ -1202,7 +1203,7 @@ Sub-pane heights are user-resizable by dragging the separator between panes.
|
|
|
1202
1203
|
| `horizontalLine` | Horizontal Line | Single click |
|
|
1203
1204
|
| `verticalLine` | Vertical Line | Single click |
|
|
1204
1205
|
| `trendLine` | Trend Line | 2 clicks |
|
|
1205
|
-
| `trendAngle` | Trend Angle | 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
1207
|
| `crossLine` | Cross Line | Single click (H+V) |
|
|
1207
1208
|
| `parallelChannel` | Parallel Channel | 3 clicks |
|
|
1208
1209
|
| `flatChannel` | Flat Channel | 2 clicks |
|
|
@@ -2280,6 +2281,13 @@ It flips to the other side of the touch rather than running off an edge, never
|
|
|
2280
2281
|
takes pointer events (the drag is still in progress), and omits volume rather
|
|
2281
2282
|
than printing a zero when the feed carries none. Default `false`.
|
|
2282
2283
|
|
|
2284
|
+
The long press itself (500 ms, finger still within 6 px) arms the crosshair on
|
|
2285
|
+
every touch platform. The chart claims that touch with `preventDefault()` so a
|
|
2286
|
+
WebView's own long-press gesture — WebKit's text-selection / callout recogniser
|
|
2287
|
+
on iOS, which used to cancel the touch at the same moment and left iPhones with
|
|
2288
|
+
no crosshair at all — cannot take it over; a cancel that still arrives after
|
|
2289
|
+
300 ms of holding counts as the long press.
|
|
2290
|
+
|
|
2283
2291
|
### Native wrapper parity
|
|
2284
2292
|
|
|
2285
2293
|
The four line tools are new `DrawingToolType` strings, so `setDrawingTool('ray')`
|
|
@@ -2492,6 +2492,12 @@ interface IDrawing {
|
|
|
2492
2492
|
visible?: boolean;
|
|
2493
2493
|
locked?: boolean;
|
|
2494
2494
|
text?: string;
|
|
2495
|
+
/**
|
|
2496
|
+
* Trend Angle only: the line's angle in degrees from the horizontal — the
|
|
2497
|
+
* drawing's defining property, kept through zooms and carried across
|
|
2498
|
+
* platforms so the label reads the same everywhere. Other tools leave it unset.
|
|
2499
|
+
*/
|
|
2500
|
+
angle?: number;
|
|
2495
2501
|
/** Optional per-instance style override; falls back to theme defaults when absent. */
|
|
2496
2502
|
style?: AnyDrawingStyle;
|
|
2497
2503
|
render(ctx: CanvasRenderingContext2D, scale: ScaleManager, viewport: Viewport, priceRange: PriceRange, chartW: number, chartH: number, isSelected: boolean, pricePrecision?: number, theme?: ChartTheme, context?: DrawingRenderContext): void;
|
|
@@ -2511,6 +2517,14 @@ interface IDrawing {
|
|
|
2511
2517
|
* a third time.
|
|
2512
2518
|
*/
|
|
2513
2519
|
onPlaced?(): void;
|
|
2520
|
+
/**
|
|
2521
|
+
* Called after the user moved this drawing's points — while placing it,
|
|
2522
|
+
* dragging the whole drawing, or dragging a handle. Not called when the
|
|
2523
|
+
* engine re-anchors points to new data (reload, scroll-back, timeframe
|
|
2524
|
+
* change), so a tool that derives a property from where the user put the
|
|
2525
|
+
* points — Trend Angle's angle — can tell the two apart.
|
|
2526
|
+
*/
|
|
2527
|
+
onPointsEdited?(): void;
|
|
2514
2528
|
/** Returns the handle under (x, y), or null if none */
|
|
2515
2529
|
getHandle(x: number, y: number, scale: ScaleManager, viewport: Viewport, priceRange: PriceRange, chartW: number, chartH: number): DrawingHandle | null;
|
|
2516
2530
|
/**
|
|
@@ -2564,6 +2578,8 @@ interface SerializedDrawing {
|
|
|
2564
2578
|
visible?: boolean;
|
|
2565
2579
|
text?: string;
|
|
2566
2580
|
locked?: boolean;
|
|
2581
|
+
/** Trend Angle: degrees from the horizontal. Present once the angle has been measured; see `IDrawing.angle`. */
|
|
2582
|
+
angle?: number;
|
|
2567
2583
|
/** Optional per-instance style override; absent for drawings using theme defaults. */
|
|
2568
2584
|
style?: AnyDrawingStyle;
|
|
2569
2585
|
}
|
|
@@ -2492,6 +2492,12 @@ interface IDrawing {
|
|
|
2492
2492
|
visible?: boolean;
|
|
2493
2493
|
locked?: boolean;
|
|
2494
2494
|
text?: string;
|
|
2495
|
+
/**
|
|
2496
|
+
* Trend Angle only: the line's angle in degrees from the horizontal — the
|
|
2497
|
+
* drawing's defining property, kept through zooms and carried across
|
|
2498
|
+
* platforms so the label reads the same everywhere. Other tools leave it unset.
|
|
2499
|
+
*/
|
|
2500
|
+
angle?: number;
|
|
2495
2501
|
/** Optional per-instance style override; falls back to theme defaults when absent. */
|
|
2496
2502
|
style?: AnyDrawingStyle;
|
|
2497
2503
|
render(ctx: CanvasRenderingContext2D, scale: ScaleManager, viewport: Viewport, priceRange: PriceRange, chartW: number, chartH: number, isSelected: boolean, pricePrecision?: number, theme?: ChartTheme, context?: DrawingRenderContext): void;
|
|
@@ -2511,6 +2517,14 @@ interface IDrawing {
|
|
|
2511
2517
|
* a third time.
|
|
2512
2518
|
*/
|
|
2513
2519
|
onPlaced?(): void;
|
|
2520
|
+
/**
|
|
2521
|
+
* Called after the user moved this drawing's points — while placing it,
|
|
2522
|
+
* dragging the whole drawing, or dragging a handle. Not called when the
|
|
2523
|
+
* engine re-anchors points to new data (reload, scroll-back, timeframe
|
|
2524
|
+
* change), so a tool that derives a property from where the user put the
|
|
2525
|
+
* points — Trend Angle's angle — can tell the two apart.
|
|
2526
|
+
*/
|
|
2527
|
+
onPointsEdited?(): void;
|
|
2514
2528
|
/** Returns the handle under (x, y), or null if none */
|
|
2515
2529
|
getHandle(x: number, y: number, scale: ScaleManager, viewport: Viewport, priceRange: PriceRange, chartW: number, chartH: number): DrawingHandle | null;
|
|
2516
2530
|
/**
|
|
@@ -2564,6 +2578,8 @@ interface SerializedDrawing {
|
|
|
2564
2578
|
visible?: boolean;
|
|
2565
2579
|
text?: string;
|
|
2566
2580
|
locked?: boolean;
|
|
2581
|
+
/** Trend Angle: degrees from the horizontal. Present once the angle has been measured; see `IDrawing.angle`. */
|
|
2582
|
+
angle?: number;
|
|
2567
2583
|
/** Optional per-instance style override; absent for drawings using theme defaults. */
|
|
2568
2584
|
style?: AnyDrawingStyle;
|
|
2569
2585
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -2632,7 +2632,7 @@ function isPriceDraggableLevel(level) {
|
|
|
2632
2632
|
function passiveBracketsVisible(alwaysShowBrackets, activeLabel) {
|
|
2633
2633
|
return alwaysShowBrackets && activeLabel === null;
|
|
2634
2634
|
}
|
|
2635
|
-
function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH, precision, hoveredLabel = null, draggingLabel = null, dragNewPrice = null, selectedLabel = null, pendingLabels = /* @__PURE__ */ new Set(), positionStyle = "line", hideConfirmCancel = false, draftOrderLabel = null, draftBracketPnl = {}, expandedClusters = /* @__PURE__ */ new Set(), clusterThresholdPx = 0, showQuantityField = false, dragNewSLPrice = null, dragNewTPPrice = null, buttonScale = 1, alwaysShowBrackets = false, anchorXResolver = null, timeDragEnabled = false, bracketLabel = DEFAULT_BRACKET_LABEL, badgeScale = 1, badgeAnchor = "auto") {
|
|
2635
|
+
function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH, precision, hoveredLabel = null, draggingLabel = null, dragNewPrice = null, selectedLabel = null, pendingLabels = /* @__PURE__ */ new Set(), positionStyle = "line", hideConfirmCancel = false, draftOrderLabel = null, draftBracketPnl = {}, expandedClusters = /* @__PURE__ */ new Set(), clusterThresholdPx = 0, showQuantityField = false, dragNewSLPrice = null, dragNewTPPrice = null, buttonScale = 1, alwaysShowBrackets = false, anchorXResolver = null, timeDragEnabled = false, bracketLabel = DEFAULT_BRACKET_LABEL, badgeScale = 1, badgeAnchor = "auto", topInset = 0) {
|
|
2636
2636
|
if (levels.length === 0)
|
|
2637
2637
|
return {
|
|
2638
2638
|
hitAreas: [],
|
|
@@ -2710,6 +2710,7 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
2710
2710
|
measured.sort((a, b) => a.y - b.y);
|
|
2711
2711
|
const placed = [];
|
|
2712
2712
|
const layouts = /* @__PURE__ */ new Map();
|
|
2713
|
+
const legendDisplaced = /* @__PURE__ */ new Set();
|
|
2713
2714
|
for (const m2 of measured) {
|
|
2714
2715
|
if (collapsedLabels.has(m2.level.label)) continue;
|
|
2715
2716
|
const ec = labelToExpandedCluster.get(m2.level.label);
|
|
@@ -2723,11 +2724,17 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
2723
2724
|
}
|
|
2724
2725
|
}
|
|
2725
2726
|
let boxY = m2.y - m2.boxH / 2;
|
|
2727
|
+
const underLegend = m2.boxH > 0 && boxY < topInset;
|
|
2728
|
+
if (underLegend) {
|
|
2729
|
+
boxY = topInset;
|
|
2730
|
+
legendDisplaced.add(m2.level.label);
|
|
2731
|
+
}
|
|
2732
|
+
const step = underLegend ? m2.boxH + 2 : -(m2.boxH + 2);
|
|
2726
2733
|
let attempts = 0;
|
|
2727
2734
|
while (attempts < placed.length + 1) {
|
|
2728
2735
|
const overlaps = placed.some((p) => rectsOverlapY(boxY, m2.boxH, p.boxY, p.boxH));
|
|
2729
2736
|
if (!overlaps) break;
|
|
2730
|
-
boxY
|
|
2737
|
+
boxY += step;
|
|
2731
2738
|
attempts++;
|
|
2732
2739
|
}
|
|
2733
2740
|
placed.push({ label: m2.level.label, boxY, boxH: m2.boxH });
|
|
@@ -2862,6 +2869,11 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
2862
2869
|
const ec = labelToExpandedCluster.get(level.label);
|
|
2863
2870
|
if (ec && layout.boxY !== layout.y - boxH / 2) {
|
|
2864
2871
|
drawFanConnector(ctx, layout.boxY + boxH / 2, layout.y, chartW, theme);
|
|
2872
|
+
} else if (legendDisplaced.has(level.label) && layout.boxY > layout.y) {
|
|
2873
|
+
const w = layout.boxW;
|
|
2874
|
+
const anchorX = timeDragEnabled && anchorXResolver ? anchorXResolver(level) : null;
|
|
2875
|
+
const centreX = anchorX !== null ? Math.max(0, Math.min(chartW - w, Math.round(anchorX - w / 2))) + w / 2 : badgeAnchor === "left" ? m.leftMargin + w / 2 : chartW / 2;
|
|
2876
|
+
drawFanConnector(ctx, layout.boxY, layout.y, chartW, theme, centreX);
|
|
2865
2877
|
}
|
|
2866
2878
|
}
|
|
2867
2879
|
if (selectedLabel !== null && !layouts.has(selectedLabel)) {
|
|
@@ -3049,7 +3061,7 @@ function drawClusterBadge(ctx, cluster, chartW, theme, clusterBadgeAreas) {
|
|
|
3049
3061
|
h: BADGE_H
|
|
3050
3062
|
});
|
|
3051
3063
|
}
|
|
3052
|
-
function drawFanConnector(ctx, fanCenterY, realY, chartW, theme) {
|
|
3064
|
+
function drawFanConnector(ctx, fanCenterY, realY, chartW, theme, x = chartW - 8) {
|
|
3053
3065
|
if (Math.abs(fanCenterY - realY) < 2) return;
|
|
3054
3066
|
const mono = monoColor(theme);
|
|
3055
3067
|
ctx.save();
|
|
@@ -3057,7 +3069,7 @@ function drawFanConnector(ctx, fanCenterY, realY, chartW, theme) {
|
|
|
3057
3069
|
ctx.globalAlpha = 0.25;
|
|
3058
3070
|
ctx.lineWidth = 1;
|
|
3059
3071
|
ctx.setLineDash([2, 3]);
|
|
3060
|
-
const connectorX =
|
|
3072
|
+
const connectorX = x;
|
|
3061
3073
|
ctx.beginPath();
|
|
3062
3074
|
ctx.moveTo(connectorX, fanCenterY);
|
|
3063
3075
|
ctx.lineTo(connectorX, realY);
|
|
@@ -15698,19 +15710,61 @@ function pointToSegmentDist3(px, py, x1, y1, x22, y22) {
|
|
|
15698
15710
|
var TrendAngle = class {
|
|
15699
15711
|
constructor(first) {
|
|
15700
15712
|
this.type = "trendAngle";
|
|
15713
|
+
/**
|
|
15714
|
+
* Set by the manager when the user moves a point, cleared once the angle
|
|
15715
|
+
* has been re-measured. Coordinates alone cannot tell a drag from the
|
|
15716
|
+
* engine re-anchoring points to freshly loaded bars — which happens on
|
|
15717
|
+
* every reload and used to re-measure the angle on the new device's
|
|
15718
|
+
* geometry, the very thing storing it was meant to stop.
|
|
15719
|
+
*/
|
|
15720
|
+
this.pendingMeasure = false;
|
|
15701
15721
|
this.id = `ta_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
15702
15722
|
this.points = [first, { ...first }];
|
|
15703
15723
|
this.complete = false;
|
|
15704
15724
|
}
|
|
15705
|
-
|
|
15706
|
-
|
|
15725
|
+
onPointsEdited() {
|
|
15726
|
+
this.pendingMeasure = true;
|
|
15727
|
+
}
|
|
15728
|
+
/**
|
|
15729
|
+
* Applies the angle rule and returns the pixel geometry to draw.
|
|
15730
|
+
*
|
|
15731
|
+
* - no stored angle, the drawing is still being placed, or the user has
|
|
15732
|
+
* just moved a point: measure the angle from the current pixels;
|
|
15733
|
+
* - otherwise the points only moved because the scale or the data did:
|
|
15734
|
+
* keep the angle and move the second point onto the line, so
|
|
15735
|
+
* hit-testing, handles and persistence agree with what is drawn. Its
|
|
15736
|
+
* length is whatever the stored point projects to on this screen.
|
|
15737
|
+
*/
|
|
15738
|
+
resolve(scale, viewport, priceRange, chartW, chartH) {
|
|
15707
15739
|
const [p1, p2] = this.points;
|
|
15708
15740
|
const x1 = scale.xToPixel(p1.barIndex, viewport, chartW);
|
|
15709
15741
|
const y1 = scale.yToPixel(p1.price, priceRange, chartH);
|
|
15710
|
-
const
|
|
15711
|
-
const
|
|
15712
|
-
const
|
|
15713
|
-
|
|
15742
|
+
const px2 = scale.xToPixel(p2.barIndex, viewport, chartW);
|
|
15743
|
+
const py2 = scale.yToPixel(p2.price, priceRange, chartH);
|
|
15744
|
+
const measured = Math.atan2(-(py2 - y1), px2 - x1) * 180 / Math.PI;
|
|
15745
|
+
if (this.angle === void 0 || this.pendingMeasure || !this.complete) {
|
|
15746
|
+
if (Number.isFinite(measured) && chartW > 0 && chartH > 0) {
|
|
15747
|
+
this.angle = measured;
|
|
15748
|
+
this.pendingMeasure = false;
|
|
15749
|
+
}
|
|
15750
|
+
return { x1, y1, x2: px2, y2: py2, angleDeg: this.angle ?? measured };
|
|
15751
|
+
}
|
|
15752
|
+
const len = Math.hypot(px2 - x1, py2 - y1);
|
|
15753
|
+
const rad = this.angle * Math.PI / 180;
|
|
15754
|
+
const x22 = x1 + len * Math.cos(rad);
|
|
15755
|
+
const y22 = y1 - len * Math.sin(rad);
|
|
15756
|
+
if (Number.isFinite(x22) && Number.isFinite(y22) && (Math.abs(x22 - px2) > 0.01 || Math.abs(y22 - py2) > 0.01)) {
|
|
15757
|
+
this.points[1] = {
|
|
15758
|
+
barIndex: scale.pixelToBarIndex(x22, viewport, chartW),
|
|
15759
|
+
price: scale.pixelToPrice(y22, priceRange, chartH)
|
|
15760
|
+
};
|
|
15761
|
+
}
|
|
15762
|
+
return { x1, y1, x2: x22, y2: y22, angleDeg: this.angle };
|
|
15763
|
+
}
|
|
15764
|
+
render(ctx, scale, viewport, priceRange, chartW, chartH, isSelected, _pricePrecision, theme) {
|
|
15765
|
+
if (this.points.length < 2) return;
|
|
15766
|
+
const { x1, y1, x2: x22, y2: y22, angleDeg } = this.resolve(scale, viewport, priceRange, chartW, chartH);
|
|
15767
|
+
const angleRad = angleDeg * Math.PI / 180;
|
|
15714
15768
|
const s = resolveStyle("trendAngle", this.style, theme);
|
|
15715
15769
|
ctx.save();
|
|
15716
15770
|
ctx.strokeStyle = s.strokeColor;
|
|
@@ -15733,10 +15787,11 @@ var TrendAngle = class {
|
|
|
15733
15787
|
ctx.stroke();
|
|
15734
15788
|
ctx.setLineDash([]);
|
|
15735
15789
|
ctx.globalAlpha = 1;
|
|
15790
|
+
const shown = Math.abs(angleDeg) < 0.05 ? 0 : angleDeg;
|
|
15736
15791
|
const labelX = x1 + arcRadius + 6;
|
|
15737
15792
|
const labelY = y1 - (angleRad >= 0 ? 6 : -14);
|
|
15738
15793
|
ctx.font = "bold 11px 'Inter', system-ui, sans-serif";
|
|
15739
|
-
ctx.fillText(`${
|
|
15794
|
+
ctx.fillText(`${shown.toFixed(1)}\xB0`, labelX, labelY);
|
|
15740
15795
|
if (isSelected) {
|
|
15741
15796
|
for (const [ax, ay] of [[x1, y1], [x22, y22]]) {
|
|
15742
15797
|
ctx.beginPath();
|
|
@@ -22194,6 +22249,7 @@ var DrawingManager = class {
|
|
|
22194
22249
|
const required = this.activeDrawing.requiredPoints ?? 2;
|
|
22195
22250
|
const current = this.activeDrawing.points.length;
|
|
22196
22251
|
this.activeDrawing.points[current - 1] = { ...point };
|
|
22252
|
+
this.activeDrawing.onPointsEdited?.();
|
|
22197
22253
|
if (current < required) {
|
|
22198
22254
|
this.activeDrawing.points.push({ ...point });
|
|
22199
22255
|
return true;
|
|
@@ -22233,6 +22289,7 @@ var DrawingManager = class {
|
|
|
22233
22289
|
const last = this.activeDrawing.points.length - 1;
|
|
22234
22290
|
this.activeDrawing.points[last] = point;
|
|
22235
22291
|
}
|
|
22292
|
+
this.activeDrawing.onPointsEdited?.();
|
|
22236
22293
|
}
|
|
22237
22294
|
if (this.dragDrawing && this.dragStartMouse && this.dragStartPoints) {
|
|
22238
22295
|
const dBar = point.barIndex - this.dragStartMouse.barIndex;
|
|
@@ -22245,6 +22302,7 @@ var DrawingManager = class {
|
|
|
22245
22302
|
price: start.price + dPrice
|
|
22246
22303
|
};
|
|
22247
22304
|
}
|
|
22305
|
+
this.dragDrawing.onPointsEdited?.();
|
|
22248
22306
|
this.onPointsMutated?.(this.dragDrawing);
|
|
22249
22307
|
}
|
|
22250
22308
|
if (this.resizeDrawing && this.resizePointIndex >= 0) {
|
|
@@ -22265,6 +22323,7 @@ var DrawingManager = class {
|
|
|
22265
22323
|
} else {
|
|
22266
22324
|
this.resizeDrawing.points[this.resizePointIndex] = { ...point };
|
|
22267
22325
|
}
|
|
22326
|
+
this.resizeDrawing.onPointsEdited?.();
|
|
22268
22327
|
this.onPointsMutated?.(this.resizeDrawing);
|
|
22269
22328
|
}
|
|
22270
22329
|
}
|
|
@@ -22592,6 +22651,7 @@ var DrawingManager = class {
|
|
|
22592
22651
|
...d.visible !== void 0 && { visible: d.visible },
|
|
22593
22652
|
...d.locked !== void 0 && { locked: d.locked },
|
|
22594
22653
|
...d.text !== void 0 && { text: d.text },
|
|
22654
|
+
...d.angle !== void 0 && { angle: d.angle },
|
|
22595
22655
|
...d.style !== void 0 && { style: { ...d.style } }
|
|
22596
22656
|
}));
|
|
22597
22657
|
}
|
|
@@ -22614,6 +22674,7 @@ var DrawingManager = class {
|
|
|
22614
22674
|
if (drawing.visible !== void 0) created.visible = drawing.visible;
|
|
22615
22675
|
if (drawing.locked !== void 0) created.locked = drawing.locked;
|
|
22616
22676
|
if (drawing.text !== void 0) created.text = drawing.text;
|
|
22677
|
+
if (drawing.angle !== void 0) created.angle = drawing.angle;
|
|
22617
22678
|
if (drawing.style !== void 0) created.style = { ...drawing.style };
|
|
22618
22679
|
this.drawings.push(created);
|
|
22619
22680
|
this.notifyChange();
|
|
@@ -22629,6 +22690,7 @@ var DrawingManager = class {
|
|
|
22629
22690
|
if (sd.visible !== void 0) drawing.visible = sd.visible;
|
|
22630
22691
|
if (sd.locked !== void 0) drawing.locked = sd.locked;
|
|
22631
22692
|
if (sd.text !== void 0) drawing.text = sd.text;
|
|
22693
|
+
if (sd.angle !== void 0) drawing.angle = sd.angle;
|
|
22632
22694
|
if (sd.style !== void 0) drawing.style = { ...sd.style };
|
|
22633
22695
|
return drawing;
|
|
22634
22696
|
});
|
|
@@ -36045,6 +36107,46 @@ function isPriceTransformSeries(series) {
|
|
|
36045
36107
|
return PRICE_TRANSFORM_SERIES.includes(series);
|
|
36046
36108
|
}
|
|
36047
36109
|
|
|
36110
|
+
// src/core/indicatorParamCoercion.ts
|
|
36111
|
+
var ALIASES = { length: "period" };
|
|
36112
|
+
function coerceIndicatorParams(fields, params) {
|
|
36113
|
+
if (!params) return {};
|
|
36114
|
+
const out = { ...params };
|
|
36115
|
+
const byKey = new Map(fields.map((f) => [f.key, f]));
|
|
36116
|
+
for (const [alias, key] of Object.entries(ALIASES)) {
|
|
36117
|
+
if (alias in out && !(key in out) && byKey.has(key)) {
|
|
36118
|
+
out[key] = out[alias];
|
|
36119
|
+
delete out[alias];
|
|
36120
|
+
}
|
|
36121
|
+
}
|
|
36122
|
+
for (const field of fields) {
|
|
36123
|
+
if (!(field.key in out)) continue;
|
|
36124
|
+
const raw = out[field.key];
|
|
36125
|
+
switch (field.kind) {
|
|
36126
|
+
case "number": {
|
|
36127
|
+
const n = typeof raw === "number" ? raw : typeof raw === "string" && raw.trim() !== "" ? Number(raw) : NaN;
|
|
36128
|
+
if (!Number.isFinite(n)) {
|
|
36129
|
+
out[field.key] = field.default;
|
|
36130
|
+
break;
|
|
36131
|
+
}
|
|
36132
|
+
let v = n;
|
|
36133
|
+
if (typeof field.min === "number") v = Math.max(field.min, v);
|
|
36134
|
+
if (typeof field.max === "number") v = Math.min(field.max, v);
|
|
36135
|
+
out[field.key] = v;
|
|
36136
|
+
break;
|
|
36137
|
+
}
|
|
36138
|
+
case "select": {
|
|
36139
|
+
if (!field.options) break;
|
|
36140
|
+
const s = String(raw);
|
|
36141
|
+
const match = field.options.find((o) => o === s) ?? field.options.find((o) => o.toLowerCase() === s.toLowerCase());
|
|
36142
|
+
out[field.key] = match ?? field.default;
|
|
36143
|
+
break;
|
|
36144
|
+
}
|
|
36145
|
+
}
|
|
36146
|
+
}
|
|
36147
|
+
return out;
|
|
36148
|
+
}
|
|
36149
|
+
|
|
36048
36150
|
// src/ui/ValueTooltip.ts
|
|
36049
36151
|
var SUB_DAILY3 = [
|
|
36050
36152
|
"1m",
|
|
@@ -36941,6 +37043,7 @@ function initialVisibleBars(mobileBarDivisor = 2, forceApply = false, defaultBar
|
|
|
36941
37043
|
return Math.max(10, Math.round(defaultBars / mobileBarDivisor));
|
|
36942
37044
|
}
|
|
36943
37045
|
var LOOKAHEAD = 4;
|
|
37046
|
+
var LONG_PRESS_CANCEL_ARM_MS = 300;
|
|
36944
37047
|
function lastIndexAtOrBefore(bars, time) {
|
|
36945
37048
|
let lo = 0, hi = bars.length - 1, ans = -1;
|
|
36946
37049
|
while (lo <= hi) {
|
|
@@ -37246,6 +37349,16 @@ var _ChartEngine = class _ChartEngine {
|
|
|
37246
37349
|
/** Per-instance sub-pane Y-scale factors, keyed by ActiveIndicatorState.instanceId. */
|
|
37247
37350
|
this.subPaneScaleFactors = /* @__PURE__ */ new Map();
|
|
37248
37351
|
this._mobileCrosshairActive = false;
|
|
37352
|
+
/**
|
|
37353
|
+
* When the pending long press began. WebKit runs its own long-press
|
|
37354
|
+
* recognizer on the page and, when it wins, ends our touch with
|
|
37355
|
+
* `touchcancel` at about the same moment our 500 ms timer would fire — on
|
|
37356
|
+
* iPhones the crosshair simply never appeared, while Android WebViews, which
|
|
37357
|
+
* have no such gesture on a canvas, were fine. The touchstart below now
|
|
37358
|
+
* claims the gesture with `preventDefault()`, and should a cancel still
|
|
37359
|
+
* arrive after `LONG_PRESS_CANCEL_ARM_MS` the hold counts as a long press.
|
|
37360
|
+
*/
|
|
37361
|
+
this._longPressStartedAt = 0;
|
|
37249
37362
|
this._mobileTapStartXY = null;
|
|
37250
37363
|
this._longPressTimer = null;
|
|
37251
37364
|
this._longPressPoint = null;
|
|
@@ -37500,6 +37613,17 @@ var _ChartEngine = class _ChartEngine {
|
|
|
37500
37613
|
};
|
|
37501
37614
|
// ── State persistence ─────────────────────────────────────────────────────
|
|
37502
37615
|
this.isRestoringState = false;
|
|
37616
|
+
/**
|
|
37617
|
+
* Height of the top-left legend block — symbol / OHLC strip plus indicator
|
|
37618
|
+
* pills — in canvas CSS pixels, with a small gap. `renderTradeLevels` keeps
|
|
37619
|
+
* info boxes below it: a position priced near the top of the visible range
|
|
37620
|
+
* used to print its box straight over the symbol name, on web and in the
|
|
37621
|
+
* Android / iOS WebViews alike, worst on phones where the strip wraps to two
|
|
37622
|
+
* lines. Measured by ResizeObserver where available, else per render.
|
|
37623
|
+
*/
|
|
37624
|
+
this._legendInset = 0;
|
|
37625
|
+
this._legendObserver = null;
|
|
37626
|
+
this._legendObserverTried = false;
|
|
37503
37627
|
this.onPointerEnter = () => {
|
|
37504
37628
|
this._cachedWrapRect = null;
|
|
37505
37629
|
this._cachedDrawRect = null;
|
|
@@ -38052,8 +38176,10 @@ var _ChartEngine = class _ChartEngine {
|
|
|
38052
38176
|
}
|
|
38053
38177
|
this._mobileTapStartXY = { x, y };
|
|
38054
38178
|
} else {
|
|
38179
|
+
if (e.cancelable) e.preventDefault();
|
|
38055
38180
|
if (this._longPressTimer !== null) clearTimeout(this._longPressTimer);
|
|
38056
38181
|
this._longPressPoint = { x, y };
|
|
38182
|
+
this._longPressStartedAt = performance.now();
|
|
38057
38183
|
this._longPressTimer = setTimeout(() => {
|
|
38058
38184
|
this._longPressTimer = null;
|
|
38059
38185
|
if (this._longPressPoint) {
|
|
@@ -38250,7 +38376,10 @@ var _ChartEngine = class _ChartEngine {
|
|
|
38250
38376
|
if (this._longPressTimer !== null) {
|
|
38251
38377
|
clearTimeout(this._longPressTimer);
|
|
38252
38378
|
this._longPressTimer = null;
|
|
38379
|
+
const held = performance.now() - this._longPressStartedAt;
|
|
38380
|
+
const at = this._longPressPoint;
|
|
38253
38381
|
this._longPressPoint = null;
|
|
38382
|
+
if (at && held >= LONG_PRESS_CANCEL_ARM_MS) this._activateMobileCrosshair(at.x, at.y);
|
|
38254
38383
|
}
|
|
38255
38384
|
let dirty = false;
|
|
38256
38385
|
this._pendingTradeDrag = null;
|
|
@@ -38665,6 +38794,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
38665
38794
|
const last = all[all.length - 1];
|
|
38666
38795
|
const intervalMs = this.timeframeToMs(this.timeframe);
|
|
38667
38796
|
const tickPrice = this.tickClosePriceSource === "ltp" ? this.liveLtp ?? tick.bid : this.tickClosePriceSource === "ask" ? tick.ask : tick.bid;
|
|
38797
|
+
const before = this.dataStore.length;
|
|
38668
38798
|
if (tick.time < last.time + intervalMs) {
|
|
38669
38799
|
const updated = {
|
|
38670
38800
|
...last,
|
|
@@ -38675,6 +38805,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
38675
38805
|
};
|
|
38676
38806
|
all[all.length - 1] = updated;
|
|
38677
38807
|
this.dataStore.invalidate();
|
|
38808
|
+
this.followLiveEdge(before);
|
|
38678
38809
|
} else {
|
|
38679
38810
|
const completedBar = last;
|
|
38680
38811
|
const newBarTime = Math.floor(tick.time / intervalMs) * intervalMs;
|
|
@@ -38693,13 +38824,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
38693
38824
|
);
|
|
38694
38825
|
}
|
|
38695
38826
|
const openingBar = all[all.length - 1];
|
|
38696
|
-
|
|
38697
|
-
this._targetViewport = {
|
|
38698
|
-
...this._targetViewport,
|
|
38699
|
-
startIndex: this._targetViewport.startIndex + 1,
|
|
38700
|
-
endIndex: this._targetViewport.endIndex + 1
|
|
38701
|
-
};
|
|
38702
|
-
}
|
|
38827
|
+
this.followLiveEdge(before);
|
|
38703
38828
|
this.emitter.emit("newBar", { completedBar, openingBar, intervalMs });
|
|
38704
38829
|
this.correctCompletedBar(completedBar.time, intervalMs);
|
|
38705
38830
|
if (newBarTime > completedBar.time + intervalMs) {
|
|
@@ -40020,10 +40145,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
40020
40145
|
this.targetCandleWidth
|
|
40021
40146
|
);
|
|
40022
40147
|
this._defaultVisibleBars = loadVisibleBars;
|
|
40148
|
+
const total = this.dataStore.length;
|
|
40023
40149
|
this.viewport = {
|
|
40024
40150
|
...this.viewport,
|
|
40025
|
-
startIndex: fitAll ? 0 : Math.max(0,
|
|
40026
|
-
endIndex:
|
|
40151
|
+
startIndex: fitAll ? 0 : Math.max(0, total - loadVisibleBars + LOOKAHEAD),
|
|
40152
|
+
endIndex: total + LOOKAHEAD
|
|
40027
40153
|
};
|
|
40028
40154
|
this._targetViewport = { ...this.viewport };
|
|
40029
40155
|
const hasVol = this.dataStore.hasVolumeData;
|
|
@@ -40770,7 +40896,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
40770
40896
|
).length;
|
|
40771
40897
|
resolved["color"] = cycleIndicatorColor(INDICATOR_COLOR_PALETTE, count);
|
|
40772
40898
|
}
|
|
40773
|
-
if (params) Object.assign(resolved, params);
|
|
40899
|
+
if (params) Object.assign(resolved, coerceIndicatorParams(schema.fields, params));
|
|
40774
40900
|
const factoryParams = { ...resolved };
|
|
40775
40901
|
delete factoryParams["timeframe"];
|
|
40776
40902
|
this.addIndicator(schema.factory(factoryParams), resolved);
|
|
@@ -40893,6 +41019,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
40893
41019
|
if (!prevState) return;
|
|
40894
41020
|
const schema = INDICATOR_PARAM_SCHEMAS.find((s) => s.shortName === prevState.shortName);
|
|
40895
41021
|
if (!schema) return;
|
|
41022
|
+
newParams = coerceIndicatorParams(schema.fields, newParams);
|
|
40896
41023
|
const { timeframe: tf, ...factoryParams } = newParams;
|
|
40897
41024
|
const newIndicator = schema.factory(factoryParams);
|
|
40898
41025
|
prevState.indicator = newIndicator;
|
|
@@ -41725,12 +41852,13 @@ var _ChartEngine = class _ChartEngine {
|
|
|
41725
41852
|
for (const entry of entries) {
|
|
41726
41853
|
const schema = INDICATOR_PARAM_SCHEMAS.find((s) => s.shortName === entry.shortName);
|
|
41727
41854
|
if (!schema) continue;
|
|
41728
|
-
const
|
|
41855
|
+
const params = coerceIndicatorParams(schema.fields, entry.params);
|
|
41856
|
+
const factoryParams = { ...params };
|
|
41729
41857
|
delete factoryParams["timeframe"];
|
|
41730
|
-
this.addIndicator(schema.factory(factoryParams),
|
|
41858
|
+
this.addIndicator(schema.factory(factoryParams), params);
|
|
41731
41859
|
const st = this.activeIndicators[this.activeIndicators.length - 1];
|
|
41732
41860
|
if (!st) continue;
|
|
41733
|
-
st.currentParams = { ...
|
|
41861
|
+
st.currentParams = { ...params };
|
|
41734
41862
|
const overlay = this.subPaneOverlays.get(st.instanceId) ?? this.indicatorOverlay;
|
|
41735
41863
|
if (entry.visible === false) {
|
|
41736
41864
|
st.visible = false;
|
|
@@ -43709,6 +43837,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
43709
43837
|
this.panHandler.destroy();
|
|
43710
43838
|
this.zoomHandler.destroy();
|
|
43711
43839
|
this.resizeObserver.disconnect();
|
|
43840
|
+
this._legendObserver?.disconnect();
|
|
43712
43841
|
this.disposeVvListener();
|
|
43713
43842
|
this.indicatorOverlay?.destroy();
|
|
43714
43843
|
for (const overlay of this.subPaneOverlays.values()) overlay.destroy();
|
|
@@ -44153,6 +44282,32 @@ var _ChartEngine = class _ChartEngine {
|
|
|
44153
44282
|
recomputeRenderStyle() {
|
|
44154
44283
|
this.positionRenderStyle = this.pnlPillExpanded ? "line" : this.baseRenderStyle();
|
|
44155
44284
|
}
|
|
44285
|
+
measureLegendInset() {
|
|
44286
|
+
let bottom = 0;
|
|
44287
|
+
for (const el of [this.siEl, this.indicatorOverlay?.el]) {
|
|
44288
|
+
if (!el || el.style.display === "none" || el.offsetHeight === 0) continue;
|
|
44289
|
+
bottom = Math.max(bottom, el.offsetTop + el.offsetHeight);
|
|
44290
|
+
}
|
|
44291
|
+
this._legendInset = bottom > 0 ? bottom + 4 : 0;
|
|
44292
|
+
}
|
|
44293
|
+
legendInset() {
|
|
44294
|
+
if (!this._legendObserverTried) {
|
|
44295
|
+
this._legendObserverTried = true;
|
|
44296
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
44297
|
+
this._legendObserver = new ResizeObserver(() => {
|
|
44298
|
+
this.measureLegendInset();
|
|
44299
|
+
this.scheduleRender();
|
|
44300
|
+
});
|
|
44301
|
+
for (const el of [this.siEl, this.indicatorOverlay?.el]) {
|
|
44302
|
+
if (el) this._legendObserver.observe(el);
|
|
44303
|
+
}
|
|
44304
|
+
}
|
|
44305
|
+
this.measureLegendInset();
|
|
44306
|
+
} else if (!this._legendObserver) {
|
|
44307
|
+
this.measureLegendInset();
|
|
44308
|
+
}
|
|
44309
|
+
return this._legendInset;
|
|
44310
|
+
}
|
|
44156
44311
|
renderTradeLayer() {
|
|
44157
44312
|
if (this._isDestroyed) return;
|
|
44158
44313
|
const dpr = window.devicePixelRatio || 1;
|
|
@@ -44257,7 +44412,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
44257
44412
|
this._orderTimeDragEnabled,
|
|
44258
44413
|
this._bracketLabel,
|
|
44259
44414
|
this.tradeLevelBadgeScale,
|
|
44260
|
-
this.tradeLevelBadgeAnchor
|
|
44415
|
+
this.tradeLevelBadgeAnchor,
|
|
44416
|
+
this.legendInset()
|
|
44261
44417
|
);
|
|
44262
44418
|
ctx.restore();
|
|
44263
44419
|
const offVpResult = renderOffViewportIndicators(
|
|
@@ -46438,6 +46594,22 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46438
46594
|
this.siStreamDot.style.background = this.theme.ui.stream[this.streamStatus];
|
|
46439
46595
|
}, this.tickActivityMs);
|
|
46440
46596
|
}
|
|
46597
|
+
/**
|
|
46598
|
+
* Shifts the target viewport by however many displayed elements the last
|
|
46599
|
+
* raw-bar mutation added, when the view was sitting at the live edge.
|
|
46600
|
+
* `before` is `dataStore.length` captured before the mutation. Without a
|
|
46601
|
+
* transform this is the old "+1 on a new bar" rule; with one it is the
|
|
46602
|
+
* number of bricks / columns the tick completed.
|
|
46603
|
+
*/
|
|
46604
|
+
followLiveEdge(before) {
|
|
46605
|
+
const grew = this.dataStore.length - before;
|
|
46606
|
+
if (grew <= 0 || this._targetViewport.endIndex < before) return;
|
|
46607
|
+
this._targetViewport = {
|
|
46608
|
+
...this._targetViewport,
|
|
46609
|
+
startIndex: this._targetViewport.startIndex + grew,
|
|
46610
|
+
endIndex: this._targetViewport.endIndex + grew
|
|
46611
|
+
};
|
|
46612
|
+
}
|
|
46441
46613
|
/** Fetch bars that fall in a gap between the last historical bar and the first live bar.
|
|
46442
46614
|
* This happens when the system clock was behind feed time during the initial data load. */
|
|
46443
46615
|
fillTickGap(fromTime, toTime) {
|
|
@@ -46455,6 +46627,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46455
46627
|
this.backfillDrawingTimestamps();
|
|
46456
46628
|
const all = this.dataStore.raw;
|
|
46457
46629
|
const liveBar = all[all.length - 1];
|
|
46630
|
+
const before = this.dataStore.length;
|
|
46458
46631
|
let insertCount = 0;
|
|
46459
46632
|
for (const bar of bars) {
|
|
46460
46633
|
if (bar.time >= liveBar.time) continue;
|
|
@@ -46469,11 +46642,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
46469
46642
|
insertCount++;
|
|
46470
46643
|
}
|
|
46471
46644
|
}
|
|
46472
|
-
|
|
46645
|
+
const grew = this.dataStore.length - before;
|
|
46646
|
+
if (insertCount > 0 && grew > 0) {
|
|
46473
46647
|
this.viewport = {
|
|
46474
46648
|
...this.viewport,
|
|
46475
|
-
startIndex: this.viewport.startIndex +
|
|
46476
|
-
endIndex: this.viewport.endIndex +
|
|
46649
|
+
startIndex: this.viewport.startIndex + grew,
|
|
46650
|
+
endIndex: this.viewport.endIndex + grew
|
|
46477
46651
|
};
|
|
46478
46652
|
this.reanchorDrawingsByTimestamp();
|
|
46479
46653
|
this.recomputeIndicators();
|