@perspective-dev/viewer-charts 4.5.2 → 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.
Files changed (61) hide show
  1. package/dist/cdn/perspective-viewer-charts.js +2 -2
  2. package/dist/cdn/perspective-viewer-charts.js.map +3 -3
  3. package/dist/esm/charts/candlestick/candlestick.d.ts +8 -10
  4. package/dist/esm/charts/chart-base.d.ts +58 -4
  5. package/dist/esm/charts/chart.d.ts +45 -9
  6. package/dist/esm/charts/series/glyphs/draw-areas.d.ts +8 -2
  7. package/dist/esm/charts/series/glyphs/draw-bars.d.ts +8 -1
  8. package/dist/esm/charts/series/glyphs/draw-lines.d.ts +8 -2
  9. package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +8 -2
  10. package/dist/esm/charts/series/series-build.d.ts +11 -0
  11. package/dist/esm/charts/series/series-interact.d.ts +13 -2
  12. package/dist/esm/charts/series/series-render.d.ts +17 -1
  13. package/dist/esm/charts/series/series.d.ts +104 -10
  14. package/dist/esm/data/view-reader.d.ts +10 -1
  15. package/dist/esm/event-detail.d.ts +7 -0
  16. package/dist/esm/layout/ticks.d.ts +5 -0
  17. package/dist/esm/perspective-viewer-charts.js +2 -2
  18. package/dist/esm/perspective-viewer-charts.js.map +3 -3
  19. package/dist/esm/plugin/charts.d.ts +6 -3
  20. package/dist/esm/plugin/plugin.d.ts +40 -2
  21. package/dist/esm/render/scheduler.d.ts +11 -8
  22. package/dist/esm/transport/protocol.d.ts +39 -2
  23. package/dist/esm/transport/renderer-transport.d.ts +75 -3
  24. package/dist/esm/webgl/instanced-attrs.d.ts +7 -1
  25. package/dist/esm/worker/renderer.worker.d.ts +12 -0
  26. package/package.json +1 -1
  27. package/src/ts/charts/candlestick/candlestick-render.ts +4 -1
  28. package/src/ts/charts/candlestick/candlestick.ts +12 -17
  29. package/src/ts/charts/cartesian/cartesian-render.ts +80 -63
  30. package/src/ts/charts/chart-base.ts +85 -5
  31. package/src/ts/charts/chart.ts +47 -9
  32. package/src/ts/charts/common/category-axis-resolver.ts +5 -1
  33. package/src/ts/charts/common/tree-data.ts +61 -16
  34. package/src/ts/charts/heatmap/heatmap-render.ts +6 -2
  35. package/src/ts/charts/series/glyphs/draw-areas.ts +21 -1
  36. package/src/ts/charts/series/glyphs/draw-bars.ts +30 -9
  37. package/src/ts/charts/series/glyphs/draw-lines.ts +21 -1
  38. package/src/ts/charts/series/glyphs/draw-scatter.ts +68 -3
  39. package/src/ts/charts/series/series-build.ts +21 -3
  40. package/src/ts/charts/series/series-interact.ts +210 -78
  41. package/src/ts/charts/series/series-render.ts +779 -49
  42. package/src/ts/charts/series/series.ts +209 -26
  43. package/src/ts/charts/sunburst/sunburst-interact.ts +2 -3
  44. package/src/ts/charts/sunburst/sunburst-render.ts +3 -1
  45. package/src/ts/charts/treemap/treemap-interact.ts +2 -5
  46. package/src/ts/charts/treemap/treemap-render.ts +3 -1
  47. package/src/ts/charts/treemap/treemap.ts +6 -1
  48. package/src/ts/data/view-reader.ts +22 -2
  49. package/src/ts/event-detail.ts +8 -0
  50. package/src/ts/index.ts +13 -7
  51. package/src/ts/interaction/raw-event-forwarder.ts +7 -0
  52. package/src/ts/interaction/zoom-controller.ts +8 -0
  53. package/src/ts/layout/ticks.ts +9 -0
  54. package/src/ts/plugin/charts.ts +20 -4
  55. package/src/ts/plugin/plugin.ts +99 -17
  56. package/src/ts/render/scheduler.ts +39 -13
  57. package/src/ts/transport/protocol.ts +42 -0
  58. package/src/ts/transport/renderer-transport.ts +198 -9
  59. package/src/ts/webgl/instanced-attrs.ts +15 -1
  60. package/src/ts/worker/dispatch.ts +4 -1
  61. package/src/ts/worker/renderer.worker.ts +35 -3
