@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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ShaderRegistry } from "./shader-registry";
|
|
2
|
+
export type WebGLCanvas = HTMLCanvasElement | OffscreenCanvas;
|
|
3
|
+
export declare class GLContext {
|
|
4
|
+
readonly id: number;
|
|
5
|
+
readonly canvas: WebGLCanvas;
|
|
6
|
+
readonly gl: WebGL2RenderingContext | WebGLRenderingContext;
|
|
7
|
+
readonly isWebGL2: boolean;
|
|
8
|
+
shaders: ShaderRegistry;
|
|
9
|
+
private _maxVertexAttribs;
|
|
10
|
+
private _angle;
|
|
11
|
+
private _precompile;
|
|
12
|
+
private _destroyed;
|
|
13
|
+
/**
|
|
14
|
+
* Per-tenant hooks fired after a `webglcontextrestored`. Each
|
|
15
|
+
* {@link WebGLContextManager} on this context registers one that
|
|
16
|
+
* rebuilds its `BufferPool` (the shaders are rebuilt here, once, for
|
|
17
|
+
* all tenants). On an intentional {@link destroy} these are dropped
|
|
18
|
+
* without firing.
|
|
19
|
+
*/
|
|
20
|
+
private _onRestore;
|
|
21
|
+
constructor(canvas: WebGLCanvas, options?: {
|
|
22
|
+
precompile?: boolean;
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Return every vertex-attribute slot to its default state: disabled,
|
|
26
|
+
* with a zero instance divisor. Vertex-array state (enable flags,
|
|
27
|
+
* pointers, divisors) is global to a context's default VAO — there
|
|
28
|
+
* are no per-chart VAOs in this codebase — so when many charts share
|
|
29
|
+
* one pooled context, a slot a prior chart enabled stays enabled for
|
|
30
|
+
* the next chart, now pointing at a buffer the prior chart's
|
|
31
|
+
* teardown deleted. Its next `drawArraysInstanced` then fails GL
|
|
32
|
+
* validation: "no buffer is bound to enabled attribute", silently
|
|
33
|
+
* dropping the draw.
|
|
34
|
+
*
|
|
35
|
+
* Glyphs already (re)enable + bind + set the divisor for every slot
|
|
36
|
+
* they use on each frame, so clearing to the default here is a safe
|
|
37
|
+
* baseline — it only removes the *leftover* enables a sibling chart
|
|
38
|
+
* left behind. Called from {@link WebGLContextManager.beginFrame} in
|
|
39
|
+
* shared (pooled) mode only; the 1:1 default never shares a context
|
|
40
|
+
* and so keeps its original, reset-free path.
|
|
41
|
+
*/
|
|
42
|
+
resetVertexArrayState(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Register a tenant's post-restore rebuild hook. Returns a disposer.
|
|
45
|
+
*/
|
|
46
|
+
onRestore(cb: () => void): () => void;
|
|
47
|
+
/**
|
|
48
|
+
* True once lost — either our own {@link destroy} or a browser
|
|
49
|
+
* eviction of the oldest context past the per-agent cap.
|
|
50
|
+
*/
|
|
51
|
+
isContextLost(): boolean;
|
|
52
|
+
get destroyed(): boolean;
|
|
53
|
+
destroy(): void;
|
|
54
|
+
}
|
|
@@ -34,11 +34,17 @@ export declare function createQuadCornerBuffer(gl: GL): WebGLBuffer;
|
|
|
34
34
|
* paint zero data). No-op return `true` when `attr` is negative
|
|
35
35
|
* (optimized-out attribute — drawing the rest is still valid).
|
|
36
36
|
*
|
|
37
|
+
* `firstInstance` byte-offsets the attribute pointer so the draw's
|
|
38
|
+
* instance 0 reads element `firstInstance` of the buffer — the WebGL
|
|
39
|
+
* substitute for a `baseInstance` draw parameter. Faceted renderers
|
|
40
|
+
* use it to draw a contiguous per-facet sub-range of a shared
|
|
41
|
+
* instance buffer.
|
|
42
|
+
*
|
|
37
43
|
* Render-path uses `peek`, never `getOrCreate`: the latter recreates
|
|
38
44
|
* with zero-initialized contents when `_totalCapacity` has grown past
|
|
39
45
|
* the current buffer, which would wipe the previous draw's vertex
|
|
40
46
|
* data and leave `drawArraysInstanced` to render zeros. See
|
|
41
47
|
* {@link BufferPool#peek} for the full rationale.
|
|
42
48
|
*/
|
|
43
|
-
export declare function bindInstancedFloatAttr(glManager: WebGLContextManager, instancing: Instancing, attr: number, name: string, components: number): boolean;
|
|
49
|
+
export declare function bindInstancedFloatAttr(glManager: WebGLContextManager, instancing: Instancing, attr: number, name: string, components: number, firstInstance?: number): boolean;
|
|
44
50
|
export {};
|
|
@@ -72,6 +72,18 @@ export declare class WorkerRenderer {
|
|
|
72
72
|
*/
|
|
73
73
|
loadAndRender(msg: LoadAndRenderMsg): Promise<void>;
|
|
74
74
|
redraw(): void;
|
|
75
|
+
/**
|
|
76
|
+
* `redraw`, acked to the host AFTER the frame's present completes —
|
|
77
|
+
* the reply backing the viewer's presize contract (`ResizeAckMsg`):
|
|
78
|
+
* `plugin.resize()` must not resolve until the resized frame is
|
|
79
|
+
* actually on screen. A resize landing mid-present defers its
|
|
80
|
+
* canvas mutation (`deferIfDraining`), but the redraw's own frame
|
|
81
|
+
* applies pending dimensions in `beginFrame`, so the present this
|
|
82
|
+
* awaits IS at the new size. Always acks — a failed present or a
|
|
83
|
+
* torn-down chart resolves the host's await rather than stranding
|
|
84
|
+
* it (completion, not success, is the contract).
|
|
85
|
+
*/
|
|
86
|
+
redrawAck(msgId: number): void;
|
|
75
87
|
resize(cssWidth: number, cssHeight: number, dpr: number): void;
|
|
76
88
|
clear(): void;
|
|
77
89
|
saveZoom(): any;
|
package/package.json
CHANGED
|
@@ -224,7 +224,10 @@ export function renderCandlestickFrame(
|
|
|
224
224
|
chart._lastCatTicks = numericCat
|
|
225
225
|
? computeNiceTicks(vis.xMin, vis.xMax, 6)
|
|
226
226
|
: null;
|
|
227
|
-
|
|
227
|
+
// Deferred past the GPU fence (see `_defer2D`) so the chrome canvas
|
|
228
|
+
// doesn't present ahead of the GL glyphs on resize. Reads the
|
|
229
|
+
// `_last*` frame state set above.
|
|
230
|
+
chart._defer2D(() => renderCandlestickChromeOverlay(chart));
|
|
228
231
|
}
|
|
229
232
|
|
|
230
233
|
export function renderCandlestickChromeOverlay(chart: CandlestickChart): void {
|
|
@@ -92,15 +92,16 @@ export class CandlestickChart extends CategoricalYChart {
|
|
|
92
92
|
_yDomain: { min: number; max: number } = { min: 0, max: 1 };
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
* `domain_mode: "expand"`
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
95
|
+
* `domain_mode: "expand"` accumulator — the VALUE (Y) axis only.
|
|
96
|
+
* The category axis has NO accumulator on band charts (see
|
|
97
|
+
* `SeriesChart._expandedLeftDomain`): a numeric/datetime `group_by`
|
|
98
|
+
* axis always fits the current data. Cleared in
|
|
99
|
+
* `resetExpandedDomain` — wired from the worker's `resetAllZooms`
|
|
100
|
+
* and from view-config mutations on `AbstractChart`. `null`
|
|
101
|
+
* whenever the option is `"fit"` or the accumulator has just been
|
|
102
|
+
* cleared.
|
|
101
103
|
*/
|
|
102
104
|
_expandedYDomain: { min: number; max: number } | null = null;
|
|
103
|
-
_expandedCategoryDomain: { min: number; max: number } | null = null;
|
|
104
105
|
|
|
105
106
|
/**
|
|
106
107
|
* Numeric category-axis state (single non-string group_by).
|
|
@@ -267,22 +268,17 @@ export class CandlestickChart extends CategoricalYChart {
|
|
|
267
268
|
scratchCandles: this._candles,
|
|
268
269
|
});
|
|
269
270
|
// `domain_mode: "expand"` post-build union — mirrors the series
|
|
270
|
-
// pipeline
|
|
271
|
-
//
|
|
271
|
+
// pipeline: VALUE (Y) axis only, never the category axis (see
|
|
272
|
+
// `SeriesChart.uploadAndRender`). `expandDomainInPlace` mutates
|
|
273
|
+
// `result.yDomain` so the assignment below picks up the grown
|
|
274
|
+
// extent automatically.
|
|
272
275
|
if (this._pluginConfig.domain_mode === "expand") {
|
|
273
276
|
this._expandedYDomain = expandDomainInPlace(
|
|
274
277
|
this._expandedYDomain,
|
|
275
278
|
result.yDomain,
|
|
276
279
|
);
|
|
277
|
-
if (result.numericCategoryDomain) {
|
|
278
|
-
this._expandedCategoryDomain = expandDomainInPlace(
|
|
279
|
-
this._expandedCategoryDomain,
|
|
280
|
-
result.numericCategoryDomain,
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
280
|
} else {
|
|
284
281
|
this._expandedYDomain = null;
|
|
285
|
-
this._expandedCategoryDomain = null;
|
|
286
282
|
}
|
|
287
283
|
|
|
288
284
|
this._rowPaths = result.rowPaths;
|
|
@@ -320,7 +316,6 @@ export class CandlestickChart extends CategoricalYChart {
|
|
|
320
316
|
|
|
321
317
|
override resetExpandedDomain(): void {
|
|
322
318
|
this._expandedYDomain = null;
|
|
323
|
-
this._expandedCategoryDomain = null;
|
|
324
319
|
}
|
|
325
320
|
|
|
326
321
|
protected destroyInternal(): void {
|
|
@@ -204,7 +204,11 @@ export function renderCartesianFrame(
|
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
|
|
207
|
+
// Defer the chrome (axes/legend/tooltip) draw past the GPU fence so
|
|
208
|
+
// the chrome canvas doesn't present ahead of the GL glyphs on resize.
|
|
209
|
+
// `renderCartesianChromeOverlay` reads the `_last*` frame state set
|
|
210
|
+
// above, which stays stable until the next `_fullRender`.
|
|
211
|
+
chart._defer2D(() => renderCartesianChromeOverlay(chart));
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
interface RenderFrameCtx {
|
|
@@ -406,26 +410,25 @@ function renderSinglePlotFrame(
|
|
|
406
410
|
|
|
407
411
|
const isMap = chart._renderMode === "map";
|
|
408
412
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
theme,
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
} else if (chart._gridlineCanvas && isMap) {
|
|
413
|
+
// Defer the gridline draw past the GPU fence (see `_defer2D`) so the
|
|
414
|
+
// gridline canvas doesn't present ahead of the GL glyphs on resize.
|
|
415
|
+
// The closure captures this frame's `layout` / ticks / `theme`.
|
|
416
|
+
const dpr = glManager.dpr;
|
|
417
|
+
const gridlineCanvas = chart._gridlineCanvas;
|
|
418
|
+
if (gridlineCanvas && !isMap) {
|
|
419
|
+
chart._defer2D(() => {
|
|
420
|
+
// One-shot destructive prep (resizes + clears + scales to
|
|
421
|
+
// DPR). `renderGridlines` itself is non-destructive.
|
|
422
|
+
initCanvas(gridlineCanvas, layout, dpr);
|
|
423
|
+
renderGridlines(gridlineCanvas, layout, xTicks, yTicks, theme, dpr);
|
|
424
|
+
});
|
|
425
|
+
} else if (gridlineCanvas && isMap) {
|
|
423
426
|
// Map mode draws no cartesian gridlines, but the gridline
|
|
424
427
|
// canvas may carry stale ink from a prior cartesian chart
|
|
425
428
|
// type. Reset it to a clean transparent surface so the
|
|
426
429
|
// basemap (rendered into the GL canvas below) reads as the
|
|
427
430
|
// only background layer.
|
|
428
|
-
initCanvas(
|
|
431
|
+
chart._defer2D(() => initCanvas(gridlineCanvas, layout, dpr));
|
|
429
432
|
}
|
|
430
433
|
|
|
431
434
|
renderInPlotFrame(gl, layout, glManager.dpr, () => {
|
|
@@ -571,10 +574,17 @@ function renderFacetedFrame(
|
|
|
571
574
|
// One-shot destructive prep for the gridline + WebGL canvases.
|
|
572
575
|
// Both phases below are per-facet; calling their destructive
|
|
573
576
|
// helpers (initCanvas / renderInPlotFrame) in the loop would wipe
|
|
574
|
-
// every previously-drawn facet, leaving only the last cell
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
|
|
577
|
+
// every previously-drawn facet, leaving only the last cell visible.
|
|
578
|
+
//
|
|
579
|
+
// The gridline draws are deferred past the GPU fence (see
|
|
580
|
+
// `_defer2D`) so the gridline canvas doesn't present ahead of the GL
|
|
581
|
+
// glyphs on resize. `initCanvas` is deferred first so it runs before
|
|
582
|
+
// the per-facet `renderGridlines` closures below (FIFO flush order);
|
|
583
|
+
// the GL prep (`clearAndSetupFrame`) and draws stay in this pass.
|
|
584
|
+
const dpr = glManager.dpr;
|
|
585
|
+
const gridlineCanvas = chart._gridlineCanvas;
|
|
586
|
+
if (gridlineCanvas && sampleLayout) {
|
|
587
|
+
chart._defer2D(() => initCanvas(gridlineCanvas, sampleLayout, dpr));
|
|
578
588
|
}
|
|
579
589
|
|
|
580
590
|
clearAndSetupFrame(gl);
|
|
@@ -609,49 +619,56 @@ function renderFacetedFrame(
|
|
|
609
619
|
// basemap layer is rendered into the GL canvas inside the
|
|
610
620
|
// facet's scissor below.
|
|
611
621
|
const isMap = chart._renderMode === "map";
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
622
|
+
if (gridlineCanvas && !isMap) {
|
|
623
|
+
// Deferred to the post-fence 2D flush. The closure captures
|
|
624
|
+
// this facet's `cell` (whose `cell.layout` already carries
|
|
625
|
+
// the padded domain from `buildProjectionMatrix` above) plus
|
|
626
|
+
// the frame-local domain/ticks, so it stays correct without
|
|
627
|
+
// recomputing at flush time.
|
|
628
|
+
chart._defer2D(() => {
|
|
629
|
+
const localXTicks = independent
|
|
630
|
+
? computeTicks(
|
|
631
|
+
buildXDomain(
|
|
632
|
+
chart,
|
|
633
|
+
facetDomain.xMin,
|
|
634
|
+
facetDomain.xMax,
|
|
635
|
+
xIsDate,
|
|
636
|
+
),
|
|
637
|
+
buildYDomain(
|
|
638
|
+
chart,
|
|
639
|
+
facetDomain.yMin,
|
|
640
|
+
facetDomain.yMax,
|
|
641
|
+
yIsDate,
|
|
642
|
+
),
|
|
643
|
+
cell.layout,
|
|
644
|
+
).xTicks
|
|
645
|
+
: xTicks;
|
|
646
|
+
const localYTicks = independent
|
|
647
|
+
? computeTicks(
|
|
648
|
+
buildXDomain(
|
|
649
|
+
chart,
|
|
650
|
+
facetDomain.xMin,
|
|
651
|
+
facetDomain.xMax,
|
|
652
|
+
xIsDate,
|
|
653
|
+
),
|
|
654
|
+
buildYDomain(
|
|
655
|
+
chart,
|
|
656
|
+
facetDomain.yMin,
|
|
657
|
+
facetDomain.yMax,
|
|
658
|
+
yIsDate,
|
|
659
|
+
),
|
|
660
|
+
cell.layout,
|
|
661
|
+
).yTicks
|
|
662
|
+
: yTicks;
|
|
663
|
+
renderGridlines(
|
|
664
|
+
gridlineCanvas,
|
|
665
|
+
cell.layout,
|
|
666
|
+
localXTicks,
|
|
667
|
+
localYTicks,
|
|
668
|
+
theme,
|
|
669
|
+
dpr,
|
|
670
|
+
);
|
|
671
|
+
});
|
|
655
672
|
}
|
|
656
673
|
|
|
657
674
|
withScissor(gl, cell.layout, glManager.dpr, () => {
|
|
@@ -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
|
|
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
|
|
819
|
-
*
|
|
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(
|
|
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
|
package/src/ts/charts/chart.ts
CHANGED
|
@@ -44,13 +44,23 @@ export interface ChartImplementation {
|
|
|
44
44
|
requestRender(glManager: WebGLContextManager): Promise<void>;
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* The chart-specific frame builder.
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
|
250
|
-
* color
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|