@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.
Files changed (72) hide show
  1. package/dist/cdn/perspective-viewer-charts.js +2 -2
  2. package/dist/cdn/perspective-viewer-charts.js.map +3 -3
  3. package/dist/esm/axis/bar-axis.d.ts +9 -1
  4. package/dist/esm/axis/categorical-axis.d.ts +0 -2
  5. package/dist/esm/charts/cartesian/cartesian.d.ts +26 -0
  6. package/dist/esm/charts/common/category-axis-resolver.d.ts +43 -1
  7. package/dist/esm/charts/common/expand-domain.d.ts +20 -0
  8. package/dist/esm/charts/common/tree-chart.d.ts +7 -0
  9. package/dist/esm/charts/common/tree-chrome.d.ts +23 -1
  10. package/dist/esm/charts/common/tree-interact.d.ts +46 -0
  11. package/dist/esm/charts/series/glyphs/draw-lines.d.ts +11 -4
  12. package/dist/esm/charts/series/series-build.d.ts +38 -2
  13. package/dist/esm/charts/series/series-render.d.ts +1 -4
  14. package/dist/esm/charts/series/series-type.d.ts +19 -17
  15. package/dist/esm/charts/series/series.d.ts +16 -0
  16. package/dist/esm/charts/sunburst/sunburst-interact.d.ts +1 -1
  17. package/dist/esm/charts/treemap/treemap-interact.d.ts +1 -6
  18. package/dist/esm/interaction/host-sink-message.d.ts +10 -28
  19. package/dist/esm/interaction/raw-event-forwarder.d.ts +6 -7
  20. package/dist/esm/interaction/zoom-controller.d.ts +31 -20
  21. package/dist/esm/interaction/zoom-router.d.ts +3 -26
  22. package/dist/esm/perspective-viewer-charts.js +2 -2
  23. package/dist/esm/perspective-viewer-charts.js.map +3 -3
  24. package/dist/esm/plugin/plugin.d.ts +0 -1
  25. package/dist/esm/theme/palette.d.ts +0 -5
  26. package/dist/esm/transport/protocol.d.ts +2 -7
  27. package/dist/esm/worker/renderer.worker.d.ts +2 -4
  28. package/package.json +1 -1
  29. package/src/ts/axis/bar-axis.ts +74 -45
  30. package/src/ts/axis/categorical-axis.ts +0 -2
  31. package/src/ts/charts/candlestick/candlestick-render.ts +10 -7
  32. package/src/ts/charts/candlestick/candlestick.ts +10 -29
  33. package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +36 -2
  34. package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +36 -2
  35. package/src/ts/charts/cartesian/cartesian-build.ts +143 -9
  36. package/src/ts/charts/cartesian/cartesian-render.ts +205 -30
  37. package/src/ts/charts/cartesian/cartesian.ts +43 -4
  38. package/src/ts/charts/cartesian/glyphs/density.ts +36 -41
  39. package/src/ts/charts/cartesian/glyphs/lines.ts +13 -15
  40. package/src/ts/charts/cartesian/glyphs/points.ts +12 -17
  41. package/src/ts/charts/chart-base.ts +20 -6
  42. package/src/ts/charts/chart.ts +1 -1
  43. package/src/ts/charts/common/category-axis-resolver.ts +135 -1
  44. package/src/ts/charts/common/expand-domain.ts +40 -0
  45. package/src/ts/charts/common/tree-chart.ts +16 -0
  46. package/src/ts/charts/common/tree-chrome.ts +86 -1
  47. package/src/ts/charts/common/tree-interact.ts +209 -0
  48. package/src/ts/charts/heatmap/heatmap-render.ts +9 -11
  49. package/src/ts/charts/series/glyphs/draw-areas.ts +30 -1
  50. package/src/ts/charts/series/glyphs/draw-lines.ts +151 -76
  51. package/src/ts/charts/series/series-build.ts +394 -21
  52. package/src/ts/charts/series/series-render.ts +159 -38
  53. package/src/ts/charts/series/series-type.ts +37 -17
  54. package/src/ts/charts/series/series.ts +63 -68
  55. package/src/ts/charts/sunburst/sunburst-interact.ts +18 -162
  56. package/src/ts/charts/sunburst/sunburst-render.ts +24 -89
  57. package/src/ts/charts/sunburst/sunburst.ts +1 -15
  58. package/src/ts/charts/treemap/treemap-interact.ts +22 -189
  59. package/src/ts/charts/treemap/treemap-render.ts +19 -46
  60. package/src/ts/charts/treemap/treemap.ts +1 -16
  61. package/src/ts/interaction/host-sink-message.ts +33 -22
  62. package/src/ts/interaction/raw-event-forwarder.ts +10 -12
  63. package/src/ts/interaction/zoom-controller.ts +120 -83
  64. package/src/ts/interaction/zoom-router.ts +3 -126
  65. package/src/ts/map/tile-layer.ts +13 -13
  66. package/src/ts/plugin/plugin.ts +100 -184
  67. package/src/ts/shaders/line-uniform.frag.glsl +2 -1
  68. package/src/ts/shaders/line-uniform.vert.glsl +19 -0
  69. package/src/ts/theme/palette.ts +1 -4
  70. package/src/ts/transport/protocol.ts +3 -8
  71. package/src/ts/worker/dispatch.ts +0 -1
  72. 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 program = glManager.shaders.getOrCreate(
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 ((valid[idx >> 3] >> (idx & 7)) & 1) {
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 every run in the series.
47
+ * GPU buffer holding `[x0,y0,x1,y1,...]` for cats `[start, end]`.
56
48
  */
57
49
  gpuBuffer: WebGLBuffer;
58
50
 
59
51
  /**
60
- * Run offsets into `gpuBuffer`. Empty when the series has no segments.
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
- runs: LineRun[];
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
- ["u_projection", "u_color", "u_resolution", "u_line_width"],
116
- ["a_start", "a_end", "a_corner"],
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 in run-major order; one `bufferData` per
146
- * series. After this, every `draw` call rebinds + dispatches with
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
- // Walk the per-category sample grid for this series, breaking
173
- // into contiguous valid runs. Write directly into a pre-sized
174
- // Float32 scratch — no boxed JS arrays, no `Float32Array.from`.
175
- const scratch = ensureLineScratch(N * 2);
176
- const runs: LineRun[] = [];
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
- if (write > runStart) {
197
- const count = (write - runStart) / 2;
198
- if (count >= 2) {
199
- runs.push({ offsetBytes: runStart * 4, count });
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
- if (runs.length === 0) {
204
- continue;
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 buf = gl.createBuffer()!;
208
- gl.bindBuffer(gl.ARRAY_BUFFER, buf);
240
+ const posBuf = gl.createBuffer()!;
241
+ gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
209
242
  gl.bufferData(
210
243
  gl.ARRAY_BUFFER,
211
- scratch.subarray(0, write),
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: buf,
220
- runs,
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 (series, run). Skips hidden series via `_hiddenSeries`.
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 stride = 2 * Float32Array.BYTES_PER_ELEMENT;
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
- for (const run of s.runs) {
283
- gl.vertexAttribPointer(
284
- cache.a_start,
285
- 2,
286
- gl.FLOAT,
287
- false,
288
- stride,
289
- run.offsetBytes,
290
- );
291
- gl.vertexAttribPointer(
292
- cache.a_end,
293
- 2,
294
- gl.FLOAT,
295
- false,
296
- stride,
297
- run.offsetBytes + stride,
298
- );
299
- drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, run.count - 1);
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 {