@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
@@ -142,6 +142,23 @@ export abstract class AbstractChart implements ChartImplementation {
142
142
  _gridlineCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
143
143
  _chromeCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
144
144
 
145
+ /**
146
+ * 2D-canvas draw closures collected during `_fullRender` (the GL
147
+ * pass) and flushed by {@link _flush2D} *after* the scheduler's
148
+ * `awaitGpuFence`. Deferring the gridline/chrome draws past the GPU
149
+ * fence stops the 2D placeholder canvases from pushing a resized
150
+ * frame to the compositor a frame ahead of the GL present — the
151
+ * visible GL/2D misalignment on resize.
152
+ *
153
+ * Renderers wrap their 2D drawing in {@link _defer2D}; the closures
154
+ * capture the exact per-frame locals (layout, ticks, theme) so no
155
+ * state needs to be recomputed or replayed from cache at flush time.
156
+ * Reset at the start of every GL pass (see {@link requestRender} /
157
+ * {@link renderFrameSync}) so a `_fullRender` that throws mid-way
158
+ * can't leak stale closures into the next frame.
159
+ */
160
+ _deferred2D: Array<() => void> = [];
161
+
145
162
  /**
146
163
  * Host-supplied CSS-variable map. The host snapshots its DOM via
147
164
  * `snapshotThemeVars(el)` and ships it over the control channel;
@@ -630,6 +647,20 @@ export abstract class AbstractChart implements ChartImplementation {
630
647
  }
631
648
  }
632
649
 
650
+ /**
651
+ * Silently dismiss any pinned tooltip (the chart's selection visual)
652
+ * WITHOUT emitting `perspective-global-filter` — unlike the `setView`
653
+ * implicit dismiss above, the HOST initiated this (its global filter
654
+ * bar removed the clause this selection contributed), so an unselect
655
+ * emit would double-mutate the host's filter set.
656
+ * `TooltipController.dismiss()` posts the host-side pin teardown but
657
+ * never fires `onUnpin` (that emit belongs to `dispatchClick`'s
658
+ * click-to-unpin gesture), so this is emit-free by construction.
659
+ */
660
+ deselect(): void {
661
+ this._tooltip.dismiss();
662
+ }
663
+
633
664
  /**
634
665
  * Build the chart-specific {@link TooltipCallbacks} object — the
635
666
  * `onHover` / `onLeave` / `onClickPre` / `onPin` / `onDblClick`
@@ -810,17 +841,66 @@ export abstract class AbstractChart implements ChartImplementation {
810
841
  * scheduler so concurrent calls collapse to one `_fullRender` per
811
842
  * RAF and the host blitter receives one bitmap per frame. The
812
843
  * returned promise resolves after this chart's `awaitGpuFence` +
813
- * `endFrame` chain — independent of other charts in the same
814
- * RAF, which run their fence waits in parallel.
844
+ * `render2D` + `endFrame` chain — independent of other charts in the
845
+ * same RAF, which run their fence waits in parallel.
846
+ *
847
+ * `_fullRender` submits the GL commands and stashes the frame's 2D
848
+ * draws (gridlines + chrome) via {@link _defer2D}; the scheduler
849
+ * flushes them with {@link _flush2D} *after* the GPU fence, so the
850
+ * 2D canvases don't present ahead of the GL frame. The GL pass
851
+ * resets `_deferred2D` first so a mid-pass throw can't leak stale
852
+ * closures into the next frame.
815
853
  *
816
854
  * Every render-triggering caller — upload chunks, zoom / pan,
817
855
  * resize, theme invalidation, host-driven redraws — calls this.
818
- * The only sanctioned bypass is `snapshotPng`, which calls
819
- * `_fullRender` directly to keep the GL backbuffer intact for
856
+ * The only sanctioned bypass is `snapshotPng`, which uses
857
+ * {@link renderFrameSync} to keep the GL backbuffer intact for
820
858
  * `gl.readPixels`.
821
859
  */
822
860
  requestRender(glManager: WebGLContextManager): Promise<void> {
823
- return scheduleRender(glManager, () => this._fullRender(glManager));
861
+ return scheduleRender(
862
+ glManager,
863
+ () => {
864
+ this._deferred2D = [];
865
+ this._fullRender(glManager);
866
+ },
867
+ () => this._flush2D(),
868
+ );
869
+ }
870
+
871
+ /**
872
+ * Queue a 2D-canvas draw (gridline or chrome) to run after the
873
+ * GPU fence. Called from the chart renderers in place of drawing to
874
+ * the gridline/chrome canvases inline during the GL pass. See
875
+ * {@link _deferred2D}.
876
+ */
877
+ _defer2D(fn: () => void): void {
878
+ this._deferred2D.push(fn);
879
+ }
880
+
881
+ /**
882
+ * Run and clear the queued 2D-canvas draws. Invoked by the scheduler
883
+ * after `awaitGpuFence` (Phase 2), and by {@link renderFrameSync}.
884
+ */
885
+ _flush2D(): void {
886
+ const draws = this._deferred2D;
887
+ this._deferred2D = [];
888
+ for (const draw of draws) {
889
+ draw();
890
+ }
891
+ }
892
+
893
+ /**
894
+ * Synchronous full frame (GL + 2D) for the `snapshotPng` bypass,
895
+ * which sits outside the scheduler and needs the gridline/chrome
896
+ * canvases painted before it composites them. Resets the deferred
897
+ * queue, runs the GL pass, then flushes the 2D draws immediately
898
+ * (no fence split — a snapshot isn't presented to the compositor).
899
+ */
900
+ renderFrameSync(glManager: WebGLContextManager): void {
901
+ this._deferred2D = [];
902
+ this._fullRender(glManager);
903
+ this._flush2D();
824
904
  }
825
905
 
826
906
  // Lifecycle
@@ -44,13 +44,23 @@ export interface ChartImplementation {
44
44
  requestRender(glManager: WebGLContextManager): Promise<void>;
45
45
 
46
46
  /**
47
- * The chart-specific frame builder. The scheduler wraps this with
48
- * fence + `endFrame`; callers must not invoke it directly except
49
- * for `snapshotPng`, which needs an intact GL backbuffer for
50
- * `gl.readPixels` and so must skip the `endFrame` pair.
47
+ * The chart-specific GL frame builder. Submits the GL draw commands
48
+ * and stashes the frame's 2D-canvas draws (gridlines + chrome) for
49
+ * the scheduler to flush after `awaitGpuFence`. The scheduler wraps
50
+ * this with fence + 2D flush + `endFrame`; callers must not invoke
51
+ * it directly — `snapshotPng` uses {@link renderFrameSync} instead.
51
52
  */
52
53
  _fullRender(glManager: WebGLContextManager): void;
53
54
 
55
+ /**
56
+ * Synchronous full frame (GL + 2D) for the `snapshotPng` bypass,
57
+ * which sits outside the scheduler and needs the gridline/chrome
58
+ * canvases painted before compositing. Runs the GL pass then flushes
59
+ * the deferred 2D draws immediately, skipping the fence split and
60
+ * `endFrame` so the GL backbuffer stays intact for `gl.readPixels`.
61
+ */
62
+ renderFrameSync(glManager: WebGLContextManager): void;
63
+
54
64
  /**
55
65
  * Hand the current View to the chart so it can make on-demand
56
66
  * per-row queries (for lazy tooltip column lookups). Called on
@@ -174,6 +184,15 @@ export interface ChartImplementation {
174
184
  */
175
185
  resetExpandedDomain?(): void;
176
186
 
187
+ /**
188
+ * Silently clear any active selection state (pinned tooltip) WITHOUT
189
+ * emitting selection events. Driven from the host's `deselect`
190
+ * message — its global filter bar removed a clause this chart's
191
+ * selection contributed, so an unselect emit would double-mutate the
192
+ * host's filter set.
193
+ */
194
+ deselect?(): void;
195
+
177
196
  destroy(): void;
178
197
  }
179
198
 
@@ -246,8 +265,19 @@ export interface PluginConfig {
246
265
  * Faceting strategy when `split_by` is non-empty.
247
266
  *
248
267
  * - `"grid"` — one small-multiple sub-plot per split group.
249
- * - `"overlay"` — single plot with split groups differentiated by
250
- * color. Synced into `_facetConfig.facet_mode`.
268
+ * - `"overlay"` — a single plot: cartesian charts differentiate
269
+ * split groups by color; the categorical band pipeline (series
270
+ * charts) stacks splits within each aggregate's band slot.
271
+ * Synced into `_facetConfig.facet_mode`.
272
+ *
273
+ * The default differs by family via
274
+ * `ChartTypeConfig.plugin_field_defaults`: cartesian / density /
275
+ * map chart types default to `"grid"`, the series / financial
276
+ * band-pipeline types to `"overlay"` (their historical split
277
+ * rendering). Series charts REBUILD on a mode change — grid mode
278
+ * keys the stack ladder per split (`facetSplits` in
279
+ * `buildSeriesPipeline`) so each facet grows from its own
280
+ * baseline.
251
281
  */
252
282
  facet_mode: "grid" | "overlay";
253
283
 
@@ -283,15 +313,23 @@ export interface PluginConfig {
283
313
  /**
284
314
  * Domain accumulation policy across successive `View` updates.
285
315
  *
286
- * - `"fit"` — every update recomputes the rendered domain (and on
287
- * cartesian charts, the X/Y range and color/size scales) from
316
+ * - `"fit"` — every update recomputes the affected domains from
288
317
  * the current data extent. Can grow or shrink frame-to-frame.
289
- * - `"expand"` — the rendered domain monotonically *grows*: each
318
+ * - `"expand"` — the affected domains monotonically *grow*: each
290
319
  * update unions the new data extent with the previously rendered
291
320
  * extent, so once a value is in scope it stays in scope. Reset
292
321
  * by the "Reset Zoom" button, view-config changes (group_by /
293
322
  * split_by / column-slot / column-type), or toggling back to
294
323
  * `"fit"`.
324
+ *
325
+ * AXIS SCOPE differs by family: cartesian charts (X/Y Scatter,
326
+ * X/Y Line, Density, Maps) apply it to BOTH axes plus the
327
+ * color/size scales (categorical string axes opt out — slot
328
+ * indices are frame-local); the categorical band pipeline (series
329
+ * / financial) applies it to the VALUE axis only — Y for the
330
+ * Y-family, X for X Bar — while the category axis always fits, so
331
+ * a streaming numeric/datetime `group_by` axis releases departed
332
+ * categories instead of pinning to its history.
295
333
  */
296
334
  domain_mode: "fit" | "expand";
297
335
 
@@ -157,7 +157,11 @@ function formatLevelValue(
157
157
  valid: boolean,
158
158
  levelType: string,
159
159
  ): string {
160
- if (!valid) {
160
+ // `value` can be `undefined` even when `valid` claims otherwise — an
161
+ // out-of-bounds read when a row window outran the delivered Arrow
162
+ // (belt to `loadAndRender`'s delivered-rows clamp) — treat it as
163
+ // null rather than crash the label formatters.
164
+ if (!valid || value == null || Number.isNaN(value)) {
161
165
  return "";
162
166
  }
163
167
 
@@ -156,6 +156,33 @@ function insertRow(
156
156
  }
157
157
  }
158
158
 
159
+ /**
160
+ * Arrow validity-bitmap test. Columns without a bitmap are all-valid.
161
+ */
162
+ function isValidCell(col: ColumnData, rowIdx: number): boolean {
163
+ const v = col.valid;
164
+ return !v || !!((v[rowIdx >> 3] >> (rowIdx & 7)) & 1);
165
+ }
166
+
167
+ /**
168
+ * Read a size cell, honoring the validity bitmap: the bytes under an
169
+ * invalid slot are UNDEFINED (DuckDB's Arrow leaves NaN where the
170
+ * engine zero-fills), and a NaN here would poison every ancestor sum
171
+ * in `finalizeTree`. Invalid → 0 (no contribution); no size column at
172
+ * all → 1 (count semantics).
173
+ */
174
+ function readSize(col: ColumnData | null | undefined, rowIdx: number): number {
175
+ if (!col?.values) {
176
+ return 1;
177
+ }
178
+
179
+ if (!isValidCell(col, rowIdx)) {
180
+ return 0;
181
+ }
182
+
183
+ return col.values[rowIdx] as number;
184
+ }
185
+
159
186
  function readColor(
160
187
  chart: TreeChartBase,
161
188
  colorCol: ColumnData | null | undefined,
@@ -163,7 +190,7 @@ function readColor(
163
190
  ): { colorValue: number; colorLabel: string } {
164
191
  let colorValue = NaN;
165
192
  let colorLabel = "";
166
- if (!colorCol) {
193
+ if (!colorCol || !isValidCell(colorCol, rowIdx)) {
167
194
  return { colorValue, colorLabel };
168
195
  }
169
196
 
@@ -261,7 +288,11 @@ export function processTreeChunk(
261
288
  chart: TreeChartBase,
262
289
  columns: ColumnDataMap,
263
290
  ): void {
264
- const rpCols: { indices: Int32Array; dictionary: string[] }[] = [];
291
+ const rpCols: {
292
+ indices: Int32Array;
293
+ dictionary: string[];
294
+ valid?: Uint8Array;
295
+ }[] = [];
265
296
  for (let n = 0; ; n++) {
266
297
  const rp = columns.get(`__ROW_PATH_${n}__`);
267
298
  if (!rp) {
@@ -269,7 +300,11 @@ export function processTreeChunk(
269
300
  }
270
301
 
271
302
  if (rp.type === "string" && rp.indices && rp.dictionary) {
272
- rpCols.push({ indices: rp.indices, dictionary: rp.dictionary });
303
+ rpCols.push({
304
+ indices: rp.indices,
305
+ dictionary: rp.dictionary,
306
+ valid: rp.valid,
307
+ });
273
308
  } else if (rp.values) {
274
309
  // Non-string group_by (integer / float / date / datetime /
275
310
  // boolean) — synthesize a string-indexed dictionary so the
@@ -397,9 +432,7 @@ export function processTreeChunk(
397
432
  // Pass the signed value through; `insertRow` stores
398
433
  // `|size|` and `sizeSign` separately so the
399
434
  // render pass can dim negative leaves.
400
- const sizeValue = src.sizeCol?.values
401
- ? (src.sizeCol.values[i] as number)
402
- : 1;
435
+ const sizeValue = readSize(src.sizeCol, i);
403
436
  const { colorValue, colorLabel } = readColor(
404
437
  chart,
405
438
  src.colorCol,
@@ -416,9 +449,7 @@ export function processTreeChunk(
416
449
  );
417
450
  }
418
451
  } else {
419
- const sizeValue = sizeCol?.values
420
- ? (sizeCol.values[i] as number)
421
- : 1;
452
+ const sizeValue = readSize(sizeCol, i);
422
453
  const { colorValue, colorLabel } = readColor(
423
454
  chart,
424
455
  colorCol,
@@ -449,6 +480,19 @@ export function processTreeChunk(
449
480
  let pathLen = 0;
450
481
  for (let d = 0; d < rpCols.length; d++) {
451
482
  const rp = rpCols[d];
483
+
484
+ // In `"rollup"` mode the view emits total / subtotal rows
485
+ // whose deeper row-path levels are INVALID — index slot 0,
486
+ // which decodes to a REAL dictionary value (the engine's
487
+ // string row-path dictionaries carry no `""` sentinel), so
488
+ // the validity bitmap is the only truthful signal. Truncate
489
+ // the path at the first invalid level; the `!label` check
490
+ // below still covers synthesized non-string levels, whose
491
+ // dictionary index 0 IS the `""` sentinel.
492
+ if (rp.valid && !((rp.valid[i >> 3] >> (i & 7)) & 1)) {
493
+ break;
494
+ }
495
+
452
496
  const label = rp.dictionary[rp.indices[i]];
453
497
  if (!label && label !== "0") {
454
498
  break;
@@ -465,9 +509,7 @@ export function processTreeChunk(
465
509
  for (const src of splitSources!) {
466
510
  pathScratch[0] = src.prefix;
467
511
  const rowPath = pathScratch.slice(0, pathLen + 1);
468
- const sizeValue = src.sizeCol?.values
469
- ? (src.sizeCol.values[i] as number)
470
- : 1;
512
+ const sizeValue = readSize(src.sizeCol, i);
471
513
  const { colorValue, colorLabel } = readColor(
472
514
  chart,
473
515
  src.colorCol,
@@ -485,9 +527,7 @@ export function processTreeChunk(
485
527
  }
486
528
  } else {
487
529
  const rowPath = pathScratch.slice(0, pathLen);
488
- const sizeValue = sizeCol?.values
489
- ? (sizeCol.values[i] as number)
490
- : 1;
530
+ const sizeValue = readSize(sizeCol, i);
491
531
  const { colorValue, colorLabel } = readColor(chart, colorCol, i);
492
532
  insertRow(
493
533
  chart,
@@ -556,7 +596,12 @@ export function finalizeTree(chart: TreeChartBase): void {
556
596
  }
557
597
  } else {
558
598
  if (firstChild[id] === NULL_NODE) {
559
- value[id] = Math.max(0, size[id]);
599
+ // NOT `Math.max(0, x)`: that maps NaN → NaN, and one NaN
600
+ // leaf would poison every ancestor sum (dropping whole
601
+ // subtrees in `squarify`'s `value > 0` filter). The
602
+ // comparison form maps NaN → 0.
603
+ const s = size[id];
604
+ value[id] = s > 0 ? s : 0;
560
605
  } else {
561
606
  let sum = 0;
562
607
  for (
@@ -225,7 +225,9 @@ export function renderHeatmapFrame(
225
225
  drawCellsInstanced(chart, gl, glManager, 0, chart._uploadedCells);
226
226
  });
227
227
 
228
- renderHeatmapChromeOverlay(chart);
228
+ // Deferred past the GPU fence (see `_defer2D`) so the chrome canvas
229
+ // doesn't present ahead of the GL cells on resize.
230
+ chart._defer2D(() => renderHeatmapChromeOverlay(chart));
229
231
  }
230
232
 
231
233
  function ensureProgram(
@@ -650,7 +652,9 @@ function renderFacetedHeatmap(
650
652
  });
651
653
  }
652
654
 
653
- renderHeatmapChromeOverlay(chart);
655
+ // Deferred past the GPU fence (see `_defer2D`) so the chrome canvas
656
+ // doesn't present ahead of the GL cells on resize.
657
+ chart._defer2D(() => renderHeatmapChromeOverlay(chart));
654
658
  }
655
659
 
656
660
  /**
@@ -187,7 +187,10 @@ export class AreaGlyph {
187
187
 
188
188
  /**
189
189
  * Bind persistent strip buffers and dispatch one TRIANGLE_STRIP per
190
- * series-run. Skips hidden series.
190
+ * series-run. Skips hidden series. `splitFilter` (faceted frames)
191
+ * draws only the series whose `splitIdx` matches; `aggRange` (mixed
192
+ * glyph-run frames) only those whose `aggIdx` lies in the inclusive
193
+ * run span.
191
194
  */
192
195
  draw(
193
196
  chart: SeriesChart,
@@ -196,6 +199,8 @@ export class AreaGlyph {
196
199
  projLeft: Float32Array,
197
200
  projRight: Float32Array,
198
201
  opacity: number,
202
+ splitFilter?: number,
203
+ aggRange?: { start: number; end: number },
199
204
  ): void {
200
205
  const buf = this._buffers;
201
206
  const cache = this._program;
@@ -212,6 +217,21 @@ export class AreaGlyph {
212
217
  continue;
213
218
  }
214
219
 
220
+ if (
221
+ splitFilter !== undefined &&
222
+ chart._series[s.seriesId].splitIdx !== splitFilter
223
+ ) {
224
+ continue;
225
+ }
226
+
227
+ const aggIdx = chart._series[s.seriesId].aggIdx;
228
+ if (
229
+ aggRange !== undefined &&
230
+ (aggIdx < aggRange.start || aggIdx > aggRange.end)
231
+ ) {
232
+ continue;
233
+ }
234
+
215
235
  gl.uniformMatrix4fv(
216
236
  cache.u_projection,
217
237
  false,
@@ -30,13 +30,20 @@ type GL = WebGL2RenderingContext | WebGLRenderingContext;
30
30
  /**
31
31
  * Draw the bar-typed subset of `chart._bars` as instanced quads. Assumes
32
32
  * the caller has already `useProgram`'d the bar shader and set uniforms.
33
+ *
34
+ * `range` draws a contiguous instance sub-range instead of the full
35
+ * uploaded set — the faceted renderer uploads instances split-major
36
+ * (see `uploadBarInstances`) and dispatches one range per facet.
33
37
  */
34
38
  export function drawBars(
35
39
  chart: SeriesChart,
36
40
  gl: GL,
37
41
  glManager: WebGLContextManager,
42
+ range?: { start: number; count: number },
38
43
  ): void {
39
- if (chart._uploadedBars === 0) {
44
+ const first = range?.start ?? 0;
45
+ const count = range?.count ?? chart._uploadedBars;
46
+ if (chart._uploadedBars === 0 || count === 0) {
40
47
  return;
41
48
  }
42
49
 
@@ -62,6 +69,7 @@ export function drawBars(
62
69
  loc.a_x_center,
63
70
  "bar_x",
64
71
  1,
72
+ first,
65
73
  ) &&
66
74
  bindInstancedFloatAttr(
67
75
  glManager,
@@ -69,15 +77,31 @@ export function drawBars(
69
77
  loc.a_half_width,
70
78
  "bar_hw",
71
79
  1,
80
+ first,
81
+ ) &&
82
+ bindInstancedFloatAttr(
83
+ glManager,
84
+ instancing,
85
+ loc.a_y0,
86
+ "bar_y0",
87
+ 1,
88
+ first,
89
+ ) &&
90
+ bindInstancedFloatAttr(
91
+ glManager,
92
+ instancing,
93
+ loc.a_y1,
94
+ "bar_y1",
95
+ 1,
96
+ first,
72
97
  ) &&
73
- bindInstancedFloatAttr(glManager, instancing, loc.a_y0, "bar_y0", 1) &&
74
- bindInstancedFloatAttr(glManager, instancing, loc.a_y1, "bar_y1", 1) &&
75
98
  bindInstancedFloatAttr(
76
99
  glManager,
77
100
  instancing,
78
101
  loc.a_color,
79
102
  "bar_color",
80
103
  3,
104
+ first,
81
105
  ) &&
82
106
  bindInstancedFloatAttr(
83
107
  glManager,
@@ -85,6 +109,7 @@ export function drawBars(
85
109
  loc.a_series_id,
86
110
  "bar_sid",
87
111
  1,
112
+ first,
88
113
  ) &&
89
114
  bindInstancedFloatAttr(
90
115
  glManager,
@@ -92,15 +117,11 @@ export function drawBars(
92
117
  loc.a_axis,
93
118
  "bar_axis",
94
119
  1,
120
+ first,
95
121
  );
96
122
 
97
123
  if (ok) {
98
- instancing.drawArraysInstanced(
99
- gl.TRIANGLE_STRIP,
100
- 0,
101
- 4,
102
- chart._uploadedBars,
103
- );
124
+ instancing.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, count);
104
125
  }
105
126
 
106
127
  setDivisor(loc.a_x_center, 0);
@@ -271,7 +271,10 @@ export class LineGlyph {
271
271
  * Bind the persistent vertex buffers and dispatch one instanced draw
272
272
  * per series. Skips hidden series via `_hiddenSeries`. Gap /
273
273
  * transparency rendering is governed by `u_interp_alpha`, set per
274
- * series.
274
+ * series. `splitFilter` (faceted frames) draws only the series
275
+ * whose `splitIdx` matches — one call per facet. `aggRange` (mixed
276
+ * glyph-run frames) draws only the series whose `aggIdx` lies in
277
+ * the inclusive run span.
275
278
  */
276
279
  draw(
277
280
  chart: SeriesChart,
@@ -279,6 +282,8 @@ export class LineGlyph {
279
282
  glManager: WebGLContextManager,
280
283
  projLeft: Float32Array,
281
284
  projRight: Float32Array,
285
+ splitFilter?: number,
286
+ aggRange?: { start: number; end: number },
282
287
  ): void {
283
288
  const buf = this._buffers;
284
289
  const cache = this._program;
@@ -310,6 +315,21 @@ export class LineGlyph {
310
315
  continue;
311
316
  }
312
317
 
318
+ if (
319
+ splitFilter !== undefined &&
320
+ chart._series[s.seriesId].splitIdx !== splitFilter
321
+ ) {
322
+ continue;
323
+ }
324
+
325
+ const aggIdx = chart._series[s.seriesId].aggIdx;
326
+ if (
327
+ aggRange !== undefined &&
328
+ (aggIdx < aggRange.start || aggIdx > aggRange.end)
329
+ ) {
330
+ continue;
331
+ }
332
+
313
333
  gl.uniformMatrix4fv(
314
334
  cache.u_projection,
315
335
  false,