@perspective-dev/viewer-charts 4.5.0 → 4.5.2

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 (84) hide show
  1. package/LICENSE.md +193 -0
  2. package/dist/cdn/perspective-viewer-charts.js +2 -2
  3. package/dist/cdn/perspective-viewer-charts.js.map +3 -3
  4. package/dist/esm/axis/bar-axis.d.ts +9 -1
  5. package/dist/esm/axis/categorical-axis.d.ts +0 -2
  6. package/dist/esm/charts/cartesian/cartesian.d.ts +26 -0
  7. package/dist/esm/charts/common/category-axis-resolver.d.ts +43 -1
  8. package/dist/esm/charts/common/expand-domain.d.ts +20 -0
  9. package/dist/esm/charts/common/tree-chart.d.ts +7 -0
  10. package/dist/esm/charts/common/tree-chrome.d.ts +23 -1
  11. package/dist/esm/charts/common/tree-interact.d.ts +46 -0
  12. package/dist/esm/charts/series/glyphs/draw-lines.d.ts +11 -4
  13. package/dist/esm/charts/series/series-build.d.ts +38 -2
  14. package/dist/esm/charts/series/series-render.d.ts +1 -4
  15. package/dist/esm/charts/series/series-type.d.ts +19 -17
  16. package/dist/esm/charts/series/series.d.ts +16 -0
  17. package/dist/esm/charts/sunburst/sunburst-interact.d.ts +1 -1
  18. package/dist/esm/charts/treemap/treemap-interact.d.ts +1 -6
  19. package/dist/esm/config.d.ts +19 -0
  20. package/dist/esm/interaction/host-sink-message.d.ts +10 -28
  21. package/dist/esm/interaction/raw-event-forwarder.d.ts +6 -7
  22. package/dist/esm/interaction/zoom-controller.d.ts +31 -20
  23. package/dist/esm/interaction/zoom-router.d.ts +3 -26
  24. package/dist/esm/perspective-viewer-charts.js +2 -2
  25. package/dist/esm/perspective-viewer-charts.js.map +3 -3
  26. package/dist/esm/plugin/plugin.d.ts +15 -3
  27. package/dist/esm/render/scheduler.d.ts +23 -0
  28. package/dist/esm/theme/palette.d.ts +0 -5
  29. package/dist/esm/transport/protocol.d.ts +2 -7
  30. package/dist/esm/webgl/context-manager.d.ts +100 -117
  31. package/dist/esm/webgl/context-pool.d.ts +40 -0
  32. package/dist/esm/webgl/gl-context.d.ts +54 -0
  33. package/dist/esm/worker/renderer.worker.d.ts +2 -4
  34. package/package.json +45 -45
  35. package/src/ts/axis/bar-axis.ts +74 -45
  36. package/src/ts/axis/categorical-axis.ts +0 -2
  37. package/src/ts/charts/candlestick/candlestick-render.ts +10 -7
  38. package/src/ts/charts/candlestick/candlestick.ts +10 -29
  39. package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +36 -2
  40. package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +36 -2
  41. package/src/ts/charts/cartesian/cartesian-build.ts +143 -9
  42. package/src/ts/charts/cartesian/cartesian-render.ts +205 -30
  43. package/src/ts/charts/cartesian/cartesian.ts +43 -4
  44. package/src/ts/charts/cartesian/glyphs/density.ts +36 -41
  45. package/src/ts/charts/cartesian/glyphs/lines.ts +13 -15
  46. package/src/ts/charts/cartesian/glyphs/points.ts +12 -17
  47. package/src/ts/charts/chart-base.ts +20 -6
  48. package/src/ts/charts/chart.ts +1 -1
  49. package/src/ts/charts/common/category-axis-resolver.ts +135 -1
  50. package/src/ts/charts/common/expand-domain.ts +40 -0
  51. package/src/ts/charts/common/tree-chart.ts +16 -0
  52. package/src/ts/charts/common/tree-chrome.ts +86 -1
  53. package/src/ts/charts/common/tree-interact.ts +209 -0
  54. package/src/ts/charts/heatmap/heatmap-render.ts +9 -11
  55. package/src/ts/charts/series/glyphs/draw-areas.ts +30 -1
  56. package/src/ts/charts/series/glyphs/draw-lines.ts +151 -76
  57. package/src/ts/charts/series/series-build.ts +394 -21
  58. package/src/ts/charts/series/series-render.ts +159 -38
  59. package/src/ts/charts/series/series-type.ts +37 -17
  60. package/src/ts/charts/series/series.ts +63 -68
  61. package/src/ts/charts/sunburst/sunburst-interact.ts +18 -162
  62. package/src/ts/charts/sunburst/sunburst-render.ts +24 -89
  63. package/src/ts/charts/sunburst/sunburst.ts +1 -15
  64. package/src/ts/charts/treemap/treemap-interact.ts +22 -189
  65. package/src/ts/charts/treemap/treemap-render.ts +19 -46
  66. package/src/ts/charts/treemap/treemap.ts +1 -16
  67. package/src/ts/config.ts +21 -1
  68. package/src/ts/interaction/host-sink-message.ts +33 -22
  69. package/src/ts/interaction/raw-event-forwarder.ts +10 -12
  70. package/src/ts/interaction/zoom-controller.ts +120 -83
  71. package/src/ts/interaction/zoom-router.ts +3 -126
  72. package/src/ts/map/tile-layer.ts +13 -13
  73. package/src/ts/plugin/plugin.ts +176 -196
  74. package/src/ts/render/scheduler.ts +125 -42
  75. package/src/ts/shaders/line-uniform.frag.glsl +2 -1
  76. package/src/ts/shaders/line-uniform.vert.glsl +19 -0
  77. package/src/ts/theme/palette.ts +1 -4
  78. package/src/ts/transport/protocol.ts +3 -8
  79. package/src/ts/webgl/context-manager.ts +195 -189
  80. package/src/ts/webgl/context-pool.ts +78 -0
  81. package/src/ts/webgl/gl-context.ts +191 -0
  82. package/src/ts/worker/dispatch.ts +0 -1
  83. package/src/ts/worker/renderer.worker.ts +72 -65
  84. package/src/ts/worker/session-host.ts +29 -0
