@perspective-dev/viewer-charts 5.2.0 → 5.3.1
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 +1 -2
- package/dist/cdn/perspective-viewer-charts.js.map +4 -4
- package/dist/esm/axis/legend.d.ts +53 -5
- package/dist/esm/axis/map-ticks.d.ts +15 -0
- package/dist/esm/charts/candlestick/candlestick-interact.d.ts +2 -6
- package/dist/esm/charts/candlestick/candlestick-render.d.ts +3 -0
- package/dist/esm/charts/cartesian/cartesian.d.ts +7 -0
- package/dist/esm/charts/cartesian/glyph.d.ts +2 -1
- package/dist/esm/charts/cartesian/glyphs/density.d.ts +2 -1
- package/dist/esm/charts/cartesian/glyphs/lines.d.ts +2 -1
- package/dist/esm/charts/cartesian/glyphs/points.d.ts +2 -1
- package/dist/esm/charts/cartesian/tooltip-lines.d.ts +1 -1
- package/dist/esm/charts/chart-base.d.ts +24 -9
- package/dist/esm/charts/chart.d.ts +81 -6
- package/dist/esm/charts/common/category-axis-resolver.d.ts +1 -1
- package/dist/esm/charts/common/draw-tooltip-box.d.ts +2 -1
- package/dist/esm/charts/common/tree-chart.d.ts +1 -0
- package/dist/esm/charts/common/tree-interact.d.ts +1 -1
- package/dist/esm/charts/heatmap/heatmap.d.ts +1 -0
- package/dist/esm/charts/series/series-build.d.ts +7 -0
- package/dist/esm/charts/series/series-interact.d.ts +1 -1
- package/dist/esm/charts/series/series-type.d.ts +18 -0
- package/dist/esm/charts/series/series.d.ts +10 -1
- package/dist/esm/charts/sunburst/sunburst-render.d.ts +4 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/interaction/axis-indicators.d.ts +11 -0
- package/dist/esm/interaction/host-sink-dom.d.ts +3 -2
- package/dist/esm/interaction/host-sink-message.d.ts +3 -2
- package/dist/esm/interaction/lazy-tooltip.d.ts +2 -2
- package/dist/esm/interaction/legend-controller.d.ts +246 -0
- package/dist/esm/interaction/tooltip-controller.d.ts +20 -95
- package/dist/esm/interaction/tooltip-grid.d.ts +33 -0
- package/dist/esm/layout/facet-grid.d.ts +6 -0
- package/dist/esm/layout/ticks.d.ts +1 -0
- package/dist/esm/map/tile-source.d.ts +77 -8
- package/dist/esm/perspective-viewer-charts.js +1 -2
- package/dist/esm/perspective-viewer-charts.js.map +4 -4
- package/dist/esm/plugin/charts.d.ts +2 -0
- package/dist/esm/plugin/format-defaults.d.ts +9 -0
- package/dist/esm/plugin/plugin.d.ts +28 -1
- package/dist/esm/theme/gradient.d.ts +32 -0
- package/dist/esm/theme/overrides.d.ts +19 -0
- package/dist/esm/transport/protocol.d.ts +58 -5
- package/dist/esm/transport/renderer-transport.d.ts +3 -0
- package/dist/esm/utils/css.d.ts +4 -0
- package/dist/esm/worker/renderer.worker.d.ts +46 -1
- package/package.json +1 -1
- package/src/css/perspective-viewer-charts.css +0 -1
- package/src/ts/axis/bar-axis.ts +2 -7
- package/src/ts/axis/legend.ts +351 -24
- package/src/ts/axis/map-ticks.ts +94 -0
- package/src/ts/axis/numeric-axis.ts +2 -9
- package/src/ts/charts/candlestick/candlestick-build.ts +1 -0
- package/src/ts/charts/candlestick/candlestick-interact.ts +46 -40
- package/src/ts/charts/candlestick/candlestick-render.ts +81 -10
- package/src/ts/charts/candlestick/candlestick.ts +4 -3
- package/src/ts/charts/cartesian/cartesian-interact.ts +9 -3
- package/src/ts/charts/cartesian/cartesian-render.ts +374 -75
- package/src/ts/charts/cartesian/cartesian.ts +13 -0
- package/src/ts/charts/cartesian/glyph.ts +6 -2
- package/src/ts/charts/cartesian/glyphs/density.ts +2 -2
- package/src/ts/charts/cartesian/glyphs/lines.ts +15 -11
- package/src/ts/charts/cartesian/glyphs/points.ts +9 -5
- package/src/ts/charts/cartesian/tooltip-lines.ts +8 -8
- package/src/ts/charts/chart-base.ts +76 -42
- package/src/ts/charts/chart.ts +111 -7
- package/src/ts/charts/common/category-axis-resolver.ts +44 -5
- package/src/ts/charts/common/draw-tooltip-box.ts +13 -30
- package/src/ts/charts/common/tree-chart.ts +4 -0
- package/src/ts/charts/common/tree-chrome.ts +102 -23
- package/src/ts/charts/common/tree-interact.ts +18 -17
- package/src/ts/charts/heatmap/heatmap-build.ts +7 -1
- package/src/ts/charts/heatmap/heatmap-interact.ts +75 -8
- package/src/ts/charts/heatmap/heatmap-render.ts +105 -24
- package/src/ts/charts/heatmap/heatmap.ts +8 -3
- package/src/ts/charts/map/map.ts +3 -5
- package/src/ts/charts/series/series-build.ts +15 -2
- package/src/ts/charts/series/series-interact.ts +14 -24
- package/src/ts/charts/series/series-render.ts +310 -132
- package/src/ts/charts/series/series-type.ts +65 -0
- package/src/ts/charts/series/series.ts +31 -8
- package/src/ts/charts/sunburst/sunburst-render.ts +16 -1
- package/src/ts/charts/treemap/treemap-render.ts +12 -1
- package/src/ts/data/view-reader.ts +1 -4
- package/src/ts/index.ts +2 -0
- package/src/ts/interaction/axis-indicators.ts +137 -0
- package/src/ts/interaction/host-sink-dom.ts +66 -2
- package/src/ts/interaction/host-sink-message.ts +8 -2
- package/src/ts/interaction/lazy-tooltip.ts +5 -5
- package/src/ts/interaction/legend-controller.ts +1084 -0
- package/src/ts/interaction/tooltip-controller.ts +43 -131
- package/src/ts/interaction/tooltip-grid.ts +187 -0
- package/src/ts/layout/facet-grid.ts +8 -1
- package/src/ts/layout/ticks.ts +48 -12
- package/src/ts/map/tile-loader.ts +16 -1
- package/src/ts/map/tile-source.ts +199 -53
- package/src/ts/map/tile-sources.json +18 -0
- package/src/ts/plugin/charts.ts +40 -6
- package/src/ts/plugin/format-defaults.ts +37 -0
- package/src/ts/plugin/plugin.ts +330 -76
- package/src/ts/shaders/line.vert.glsl +5 -2
- package/src/ts/shaders/scatter.vert.glsl +0 -7
- package/src/ts/theme/gradient.ts +265 -1
- package/src/ts/theme/overrides.ts +76 -0
- package/src/ts/transport/protocol.ts +60 -4
- package/src/ts/transport/renderer-transport.ts +107 -14
- package/src/ts/utils/css.ts +12 -0
- package/src/ts/worker/dispatch.ts +6 -2
- package/src/ts/worker/renderer.worker.ts +191 -7
|
@@ -14,6 +14,7 @@ import type { Client, View, ViewConfig } from "@perspective-dev/client";
|
|
|
14
14
|
import type { FacetConfig, PluginConfig } from "../charts/chart";
|
|
15
15
|
import type {
|
|
16
16
|
ControlMsg,
|
|
17
|
+
ErrorMsg,
|
|
17
18
|
InitMsg,
|
|
18
19
|
InteractionEvent,
|
|
19
20
|
LoadAndRenderMsg,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
} from "../event-detail";
|
|
27
28
|
import { snapshotThemeVars } from "../theme/theme-snapshot";
|
|
28
29
|
import { snapshotFontFaces } from "../utils/font-snapshot";
|
|
30
|
+
import { TILE_SOURCES } from "../map/tile-source";
|
|
29
31
|
import { DomHostSink } from "../interaction/host-sink-dom";
|
|
30
32
|
import { RUNTIME_MODE } from "../config";
|
|
31
33
|
|
|
@@ -72,6 +74,24 @@ async function getSharedWorker(): Promise<Worker> {
|
|
|
72
74
|
const env = e.data as WorkerEnvelope;
|
|
73
75
|
HOST_LISTENERS.get(env.sessionId)?.(env.msg);
|
|
74
76
|
});
|
|
77
|
+
|
|
78
|
+
let poisoned = false;
|
|
79
|
+
w.addEventListener("error", (event: ErrorEvent) => {
|
|
80
|
+
if (poisoned) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
poisoned = true;
|
|
85
|
+
SHARED_WORKER = null;
|
|
86
|
+
const detail = event.message || `failed to load ${url}`;
|
|
87
|
+
const message = `perspective-viewer-charts renderer worker error: ${detail}`;
|
|
88
|
+
console.error(message, event.error ?? "");
|
|
89
|
+
const errorMsg: ErrorMsg = { kind: "error", message };
|
|
90
|
+
for (const listener of HOST_LISTENERS.values()) {
|
|
91
|
+
listener(errorMsg);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
75
95
|
return w;
|
|
76
96
|
})();
|
|
77
97
|
|
|
@@ -156,6 +176,10 @@ export class RendererTransport {
|
|
|
156
176
|
private _pendingCounter = 0;
|
|
157
177
|
private _onZoomChanged: ((isDefault: boolean) => void) | null = null;
|
|
158
178
|
|
|
179
|
+
private _onPluginConfigDelta:
|
|
180
|
+
| ((fields: Record<string, string | number | boolean>) => void)
|
|
181
|
+
| null = null;
|
|
182
|
+
|
|
159
183
|
/**
|
|
160
184
|
* Cached zoom-default flag pushed by the renderer after each zoom
|
|
161
185
|
* mutation. Surfaced sync via `allZoomsDefault()`; updates between
|
|
@@ -235,6 +259,9 @@ export class RendererTransport {
|
|
|
235
259
|
maxCells: number;
|
|
236
260
|
precompileShaders?: boolean;
|
|
237
261
|
onZoomChanged?: (isDefault: boolean) => void;
|
|
262
|
+
onPluginConfigDelta?: (
|
|
263
|
+
fields: Record<string, string | number | boolean>,
|
|
264
|
+
) => void;
|
|
238
265
|
}) {
|
|
239
266
|
this._client = opts.client;
|
|
240
267
|
this._view = opts.view;
|
|
@@ -246,6 +273,7 @@ export class RendererTransport {
|
|
|
246
273
|
this._maxCells = opts.maxCells;
|
|
247
274
|
this._precompileShaders = opts.precompileShaders ?? false;
|
|
248
275
|
this._onZoomChanged = opts.onZoomChanged ?? null;
|
|
276
|
+
this._onPluginConfigDelta = opts.onPluginConfigDelta ?? null;
|
|
249
277
|
this._ready = new Promise((resolve, reject) => {
|
|
250
278
|
this._resolveReady = resolve;
|
|
251
279
|
this._rejectReady = reject;
|
|
@@ -299,15 +327,24 @@ export class RendererTransport {
|
|
|
299
327
|
// `ImageBitmap`. Direct mode transfers the visible canvas's
|
|
300
328
|
// drawing buffer to the renderer so GL paints straight to
|
|
301
329
|
// screen.
|
|
330
|
+
// Blit mode also keeps the gridlines/chrome layers worker-side:
|
|
331
|
+
// a transferred canvas presents on the compositor's schedule,
|
|
332
|
+
// unsynchronized with the host's layout commits, so it can't
|
|
333
|
+
// participate in the staged-present hold. The renderer
|
|
334
|
+
// composites both layers into the shipped frame instead, and
|
|
335
|
+
// the host-side placeholder canvases stay untouched
|
|
336
|
+
// (transparent). Direct mode transfers all three surfaces.
|
|
302
337
|
let glOC: OffscreenCanvas | undefined;
|
|
338
|
+
let gridlinesOC: OffscreenCanvas | undefined;
|
|
339
|
+
let chromeOC: OffscreenCanvas | undefined;
|
|
303
340
|
if (opts.renderBlitMode === "blit") {
|
|
304
341
|
this._displayCtx = opts.gl.getContext("2d");
|
|
305
342
|
} else {
|
|
306
343
|
glOC = opts.gl.transferControlToOffscreen();
|
|
344
|
+
gridlinesOC = opts.gridlines.transferControlToOffscreen();
|
|
345
|
+
chromeOC = opts.chrome.transferControlToOffscreen();
|
|
307
346
|
}
|
|
308
347
|
|
|
309
|
-
const gridlinesOC = opts.gridlines.transferControlToOffscreen();
|
|
310
|
-
const chromeOC = opts.chrome.transferControlToOffscreen();
|
|
311
348
|
const rect = opts.gl.getBoundingClientRect();
|
|
312
349
|
const dpr = window.devicePixelRatio || 1;
|
|
313
350
|
const themeVars = snapshotThemeVars(opts.gl);
|
|
@@ -342,6 +379,9 @@ export class RendererTransport {
|
|
|
342
379
|
pluginConfig: opts.pluginConfig,
|
|
343
380
|
columnsConfig: opts.columnsConfig,
|
|
344
381
|
defaultChartType: opts.defaultChartType,
|
|
382
|
+
tileSource: TILE_SOURCES.specFor(
|
|
383
|
+
opts.pluginConfig.map_tile_provider,
|
|
384
|
+
),
|
|
345
385
|
themeVars,
|
|
346
386
|
fontFaces,
|
|
347
387
|
cssWidth: rect.width,
|
|
@@ -365,14 +405,19 @@ export class RendererTransport {
|
|
|
365
405
|
// Worker mode: the bootstrap is triggered by posting the
|
|
366
406
|
// init message into the worker's scope (which the
|
|
367
407
|
// `if (IS_WORKER_SCOPE)` block in `renderer.worker.ts`
|
|
368
|
-
// listens for).
|
|
369
|
-
// renderer allocates its own
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
408
|
+
// listens for). All three canvases are omitted in blit mode
|
|
409
|
+
// (the renderer allocates its own offscreens and ships
|
|
410
|
+
// composited frames) — only include the transferred
|
|
411
|
+
// surfaces present on this mode's init message.
|
|
412
|
+
const transfer: Transferable[] = [this._proxyChannel!.port2];
|
|
413
|
+
if (chromeOC) {
|
|
414
|
+
transfer.unshift(chromeOC);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (gridlinesOC) {
|
|
418
|
+
transfer.unshift(gridlinesOC);
|
|
419
|
+
}
|
|
420
|
+
|
|
376
421
|
if (glOC) {
|
|
377
422
|
transfer.unshift(glOC);
|
|
378
423
|
}
|
|
@@ -474,7 +519,11 @@ export class RendererTransport {
|
|
|
474
519
|
}
|
|
475
520
|
|
|
476
521
|
setPluginConfig(cfg: PluginConfig): void {
|
|
477
|
-
this._post({
|
|
522
|
+
this._post({
|
|
523
|
+
kind: "setPluginConfig",
|
|
524
|
+
cfg,
|
|
525
|
+
tileSource: TILE_SOURCES.specFor(cfg.map_tile_provider),
|
|
526
|
+
});
|
|
478
527
|
}
|
|
479
528
|
|
|
480
529
|
setBufferMaxCapacity(n: number): void {
|
|
@@ -793,7 +842,12 @@ export class RendererTransport {
|
|
|
793
842
|
this._resolvePending(msg.requestId, "saveZoom", msg.state);
|
|
794
843
|
break;
|
|
795
844
|
case "pinTooltip":
|
|
796
|
-
this._ensureHostSink()?.pin(
|
|
845
|
+
this._ensureHostSink()?.pin(
|
|
846
|
+
msg.grid,
|
|
847
|
+
msg.pos,
|
|
848
|
+
msg.bounds,
|
|
849
|
+
msg.style,
|
|
850
|
+
);
|
|
797
851
|
break;
|
|
798
852
|
case "dismissTooltip":
|
|
799
853
|
this._hostSink?.dismiss();
|
|
@@ -801,6 +855,9 @@ export class RendererTransport {
|
|
|
801
855
|
case "setCursor":
|
|
802
856
|
this._ensureHostSink()?.setCursor(msg.cursor);
|
|
803
857
|
break;
|
|
858
|
+
case "pluginConfigDelta":
|
|
859
|
+
this._onPluginConfigDelta?.(msg.fields);
|
|
860
|
+
break;
|
|
804
861
|
case "userClick":
|
|
805
862
|
this._dispatchOnViewer(
|
|
806
863
|
new CustomEvent<PerspectiveClickDetail>(
|
|
@@ -864,13 +921,35 @@ export class RendererTransport {
|
|
|
864
921
|
this._rejectReady(new Error(msg.message));
|
|
865
922
|
break;
|
|
866
923
|
case "loadAndRenderAck":
|
|
867
|
-
|
|
924
|
+
if (msg.error !== undefined) {
|
|
925
|
+
this._rejectPending(
|
|
926
|
+
msg.msgId,
|
|
927
|
+
"loadAndRender",
|
|
928
|
+
new Error(msg.error),
|
|
929
|
+
);
|
|
930
|
+
} else {
|
|
931
|
+
this._resolvePending(msg.msgId, "loadAndRender", undefined);
|
|
932
|
+
}
|
|
933
|
+
|
|
868
934
|
break;
|
|
869
935
|
case "resizeAck":
|
|
870
936
|
this._resolvePending(msg.msgId, "resize", undefined);
|
|
871
937
|
break;
|
|
872
938
|
case "snapshotPngReply":
|
|
873
|
-
|
|
939
|
+
if (msg.error !== undefined) {
|
|
940
|
+
this._rejectPending(
|
|
941
|
+
msg.requestId,
|
|
942
|
+
"snapshotPng",
|
|
943
|
+
new Error(msg.error),
|
|
944
|
+
);
|
|
945
|
+
} else {
|
|
946
|
+
this._resolvePending(
|
|
947
|
+
msg.requestId,
|
|
948
|
+
"snapshotPng",
|
|
949
|
+
msg.blob,
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
|
|
874
953
|
break;
|
|
875
954
|
}
|
|
876
955
|
}
|
|
@@ -896,6 +975,20 @@ export class RendererTransport {
|
|
|
896
975
|
entry.resolve(value);
|
|
897
976
|
}
|
|
898
977
|
|
|
978
|
+
private _rejectPending(
|
|
979
|
+
id: number,
|
|
980
|
+
kind: PendingRenderType,
|
|
981
|
+
error: Error,
|
|
982
|
+
): void {
|
|
983
|
+
const entry = this._pending.get(id);
|
|
984
|
+
if (!entry || entry.kind !== kind) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
this._pending.delete(id);
|
|
989
|
+
entry.reject(error);
|
|
990
|
+
}
|
|
991
|
+
|
|
899
992
|
/**
|
|
900
993
|
* Blit-mode handler: draw a renderer-emitted frame into the
|
|
901
994
|
* visible 2D-context display canvas, then close the bitmap so its
|
package/src/ts/utils/css.ts
CHANGED
|
@@ -34,3 +34,15 @@ export function parseCSSColorToVec3(
|
|
|
34
34
|
|
|
35
35
|
return [0.5, 0.5, 0.5];
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Format a `[0, 1]`-range RGB triple as a plain `#rrggbb` string.
|
|
40
|
+
*/
|
|
41
|
+
export function vec3ToHexColor(c: [number, number, number]): string {
|
|
42
|
+
const channel = (x: number) =>
|
|
43
|
+
Math.round(Math.max(0, Math.min(1, x)) * 255)
|
|
44
|
+
.toString(16)
|
|
45
|
+
.padStart(2, "0");
|
|
46
|
+
|
|
47
|
+
return `#${channel(c[0])}${channel(c[1])}${channel(c[2])}`;
|
|
48
|
+
}
|
|
@@ -28,7 +28,7 @@ export function dispatch(r: WorkerRenderer, msg: ControlMsg): void {
|
|
|
28
28
|
r.chartImpl.setColumnsConfig?.(msg.cfg);
|
|
29
29
|
break;
|
|
30
30
|
case "setPluginConfig":
|
|
31
|
-
r.
|
|
31
|
+
r.setPluginConfig(msg.cfg, msg.tileSource);
|
|
32
32
|
r.redraw();
|
|
33
33
|
break;
|
|
34
34
|
case "setBufferMaxCapacity":
|
|
@@ -93,7 +93,11 @@ export function dispatch(r: WorkerRenderer, msg: ControlMsg): void {
|
|
|
93
93
|
r.post({ kind: "snapshotPngReply", requestId, blob });
|
|
94
94
|
})
|
|
95
95
|
.catch((err) => {
|
|
96
|
-
r.post({
|
|
96
|
+
r.post({
|
|
97
|
+
kind: "snapshotPngReply",
|
|
98
|
+
requestId,
|
|
99
|
+
error: String(err),
|
|
100
|
+
});
|
|
97
101
|
});
|
|
98
102
|
break;
|
|
99
103
|
}
|
|
@@ -18,7 +18,7 @@ import type * as wasm_module_type from "@perspective-dev/viewer/dist/wasm/perspe
|
|
|
18
18
|
import { WebGLContextManager } from "../webgl/context-manager";
|
|
19
19
|
import { ContextPool } from "../webgl/context-pool";
|
|
20
20
|
import { RENDER_CONTEXT_POOL_SIZE } from "../config";
|
|
21
|
-
import { ChartImplementation } from "../charts/chart";
|
|
21
|
+
import { ChartImplementation, type PluginConfig } from "../charts/chart";
|
|
22
22
|
import { ZoomController } from "../interaction/zoom-controller";
|
|
23
23
|
import {
|
|
24
24
|
applyPan,
|
|
@@ -36,6 +36,7 @@ import type {
|
|
|
36
36
|
WorkerMsg,
|
|
37
37
|
} from "../transport/protocol";
|
|
38
38
|
import { viewToColumnDataMap } from "../data/view-reader";
|
|
39
|
+
import { TILE_SOURCES, type TileSourceSpec } from "../map/tile-source";
|
|
39
40
|
import { loadFontDeduped } from "./font-loader";
|
|
40
41
|
import { dispatch } from "./dispatch";
|
|
41
42
|
import { installSessionHost } from "./session-host";
|
|
@@ -106,6 +107,20 @@ export class WorkerRenderer {
|
|
|
106
107
|
cssWidth: number;
|
|
107
108
|
cssHeight: number;
|
|
108
109
|
dpr: number;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Blit-mode compose surface: gridlines + GL frame + chrome are
|
|
113
|
+
* drawn here each `endFrame` and shipped as ONE `ImageBitmap`, so
|
|
114
|
+
* every layer rides the host's staged-present hold (see
|
|
115
|
+
* `InitMsg.gridlinesCanvas`). `null` in direct mode.
|
|
116
|
+
*/
|
|
117
|
+
private _composeCanvas: OffscreenCanvas | null = null;
|
|
118
|
+
private _composeCtx: OffscreenCanvasRenderingContext2D | null = null;
|
|
119
|
+
|
|
120
|
+
private _lastPlot: ImageBitmap | null = null;
|
|
121
|
+
|
|
122
|
+
private _frameSerial = 0;
|
|
123
|
+
private _overlayPresentQueued = false;
|
|
109
124
|
client: Client;
|
|
110
125
|
view: View;
|
|
111
126
|
|
|
@@ -152,6 +167,13 @@ export class WorkerRenderer {
|
|
|
152
167
|
|
|
153
168
|
this.chartImpl = new ImplClass();
|
|
154
169
|
|
|
170
|
+
// Registry write must precede the chart impl's first
|
|
171
|
+
// `setPluginConfig` — `pluginConfig.map_tile_provider` may
|
|
172
|
+
// name this runtime-registered source.
|
|
173
|
+
if (msg.tileSource) {
|
|
174
|
+
TILE_SOURCES.register(msg.tileSource);
|
|
175
|
+
}
|
|
176
|
+
|
|
155
177
|
// Three surfaces, by mode:
|
|
156
178
|
// - direct: the host's transferred `.webgl-canvas` (1:1 with a
|
|
157
179
|
// context, permanently).
|
|
@@ -182,20 +204,31 @@ export class WorkerRenderer {
|
|
|
182
204
|
});
|
|
183
205
|
}
|
|
184
206
|
|
|
207
|
+
// Blit mode: the 2D layers are worker-local (the host omits
|
|
208
|
+
// them from the init message) and every shipped frame is the
|
|
209
|
+
// full composite — gridlines under the GL plot under chrome,
|
|
210
|
+
// the same stacking as the host's canvas elements and the
|
|
211
|
+
// `snapshotPng` composite. Direct mode draws into the host's
|
|
212
|
+
// transferred surfaces and ships nothing.
|
|
213
|
+
const w = Math.max(1, Math.round(msg.cssWidth * msg.dpr));
|
|
214
|
+
const h = Math.max(1, Math.round(msg.cssHeight * msg.dpr));
|
|
215
|
+
this.gridlines = msg.gridlinesCanvas ?? new OffscreenCanvas(w, h);
|
|
216
|
+
this.chrome = msg.chromeCanvas ?? new OffscreenCanvas(w, h);
|
|
185
217
|
if (msg.renderMode === "blit") {
|
|
186
218
|
this.glManager.setFrameCallback((bitmap) => {
|
|
187
|
-
this.
|
|
219
|
+
this._frameSerial += 1;
|
|
220
|
+
const frame = this._composeFrame(bitmap);
|
|
221
|
+
this.post({ kind: "frameBitmap", bitmap: frame }, [frame]);
|
|
188
222
|
});
|
|
223
|
+
this.chartImpl.setOverlayPresenter?.(() => this._presentOverlay());
|
|
189
224
|
}
|
|
190
225
|
|
|
191
|
-
this.gridlines = msg.gridlinesCanvas;
|
|
192
|
-
this.chrome = msg.chromeCanvas;
|
|
193
226
|
this.cssWidth = msg.cssWidth;
|
|
194
227
|
this.cssHeight = msg.cssHeight;
|
|
195
228
|
this.dpr = msg.dpr;
|
|
196
229
|
|
|
197
|
-
this.chartImpl.setGridlineCanvas?.(
|
|
198
|
-
this.chartImpl.setChromeCanvas?.(
|
|
230
|
+
this.chartImpl.setGridlineCanvas?.(this.gridlines);
|
|
231
|
+
this.chartImpl.setChromeCanvas?.(this.chrome);
|
|
199
232
|
this.chartImpl.setTheme?.(msg.themeVars);
|
|
200
233
|
|
|
201
234
|
if (msg.defaultChartType) {
|
|
@@ -228,6 +261,20 @@ export class WorkerRenderer {
|
|
|
228
261
|
this.chartImpl.setView?.(this.view);
|
|
229
262
|
}
|
|
230
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Registry write precedes the config apply — `cfg` may name the
|
|
266
|
+
* spec riding alongside it (see `SetPluginConfigMsg.tileSource`),
|
|
267
|
+
* and a replaced template must be registered first so the map
|
|
268
|
+
* chart's rebind sees the new content-derived cache id.
|
|
269
|
+
*/
|
|
270
|
+
setPluginConfig(cfg: PluginConfig, tileSource?: TileSourceSpec): void {
|
|
271
|
+
if (tileSource) {
|
|
272
|
+
TILE_SOURCES.register(tileSource);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.chartImpl.setPluginConfig?.(cfg);
|
|
276
|
+
}
|
|
277
|
+
|
|
231
278
|
/**
|
|
232
279
|
* Full data-fetch + render pipeline. Owns every `Client`/`Table`/
|
|
233
280
|
* `View` await on the render path:
|
|
@@ -258,6 +305,7 @@ export class WorkerRenderer {
|
|
|
258
305
|
*/
|
|
259
306
|
async loadAndRender(msg: LoadAndRenderMsg): Promise<void> {
|
|
260
307
|
const myGen = ++this._renderGen;
|
|
308
|
+
let error: string | undefined;
|
|
261
309
|
try {
|
|
262
310
|
const [numRows, schema, exprSchema, tableSchema] =
|
|
263
311
|
await Promise.all([
|
|
@@ -327,9 +375,10 @@ export class WorkerRenderer {
|
|
|
327
375
|
} catch (err) {
|
|
328
376
|
if ((err + "").indexOf("View not found") === -1) {
|
|
329
377
|
console.error("loadAndRender failed", err);
|
|
378
|
+
error = String(err);
|
|
330
379
|
}
|
|
331
380
|
} finally {
|
|
332
|
-
this.post({ kind: "loadAndRenderAck", msgId: msg.msgId });
|
|
381
|
+
this.post({ kind: "loadAndRenderAck", msgId: msg.msgId, error });
|
|
333
382
|
}
|
|
334
383
|
}
|
|
335
384
|
|
|
@@ -498,7 +547,59 @@ export class WorkerRenderer {
|
|
|
498
547
|
return { controller: this.zoomController, layout };
|
|
499
548
|
}
|
|
500
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Legend-first interaction routing. The chart's `LegendController`
|
|
552
|
+
* sees every forwarded event BEFORE the zoom / tooltip paths, so
|
|
553
|
+
* legend gestures (scroll, width drag, floating move / resize)
|
|
554
|
+
* structurally preempt plot pan / zoom / hover — a wheel over the
|
|
555
|
+
* legend can never zoom the plot under it, and a floating-panel
|
|
556
|
+
* drag can never start a pan.
|
|
557
|
+
*/
|
|
558
|
+
private _legendInteraction(event: InteractionEvent): boolean {
|
|
559
|
+
const chart = this.chartImpl as any;
|
|
560
|
+
const legend = chart?._legend;
|
|
561
|
+
const cfg = chart?._pluginConfig;
|
|
562
|
+
if (!legend || !cfg) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return legend.handleEvent(event, {
|
|
567
|
+
cfg,
|
|
568
|
+
cssWidth: this.cssWidth,
|
|
569
|
+
cssHeight: this.cssHeight,
|
|
570
|
+
legendRects: chart._legendRects ?? [],
|
|
571
|
+
repaint: (relayout: boolean) => {
|
|
572
|
+
if (!relayout && typeof chart.repaintChrome === "function") {
|
|
573
|
+
chart.repaintChrome();
|
|
574
|
+
} else {
|
|
575
|
+
this.chartImpl.requestRender(this.glManager);
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
postDelta: (fields: Record<string, string | number | boolean>) =>
|
|
579
|
+
this.post({ kind: "pluginConfigDelta", fields }),
|
|
580
|
+
setCursor: (cursor: string) => chart._hostSink?.setCursor?.(cursor),
|
|
581
|
+
dispatchLeave: () => this._tooltip()?.dispatchLeave(),
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
501
585
|
onInteraction(event: InteractionEvent): void {
|
|
586
|
+
try {
|
|
587
|
+
this._routeInteraction(event);
|
|
588
|
+
} catch (err) {
|
|
589
|
+
this._dragTarget = null;
|
|
590
|
+
(this.chartImpl as any)?._legend?.cancelGesture?.();
|
|
591
|
+
console.error("interaction dispatch failed", err);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private _routeInteraction(event: InteractionEvent): void {
|
|
596
|
+
const plotDragging =
|
|
597
|
+
this._dragTarget !== null &&
|
|
598
|
+
(event.type === "pointermove" || event.type === "pointerup");
|
|
599
|
+
if (!plotDragging && this._legendInteraction(event)) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
502
603
|
switch (event.type) {
|
|
503
604
|
case "wheel": {
|
|
504
605
|
const target = this._resolveTarget(event.mx, event.my);
|
|
@@ -588,6 +689,87 @@ export class WorkerRenderer {
|
|
|
588
689
|
return tt ?? null;
|
|
589
690
|
}
|
|
590
691
|
|
|
692
|
+
/**
|
|
693
|
+
* Blit-mode frame composite: draw the gridlines (bottom), the GL
|
|
694
|
+
* plot bitmap (middle) and the chrome/axes (top) into the compose
|
|
695
|
+
* surface and transfer the result — the single `ImageBitmap` the
|
|
696
|
+
* host blits, and the unit the staged-present hold stages. Runs in
|
|
697
|
+
* `endFrame`, which the scheduler orders after `render2D`'s 2D
|
|
698
|
+
* flush, so both layers hold this frame's content.
|
|
699
|
+
*
|
|
700
|
+
* A layer whose buffer differs from the plot's dimensions is
|
|
701
|
+
* skipped rather than scaled: its content is a stale-sized frame
|
|
702
|
+
* mid-resize (the 2D draws re-size their canvas to `css × dpr` on
|
|
703
|
+
* the next flush), and scaling it would re-introduce exactly the
|
|
704
|
+
* warp this composite exists to prevent.
|
|
705
|
+
*/
|
|
706
|
+
private _composeFrame(plot: ImageBitmap): ImageBitmap {
|
|
707
|
+
const w = plot.width;
|
|
708
|
+
const h = plot.height;
|
|
709
|
+
if (!this._composeCanvas) {
|
|
710
|
+
this._composeCanvas = new OffscreenCanvas(w, h);
|
|
711
|
+
this._composeCtx = this._composeCanvas.getContext("2d");
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const canvas = this._composeCanvas;
|
|
715
|
+
const ctx = this._composeCtx;
|
|
716
|
+
if (!ctx) {
|
|
717
|
+
return plot;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (canvas.width !== w || canvas.height !== h) {
|
|
721
|
+
canvas.width = w;
|
|
722
|
+
canvas.height = h;
|
|
723
|
+
} else {
|
|
724
|
+
ctx.clearRect(0, 0, w, h);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (this.gridlines.width === w && this.gridlines.height === h) {
|
|
728
|
+
ctx.drawImage(this.gridlines, 0, 0);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
ctx.drawImage(plot, 0, 0);
|
|
732
|
+
if (this._lastPlot !== plot) {
|
|
733
|
+
this._lastPlot?.close();
|
|
734
|
+
this._lastPlot = plot;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (this.chrome.width === w && this.chrome.height === h) {
|
|
738
|
+
ctx.drawImage(this.chrome, 0, 0);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return canvas.transferToImageBitmap();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private _presentOverlay(): void {
|
|
745
|
+
if (this._overlayPresentQueued) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
this._overlayPresentQueued = true;
|
|
750
|
+
const mark = this._frameSerial;
|
|
751
|
+
queueMicrotask(() => {
|
|
752
|
+
this._overlayPresentQueued = false;
|
|
753
|
+
const plot = this._lastPlot;
|
|
754
|
+
if (this._frameSerial !== mark || !plot) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const w = Math.max(1, Math.round(this.cssWidth * this.dpr));
|
|
759
|
+
const h = Math.max(1, Math.round(this.cssHeight * this.dpr));
|
|
760
|
+
if (plot.width !== w || plot.height !== h) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const frame = this._composeFrame(plot);
|
|
765
|
+
if (frame === plot) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
this.post({ kind: "frameBitmap", bitmap: frame }, [frame]);
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
591
773
|
/**
|
|
592
774
|
* Composite the three layers into a single PNG `Blob`.
|
|
593
775
|
*/
|
|
@@ -649,6 +831,8 @@ export class WorkerRenderer {
|
|
|
649
831
|
// next-RAF `drain()` can't paint/present against a dead
|
|
650
832
|
// context (the "scheduler: present failed" path).
|
|
651
833
|
unregister(this.glManager);
|
|
834
|
+
this._lastPlot?.close();
|
|
835
|
+
this._lastPlot = null;
|
|
652
836
|
this.chartImpl.destroy();
|
|
653
837
|
this.glManager.destroy();
|
|
654
838
|
}
|