@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
@@ -48,9 +48,12 @@ export interface ChartTypeConfig {
48
48
  * Per-chart-type overrides for `DEFAULT_PLUGIN_CONFIG`. Used when a
49
49
  * field's sensible default differs by chart family — currently
50
50
  * `include_zero` (true for Y Bar / Y Area / X Bar, false for line
51
- * / scatter / cartesian / financial). Applied at schema generation
52
- * and at `restore({})` so the effective default matches the
53
- * surfaced UI default.
51
+ * / scatter / cartesian / financial) and `facet_mode` ("overlay"
52
+ * for the band-pipeline families — series / financial — whose
53
+ * historical split_by rendering is the single stacked/colored
54
+ * plot; "grid" elsewhere). Applied at schema generation and at
55
+ * `restore({})` so the effective default matches the surfaced UI
56
+ * default.
54
57
  */
55
58
  plugin_field_defaults?: Partial<PluginConfig>;
56
59
  }
@@ -85,10 +88,19 @@ const SERIES_FIELDS: readonly PluginConfigField[] = [
85
88
  "bar_inner_pad",
86
89
  ];
87
90
 
91
+ // The band pipeline's historical split_by rendering is a single plot
92
+ // (splits stacked / colored in place), so the series family defaults
93
+ // `facet_mode` to "overlay" — grid faceting is the opt-in. Cartesian
94
+ // charts keep the global "grid" default from `DEFAULT_PLUGIN_CONFIG`.
95
+ const SERIES_DEFAULTS: Partial<PluginConfig> = { facet_mode: "overlay" };
96
+
88
97
  // Bar / area series glyphs grow from the zero baseline, so the value
89
98
  // axis must enclose 0 to render correctly. Line / scatter glyphs have
90
99
  // no such constraint — their default `include_zero` stays `false`.
91
- const ZERO_ANCHORED_DEFAULTS: Partial<PluginConfig> = { include_zero: true };
100
+ const ZERO_ANCHORED_DEFAULTS: Partial<PluginConfig> = {
101
+ ...SERIES_DEFAULTS,
102
+ include_zero: true,
103
+ };
92
104
 
93
105
  // Pure Cartesian (X/Y Scatter, X/Y Line) — no categorical axis, so no
94
106
  // band geometry; gets the facet-routing variant of zoom_mode.
@@ -210,9 +222,11 @@ const CHARTS: ChartTypeConfig[] = [
210
222
  }),
211
223
  make("Y Line", "y-line", SERIES, SELECT, 1, Y_AXIS, SERIES_FIELDS, {
212
224
  default_chart_type: "line",
225
+ plugin_field_defaults: SERIES_DEFAULTS,
213
226
  }),
214
227
  make("Y Scatter", "y-scatter", SERIES, SELECT, 1, Y_AXIS, SERIES_FIELDS, {
215
228
  default_chart_type: "scatter",
229
+ plugin_field_defaults: SERIES_DEFAULTS,
216
230
  }),
