@perspective-dev/viewer-charts 4.3.0 → 4.5.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 +2 -2
- package/dist/cdn/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/axis/bar-axis.d.ts +9 -1
- package/dist/esm/axis/categorical-axis.d.ts +0 -2
- package/dist/esm/charts/cartesian/cartesian.d.ts +26 -0
- package/dist/esm/charts/common/category-axis-resolver.d.ts +43 -1
- package/dist/esm/charts/common/expand-domain.d.ts +20 -0
- package/dist/esm/charts/common/tree-chart.d.ts +7 -0
- package/dist/esm/charts/common/tree-chrome.d.ts +23 -1
- package/dist/esm/charts/common/tree-interact.d.ts +46 -0
- package/dist/esm/charts/series/glyphs/draw-lines.d.ts +11 -4
- package/dist/esm/charts/series/series-build.d.ts +38 -2
- package/dist/esm/charts/series/series-render.d.ts +1 -4
- package/dist/esm/charts/series/series-type.d.ts +19 -17
- package/dist/esm/charts/series/series.d.ts +16 -0
- package/dist/esm/charts/sunburst/sunburst-interact.d.ts +1 -1
- package/dist/esm/charts/treemap/treemap-interact.d.ts +1 -6
- package/dist/esm/interaction/host-sink-message.d.ts +10 -28
- package/dist/esm/interaction/raw-event-forwarder.d.ts +6 -7
- package/dist/esm/interaction/zoom-controller.d.ts +31 -20
- package/dist/esm/interaction/zoom-router.d.ts +3 -26
- package/dist/esm/perspective-viewer-charts.js +2 -2
- package/dist/esm/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/plugin/plugin.d.ts +0 -1
- package/dist/esm/theme/palette.d.ts +0 -5
- package/dist/esm/transport/protocol.d.ts +2 -7
- package/dist/esm/worker/renderer.worker.d.ts +2 -4
- package/package.json +1 -1
- package/src/ts/axis/bar-axis.ts +74 -45
- package/src/ts/axis/categorical-axis.ts +0 -2
- package/src/ts/charts/candlestick/candlestick-render.ts +10 -7
- package/src/ts/charts/candlestick/candlestick.ts +10 -29
- package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +36 -2
- package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +36 -2
- package/src/ts/charts/cartesian/cartesian-build.ts +143 -9
- package/src/ts/charts/cartesian/cartesian-render.ts +205 -30
- package/src/ts/charts/cartesian/cartesian.ts +43 -4
- package/src/ts/charts/cartesian/glyphs/density.ts +36 -41
- package/src/ts/charts/cartesian/glyphs/lines.ts +13 -15
- package/src/ts/charts/cartesian/glyphs/points.ts +12 -17
- package/src/ts/charts/chart-base.ts +20 -6
- package/src/ts/charts/chart.ts +1 -1
- package/src/ts/charts/common/category-axis-resolver.ts +135 -1
- package/src/ts/charts/common/expand-domain.ts +40 -0
- package/src/ts/charts/common/tree-chart.ts +16 -0
- package/src/ts/charts/common/tree-chrome.ts +86 -1
- package/src/ts/charts/common/tree-interact.ts +209 -0
- package/src/ts/charts/heatmap/heatmap-render.ts +9 -11
- package/src/ts/charts/series/glyphs/draw-areas.ts +30 -1
- package/src/ts/charts/series/glyphs/draw-lines.ts +151 -76
- package/src/ts/charts/series/series-build.ts +394 -21
- package/src/ts/charts/series/series-render.ts +159 -38
- package/src/ts/charts/series/series-type.ts +37 -17
- package/src/ts/charts/series/series.ts +63 -68
- package/src/ts/charts/sunburst/sunburst-interact.ts +18 -162
- package/src/ts/charts/sunburst/sunburst-render.ts +24 -89
- package/src/ts/charts/sunburst/sunburst.ts +1 -15
- package/src/ts/charts/treemap/treemap-interact.ts +22 -189
- package/src/ts/charts/treemap/treemap-render.ts +19 -46
- package/src/ts/charts/treemap/treemap.ts +1 -16
- package/src/ts/interaction/host-sink-message.ts +33 -22
- package/src/ts/interaction/raw-event-forwarder.ts +10 -12
- package/src/ts/interaction/zoom-controller.ts +120 -83
- package/src/ts/interaction/zoom-router.ts +3 -126
- package/src/ts/map/tile-layer.ts +13 -13
- package/src/ts/plugin/plugin.ts +100 -184
- package/src/ts/shaders/line-uniform.frag.glsl +2 -1
- package/src/ts/shaders/line-uniform.vert.glsl +19 -0
- package/src/ts/theme/palette.ts +1 -4
- package/src/ts/transport/protocol.ts +3 -8
- package/src/ts/worker/dispatch.ts +0 -1
- package/src/ts/worker/renderer.worker.ts +10 -46
|
@@ -0,0 +1,209 @@
|
|
|
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 { TreeChartBase } from "./tree-chart";
|
|
14
|
+
import { NULL_NODE, ancestorNames } from "./node-store";
|
|
15
|
+
import { rebuildBreadcrumbs } from "./tree-data";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Common subset of `TreemapChart` / `SunburstChart` reached by the
|
|
19
|
+
* shared interaction helpers — anything that lives on `TreeChartBase`
|
|
20
|
+
* plus the pinned/hover/facet-drill state the two charts both declare
|
|
21
|
+
* with identical shape but on the subclass (so we type it as an
|
|
22
|
+
* intersection).
|
|
23
|
+
*/
|
|
24
|
+
export type TreeInteractChart = TreeChartBase & {
|
|
25
|
+
_pinnedNodeId: number;
|
|
26
|
+
_hoveredNodeId: number;
|
|
27
|
+
_facetDrillRoots: Map<string, number>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Emit `perspective-click` + `perspective-global-filter selected:true`
|
|
32
|
+
* for a treemap/sunburst node. The path is walked via `ancestorNames`
|
|
33
|
+
* and split into split-by prefix + group-by levels using
|
|
34
|
+
* `_splitBy.length` as the boundary; faceted mode keeps the depth-0
|
|
35
|
+
* ancestor as the split prefix.
|
|
36
|
+
*/
|
|
37
|
+
export async function emitTreeNodeEvent(
|
|
38
|
+
chart: TreeInteractChart,
|
|
39
|
+
nodeId: number,
|
|
40
|
+
kind: "leaf" | "branch",
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const store = chart._nodeStore;
|
|
43
|
+
const path = ancestorNames(store, nodeId);
|
|
44
|
+
const isFaceted =
|
|
45
|
+
chart._splitBy.length > 0 && chart._facetConfig.facet_mode === "grid";
|
|
46
|
+
const splitByValues: (string | null)[] = isFaceted
|
|
47
|
+
? path.slice(0, chart._splitBy.length)
|
|
48
|
+
: [];
|
|
49
|
+
const groupByValues: (string | null)[] = isFaceted
|
|
50
|
+
? path.slice(
|
|
51
|
+
chart._splitBy.length,
|
|
52
|
+
chart._splitBy.length + chart._groupBy.length,
|
|
53
|
+
)
|
|
54
|
+
: path.slice(0, chart._groupBy.length);
|
|
55
|
+
|
|
56
|
+
const rowIdx = kind === "leaf" ? (store.leafRowIdx[nodeId] ?? null) : null;
|
|
57
|
+
|
|
58
|
+
await chart.emitClickAndSelect({
|
|
59
|
+
rowIdx: rowIdx != null && rowIdx >= 0 ? rowIdx : null,
|
|
60
|
+
columnName: chart._sizeName,
|
|
61
|
+
groupByValues,
|
|
62
|
+
splitByValues,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build tooltip lines for `nodeId`: ancestor name path + aggregate
|
|
68
|
+
* value + (numeric) color value + per-row tooltip columns from
|
|
69
|
+
* `_lazyRows` for leaves. The leaf branch awaits the source-view row
|
|
70
|
+
* fetch; branch nodes have no underlying row so they emit a Children
|
|
71
|
+
* count instead.
|
|
72
|
+
*/
|
|
73
|
+
export async function buildTreeTooltipLines(
|
|
74
|
+
chart: TreeInteractChart,
|
|
75
|
+
nodeId: number,
|
|
76
|
+
): Promise<string[]> {
|
|
77
|
+
const store = chart._nodeStore;
|
|
78
|
+
const lines: string[] = [];
|
|
79
|
+
|
|
80
|
+
const pathNames: string[] = [];
|
|
81
|
+
let p = nodeId;
|
|
82
|
+
while (store.parent[p] !== NULL_NODE) {
|
|
83
|
+
pathNames.push(store.name[p]);
|
|
84
|
+
p = store.parent[p];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pathNames.reverse();
|
|
88
|
+
if (pathNames.length > 0) {
|
|
89
|
+
lines.push(pathNames.join(" › "));
|
|
90
|
+
} else {
|
|
91
|
+
lines.push(store.name[nodeId]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const sizeFmt = chart.getColumnFormatter(chart._sizeName, "value");
|
|
95
|
+
lines.push(`Value: ${sizeFmt(store.value[nodeId])}`);
|
|
96
|
+
|
|
97
|
+
if (chart._colorName && !isNaN(store.colorValue[nodeId])) {
|
|
98
|
+
const colorFmt = chart.getColumnFormatter(chart._colorName, "value");
|
|
99
|
+
lines.push(
|
|
100
|
+
`${chart._colorName}: ${colorFmt(store.colorValue[nodeId])}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const rowIdx = store.leafRowIdx[nodeId];
|
|
105
|
+
const isLeaf =
|
|
106
|
+
store.firstChild[nodeId] === NULL_NODE && rowIdx !== NULL_NODE;
|
|
107
|
+
|
|
108
|
+
if (isLeaf && chart._lazyRows) {
|
|
109
|
+
const row = await chart._lazyRows.fetchRow(rowIdx);
|
|
110
|
+
for (const [name, value] of row) {
|
|
111
|
+
if (value === null || value === undefined) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (name === chart._colorName && !isNaN(store.colorValue[nodeId])) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (typeof value === "number") {
|
|
120
|
+
lines.push(
|
|
121
|
+
`${name}: ${chart.getColumnFormatter(name, "value")(value)}`,
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
lines.push(`${name}: ${value}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (store.firstChild[nodeId] !== NULL_NODE) {
|
|
130
|
+
lines.push(`Children: ${store.childCount[nodeId]}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return lines;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Pin a tooltip at the chart-supplied anchor. Lines are fetched lazily;
|
|
138
|
+
* the `_pinnedNodeId` check on resolve discards stale results from a
|
|
139
|
+
* prior pin or dismissal.
|
|
140
|
+
*/
|
|
141
|
+
export function showTreePinnedTooltip(
|
|
142
|
+
chart: TreeInteractChart,
|
|
143
|
+
nodeId: number,
|
|
144
|
+
anchor: { cx: number; cy: number },
|
|
145
|
+
renderChromeOverlay: () => void,
|
|
146
|
+
): void {
|
|
147
|
+
chart._tooltip.dismiss();
|
|
148
|
+
chart._pinnedNodeId = nodeId;
|
|
149
|
+
|
|
150
|
+
const cssWidth = chart._glManager?.cssWidth ?? 0;
|
|
151
|
+
const cssHeight = chart._glManager?.cssHeight ?? 0;
|
|
152
|
+
|
|
153
|
+
buildTreeTooltipLines(chart, nodeId).then((lines) => {
|
|
154
|
+
if (chart._pinnedNodeId !== nodeId) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (lines.length === 0) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
chart._tooltip.pin(
|
|
163
|
+
lines,
|
|
164
|
+
{ px: anchor.cx, py: anchor.cy },
|
|
165
|
+
{ cssWidth, cssHeight },
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
chart._hoveredNodeId = NULL_NODE;
|
|
170
|
+
renderChromeOverlay();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function dismissTreePinnedTooltip(chart: TreeInteractChart): void {
|
|
174
|
+
chart._tooltip.dismiss();
|
|
175
|
+
chart._pinnedNodeId = NULL_NODE;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Drill the clicked facet (or the whole chart in non-facet mode).
|
|
180
|
+
* Faceted drill walks up to the facet root (top-level child of
|
|
181
|
+
* `_rootId`), records the new drill node under that facet's label, and
|
|
182
|
+
* re-renders.
|
|
183
|
+
*/
|
|
184
|
+
export function treeDrillTo(
|
|
185
|
+
chart: TreeInteractChart,
|
|
186
|
+
nodeId: number,
|
|
187
|
+
renderFrame: () => void,
|
|
188
|
+
): void {
|
|
189
|
+
const store = chart._nodeStore;
|
|
190
|
+
if (chart._splitBy.length > 0 && chart._facetConfig.facet_mode === "grid") {
|
|
191
|
+
let p = nodeId;
|
|
192
|
+
while (p !== NULL_NODE && store.parent[p] !== chart._rootId) {
|
|
193
|
+
p = store.parent[p];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (p !== NULL_NODE) {
|
|
197
|
+
chart._facetDrillRoots.set(store.name[p], nodeId);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
chart._hoveredNodeId = NULL_NODE;
|
|
201
|
+
renderFrame();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
chart._currentRootId = nodeId;
|
|
206
|
+
rebuildBreadcrumbs(chart, nodeId);
|
|
207
|
+
chart._hoveredNodeId = NULL_NODE;
|
|
208
|
+
renderFrame();
|
|
209
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
withScissor,
|
|
21
21
|
} from "../../webgl/plot-frame";
|
|
22
22
|
import { getInstancing } from "../../webgl/instanced-attrs";
|
|
23
|
+
import { compileProgram } from "../../webgl/program-cache";
|
|
23
24
|
import { initCanvas } from "../../axis/canvas";
|
|
24
25
|
import { buildFacetGrid } from "../../layout/facet-grid";
|
|
25
26
|
import {
|
|
@@ -236,21 +237,18 @@ function ensureProgram(
|
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
const gl = glManager.gl;
|
|
239
|
-
const
|
|
240
|
+
const compiled = compileProgram<
|
|
241
|
+
{ program: WebGLProgram } & NonNullable<HeatmapChart["_locations"]>
|
|
242
|
+
>(
|
|
243
|
+
glManager,
|
|
240
244
|
"heatmap",
|
|
241
245
|
heatmapVert,
|
|
242
246
|
heatmapFrag,
|
|
247
|
+
["u_projection", "u_cell_inset", "u_cell_size", "u_gradient_lut"],
|
|
248
|
+
["a_corner", "a_cell", "a_color_t"],
|
|
243
249
|
);
|
|
244
|
-
chart._program = program;
|
|
245
|
-
chart._locations =
|
|
246
|
-
u_projection: gl.getUniformLocation(program, "u_projection"),
|
|
247
|
-
u_cell_inset: gl.getUniformLocation(program, "u_cell_inset"),
|
|
248
|
-
u_cell_size: gl.getUniformLocation(program, "u_cell_size"),
|
|
249
|
-
u_gradient_lut: gl.getUniformLocation(program, "u_gradient_lut"),
|
|
250
|
-
a_corner: gl.getAttribLocation(program, "a_corner"),
|
|
251
|
-
a_cell: gl.getAttribLocation(program, "a_cell"),
|
|
252
|
-
a_color_t: gl.getAttribLocation(program, "a_color_t"),
|
|
253
|
-
};
|
|
250
|
+
chart._program = compiled.program;
|
|
251
|
+
chart._locations = compiled;
|
|
254
252
|
|
|
255
253
|
const cornerBuffer = gl.createBuffer()!;
|
|
256
254
|
gl.bindBuffer(gl.ARRAY_BUFFER, cornerBuffer);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { WebGLContextManager } from "../../../webgl/context-manager";
|
|
14
14
|
import type { SeriesChart } from "../series";
|
|
15
15
|
import type { SeriesInfo } from "../series-build";
|
|
16
|
+
import type { InterpolateMode } from "../series-type";
|
|
16
17
|
import { compileProgram } from "../../../webgl/program-cache";
|
|
17
18
|
import areaVert from "../../../shaders/area.vert.glsl";
|
|
18
19
|
import areaFrag from "../../../shaders/area.frag.glsl";
|
|
@@ -144,6 +145,7 @@ export class AreaGlyph {
|
|
|
144
145
|
|
|
145
146
|
const entries: AreaSeriesEntry[] = [];
|
|
146
147
|
for (const s of areaSeries) {
|
|
148
|
+
const seriesInfo = chart._series[s.seriesId];
|
|
147
149
|
const strips = collectAreaStrips(
|
|
148
150
|
s,
|
|
149
151
|
N,
|
|
@@ -155,6 +157,9 @@ export class AreaGlyph {
|
|
|
155
157
|
bars.y1,
|
|
156
158
|
positions,
|
|
157
159
|
xOrigin,
|
|
160
|
+
seriesInfo.start,
|
|
161
|
+
seriesInfo.end,
|
|
162
|
+
seriesInfo.interpolateMode,
|
|
158
163
|
);
|
|
159
164
|
if (strips.totalVertices === 0) {
|
|
160
165
|
continue;
|
|
@@ -254,6 +259,21 @@ interface CollectedStrips {
|
|
|
254
259
|
* Reads stacked y0/y1 from the pre-built `barIndex` (cached on the
|
|
255
260
|
* chart at data load) so this hot path doesn't rebuild the map each
|
|
256
261
|
* call.
|
|
262
|
+
*
|
|
263
|
+
* The "present" predicate is mode-aware for the unstacked branch:
|
|
264
|
+
*
|
|
265
|
+
* - `mode = "solid"` (also coerced from `"transparent"` for area):
|
|
266
|
+
* Pass 2 has populated every cell in `[start, end]`, including
|
|
267
|
+
* leading/trailing zero-fills. Treat `c in [start, end]` as
|
|
268
|
+
* present and ignore `sampleValid` (the bit is still 0 at
|
|
269
|
+
* synthesized cells).
|
|
270
|
+
* - `mode = "skip"`: Pass 2 didn't run for this series. Interior
|
|
271
|
+
* nulls inside `[start, end]` remain and the strip must break at
|
|
272
|
+
* them — use `sampleValid` as the "is present" check, matching
|
|
273
|
+
* the pre-feature behavior.
|
|
274
|
+
*
|
|
275
|
+
* The stacked branch is unchanged: the `barIndex` lookup already
|
|
276
|
+
* encodes presence post-stacking.
|
|
257
277
|
*/
|
|
258
278
|
function collectAreaStrips(
|
|
259
279
|
s: SeriesInfo,
|
|
@@ -266,10 +286,14 @@ function collectAreaStrips(
|
|
|
266
286
|
barY1: Float64Array,
|
|
267
287
|
positions: Float64Array | null,
|
|
268
288
|
xOrigin: number,
|
|
289
|
+
seriesStart: number,
|
|
290
|
+
seriesEnd: number,
|
|
291
|
+
interpolateMode: InterpolateMode,
|
|
269
292
|
): CollectedStrips {
|
|
270
293
|
const scratch = ensureStripScratch(N * 4);
|
|
271
294
|
const descriptors: AreaStrip[] = [];
|
|
272
295
|
const seriesBase = s.seriesId * 1_000_000_000;
|
|
296
|
+
const trustRange = interpolateMode !== "skip";
|
|
273
297
|
|
|
274
298
|
let write = 0;
|
|
275
299
|
let runStart = 0;
|
|
@@ -288,7 +312,12 @@ function collectAreaStrips(
|
|
|
288
312
|
}
|
|
289
313
|
} else {
|
|
290
314
|
const idx = c * S + s.seriesId;
|
|
291
|
-
if (
|
|
315
|
+
if (trustRange) {
|
|
316
|
+
if (c >= seriesStart && c <= seriesEnd) {
|
|
317
|
+
top = samples[idx];
|
|
318
|
+
present = true;
|
|
319
|
+
}
|
|
320
|
+
} else if ((valid[idx >> 3] >> (idx & 7)) & 1) {
|
|
292
321
|
top = samples[idx];
|
|
293
322
|
present = true;
|
|
294
323
|
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { compileProgram } from "../../../webgl/program-cache";
|
|
20
20
|
import lineVert from "../../../shaders/line-uniform.vert.glsl";
|
|
21
21
|
import lineFrag from "../../../shaders/line-uniform.frag.glsl";
|
|
22
|
+
import type { InterpolateMode } from "../series-type";
|
|
22
23
|
|
|
23
24
|
type GL = WebGL2RenderingContext | WebGLRenderingContext;
|
|
24
25
|
|
|
@@ -29,21 +30,12 @@ interface LineProgramCache {
|
|
|
29
30
|
u_color: WebGLUniformLocation | null;
|
|
30
31
|
u_resolution: WebGLUniformLocation | null;
|
|
31
32
|
u_line_width: WebGLUniformLocation | null;
|
|
33
|
+
u_interp_alpha: WebGLUniformLocation | null;
|
|
32
34
|
a_start: number;
|
|
33
35
|
a_end: number;
|
|
34
36
|
a_corner: number;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
interface LineRun {
|
|
38
|
-
/**
|
|
39
|
-
* Byte offset into the per-series GPU buffer at the start of this run.
|
|
40
|
-
*/
|
|
41
|
-
offsetBytes: number;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Number of points in this run; the run draws `count - 1` segments.
|
|
45
|
-
*/
|
|
46
|
-
count: number;
|
|
37
|
+
a_real_start: number;
|
|
38
|
+
a_real_end: number;
|
|
47
39
|
}
|
|
48
40
|
|
|
49
41
|
interface LineSeriesEntry {
|
|
@@ -52,14 +44,31 @@ interface LineSeriesEntry {
|
|
|
52
44
|
color: [number, number, number];
|
|
53
45
|
|
|
54
46
|
/**
|
|
55
|
-
* GPU buffer holding `[x0,y0,x1,y1,...]` for
|
|
47
|
+
* GPU buffer holding `[x0,y0,x1,y1,...]` for cats `[start, end]`.
|
|
56
48
|
*/
|
|
57
49
|
gpuBuffer: WebGLBuffer;
|
|
58
50
|
|
|
59
51
|
/**
|
|
60
|
-
*
|
|
52
|
+
* GPU buffer of per-vertex real-flag bytes (1 = real, 0 = synthesized).
|
|
53
|
+
* Bound twice as `a_real_start` / `a_real_end` with overlapping
|
|
54
|
+
* byte offsets so the segment shader sees both endpoints' flags.
|
|
55
|
+
*/
|
|
56
|
+
gpuRealBuffer: WebGLBuffer;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Number of points = `end - start + 1`. Series draws `count - 1`
|
|
60
|
+
* segments. The renderer always emits a single contiguous run;
|
|
61
|
+
* gap rendering for skip mode happens in the shader via
|
|
62
|
+
* `u_interp_alpha`.
|
|
61
63
|
*/
|
|
62
|
-
|
|
64
|
+
count: number;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Interpolation mode for this series. Drives `u_interp_alpha` at
|
|
68
|
+
* draw time. Same value the build pipeline resolved via
|
|
69
|
+
* `resolveInterpolate`.
|
|
70
|
+
*/
|
|
71
|
+
interpolateMode: InterpolateMode;
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
/**
|
|
@@ -82,6 +91,7 @@ interface LineBuffers {
|
|
|
82
91
|
* pattern.
|
|
83
92
|
*/
|
|
84
93
|
let _lineScratch: Float32Array = new Float32Array(0);
|
|
94
|
+
let _realScratch: Uint8Array = new Uint8Array(0);
|
|
85
95
|
|
|
86
96
|
function ensureLineScratch(n: number): Float32Array {
|
|
87
97
|
if (_lineScratch.length >= n) {
|
|
@@ -92,6 +102,27 @@ function ensureLineScratch(n: number): Float32Array {
|
|
|
92
102
|
return _lineScratch;
|
|
93
103
|
}
|
|
94
104
|
|
|
105
|
+
function ensureRealScratch(n: number): Uint8Array {
|
|
106
|
+
if (_realScratch.length >= n) {
|
|
107
|
+
return _realScratch;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_realScratch = new Uint8Array(Math.max(n, _realScratch.length * 2));
|
|
111
|
+
return _realScratch;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function alphaForMode(mode: InterpolateMode): number {
|
|
115
|
+
if (mode === "solid") {
|
|
116
|
+
return 1.0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (mode === "transparent") {
|
|
120
|
+
return 0.5;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return 0.0;
|
|
124
|
+
}
|
|
125
|
+
|
|
95
126
|
/**
|
|
96
127
|
* Line glyph for {@link SeriesChart}. Owns its program + per-series
|
|
97
128
|
* GPU buffers privately; chart routes lifecycle through
|
|
@@ -112,8 +143,14 @@ export class LineGlyph {
|
|
|
112
143
|
"bar-line",
|
|
113
144
|
lineVert,
|
|
114
145
|
lineFrag,
|
|
115
|
-
[
|
|
116
|
-
|
|
146
|
+
[
|
|
147
|
+
"u_projection",
|
|
148
|
+
"u_color",
|
|
149
|
+
"u_resolution",
|
|
150
|
+
"u_line_width",
|
|
151
|
+
"u_interp_alpha",
|
|
152
|
+
],
|
|
153
|
+
["a_start", "a_end", "a_corner", "a_real_start", "a_real_end"],
|
|
117
154
|
);
|
|
118
155
|
this._program = { ...partial, cornerBuffer };
|
|
119
156
|
return this._program;
|
|
@@ -133,6 +170,7 @@ export class LineGlyph {
|
|
|
133
170
|
const gl = chart._glManager.gl;
|
|
134
171
|
for (const s of buf.series) {
|
|
135
172
|
gl.deleteBuffer(s.gpuBuffer);
|
|
173
|
+
gl.deleteBuffer(s.gpuRealBuffer);
|
|
136
174
|
}
|
|
137
175
|
|
|
138
176
|
this._buffers = null;
|
|
@@ -142,9 +180,14 @@ export class LineGlyph {
|
|
|
142
180
|
* Rebuild the per-series GPU buffers for line glyphs. Called once
|
|
143
181
|
* per data load (and once after `restyle()` because palette colors
|
|
144
182
|
* are captured on the {@link LineSeriesEntry}). The buffer contents
|
|
145
|
-
* encode `[x,y]` points
|
|
146
|
-
* series. After this, every `draw` call rebinds +
|
|
147
|
-
* no further uploads until the next data load.
|
|
183
|
+
* encode `[x,y]` points for every cat in `[start, end]`; one
|
|
184
|
+
* `bufferData` per series. After this, every `draw` call rebinds +
|
|
185
|
+
* dispatches with no further uploads until the next data load.
|
|
186
|
+
*
|
|
187
|
+
* Gap behavior at synthesized cells is handled in the shader via
|
|
188
|
+
* `u_interp_alpha` (set per draw based on the series'
|
|
189
|
+
* `interpolateMode`): `skip` → 0 (invisible segments touching a
|
|
190
|
+
* synthesized endpoint), `solid` → 1, `transparent` → 0.5.
|
|
148
191
|
*/
|
|
149
192
|
rebuildBuffers(chart: SeriesChart, glManager: WebGLContextManager): void {
|
|
150
193
|
const lineSeries = chart._lineSeries;
|
|
@@ -169,46 +212,44 @@ export class LineGlyph {
|
|
|
169
212
|
|
|
170
213
|
const entries: LineSeriesEntry[] = [];
|
|
171
214
|
for (const s of lineSeries) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
let write = 0;
|
|
178
|
-
let runStart = 0;
|
|
179
|
-
for (let c = 0; c < N; c++) {
|
|
180
|
-
const idx = c * S + s.seriesId;
|
|
181
|
-
const ok = (valid[idx >> 3] >> (idx & 7)) & 1;
|
|
182
|
-
if (ok) {
|
|
183
|
-
const x = positions ? positions[c] - xOrigin : c;
|
|
184
|
-
scratch[write++] = x;
|
|
185
|
-
scratch[write++] = samples[idx];
|
|
186
|
-
} else if (write > runStart) {
|
|
187
|
-
const count = (write - runStart) / 2;
|
|
188
|
-
if (count >= 2) {
|
|
189
|
-
runs.push({ offsetBytes: runStart * 4, count });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
runStart = write;
|
|
193
|
-
}
|
|
215
|
+
const seriesInfo = chart._series[s.seriesId];
|
|
216
|
+
const start = seriesInfo.start;
|
|
217
|
+
const end = seriesInfo.end;
|
|
218
|
+
if (start < 0 || end < start) {
|
|
219
|
+
continue;
|
|
194
220
|
}
|
|
195
221
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
222
|
+
const count = end - start + 1;
|
|
223
|
+
if (count < 2) {
|
|
224
|
+
// A 1-point "line" has no segments to draw.
|
|
225
|
+
continue;
|
|
201
226
|
}
|
|
202
227
|
|
|
203
|
-
|
|
204
|
-
|
|
228
|
+
const posScratch = ensureLineScratch(count * 2);
|
|
229
|
+
const realScratch = ensureRealScratch(count);
|
|
230
|
+
let write = 0;
|
|
231
|
+
for (let c = start; c <= end; c++) {
|
|
232
|
+
const x = positions ? positions[c] - xOrigin : c;
|
|
233
|
+
const idx = c * S + s.seriesId;
|
|
234
|
+
posScratch[write * 2] = x;
|
|
235
|
+
posScratch[write * 2 + 1] = samples[idx];
|
|
236
|
+
realScratch[write] = (valid[idx >> 3] >> (idx & 7)) & 1;
|
|
237
|
+
write++;
|
|
205
238
|
}
|
|
206
239
|
|
|
207
|
-
const
|
|
208
|
-
gl.bindBuffer(gl.ARRAY_BUFFER,
|
|
240
|
+
const posBuf = gl.createBuffer()!;
|
|
241
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
|
|
209
242
|
gl.bufferData(
|
|
210
243
|
gl.ARRAY_BUFFER,
|
|
211
|
-
|
|
244
|
+
posScratch.subarray(0, write * 2),
|
|
245
|
+
gl.STATIC_DRAW,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const realBuf = gl.createBuffer()!;
|
|
249
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, realBuf);
|
|
250
|
+
gl.bufferData(
|
|
251
|
+
gl.ARRAY_BUFFER,
|
|
252
|
+
realScratch.subarray(0, write),
|
|
212
253
|
gl.STATIC_DRAW,
|
|
213
254
|
);
|
|
214
255
|
|
|
@@ -216,8 +257,10 @@ export class LineGlyph {
|
|
|
216
257
|
seriesId: s.seriesId,
|
|
217
258
|
axis: s.axis,
|
|
218
259
|
color: [s.color[0], s.color[1], s.color[2]],
|
|
219
|
-
gpuBuffer:
|
|
220
|
-
|
|
260
|
+
gpuBuffer: posBuf,
|
|
261
|
+
gpuRealBuffer: realBuf,
|
|
262
|
+
count,
|
|
263
|
+
interpolateMode: seriesInfo.interpolateMode,
|
|
221
264
|
});
|
|
222
265
|
}
|
|
223
266
|
|
|
@@ -226,7 +269,9 @@ export class LineGlyph {
|
|
|
226
269
|
|
|
227
270
|
/**
|
|
228
271
|
* Bind the persistent vertex buffers and dispatch one instanced draw
|
|
229
|
-
* per
|
|
272
|
+
* per series. Skips hidden series via `_hiddenSeries`. Gap /
|
|
273
|
+
* transparency rendering is governed by `u_interp_alpha`, set per
|
|
274
|
+
* series.
|
|
230
275
|
*/
|
|
231
276
|
draw(
|
|
232
277
|
chart: SeriesChart,
|
|
@@ -257,14 +302,14 @@ export class LineGlyph {
|
|
|
257
302
|
gl.vertexAttribPointer(cache.a_corner, 1, gl.FLOAT, false, 0, 0);
|
|
258
303
|
setDivisor(cache.a_corner, 0);
|
|
259
304
|
|
|
260
|
-
const
|
|
305
|
+
const posStride = 2 * Float32Array.BYTES_PER_ELEMENT;
|
|
306
|
+
const realStride = Uint8Array.BYTES_PER_ELEMENT;
|
|
261
307
|
const hidden = chart._hiddenSeries;
|
|
262
308
|
for (const s of buf.series) {
|
|
263
309
|
if (hidden.has(s.seriesId)) {
|
|
264
310
|
continue;
|
|
265
311
|
}
|
|
266
312
|
|
|
267
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, s.gpuBuffer);
|
|
268
313
|
gl.uniformMatrix4fv(
|
|
269
314
|
cache.u_projection,
|
|
270
315
|
false,
|
|
@@ -273,35 +318,65 @@ export class LineGlyph {
|
|
|
273
318
|
|
|
274
319
|
const color = chart._series[s.seriesId].color;
|
|
275
320
|
gl.uniform4f(cache.u_color, color[0], color[1], color[2], 1.0);
|
|
321
|
+
gl.uniform1f(cache.u_interp_alpha, alphaForMode(s.interpolateMode));
|
|
276
322
|
|
|
277
323
|
gl.enableVertexAttribArray(cache.a_start);
|
|
278
324
|
setDivisor(cache.a_start, 1);
|
|
279
325
|
gl.enableVertexAttribArray(cache.a_end);
|
|
280
326
|
setDivisor(cache.a_end, 1);
|
|
327
|
+
gl.enableVertexAttribArray(cache.a_real_start);
|
|
328
|
+
setDivisor(cache.a_real_start, 1);
|
|
329
|
+
gl.enableVertexAttribArray(cache.a_real_end);
|
|
330
|
+
setDivisor(cache.a_real_end, 1);
|
|
281
331
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
332
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, s.gpuBuffer);
|
|
333
|
+
gl.vertexAttribPointer(
|
|
334
|
+
cache.a_start,
|
|
335
|
+
2,
|
|
336
|
+
gl.FLOAT,
|
|
337
|
+
false,
|
|
338
|
+
posStride,
|
|
339
|
+
0,
|
|
340
|
+
);
|
|
341
|
+
gl.vertexAttribPointer(
|
|
342
|
+
cache.a_end,
|
|
343
|
+
2,
|
|
344
|
+
gl.FLOAT,
|
|
345
|
+
false,
|
|
346
|
+
posStride,
|
|
347
|
+
posStride,
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// Bind the real-flag buffer twice with offsets 0 and 1 byte
|
|
351
|
+
// — same overlap trick as the position buffer. `normalized
|
|
352
|
+
// = false` makes the byte value cast directly to float
|
|
353
|
+
// (0 → 0.0, 1 → 1.0) so the shader's `step(0.5, bothReal)`
|
|
354
|
+
// cleanly discriminates real vs synthesized endpoints.
|
|
355
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, s.gpuRealBuffer);
|
|
356
|
+
gl.vertexAttribPointer(
|
|
357
|
+
cache.a_real_start,
|
|
358
|
+
1,
|
|
359
|
+
gl.UNSIGNED_BYTE,
|
|
360
|
+
false,
|
|
361
|
+
realStride,
|
|
362
|
+
0,
|
|
363
|
+
);
|
|
364
|
+
gl.vertexAttribPointer(
|
|
365
|
+
cache.a_real_end,
|
|
366
|
+
1,
|
|
367
|
+
gl.UNSIGNED_BYTE,
|
|
368
|
+
false,
|
|
369
|
+
realStride,
|
|
370
|
+
realStride,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, s.count - 1);
|
|
301
374
|
}
|
|
302
375
|
|
|
303
376
|
setDivisor(cache.a_start, 0);
|
|
304
377
|
setDivisor(cache.a_end, 0);
|
|
378
|
+
setDivisor(cache.a_real_start, 0);
|
|
379
|
+
setDivisor(cache.a_real_end, 0);
|
|
305
380
|
}
|
|
306
381
|
|
|
307
382
|
destroy(chart: SeriesChart): void {
|