@perspective-dev/viewer-charts 4.5.1 → 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 (70) 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/config.d.ts +19 -0
  15. package/dist/esm/data/view-reader.d.ts +10 -1
  16. package/dist/esm/event-detail.d.ts +7 -0
  17. package/dist/esm/layout/ticks.d.ts +5 -0
  18. package/dist/esm/perspective-viewer-charts.js +2 -2
  19. package/dist/esm/perspective-viewer-charts.js.map +3 -3
  20. package/dist/esm/plugin/charts.d.ts +6 -3
  21. package/dist/esm/plugin/plugin.d.ts +46 -4
  22. package/dist/esm/render/scheduler.d.ts +34 -8
  23. package/dist/esm/transport/protocol.d.ts +31 -2
  24. package/dist/esm/transport/renderer-transport.d.ts +74 -3
  25. package/dist/esm/webgl/context-manager.d.ts +100 -117
  26. package/dist/esm/webgl/context-pool.d.ts +40 -0
  27. package/dist/esm/webgl/gl-context.d.ts +54 -0
  28. package/dist/esm/webgl/instanced-attrs.d.ts +7 -1
  29. package/dist/esm/worker/renderer.worker.d.ts +12 -0
  30. package/package.json +1 -1
  31. package/src/ts/charts/candlestick/candlestick-render.ts +4 -1
  32. package/src/ts/charts/candlestick/candlestick.ts +12 -17
  33. package/src/ts/charts/cartesian/cartesian-render.ts +80 -63
  34. package/src/ts/charts/chart-base.ts +85 -5
  35. package/src/ts/charts/chart.ts +47 -9
  36. package/src/ts/charts/common/category-axis-resolver.ts +5 -1
  37. package/src/ts/charts/common/tree-data.ts +61 -16
  38. package/src/ts/charts/heatmap/heatmap-render.ts +6 -2
  39. package/src/ts/charts/series/glyphs/draw-areas.ts +10 -1
  40. package/src/ts/charts/series/glyphs/draw-bars.ts +30 -9
  41. package/src/ts/charts/series/glyphs/draw-lines.ts +10 -1
  42. package/src/ts/charts/series/glyphs/draw-scatter.ts +56 -3
  43. package/src/ts/charts/series/series-build.ts +21 -3
  44. package/src/ts/charts/series/series-interact.ts +86 -13
  45. package/src/ts/charts/series/series-render.ts +660 -17
  46. package/src/ts/charts/series/series.ts +96 -26
  47. package/src/ts/charts/sunburst/sunburst-interact.ts +2 -3
  48. package/src/ts/charts/sunburst/sunburst-render.ts +3 -1
  49. package/src/ts/charts/treemap/treemap-interact.ts +2 -5
  50. package/src/ts/charts/treemap/treemap-render.ts +3 -1
  51. package/src/ts/charts/treemap/treemap.ts +6 -1
  52. package/src/ts/config.ts +21 -1
  53. package/src/ts/data/view-reader.ts +22 -2
  54. package/src/ts/event-detail.ts +8 -0
  55. package/src/ts/index.ts +13 -7
  56. package/src/ts/interaction/raw-event-forwarder.ts +7 -0
  57. package/src/ts/interaction/zoom-controller.ts +8 -0
  58. package/src/ts/layout/ticks.ts +9 -0
  59. package/src/ts/plugin/charts.ts +20 -4
  60. package/src/ts/plugin/plugin.ts +157 -23
  61. package/src/ts/render/scheduler.ts +164 -55
  62. package/src/ts/transport/protocol.ts +33 -0
  63. package/src/ts/transport/renderer-transport.ts +185 -9
  64. package/src/ts/webgl/context-manager.ts +195 -189
  65. package/src/ts/webgl/context-pool.ts +78 -0
  66. package/src/ts/webgl/gl-context.ts +191 -0
  67. package/src/ts/webgl/instanced-attrs.ts +15 -1
  68. package/src/ts/worker/dispatch.ts +4 -1
  69. package/src/ts/worker/renderer.worker.ts +94 -22
  70. package/src/ts/worker/session-host.ts +29 -0
@@ -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;
@@ -301,6 +349,11 @@ export class RendererTransport {
301
349
  precompileShaders: this._precompileShaders,
302
350
  };
303
351
 
352
+ this._lastPostedSize = {
353
+ cssWidth: rect.width,
354
+ cssHeight: rect.height,
355
+ dpr,
356
+ };
304
357
  this._handle = await this._createHandle(workerURL, initMsg);
