@forgecharts/sdk 1.5.68 → 1.5.70

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/dist/internal.js CHANGED
@@ -100,6 +100,36 @@ function isGLBXDailyBreak(nowMs = Date.now()) {
100
100
  return weekday === "Mon" || weekday === "Tue" || weekday === "Wed" || weekday === "Thu";
101
101
  }
102
102
 
103
+ // src/core/cursorTime.ts
104
+ function cursorBucketTime(bars, transform, interval, x) {
105
+ const raw = transform.xToTime(x);
106
+ const tfSec = parseTfSeconds(interval) ?? 0;
107
+ const floored = tfSec > 0 ? Math.floor(raw / tfSec) * tfSec : raw;
108
+ if (!bars || bars.length === 0) return floored;
109
+ let lo = 0, hi = bars.length - 1;
110
+ while (lo < hi) {
111
+ const mid = lo + hi >>> 1;
112
+ if (bars[mid].time < raw) lo = mid + 1;
113
+ else hi = mid;
114
+ }
115
+ let nearest = lo;
116
+ let best = Math.abs(transform.timeToX(bars[lo].time) - x);
117
+ for (const i of [lo - 1, lo + 1]) {
118
+ if (i < 0 || i >= bars.length) continue;
119
+ const d = Math.abs(transform.timeToX(bars[i].time) - x);
120
+ if (d < best) {
121
+ best = d;
122
+ nearest = i;
123
+ }
124
+ }
125
+ const first = bars[0].time;
126
+ const last = bars[bars.length - 1].time;
127
+ const halfCol = bars.length > 1 ? Math.abs(transform.timeToX(bars[1].time) - transform.timeToX(first)) / 2 : Infinity;
128
+ const edgeX = raw > last ? transform.timeToX(last) : raw < first ? transform.timeToX(first) : null;
129
+ if (edgeX !== null && Math.abs(x - edgeX) > halfCol) return floored;
130
+ return bars[nearest].time;
131
+ }
132
+
103
133
  // src/core/CanvasLayer.ts
