@trdrs/quickcharts 5.0.1 → 5.1.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 5.1.0
6
+
7
+ Charts open live subscriptions alongside history, frame the first arriving data, and retain newer streamed prices when history arrives later. A snapshot with `backfill: true` adds missing history without replacing bars already held.
8
+
5
9
  ## 5.0.1
6
10
 
7
11
  Drawing gestures respect presses handled by host overlays and leave presses outside the plot to the chart scales.
package/README.md CHANGED
@@ -143,6 +143,9 @@ export const myFeed: ChartDatafeed = {
143
143
  4. **Live updates mutate the last bar or append.** `subscribeBars` emits `{ kind: 'snapshot', bars }` on
144
144
  connect and after every reconnect (the self-healing re-sync), then `{ kind: 'bar', bar }` per update.
145
145
  A bar's `t` is its **bucket-open** epoch-seconds time, never the update's wall-clock time.
146
+ The chart opens the subscription alongside history. The first data frames the chart; history
147
+ arriving later adds older bars and retains newer stream readings. Mark a preliminary history
148
+ snapshot with `backfill: true`: it fills missing timestamps without replacing bars already held.
146
149
  5. **Bar time is epoch SECONDS.** Not milliseconds.
147
150
  6. **No feed for a symbol is terminal.** Throw `FeedUnavailableError` from `history` when no feed serves
148
151
  the symbol; the chart shows "market data unavailable" and stops. A transient fetch failure should
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.0.1",
2
+ "version": "5.1.0",
3
3
  "styles": [
4
4
  {
5
5
  "id": "candles",
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as ChartDatafeed, M as MarkIconId, F as FeedBar, S as SymbolRow, D as DatafeedSearchOptions, a as SearchPage, b as SearchClassNode } from './symbols-DdHLHQFC.js';
2
- export { B as BarMark, c as BarsEvent, d as DatafeedConfig, e as FeedUnavailableError, H as HistoryPage, f as MarkColor, g as MarkColorPair, h as MarkColorRole, i as MarkLine, j as MarkLineStyle, k as MarkPlacement, l as MarkShape, O as OlderPageVerdict, P as PricesHandlers, m as SubscribeHandlers, n as SymbolNames, o as SymbolPrices, T as TimescaleMark, p as olderPageVerdict, s as symbolNames } from './symbols-DdHLHQFC.js';
1
+ import { C as ChartDatafeed, M as MarkIconId, F as FeedBar, S as SymbolRow, D as DatafeedSearchOptions, a as SearchPage, b as SearchClassNode } from './symbols-CHJGWrf-.js';
2
+ export { B as BarMark, c as BarsEvent, d as DatafeedConfig, e as FeedUnavailableError, H as HistoryPage, f as MarkColor, g as MarkColorPair, h as MarkColorRole, i as MarkLine, j as MarkLineStyle, k as MarkPlacement, l as MarkShape, O as OlderPageVerdict, P as PricesHandlers, m as SubscribeHandlers, n as SymbolNames, o as SymbolPrices, T as TimescaleMark, p as olderPageVerdict, s as symbolNames } from './symbols-CHJGWrf-.js';
3
3
  import { ao as en, C as ChartMessageKey, G as GlyphSourcePort, M as MagnetMode, d as CursorMode, Q as TextEditFrame, N as TextDraft, L as LineStyle, I as IDrawing, m as DrawingCounts, D as DrawingStyle, $ as VisibilityPreset, X as ToolPreset, Y as ToolTemplate, S as SerializedDrawing, o as DrawingPreferences, l as DrawingAssetPort } from './assets-DF3nozNV.js';
4
4
  import { b as ResourceRef, C as ChartSaveLoadAdapter, q as ChartMeta, t as ChartBody, h as DrawingResourceContext, R as ResourceStore, u as DrawingsMeta, j as DrawingsBody, T as TemplateMeta, a as TemplateBody, v as LayoutMeta, w as LayoutBody, e as DrawingContextKind } from './resources-XglhzzW0.js';
5
5
  export { c as ChartLocalDrawingContext, D as DRAWING_CONTEXT_VERSION, d as DRAWING_DOCUMENT_VERSION, f as DrawingEntry, g as DrawingGroup, i as DrawingTombstone, L as LayoutSharedDrawingContext, M as MemoryResourcesOptions, x as ResourceAbortError, S as SymbolGlobalDrawingContext, y as TemplateKind, W as WriteOutcome, k as drawingContextKey, l as emptyDrawingDocument, m as liveDrawingEntries, n as liveDrawingGroups, z as memorySaveLoadAdapter, o as mergeDrawingDocuments, p as parseDrawingDocument, r as reviseDrawingDocument, s as sameDrawingContext } from './resources-XglhzzW0.js';
package/dist/index.js CHANGED
@@ -4912,6 +4912,17 @@ function layoutChoices(offered) {
4912
4912
  return (arrangementOf(offered.arrangements[0])?.count ?? 1) > 1 && offered.sync.length > 0;
4913
4913
  }
4914
4914
 
4915
+ // src/widget/barSnapshot.ts
4916
+ function mergeBarSnapshot(held, incoming, backfill = false) {
4917
+ if (backfill) {
4918
+ const merged2 = new Map(incoming.map((bar) => [bar.t, bar]));
4919
+ for (const bar of held) merged2.set(bar.t, bar);
4920
+ return [...merged2.values()].sort((a, b) => a.t - b.t);
4921
+ }
4922
+ const first = incoming[0]?.t;
4923
+ return first === void 0 ? [...incoming] : [...held.filter((bar) => bar.t < first), ...incoming];
4924
+ }
4925
+
4915
4926
  // src/widget/chart.ts
4916
4927
  import {
4917
4928
  CrosshairMode as CrosshairMode2,
@@ -16653,8 +16664,7 @@ function attachReplayPlane(deps) {
16653
16664
  if (master === null) return false;
16654
16665
  if (event.kind === "snapshot") {
16655
16666
  const fresh = event.bars.filter((b) => deps.visible(b.t));
16656
- const first = fresh[0]?.t;
16657
- master = first === void 0 ? fresh : [...master.filter((b) => b.t < first), ...fresh];
16667
+ master = mergeBarSnapshot(master, fresh, event.backfill);
16658
16668
  } else if (deps.visible(event.bar.t)) {
16659
16669
  const last = master[master.length - 1];
16660
16670
  if (!last || event.bar.t > last.t) master = [...master, event.bar];
@@ -21961,6 +21971,7 @@ function createChartInstance(deps) {
21961
21971
  let symbolFormatter = createPriceFormatter(UNRESOLVED_PRICE_FORMAT, { locale: i18n.tag() });
21962
21972
  let epoch = 0;
21963
21973
  let historyPainted = false;
21974
+ let initialHistoryPending = false;
21964
21975
  let pendingFrame = null;
21965
21976
  let pendingLeftEdge = null;
21966
21977
  let menuLevel = null;
@@ -22995,7 +23006,7 @@ function createChartInstance(deps) {
22995
23006
  }
22996
23007
  function maybePageBack(runway = PAGE_RUNWAY) {
22997
23008
  if (replay2.active()) return;
22998
- if (paging || noMoreHistory || bars.length === 0) return;
23009
+ if (initialHistoryPending || paging || noMoreHistory || bars.length === 0) return;
22999
23010
  const range2 = chart.timeScale().getVisibleLogicalRange();
23000
23011
  if (!range2 || range2.from > PAGE_TRIGGER_BARS) return;
23001
23012
  paging = true;
@@ -23124,6 +23135,7 @@ function createChartInstance(deps) {
23124
23135
  bars = [];
23125
23136
  paging = false;
23126
23137
  historyPainted = false;
23138
+ initialHistoryPending = true;
23127
23139
  if (holdFrame !== null) window.cancelAnimationFrame(holdFrame);
23128
23140
  holdFrame = null;
23129
23141
  mainScale().applyOptions({ autoScale: true });
@@ -23169,49 +23181,72 @@ function createChartInstance(deps) {
23169
23181
  }).catch(() => {
23170
23182
  });
23171
23183
  }
23172
- void datafeed.history(symbol, tf, { countBack: firstPage }).then((page) => {
23173
- if (disposed || myEpoch !== epoch) return;
23174
- bars = [...page.bars];
23175
- paintAll();
23176
- historyPainted = true;
23177
- const framing = pendingFrame?.epoch === myEpoch ? pendingFrame : null;
23178
- pendingFrame = null;
23179
- const edge = pendingLeftEdge?.epoch === myEpoch ? pendingLeftEdge : null;
23180
- pendingLeftEdge = null;
23181
- if (framing) {
23182
- frameRange(chart, anchor, framing.span, framing.tf);
23183
- fillSpan(framing.span, framing.tf);
23184
- } else if (edge) keepLeftEdge(edge);
23185
- else chart.timeScale().fitContent();
23186
- holdAxisAfterFrame();
23187
- refreshMarks();
23184
+ let firstFrameSize = 0;
23185
+ let frameNavigation = -1;
23186
+ const firstPaint = (complete = false) => {
23187
+ if (complete) initialHistoryPending = false;
23188
+ const grow = complete && firstFrameSize < firstPage && bars.length > firstFrameSize && frameNavigation === navigationEpoch;
23189
+ if (bars.length > 0 && (!historyPainted || grow)) {
23190
+ historyPainted = true;
23191
+ const framing = pendingFrame?.epoch === myEpoch ? pendingFrame : null;
23192
+ pendingFrame = null;
23193
+ const edge = pendingLeftEdge?.epoch === myEpoch ? pendingLeftEdge : null;
23194
+ pendingLeftEdge = null;
23195
+ if (framing) {
23196
+ frameRange(chart, anchor, framing.span, framing.tf);
23197
+ fillSpan(framing.span, framing.tf);
23198
+ } else if (edge) keepLeftEdge(edge);
23199
+ else chart.timeScale().fitContent();
23200
+ firstFrameSize = bars.length;
23201
+ frameNavigation = framing || edge ? -1 : navigationEpoch;
23202
+ holdAxisAfterFrame();
23203
+ refreshMarks();
23204
+ }
23188
23205
  if (!ready) {
23189
23206
  ready = true;
23190
23207
  deps.onReady();
23191
23208
  }
23192
- openSubscription(myEpoch);
23209
+ };
23210
+ queueMicrotask(() => openSubscription(myEpoch, firstPaint));
23211
+ void datafeed.history(symbol, tf, { countBack: firstPage }).then((page) => {
23212
+ if (disposed || myEpoch !== epoch) return;
23213
+ const merged2 = new Map(page.bars.map((bar) => [bar.t, bar]));
23214
+ for (const bar of bars) merged2.set(bar.t, bar);
23215
+ const write = () => {
23216
+ bars = [...merged2.values()].sort((a, b) => a.t - b.t);
23217
+ paintAll();
23218
+ };
23219
+ if (historyPainted) maintainTimeline(write);
23220
+ else write();
23221
+ firstPaint(true);
23193
23222
  }).catch((e) => {
23194
23223
  if (disposed || myEpoch !== epoch) return;
23195
- historyPainted = true;
23196
- pendingFrame = null;
23197
23224
  if (e instanceof FeedUnavailableError) {
23225
+ unsubscribe?.();
23226
+ unsubscribe = null;
23227
+ bars = [];
23228
+ paintAll();
23198
23229
  feedStatus = "feed_unavailable";
23199
23230
  events.emit("feedStatus", "feed_unavailable");
23200
- return;
23201
23231
  }
23202
- openSubscription(myEpoch);
23232
+ }).finally(() => {
23233
+ if (myEpoch === epoch) initialHistoryPending = false;
23203
23234
  });
23204
23235
  }
23205
- function openSubscription(myEpoch) {
23236
+ function openSubscription(myEpoch, firstPaint) {
23206
23237
  if (disposed || myEpoch !== epoch) return;
23207
- unsubscribe = datafeed.subscribeBars(symbol, tf, {
23238
+ let active = true;
23239
+ const off = datafeed.subscribeBars(symbol, tf, {
23208
23240
  onBars: (e) => {
23209
- if (disposed || myEpoch !== epoch) return;
23241
+ if (!active || disposed || myEpoch !== epoch) return;
23210
23242
  const replaying = replay2.absorb(e);
23211
23243
  if (e.kind === "snapshot") {
23212
- const first = e.bars[0]?.t;
23213
- bars = first === void 0 ? [...e.bars] : [...bars.filter((b) => b.t < first), ...e.bars];
23214
- if (!replaying) paintAll();
23244
+ const write = () => {
23245
+ bars = mergeBarSnapshot(bars, e.bars, e.backfill);
23246
+ if (!replaying) paintAll();
23247
+ };
23248
+ if (!replaying && historyPainted) maintainTimeline(write);
23249
+ else write();
23215
23250
  } else {
23216
23251
  const next = applyBar(bars, e.bar);
23217
23252
  if (next) {
@@ -23219,14 +23254,19 @@ function createChartInstance(deps) {
23219
23254
  if (!replaying) paintLast(e.bar);
23220
23255
  }
23221
23256
  }
23257
+ if (!replaying) firstPaint(e.kind === "snapshot" && !e.backfill && e.bars.length >= SNAPSHOT_BARS);
23222
23258
  },
23223
23259
  onStatus: (status2) => {
23224
- if (disposed || myEpoch !== epoch) return;
23260
+ if (!active || disposed || myEpoch !== epoch) return;
23225
23261
  feedStatus = status2;
23226
23262
  events.emit("feedStatus", status2);
23227
23263
  countdown?.refresh();
23228
23264
  }
23229
23265
  });
23266
+ unsubscribe = () => {
23267
+ active = false;
23268
+ off();
23269
+ };
23230
23270
  }
23231
23271
  const LIVE_EDGE_SLACK_BARS = 1;
23232
23272
  let awayFromLive = false;