@@ -177,6 +177,41 @@ export function deferIfDraining(
177
177
  ops.push(op);
178
178
  }
179
179
 
180
+ /**
181
+ * Drop every scheduler reference to `glManager` — called from
182
+ * `WebGLContextManager`'s owning renderer at teardown, *before*
183
+ * `glManager.destroy()` loses the GL context.
184
+ *
185
+ * Without this, a frame queued for the next RAF (an `Entry` in
186
+ * `pending`) or a `deferIfDraining` op still parked in `deferred`
187
+ * would survive the destroy and, on the next `drain()`, drive
188
+ * `fullRender` / `present` against a context-lost manager. In blit
189
+ * mode that surfaces as `endFrame`'s `transferToImageBitmap` throwing
190
+ * "Cannot transfer to ImageBitmap because WebGL context is lost" —
191
+ * logged as "scheduler: present failed". In direct mode it's a wasted
192
+ * paint against a dead context.
193
+ *
194
+ * Outstanding waiters for a destroyed manager are resolved (not
195
+ * rejected): the caller asked to tear the chart down, so its awaited
196
+ * `draw()` should observe a clean no-op rather than an error it would
197
+ * have to suppress. An in-flight `present()` (manager in `inFlight`)
198
+ * is left to unwind on its own — its `finally` clause will not find a
199
+ * `deferred` entry to flush, and the manager is already gone from
200
+ * `pending`, so it cannot re-enqueue.
201
+ */
202
+ export function unregister(glManager: WebGLContextManager): void {
203
+ const entry = pending.get(glManager);
204
+ if (entry) {
205
+ pending.delete(glManager);
206
+ for (const w of entry.waiters) {
207
+ w.resolve();
208
+ }
209
+ }
210
+
211
+ deferred.delete(glManager);
212
+ inFlight.delete(glManager);
213
+ }
214
+
180
215
  /**
181
216
  * Test-only: clear pending state. Production callers must not use
182
217
  * this — outstanding waiters are silently dropped.
@@ -230,31 +265,39 @@ export function _resetForTest(): void {
230
265
  async function drain(): Promise<void> {
231
266
  const snapshot = Array.from(pending.values());
232
267
  pending.clear();
233
- const ready: Entry[] = [];
268
+
269
+ // Group ready entries by the GL context behind each manager. Under
270
+ // pooled blit mode many managers share one context (one drawing
271
+ // buffer): their renders MUST serialize — interleaving two charts'
272
+ // paints into one canvas would corrupt the bitmap the first ships.
273
+ // In the default 1:1 mode every manager has its own backend, so
274
+ // every group has exactly one entry and all groups run in parallel,
275
+ // exactly as before pooling.
276
+ const groups = new Map<number, Entry[]>();
234
277
  for (const entry of snapshot) {
235
- try {
236
- // Apply any dimension change recorded by
237
- // `glManager.requestResize` *before* the paint, in the
238
- // same un-yielded synchronous Phase 1 loop. This pairs
239
- // the canvas-clearing `canvas.width = N` assignment
240
- // with the immediately-following `_fullRender`, so the
241
- // browser's compositor only ever observes the canvas
242
- // post-paint. In direct/in-process modes the visible
243
- // canvas IS the GL canvas, and a clear-without-matching-
244
- // paint in the previous task would otherwise present an
245
- // empty frame to the user.
246
- entry.glManager.applyPendingResize();
247
- entry.fullRender();
248
- ready.push(entry);
249
- } catch (err) {
250
- console.error("scheduler: fullRender threw", err);
278
+ // The browser force-loses the oldest context when a page
279
+ // exceeds its per-agent WebGL context cap (~16). A frame queued
280
+ // before that eviction would paint + present against a dead
281
+ // context; skip it and settle its waiters cleanly rather than
282
+ // letting `endFrame`'s `transferToImageBitmap` throw.
283
+ if (entry.glManager.isContextLost()) {
251
284
  for (const w of entry.waiters) {
252
- w.reject(err);
285
+ w.resolve();
253
286
  }
287
+
288
+ continue;
289
+ }
290
+
291
+ const key = entry.glManager.backendId;
292
+ const group = groups.get(key);
293
+ if (group) {
294
+ group.push(entry);
295
+ } else {
296
+ groups.set(key, [entry]);
254
297
  }
255
298
  }
256
299
 
257
- await Promise.all(ready.map(present));
300
+ await Promise.all(Array.from(groups.values()).map(presentGroup));
258
301
 
259
302
  // Now (and only now) clear rafId. If new requests landed during
260
303
  // this drain, schedule the next RAF.
@@ -264,16 +307,51 @@ async function drain(): Promise<void> {
264
307
  }
265
308
  }
266
309
 
310
+ /**
311
+ * Render every entry sharing one GL context, sequentially: each chart
312
+ * gets the shared drawing buffer to itself for a full
313
+ * `beginFrame` → `fullRender` → `awaitGpuFence` → `endFrame` (present)
314
+ * cycle before the next chart touches it. Groups for *different*
315
+ * contexts run concurrently (via `Promise.all` in `drain`), so K pooled
316
+ * contexts give K-way parallelism and the 1:1 default keeps full
317
+ * cross-chart overlap.
318
+ *
319
+ * The synchronous prefix of each group (`beginFrame` + `fullRender`)
320
+ * runs before its first `awaitGpuFence` yields, so when groups run
321
+ * concurrently all first-chart draws are still submitted before any
322
+ * fence wait — preserving the GPU overlap the old two-phase drain had.
323
+ */
324
+ async function presentGroup(entries: Entry[]): Promise<void> {
325
+ for (const entry of entries) {
326
+ await present(entry);
327
+ }
328
+ }
329
+
267
330
  async function present(entry: Entry): Promise<void> {
268
- // Mark this glManager as in-flight *synchronously*, before the
269
- // first await. `Promise.all(ready.map(present))` calls each
270
- // `present` synchronously to collect its returned promise, so
271
- // every entry's glManager is registered in `inFlight` before
272
- // any fence-wait yields and before any sibling message handler
273
- // can run. Mutations posted by sibling handlers (resize, clear)
274
- // route through `deferIfDraining` and queue into `deferred`
275
- // until the `finally` block flushes them.
331
+ // Mark this glManager as in-flight *synchronously*, before the first
332
+ // await, so sibling message handlers' canvas mutations (resize,
333
+ // clear) route through `deferIfDraining` into `deferred` until the
334
+ // `finally` flushes them.
276
335
  inFlight.add(entry.glManager);
336
+ try {
337
+ // Apply the dimension change and (in pooled mode) reset shared
338
+ // GL state in the same un-yielded step as the paint that fills
339
+ // the buffer. Pairing the canvas-clearing `canvas.width = N`
340
+ // with `_fullRender` keeps the compositor from ever observing a
341
+ // cleared-but-unpainted canvas (visible flicker in direct/in-
342
+ // process modes, where the visible canvas IS the GL canvas).
343
+ entry.glManager.beginFrame();
344
+ entry.fullRender();
345
+ } catch (err) {
346
+ console.error("scheduler: fullRender threw", err);
347
+ for (const w of entry.waiters) {
348
+ w.reject(err);
349
+ }
350
+
351
+ flushDeferred(entry.glManager);
352
+ return;
353
+ }
354
+
277
355
  try {
278
356
  await entry.glManager.awaitGpuFence();
279
357
  entry.glManager.endFrame();
@@ -296,21 +374,26 @@ async function present(entry: Entry): Promise<void> {
296
374
  w.reject(err);
297
375
  }
298
376
  } finally {
299
- // Bitmap shipped (or error reported). Re-open the canvas to
300
- // mutations and flush any deferred ops in arrival order.
301
- // Deferred ops may call `requestRender`; the resulting
302
- // entry queues into `pending` and the drain's tail check
303
- // picks it up for the next RAF.
304
- inFlight.delete(entry.glManager);
305
- const ops = deferred.get(entry.glManager);
306
- if (ops) {
307
- deferred.delete(entry.glManager);
308
- for (const op of ops) {
309
- try {
310
- op();
311
- } catch (err) {
312
- console.error("scheduler: deferred op threw", err);
313
- }
377
+ flushDeferred(entry.glManager);
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Re-open a glManager's canvas to mutations and flush any ops deferred
383
+ * during its present, in arrival order. Deferred ops may call
384
+ * `requestRender`; the resulting entry queues into `pending` and the
385
+ * drain's tail check picks it up for the next RAF.
386
+ */
387
+ function flushDeferred(glManager: WebGLContextManager): void {
388
+ inFlight.delete(glManager);
389
+ const ops = deferred.get(glManager);
390
+ if (ops) {
391
+ deferred.delete(glManager);
392
+ for (const op of ops) {
393
+ try {
394
+ op();
395
+ } catch (err) {
396
+ console.error("scheduler: deferred op threw", err);
314
397
  }
315
398
  }
316
399
  }
@@ -16,11 +16,12 @@ uniform vec4 u_color;
16
16
  uniform float u_line_width;
17
17
 
18
18
  varying float v_edge_dist;
19
+ varying float v_seg_alpha;
19
20
 
20
21
  void main() {
21
22
  float dist = abs(v_edge_dist);
22
23
  float coreEdge = u_line_width / (u_line_width + 1.5);
23
24
  float alpha = 1.0 - smoothstep(coreEdge, 1.0, dist);
24
25
 
25
- gl_FragColor = vec4(u_color.rgb, u_color.a * alpha);
26
+ gl_FragColor = vec4(u_color.rgb, u_color.a * alpha * v_seg_alpha);
26
27
  }
@@ -22,11 +22,27 @@ attribute vec2 a_end;
22
22
  // 0 = start+left, 1 = start+right, 2 = end+left, 3 = end+right
23
23
  attribute float a_corner;
24
24
 
25
+ // Per-segment "is endpoint a real source-data cell" flags. Both vertices
26
+ // of a segment's quad see the same instance values, so `v_seg_alpha`
27
+ // below is constant across the quad — no gradient fade. Read from the
28
+ // per-cell real-flag buffer with offset 0 / 1 (same overlap trick as
29
+ // the bar-line glyph's segment-position attributes).
30
+ attribute float a_real_start;
31
+ attribute float a_real_end;
32
+
25
33
  uniform mat4 u_projection;
26
34
  uniform vec2 u_resolution;
27
35
  uniform float u_line_width;
28
36
 
37
+ // Alpha multiplier applied to any segment whose endpoints are not both
38
+ // real. Set per draw based on the series' interpolate mode:
39
+ // 0.0 = skip (gaps at synthesized cells)
40
+ // 1.0 = solid (no visible difference; default for non-line)
41
+ // 0.5 = transparent (50% opacity on segments touching synthesized cells)
42
+ uniform float u_interp_alpha;
43
+
29
44
  varying float v_edge_dist;
45
+ varying float v_seg_alpha;
30
46
 
31
47
  void main() {
32
48
  vec4 clipStart = u_projection * vec4(a_start, 0.0, 1.0);
@@ -51,4 +67,7 @@ void main() {
51
67
 
52
68
  gl_Position = clipPos + vec4(clipOffset, 0.0, 0.0);
53
69
  v_edge_dist = side;
70
+
71
+ float bothReal = a_real_start * a_real_end;
72
+ v_seg_alpha = mix(u_interp_alpha, 1.0, step(0.5, bothReal));
54
73
  }
@@ -18,10 +18,7 @@ export type Vec3 = [number, number, number];
18
18
  * Build a series palette of length `count` by sampling the theme gradient
19
19
  * at evenly-spaced offsets. For count == 1 returns the 50% stop.
20
20
  */
21
- export function interpolatePalette(
22
- stops: GradientStop[],
23
- count: number,
24
- ): Vec3[] {
21
+ function interpolatePalette(stops: GradientStop[], count: number): Vec3[] {
25
22
  if (count <= 0) {
26
23
  return [];
27
24
  }
@@ -12,8 +12,11 @@
12
12
 
13
13
  import type { FacetConfig, PluginConfig } from "../charts/chart";
14
14
  import type { PerspectiveClickDetail } from "../event-detail";
15
+ import type { ThemeSnapshot } from "../theme/theme";
15
16
  import type { ViewConfig } from "@perspective-dev/client";
16
17
 
18
+ export type { ThemeSnapshot };
19
+
17
20
  /**
18
21
  * Worker-mode control-channel messages. Distinct from the perspective
19
22
  * `ProxySession` channel that the worker's `Client` uses to talk to the
@@ -222,14 +225,6 @@ export interface FontFaceDescriptor {
222
225
  display?: string;
223
226
  }
224
227
 
225
- /**
226
- * Theme values resolved on the host via `getComputedStyle` and shipped
227
- * to the renderer scope, which has no DOM. Decoded by the chart via
228
- * `theme/theme.ts::resolveThemeFromVars`. Plain map for
229
- * structured-clone.
230
- */
231
- export type ThemeSnapshot = Record<string, string>;
232
-
233
228
  export interface SetViewByNameMsg {
234
229
  kind: "setViewByName";
235
230
  name: string;