@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.
- package/dist/cdn/perspective-viewer-charts.js +2 -2
- package/dist/cdn/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/charts/candlestick/candlestick.d.ts +8 -10
- package/dist/esm/charts/chart-base.d.ts +58 -4
- package/dist/esm/charts/chart.d.ts +45 -9
- package/dist/esm/charts/series/glyphs/draw-areas.d.ts +3 -2
- package/dist/esm/charts/series/glyphs/draw-bars.d.ts +8 -1
- package/dist/esm/charts/series/glyphs/draw-lines.d.ts +3 -2
- package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +5 -2
- package/dist/esm/charts/series/series-build.d.ts +11 -0
- package/dist/esm/charts/series/series-interact.d.ts +10 -0
- package/dist/esm/charts/series/series-render.d.ts +17 -1
- package/dist/esm/charts/series/series.d.ts +53 -10
- package/dist/esm/config.d.ts +19 -0
- package/dist/esm/data/view-reader.d.ts +10 -1
- package/dist/esm/event-detail.d.ts +7 -0
- package/dist/esm/layout/ticks.d.ts +5 -0
- package/dist/esm/perspective-viewer-charts.js +2 -2
- package/dist/esm/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/plugin/charts.d.ts +6 -3
- package/dist/esm/plugin/plugin.d.ts +46 -4
- package/dist/esm/render/scheduler.d.ts +34 -8
- package/dist/esm/transport/protocol.d.ts +31 -2
- package/dist/esm/transport/renderer-transport.d.ts +74 -3
- package/dist/esm/webgl/context-manager.d.ts +100 -117
- package/dist/esm/webgl/context-pool.d.ts +40 -0
- package/dist/esm/webgl/gl-context.d.ts +54 -0
- package/dist/esm/webgl/instanced-attrs.d.ts +7 -1
- package/dist/esm/worker/renderer.worker.d.ts +12 -0
- package/package.json +1 -1
- package/src/ts/charts/candlestick/candlestick-render.ts +4 -1
- package/src/ts/charts/candlestick/candlestick.ts +12 -17
- package/src/ts/charts/cartesian/cartesian-render.ts +80 -63
- package/src/ts/charts/chart-base.ts +85 -5
- package/src/ts/charts/chart.ts +47 -9
- package/src/ts/charts/common/category-axis-resolver.ts +5 -1
- package/src/ts/charts/common/tree-data.ts +61 -16
- package/src/ts/charts/heatmap/heatmap-render.ts +6 -2
- package/src/ts/charts/series/glyphs/draw-areas.ts +10 -1
- package/src/ts/charts/series/glyphs/draw-bars.ts +30 -9
- package/src/ts/charts/series/glyphs/draw-lines.ts +10 -1
- package/src/ts/charts/series/glyphs/draw-scatter.ts +56 -3
- package/src/ts/charts/series/series-build.ts +21 -3
- package/src/ts/charts/series/series-interact.ts +86 -13
- package/src/ts/charts/series/series-render.ts +660 -17
- package/src/ts/charts/series/series.ts +96 -26
- package/src/ts/charts/sunburst/sunburst-interact.ts +2 -3
- package/src/ts/charts/sunburst/sunburst-render.ts +3 -1
- package/src/ts/charts/treemap/treemap-interact.ts +2 -5
- package/src/ts/charts/treemap/treemap-render.ts +3 -1
- package/src/ts/charts/treemap/treemap.ts +6 -1
- package/src/ts/config.ts +21 -1
- package/src/ts/data/view-reader.ts +22 -2
- package/src/ts/event-detail.ts +8 -0
- package/src/ts/index.ts +13 -7
- package/src/ts/interaction/raw-event-forwarder.ts +7 -0
- package/src/ts/interaction/zoom-controller.ts +8 -0
- package/src/ts/layout/ticks.ts +9 -0
- package/src/ts/plugin/charts.ts +20 -4
- package/src/ts/plugin/plugin.ts +157 -23
- package/src/ts/render/scheduler.ts +164 -55
- package/src/ts/transport/protocol.ts +33 -0
- package/src/ts/transport/renderer-transport.ts +185 -9
- package/src/ts/webgl/context-manager.ts +195 -189
- package/src/ts/webgl/context-pool.ts +78 -0
- package/src/ts/webgl/gl-context.ts +191 -0
- package/src/ts/webgl/instanced-attrs.ts +15 -1
- package/src/ts/worker/dispatch.ts +4 -1
- package/src/ts/worker/renderer.worker.ts +94 -22
- package/src/ts/worker/session-host.ts +29 -0
|
@@ -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: {
|
|
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({
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,8 @@ 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.
|
|
191
192
|
*/
|
|
192
193
|
draw(
|
|
193
194
|
chart: SeriesChart,
|
|
@@ -196,6 +197,7 @@ export class AreaGlyph {
|
|
|
196
197
|
projLeft: Float32Array,
|
|
197
198
|
projRight: Float32Array,
|
|
198
199
|
opacity: number,
|
|
200
|
+
splitFilter?: number,
|
|
199
201
|
): void {
|
|
200
202
|
const buf = this._buffers;
|
|
201
203
|
const cache = this._program;
|
|
@@ -212,6 +214,13 @@ export class AreaGlyph {
|
|
|
212
214
|
continue;
|
|
213
215
|
}
|
|
214
216
|
|
|
217
|
+
if (
|
|
218
|
+
splitFilter !== undefined &&
|
|
219
|
+
chart._series[s.seriesId].splitIdx !== splitFilter
|
|
220
|
+
) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
215
224
|
gl.uniformMatrix4fv(
|
|
216
225
|
cache.u_projection,
|
|
217
226
|
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
|
-
|
|
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,8 @@ 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.
|
|
275
276
|
*/
|
|
276
277
|
draw(
|
|
277
278
|
chart: SeriesChart,
|
|
@@ -279,6 +280,7 @@ export class LineGlyph {
|
|
|
279
280
|
glManager: WebGLContextManager,
|
|
280
281
|
projLeft: Float32Array,
|
|
281
282
|
projRight: Float32Array,
|
|
283
|
+
splitFilter?: number,
|
|
282
284
|
): void {
|
|
283
285
|
const buf = this._buffers;
|
|
284
286
|
const cache = this._program;
|
|
@@ -310,6 +312,13 @@ export class LineGlyph {
|
|
|
310
312
|
continue;
|
|
311
313
|
}
|
|
312
314
|
|
|
315
|
+
if (
|
|
316
|
+
splitFilter !== undefined &&
|
|
317
|
+
chart._series[s.seriesId].splitIdx !== splitFilter
|
|
318
|
+
) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
313
322
|
gl.uniformMatrix4fv(
|
|
314
323
|
cache.u_projection,
|
|
315
324
|
false,
|
|
@@ -34,10 +34,22 @@ interface ScatterProgramCache {
|
|
|
34
34
|
* Persistent scatter glyph state — left/right axis position + color
|
|
35
35
|
* buffers built once at data load. Pan/zoom redraws rebind without
|
|
36
36
|
* uploading.
|
|
37
|
+
*
|
|
38
|
+
* `seriesRanges` records each visible series' contiguous run within
|
|
39
|
+
* its axis bucket (`first` is bucket-relative). The fill loop appends
|
|
40
|
+
* series in order, so runs are contiguous by construction; the
|
|
41
|
+
* faceted draw path uses them to dispatch per-facet sub-ranges via
|
|
42
|
+
* `drawArrays(POINTS, first, count)`.
|
|
37
43
|
*/
|
|
38
44
|
interface ScatterBuffers {
|
|
39
45
|
leftCount: number;
|
|
40
46
|
rightCount: number;
|
|
47
|
+
seriesRanges: {
|
|
48
|
+
seriesId: number;
|
|
49
|
+
axis: 0 | 1;
|
|
50
|
+
first: number;
|
|
51
|
+
count: number;
|
|
52
|
+
}[];
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
/**
|
|
@@ -170,6 +182,9 @@ export class ScatterGlyph {
|
|
|
170
182
|
|
|
171
183
|
// Fill left bucket from `[0, leftCount)`, right bucket from
|
|
172
184
|
// `[leftCount, total)` — single typed-array allocation each.
|
|
185
|
+
// Series runs are contiguous within their bucket; record each
|
|
186
|
+
// run's bucket-relative `[first, count)` for the faceted draw.
|
|
187
|
+
const seriesRanges: ScatterBuffers["seriesRanges"] = [];
|
|
173
188
|
let leftWrite = 0;
|
|
174
189
|
let rightWrite = leftCount;
|
|
175
190
|
for (const s of scatterSeries) {
|
|
@@ -177,6 +192,7 @@ export class ScatterGlyph {
|
|
|
177
192
|
continue;
|
|
178
193
|
}
|
|
179
194
|
|
|
195
|
+
const runFirst = s.axis === 1 ? rightWrite - leftCount : leftWrite;
|
|
180
196
|
const r = s.color[0];
|
|
181
197
|
const g = s.color[1];
|
|
182
198
|
const b = s.color[2];
|
|
@@ -204,6 +220,16 @@ export class ScatterGlyph {
|
|
|
204
220
|
leftWrite++;
|
|
205
221
|
}
|
|
206
222
|
}
|
|
223
|
+
|
|
224
|
+
const runEnd = s.axis === 1 ? rightWrite - leftCount : leftWrite;
|
|
225
|
+
if (runEnd > runFirst) {
|
|
226
|
+
seriesRanges.push({
|
|
227
|
+
seriesId: s.seriesId,
|
|
228
|
+
axis: s.axis,
|
|
229
|
+
first: runFirst,
|
|
230
|
+
count: runEnd - runFirst,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
207
233
|
}
|
|
208
234
|
|
|
209
235
|
if (leftCount > 0) {
|
|
@@ -236,12 +262,15 @@ export class ScatterGlyph {
|
|
|
236
262
|
);
|
|
237
263
|
}
|
|
238
264
|
|
|
239
|
-
this._buffers = { leftCount, rightCount };
|
|
265
|
+
this._buffers = { leftCount, rightCount, seriesRanges };
|
|
240
266
|
}
|
|
241
267
|
|
|
242
268
|
/**
|
|
243
269
|
* Bind the persistent left/right buffers and issue up to two draw
|
|
244
|
-
* calls. No per-frame allocations or buffer uploads.
|
|
270
|
+
* calls. No per-frame allocations or buffer uploads. `splitFilter`
|
|
271
|
+
* (faceted frames) instead draws each matching series' contiguous
|
|
272
|
+
* bucket sub-range — one `drawArrays(POINTS, first, count)` per
|
|
273
|
+
* series of the facet.
|
|
245
274
|
*/
|
|
246
275
|
draw(
|
|
247
276
|
chart: SeriesChart,
|
|
@@ -249,6 +278,7 @@ export class ScatterGlyph {
|
|
|
249
278
|
glManager: WebGLContextManager,
|
|
250
279
|
projLeft: Float32Array,
|
|
251
280
|
projRight: Float32Array,
|
|
281
|
+
splitFilter?: number,
|
|
252
282
|
): void {
|
|
253
283
|
const buf = this._buffers;
|
|
254
284
|
const cache = this._program;
|
|
@@ -267,6 +297,28 @@ export class ScatterGlyph {
|
|
|
267
297
|
chart._pluginConfig.point_size_px * dpr,
|
|
268
298
|
);
|
|
269
299
|
|
|
300
|
+
if (splitFilter !== undefined) {
|
|
301
|
+
for (const r of buf.seriesRanges) {
|
|
302
|
+
if (chart._series[r.seriesId].splitIdx !== splitFilter) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
drawBucket(
|
|
307
|
+
gl,
|
|
308
|
+
cache,
|
|
309
|
+
r.axis === 1 ? cache.posRightBuffer : cache.posLeftBuffer,
|
|
310
|
+
r.axis === 1
|
|
311
|
+
? cache.colorRightBuffer
|
|
312
|
+
: cache.colorLeftBuffer,
|
|
313
|
+
r.count,
|
|
314
|
+
r.axis === 1 ? projRight : projLeft,
|
|
315
|
+
r.first,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
270
322
|
drawBucket(
|
|
271
323
|
gl,
|
|
272
324
|
cache,
|
|
@@ -309,6 +361,7 @@ function drawBucket(
|
|
|
309
361
|
colBuf: WebGLBuffer,
|
|
310
362
|
count: number,
|
|
311
363
|
proj: Float32Array,
|
|
364
|
+
first = 0,
|
|
312
365
|
): void {
|
|
313
366
|
if (count === 0) {
|
|
314
367
|
return;
|
|
@@ -324,5 +377,5 @@ function drawBucket(
|
|
|
324
377
|
gl.enableVertexAttribArray(cache.a_color);
|
|
325
378
|
gl.vertexAttribPointer(cache.a_color, 3, gl.FLOAT, false, 0, 0);
|
|
326
379
|
|
|
327
|
-
gl.drawArrays(gl.POINTS,
|
|
380
|
+
gl.drawArrays(gl.POINTS, first, count);
|
|
328
381
|
}
|
|
@@ -285,6 +285,18 @@ export interface SeriesPipelineInput {
|
|
|
285
285
|
*/
|
|
286
286
|
includeZero: boolean;
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* `facet_mode: "grid"` geometry: key the stack ladder by
|
|
290
|
+
* `(catIdx, aggIdx, splitIdx)` instead of `(catIdx, aggIdx)`, so
|
|
291
|
+
* splits never stack on each other — each facet's bars / areas
|
|
292
|
+
* grow from their own zero baseline. Slot geometry (`xCenter` /
|
|
293
|
+
* `halfWidth`) is unchanged: facets separate splits by projection
|
|
294
|
+
* + scissor, not by X offset, so every split's records share the
|
|
295
|
+
* same band-slot coordinates. Value extents still accumulate
|
|
296
|
+
* across ALL splits — the facet grid renders a shared value axis.
|
|
297
|
+
*/
|
|
298
|
+
facetSplits?: boolean;
|
|
299
|
+
|
|
288
300
|
/**
|
|
289
301
|
* Reusable scratch — pipeline writes records into these in place
|
|
290
302
|
* and zero-fills the stack ladder. Pass the previous build's
|
|
@@ -404,6 +416,7 @@ export function buildSeriesPipeline(
|
|
|
404
416
|
bandInnerFrac,
|
|
405
417
|
barInnerPad,
|
|
406
418
|
includeZero,
|
|
419
|
+
facetSplits,
|
|
407
420
|
scratchBars,
|
|
408
421
|
scratchPosStack,
|
|
409
422
|
scratchNegStack,
|
|
@@ -529,9 +542,12 @@ export function buildSeriesPipeline(
|
|
|
529
542
|
const N = numCategories;
|
|
530
543
|
const S = series.length;
|
|
531
544
|
|
|
532
|
-
// Stacking ladder, keyed by (catIdx, aggIdx)
|
|
545
|
+
// Stacking ladder, keyed by (catIdx, aggIdx) — or per-split
|
|
546
|
+
// (catIdx, aggIdx, splitIdx) in facet-grid mode, where each split
|
|
547
|
+
// renders in its own facet and cross-split stacking would lift
|
|
548
|
+
// every facet after the first off its baseline. Reuse chart-owned
|
|
533
549
|
// scratch when sized; else allocate. Active prefix is zero-filled.
|
|
534
|
-
const stackLen = N * M;
|
|
550
|
+
const stackLen = facetSplits ? N * M * P : N * M;
|
|
535
551
|
const posStack = ensureFloat64Scratch(scratchPosStack ?? null, stackLen);
|
|
536
552
|
const negStack = ensureFloat64Scratch(scratchNegStack ?? null, stackLen);
|
|
537
553
|
posStack.fill(0, 0, stackLen);
|
|
@@ -922,7 +938,9 @@ export function buildSeriesPipeline(
|
|
|
922
938
|
}
|
|
923
939
|
}
|
|
924
940
|
|
|
925
|
-
const stackIdx =
|
|
941
|
+
const stackIdx = facetSplits
|
|
942
|
+
? (catI * M + k) * P + p
|
|
943
|
+
: catI * M + k;
|
|
926
944
|
let y0: number;
|
|
927
945
|
let y1: number;
|
|
928
946
|
if (v >= 0) {
|