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 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 (shows angle) |
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 -= m2.boxH + 2;
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 = chartW - 8;
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
- render(ctx, scale, viewport, priceRange, chartW, chartH, isSelected, _pricePrecision, theme) {
15706
- if (this.points.length < 2) return;
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 x22 = scale.xToPixel(p2.barIndex, viewport, chartW);
15711
- const y22 = scale.yToPixel(p2.price, priceRange, chartH);
15712
- const angleDeg = Math.atan2(-(y22 - y1), x22 - x1) * 180 / Math.PI;
15713
- const angleRad = Math.atan2(-(y22 - y1), x22 - x1);
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(`${angleDeg.toFixed(1)}\xB0`, labelX, labelY);
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
- if (this._targetViewport.endIndex >= all.length) {
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, bars.length - loadVisibleBars + LOOKAHEAD),
40026
- endIndex: bars.length + LOOKAHEAD
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 factoryParams = { ...entry.params };
41855
+ const params = coerceIndicatorParams(schema.fields, entry.params);
41856
+ const factoryParams = { ...params };
41729
41857
  delete factoryParams["timeframe"];
41730
- this.addIndicator(schema.factory(factoryParams), entry.params);
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 = { ...entry.params };
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
- if (insertCount > 0) {
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 + insertCount,
46476
- endIndex: this.viewport.endIndex + insertCount
46649
+ startIndex: this.viewport.startIndex + grew,
46650
+ endIndex: this.viewport.endIndex + grew
46477
46651
  };
46478
46652
  this.reanchorDrawingsByTimestamp();
46479
46653
  this.recomputeIndicators();