@perspective-dev/viewer-charts 4.3.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/LICENSE.md +193 -0
- package/dist/cdn/perspective-viewer-charts.js +3 -0
- package/dist/cdn/perspective-viewer-charts.js.map +7 -0
- package/dist/esm/axis/axis-primitives.d.ts +24 -0
- package/dist/esm/axis/bar-axis.d.ts +51 -0
- package/dist/esm/axis/canvas.d.ts +24 -0
- package/dist/esm/axis/categorical-axis-core.d.ts +42 -0
- package/dist/esm/axis/categorical-axis.d.ts +27 -0
- package/dist/esm/axis/facet-chrome.d.ts +13 -0
- package/dist/esm/axis/label-geometry.d.ts +41 -0
- package/dist/esm/axis/legend.d.ts +44 -0
- package/dist/esm/axis/numeric-axis.d.ts +20 -0
- package/dist/esm/charts/candlestick/candlestick-build.d.ts +129 -0
- package/dist/esm/charts/candlestick/candlestick-interact.d.ts +10 -0
- package/dist/esm/charts/candlestick/candlestick-render.d.ts +24 -0
- package/dist/esm/charts/candlestick/candlestick.d.ts +144 -0
- package/dist/esm/charts/candlestick/glyphs/draw-candlesticks.d.ts +36 -0
- package/dist/esm/charts/candlestick/glyphs/draw-ohlc.d.ts +33 -0
- package/dist/esm/charts/canvas-types.d.ts +15 -0
- package/dist/esm/charts/cartesian/cartesian-build.d.ts +14 -0
- package/dist/esm/charts/cartesian/cartesian-interact.d.ts +20 -0
- package/dist/esm/charts/cartesian/cartesian-render.d.ts +26 -0
- package/dist/esm/charts/cartesian/cartesian.d.ts +239 -0
- package/dist/esm/charts/cartesian/glyph.d.ts +53 -0
- package/dist/esm/charts/cartesian/glyphs/density.d.ts +142 -0
- package/dist/esm/charts/cartesian/glyphs/lines.d.ts +23 -0
- package/dist/esm/charts/cartesian/glyphs/points.d.ts +24 -0
- package/dist/esm/charts/cartesian/label-interner.d.ts +21 -0
- package/dist/esm/charts/cartesian/tooltip-lines.d.ts +11 -0
- package/dist/esm/charts/chart-base.d.ts +402 -0
- package/dist/esm/charts/chart.d.ts +338 -0
- package/dist/esm/charts/common/band-layout.d.ts +32 -0
- package/dist/esm/charts/common/categorical-y-chart.d.ts +53 -0
- package/dist/esm/charts/common/category-axis-resolver.d.ts +90 -0
- package/dist/esm/charts/common/chrome-cache.d.ts +18 -0
- package/dist/esm/charts/common/draw-tooltip-box.d.ts +9 -0
- package/dist/esm/charts/common/leaf-color.d.ts +33 -0
- package/dist/esm/charts/common/node-store.d.ts +81 -0
- package/dist/esm/charts/common/tree-chart.d.ts +48 -0
- package/dist/esm/charts/common/tree-chrome.d.ts +31 -0
- package/dist/esm/charts/common/tree-data.d.ts +54 -0
- package/dist/esm/charts/common/visible-extent.d.ts +51 -0
- package/dist/esm/charts/heatmap/heatmap-build.d.ts +86 -0
- package/dist/esm/charts/heatmap/heatmap-interact.d.ts +19 -0
- package/dist/esm/charts/heatmap/heatmap-render.d.ts +19 -0
- package/dist/esm/charts/heatmap/heatmap-y-axis.d.ts +46 -0
- package/dist/esm/charts/heatmap/heatmap.d.ts +117 -0
- package/dist/esm/charts/map/map.d.ts +67 -0
- package/dist/esm/charts/registry.d.ts +14 -0
- package/dist/esm/charts/series/glyphs/draw-areas.d.ts +30 -0
- package/dist/esm/charts/series/glyphs/draw-bars.d.ts +15 -0
- package/dist/esm/charts/series/glyphs/draw-lines.d.ts +34 -0
- package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +33 -0
- package/dist/esm/charts/series/series-build.d.ts +228 -0
- package/dist/esm/charts/series/series-interact.d.ts +35 -0
- package/dist/esm/charts/series/series-render.d.ts +41 -0
- package/dist/esm/charts/series/series-type.d.ts +49 -0
- package/dist/esm/charts/series/series.d.ts +317 -0
- package/dist/esm/charts/sunburst/sunburst-interact.d.ts +7 -0
- package/dist/esm/charts/sunburst/sunburst-layout.d.ts +33 -0
- package/dist/esm/charts/sunburst/sunburst-render.d.ts +22 -0
- package/dist/esm/charts/sunburst/sunburst.d.ts +85 -0
- package/dist/esm/charts/treemap/treemap-interact.d.ts +12 -0
- package/dist/esm/charts/treemap/treemap-layout.d.ts +28 -0
- package/dist/esm/charts/treemap/treemap-render.d.ts +18 -0
- package/dist/esm/charts/treemap/treemap.d.ts +74 -0
- package/dist/esm/config.d.ts +27 -0
- package/dist/esm/data/lazy-row.d.ts +32 -0
- package/dist/esm/data/split-groups.d.ts +20 -0
- package/dist/esm/data/view-reader.d.ts +35 -0
- package/dist/esm/event-detail.d.ts +28 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/interaction/hit-test.d.ts +30 -0
- package/dist/esm/interaction/host-sink-dom.d.ts +19 -0
- package/dist/esm/interaction/host-sink-message.d.ts +46 -0
- package/dist/esm/interaction/lazy-tooltip.d.ts +61 -0
- package/dist/esm/interaction/raw-event-forwarder.d.ts +27 -0
- package/dist/esm/interaction/spatial-grid.d.ts +15 -0
- package/dist/esm/interaction/tooltip-controller.d.ts +193 -0
- package/dist/esm/interaction/zoom-controller.d.ts +106 -0
- package/dist/esm/interaction/zoom-router.d.ts +48 -0
- package/dist/esm/layout/facet-grid.d.ts +126 -0
- package/dist/esm/layout/plot-layout.d.ts +104 -0
- package/dist/esm/layout/ticks.d.ts +17 -0
- package/dist/esm/map/mercator.d.ts +102 -0
- package/dist/esm/map/tile-cache.d.ts +38 -0
- package/dist/esm/map/tile-layer.d.ts +66 -0
- package/dist/esm/map/tile-loader.d.ts +52 -0
- package/dist/esm/map/tile-source.d.ts +66 -0
- package/dist/esm/perspective-viewer-charts.js +3 -0
- package/dist/esm/perspective-viewer-charts.js.map +7 -0
- package/dist/esm/plugin/charts.d.ts +40 -0
- package/dist/esm/plugin/plugin.d.ts +95 -0
- package/dist/esm/render/scheduler.d.ts +41 -0
- package/dist/esm/theme/gradient.d.ts +48 -0
- package/dist/esm/theme/palette.d.ts +13 -0
- package/dist/esm/theme/theme-snapshot.d.ts +7 -0
- package/dist/esm/theme/theme.d.ts +53 -0
- package/dist/esm/transport/protocol.d.ts +430 -0
- package/dist/esm/transport/renderer-transport.d.ts +201 -0
- package/dist/esm/utils/css.d.ts +1 -0
- package/dist/esm/utils/font-snapshot.d.ts +50 -0
- package/dist/esm/webgl/buffer-pool.d.ts +62 -0
- package/dist/esm/webgl/context-manager.d.ts +184 -0
- package/dist/esm/webgl/gradient-texture.d.ts +17 -0
- package/dist/esm/webgl/instanced-attrs.d.ts +44 -0
- package/dist/esm/webgl/plot-frame.d.ts +39 -0
- package/dist/esm/webgl/program-cache.d.ts +13 -0
- package/dist/esm/webgl/shader-manifest.d.ts +53 -0
- package/dist/esm/webgl/shader-registry.d.ts +22 -0
- package/dist/esm/worker/boot.d.ts +0 -0
- package/dist/esm/worker/dispatch.d.ts +9 -0
- package/dist/esm/worker/font-loader.d.ts +2 -0
- package/dist/esm/worker/renderer.worker.d.ts +115 -0
- package/dist/esm/worker/session-host.d.ts +26 -0
- package/package.json +47 -0
- package/src/css/perspective-viewer-charts.css +95 -0
- package/src/ts/axis/axis-primitives.ts +125 -0
- package/src/ts/axis/bar-axis.ts +345 -0
- package/src/ts/axis/canvas.ts +64 -0
- package/src/ts/axis/categorical-axis-core.ts +125 -0
- package/src/ts/axis/categorical-axis.ts +716 -0
- package/src/ts/axis/facet-chrome.ts +42 -0
- package/src/ts/axis/label-geometry.ts +188 -0
- package/src/ts/axis/legend.ts +218 -0
- package/src/ts/axis/numeric-axis.ts +353 -0
- package/src/ts/charts/candlestick/candlestick-build.ts +516 -0
- package/src/ts/charts/candlestick/candlestick-interact.ts +256 -0
- package/src/ts/charts/candlestick/candlestick-render.ts +387 -0
- package/src/ts/charts/candlestick/candlestick.ts +367 -0
- package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +432 -0
- package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +317 -0
- package/src/ts/charts/canvas-types.ts +30 -0
- package/src/ts/charts/cartesian/cartesian-build.ts +616 -0
- package/src/ts/charts/cartesian/cartesian-interact.ts +355 -0
- package/src/ts/charts/cartesian/cartesian-render.ts +948 -0
- package/src/ts/charts/cartesian/cartesian.ts +469 -0
- package/src/ts/charts/cartesian/glyph.ts +81 -0
- package/src/ts/charts/cartesian/glyphs/density.ts +1263 -0
- package/src/ts/charts/cartesian/glyphs/lines.ts +320 -0
- package/src/ts/charts/cartesian/glyphs/points.ts +239 -0
- package/src/ts/charts/cartesian/label-interner.ts +56 -0
- package/src/ts/charts/cartesian/tooltip-lines.ts +80 -0
- package/src/ts/charts/chart-base.ts +840 -0
- package/src/ts/charts/chart.ts +427 -0
- package/src/ts/charts/common/band-layout.ts +63 -0
- package/src/ts/charts/common/categorical-y-chart.ts +81 -0
- package/src/ts/charts/common/category-axis-resolver.ts +314 -0
- package/src/ts/charts/common/chrome-cache.ts +79 -0
- package/src/ts/charts/common/draw-tooltip-box.ts +84 -0
- package/src/ts/charts/common/leaf-color.ts +92 -0
- package/src/ts/charts/common/node-store.ts +235 -0
- package/src/ts/charts/common/tree-chart.ts +76 -0
- package/src/ts/charts/common/tree-chrome.ts +123 -0
- package/src/ts/charts/common/tree-data.ts +623 -0
- package/src/ts/charts/common/visible-extent.ts +112 -0
- package/src/ts/charts/heatmap/heatmap-build.ts +426 -0
- package/src/ts/charts/heatmap/heatmap-interact.ts +274 -0
- package/src/ts/charts/heatmap/heatmap-render.ts +815 -0
- package/src/ts/charts/heatmap/heatmap-y-axis.ts +351 -0
- package/src/ts/charts/heatmap/heatmap.ts +368 -0
- package/src/ts/charts/map/map.ts +201 -0
- package/src/ts/charts/registry.ts +65 -0
- package/src/ts/charts/series/glyphs/draw-areas.ts +331 -0
- package/src/ts/charts/series/glyphs/draw-bars.ts +113 -0
- package/src/ts/charts/series/glyphs/draw-lines.ts +320 -0
- package/src/ts/charts/series/glyphs/draw-scatter.ts +328 -0
- package/src/ts/charts/series/series-build.ts +848 -0
- package/src/ts/charts/series/series-interact.ts +604 -0
- package/src/ts/charts/series/series-render.ts +1109 -0
- package/src/ts/charts/series/series-type.ts +99 -0
- package/src/ts/charts/series/series.ts +794 -0
- package/src/ts/charts/sunburst/sunburst-interact.ts +460 -0
- package/src/ts/charts/sunburst/sunburst-layout.ts +238 -0
- package/src/ts/charts/sunburst/sunburst-render.ts +887 -0
- package/src/ts/charts/sunburst/sunburst.ts +248 -0
- package/src/ts/charts/treemap/treemap-interact.ts +445 -0
- package/src/ts/charts/treemap/treemap-layout.ts +328 -0
- package/src/ts/charts/treemap/treemap-render.ts +886 -0
- package/src/ts/charts/treemap/treemap.ts +247 -0
- package/src/ts/config.ts +41 -0
- package/src/ts/data/lazy-row.ts +140 -0
- package/src/ts/data/split-groups.ts +97 -0
- package/src/ts/data/view-reader.ts +107 -0
- package/src/ts/event-detail.ts +44 -0
- package/src/ts/index.ts +53 -0
- package/src/ts/interaction/hit-test.ts +106 -0
- package/src/ts/interaction/host-sink-dom.ts +85 -0
- package/src/ts/interaction/host-sink-message.ts +75 -0
- package/src/ts/interaction/lazy-tooltip.ts +102 -0
- package/src/ts/interaction/raw-event-forwarder.ts +175 -0
- package/src/ts/interaction/spatial-grid.ts +100 -0
- package/src/ts/interaction/tooltip-controller.ts +407 -0
- package/src/ts/interaction/zoom-controller.ts +468 -0
- package/src/ts/interaction/zoom-router.ts +230 -0
- package/src/ts/layout/facet-grid.ts +346 -0
- package/src/ts/layout/plot-layout.ts +277 -0
- package/src/ts/layout/ticks.ts +168 -0
- package/src/ts/map/mercator.ts +204 -0
- package/src/ts/map/tile-cache.ts +96 -0
- package/src/ts/map/tile-layer.ts +382 -0
- package/src/ts/map/tile-loader.ts +143 -0
- package/src/ts/map/tile-source.ts +156 -0
- package/src/ts/plugin/charts.ts +286 -0
- package/src/ts/plugin/plugin.ts +668 -0
- package/src/ts/render/scheduler.ts +339 -0
- package/src/ts/shaders/area.frag.glsl +20 -0
- package/src/ts/shaders/area.vert.glsl +19 -0
- package/src/ts/shaders/bar.frag.glsl +25 -0
- package/src/ts/shaders/bar.vert.glsl +60 -0
- package/src/ts/shaders/candlestick-body.frag.glsl +19 -0
- package/src/ts/shaders/candlestick-body.vert.glsl +34 -0
- package/src/ts/shaders/density-extreme.frag.glsl +30 -0
- package/src/ts/shaders/density-mrt.frag.glsl +44 -0
- package/src/ts/shaders/density-mrt.vert.glsl +48 -0
- package/src/ts/shaders/density-resolve.frag.glsl +89 -0
- package/src/ts/shaders/density-resolve.vert.glsl +23 -0
- package/src/ts/shaders/density-splat.frag.glsl +34 -0
- package/src/ts/shaders/density-splat.vert.glsl +52 -0
- package/src/ts/shaders/gridline.frag.glsl +18 -0
- package/src/ts/shaders/gridline.vert.glsl +18 -0
- package/src/ts/shaders/heatmap.frag.glsl +23 -0
- package/src/ts/shaders/heatmap.vert.glsl +42 -0
- package/src/ts/shaders/line-uniform.frag.glsl +26 -0
- package/src/ts/shaders/line-uniform.vert.glsl +54 -0
- package/src/ts/shaders/line.frag.glsl +28 -0
- package/src/ts/shaders/line.vert.glsl +87 -0
- package/src/ts/shaders/scatter.frag.glsl +39 -0
- package/src/ts/shaders/scatter.vert.glsl +67 -0
- package/src/ts/shaders/sunburst-arc.frag.glsl +19 -0
- package/src/ts/shaders/sunburst-arc.vert.glsl +79 -0
- package/src/ts/shaders/tile.frag.glsl +27 -0
- package/src/ts/shaders/tile.vert.glsl +35 -0
- package/src/ts/shaders/treemap.frag.glsl +19 -0
- package/src/ts/shaders/treemap.vert.glsl +25 -0
- package/src/ts/shaders/y-scatter.frag.glsl +30 -0
- package/src/ts/shaders/y-scatter.vert.glsl +31 -0
- package/src/ts/theme/gradient.ts +312 -0
- package/src/ts/theme/palette.ts +64 -0
- package/src/ts/theme/theme-snapshot.ts +66 -0
- package/src/ts/theme/theme.ts +166 -0
- package/src/ts/transport/protocol.ts +497 -0
- package/src/ts/transport/renderer-transport.ts +788 -0
- package/src/ts/utils/css.ts +36 -0
- package/src/ts/utils/font-snapshot.ts +159 -0
- package/src/ts/webgl/buffer-pool.ts +163 -0
- package/src/ts/webgl/context-manager.ts +414 -0
- package/src/ts/webgl/gradient-texture.ts +84 -0
- package/src/ts/webgl/instanced-attrs.ts +139 -0
- package/src/ts/webgl/plot-frame.ts +91 -0
- package/src/ts/webgl/program-cache.ts +46 -0
- package/src/ts/webgl/shader-manifest.ts +148 -0
- package/src/ts/webgl/shader-registry.ts +97 -0
- package/src/ts/worker/boot.ts +22 -0
- package/src/ts/worker/dispatch.ts +99 -0
- package/src/ts/worker/font-loader.ts +89 -0
- package/src/ts/worker/renderer.worker.ts +734 -0
- package/src/ts/worker/session-host.ts +118 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
import type { View } from "@perspective-dev/client";
|
|
14
|
+
import {
|
|
15
|
+
createNumberFormatter,
|
|
16
|
+
createDatetimeFormatter,
|
|
17
|
+
createDateFormatter,
|
|
18
|
+
sourceColumn,
|
|
19
|
+
type NumberFormatConfig,
|
|
20
|
+
type DateFormatConfig,
|
|
21
|
+
} from "@perspective-dev/viewer/src/ts/column-format.js";
|
|
22
|
+
import type { ColumnDataMap } from "../data/view-reader";
|
|
23
|
+
import { LazyRowFetcher } from "../data/lazy-row";
|
|
24
|
+
import { formatTickValue, formatDateTickValue } from "../layout/ticks";
|
|
25
|
+
import type { WebGLContextManager } from "../webgl/context-manager";
|
|
26
|
+
import {
|
|
27
|
+
ZoomController,
|
|
28
|
+
type ZoomConfig,
|
|
29
|
+
} from "../interaction/zoom-controller";
|
|
30
|
+
import {
|
|
31
|
+
DEFAULT_FACET_CONFIG,
|
|
32
|
+
DEFAULT_PLUGIN_CONFIG,
|
|
33
|
+
type ChartImplementation,
|
|
34
|
+
type FacetConfig,
|
|
35
|
+
type PluginConfig,
|
|
36
|
+
} from "./chart";
|
|
37
|
+
import {
|
|
38
|
+
TooltipController,
|
|
39
|
+
type HostSink,
|
|
40
|
+
type TooltipCallbacks,
|
|
41
|
+
type UserClickPayload,
|
|
42
|
+
type UserSelectPayload,
|
|
43
|
+
} from "../interaction/tooltip-controller";
|
|
44
|
+
import type { PerspectiveClickDetail } from "../event-detail";
|
|
45
|
+
import type { ViewConfig } from "@perspective-dev/client";
|
|
46
|
+
import { resolveThemeFromVars, type Theme } from "../theme/theme";
|
|
47
|
+
import { requestRender as scheduleRender } from "../render/scheduler";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Locale-aware fallback formatter applied to numeric tooltip / legend
|
|
51
|
+
* values when the column has no `number_format` configured. Two
|
|
52
|
+
* fractional digits matches the legacy datagrid default and gives
|
|
53
|
+
* tooltips a stable display width.
|
|
54
|
+
*/
|
|
55
|
+
const DEFAULT_VALUE_FORMATTER: (v: number) => string = ((): ((
|
|
56
|
+
v: number,
|
|
57
|
+
) => string) => {
|
|
58
|
+
return formatTickValue;
|
|
59
|
+
// const intl = createNumberFormatter("float");
|
|
60
|
+
// return (v) => intl.format(v);
|
|
61
|
+
})();
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Locale-aware fallback formatter for datetime tooltip / legend values
|
|
65
|
+
* when the column has no `date_format` configured. Uses the locale
|
|
66
|
+
* default (no `dateStyle` / `timeStyle`) to match what most users
|
|
67
|
+
* expect from an `Intl.DateTimeFormat()` constructed with no options.
|
|
68
|
+
*/
|
|
69
|
+
const DEFAULT_DATETIME_FORMATTER: (v: number) => string = ((): ((
|
|
70
|
+
v: number,
|
|
71
|
+
) => string) => {
|
|
72
|
+
return formatDateTickValue;
|
|
73
|
+
// const intl = createDatetimeFormatter();
|
|
74
|
+
// return (v) => intl.format(v);
|
|
75
|
+
})();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Base class for WebGL chart implementations. Owns the common lifecycle
|
|
79
|
+
* plumbing (canvas wiring, viewer config setters, tooltip controller)
|
|
80
|
+
* so each concrete chart only implements data pipeline, rendering, and
|
|
81
|
+
* destruction hooks.
|
|
82
|
+
*
|
|
83
|
+
* ## Frame lifecycle (three phases)
|
|
84
|
+
*
|
|
85
|
+
* Every render of a chart passes through three phases:
|
|
86
|
+
*
|
|
87
|
+
* 1. `uploadAndRender(glManager, columns, startRow, endRow)`.
|
|
88
|
+
* Driven by the plugin wrapper once per data chunk. The subclass
|
|
89
|
+
* runs its build pipeline (axis/series resolution, record
|
|
90
|
+
* generation, domain accumulation) and pushes typed-array results
|
|
91
|
+
* into GPU buffers via `glManager.bufferPool`. Most charts also
|
|
92
|
+
* compile their shaders lazily here on first call.
|
|
93
|
+
*
|
|
94
|
+
* 2. `requestRender(glManager)` — single entrypoint for triggering a
|
|
95
|
+
* paint. Routes through the module-level scheduler
|
|
96
|
+
* ([render/scheduler.ts]) which coalesces by glManager and runs
|
|
97
|
+
* `_fullRender` + `awaitGpuFence` + `endFrame` on the next RAF.
|
|
98
|
+
* Concurrent requests collapse to one `_fullRender` per frame and
|
|
99
|
+
* fence waits across charts run in parallel, so per-chart latency
|
|
100
|
+
* is bounded by that chart's own GPU work.
|
|
101
|
+
*
|
|
102
|
+
* 3. `_fullRender(glManager)` — the subclass implements its own draw
|
|
103
|
+
* loop: resolve visible domains from the zoom controller, build
|
|
104
|
+
* projection matrices, call into its glyph draw helpers, and paint
|
|
105
|
+
* the chrome overlay (axes, legend, tooltip).
|
|
106
|
+
*
|
|
107
|
+
* `destroy()` is called by the plugin wrapper on teardown. It detaches
|
|
108
|
+
* tooltip listeners, then invokes the subclass's `destroyInternal()`
|
|
109
|
+
* to free chart-specific GL resources.
|
|
110
|
+
*
|
|
111
|
+
* ## What subclasses implement
|
|
112
|
+
* - `uploadAndRender` — phase 1; ends by `await this.requestRender(glManager)`.
|
|
113
|
+
* - `tooltipCallbacks()` — return chart-specific hover/click handlers.
|
|
114
|
+
* - `_fullRender` — phase 3; must be safe to call with no data
|
|
115
|
+
* (subclass guards on its own state machine — empty trees, missing
|
|
116
|
+
* programs, etc — and returns early without touching GL).
|
|
117
|
+
* - `destroyInternal` — release chart-specific resources.
|
|
118
|
+
*
|
|
119
|
+
* `getZoomConfig()` is an optional override; default = both axes
|
|
120
|
+
* zoom-unlocked. See {@link ZoomConfig}.
|
|
121
|
+
*/
|
|
122
|
+
export abstract class AbstractChart implements ChartImplementation {
|
|
123
|
+
// Access is `public` so the per-chart helper modules
|
|
124
|
+
// (e.g. `./bar/bar-build.ts`) can read/write these without fighting
|
|
125
|
+
// TypeScript's `protected` check. The underscore prefix marks them
|
|
126
|
+
// as internal by convention.
|
|
127
|
+
_glManager: WebGLContextManager | null = null;
|
|
128
|
+
_gridlineCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
|
|
129
|
+
_chromeCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Host-supplied CSS-variable map. The host snapshots its DOM via
|
|
133
|
+
* `snapshotThemeVars(el)` and ships it over the control channel;
|
|
134
|
+
* the chart decodes via `resolveThemeFromVars` lazily in
|
|
135
|
+
* `_resolveTheme()`. The chart never reads the DOM itself (it
|
|
136
|
+
* always runs inside `WorkerRenderer`, possibly off-thread).
|
|
137
|
+
*/
|
|
138
|
+
_themeVars: Record<string, string> = {};
|
|
139
|
+
_zoomController: ZoomController | null = null;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Per-facet zoom controllers. Populated when `zoom_mode ===
|
|
143
|
+
* "independent"` and the chart enters faceted mode; each facet's
|
|
144
|
+
* render path reads its own viewport from the matching entry.
|
|
145
|
+
*
|
|
146
|
+
* Shared-zoom mode leaves this empty; `_zoomController` is the
|
|
147
|
+
* single domain used for every facet.
|
|
148
|
+
*/
|
|
149
|
+
_facetZoomControllers: ZoomController[] = [];
|
|
150
|
+
|
|
151
|
+
_columnSlots: (string | null)[] = [];
|
|
152
|
+
_groupBy: string[] = [];
|
|
153
|
+
_splitBy: string[] = [];
|
|
154
|
+
_columnTypes: Record<string, string> = {};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Effective shared-axis flags for the most recent faceted frame.
|
|
158
|
+
* Derived per-frame from `_facetConfig.shared_x_axis` /
|
|
159
|
+
* `shared_y_axis` and `zoom_mode` via
|
|
160
|
+
* {@link computeEffectiveFacetFlags} — independent-zoom mode forces
|
|
161
|
+
* both off because an outer axis band has no single domain it could
|
|
162
|
+
* display. Stored here (rather than mutated back onto
|
|
163
|
+
* `_facetConfig`) so the user's configured shared-axis preferences
|
|
164
|
+
* survive a "shared → independent → shared" round-trip. Read by
|
|
165
|
+
* chrome-overlay code (e.g. `renderFacetedChromeOverlay`,
|
|
166
|
+
* `renderFacetedHeatmapChromeOverlay`) after the main render pass
|
|
167
|
+
* sets them.
|
|
168
|
+
*/
|
|
169
|
+
_lastEffectiveSharedX = false;
|
|
170
|
+
_lastEffectiveSharedY = false;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Source-column types for `group_by` columns — sourced from
|
|
174
|
+
* `table.schema()` (plain columns) merged with `view.expression_schema()`
|
|
175
|
+
* (expression-typed group_bys). Distinct from `_columnTypes` (which
|
|
176
|
+
* is the post-aggregation `view.schema()` map): the level-type
|
|
177
|
+
* lookup for `__ROW_PATH_N__` columns must use the unaggregated
|
|
178
|
+
* type, since `view.schema()` doesn't key these synthetic columns.
|
|
179
|
+
*/
|
|
180
|
+
_groupByTypes: Record<string, string> = {};
|
|
181
|
+
_columnsConfig: Record<string, any> = {};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Pre-compiled per-column value formatters, keyed by the **source**
|
|
185
|
+
* column name (synthetic split-by paths are normalized via
|
|
186
|
+
* `sourceColumn`). Rebuilt by `setColumnsConfig` from the active
|
|
187
|
+
* plugin's `column_config_schema` output, then consulted by axis /
|
|
188
|
+
* tooltip / legend paths via {@link getColumnFormatter}.
|
|
189
|
+
*
|
|
190
|
+
* `undefined` means "no configured formatter for this column" — the
|
|
191
|
+
* caller falls back to the chart's hand-rolled tick formatter.
|
|
192
|
+
*/
|
|
193
|
+
_columnFormatters: Map<string, (v: number) => string> = new Map();
|
|
194
|
+
_defaultChartType: string | undefined = undefined;
|
|
195
|
+
_facetConfig: FacetConfig = { ...DEFAULT_FACET_CONFIG };
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Plugin-scoped global configuration. Updated by `setPluginConfig`
|
|
199
|
+
* (driven from the host's `plugin.restore()`) and read by render-
|
|
200
|
+
* path glyphs (`line_width_px`, `point_size_px`, etc.) and by the
|
|
201
|
+
* build pipelines (`auto_alt_y_axis`, `band_inner_frac`,
|
|
202
|
+
* `bar_inner_pad`). Defaults preserve the previous compile-time
|
|
203
|
+
* constants so first-frame rendering before `restore()` matches
|
|
204
|
+
* the pre-refactor output.
|
|
205
|
+
*/
|
|
206
|
+
_pluginConfig: PluginConfig = { ...DEFAULT_PLUGIN_CONFIG };
|
|
207
|
+
|
|
208
|
+
_tooltip = new TooltipController();
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Reference to the active host sink, captured in {@link attachTooltip}.
|
|
212
|
+
* Used to emit `perspective-click` / `perspective-global-filter` user
|
|
213
|
+
* events back to the host. Distinct from `_tooltip._host` to avoid
|
|
214
|
+
* reaching into the tooltip controller's internals.
|
|
215
|
+
*/
|
|
216
|
+
_hostSink: HostSink | null = null;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Promise chain that serializes user-event emissions so a rapid
|
|
220
|
+
* pin → unpin sequence stays in order even when `buildClickDetail`
|
|
221
|
+
* awaits `_lazyRows.fetchRow`. Without the queue, click 1's async
|
|
222
|
+
* row fetch could resolve AFTER click 2's synchronous `emitUnselect`
|
|
223
|
+
* — flipping the host's observed event order. All emit helpers
|
|
224
|
+
* (`emitClickAndSelect`, `emitUserClick`, `emitUserSelect`,
|
|
225
|
+
* `emitUnselect`) chain through this.
|
|
226
|
+
*/
|
|
227
|
+
_emitQueue: Promise<void> = Promise.resolve();
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Cached resolved theme — populated on first `_resolveTheme()` call,
|
|
231
|
+
* cleared by `invalidateTheme()` (driven from `plugin.restyle()`).
|
|
232
|
+
* `getComputedStyle` / `getPropertyValue` reads cost ~100µs each;
|
|
233
|
+
* zoom/hover dispatch redraws at 60Hz so we resolve once and reuse.
|
|
234
|
+
*/
|
|
235
|
+
_theme: Theme | null = null;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* On-demand single-row fetcher used by lazy tooltip column
|
|
239
|
+
* lookups. Reset on every `setView` call; subclasses read
|
|
240
|
+
* `_lazyRows.fetchRow(rowIdx)` from their hover/pin paths and
|
|
241
|
+
* compare a captured serial against the current hovered/pinned
|
|
242
|
+
* state at resolution time, so stale fetches never paint.
|
|
243
|
+
*
|
|
244
|
+
* Can be `null` on chart types that don't surface the View
|
|
245
|
+
* (unit-tested charts) or before the first `draw`.
|
|
246
|
+
*/
|
|
247
|
+
_lazyRows: LazyRowFetcher | null = null;
|
|
248
|
+
|
|
249
|
+
// ChartImplementation setters (trivial stores)
|
|
250
|
+
|
|
251
|
+
setGridlineCanvas(canvas: HTMLCanvasElement | OffscreenCanvas): void {
|
|
252
|
+
this._gridlineCanvas = canvas;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
setChromeCanvas(canvas: HTMLCanvasElement | OffscreenCanvas): void {
|
|
256
|
+
this._chromeCanvas = canvas;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
setTheme(vars: Record<string, string>): void {
|
|
260
|
+
this._themeVars = vars;
|
|
261
|
+
this._theme = null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setZoomController(zc: ZoomController): void {
|
|
265
|
+
this._zoomController = zc;
|
|
266
|
+
zc.configure(this.getZoomConfig());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Resolve the zoom controller that owns facet `idx`. In shared-zoom
|
|
271
|
+
* mode (default) this is always the chart's single `_zoomController`.
|
|
272
|
+
* In independent-zoom mode the router provisions one controller per
|
|
273
|
+
* facet; this returns the matching entry, allocating on demand so
|
|
274
|
+
* the render path never has to check `zoom_mode` itself.
|
|
275
|
+
*/
|
|
276
|
+
getZoomControllerForFacet(idx: number): ZoomController | null {
|
|
277
|
+
if (this._facetConfig.zoom_mode === "shared") {
|
|
278
|
+
return this._zoomController;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!this._zoomController) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let zc = this._facetZoomControllers[idx];
|
|
286
|
+
if (!zc) {
|
|
287
|
+
zc = new ZoomController();
|
|
288
|
+
zc.configure(this.getZoomConfig());
|
|
289
|
+
this._facetZoomControllers[idx] = zc;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return zc;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Derive the effective shared-X / shared-Y flags for the current
|
|
297
|
+
* frame and stamp them onto `_lastEffectiveSharedX/Y` for downstream
|
|
298
|
+
* chrome-overlay code to consume. Independent-zoom mode forces both
|
|
299
|
+
* shared flags off — the outer axis band cannot display per-cell
|
|
300
|
+
* viewports — without mutating the user's stored `_facetConfig`.
|
|
301
|
+
*
|
|
302
|
+
* Returns `{ independentZoom, effectiveSharedX, effectiveSharedY }`
|
|
303
|
+
* for callers that need the values immediately (e.g. to pass
|
|
304
|
+
* `xAxis: "outer" | "cell"` into `buildFacetGrid`).
|
|
305
|
+
*/
|
|
306
|
+
computeEffectiveFacetFlags(): {
|
|
307
|
+
independentZoom: boolean;
|
|
308
|
+
effectiveSharedX: boolean;
|
|
309
|
+
effectiveSharedY: boolean;
|
|
310
|
+
} {
|
|
311
|
+
const independentZoom = this._facetConfig.zoom_mode === "independent";
|
|
312
|
+
const effectiveSharedX =
|
|
313
|
+
!independentZoom && this._facetConfig.shared_x_axis;
|
|
314
|
+
const effectiveSharedY =
|
|
315
|
+
!independentZoom && this._facetConfig.shared_y_axis;
|
|
316
|
+
this._lastEffectiveSharedX = effectiveSharedX;
|
|
317
|
+
this._lastEffectiveSharedY = effectiveSharedY;
|
|
318
|
+
return { independentZoom, effectiveSharedX, effectiveSharedY };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Wire every active zoom controller's layout pointer for the
|
|
323
|
+
* supplied facet cells. In shared-zoom mode every
|
|
324
|
+
* `getZoomControllerForFacet(i)` returns the same `_zoomController`,
|
|
325
|
+
* so iterating past the first cell would just re-write the same
|
|
326
|
+
* pointer — `break`-on-shared keeps the cost O(1) and avoids the
|
|
327
|
+
* subtle bug where every facet's `updateLayout` overwrites the
|
|
328
|
+
* previous one with the last cell's layout.
|
|
329
|
+
*/
|
|
330
|
+
syncFacetZoomLayouts(
|
|
331
|
+
cells: ReadonlyArray<{
|
|
332
|
+
layout: import("../layout/plot-layout").PlotLayout;
|
|
333
|
+
}>,
|
|
334
|
+
): void {
|
|
335
|
+
const independent = this._facetConfig.zoom_mode === "independent";
|
|
336
|
+
for (let i = 0; i < cells.length; i++) {
|
|
337
|
+
this.getZoomControllerForFacet(i)?.updateLayout(cells[i].layout);
|
|
338
|
+
if (!independent) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Set base domain on every zoom controller owned by this chart.
|
|
346
|
+
*/
|
|
347
|
+
setZoomBaseDomain(
|
|
348
|
+
xMin: number,
|
|
349
|
+
xMax: number,
|
|
350
|
+
yMin: number,
|
|
351
|
+
yMax: number,
|
|
352
|
+
): void {
|
|
353
|
+
if (this._zoomController) {
|
|
354
|
+
this._zoomController.setBaseDomain(xMin, xMax, yMin, yMax);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const zc of this._facetZoomControllers) {
|
|
358
|
+
if (zc) {
|
|
359
|
+
zc.setBaseDomain(xMin, xMax, yMin, yMax);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Zoom-controller config for this chart type. Subclasses override to
|
|
366
|
+
* pin an axis (e.g. bar charts pin the categorical axis). Default:
|
|
367
|
+
* both axes freely zoomable.
|
|
368
|
+
*/
|
|
369
|
+
protected getZoomConfig(): ZoomConfig {
|
|
370
|
+
return {};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
setColumnSlots(slots: (string | null)[]): void {
|
|
374
|
+
this._columnSlots = slots;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
setViewPivots(groupBy: string[], splitBy: string[]): void {
|
|
378
|
+
this._groupBy = groupBy;
|
|
379
|
+
this._splitBy = splitBy;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
setColumnTypes(schema: Record<string, string>): void {
|
|
383
|
+
this._columnTypes = schema;
|
|
384
|
+
this._rebuildColumnFormatters();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Clear any `domain_mode: "expand"` accumulator state. Driven by
|
|
389
|
+
* `plugin.draw()` (a fresh `draw` always indicates a view-level
|
|
390
|
+
* change — viewer config, filters, sorts, etc. — that invalidates
|
|
391
|
+
* the previously-accumulated extent) and by the worker's
|
|
392
|
+
* `resetAllZooms` path (user clicked "Reset Zoom"). `plugin.update()`
|
|
393
|
+
* deliberately does *not* call this — same view, more data, the
|
|
394
|
+
* accumulator should keep growing. No-op on the base; chart
|
|
395
|
+
* families that hold accumulator fields override.
|
|
396
|
+
*/
|
|
397
|
+
resetExpandedDomain(): void {}
|
|
398
|
+
|
|
399
|
+
setGroupByTypes(schema: Record<string, string>): void {
|
|
400
|
+
this._groupByTypes = schema;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
setColumnsConfig(cfg: Record<string, any>): void {
|
|
404
|
+
this._columnsConfig = cfg ?? {};
|
|
405
|
+
this._rebuildColumnFormatters();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Rebuild {@link _columnFormatters} from `_columnsConfig` +
|
|
410
|
+
* `_columnTypes`. Called from both `setColumnsConfig` and
|
|
411
|
+
* `setColumnTypes` since either side of the (config, types) pair
|
|
412
|
+
* can arrive first depending on the host's restore order. Idempotent.
|
|
413
|
+
*/
|
|
414
|
+
private _rebuildColumnFormatters(): void {
|
|
415
|
+
this._columnFormatters = new Map();
|
|
416
|
+
for (const [name, columnCfg] of Object.entries(this._columnsConfig)) {
|
|
417
|
+
// `_columnTypes` is the post-aggregation `view.schema()` map
|
|
418
|
+
// and doesn't key group_by source columns; fall back to
|
|
419
|
+
// `_groupByTypes` so a configured `date_format` on a
|
|
420
|
+
// group_by column (e.g. an "Order Date" pivot) still
|
|
421
|
+
// compiles to an `Intl.DateTimeFormat` rather than being
|
|
422
|
+
// silently dropped.
|
|
423
|
+
const type = this._columnTypes[name] ?? this._groupByTypes[name];
|
|
424
|
+
const fmt = this._compileColumnFormatter(type, columnCfg);
|
|
425
|
+
if (fmt) {
|
|
426
|
+
this._columnFormatters.set(name, fmt);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private _compileColumnFormatter(
|
|
432
|
+
type: string | undefined,
|
|
433
|
+
cfg: Record<string, any> | undefined,
|
|
434
|
+
): ((v: number) => string) | undefined {
|
|
435
|
+
if (!type || !cfg) {
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (type === "integer" || type === "float") {
|
|
440
|
+
const numberFormat = cfg.number_format as
|
|
441
|
+
| NumberFormatConfig
|
|
442
|
+
| undefined;
|
|
443
|
+
if (!numberFormat) {
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const intl = createNumberFormatter(type, numberFormat);
|
|
448
|
+
return (v) => intl.format(v);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (type === "datetime") {
|
|
452
|
+
const dateFormat = cfg.date_format as DateFormatConfig | undefined;
|
|
453
|
+
if (!dateFormat) {
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const intl = createDatetimeFormatter(dateFormat);
|
|
458
|
+
return (v) => intl.format(v);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (type === "date") {
|
|
462
|
+
const dateFormat = cfg.date_format as DateFormatConfig | undefined;
|
|
463
|
+
if (!dateFormat) {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const intl = createDateFormatter(dateFormat);
|
|
468
|
+
return (v) => intl.format(v);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Returns the formatter for `columnName` if one has been configured
|
|
476
|
+
* (via `column_config_schema` + the user's sidebar choices), else a
|
|
477
|
+
* type-appropriate fallback for the chart context.
|
|
478
|
+
*
|
|
479
|
+
* @param columnName May be a synthetic split-by path
|
|
480
|
+
* (`<split_val>|...|<source_col>`); the source column is recovered
|
|
481
|
+
* internally before lookup.
|
|
482
|
+
* @param context `"tick"` returns `undefined` when no per-column
|
|
483
|
+
* formatter is configured, so the receiving axis renderer can
|
|
484
|
+
* apply its own step-aware default (adaptive date precision from
|
|
485
|
+
* tick spacing, K/M/B suffixes for numerics). `"value"` returns
|
|
486
|
+
* a precise `Intl.NumberFormat` / `Intl.DateTimeFormat` fallback —
|
|
487
|
+
* appropriate for tooltips, legends, overlays where the caller
|
|
488
|
+
* invokes the formatter directly and needs a guaranteed function.
|
|
489
|
+
*/
|
|
490
|
+
getColumnFormatter(
|
|
491
|
+
columnName: string | null | undefined,
|
|
492
|
+
context: "tick",
|
|
493
|
+
): ((v: number) => string) | undefined;
|
|
494
|
+
getColumnFormatter(
|
|
495
|
+
columnName: string | null | undefined,
|
|
496
|
+
context?: "value",
|
|
497
|
+
): (v: number) => string;
|
|
498
|
+
getColumnFormatter(
|
|
499
|
+
columnName: string | null | undefined,
|
|
500
|
+
context: "tick" | "value" = "value",
|
|
501
|
+
): ((v: number) => string) | undefined {
|
|
502
|
+
if (columnName) {
|
|
503
|
+
const formatter = this._columnFormatters.get(
|
|
504
|
+
sourceColumn(columnName),
|
|
505
|
+
);
|
|
506
|
+
if (formatter) {
|
|
507
|
+
return formatter;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (context === "tick") {
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// `_columnTypes` is the post-aggregation schema and doesn't
|
|
516
|
+
// key group_by source columns (their post-aggregate form is
|
|
517
|
+
// `__ROW_PATH_N__`); fall back to `_groupByTypes` so date /
|
|
518
|
+
// datetime group_by axes don't get formatted as numbers.
|
|
519
|
+
const sourceName = columnName ? sourceColumn(columnName) : undefined;
|
|
520
|
+
const type = sourceName
|
|
521
|
+
? (this._columnTypes[sourceName] ?? this._groupByTypes[sourceName])
|
|
522
|
+
: undefined;
|
|
523
|
+
|
|
524
|
+
if (type === "date" || type === "datetime") {
|
|
525
|
+
return DEFAULT_DATETIME_FORMATTER;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return DEFAULT_VALUE_FORMATTER;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
setDefaultChartType(chartType: string): void {
|
|
532
|
+
this._defaultChartType = chartType;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
setFacetConfig(cfg: FacetConfig): void {
|
|
536
|
+
this._facetConfig = { ...cfg };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Apply plugin-scoped global config. Stores `cfg` for later reads
|
|
541
|
+
* and mirrors the overlapping fields onto adjacent state so deep
|
|
542
|
+
* render code keeps reading the single struct it already does:
|
|
543
|
+
*
|
|
544
|
+
* - `facet_mode` / `facet_zoom_mode` sync into `_facetConfig` so
|
|
545
|
+
* `cartesian-render.ts` (and the treemap/sunburst grid checks)
|
|
546
|
+
* keep working unchanged.
|
|
547
|
+
* - `series_zoom_mode` toggles the `_autoFitValue` flag declared
|
|
548
|
+
* on `CategoricalYChart` ("dynamic" = refit on zoom, "fixed" =
|
|
549
|
+
* pinned to full extent). Harmless write on charts that don't
|
|
550
|
+
* expose the field.
|
|
551
|
+
*
|
|
552
|
+
* Render-path uniform fields (`line_width_px`, `point_size_px`,
|
|
553
|
+
* `wick_width_px`, `ohlc_line_width_px`) are read directly from
|
|
554
|
+
* `_pluginConfig` by their respective glyphs on each draw — no
|
|
555
|
+
* sync needed. Build-time fields (`auto_alt_y_axis`,
|
|
556
|
+
* `band_inner_frac`, `bar_inner_pad`) are read by the pipeline
|
|
557
|
+
* inputs in `uploadAndRender`; they take effect on next data load.
|
|
558
|
+
*/
|
|
559
|
+
setPluginConfig(cfg: PluginConfig): void {
|
|
560
|
+
this._pluginConfig = { ...cfg };
|
|
561
|
+
this._facetConfig = {
|
|
562
|
+
...this._facetConfig,
|
|
563
|
+
facet_mode: cfg.facet_mode,
|
|
564
|
+
zoom_mode: cfg.facet_zoom_mode,
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
(this as { _autoFitValue?: boolean })._autoFitValue =
|
|
568
|
+
cfg.series_zoom_mode === "dynamic";
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Lazily decode the host-supplied theme vars. Subsequent calls hit
|
|
573
|
+
* the cache until `invalidateTheme()` clears it. Render-path
|
|
574
|
+
* callers should always read theme values through this method so
|
|
575
|
+
* the parsed `Theme` (gradient stops, palette, etc.) amortizes
|
|
576
|
+
* across an entire frame.
|
|
577
|
+
*/
|
|
578
|
+
_resolveTheme(): Theme {
|
|
579
|
+
if (!this._theme) {
|
|
580
|
+
this._theme = resolveThemeFromVars(this._themeVars);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return this._theme;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Drop the cached theme so the next `_resolveTheme()` call re-decodes
|
|
588
|
+
* from `_themeVars`. Wired to `plugin.restyle()` — the host pushes
|
|
589
|
+
* a fresh var snapshot before invalidating.
|
|
590
|
+
*/
|
|
591
|
+
invalidateTheme(): void {
|
|
592
|
+
this._theme = null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Install a new view for lazy row fetches. Disposes any prior
|
|
597
|
+
* fetcher and dismisses the pinned tooltip — the prior pinned
|
|
598
|
+
* row index has no guaranteed correspondence in the new view
|
|
599
|
+
* (pivot / filter / sort changes can all reshuffle rows).
|
|
600
|
+
*/
|
|
601
|
+
setView(view: View): void {
|
|
602
|
+
if (this._lazyRows) {
|
|
603
|
+
this._lazyRows.dispose();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
this._lazyRows = new LazyRowFetcher(view);
|
|
607
|
+
// A view change (filter / pivot / sort / schema) implicitly
|
|
608
|
+
// dismisses any active pin — the prior row index has no
|
|
609
|
+
// guaranteed correspondence in the new view. Emit a matching
|
|
610
|
+
// `selected: false` so downstream filter-coordinated consumers
|
|
611
|
+
// can roll back their derived state.
|
|
612
|
+
const wasPinned = this._tooltip.isPinned;
|
|
613
|
+
this._tooltip.dismiss();
|
|
614
|
+
if (wasPinned) {
|
|
615
|
+
this.emitUnselect();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Build the chart-specific {@link TooltipCallbacks} object — the
|
|
621
|
+
* `onHover` / `onLeave` / `onClickPre` / `onPin` / `onDblClick`
|
|
622
|
+
* surface that mediates between the cursor and chart state.
|
|
623
|
+
* Subclasses override this; the base returns a no-op pair.
|
|
624
|
+
*/
|
|
625
|
+
protected tooltipCallbacks(): TooltipCallbacks {
|
|
626
|
+
return {
|
|
627
|
+
onHover: () => {},
|
|
628
|
+
onLeave: () => {},
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Wire the chart's `TooltipController` for virtual-dispatch
|
|
634
|
+
* `InteractionEvent`s forwarded from the host, and install the
|
|
635
|
+
* host sink that materializes pinned tooltips and cursor changes
|
|
636
|
+
* host-side.
|
|
637
|
+
*/
|
|
638
|
+
attachTooltip(host: HostSink): void {
|
|
639
|
+
this._tooltip.attach(this.tooltipCallbacks());
|
|
640
|
+
this._tooltip.setHost(host);
|
|
641
|
+
this._hostSink = host;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Build a `PerspectiveClickDetail` payload from a per-family
|
|
646
|
+
* resolved click target. Fetches the source-view row via
|
|
647
|
+
* `_lazyRows` (returns `row: {}` if the row can't be resolved —
|
|
648
|
+
* e.g., aggregate / density cells), and concatenates the
|
|
649
|
+
* `group_by` and `split_by` pivot values into a
|
|
650
|
+
* `viewer.restore({ filter })`-shaped patch.
|
|
651
|
+
*
|
|
652
|
+
* Mirrors the filter-building logic in datagrid's
|
|
653
|
+
* `getCellConfig` ([packages/viewer-datagrid/src/ts/get_cell_config.ts]),
|
|
654
|
+
* but operates on `AbstractChart` state rather than a `DatagridModel`.
|
|
655
|
+
*/
|
|
656
|
+
async buildClickDetail(target: {
|
|
657
|
+
rowIdx: number | null;
|
|
658
|
+
columnName: string;
|
|
659
|
+
groupByValues: (string | number | null)[];
|
|
660
|
+
splitByValues: (string | number | null)[];
|
|
661
|
+
}): Promise<PerspectiveClickDetail> {
|
|
662
|
+
let row: Record<string, unknown> = {};
|
|
663
|
+
if (target.rowIdx != null && target.rowIdx >= 0 && this._lazyRows) {
|
|
664
|
+
try {
|
|
665
|
+
const r = await this._lazyRows.fetchRow(target.rowIdx);
|
|
666
|
+
row = Object.fromEntries(r);
|
|
667
|
+
} catch {
|
|
668
|
+
// Fetcher may have been disposed mid-flight; treat as
|
|
669
|
+
// "no row" and emit the filter-only detail anyway.
|
|
670
|
+
row = {};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const filter: Array<[string, "==", string | number]> = [];
|
|
675
|
+
for (let i = 0; i < this._groupBy.length; i++) {
|
|
676
|
+
const v = target.groupByValues[i];
|
|
677
|
+
if (v != null && v !== "") {
|
|
678
|
+
filter.push([this._groupBy[i], "==", v]);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
for (let i = 0; i < this._splitBy.length; i++) {
|
|
683
|
+
const v = target.splitByValues[i];
|
|
684
|
+
if (v != null && v !== "") {
|
|
685
|
+
filter.push([this._splitBy[i], "==", v]);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return {
|
|
690
|
+
row,
|
|
691
|
+
column_names: [target.columnName],
|
|
692
|
+
config: { filter } as Partial<ViewConfig>,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Forward a `perspective-click` to the host. No-op when the chart
|
|
698
|
+
* has not been wired to a host sink (e.g., unit-tested charts).
|
|
699
|
+
* Synchronous; callers needing ordering with async emits should
|
|
700
|
+
* chain through `_emitQueue`.
|
|
701
|
+
*/
|
|
702
|
+
emitUserClick(detail: PerspectiveClickDetail): void {
|
|
703
|
+
const payload: UserClickPayload = {
|
|
704
|
+
row: detail.row,
|
|
705
|
+
column_names: detail.column_names,
|
|
706
|
+
config: detail.config as { filter?: unknown[] },
|
|
707
|
+
};
|
|
708
|
+
this._hostSink?.emitUserClick?.(payload);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Forward a `perspective-global-filter` to the host. The host
|
|
713
|
+
* transport materializes a `PerspectiveSelectDetail` from this plus
|
|
714
|
+
* its cached previous-insert config and dispatches. Synchronous.
|
|
715
|
+
*/
|
|
716
|
+
emitUserSelect(args: {
|
|
717
|
+
selected: boolean;
|
|
718
|
+
row: Record<string, unknown>;
|
|
719
|
+
column_names: string[];
|
|
720
|
+
insertConfig: Partial<ViewConfig>;
|
|
721
|
+
}): void {
|
|
722
|
+
const payload: UserSelectPayload = {
|
|
723
|
+
selected: args.selected,
|
|
724
|
+
row: args.row,
|
|
725
|
+
column_names: args.column_names,
|
|
726
|
+
insertConfig: args.insertConfig as { filter?: unknown[] },
|
|
727
|
+
};
|
|
728
|
+
this._hostSink?.emitUserSelect?.(payload);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Convenience: fire both `perspective-click` and
|
|
733
|
+
* `perspective-global-filter` (`selected: true`) from a resolved
|
|
734
|
+
* click target. Used by chart families where every click both
|
|
735
|
+
* "selects" and "filters" (series, heatmap, candlestick, scatter,
|
|
736
|
+
* treemap-leaf, etc.). Treemap branch / breadcrumb gestures use
|
|
737
|
+
* the lower-level helpers directly.
|
|
738
|
+
*
|
|
739
|
+
* Chains through `_emitQueue` so the row-fetch await can't reorder
|
|
740
|
+
* this emit behind a follow-up `emitUnselect`.
|
|
741
|
+
*/
|
|
742
|
+
emitClickAndSelect(target: {
|
|
743
|
+
rowIdx: number | null;
|
|
744
|
+
columnName: string;
|
|
745
|
+
groupByValues: (string | number | null)[];
|
|
746
|
+
splitByValues: (string | number | null)[];
|
|
747
|
+
}): Promise<void> {
|
|
748
|
+
const next = this._emitQueue.then(async () => {
|
|
749
|
+
const detail = await this.buildClickDetail(target);
|
|
750
|
+
this.emitUserClick(detail);
|
|
751
|
+
this.emitUserSelect({
|
|
752
|
+
selected: true,
|
|
753
|
+
row: detail.row,
|
|
754
|
+
column_names: detail.column_names,
|
|
755
|
+
insertConfig: detail.config,
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
// Swallow errors on the chain so a single failure doesn't
|
|
759
|
+
// poison subsequent emits; surface to console for debugging.
|
|
760
|
+
this._emitQueue = next.catch((e) => {
|
|
761
|
+
console.error("emitClickAndSelect failed", e);
|
|
762
|
+
});
|
|
763
|
+
return next;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Fire a `perspective-global-filter` with `selected: false`. Used
|
|
768
|
+
* by treemap / sunburst breadcrumb navigation and by chart-base's
|
|
769
|
+
* own `setView` when a view change implicitly dismisses any active
|
|
770
|
+
* pin. Chains through `_emitQueue` so it lands AFTER any in-flight
|
|
771
|
+
* `emitClickAndSelect`.
|
|
772
|
+
*/
|
|
773
|
+
emitUnselect(
|
|
774
|
+
args: {
|
|
775
|
+
row?: Record<string, unknown>;
|
|
776
|
+
column_names?: string[];
|
|
777
|
+
} = {},
|
|
778
|
+
): void {
|
|
779
|
+
const next = this._emitQueue.then(() => {
|
|
780
|
+
this.emitUserSelect({
|
|
781
|
+
selected: false,
|
|
782
|
+
row: args.row ?? {},
|
|
783
|
+
column_names: args.column_names ?? [],
|
|
784
|
+
insertConfig: { filter: [] },
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
this._emitQueue = next.catch((e) => {
|
|
788
|
+
console.error("emitUnselect failed", e);
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Render entrypoint
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Public coalesced render. Routes through the module-level
|
|
796
|
+
* scheduler so concurrent calls collapse to one `_fullRender` per
|
|
797
|
+
* RAF and the host blitter receives one bitmap per frame. The
|
|
798
|
+
* returned promise resolves after this chart's `awaitGpuFence` +
|
|
799
|
+
* `endFrame` chain — independent of other charts in the same
|
|
800
|
+
* RAF, which run their fence waits in parallel.
|
|
801
|
+
*
|
|
802
|
+
* Every render-triggering caller — upload chunks, zoom / pan,
|
|
803
|
+
* resize, theme invalidation, host-driven redraws — calls this.
|
|
804
|
+
* The only sanctioned bypass is `snapshotPng`, which calls
|
|
805
|
+
* `_fullRender` directly to keep the GL backbuffer intact for
|
|
806
|
+
* `gl.readPixels`.
|
|
807
|
+
*/
|
|
808
|
+
requestRender(glManager: WebGLContextManager): Promise<void> {
|
|
809
|
+
return scheduleRender(glManager, () => this._fullRender(glManager));
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Lifecycle
|
|
813
|
+
|
|
814
|
+
destroy(): void {
|
|
815
|
+
this._tooltip.detach();
|
|
816
|
+
this._tooltip.dismiss();
|
|
817
|
+
if (this._lazyRows) {
|
|
818
|
+
this._lazyRows.dispose();
|
|
819
|
+
this._lazyRows = null;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
this.destroyInternal();
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Abstract surface
|
|
826
|
+
|
|
827
|
+
abstract uploadAndRender(
|
|
828
|
+
glManager: WebGLContextManager,
|
|
829
|
+
columns: ColumnDataMap,
|
|
830
|
+
startRow: number,
|
|
831
|
+
endRow: number,
|
|
832
|
+
): Promise<void>;
|
|
833
|
+
|
|
834
|
+
abstract _fullRender(glManager: WebGLContextManager): void;
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Release chart-specific GL/CPU resources. `destroy` calls this.
|
|
838
|
+
*/
|
|
839
|
+
protected abstract destroyInternal(): void;
|
|
840
|
+
}
|