217
231
  make("Y Area", "y-area", SERIES, SELECT, 1, Y_AXIS, SERIES_FIELDS, {
218
232
  default_chart_type: "area",
@@ -250,9 +264,11 @@ const CHARTS: ChartTypeConfig[] = [
250
264
  make("Heatmap", "heatmap", HIER, SELECT, 1, ["Color"], HEATMAP_FIELDS),
251
265
  make("Candlestick", "candlestick", FIN, TOGGLE, 1, FIN_NAMES, FIN_FIELDS, {
252
266
  default_chart_type: "candlestick",
267
+ plugin_field_defaults: SERIES_DEFAULTS,
253
268
  }),
254
269
  make("OHLC", "ohlc", FIN, TOGGLE, 1, FIN_NAMES, FIN_FIELDS, {
255
270
  default_chart_type: "ohlc",
271
+ plugin_field_defaults: SERIES_DEFAULTS,
256
272
  }),
257
273
  make(
258
274
  "Map Scatter",
@@ -154,7 +154,6 @@ export class HTMLPerspectiveViewerWebGLPluginElement
154
154
  private _renderer: RendererTransport | null = null;
155
155
  private _rendererPromise: Promise<RendererTransport> | null = null;
156
156
  private _rawEventForwarder: RawEventForwarder | null = null;
157
- private _generation = 0;
158
157
  private _resetClickAbort: AbortController | null = null;
159
158
 
160
159
  /**
@@ -172,6 +171,16 @@ export class HTMLPerspectiveViewerWebGLPluginElement
172
171
  */
173
172
  private _pluginConfigStore: PluginConfig | null = null;
174
173
 
174
+ /**
175
+ * Per-column config (`alt_axis`, `chart_type`, formats, …), held on
176
+ * the element for the same reason as `_pluginConfigStore`: the host
177
+ * calls `restore()` BEFORE the first draw builds the renderer, so
178
+ * forwarding only to a live renderer silently drops the initial
179
+ * `columns_config` — `_buildRenderer` ships it in the `InitMsg`
180
+ * instead.
181
+ */
182
+ private _columnsConfig: Record<string, any> = {};
183
+
175
184
  private get _pluginConfig(): PluginConfig {
176
185
  if (!this._pluginConfigStore) {
177
186
  this._pluginConfigStore = this._effectiveDefaults();
@@ -285,8 +294,7 @@ export class HTMLPerspectiveViewerWebGLPluginElement
285
294
  // is the dispose signal: if `this._rendererPromise` no longer
286
295
  // points at *this* build when it resolves, the element was
287
296
  // deleted (or a reconnect started a newer build) and we destroy
288
- // the orphan instead of adopting it. Promise identity — not
289
- // `_generation`, which every `draw`/`update` bumps — is the
297
+ // the orphan instead of adopting it. Promise identity is the
290
298
  // right token: a rapid draw→update must NOT tear down the
291
299
  // single in-flight build it shares via this memoized promise.
292
300
  const p: Promise<RendererTransport> = this._buildRenderer(view).then(
@@ -353,11 +361,16 @@ export class HTMLPerspectiveViewerWebGLPluginElement
353
361
 
354
362
  private async _buildRenderer(view: View): Promise<RendererTransport> {
355
363
  const viewer = this.parentElement as HTMLPerspectiveViewerElement;
356
- const client = await viewer.getClient();
364
+ // This chart's own panel (its `slot`). Used to scope the client/table
365
+ // to THIS panel rather than the host's active/seed panel (the bare
366
+ // getClient()/getTable() resolve element-wide in a multi-panel viewer),
367
+ // and to tag the dispatched interaction events with their source panel.
368
+ const panel = this.getAttribute("slot") ?? undefined;
369
+ const client = await viewer.getClient({ panel });
357
370
  const viewer_class = customElements.get("perspective-viewer");
358
371
  const clientWasm = viewer_class.get_wasm_module();
359
372
  const clientWorkerURL = viewer_class.get_worker_url();
360
- const table = await viewer?.getTable?.();
373
+ const table = await viewer?.getTable?.({ panel });
361
374
  const tableName: string | undefined = table
362
375
  ? await table.get_name()
363
376
  : undefined;
@@ -370,6 +383,7 @@ export class HTMLPerspectiveViewerWebGLPluginElement
370
383
  client,
371
384
  view,
372
385
  tableName,
386
+ panel,
373
387
  clientWorkerURL,
374
388
  clientWasm,
375
389
  chartTag: this._chartType.tag,
@@ -392,6 +406,7 @@ export class HTMLPerspectiveViewerWebGLPluginElement
392
406
  zoom_mode: this._pluginConfig.facet_zoom_mode,
393
407
  },
394
408
  pluginConfig: this._pluginConfig,
409
+ columnsConfig: this._columnsConfig,
395
410
  defaultChartType: this._chartType.default_chart_type,
396
411
  renderBlitMode: BLIT_MODE,
397
412
  });
@@ -553,8 +568,16 @@ export class HTMLPerspectiveViewerWebGLPluginElement
553
568
  return this._drawImpl(view);
554
569
  }
555
570
 
571
+ /**
572
+ * Shared body of `draw` / `update`. No re-entrancy guard: the host
573
+ * serializes every rendering call (`draw`, `update`, `render`,
574
+ * `resize`) and `delete` on its per-renderer draw lock, so this
575
+ * method never overlaps itself or a teardown. The one exception —
576
+ * an external DOM disconnect `delete()`ing the element mid-draw —
577
+ * is absorbed by `RendererTransport`, whose post-`destroy()`
578
+ * requests settle immediately instead of pending forever.
579
+ */
556
580
  private async _drawImpl(view: View): Promise<void> {
557
- const gen = ++this._generation;
558
581
  let renderer: RendererTransport;
559
582
  try {
560
583
  renderer = await this._ensureRenderer(view);
@@ -564,19 +587,17 @@ export class HTMLPerspectiveViewerWebGLPluginElement
564
587
  return;
565
588
  }
566
589
 
567
- if (this._generation !== gen) {
568
- return;
569
- }
570
-
571
590
  renderer.setView(view);
572
591
  renderer.setBufferMaxCapacity(this._chartType.max_cells);
592
+ const panel = this.getAttribute("slot") ?? undefined;
573
593
  const viewer = this
574
594
  .parentElement as HTMLPerspectiveViewerElement | null;
575
- const viewerConfig = (await viewer?.getViewConfig?.()) ?? {};
576
- if (this._generation !== gen) {
595
+
596
+ if (viewer === null) {
577
597
  return;
578
598
  }
579
599
 
600
+ const viewerConfig = await viewer.getViewConfig({ panel });
580
601
  await renderer.loadAndRender({
581
602
  viewerConfig: {
582
603
  group_by: viewerConfig?.group_by ?? [],
@@ -588,17 +609,78 @@ export class HTMLPerspectiveViewerWebGLPluginElement
588
609
  }
589
610
 
590
611
  async clear(): Promise<void> {
591
- this._generation++;
592
612
  this._renderer?.clear();
593
613
  }
594
614
 
595
615
  async resize(): Promise<void> {
596
- this._renderer?.resize();
616
+ // Hidden (an unslotted tab-stack panel, a `display: none` host):
617
+ // the 0×0 rect a hidden element reports would resize the worker
618
+ // canvas to zero — CLEARING the retained frame, so the next
619
+ // activation repaints from blank (the two-stage tab-switch
620
+ // artifact). Skip instead and keep the last frame for an instant,
621
+ // single-blit reveal — mirroring the datagrid's guard. The host's
622
+ // activation nudge is also a `resize()`, so a hidden panel's nudge
623
+ // is free by the same check.
624
+ if (!this.isConnected || this.offsetParent == null) {
625
+ return;
626
+ }
627
+
628
+ // AWAITED to the resized frame's PRESENT (the worker's
629
+ // `resizeAck`) — the host's presize protocol style-overrides
630
+ // this element to its target box and holds the layout commit on
631
+ // this promise, so resolving at message-post would commit the
632
+ // settings-pane layout against the old-dimensions bitmap (the
633
+ // aspect-ratio warp the datagrid's synchronous resize never
634
+ // shows).
635
+ await this._renderer?.resize();
636
+ }
637
+
638
+ /**
639
+ * OPTIONAL host presize protocol: render at the TARGET element box
640
+ * `(width, height)` — the box the host's pending layout commit will
641
+ * produce — holding the resulting frame offscreen, so nothing on
642
+ * screen changes during the round-trip. Resolves, once the resized
643
+ * frame is staged, to a present closure; the host calls it in the
644
+ * same task as the layout commit, landing geometry and pixels in
645
+ * one paint. Plugins without this method get the host's held
646
+ * style-override presize path instead.
647
+ */
648
+ async presize(width: number, height: number): Promise<(() => void) | void> {
649
+ if (
650
+ !this.isConnected ||
651
+ this.offsetParent == null ||
652
+ !this._renderer ||
653
+ !this._glCanvas
654
+ ) {
655
+ return;
656
+ }
657
+
658
+ // Target GL-canvas box = its current box shifted by the element's
659
+ // box delta — the chrome between the element edge and the canvas
660
+ // is constant across a resize.
661
+ const hostRect = this.getBoundingClientRect();
662
+ const glRect = this._glCanvas.getBoundingClientRect();
663
+ return await this._renderer.presize(
664
+ Math.max(0, glRect.width + (width - hostRect.width)),
665
+ Math.max(0, glRect.height + (height - hostRect.height)),
666
+ );
597
667
  }
598
668
 
599
669
  restyle() {
600
670
  this._renderer?.invalidateTheme();
601
- return 5;
671
+ }
672
+
673
+ /**
674
+ * Clear any active selection state (pinned tooltip) WITHOUT emitting
675
+ * selection events — the host calls this when a global filter
676
+ * contributed by this panel's selection is removed from the global
677
+ * filter bar, so the pin can't outlive the filter it produced. The
678
+ * transport's `_lastInsertConfig` (remove-set memory) is deliberately
679
+ * RETAINED so a subsequent selection still replaces any leftover
680
+ * clauses rather than accumulating.
681
+ */
682
+ async deselect(): Promise<void> {
683
+ this._renderer?.deselect();
602
684
  }
603
685
 
604
686
  async render(view: View): Promise<Blob> {
@@ -622,12 +704,12 @@ export class HTMLPerspectiveViewerWebGLPluginElement
622
704
  ...config,
623
705
  };
624
706
 
707
+ this._columnsConfig = columns_config ?? {};
625
708
  this._renderer?.setPluginConfig(this._pluginConfig);
626
- this._renderer?.setColumnsConfig(columns_config ?? {});
709
+ this._renderer?.setColumnsConfig(this._columnsConfig);
627
710
  }
628
711
 
629
712
  delete() {
630
- this._generation++;
631
713
  if (this._rawEventForwarder) {
632
714
  this._rawEventForwarder.detach();
633
715
  this._rawEventForwarder = null;
@@ -16,8 +16,8 @@ import type { WebGLContextManager } from "../webgl/context-manager";
16
16
  * Module-level render scheduler. The single entry point for driving a
17
17
  * chart frame. Every render-triggering caller — upload chunks, zoom /
18
18
  * pan, resize, theme invalidation, host-driven redraws — calls
19
- * `requestRender(glManager, fullRender)` and awaits the returned
20
- * promise.
19
+ * `requestRender(glManager, fullRender, render2D)` and awaits the
20
+ * returned promise.
21
21
  *
22
22
  * ## Guarantees
23
23
  *
@@ -51,9 +51,12 @@ import type { WebGLContextManager } from "../webgl/context-manager";
51
51
  * begins, letting per-context GPU work overlap.
52
52
  *
53
53
  * - **Phase 2 (parallel):** `Promise.all(snapshot.map(present))`
54
- * where `present` does `await awaitGpuFence(); endFrame();
55
- * resolve waiters`. Each entry's waiters resolve as soon as its
56
- * own present completes — independent of other entries.
54
+ * where `present` does `await awaitGpuFence(); render2D();
55
+ * endFrame(); resolve waiters`. The `render2D()` step draws the 2D
56
+ * layers (gridlines + chrome) only after the GPU fence, so the 2D
57
+ * placeholder canvases don't present a frame ahead of the GL frame
58
+ * on resize. Each entry's waiters resolve as soon as its own
59
+ * present completes — independent of other entries.
57
60
  *
58
61
  * ## Failure modes
59
62
  *
@@ -78,6 +81,15 @@ import type { WebGLContextManager } from "../webgl/context-manager";
78
81
  interface Entry {
79
82
  glManager: WebGLContextManager;
80
83
  fullRender: () => void;
84
+ /**
85
+ * The chart's 2D-canvas draws (gridlines + chrome), split out of
86
+ * `fullRender` so they run *after* `awaitGpuFence` — see
87
+ * {@link present}. Deferring the 2D work past the GPU fence keeps
88
+ * the 2D placeholder canvases from pushing a resized frame to the
89
+ * compositor a frame ahead of the GL present (the GL/2D "distortion"
90
+ * on resize). Populated as captured closures during `fullRender`.
91
+ */
92
+ render2D: () => void;
81
93
  waiters: PromiseWithResolvers<void>[];
82
94
  }
83
95
 
@@ -105,25 +117,30 @@ const inFlight = new Set<WebGLContextManager>();
105
117
  const deferred = new Map<WebGLContextManager, (() => void)[]>();
106
118
 
107
119
  /**
108
- * Request a coalesced render of `glManager` whose body is
109
- * `fullRender`. Returns a promise that resolves when this entry's
110
- * Phase 2 (`awaitGpuFence` + `endFrame`) completes.
120
+ * Request a coalesced render of `glManager`. `fullRender` submits the
121
+ * GL commands (Phase 1, synchronous); `render2D` draws the 2D layers
122
+ * (gridlines + chrome) and runs in Phase 2 *after* `awaitGpuFence`, so
123
+ * the 2D canvases don't present ahead of the GL frame. Returns a
124
+ * promise that resolves when this entry's Phase 2 (`awaitGpuFence` +
125
+ * `render2D` + `endFrame`) completes.
111
126
  *
112
127
  * If a request is already pending for the same glManager, the new
113
- * call's `fullRender` closure replaces the prior one (latest call
114
- * wins; closures read chart state lazily so this is functionally a
115
- * no-op, but keeps the closure fresh) and the returned promise
116
- * resolves alongside the existing waiters.
128
+ * call's `fullRender` / `render2D` closures replace the prior ones
129
+ * (latest call wins; closures read chart state lazily so this is
130
+ * functionally a no-op, but keeps the closures fresh) and the returned
131
+ * promise resolves alongside the existing waiters.
117
132
  */
118
133
  export function requestRender(
119
134
  glManager: WebGLContextManager,
120
135
  fullRender: () => void,
136
+ render2D: () => void,
121
137
  ): Promise<void> {
122
138
  let entry = pending.get(glManager);
123
139
  if (entry) {
124
140
  entry.fullRender = fullRender;
141
+ entry.render2D = render2D;
125
142
  } else {
126
- entry = { glManager, fullRender, waiters: [] };
143
+ entry = { glManager, fullRender, render2D, waiters: [] };
127
144
  pending.set(glManager, entry);
128
145
  }
129
146
 
@@ -354,6 +371,15 @@ async function present(entry: Entry): Promise<void> {
354
371
 
355
372
  try {
356
373
  await entry.glManager.awaitGpuFence();
374
+ // Draw the 2D layers (gridlines + chrome) only now that the GL
375
+ // frame's GPU work has completed. Issuing the 2D-canvas commands
376
+ // here — rather than inline in `fullRender` (Phase 1) — keeps the
377
+ // 2D placeholder canvases from pushing their resized frame to the
378
+ // compositor during the fence-wait yields, a frame ahead of the
379
+ // GL present. That early push is the visible GL/2D misalignment
380
+ // on resize. Failures fall through to the same present-failed
381
+ // path as the fence.
382
+ entry.render2D();
357
383
  entry.glManager.endFrame();
358
384
  for (const w of entry.waiters) {
359
385
  w.resolve();
@@ -31,6 +31,7 @@ export type ControlMsg =
31
31
  | SetBufferMaxCapacityMsg
32
32
  | LoadAndRenderMsg
33
33
  | RedrawMsg
34
+ | DeselectMsg
34
35
  | ResizeMsg
35
36
  | ClearMsg
36
37
  | InvalidateThemeMsg
@@ -53,6 +54,7 @@ export type WorkerMsg =
53
54
  | UserClickMsg
54
55
  | UserSelectMsg
55
56
  | LoadAndRenderAckMsg
57
+ | ResizeAckMsg
56
58
  | FrameBitmapMsg
57
59
  | ErrorMsg;
58
60
 
@@ -160,6 +162,15 @@ export interface InitMsg {
160
162
  * a `setPluginConfig` control msg.
161
163
  */
162
164
  pluginConfig: PluginConfig;
165
+
166
+ /**
167
+ * Initial per-column config (`alt_axis`, `chart_type`, formats, …).
168
+ * Seeds the chart impl before the first `loadAndRender` — the host
169
+ * calls `plugin.restore` before the renderer exists, so without
170
+ * this the initial `columns_config` never reaches the worker.
171
+ * Later changes arrive as `setColumnsConfig` control msgs.
172
+ */
173
+ columnsConfig?: Record<string, any>;
163
174
  defaultChartType?: string;
164
175
 
165
176
  /**
@@ -300,13 +311,44 @@ export interface RedrawMsg {
300
311
  kind: "redraw";
301
312
  }
302
313
 
314
+ /**
315
+ * Host → worker: silently clear the chart's selection state (pinned
316
+ * tooltip) — no `userSelect` reply, so the host filter-bar removal that
317
+ * triggered it can't echo back into the host's filter set. The worker's
318
+ * `TooltipController.dismiss()` posts `dismissTooltip` to tear down the
319
+ * host-side pinned artifact.
320
+ */
321
+ export interface DeselectMsg {
322
+ kind: "deselect";
323
+ }
324
+
325
+ /**
326
+ * Host → worker: resize the chart to a new CSS box. Replied with
327
+ * {@link ResizeAckMsg} AFTER the resized frame PRESENTS — the host's
328
+ * `plugin.resize()` promise is the viewer's presize contract ("the
329
+ * plugin has repainted at the target box when this resolves"), so an
330
+ * ack at message-receipt would let the settings-pane layout commit
331
+ * against the old-dimensions bitmap (the aspect-ratio warp).
332
+ */
303
333
  export interface ResizeMsg {
304
334
  kind: "resize";
335
+ msgId: number;
305
336
  cssWidth: number;
306
337
  cssHeight: number;
307
338
  dpr: number;
308
339
  }
309
340
 
341
+ /**
342
+ * Worker → host reply to a `ResizeMsg`, posted after the resized
343
+ * frame's present completes. Always sent — present failures and
344
+ * torn-down charts included — so the host's awaited promise resolves
345
+ * deterministically.
346
+ */
347
+ export interface ResizeAckMsg {
348
+ kind: "resizeAck";
349
+ msgId: number;
350
+ }
351
+
310
352
  export interface ClearMsg {
311
353
  kind: "clear";
312
354
  }