@perspective-dev/viewer-charts 4.5.2 → 5.0.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 +3 -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 +3 -2
  9. package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +5 -2
  10. package/dist/esm/charts/series/series-build.d.ts +11 -0
  11. package/dist/esm/charts/series/series-interact.d.ts +10 -0
  12. package/dist/esm/charts/series/series-render.d.ts +17 -1
  13. package/dist/esm/charts/series/series.d.ts +53 -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 +31 -2
  21. package/dist/esm/render/scheduler.d.ts +11 -8
  22. package/dist/esm/transport/protocol.d.ts +31 -2
  23. package/dist/esm/transport/renderer-transport.d.ts +74 -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 +10 -1
  36. package/src/ts/charts/series/glyphs/draw-bars.ts +30 -9
  37. package/src/ts/charts/series/glyphs/draw-lines.ts +10 -1
  38. package/src/ts/charts/series/glyphs/draw-scatter.ts +56 -3
  39. package/src/ts/charts/series/series-build.ts +21 -3
  40. package/src/ts/charts/series/series-interact.ts +86 -13
  41. package/src/ts/charts/series/series-render.ts +660 -17
  42. package/src/ts/charts/series/series.ts +96 -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 +86 -16
  56. package/src/ts/render/scheduler.ts +39 -13
  57. package/src/ts/transport/protocol.ts +33 -0
  58. package/src/ts/transport/renderer-transport.ts +185 -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 +32 -3
@@ -30,9 +30,12 @@ export interface ChartTypeConfig {
30
30
  * Per-chart-type overrides for `DEFAULT_PLUGIN_CONFIG`. Used when a
31
31
  * field's sensible default differs by chart family — currently
32
32
  * `include_zero` (true for Y Bar / Y Area / X Bar, false for line
33
- * / scatter / cartesian / financial). Applied at schema generation
34
- * and at `restore({})` so the effective default matches the
35
- * surfaced UI default.
33
+ * / scatter / cartesian / financial) and `facet_mode` ("overlay"
34
+ * for the band-pipeline families — series / financial — whose
35
+ * historical split_by rendering is the single stacked/colored
36
+ * plot; "grid" elsewhere). Applied at schema generation and at
37
+ * `restore({})` so the effective default matches the surfaced UI
38
+ * default.
36
39
  */
37
40
  plugin_field_defaults?: Partial<PluginConfig>;
38
41
  }
@@ -10,7 +10,6 @@ export declare class HTMLPerspectiveViewerWebGLPluginElement extends HTMLElement
10
10
  private _renderer;
11
11
  private _rendererPromise;
12
12
  private _rawEventForwarder;
13
- private _generation;
14
13
  private _resetClickAbort;
15
14
  /**
16
15
  * Plugin-scoped global config. Seeded lazily from
@@ -97,10 +96,40 @@ export declare class HTMLPerspectiveViewerWebGLPluginElement extends HTMLElement
97
96
  };
98
97
  draw(view: View): Promise<void>;
99
98
  update(view: View): Promise<void>;
99
+ /**
100
+ * Shared body of `draw` / `update`. No re-entrancy guard: the host
101
+ * serializes every rendering call (`draw`, `update`, `render`,
102
+ * `resize`) and `delete` on its per-renderer draw lock, so this
103
+ * method never overlaps itself or a teardown. The one exception —
104
+ * an external DOM disconnect `delete()`ing the element mid-draw —
105
+ * is absorbed by `RendererTransport`, whose post-`destroy()`
106
+ * requests settle immediately instead of pending forever.
107
+ */
100
108
  private _drawImpl;
101
109
  clear(): Promise<void>;
102
110
  resize(): Promise<void>;
103
- restyle(): number;
111
+ /**
112
+ * OPTIONAL host presize protocol: render at the TARGET element box
113
+ * `(width, height)` — the box the host's pending layout commit will
114
+ * produce — holding the resulting frame offscreen, so nothing on
115
+ * screen changes during the round-trip. Resolves, once the resized
116
+ * frame is staged, to a present closure; the host calls it in the
117
+ * same task as the layout commit, landing geometry and pixels in
118
+ * one paint. Plugins without this method get the host's held
119
+ * style-override presize path instead.
120
+ */
121
+ presize(width: number, height: number): Promise<(() => void) | void>;
122
+ restyle(): void;
123
+ /**
124
+ * Clear any active selection state (pinned tooltip) WITHOUT emitting
125
+ * selection events — the host calls this when a global filter
126
+ * contributed by this panel's selection is removed from the global
127
+ * filter bar, so the pin can't outlive the filter it produced. The
128
+ * transport's `_lastInsertConfig` (remove-set memory) is deliberately
129
+ * RETAINED so a subsequent selection still replaces any leftover
130
+ * clauses rather than accumulating.
131
+ */
132
+ deselect(): Promise<void>;
104
133
  render(view: View): Promise<Blob>;
