@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
@@ -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
@@ -26,6 +25,15 @@ export declare class HTMLPerspectiveViewerWebGLPluginElement extends HTMLElement
26
25
  * resolved values in the `InitMsg`.
27
26
  */
28
27
  private _pluginConfigStore;
28
+ /**
29
+ * Per-column config (`alt_axis`, `chart_type`, formats, …), held on
30
+ * the element for the same reason as `_pluginConfigStore`: the host
31
+ * calls `restore()` BEFORE the first draw builds the renderer, so
32
+ * forwarding only to a live renderer silently drops the initial
33
+ * `columns_config` — `_buildRenderer` ships it in the `InitMsg`
34
+ * instead.
35
+ */
36
+ private _columnsConfig;
29
37
  private get _pluginConfig();
30
38
  private set _pluginConfig(value);
31
39
  private _effectiveDefaults;
@@ -97,10 +105,40 @@ export declare class HTMLPerspectiveViewerWebGLPluginElement extends HTMLElement
97
105
  };
98
106
  draw(view: View): Promise<void>;
99
107
  update(view: View): Promise<void>;
108
+ /**
109
+ * Shared body of `draw` / `update`. No re-entrancy guard: the host
110
+ * serializes every rendering call (`draw`, `update`, `render`,
111
+ * `resize`) and `delete` on its per-renderer draw lock, so this
112
+ * method never overlaps itself or a teardown. The one exception —
113
+ * an external DOM disconnect `delete()`ing the element mid-draw —
114
+ * is absorbed by `RendererTransport`, whose post-`destroy()`
115
+ * requests settle immediately instead of pending forever.
116
+ */
100
117
  private _drawImpl;
101
118
  clear(): Promise<void>;
102
119
  resize(): Promise<void>;
103
- restyle(): number;
120
+ /**
121
+ * OPTIONAL host presize protocol: render at the TARGET element box
122
+ * `(width, height)` — the box the host's pending layout commit will
123
+ * produce — holding the resulting frame offscreen, so nothing on
124
+ * screen changes during the round-trip. Resolves, once the resized
125
+ * frame is staged, to a present closure; the host calls it in the
126
+ * same task as the layout commit, landing geometry and pixels in
127
+ * one paint. Plugins without this method get the host's held
128
+ * style-override presize path instead.
129
+ */
130
+ presize(width: number, height: number): Promise<(() => void) | void>;
131
+ restyle(): void;
132
+ /**
133
+ * Clear any active selection state (pinned tooltip) WITHOUT emitting
134
+ * selection events — the host calls this when a global filter
135
+ * contributed by this panel's selection is removed from the global
136
+ * filter bar, so the pin can't outlive the filter it produced. The
137
+ * transport's `_lastInsertConfig` (remove-set memory) is deliberately
138
+ * RETAINED so a subsequent selection still replaces any leftover
139
+ * clauses rather than accumulating.
140
+ */
141
+ deselect(): Promise<void>;
104
142
  render(view: View): Promise<Blob>;
105
143
  restore(config: any, columns_config?: Record<string, any>): void;
106
144
  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
@@ -104,6 +104,14 @@ export interface InitMsg {
104
104
  * a `setPluginConfig` control msg.
105
105
  */
106
106
  pluginConfig: PluginConfig;
107
+ /**
108
+ * Initial per-column config (`alt_axis`, `chart_type`, formats, …).
109
+ * Seeds the chart impl before the first `loadAndRender` — the host
110
+ * calls `plugin.restore` before the renderer exists, so without
111
+ * this the initial `columns_config` never reaches the worker.
112
+ * Later changes arrive as `setColumnsConfig` control msgs.
113
+ */
114
+ columnsConfig?: Record<string, any>;
107
115
  defaultChartType?: string;
108
116
  /**
109
117
  * Pre-resolved CSS-variable theme snapshot from the host.
@@ -230,12 +238,41 @@ export interface LoadAndRenderAckMsg {
230
238
  export interface RedrawMsg {
231
239
  kind: "redraw";
232
240
  }
241
+ /**
242
+ * Host → worker: silently clear the chart's selection state (pinned
243
+ * tooltip) — no `userSelect` reply, so the host filter-bar removal that
244
+ * triggered it can't echo back into the host's filter set. The worker's
245
+ * `TooltipController.dismiss()` posts `dismissTooltip` to tear down the
246
+ * host-side pinned artifact.
247
+ */
248
+ export interface DeselectMsg {
249
+ kind: "deselect";
250
+ }
251
+ /**
252
+ * Host → worker: resize the chart to a new CSS box. Replied with
253
+ * {@link ResizeAckMsg} AFTER the resized frame PRESENTS — the host's
254
+ * `plugin.resize()` promise is the viewer's presize contract ("the
255
+ * plugin has repainted at the target box when this resolves"), so an
256
+ * ack at message-receipt would let the settings-pane layout commit
257
+ * against the old-dimensions bitmap (the aspect-ratio warp).
258
+ */
233
259
  export interface ResizeMsg {
234
260
  kind: "resize";
261
+ msgId: number;
235
262
  cssWidth: number;
236
263
  cssHeight: number;
237
264
  dpr: number;
238
265
  }
266
+ /**
267
+ * Worker → host reply to a `ResizeMsg`, posted after the resized
268
+ * frame's present completes. Always sent — present failures and
269
+ * torn-down charts included — so the host's awaited promise resolves
270
+ * deterministically.
271
+ */
272
+ export interface ResizeAckMsg {
273
+ kind: "resizeAck";
274
+ msgId: number;
275
+ }
239
276
  export interface ClearMsg {
240
277
  kind: "clear";
241
278
  }
@@ -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;
@@ -98,6 +133,7 @@ export declare class RendererTransport {
98
133
  chrome: HTMLCanvasElement;
99
134
  facetConfig: FacetConfig;
100
135
  pluginConfig: PluginConfig;
136
+ columnsConfig?: Record<string, any>;
101
137
  defaultChartType?: string;
102
138
  renderBlitMode: "blit" | "direct";
103
139
  }): Promise<void>;