@@ -84,7 +84,11 @@ interface RendererHandle {
84
84
  terminate(): void;
85
85
  }
86
86
 
87
- type PendingRenderType = "saveZoom" | "loadAndRender" | "snapshotPng";
87
+ type PendingRenderType =
88
+ | "saveZoom"
89
+ | "loadAndRender"
90
+ | "snapshotPng"
91
+ | "resize";
88
92
  interface PendingRenderRequest {
89
93
  kind: PendingRenderType;
90
94
  resolve: (v: any) => void;
@@ -110,11 +114,22 @@ interface PendingRenderRequest {
110
114
  */
111
115
  export class RendererTransport {
112
116
  private _handle: RendererHandle | null = null;
117
+
118
+ /**
119
+ * Set by {@link destroy}. Requests allocated after teardown settle
120
+ * immediately in `_allocPending` — `_post` is already a no-op once
121
+ * `_handle` is null, so a late request would otherwise never
122
+ * receive its reply and pend forever.
123
+ */
124
+ private _destroyed = false;
113
125
  private _proxyChannel: MessageChannel | null = null;
114
126
  private _proxySession: any = null;
115
127
  private _client: Client;
116
128
  private _view: View;
117
129
  private _tableName: string | undefined;
130
+ /** Source panel `slot`, tagged onto dispatched interaction events so the
131
+ * host can route them (master/detail, global filter) to the right panel. */
132
+ private _panel: string | undefined;
118
133
  private _clientWorkerURL: URL;
119
134
  private _clientWasm: WebAssembly.Module;
120
135
  private _chartTag: string;
@@ -158,6 +173,37 @@ export class RendererTransport {
158
173
  */
159
174
  private _displayCtx: CanvasRenderingContext2D | null = null;
160
175
 
176
+ /**
177
+ * Present-hold state for the staged-present presize protocol
178
+ * ({@link presize}). While holding, inbound `frameBitmap`s are
179
+ * STAGED (latest wins) instead of blitted, so the on-screen frame
180
+ * — old dimensions in the old, unchanged box — stays put until the
181
+ * host invokes the present closure `presize` resolved to, in the
182
+ * same task as its layout commit. Entered only by `presize`;
183
+ * exited by the present closure, by `presize`'s rejection path, or
184
+ * by `destroy` — every settle path releases the hold or hands the
185
+ * caller the release.
186
+ */
187
+ /**
188
+ * The last `(cssWidth, cssHeight, dpr)` posted to the worker — at init
189
+ * or any resize — i.e. the dimensions the worker has already rendered
190
+ * (or has an in-flight render for). {@link resize} short-circuits when
191
+ * its measurement matches (±0.5px, the presize protocol's own
192
+ * threshold): reactive resizes fan in from several host paths per
193
+ * layout transition (the post-commit fan-out, activation nudges, the
194
+ * fresh-restore tail), and every worker resize is an unconditional
195
+ * full re-render.
196
+ */
197
+ private _lastPostedSize: {
198
+ cssWidth: number;
199
+ cssHeight: number;
200
+ dpr: number;
201
+ } | null = null;
202
+
203
+ private _holdPresent = false;
204
+
205
+ private _stagedFrame: ImageBitmap | null = null;
206
+
161
207
  /**
162
208
  * Host-side sink for tooltip + cursor side-effects. The chart
163
209
  * inside the renderer calls into a `MessageHostSink` that posts
@@ -182,6 +228,7 @@ export class RendererTransport {
182
228
  client: Client;
183
229
  view: View;
184
230
  tableName?: string;
231
+ panel?: string;
185
232
  clientWasm: WebAssembly.Module;
186
233
  clientWorkerURL: URL;
187
234
  chartTag: string;
@@ -192,6 +239,7 @@ export class RendererTransport {
192
239
  this._client = opts.client;
193
240
  this._view = opts.view;
194
241
  this._tableName = opts.tableName;
242
+ this._panel = opts.panel;
195
243
  this._clientWorkerURL = opts.clientWorkerURL;
196
244
  this._clientWasm = opts.clientWasm;
197
245
  this._chartTag = opts.chartTag;
@@ -210,6 +258,7 @@ export class RendererTransport {
210
258
  chrome: HTMLCanvasElement;
211
259
  facetConfig: FacetConfig;
212
260
  pluginConfig: PluginConfig;
261
+ columnsConfig?: Record<string, any>;
213
262
  defaultChartType?: string;
214
263
  renderBlitMode: "blit" | "direct";
215
264
  }): Promise<void> {
@@ -291,6 +340,7 @@ export class RendererTransport {
291
340
  tableName: this._tableName,
292
341
  facetConfig: opts.facetConfig,
293
342
  pluginConfig: opts.pluginConfig,
343
+ columnsConfig: opts.columnsConfig,
294
344
  defaultChartType: opts.defaultChartType,
295
345
  themeVars,
296
346
  fontFaces,
@@ -301,6 +351,11 @@ export class RendererTransport {
301
351
  precompileShaders: this._precompileShaders,
302
352
  };
303
353
 
354
+ this._lastPostedSize = {
355
+ cssWidth: rect.width,
356
+ cssHeight: rect.height,
357
+ dpr,
358
+ };
304
359
  this._handle = await this._createHandle(workerURL, initMsg);
305
360
  this._handle.addMessageListener((msg) =>
306
361
  this._handleRendererMsg(msg as WorkerMsg),
@@ -438,7 +493,8 @@ export class RendererTransport {
438
493
  * `loadAndRenderAck`. Per the worker's "resolve on stale"
439
494
  * contract, a mid-flight cancellation (a newer `loadAndRender`
440
495
  * superseding this one) still acks — the host's awaiter just
441
- * resolves quietly.
496
+ * resolves quietly. Calls issued after `destroy()` resolve
497
+ * immediately (see `_allocPending`).
442
498
  */
443
499
  loadAndRender(opts: {
444
500
  viewerConfig: {
@@ -464,19 +520,107 @@ export class RendererTransport {
464
520
  this._post({ kind: "redraw" });
465
521
  }
466
522
 
467
- resize(): void {
523
+ /**
524
+ * Silently clear the worker chart's selection state (pinned tooltip) —
525
+ * no selection events are emitted; the worker echoes `dismissTooltip`
526
+ * to drop the host-side pinned artifact. See `DeselectMsg`.
527
+ */
528
+ deselect(): void {
529
+ this._post({ kind: "deselect" });
530
+ }
531
+
532
+ /**
533
+ * Resize the chart to the GL canvas's CURRENT CSS box and resolve
534
+ * once the resized frame has PRESENTED (the worker's `resizeAck`) —
535
+ * not at message-post, which would let callers proceed while the
536
+ * old-dimensions bitmap is still on screen. Reactive geometry
537
+ * changes only; anticipated ones go through {@link presize}.
538
+ *
539
+ * A measurement matching {@link _lastPostedSize} resolves immediately
540
+ * WITHOUT a worker round trip: the worker has already rendered (or is
541
+ * presenting) that exact frame, and re-posting would be a full
542
+ * re-render of an identical bitmap.
543
+ */
544
+ resize(): Promise<void> {
468
545
  if (!this._hostGlCanvas) {
469
- return;
546
+ return Promise.resolve();
470
547
  }
471
548
 
472
549
  const rect = this._hostGlCanvas.getBoundingClientRect();
473
550
  const dpr = window.devicePixelRatio || 1;
551
+ const last = this._lastPostedSize;
552
+ if (
553
+ last &&
554
+ Math.abs(last.cssWidth - rect.width) <= 0.5 &&
555
+ Math.abs(last.cssHeight - rect.height) <= 0.5 &&
556
+ last.dpr === dpr
557
+ ) {
558
+ return Promise.resolve();
559
+ }
560
+
561
+ return this._postResize(rect.width, rect.height);
562
+ }
563
+
564
+ /**
565
+ * Staged-present variant of {@link resize}: render at the given
566
+ * TARGET canvas CSS box (no measurement — the caller supplies the
567
+ * box the pending layout commit will produce), HOLDING the
568
+ * resulting frame (and any others that arrive meanwhile) offscreen
569
+ * instead of blitting, so nothing on screen changes. Resolves,
570
+ * once the resized frame is staged, to a present closure: calling
571
+ * it blits the latest staged frame and exits the hold — the host
572
+ * runs it in the same task as its layout commit so geometry and
573
+ * pixels land in one paint. On rejection (teardown) the hold is
574
+ * released here, so every settle path either frees the display or
575
+ * hands the caller the release.
576
+ */
577
+ presize(cssWidth: number, cssHeight: number): Promise<(() => void) | void> {
578
+ if (!this._hostGlCanvas) {
579
+ return Promise.resolve();
580
+ }
581
+
582
+ const dpr = window.devicePixelRatio || 1;
583
+ const last = this._lastPostedSize;
584
+ if (
585
+ last &&
586
+ Math.abs(last.cssWidth - cssWidth) <= 0.5 &&
587
+ Math.abs(last.cssHeight - cssHeight) <= 0.5 &&
588
+ last.dpr === dpr
589
+ ) {
590
+ return Promise.resolve();
591
+ }
592
+
593
+ this._holdPresent = true;
594
+ return this._postResize(cssWidth, cssHeight).then(
595
+ () => () => this._presentStaged(),
596
+ (err) => {
597
+ this._presentStaged();
598
+ throw err;
599
+ },
600
+ );
601
+ }
602
+
603
+ private _postResize(cssWidth: number, cssHeight: number): Promise<void> {
604
+ const dpr = window.devicePixelRatio || 1;
605
+ const { id, promise } = this._allocPending<void>("resize");
606
+ this._lastPostedSize = { cssWidth, cssHeight, dpr };
474
607
  this._post({
475
608
  kind: "resize",
476
- cssWidth: rect.width,
477
- cssHeight: rect.height,
609
+ msgId: id,
610
+ cssWidth,
611
+ cssHeight,
478
612
  dpr,
479
613
  });
614
+ return promise;
615
+ }
616
+
617
+ private _presentStaged(): void {
618
+ this._holdPresent = false;
619
+ const staged = this._stagedFrame;
620
+ this._stagedFrame = null;
621
+ if (staged) {
622
+ this._drawFrameBitmap(staged);
623
+ }
480
624
  }
481
625
 
482
626
  clear() {
@@ -501,12 +645,39 @@ export class RendererTransport {
501
645
  * Allocate a pending request slot of the given `kind`. Returns the
502
646
  * id (encoded into the outgoing `ControlMsg`) and a promise that
503
647
  * resolves / rejects when the matching reply arrives or
504
- * `destroy()` drains the table.
648
+ * `destroy()` drains the table. Requests issued *after* `destroy()`
649
+ * settle immediately with the same per-kind semantics as the drain.
505
650
  */
506
651
  private _allocPending<T>(kind: PendingRenderType): {
507
652
  id: number;
508
653
  promise: Promise<T>;
509
654
  } {
655
+ // A request issued after `destroy()` would never settle: `_post`
656
+ // no-ops with no handle, so no reply ever lands — and the host
657
+ // awaits `loadAndRender` while holding its per-renderer draw
658
+ // lock, so a pending-forever promise wedges that panel's lock
659
+ // permanently. (Reachable when an external DOM disconnect
660
+ // `delete()`s the element mid-draw; the host's own teardown
661
+ // paths are serialized behind the same lock.) Mirror the
662
+ // `destroy()` drain: `loadAndRender` resolves silently,
663
+ // reply-bearing kinds reject. The pre-attached no-op `catch`
664
+ // marks the rejection handled for fire-and-forget callers
665
+ // (`saveZoom`) without affecting real awaiters.
666
+ if (this._destroyed) {
667
+ if (kind === "loadAndRender") {
668
+ return {
669
+ id: -1,
670
+ promise: Promise.resolve(undefined as unknown as T),
671
+ };
672
+ }
673
+
674
+ const promise = Promise.reject<T>(
675
+ new Error("RendererTransport destroyed"),
676
+ );
677
+ promise.catch(() => {});
678
+ return { id: -1, promise };
679
+ }
680
+
510
681
  const id = ++this._pendingCounter;
511
682
  const promise = new Promise<T>((resolve, reject) => {
512
683
  this._pending.set(id, { kind, resolve, reject });
@@ -548,6 +719,7 @@ export class RendererTransport {
548
719
  }
549
720
 
550
721
  destroy(): void {
722
+ this._destroyed = true;
551
723
  this._post({ kind: "destroy" });
552
724
  if (this._proxySession) {
553
725
  this._proxySession.close().catch(() => {});
@@ -572,6 +744,9 @@ export class RendererTransport {
572
744
  // them, and so the GPU-backed 2D context can release earlier.
573
745
  this._hostGlCanvas = null;
574
746
  this._displayCtx = null;
747
+ this._holdPresent = false;
748
+ this._stagedFrame?.close();
749
+ this._stagedFrame = null;
575
750
 
576
751
  // Drain pending request promises with kind-aware semantics:
577
752
  // - `loadAndRender` resolves silently (the host's awaited draw
@@ -633,7 +808,9 @@ export class RendererTransport {
633
808
  {
634
809
  bubbles: true,
635
810
  composed: true,
636
- detail: msg.detail,
811
+ // Tag with the source panel so a multi-panel host
812
+ // can attribute the click to the right panel.
813
+ detail: { ...msg.detail, panel: this._panel },
637
814
  },
638
815
  ),
639
816
  );
@@ -658,6 +835,9 @@ export class RendererTransport {
658
835
  removeConfigs as any,
659
836
  insertConfigs as any,
660
837
  );
838
+ // Tag with the source panel so the host's master/detail +
839
+ // global-filter routing can identify the originating panel.
840
+ detail.panel = this._panel;
661
841
  this._dispatchOnViewer(
662
842
  new CustomEvent<PerspectiveSelectDetail>(
663
843
  "perspective-global-filter",
@@ -672,7 +852,13 @@ export class RendererTransport {
672
852
  }
673
853
 
674
854
  case "frameBitmap":
675
- this._drawFrameBitmap(msg.bitmap);
855
+ if (this._holdPresent) {
856
+ this._stagedFrame?.close();
857
+ this._stagedFrame = msg.bitmap;
858
+ } else {
859
+ this._drawFrameBitmap(msg.bitmap);
860
+ }
861
+
676
862
  break;
677
863
  case "error":
678
864
  this._rejectReady(new Error(msg.message));
@@ -680,6 +866,9 @@ export class RendererTransport {
680
866
  case "loadAndRenderAck":
681
867
  this._resolvePending(msg.msgId, "loadAndRender", undefined);
682
868
  break;
869
+ case "resizeAck":
870
+ this._resolvePending(msg.msgId, "resize", undefined);
871
+ break;
683
872
  case "snapshotPngReply":
684
873
  this._resolvePending(msg.requestId, "snapshotPng", msg.blob);
685
874
  break;
@@ -108,6 +108,12 @@ export function createQuadCornerBuffer(gl: GL): WebGLBuffer {
108
108
  * paint zero data). No-op return `true` when `attr` is negative
109
109
  * (optimized-out attribute — drawing the rest is still valid).
110
110
  *
111
+ * `firstInstance` byte-offsets the attribute pointer so the draw's
112
+ * instance 0 reads element `firstInstance` of the buffer — the WebGL
113
+ * substitute for a `baseInstance` draw parameter. Faceted renderers
114
+ * use it to draw a contiguous per-facet sub-range of a shared
115
+ * instance buffer.
116
+ *
111
117
  * Render-path uses `peek`, never `getOrCreate`: the latter recreates
112
118
  * with zero-initialized contents when `_totalCapacity` has grown past
113
119
  * the current buffer, which would wipe the previous draw's vertex
@@ -120,6 +126,7 @@ export function bindInstancedFloatAttr(
120
126
  attr: number,
121
127
  name: string,
122
128
  components: number,
129
+ firstInstance = 0,
123
130
  ): boolean {
124
131
  if (attr < 0) {
125
132
  return true;
@@ -133,7 +140,14 @@ export function bindInstancedFloatAttr(
133
140
  const gl: GL = glManager.gl;
134
141
  gl.bindBuffer(gl.ARRAY_BUFFER, buf.buffer);
135
142
  gl.enableVertexAttribArray(attr);
136
- gl.vertexAttribPointer(attr, components, gl.FLOAT, false, 0, 0);
143
+ gl.vertexAttribPointer(
144
+ attr,
145
+ components,
146
+ gl.FLOAT,
147
+ false,
148
+ 0,
149
+ firstInstance * components * Float32Array.BYTES_PER_ELEMENT,
150
+ );
137
151
  instancing.setDivisor(attr, 1);
138
152
  return true;
139
153
  }
@@ -40,9 +40,12 @@ export function dispatch(r: WorkerRenderer, msg: ControlMsg): void {
40
40
  case "redraw":
41
41
  r.redraw();
42
42
  break;
43
+ case "deselect":
44
+ r.chartImpl.deselect?.();
45
+ break;
43
46
  case "resize":
44
47
  r.resize(msg.cssWidth, msg.cssHeight, msg.dpr);
45
- r.redraw();
48
+ r.redrawAck(msg.msgId);
46
49
  break;
47
50
  case "clear":
48
51
  r.clear();
@@ -204,6 +204,9 @@ export class WorkerRenderer {
204
204
 
205
205
  this.chartImpl.setFacetConfig?.(msg.facetConfig);
206
206
  this.chartImpl.setPluginConfig?.(msg.pluginConfig);
207
+ if (msg.columnsConfig) {
208
+ this.chartImpl.setColumnsConfig?.(msg.columnsConfig);
209
+ }
207
210
 
208
211
  if (this.chartImpl.setZoomController) {
209
212
  this.zoomController = new ZoomController();
@@ -295,16 +298,23 @@ export class WorkerRenderer {
295
298
  try {
296
299
  await viewToColumnDataMap(
297
300
  this.view,
298
- async (cols) => {
301
+ async (cols, deliveredRows) => {
299
302
  if (this._renderGen !== myGen) {
300
303
  throw new StaleGenerationError();
301
304
  }
302
305
 
306
+ // `totalRows` came from a SEPARATE `num_rows()`
307
+ // round-trip; a fast-updating table with deletions
308
+ // can shrink the view before the Arrow fetch, which
309
+ // clamps its window silently. Iterating to the stale
310
+ // count reads typed arrays out-of-bounds (`undefined`
311
+ // values no validity bitmap covers) — clamp to what
312
+ // was actually delivered.
303
313
  await this.chartImpl.uploadAndRender(
304
314
  this.glManager,
305
315
  cols,
306
316
  0,
307
- totalRows,
317
+ Math.min(totalRows, deliveredRows),
308
318
  );
309
319
  },
310
320
  { end_row: totalRows, float32: msg.options.float32 },
@@ -327,6 +337,24 @@ export class WorkerRenderer {
327
337
  this.chartImpl.requestRender(this.glManager);
328
338
  }
329
339
 
340
+ /**
341
+ * `redraw`, acked to the host AFTER the frame's present completes —
342
+ * the reply backing the viewer's presize contract (`ResizeAckMsg`):
343
+ * `plugin.resize()` must not resolve until the resized frame is
344
+ * actually on screen. A resize landing mid-present defers its
345
+ * canvas mutation (`deferIfDraining`), but the redraw's own frame
346
+ * applies pending dimensions in `beginFrame`, so the present this
347
+ * awaits IS at the new size. Always acks — a failed present or a
348
+ * torn-down chart resolves the host's await rather than stranding
349
+ * it (completion, not success, is the contract).
350
+ */
351
+ redrawAck(msgId: number): void {
352
+ this.chartImpl
353
+ .requestRender(this.glManager)
354
+ .catch(() => {})
355
+ .finally(() => this.post({ kind: "resizeAck", msgId }));
356
+ }
357
+
330
358
  resize(cssWidth: number, cssHeight: number, dpr: number): void {
331
359
  // `glManager.resize` would set `canvas.width = N`, which the
332
360
  // spec mandates clears the drawing buffer immediately. In
@@ -572,7 +600,11 @@ export class WorkerRenderer {
572
600
  // chart left the shared canvas at its own size) would render at
573
601
  // the wrong dimensions.
574
602
  this.glManager.beginFrame();
575
- this.chartImpl._fullRender(this.glManager);
603
+ // `renderFrameSync` (not `_fullRender`) so the gridline + chrome
604
+ // 2D canvases are actually painted here — the scheduler normally
605
+ // defers those to a post-fence flush, but the snapshot path
606
+ // bypasses the scheduler and composites the 2D layers below.
607
+ this.chartImpl.renderFrameSync(this.glManager);
576
608
  const gl = this.glManager.gl;
577
609
  const glCanvas = gl.canvas as OffscreenCanvas;
578
610
  const w = glCanvas.width;