305
358
  this._handle.addMessageListener((msg) =>
306
359
  this._handleRendererMsg(msg as WorkerMsg),
@@ -438,7 +491,8 @@ export class RendererTransport {
438
491
  * `loadAndRenderAck`. Per the worker's "resolve on stale"
439
492
  * contract, a mid-flight cancellation (a newer `loadAndRender`
440
493
  * superseding this one) still acks — the host's awaiter just
441
- * resolves quietly.
494
+ * resolves quietly. Calls issued after `destroy()` resolve
495
+ * immediately (see `_allocPending`).
442
496
  */
443
497
  loadAndRender(opts: {
444
498
  viewerConfig: {
@@ -464,19 +518,96 @@ export class RendererTransport {
464
518
  this._post({ kind: "redraw" });
465
519
  }
466
520
 
467
- resize(): void {
521
+ /**
522
+ * Silently clear the worker chart's selection state (pinned tooltip) —
523
+ * no selection events are emitted; the worker echoes `dismissTooltip`
524
+ * to drop the host-side pinned artifact. See `DeselectMsg`.
525
+ */
526
+ deselect(): void {
527
+ this._post({ kind: "deselect" });
528
+ }
529
+
530
+ /**
531
+ * Resize the chart to the GL canvas's CURRENT CSS box and resolve
532
+ * once the resized frame has PRESENTED (the worker's `resizeAck`) —
533
+ * not at message-post, which would let callers proceed while the
534
+ * old-dimensions bitmap is still on screen. Reactive geometry
535
+ * changes only; anticipated ones go through {@link presize}.
536
+ *
537
+ * A measurement matching {@link _lastPostedSize} resolves immediately
538
+ * WITHOUT a worker round trip: the worker has already rendered (or is
539
+ * presenting) that exact frame, and re-posting would be a full
540
+ * re-render of an identical bitmap.
541
+ */
542
+ resize(): Promise<void> {
468
543
  if (!this._hostGlCanvas) {
469
- return;
544
+ return Promise.resolve();
470
545
  }
471
546
 
472
547
  const rect = this._hostGlCanvas.getBoundingClientRect();
473
548
  const dpr = window.devicePixelRatio || 1;
549
+ const last = this._lastPostedSize;
550
+ if (
551
+ last &&
552
+ Math.abs(last.cssWidth - rect.width) <= 0.5 &&
553
+ Math.abs(last.cssHeight - rect.height) <= 0.5 &&
554
+ last.dpr === dpr
555
+ ) {
556
+ return Promise.resolve();
557
+ }
558
+
559
+ return this._postResize(rect.width, rect.height);
560
+ }
561
+
562
+ /**
563
+ * Staged-present variant of {@link resize}: render at the given
564
+ * TARGET canvas CSS box (no measurement — the caller supplies the
565
+ * box the pending layout commit will produce), HOLDING the
566
+ * resulting frame (and any others that arrive meanwhile) offscreen
567
+ * instead of blitting, so nothing on screen changes. Resolves,
568
+ * once the resized frame is staged, to a present closure: calling
569
+ * it blits the latest staged frame and exits the hold — the host
570
+ * runs it in the same task as its layout commit so geometry and
571
+ * pixels land in one paint. On rejection (teardown) the hold is
572
+ * released here, so every settle path either frees the display or
573
+ * hands the caller the release.
574
+ */
575
+ presize(cssWidth: number, cssHeight: number): Promise<(() => void) | void> {
576
+ if (!this._hostGlCanvas) {
577
+ return Promise.resolve();
578
+ }
579
+
580
+ this._holdPresent = true;
581
+ return this._postResize(cssWidth, cssHeight).then(
582
+ () => () => this._presentStaged(),
583
+ (err) => {
584
+ this._presentStaged();
585
+ throw err;
586
+ },
587
+ );
588
+ }
589
+
590
+ private _postResize(cssWidth: number, cssHeight: number): Promise<void> {
591
+ const dpr = window.devicePixelRatio || 1;
592
+ const { id, promise } = this._allocPending<void>("resize");
593
+ this._lastPostedSize = { cssWidth, cssHeight, dpr };
474
594
  this._post({
475
595
  kind: "resize",
476
- cssWidth: rect.width,
477
- cssHeight: rect.height,
596
+ msgId: id,
597
+ cssWidth,
598
+ cssHeight,
478
599
  dpr,
479
600
  });
601
+ return promise;
602
+ }
603
+
604
+ private _presentStaged(): void {
605
+ this._holdPresent = false;
606
+ const staged = this._stagedFrame;
607
+ this._stagedFrame = null;
608
+ if (staged) {
609
+ this._drawFrameBitmap(staged);
610
+ }
480
611
  }
481
612
 
482
613
  clear() {
@@ -501,12 +632,39 @@ export class RendererTransport {
501
632
  * Allocate a pending request slot of the given `kind`. Returns the
502
633
  * id (encoded into the outgoing `ControlMsg`) and a promise that
503
634
  * resolves / rejects when the matching reply arrives or
504
- * `destroy()` drains the table.
635
+ * `destroy()` drains the table. Requests issued *after* `destroy()`
636
+ * settle immediately with the same per-kind semantics as the drain.
505
637
  */
506
638
  private _allocPending<T>(kind: PendingRenderType): {
507
639
  id: number;
508
640
  promise: Promise<T>;
509
641
  } {
642
+ // A request issued after `destroy()` would never settle: `_post`
643
+ // no-ops with no handle, so no reply ever lands — and the host
644
+ // awaits `loadAndRender` while holding its per-renderer draw
645
+ // lock, so a pending-forever promise wedges that panel's lock
646
+ // permanently. (Reachable when an external DOM disconnect
647
+ // `delete()`s the element mid-draw; the host's own teardown
648
+ // paths are serialized behind the same lock.) Mirror the
649
+ // `destroy()` drain: `loadAndRender` resolves silently,
650
+ // reply-bearing kinds reject. The pre-attached no-op `catch`
651
+ // marks the rejection handled for fire-and-forget callers
652
+ // (`saveZoom`) without affecting real awaiters.
653
+ if (this._destroyed) {
654
+ if (kind === "loadAndRender") {
655
+ return {
656
+ id: -1,
657
+ promise: Promise.resolve(undefined as unknown as T),
658
+ };
659
+ }
660
+
661
+ const promise = Promise.reject<T>(
662
+ new Error("RendererTransport destroyed"),
663
+ );
664
+ promise.catch(() => {});
665
+ return { id: -1, promise };
666
+ }
667
+
510
668
  const id = ++this._pendingCounter;
511
669
  const promise = new Promise<T>((resolve, reject) => {
512
670
  this._pending.set(id, { kind, resolve, reject });
@@ -548,6 +706,7 @@ export class RendererTransport {
548
706
  }
549
707
 
550
708
  destroy(): void {
709
+ this._destroyed = true;
551
710
  this._post({ kind: "destroy" });
552
711
  if (this._proxySession) {
553
712
  this._proxySession.close().catch(() => {});
@@ -572,6 +731,9 @@ export class RendererTransport {
572
731
  // them, and so the GPU-backed 2D context can release earlier.
573
732
  this._hostGlCanvas = null;
574
733
  this._displayCtx = null;
734
+ this._holdPresent = false;
735
+ this._stagedFrame?.close();
736
+ this._stagedFrame = null;
575
737
 
576
738
  // Drain pending request promises with kind-aware semantics:
577
739
  // - `loadAndRender` resolves silently (the host's awaited draw
@@ -633,7 +795,9 @@ export class RendererTransport {
633
795
  {
634
796
  bubbles: true,
635
797
  composed: true,
636
- detail: msg.detail,
798
+ // Tag with the source panel so a multi-panel host
799
+ // can attribute the click to the right panel.
800
+ detail: { ...msg.detail, panel: this._panel },
637
801
  },
638
802
  ),
639
803
  );
@@ -658,6 +822,9 @@ export class RendererTransport {
658
822
  removeConfigs as any,
659
823
  insertConfigs as any,
660
824
  );
825
+ // Tag with the source panel so the host's master/detail +
826
+ // global-filter routing can identify the originating panel.
827
+ detail.panel = this._panel;
661
828
  this._dispatchOnViewer(
662
829
  new CustomEvent<PerspectiveSelectDetail>(
663
830
  "perspective-global-filter",
@@ -672,7 +839,13 @@ export class RendererTransport {
672
839
  }
673
840
 
674
841
  case "frameBitmap":
675
- this._drawFrameBitmap(msg.bitmap);
842
+ if (this._holdPresent) {
843
+ this._stagedFrame?.close();
844
+ this._stagedFrame = msg.bitmap;
845
+ } else {
846
+ this._drawFrameBitmap(msg.bitmap);
847
+ }
848
+
676
849
  break;
677
850
  case "error":
678
851
  this._rejectReady(new Error(msg.message));
@@ -680,6 +853,9 @@ export class RendererTransport {
680
853
  case "loadAndRenderAck":
681
854
  this._resolvePending(msg.msgId, "loadAndRender", undefined);
682
855
  break;
856
+ case "resizeAck":
857
+ this._resolvePending(msg.msgId, "resize", undefined);
858
+ break;
683
859
  case "snapshotPngReply":
684
860
  this._resolvePending(msg.requestId, "snapshotPng", msg.blob);
685
861
  break;