@@ -125,7 +161,8 @@ export declare class RendererTransport {
125
161
  * `loadAndRenderAck`. Per the worker's "resolve on stale"
126
162
  * contract, a mid-flight cancellation (a newer `loadAndRender`
127
163
  * superseding this one) still acks — the host's awaiter just
128
- * resolves quietly.
164
+ * resolves quietly. Calls issued after `destroy()` resolve
165
+ * immediately (see `_allocPending`).
129
166
  */
130
167
  loadAndRender(opts: {
131
168
  viewerConfig: {
@@ -138,7 +175,41 @@ export declare class RendererTransport {
138
175
  };
139
176
  }): Promise<void>;
140
177
  redraw(): void;
141
- resize(): void;
178
+ /**
179
+ * Silently clear the worker chart's selection state (pinned tooltip) —
180
+ * no selection events are emitted; the worker echoes `dismissTooltip`
181
+ * to drop the host-side pinned artifact. See `DeselectMsg`.
182
+ */
183
+ deselect(): void;
184
+ /**
185
+ * Resize the chart to the GL canvas's CURRENT CSS box and resolve
186
+ * once the resized frame has PRESENTED (the worker's `resizeAck`) —
187
+ * not at message-post, which would let callers proceed while the
188
+ * old-dimensions bitmap is still on screen. Reactive geometry
189
+ * changes only; anticipated ones go through {@link presize}.
190
+ *
191
+ * A measurement matching {@link _lastPostedSize} resolves immediately
192
+ * WITHOUT a worker round trip: the worker has already rendered (or is
193
+ * presenting) that exact frame, and re-posting would be a full
194
+ * re-render of an identical bitmap.
195
+ */
196
+ resize(): Promise<void>;
197
+ /**
198
+ * Staged-present variant of {@link resize}: render at the given
199
+ * TARGET canvas CSS box (no measurement — the caller supplies the
200
+ * box the pending layout commit will produce), HOLDING the
201
+ * resulting frame (and any others that arrive meanwhile) offscreen
202
+ * instead of blitting, so nothing on screen changes. Resolves,
203
+ * once the resized frame is staged, to a present closure: calling
204
+ * it blits the latest staged frame and exits the hold — the host
205
+ * runs it in the same task as its layout commit so geometry and
206
+ * pixels land in one paint. On rejection (teardown) the hold is
207
+ * released here, so every settle path either frees the display or
208
+ * hands the caller the release.
209
+ */
210
+ presize(cssWidth: number, cssHeight: number): Promise<(() => void) | void>;
211
+ private _postResize;
212
+ private _presentStaged;
142
213
  clear(): void;
143
214
  invalidateTheme(): void;
144
215
  saveZoom(): Promise<void>;
@@ -146,7 +217,8 @@ export declare class RendererTransport {
146
217
  * Allocate a pending request slot of the given `kind`. Returns the
147
218
  * id (encoded into the outgoing `ControlMsg`) and a promise that
148
219
  * resolves / rejects when the matching reply arrives or
149
- * `destroy()` drains the table.
220
+ * `destroy()` drains the table. Requests issued *after* `destroy()`
221
+ * settle immediately with the same per-kind semantics as the drain.
150
222
  */
151
223
  private _allocPending;
152
224
  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.1.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, () => {