105
134
  restore(config: any, columns_config?: Record<string, any>): void;
106
135
  delete(): void;
@@ -1,16 +1,19 @@
1
1
  import type { WebGLContextManager } from "../webgl/context-manager";
2
2
  /**
3
- * Request a coalesced render of `glManager` whose body is
4
- * `fullRender`. Returns a promise that resolves when this entry's
5
- * Phase 2 (`awaitGpuFence` + `endFrame`) completes.
3
+ * Request a coalesced render of `glManager`. `fullRender` submits the
4
+ * GL commands (Phase 1, synchronous); `render2D` draws the 2D layers
5
+ * (gridlines + chrome) and runs in Phase 2 *after* `awaitGpuFence`, so
6
+ * the 2D canvases don't present ahead of the GL frame. Returns a
7
+ * promise that resolves when this entry's Phase 2 (`awaitGpuFence` +
8
+ * `render2D` + `endFrame`) completes.
6
9
  *
7
10
  * If a request is already pending for the same glManager, the new
8
- * call's `fullRender` closure replaces the prior one (latest call
9
- * wins; closures read chart state lazily so this is functionally a
10
- * no-op, but keeps the closure fresh) and the returned promise
11
- * resolves alongside the existing waiters.
11
+ * call's `fullRender` / `render2D` closures replace the prior ones
12
+ * (latest call wins; closures read chart state lazily so this is
13
+ * functionally a no-op, but keeps the closures fresh) and the returned
14
+ * promise resolves alongside the existing waiters.
12
15
  */
13
- export declare function requestRender(glManager: WebGLContextManager, fullRender: () => void): Promise<void>;
16
+ export declare function requestRender(glManager: WebGLContextManager, fullRender: () => void, render2D: () => void): Promise<void>;
14
17
  /**
15
18
  * Run `op` synchronously if no drain `present()` is currently active
16
19
  * for `glManager`. Otherwise queue `op` to run as soon as that
@@ -9,8 +9,8 @@ export type { ThemeSnapshot };
9
9
  * host's real `Client` — that's pure protobuf bytes; this one is the
10
10
  * chart's own renderer control plane.
11
11
  */
12
- export type ControlMsg = InitMsg | SetViewByNameMsg | SetColumnsConfigMsg | SetPluginConfigMsg | SetBufferMaxCapacityMsg | LoadAndRenderMsg | RedrawMsg | ResizeMsg | ClearMsg | InvalidateThemeMsg | RestoreZoomMsg | ResetAllZoomsMsg | ResetExpandedDomainMsg | SaveZoomReqMsg | SnapshotPngReqMsg | InteractionMsg | DestroyMsg;
13
- export type WorkerMsg = ReadyMsg | ZoomChangedMsg | SaveZoomReplyMsg | SnapshotPngReplyMsg | PinTooltipMsg | DismissTooltipMsg | SetCursorMsg | UserClickMsg | UserSelectMsg | LoadAndRenderAckMsg | FrameBitmapMsg | ErrorMsg;
12
+ export type ControlMsg = InitMsg | SetViewByNameMsg | SetColumnsConfigMsg | SetPluginConfigMsg | SetBufferMaxCapacityMsg | LoadAndRenderMsg | RedrawMsg | DeselectMsg | ResizeMsg | ClearMsg | InvalidateThemeMsg | RestoreZoomMsg | ResetAllZoomsMsg | ResetExpandedDomainMsg | SaveZoomReqMsg | SnapshotPngReqMsg | InteractionMsg | DestroyMsg;
13
+ export type WorkerMsg = ReadyMsg | ZoomChangedMsg | SaveZoomReplyMsg | SnapshotPngReplyMsg | PinTooltipMsg | DismissTooltipMsg | SetCursorMsg | UserClickMsg | UserSelectMsg | LoadAndRenderAckMsg | ResizeAckMsg | FrameBitmapMsg | ErrorMsg;
14
14
  /**
15
15
  * Session-tagged envelopes for the shared-worker transport. Every
16
16
  * message between the host and the shared `Worker` carries a numeric
@@ -230,12 +230,41 @@ export interface LoadAndRenderAckMsg {
230
230
  export interface RedrawMsg {
231
231
  kind: "redraw";
232
232
  }
233
+ /**
234
+ * Host → worker: silently clear the chart's selection state (pinned
235
+ * tooltip) — no `userSelect` reply, so the host filter-bar removal that
236
+ * triggered it can't echo back into the host's filter set. The worker's
237
+ * `TooltipController.dismiss()` posts `dismissTooltip` to tear down the
238
+ * host-side pinned artifact.
239
+ */
240
+ export interface DeselectMsg {
241
+ kind: "deselect";
242
+ }
243
+ /**
244
+ * Host → worker: resize the chart to a new CSS box. Replied with
245
+ * {@link ResizeAckMsg} AFTER the resized frame PRESENTS — the host's
246
+ * `plugin.resize()` promise is the viewer's presize contract ("the
247
+ * plugin has repainted at the target box when this resolves"), so an
248
+ * ack at message-receipt would let the settings-pane layout commit
249
+ * against the old-dimensions bitmap (the aspect-ratio warp).
250
+ */
233
251
  export interface ResizeMsg {
234
252
  kind: "resize";
253
+ msgId: number;
235
254
  cssWidth: number;
236
255
  cssHeight: number;
237
256
  dpr: number;
238
257
  }