104
134
  var CanvasLayer = class {
105
135
  canvas;
@@ -4792,10 +4822,7 @@ var Chart = class {
4792
4822
  onCrosshairMove: (x, y) => {
4793
4823
  if (!this._crosshairEnabled) return;
4794
4824
  if (this._magnetMode === "off") {
4795
- const tfSec = _tfToSeconds(this._interval);
4796
- const rawTime = this._transform.xToTime(x);
4797
- const flooredTime = Math.floor(rawTime / tfSec) * tfSec;
4798
- this._crosshair.update(x, y, flooredTime);
4825
+ this._crosshair.update(x, y, this._cursorBucketTime(x));
4799
4826
  } else {
4800
4827
  const snap = this.snapXToBar(x);
4801
4828
  const sy = this._magnetMode === "snap-xy" ? snap.closeY ?? y : y;
@@ -4836,7 +4863,8 @@ var Chart = class {
4836
4863
  },
4837
4864
  onDrawPointerMove: (x, y) => {
4838
4865
  this._drawingCursorPt = {
4839
- time: this._transform.xToTime(x),
4866
+ // Bucket time, not raw: the label and the stored anchor agree.
4867
+ time: this._cursorBucketTime(x),
4840
4868
  price: this._transform.yToPrice(y)
4841
4869
  };
4842
4870
  this._dirty = true;
@@ -5109,9 +5137,19 @@ var Chart = class {
5109
5137
  closeY: this._transform.priceToY(bar.close)
5110
5138
  };
5111
5139
  }
5112
- const tfSec = _tfToSeconds(this._interval);
5113
- const rawTime = this._transform.xToTime(x);
5114
- return { x, time: Math.floor(rawTime / tfSec) * tfSec };
5140
+ return { x, time: this._cursorBucketTime(x) };
5141
+ }
5142
+ /** Bucket time for a cursor x see core/cursorTime.ts for the rule. */
5143
+ _cursorBucketTime(x) {
5144
+ let bars = null;
5145
+ for (const s of this._series) {
5146
+ const data = s.data();
5147
+ if (data.length > 0) {
5148
+ bars = data;
5149
+ break;
5150
+ }
5151
+ }
5152
+ return cursorBucketTime(bars, this._transform, this._interval, x);
5115
5153
  }
5116
5154
  _computeViewport() {
5117
5155
  return {
@@ -5588,7 +5626,7 @@ var Chart = class {
5588
5626
  const MAGNET_PX = 15;
5589
5627
  const t = this._transform;
5590
5628
  const price = t.yToPrice(y);
5591
- const time = t.xToTime(x);
5629
+ const time = this._cursorBucketTime(x);
5592
5630
  if (this._magnetMode === "off") return { time, price };
5593
5631
  let bestBar = null;
5594
5632
  let bestDist = SNAP_PX;
@@ -6297,7 +6335,16 @@ var PixiCrosshairRenderer = class {
6297
6335
  interval = "1h";
6298
6336
  /** IANA timezone for the time label. Falls back to UTC. */
6299
6337
  timezone = "UTC";
6300
- update(x, y) {
6338
+ /**
6339
+ * The time the chart computed for this cursor position — nearest bar, else
6340
+ * floored to the timeframe grid. This renderer must never derive its own:
6341
+ * it once did (raw xToTime), and every pointer style's time label showed
6342
+ * interpolated minute noise (10:02 on an hourly chart) on any timeframe
6343
+ * wider than the interpolation error.
6344
+ */
6345
+ _snapTime = null;
6346
+ update(x, y, snapTime) {
6347
+ this._snapTime = snapTime ?? null;
6301
6348
  this._x = x;
6302
6349
  this._y = y;
6303
6350
  }
@@ -6339,7 +6386,7 @@ var PixiCrosshairRenderer = class {
6339
6386
  this._priceLabel.text = priceText;
6340
6387
  this._priceLabel.x = pw + 4;
6341
6388
  this._priceLabel.y = y - this._priceLabel.height / 2;
6342
- const time = transform.xToTime(x);
6389
+ const time = this._snapTime ?? transform.xToTime(x);
6343
6390
  const timeText = this._formatTime(time);
6344
6391
  const tlw = DAILY_INTERVALS.has(this.interval) ? 120 : 170;
6345
6392
  const tlh = tsh - 4;
@@ -8736,11 +8783,11 @@ var PixiChart = class {
8736
8783
  onCrosshairMove: (x, y) => {
8737
8784
  if (!this._crosshairEnabled) return;
8738
8785
  if (this._magnetMode === "off") {
8739
- this._crosshairRenderer.update(x, y);
8786
+ this._crosshairRenderer.update(x, y, this._cursorBucketTime(x));
8740
8787
  } else {
8741
8788
  const snap = this.snapXToBar(x);
8742
8789
  const sy = this._magnetMode === "snap-xy" ? snap.closeY ?? y : y;
8743
- this._crosshairRenderer.update(snap.x, sy);
8790
+ this._crosshairRenderer.update(snap.x, sy, snap.time);
8744
8791
  }
8745
8792
  const ah = this._interaction.axisHover;
8746
8793
  if (ah !== this._lastAxisHover) {
@@ -8786,7 +8833,9 @@ var PixiChart = class {
8786
8833
  },
8787
8834
  onDrawPointerMove: (x, y) => {
8788
8835
  this._drawingCursorPt = {
8789
- time: this._transform.xToTime(x),
8836
+ // Bucket time, not raw: what the crosshair label shows is what a
8837
+ // drawing placed here will store.
8838
+ time: this._cursorBucketTime(x),
8790
8839
  price: this._transform.yToPrice(y)
8791
8840
  };
8792
8841
  this._layers.dirty.mark(3 /* Drawing */);
@@ -9353,6 +9402,18 @@ var PixiChart = class {
9353
9402
  setNativeCursorHidden(hidden) {
9354
9403
  this._interaction.setHideCursor(hidden);
9355
9404
  }
9405
+ /** Bucket time for a cursor x — see core/cursorTime.ts for the rule. */
9406
+ _cursorBucketTime(x) {
9407
+ let bars = null;
9408
+ for (const s of this._series) {
9409
+ const data = s.data();
9410
+ if (data.length > 0) {
9411
+ bars = data;
9412
+ break;
9413
+ }
9414
+ }
9415
+ return cursorBucketTime(bars, this._transform, this._interval, x);
9416
+ }
9356
9417
  snapXToBar(x) {
9357
9418
  const SNAP_PX = 20;
9358
9419
  let bars = null;
@@ -9396,7 +9457,7 @@ var PixiChart = class {
9396
9457
  closeY: t.priceToY(bar.close)
9397
9458
  };
9398
9459
  }
9399
- return { x, time: t.xToTime(x) };
9460
+ return { x, time: this._cursorBucketTime(x) };
9400
9461
  }
9401
9462
  setIndicatorsVisible(v) {
9402
9463
  this._indicatorsVisible = v;
@@ -10369,14 +10430,13 @@ function toOHLCV(bar) {
10369
10430
  function _historyWindow(tf) {
10370
10431
  return 500 * (parseTfSeconds(tf) ?? 3600);
10371
10432
  }
10372
- var BATCH_SUPPRESSION_MS = 1e4;
10433
+ var REFETCH_ON_DOUBT_DELAY_MS = 3e3;
10373
10434
  function tfSeconds(tf) {
10374
10435
  const m = /^(\d+)\s*(s|m|h|D|W|M)$/.exec(tf);
10375
10436
  if (!m) return 0;
10376
10437
  const mult = { s: 1, m: 60, h: 3600, D: 86400, W: 604800, M: 2592e3 }[m[2]];
10377
10438
  return parseInt(m[1], 10) * mult;
10378
10439
  }
10379
- var LIVE_GAP_REPAIR_DELAY_MS = 3e3;
10380
10440
  function _pageSize(tf) {
10381
10441
  return 500 * (parseTfSeconds(tf) ?? 3600);
10382
10442
  }
@@ -10412,9 +10472,11 @@ var DatafeedConnector = class {
10412
10472
  */
10413
10473
  _emptyPageStreak = 0;
10414
10474
  /**
10415
- * Safety timer: clears `batchPending` if bar_batch never arrives, so live
10416
- * ticks resume. It no longer gates the chart reveal.
10417
- * Prevents the chart from staying blank when a provider doesn't fire onHistoricalReady.
10475
+ * Nothing arms this any more live-tick suppression went with the rest of
10476
+ * the client-side stitching once history became authoritative through the
10477
+ * forming bar. Kept (with its clear helper) because reconnect paths call
10478
+ * the helper and older subclasses may too; clearing a never-set timer is
10479
+ * free. Remove both in the next breaking cleanup.
10418
10480
  */
10419
10481
  _batchSafetyTimer = null;
10420
10482
  constructor(datafeed, series, callbacks) {
@@ -10438,12 +10500,10 @@ var DatafeedConnector = class {
10438
10500
  const to = Math.floor(Date.now() / 1e3);
10439
10501
  const from = to - _historyWindow(timeframe);
10440
10502
  this._clearBatchTimer();
10441
- let batchPending = false;
10442
10503
  const engine = new CandleEngine({
10443
10504
  // Every live tick mutates the engine's bar array and the chart series.
10444
10505
  onBarUpdated: (bar) => {
10445
10506
  if (this._activeUID !== uid) return;
10446
- if (batchPending) return;
10447
10507
  const ohlcv = toOHLCV(bar);
10448
10508
  this._series.update(ohlcv);
10449
10509
  this._cb.onUpdate?.(ohlcv);
@@ -10497,54 +10557,33 @@ var DatafeedConnector = class {
10497
10557
  }).catch(() => {
10498
10558
  });
10499
10559
  if (this._activeUID !== uid) return;
10500
- if (lastBarTime != null) {
10501
- batchPending = true;
10502
- this._batchSafetyTimer = setTimeout(() => {
10503
- batchPending = false;
10504
- this._batchSafetyTimer = null;
10505
- }, BATCH_SUPPRESSION_MS);
10506
- }
10507
10560
  const tfSec = tfSeconds(timeframe);
10508
10561
  let lastSeenBarTime = lastBarTime ?? 0;
10509
- let repairTimer = null;
10510
- const REPAIR_RETRY_MS = 6e4;
10511
- const REPAIR_MAX_ATTEMPTS = 15;
10512
- let repairAttempts = 0;
10513
- const repairGap = (fromTime, toTime) => {
10514
- if (repairTimer) clearTimeout(repairTimer);
10515
- repairTimer = setTimeout(() => {
10516
- repairTimer = null;
10562
+ let refetchTimer = null;
10563
+ const refetchOnDoubt = (fromTime, toTime) => {
10564
+ if (refetchTimer) clearTimeout(refetchTimer);
10565
+ refetchTimer = setTimeout(() => {
10566
+ refetchTimer = null;
10517
10567
  if (this._activeUID !== uid || this._destroyed) return;
10518
10568
  void this._datafeed.getHistoricalBars(symbol, timeframe, fromTime, toTime).then(({ bars: filled }) => {
10519
- if (this._activeUID !== uid) return;
10520
- if (filled.length > 0) engine.backfillGap(filled);
10521
- const newest = filled.length > 0 ? filled[filled.length - 1].time : 0;
10522
- const covered = tfSec > 0 && newest >= toTime - 2 * tfSec;
10523
- if (!covered && repairAttempts < REPAIR_MAX_ATTEMPTS) {
10524
- repairAttempts += 1;
10525
- if (repairTimer) clearTimeout(repairTimer);
10526
- repairTimer = setTimeout(() => {
10527
- repairTimer = null;
10528
- repairGap(fromTime, toTime);
10529
- }, REPAIR_RETRY_MS);
10530
- } else if (covered) {
10531
- repairAttempts = 0;
10532
- }
10569
+ if (this._activeUID !== uid || filled.length === 0) return;
10570
+ engine.backfillGap(filled);
10571
+ const newest = filled[filled.length - 1].time;
10572
+ if (newest > lastSeenBarTime) lastSeenBarTime = newest;
10533
10573
  }).catch(() => {
10534
10574
  });
10535
- }, LIVE_GAP_REPAIR_DELAY_MS);
10575
+ }, REFETCH_ON_DOUBT_DELAY_MS);
10536
10576
  };
10537
10577
  this._datafeed.subscribeBars(symbol, timeframe, (bar) => {
10538
10578
  if (this._activeUID !== uid) return;
10539
10579
  const t = bar.time;
10540
10580
  if (tfSec > 0 && lastSeenBarTime > 0 && t > lastSeenBarTime + tfSec) {
10541
- repairGap(lastSeenBarTime, t);
10581
+ refetchOnDoubt(lastSeenBarTime, t);
10542
10582
  }
10543
10583
  if (t > lastSeenBarTime) lastSeenBarTime = t;
10544
10584
  engine.applyLiveUpdate(bar);
10545
10585
  }, uid, lastBarTime, (bars) => {
10546
10586
  this._clearBatchTimer();
10547
- batchPending = false;
10548
10587
  if (this._activeUID === uid) {
10549
10588
  engine.backfillGap(bars);
10550
10589
  const newest = bars.length > 0 ? bars[bars.length - 1].time : 0;
@@ -10552,16 +10591,7 @@ var DatafeedConnector = class {
10552
10591
  }
10553
10592
  fireLoaded();
10554
10593
  });
10555
- if (result.filling) {
10556
- setTimeout(() => {
10557
- if (this._activeUID !== uid || this._destroyed) return;
10558
- void this._datafeed.getHistoricalBars(symbol, timeframe, from, to).then(({ bars: refetchedBars }) => {
10559
- if (this._activeUID !== uid) return;
10560
- engine.backfillGap(refetchedBars);
10561
- }).catch(() => {
10562
- });
10563
- }, 8e3);
10564
- }
10594
+ if (result.filling) refetchOnDoubt(from, to);
10565
10595
  }).catch((err) => {
10566
10596
  if (this._activeUID !== uid) return;
10567
10597
  const message = err instanceof Error ? err.message : String(err);
@@ -25585,7 +25615,7 @@ var demonstrationTool = {
25585
25615
  };
25586
25616
 
25587
25617
  // src/version.ts
25588
- var FORGECHARTS_VERSION = "1.5.68" ;
25618
+ var FORGECHARTS_VERSION = "1.5.70" ;
25589
25619
 
25590
25620
  // src/compatibility.ts
25591
25621
  var MIN_KIT_API = 1;