258
+ /**
259
+ * Worker → host reply to a `ResizeMsg`, posted after the resized
260
+ * frame's present completes. Always sent — present failures and
261
+ * torn-down charts included — so the host's awaited promise resolves
262
+ * deterministically.
263
+ */
264
+ export interface ResizeAckMsg {
265
+ kind: "resizeAck";
266
+ msgId: number;
267
+ }
239
268
  export interface ClearMsg {
240
269
  kind: "clear";
241
270
  }
@@ -20,11 +20,21 @@ import type { InteractionEvent } from "./protocol";
20
20
  */
21
21
  export declare class RendererTransport {
22
22
  private _handle;
23
+ /**
24
+ * Set by {@link destroy}. Requests allocated after teardown settle
25
+ * immediately in `_allocPending` — `_post` is already a no-op once
26
+ * `_handle` is null, so a late request would otherwise never
27
+ * receive its reply and pend forever.
28
+ */
29
+ private _destroyed;
23
30
  private _proxyChannel;
24
31
  private _proxySession;
25
32
  private _client;
26
33
  private _view;
27
34
  private _tableName;
35
+ /** Source panel `slot`, tagged onto dispatched interaction events so the
36
+ * host can route them (master/detail, global filter) to the right panel. */
37
+ private _panel;
28
38
  private _clientWorkerURL;
29
39
  private _clientWasm;
30
40
  private _chartTag;
@@ -63,6 +73,30 @@ export declare class RendererTransport {
63
73
  * canvas's drawing buffer is the worker's transferred GL canvas).
64
74
  */
65
75
  private _displayCtx;
76
+ /**
77
+ * Present-hold state for the staged-present presize protocol
78
+ * ({@link presize}). While holding, inbound `frameBitmap`s are
79
+ * STAGED (latest wins) instead of blitted, so the on-screen frame
80
+ * — old dimensions in the old, unchanged box — stays put until the
81
+ * host invokes the present closure `presize` resolved to, in the
82
+ * same task as its layout commit. Entered only by `presize`;
83
+ * exited by the present closure, by `presize`'s rejection path, or
84
+ * by `destroy` — every settle path releases the hold or hands the
85
+ * caller the release.
86
+ */
87
+ /**
88
+ * The last `(cssWidth, cssHeight, dpr)` posted to the worker — at init
89
+ * or any resize — i.e. the dimensions the worker has already rendered
90
+ * (or has an in-flight render for). {@link resize} short-circuits when
91
+ * its measurement matches (±0.5px, the presize protocol's own
92
+ * threshold): reactive resizes fan in from several host paths per
93
+ * layout transition (the post-commit fan-out, activation nudges, the
94
+ * fresh-restore tail), and every worker resize is an unconditional
95
+ * full re-render.
96
+ */
97
+ private _lastPostedSize;
98
+ private _holdPresent;
99
+ private _stagedFrame;
66
100
  /**
67
101
  * Host-side sink for tooltip + cursor side-effects. The chart
68
102
  * inside the renderer calls into a `MessageHostSink` that posts
@@ -85,6 +119,7 @@ export declare class RendererTransport {
85
119
  client: Client;
86
120
  view: View;
87
121
  tableName?: string;
122
+ panel?: string;
88
123
  clientWasm: WebAssembly.Module;
89
124
  clientWorkerURL: URL;
90
125
  chartTag: string;
@@ -125,7 +160,8 @@ export declare class RendererTransport {
125
160
  * `loadAndRenderAck`. Per the worker's "resolve on stale"
126
161
  * contract, a mid-flight cancellation (a newer `loadAndRender`
127
162
  * superseding this one) still acks — the host's awaiter just
128
- * resolves quietly.
163
+ * resolves quietly. Calls issued after `destroy()` resolve
164
+ * immediately (see `_allocPending`).
129
165
  */
130
166
  loadAndRender(opts: {
131
167
  viewerConfig: {
@@ -138,7 +174,41 @@ export declare class RendererTransport {
138
174
  };
139
175
  }): Promise<void>;
140
176
  redraw(): void;
141
- resize(): void;
177
+ /**
178
+ * Silently clear the worker chart's selection state (pinned tooltip) —
179
+ * no selection events are emitted; the worker echoes `dismissTooltip`
180
+ * to drop the host-side pinned artifact. See `DeselectMsg`.
181
+ */
182
+ deselect(): void;
183
+ /**
184
+ * Resize the chart to the GL canvas's CURRENT CSS box and resolve
185
+ * once the resized frame has PRESENTED (the worker's `resizeAck`) —
186
+ * not at message-post, which would let callers proceed while the
187
+ * old-dimensions bitmap is still on screen. Reactive geometry
188
+ * changes only; anticipated ones go through {@link presize}.
189
+ *
190
+ * A measurement matching {@link _lastPostedSize} resolves immediately
191
+ * WITHOUT a worker round trip: the worker has already rendered (or is
192
+ * presenting) that exact frame, and re-posting would be a full
193
+ * re-render of an identical bitmap.
194
+ */
195
+ resize(): Promise<void>;
196
+ /**
197
+ * Staged-present variant of {@link resize}: render at the given
198
+ * TARGET canvas CSS box (no measurement — the caller supplies the
199
+ * box the pending layout commit will produce), HOLDING the
200
+ * resulting frame (and any others that arrive meanwhile) offscreen
201
+ * instead of blitting, so nothing on screen changes. Resolves,
202
+ * once the resized frame is staged, to a present closure: calling
203
+ * it blits the latest staged frame and exits the hold — the host
204
+ * runs it in the same task as its layout commit so geometry and
205
+ * pixels land in one paint. On rejection (teardown) the hold is
206
+ * released here, so every settle path either frees the display or
207
+ * hands the caller the release.
208
+ */
209
+ presize(cssWidth: number, cssHeight: number): Promise<(() => void) | void>;
210
+ private _postResize;
211
+ private _presentStaged;
142
212
  clear(): void;
143
213
  invalidateTheme(): void;
144
214
  saveZoom(): Promise<void>;
@@ -146,7 +216,8 @@ export declare class RendererTransport {
146
216
  * Allocate a pending request slot of the given `kind`. Returns the
147
217
  * id (encoded into the outgoing `ControlMsg`) and a promise that
148
218
  * resolves / rejects when the matching reply arrives or
149
- * `destroy()` drains the table.
219
+ * `destroy()` drains the table. Requests issued *after* `destroy()`
220
+ * settle immediately with the same per-kind semantics as the drain.
150
221
  */
151
222
  private _allocPending;
152
223
  restoreZoom(state: any): void;
@@ -34,11 +34,17 @@ export declare function createQuadCornerBuffer(gl: GL): WebGLBuffer;
34
34
  * paint zero data). No-op return `true` when `attr` is negative
35
35
  * (optimized-out attribute — drawing the rest is still valid).
36
36
  *
37
+ * `firstInstance` byte-offsets the attribute pointer so the draw's
38
+ * instance 0 reads element `firstInstance` of the buffer — the WebGL
39
+ * substitute for a `baseInstance` draw parameter. Faceted renderers
40
+ * use it to draw a contiguous per-facet sub-range of a shared
41
+ * instance buffer.
42
+ *
37
43
  * Render-path uses `peek`, never `getOrCreate`: the latter recreates
38
44
  * with zero-initialized contents when `_totalCapacity` has grown past
39
45
  * the current buffer, which would wipe the previous draw's vertex
40
46
  * data and leave `drawArraysInstanced` to render zeros. See
41
47
  * {@link BufferPool#peek} for the full rationale.
42
48
  */
43
- export declare function bindInstancedFloatAttr(glManager: WebGLContextManager, instancing: Instancing, attr: number, name: string, components: number): boolean;
49
+ export declare function bindInstancedFloatAttr(glManager: WebGLContextManager, instancing: Instancing, attr: number, name: string, components: number, firstInstance?: number): boolean;
44
50
  export {};
@@ -72,6 +72,18 @@ export declare class WorkerRenderer {
72
72
  */
73
73
  loadAndRender(msg: LoadAndRenderMsg): Promise<void>;
74
74
  redraw(): void;
75
+ /**
76
+ * `redraw`, acked to the host AFTER the frame's present completes —
77
+ * the reply backing the viewer's presize contract (`ResizeAckMsg`):
78
+ * `plugin.resize()` must not resolve until the resized frame is
79
+ * actually on screen. A resize landing mid-present defers its
80
+ * canvas mutation (`deferIfDraining`), but the redraw's own frame
81
+ * applies pending dimensions in `beginFrame`, so the present this
82
+ * awaits IS at the new size. Always acks — a failed present or a
83
+ * torn-down chart resolves the host's await rather than stranding
84
+ * it (completion, not success, is the contract).
85
+ */
86
+ redrawAck(msgId: number): void;
75
87
  resize(cssWidth: number, cssHeight: number, dpr: number): void;
76
88
  clear(): void;
77
89
  saveZoom(): any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@perspective-dev/viewer-charts",
3
- "version": "4.5.2",
3
+ "version": "5.0.0",
4
4
  "description": "Perspective.js WebGL Plugin",
5
5
  "unpkg": "./dist/cdn/perspective-viewer-charts.js",
6
6
  "jsdelivr": "./dist/cdn/perspective-viewer-charts.js",
@@ -224,7 +224,10 @@ export function renderCandlestickFrame(
224
224
  chart._lastCatTicks = numericCat
225
225
  ? computeNiceTicks(vis.xMin, vis.xMax, 6)
226
226
  : null;
227
- renderCandlestickChromeOverlay(chart);
227
+ // Deferred past the GPU fence (see `_defer2D`) so the chrome canvas
228
+ // doesn't present ahead of the GL glyphs on resize. Reads the
229
+ // `_last*` frame state set above.
230
+ chart._defer2D(() => renderCandlestickChromeOverlay(chart));
228
231
  }
229
232
 
230
233
  export function renderCandlestickChromeOverlay(chart: CandlestickChart): void {
@@ -92,15 +92,16 @@ export class CandlestickChart extends CategoricalYChart {
92
92
  _yDomain: { min: number; max: number } = { min: 0, max: 1 };
93
93
 
94
94
  /**
95
- * `domain_mode: "expand"` accumulators. Hold the running union of
96
- * the value-axis (and, in numeric-category mode, category-axis)
97
- * extent across data loads. Cleared in `resetExpandedDomain` —
98
- * wired from the worker's `resetAllZooms` and from view-config
99
- * mutations on `AbstractChart`. `null` whenever the option is
100
- * `"fit"` or the accumulator has just been cleared.
95
+ * `domain_mode: "expand"` accumulator — the VALUE (Y) axis only.
96
+ * The category axis has NO accumulator on band charts (see
97
+ * `SeriesChart._expandedLeftDomain`): a numeric/datetime `group_by`
98
+ * axis always fits the current data. Cleared in
99
+ * `resetExpandedDomain` — wired from the worker's `resetAllZooms`
100
+ * and from view-config mutations on `AbstractChart`. `null`
101
+ * whenever the option is `"fit"` or the accumulator has just been
102
+ * cleared.
101
103
  */
102
104
  _expandedYDomain: { min: number; max: number } | null = null;
103
- _expandedCategoryDomain: { min: number; max: number } | null = null;
104
105
 
105
106
  /**
106
107
  * Numeric category-axis state (single non-string group_by).
@@ -267,22 +268,17 @@ export class CandlestickChart extends CategoricalYChart {
267
268
  scratchCandles: this._candles,
268
269
  });
269
270
  // `domain_mode: "expand"` post-build union — mirrors the series
270
- // pipeline. `expandDomainInPlace` mutates `result.*` so the
271
- // assignments below pick up the grown extent automatically.
271
+ // pipeline: VALUE (Y) axis only, never the category axis (see
272
+ // `SeriesChart.uploadAndRender`). `expandDomainInPlace` mutates
273
+ // `result.yDomain` so the assignment below picks up the grown
274
+ // extent automatically.
272
275
  if (this._pluginConfig.domain_mode === "expand") {
273
276
  this._expandedYDomain = expandDomainInPlace(
274
277
  this._expandedYDomain,
275
278
  result.yDomain,
276
279
  );
277
- if (result.numericCategoryDomain) {
278
- this._expandedCategoryDomain = expandDomainInPlace(
279
- this._expandedCategoryDomain,
280
- result.numericCategoryDomain,
281
- );
282
- }
283
280
  } else {
284
281
  this._expandedYDomain = null;
285
- this._expandedCategoryDomain = null;
286
282
  }
287
283
 
288
284
  this._rowPaths = result.rowPaths;
@@ -320,7 +316,6 @@ export class CandlestickChart extends CategoricalYChart {
320
316
 
321
317
  override resetExpandedDomain(): void {
322
318
  this._expandedYDomain = null;
323
- this._expandedCategoryDomain = null;
324
319
  }
325
320
 
326
321
  protected destroyInternal(): void {
@@ -204,7 +204,11 @@ export function renderCartesianFrame(
204
204
  });
205
205
  }
206
206
 
207
- renderCartesianChromeOverlay(chart);
207
+ // Defer the chrome (axes/legend/tooltip) draw past the GPU fence so
208
+ // the chrome canvas doesn't present ahead of the GL glyphs on resize.
209
+ // `renderCartesianChromeOverlay` reads the `_last*` frame state set
210
+ // above, which stays stable until the next `_fullRender`.
211
+ chart._defer2D(() => renderCartesianChromeOverlay(chart));
208
212
  }
209
213
 
210
214
  interface RenderFrameCtx {
@@ -406,26 +410,25 @@ function renderSinglePlotFrame(
406
410
 
407
411
  const isMap = chart._renderMode === "map";
408
412
 
409
- if (chart._gridlineCanvas && !isMap) {
410
- // One-shot destructive prep (resizes + clears + scales to DPR).
411
- // `renderGridlines` itself is non-destructive.
412
- const dpr = glManager.dpr;
413
- initCanvas(chart._gridlineCanvas, layout, dpr);
414
- renderGridlines(
415
- chart._gridlineCanvas,
416
- layout,
417
- xTicks,
418
- yTicks,
419
- theme,
420
- dpr,
421
- );
422
- } else if (chart._gridlineCanvas && isMap) {
413
+ // Defer the gridline draw past the GPU fence (see `_defer2D`) so the
414
+ // gridline canvas doesn't present ahead of the GL glyphs on resize.
415
+ // The closure captures this frame's `layout` / ticks / `theme`.
416
+ const dpr = glManager.dpr;
417
+ const gridlineCanvas = chart._gridlineCanvas;
418
+ if (gridlineCanvas && !isMap) {
419
+ chart._defer2D(() => {
420
+ // One-shot destructive prep (resizes + clears + scales to
421
+ // DPR). `renderGridlines` itself is non-destructive.
422
+ initCanvas(gridlineCanvas, layout, dpr);
423
+ renderGridlines(gridlineCanvas, layout, xTicks, yTicks, theme, dpr);
424
+ });
425
+ } else if (gridlineCanvas && isMap) {
423
426
  // Map mode draws no cartesian gridlines, but the gridline
424
427
  // canvas may carry stale ink from a prior cartesian chart
425
428
  // type. Reset it to a clean transparent surface so the
426
429
  // basemap (rendered into the GL canvas below) reads as the
427
430
  // only background layer.
428
- initCanvas(chart._gridlineCanvas, layout, glManager.dpr);
431
+ chart._defer2D(() => initCanvas(gridlineCanvas, layout, dpr));
429
432
  }
430
433
 
431
434
  renderInPlotFrame(gl, layout, glManager.dpr, () => {
@@ -571,10 +574,17 @@ function renderFacetedFrame(
571
574
  // One-shot destructive prep for the gridline + WebGL canvases.
572
575
  // Both phases below are per-facet; calling their destructive
573
576
  // helpers (initCanvas / renderInPlotFrame) in the loop would wipe
574
- // every previously-drawn facet, leaving only the last cell
575
- // visible.
576
- if (chart._gridlineCanvas && sampleLayout) {
577
- initCanvas(chart._gridlineCanvas, sampleLayout, glManager.dpr);
577
+ // every previously-drawn facet, leaving only the last cell visible.
578
+ //
579
+ // The gridline draws are deferred past the GPU fence (see
580
+ // `_defer2D`) so the gridline canvas doesn't present ahead of the GL
581
+ // glyphs on resize. `initCanvas` is deferred first so it runs before
582
+ // the per-facet `renderGridlines` closures below (FIFO flush order);
583
+ // the GL prep (`clearAndSetupFrame`) and draws stay in this pass.
584
+ const dpr = glManager.dpr;
585
+ const gridlineCanvas = chart._gridlineCanvas;
586
+ if (gridlineCanvas && sampleLayout) {
587
+ chart._defer2D(() => initCanvas(gridlineCanvas, sampleLayout, dpr));
578
588
  }
579
589
 
580
590
  clearAndSetupFrame(gl);
@@ -609,49 +619,56 @@ function renderFacetedFrame(
609
619
  // basemap layer is rendered into the GL canvas inside the
610
620
  // facet's scissor below.
611
621
  const isMap = chart._renderMode === "map";
612
- if (chart._gridlineCanvas && !isMap) {
613
- const localXTicks = independent
614
- ? computeTicks(
615
- buildXDomain(
616
- chart,
617
- facetDomain.xMin,
618
- facetDomain.xMax,
619
- xIsDate,
620
- ),
621
- buildYDomain(
622
- chart,
623
- facetDomain.yMin,
624
- facetDomain.yMax,
625
- yIsDate,
626
- ),
627
- cell.layout,
628
- ).xTicks
629
- : xTicks;
630
- const localYTicks = independent
631
- ? computeTicks(
632
- buildXDomain(
633
- chart,
634
- facetDomain.xMin,
635
- facetDomain.xMax,
636
- xIsDate,
637
- ),
638
- buildYDomain(
639
- chart,
640
- facetDomain.yMin,
641
- facetDomain.yMax,
642
- yIsDate,
643
- ),
644
- cell.layout,
645
- ).yTicks
646
- : yTicks;
647
- renderGridlines(
648
- chart._gridlineCanvas,
649
- cell.layout,
650
- localXTicks,
651
- localYTicks,
652
- theme,
653
- glManager.dpr,
654
- );
622
+ if (gridlineCanvas && !isMap) {
623
+ // Deferred to the post-fence 2D flush. The closure captures
624
+ // this facet's `cell` (whose `cell.layout` already carries
625
+ // the padded domain from `buildProjectionMatrix` above) plus
626
+ // the frame-local domain/ticks, so it stays correct without
627
+ // recomputing at flush time.
628
+ chart._defer2D(() => {
629
+ const localXTicks = independent
630
+ ? computeTicks(
631
+ buildXDomain(
632
+ chart,
633
+ facetDomain.xMin,
634
+ facetDomain.xMax,
635
+ xIsDate,
636
+ ),
637
+ buildYDomain(
638
+ chart,
639
+ facetDomain.yMin,
640
+ facetDomain.yMax,
641
+ yIsDate,
642
+ ),
643
+ cell.layout,
644
+ ).xTicks
645
+ : xTicks;
646
+ const localYTicks = independent
647
+ ? computeTicks(
648
+ buildXDomain(
649
+ chart,
650
+ facetDomain.xMin,
651
+ facetDomain.xMax,
652
+ xIsDate,
653
+ ),
654
+ buildYDomain(
655
+ chart,
656
+ facetDomain.yMin,
657
+ facetDomain.yMax,
658
+ yIsDate,
659
+ ),
660
+ cell.layout,
661
+ ).yTicks
662
+ : yTicks;
663
+ renderGridlines(
664
+ gridlineCanvas,
665
+ cell.layout,
666
+ localXTicks,
667
+ localYTicks,
668
+ theme,
669
+ dpr,
670
+ );
671
+ });
655
672
  }
656
673
 
657
674
  withScissor(gl, cell.layout, glManager.dpr, () => {