gladly-plot 0.0.5 → 0.0.7

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 (44) hide show
  1. package/README.md +9 -2
  2. package/package.json +10 -11
  3. package/src/axes/Axis.js +320 -172
  4. package/src/axes/AxisLink.js +6 -2
  5. package/src/axes/AxisRegistry.js +116 -39
  6. package/src/axes/Camera.js +47 -0
  7. package/src/axes/ColorAxisRegistry.js +10 -2
  8. package/src/axes/FilterAxisRegistry.js +1 -1
  9. package/src/axes/TickLabelAtlas.js +99 -0
  10. package/src/axes/ZoomController.js +446 -124
  11. package/src/colorscales/ColorscaleRegistry.js +30 -10
  12. package/src/compute/ComputationRegistry.js +126 -184
  13. package/src/compute/axisFilter.js +21 -9
  14. package/src/compute/conv.js +64 -8
  15. package/src/compute/elementwise.js +72 -0
  16. package/src/compute/fft.js +106 -20
  17. package/src/compute/filter.js +105 -103
  18. package/src/compute/hist.js +247 -142
  19. package/src/compute/kde.js +64 -46
  20. package/src/compute/scatter2dInterpolate.js +277 -0
  21. package/src/compute/util.js +196 -0
  22. package/src/core/ComputePipeline.js +153 -0
  23. package/src/core/GlBase.js +141 -0
  24. package/src/core/Layer.js +22 -8
  25. package/src/core/LayerType.js +253 -92
  26. package/src/core/Plot.js +644 -162
  27. package/src/core/PlotGroup.js +204 -0
  28. package/src/core/ShaderQueue.js +73 -0
  29. package/src/data/ColumnData.js +269 -0
  30. package/src/data/Computation.js +95 -0
  31. package/src/data/Data.js +270 -0
  32. package/src/floats/Float.js +56 -0
  33. package/src/index.js +16 -4
  34. package/src/layers/BarsLayer.js +168 -0
  35. package/src/layers/ColorbarLayer.js +10 -14
  36. package/src/layers/ColorbarLayer2d.js +13 -24
  37. package/src/layers/FilterbarLayer.js +4 -3
  38. package/src/layers/LinesLayer.js +108 -122
  39. package/src/layers/PointsLayer.js +73 -69
  40. package/src/layers/ScatterShared.js +62 -106
  41. package/src/layers/TileLayer.js +20 -16
  42. package/src/math/mat4.js +100 -0
  43. package/src/core/Data.js +0 -67
  44. package/src/layers/HistogramLayer.js +0 -212
@@ -0,0 +1,277 @@
1
+ import { registerComputedData, EXPRESSION_REF } from "./ComputationRegistry.js"
2
+ import { ComputedData } from "../data/Computation.js"
3
+ import { ArrayColumn, uploadToTexture, SAMPLE_COLUMN_GLSL } from "../data/ColumnData.js"
4
+
5
+ function colDomain(col) {
6
+ if (col instanceof ArrayColumn) {
7
+ const arr = col.array
8
+ let min = arr[0], max = arr[0]
9
+ for (let i = 1; i < arr.length; i++) {
10
+ if (arr[i] < min) min = arr[i]
11
+ if (arr[i] > max) max = arr[i]
12
+ }
13
+ return [min, max]
14
+ }
15
+ return col.domain ?? [0, 1]
16
+ }
17
+
18
+ function makeSplatPass(regl, W, H, pickIds, xTex, yTex, valueTex, xDomain, yDomain, radius) {
19
+ const accTex = regl.texture({ width: W, height: H, type: 'float', format: 'rgba' })
20
+ const accFBO = regl.framebuffer({ color: accTex, depth: false, stencil: false })
21
+ regl.clear({ color: [0, 0, 0, 0], framebuffer: accFBO })
22
+
23
+ const N = pickIds.length
24
+ const pointSize = radius * 2.0 + 1.0
25
+
26
+ const drawSplat = regl({
27
+ framebuffer: accFBO,
28
+ blend: { enable: true, func: { src: 'one', dst: 'one' } },
29
+ vert: `#version 300 es
30
+ precision highp float;
31
+ precision highp sampler2D;
32
+ in float a_pickId;
33
+ uniform sampler2D u_xTex, u_yTex, u_valueTex;
34
+ uniform float u_xMin, u_xMax, u_yMin, u_yMax;
35
+ ${SAMPLE_COLUMN_GLSL}
36
+ out float v_value;
37
+ void main() {
38
+ float xVal = sampleColumn(u_xTex, a_pickId);
39
+ float yVal = sampleColumn(u_yTex, a_pickId);
40
+ v_value = sampleColumn(u_valueTex, a_pickId);
41
+ float ndcX = (xVal - u_xMin) / max(u_xMax - u_xMin, 1e-10) * 2.0 - 1.0;
42
+ float ndcY = (yVal - u_yMin) / max(u_yMax - u_yMin, 1e-10) * 2.0 - 1.0;
43
+ gl_Position = vec4(ndcX, ndcY, 0.0, 1.0);
44
+ gl_PointSize = ${pointSize.toFixed(2)};
45
+ }`,
46
+ frag: `#version 300 es
47
+ precision highp float;
48
+ in float v_value;
49
+ out vec4 fragColor;
50
+ void main() {
51
+ vec2 pc = gl_PointCoord - 0.5;
52
+ float d = length(pc) * ${pointSize.toFixed(2)};
53
+ if (d > ${radius.toFixed(2)}) discard;
54
+ float w = exp(-0.5 * (d / ${radius.toFixed(2)}) * (d / ${radius.toFixed(2)}));
55
+ fragColor = vec4(v_value * w, 0.0, 0.0, w);
56
+ }`,
57
+ attributes: { a_pickId: pickIds },
58
+ uniforms: {
59
+ u_xTex: xTex,
60
+ u_yTex: yTex,
61
+ u_valueTex: valueTex,
62
+ u_xMin: xDomain[0], u_xMax: xDomain[1],
63
+ u_yMin: yDomain[0], u_yMax: yDomain[1],
64
+ },
65
+ count: N,
66
+ primitive: 'points'
67
+ })
68
+
69
+ drawSplat()
70
+ return accTex
71
+ }
72
+
73
+ function makeValueTexture(regl, W, H, accum1, accum2, accum3, w1, w2, w3) {
74
+ const totalN = W * H
75
+ const nTexels = Math.ceil(totalN / 4)
76
+ const outW = Math.min(nTexels, regl.limits.maxTextureSize)
77
+ const outH = Math.ceil(nTexels / outW)
78
+
79
+ const outTex = regl.texture({ width: outW, height: outH, type: 'float', format: 'rgba' })
80
+ const outFBO = regl.framebuffer({ color: outTex, depth: false, stencil: false })
81
+
82
+ regl({
83
+ framebuffer: outFBO,
84
+ vert: `#version 300 es
85
+ precision highp float;
86
+ in vec2 a_position;
87
+ void main() { gl_Position = vec4(a_position, 0.0, 1.0); }`,
88
+ frag: `#version 300 es
89
+ precision highp float;
90
+ precision highp sampler2D;
91
+ uniform sampler2D u_accum1, u_accum2, u_accum3;
92
+ uniform float u_w1, u_w2, u_w3;
93
+ out vec4 fragColor;
94
+
95
+ float sampleInterp(sampler2D accum, int linearIdx) {
96
+ if (linearIdx >= ${totalN}) return 0.0;
97
+ int px = linearIdx % ${W};
98
+ int py = linearIdx / ${W};
99
+ vec4 a = texelFetch(accum, ivec2(px, py), 0);
100
+ if (a.a < 1e-6) return 0.0;
101
+ return a.r / a.a;
102
+ }
103
+
104
+ float combine(int idx) {
105
+ return u_w1 * sampleInterp(u_accum1, idx)
106
+ + u_w2 * sampleInterp(u_accum2, idx)
107
+ + u_w3 * sampleInterp(u_accum3, idx);
108
+ }
109
+
110
+ void main() {
111
+ int texelI = int(gl_FragCoord.y) * ${outW} + int(gl_FragCoord.x);
112
+ int base = texelI * 4;
113
+ fragColor = vec4(
114
+ combine(base + 0),
115
+ combine(base + 1),
116
+ combine(base + 2),
117
+ combine(base + 3)
118
+ );
119
+ }`,
120
+ attributes: { a_position: [[-1, -1], [1, -1], [-1, 1], [1, 1]] },
121
+ uniforms: {
122
+ u_accum1: accum1,
123
+ u_accum2: accum2,
124
+ u_accum3: accum3,
125
+ u_w1: w1, u_w2: w2, u_w3: w3,
126
+ },
127
+ count: 4,
128
+ primitive: 'triangle strip'
129
+ })()
130
+
131
+ outTex._dataLength = totalN
132
+ outTex._dataShape = [W, H]
133
+ return outTex
134
+ }
135
+
136
+ function makeCoordTexturesCorners(regl, xDomain, yDomain) {
137
+ // Four corners in row-major [2,2] order: index = iy*2 + ix
138
+ // (ix=0,iy=0)=xMin/yMin (ix=1,iy=0)=xMax/yMin
139
+ // (ix=0,iy=1)=xMin/yMax (ix=1,iy=1)=xMax/yMax
140
+ const xArr = new Float32Array([xDomain[0], xDomain[1], xDomain[0], xDomain[1]])
141
+ const yArr = new Float32Array([yDomain[0], yDomain[0], yDomain[1], yDomain[1]])
142
+ const xTex = uploadToTexture(regl, xArr)
143
+ const yTex = uploadToTexture(regl, yArr)
144
+ xTex._dataShape = [2, 2]
145
+ yTex._dataShape = [2, 2]
146
+ return { xTex, yTex }
147
+ }
148
+
149
+ function makeCoordTexturesFull(regl, W, H, xDomain, yDomain) {
150
+ const totalN = W * H
151
+ const nTexels = Math.ceil(totalN / 4)
152
+ const outW = Math.min(nTexels, regl.limits.maxTextureSize)
153
+ const outH = Math.ceil(nTexels / outW)
154
+
155
+ const makeCoordTex = (axis) => {
156
+ const outTex = regl.texture({ width: outW, height: outH, type: 'float', format: 'rgba' })
157
+ const outFBO = regl.framebuffer({ color: outTex, depth: false, stencil: false })
158
+
159
+ regl({
160
+ framebuffer: outFBO,
161
+ vert: `#version 300 es
162
+ precision highp float;
163
+ in vec2 a_position;
164
+ void main() { gl_Position = vec4(a_position, 0.0, 1.0); }`,
165
+ frag: `#version 300 es
166
+ precision highp float;
167
+ out vec4 fragColor;
168
+ float coordAt(int linearIdx) {
169
+ if (linearIdx >= ${totalN}) return 0.0;
170
+ int px = linearIdx % ${W};
171
+ int py = linearIdx / ${W};
172
+ ${axis === 'x'
173
+ ? `return ${xDomain[0].toFixed(10)} + (float(px) + 0.5) / float(${W}) * float(${(xDomain[1] - xDomain[0]).toFixed(10)});`
174
+ : `return ${yDomain[0].toFixed(10)} + (float(py) + 0.5) / float(${H}) * float(${(yDomain[1] - yDomain[0]).toFixed(10)});`
175
+ }
176
+ }
177
+ void main() {
178
+ int texelI = int(gl_FragCoord.y) * ${outW} + int(gl_FragCoord.x);
179
+ int base = texelI * 4;
180
+ fragColor = vec4(
181
+ coordAt(base + 0),
182
+ coordAt(base + 1),
183
+ coordAt(base + 2),
184
+ coordAt(base + 3)
185
+ );
186
+ }`,
187
+ attributes: { a_position: [[-1, -1], [1, -1], [-1, 1], [1, 1]] },
188
+ uniforms: {},
189
+ count: 4,
190
+ primitive: 'triangle strip'
191
+ })()
192
+
193
+ outTex._dataLength = totalN
194
+ outTex._dataShape = [W, H]
195
+ return outTex
196
+ }
197
+
198
+ return { xTex: makeCoordTex('x'), yTex: makeCoordTex('y') }
199
+ }
200
+
201
+ class Scatter2dInterpolateData extends ComputedData {
202
+ columns() { return ['value', 'x', 'y'] }
203
+
204
+ compute(regl, params, data, getAxisDomain) {
205
+ const xCol = data.getData(params.x)
206
+ const yCol = data.getData(params.y)
207
+ const valCol = data.getData(params.value)
208
+
209
+ const W = params.resolutionX | 0
210
+ const H = params.resolutionY | 0
211
+ const radius = params.radius ?? 5.0
212
+ const w1 = params.w1 ?? 0.5
213
+ const w2 = params.w2 ?? 0.3
214
+ const w3 = params.w3 ?? 0.2
215
+ const fullCoordinates = params.full_coordinates ?? false
216
+
217
+ const N = xCol.length
218
+ const pickIds = new Float32Array(N)
219
+ for (let i = 0; i < N; i++) pickIds[i] = i
220
+
221
+ const xTex = xCol.toTexture(regl)
222
+ const yTex = yCol.toTexture(regl)
223
+ const valTex = valCol.toTexture(regl)
224
+
225
+ const xDomain = colDomain(xCol)
226
+ const yDomain = colDomain(yCol)
227
+
228
+ const accum1 = makeSplatPass(regl, W, H, pickIds, xTex, yTex, valTex, xDomain, yDomain, radius)
229
+ const accum2 = makeSplatPass(regl, W, H, pickIds, xTex, yTex, valTex, xDomain, yDomain, radius * 2)
230
+ const accum3 = makeSplatPass(regl, W, H, pickIds, xTex, yTex, valTex, xDomain, yDomain, radius * 4)
231
+
232
+ const valueTex = makeValueTexture(regl, W, H, accum1, accum2, accum3, w1, w2, w3)
233
+
234
+ const coordShape = fullCoordinates ? [W, H] : [2, 2]
235
+ const { xTex: xOutTex, yTex: yOutTex } = fullCoordinates
236
+ ? makeCoordTexturesFull(regl, W, H, xDomain, yDomain)
237
+ : makeCoordTexturesCorners(regl, xDomain, yDomain)
238
+
239
+ const xQK = (typeof params.x === 'string' && data) ? (data.getQuantityKind(params.x) ?? null) : null
240
+ const yQK = (typeof params.y === 'string' && data) ? (data.getQuantityKind(params.y) ?? null) : null
241
+ const valQK = (typeof params.value === 'string' && data) ? (data.getQuantityKind(params.value) ?? null) : null
242
+
243
+ return {
244
+ value: valueTex,
245
+ x: xOutTex,
246
+ y: yOutTex,
247
+ _meta: {
248
+ domains: { value: null, x: xDomain, y: yDomain },
249
+ quantityKinds: { value: valQK, x: xQK, y: yQK },
250
+ shapes: { value: [W, H], x: coordShape, y: coordShape },
251
+ }
252
+ }
253
+ }
254
+
255
+ schema(data) {
256
+ const cols = data ? data.columns() : []
257
+ return {
258
+ type: 'object',
259
+ title: 'Scatter2dInterpolate',
260
+ properties: {
261
+ x: { type: 'string', enum: cols },
262
+ y: { type: 'string', enum: cols },
263
+ value: { type: 'string', enum: cols },
264
+ resolutionX: { type: 'integer', default: 256 },
265
+ resolutionY: { type: 'integer', default: 256 },
266
+ radius: { type: 'number', default: 5.0 },
267
+ w1: { type: 'number', default: 0.5 },
268
+ w2: { type: 'number', default: 0.3 },
269
+ w3: { type: 'number', default: 0.2 },
270
+ full_coordinates: { type: 'boolean', default: false },
271
+ },
272
+ required: ['x', 'y', 'value', 'resolutionX', 'resolutionY']
273
+ }
274
+ }
275
+ }
276
+
277
+ registerComputedData('Scatter2dInterpolate', new Scatter2dInterpolateData())
@@ -0,0 +1,196 @@
1
+ import { registerTextureComputation, registerGlslComputation, EXPRESSION_REF } from "./ComputationRegistry.js"
2
+ import { TextureComputation, GlslComputation } from "../data/Computation.js"
3
+ import { GlslColumn } from "../data/ColumnData.js"
4
+
5
+ // Shared helper: allocate an output texture + FBO and run a fullscreen quad.
6
+ function runFullscreenQuad(regl, N, fragGlsl, uniforms = {}) {
7
+ const nTexels = Math.ceil(N / 4)
8
+ const w = Math.min(nTexels, regl.limits.maxTextureSize)
9
+ const h = Math.ceil(nTexels / w)
10
+ const outputTex = regl.texture({ width: w, height: h, type: 'float', format: 'rgba' })
11
+ const outputFBO = regl.framebuffer({ color: outputTex, depth: false, stencil: false })
12
+ regl({
13
+ framebuffer: outputFBO,
14
+ vert: `#version 300 es
15
+ precision highp float;
16
+ in vec2 a_position;
17
+ void main() { gl_Position = vec4(a_position, 0.0, 1.0); }`,
18
+ frag: fragGlsl(w),
19
+ attributes: { a_position: [[-1, -1], [1, -1], [-1, 1], [1, 1]] },
20
+ uniforms,
21
+ count: 4,
22
+ primitive: 'triangle strip'
23
+ })()
24
+ outputTex._dataLength = N
25
+ return outputTex
26
+ }
27
+
28
+ // ─── linspace ─────────────────────────────────────────────────────────────────
29
+ // Produces N values in ]0, 1[ : value[i] = (i + 0.5) / N — fully on GPU.
30
+ class LinspaceComputation extends TextureComputation {
31
+ compute(regl, inputs, _getAxisDomain) {
32
+ const N = inputs.length
33
+ return runFullscreenQuad(regl, N, w => `#version 300 es
34
+ precision highp float;
35
+ uniform int u_N;
36
+ out vec4 fragColor;
37
+ float linVal(int idx) { return (float(idx) + 0.5) / float(u_N); }
38
+ void main() {
39
+ int texelI = int(gl_FragCoord.y) * ${w} + int(gl_FragCoord.x);
40
+ int base = texelI * 4;
41
+ fragColor = vec4(
42
+ base + 0 < u_N ? linVal(base + 0) : 0.0,
43
+ base + 1 < u_N ? linVal(base + 1) : 0.0,
44
+ base + 2 < u_N ? linVal(base + 2) : 0.0,
45
+ base + 3 < u_N ? linVal(base + 3) : 0.0
46
+ );
47
+ }`, { u_N: N })
48
+ }
49
+
50
+ schema(_data) {
51
+ return {
52
+ type: 'object',
53
+ title: 'linspace',
54
+ properties: {
55
+ length: { type: 'integer', description: 'Number of values' }
56
+ },
57
+ required: ['length']
58
+ }
59
+ }
60
+ }
61
+
62
+ registerTextureComputation('linspace', new LinspaceComputation())
63
+
64
+ // ─── range ────────────────────────────────────────────────────────────────────
65
+ // Produces N integer values: 0, 1, 2, ..., N-1 — fully on GPU.
66
+ class RangeComputation extends TextureComputation {
67
+ compute(regl, inputs, _getAxisDomain) {
68
+ const N = inputs.length
69
+ return runFullscreenQuad(regl, N, w => `#version 300 es
70
+ precision highp float;
71
+ uniform int u_N;
72
+ out vec4 fragColor;
73
+ void main() {
74
+ int texelI = int(gl_FragCoord.y) * ${w} + int(gl_FragCoord.x);
75
+ int base = texelI * 4;
76
+ fragColor = vec4(
77
+ base + 0 < u_N ? float(base + 0) : 0.0,
78
+ base + 1 < u_N ? float(base + 1) : 0.0,
79
+ base + 2 < u_N ? float(base + 2) : 0.0,
80
+ base + 3 < u_N ? float(base + 3) : 0.0
81
+ );
82
+ }`, { u_N: N })
83
+ }
84
+
85
+ schema(_data) {
86
+ return {
87
+ type: 'object',
88
+ title: 'range',
89
+ properties: {
90
+ length: { type: 'integer', description: 'Number of values' }
91
+ },
92
+ required: ['length']
93
+ }
94
+ }
95
+ }
96
+
97
+ registerTextureComputation('range', new RangeComputation())
98
+
99
+ // ─── glslExpr ─────────────────────────────────────────────────────────────────
100
+ // A GlslComputation where the GLSL expression is a user-supplied string.
101
+ // Named inputs are referenced in expr as {name} placeholders.
102
+ //
103
+ // Example:
104
+ // { glslExpr: { expr: "sin({x}) * {y}", inputs: { x: "col1", y: "col2" } } }
105
+ class GlslExprComputation extends GlslComputation {
106
+ glsl(_resolvedExprs) { throw new Error('glslExpr: use createColumn, not glsl()') }
107
+
108
+ createColumn(inputs) {
109
+ const expr = inputs.expr // raw string from user
110
+ const colInputs = inputs.inputs ?? {}
111
+ return new GlslColumn(colInputs, (resolvedExprs) => {
112
+ let result = expr
113
+ for (const [name, glslExpr] of Object.entries(resolvedExprs)) {
114
+ result = result.replaceAll(`{${name}}`, glslExpr)
115
+ }
116
+ return result
117
+ })
118
+ }
119
+
120
+ schema(data) {
121
+ return {
122
+ type: 'object',
123
+ title: 'glslExpr',
124
+ properties: {
125
+ expr: {
126
+ type: 'string',
127
+ description: 'GLSL expression; reference inputs as {name} placeholders'
128
+ },
129
+ inputs: {
130
+ type: 'object',
131
+ additionalProperties: EXPRESSION_REF,
132
+ description: 'Named input columns referenced in expr as {name}'
133
+ }
134
+ },
135
+ required: ['expr']
136
+ }
137
+ }
138
+ }
139
+
140
+ registerGlslComputation('glslExpr', new GlslExprComputation())
141
+
142
+ // ─── random ───────────────────────────────────────────────────────────────────
143
+ // Produces N pseudorandom values in ]0, 1[ derived from index ^ seed.
144
+ // All computation is done on the GPU via a fullscreen quad render pass.
145
+ // Hash: 3-round xorshift-multiply (good avalanche, no trig, GLSL ES 300).
146
+ class RandomComputation extends TextureComputation {
147
+ compute(regl, inputs, _getAxisDomain) {
148
+ const N = inputs.length
149
+ const seed = (inputs.seed || 0) === 0 ? (Math.random() * 0x7fffffff) | 0 : inputs.seed
150
+ return runFullscreenQuad(regl, N, w => `#version 300 es
151
+ precision highp float;
152
+ uniform int u_seed;
153
+ uniform int u_N;
154
+ out vec4 fragColor;
155
+
156
+ uint uhash(uint x) {
157
+ x ^= x >> 17u;
158
+ x *= 0xbf324c81u;
159
+ x ^= x >> 11u;
160
+ x *= 0x68bc4b39u;
161
+ x ^= x >> 16u;
162
+ return x;
163
+ }
164
+
165
+ // Maps uint to ]0, 1[ : (bits + 0.5) / 2^24
166
+ float randVal(int idx) {
167
+ uint h = uhash(uint(idx) ^ uint(u_seed));
168
+ return (float(h >> 8u) + 0.5) / 16777216.0;
169
+ }
170
+
171
+ void main() {
172
+ int texelI = int(gl_FragCoord.y) * ${w} + int(gl_FragCoord.x);
173
+ int base = texelI * 4;
174
+ fragColor = vec4(
175
+ base + 0 < u_N ? randVal(base + 0) : 0.0,
176
+ base + 1 < u_N ? randVal(base + 1) : 0.0,
177
+ base + 2 < u_N ? randVal(base + 2) : 0.0,
178
+ base + 3 < u_N ? randVal(base + 3) : 0.0
179
+ );
180
+ }`, { u_seed: seed | 0, u_N: N })
181
+ }
182
+
183
+ schema(_data) {
184
+ return {
185
+ type: 'object',
186
+ title: 'random',
187
+ properties: {
188
+ length: { type: 'integer', description: 'Number of values' },
189
+ seed: { type: 'integer', description: 'Integer seed (default 0)', default: 0 }
190
+ },
191
+ required: ['length']
192
+ }
193
+ }
194
+ }
195
+
196
+ registerTextureComputation('random', new RandomComputation())
@@ -0,0 +1,153 @@
1
+ import { GlBase } from "./GlBase.js"
2
+ import { FilterAxisRegistry } from "../axes/FilterAxisRegistry.js"
3
+ import { DataGroup, normalizeData } from "../data/Data.js"
4
+ import { ColumnData, ArrayColumn } from "../data/ColumnData.js"
5
+
6
+ // Read a 4-packed RGBA float texture back to a flat Float32Array of length dataLength.
7
+ function readTextureToArray(regl, texture) {
8
+ const dataLength = texture._dataLength ?? (texture.width * texture.height * 4)
9
+ const fbo = regl.framebuffer({ color: texture, depth: false })
10
+ let pixels
11
+ try {
12
+ regl({ framebuffer: fbo })(() => {
13
+ pixels = regl.read()
14
+ })
15
+ } finally {
16
+ fbo.destroy()
17
+ }
18
+ const arr = pixels instanceof Float32Array ? pixels : new Float32Array(pixels.buffer, pixels.byteOffset, pixels.byteLength / 4)
19
+ return arr.slice(0, dataLength)
20
+ }
21
+
22
+ // Wraps any ColumnData and adds getArray() for CPU readback.
23
+ class ReadableColumn extends ColumnData {
24
+ constructor(col, regl) {
25
+ super()
26
+ this._col = col
27
+ this._regl = regl
28
+ }
29
+
30
+ get length() { return this._col.length }
31
+ get domain() { return this._col.domain }
32
+ get quantityKind() { return this._col.quantityKind }
33
+
34
+ resolve(path, regl) { return this._col.resolve(path, regl) }
35
+ toTexture(regl) { return this._col.toTexture(regl) }
36
+ refresh(plot) { return this._col.refresh(plot) }
37
+ withOffset(expr) { return this._col.withOffset(expr) }
38
+
39
+ getArray() {
40
+ if (this._col instanceof ArrayColumn) return this._col.array
41
+ const tex = this._col.toTexture(this._regl)
42
+ return readTextureToArray(this._regl, tex)
43
+ }
44
+ }
45
+
46
+ // Output object returned by ComputePipeline.update().
47
+ // Like DataGroup but getData() returns ReadableColumn with getArray(),
48
+ // and getArrays() reads all columns to CPU at once.
49
+ export class ComputeOutput {
50
+ constructor(dataGroup, regl) {
51
+ this._dataGroup = dataGroup
52
+ this._regl = regl
53
+ }
54
+
55
+ columns() {
56
+ return this._dataGroup.columns()
57
+ }
58
+
59
+ getData(col) {
60
+ const colData = this._dataGroup.getData(col)
61
+ if (!colData) return null
62
+ return new ReadableColumn(colData, this._regl)
63
+ }
64
+
65
+ getArrays() {
66
+ const result = {}
67
+ for (const col of this.columns()) {
68
+ const readable = this.getData(col)
69
+ if (readable) {
70
+ try {
71
+ result[col] = readable.getArray()
72
+ } catch (e) {
73
+ console.warn(`[gladly] ComputeOutput.getArrays(): failed to read column '${col}': ${e.message}`)
74
+ }
75
+ }
76
+ }
77
+ return result
78
+ }
79
+ }
80
+
81
+ // Headless GPU compute pipeline for running data transforms without any visual output.
82
+ // Creates its own offscreen WebGL context; no DOM container needed.
83
+ //
84
+ // Usage:
85
+ // const pipeline = new ComputePipeline()
86
+ // const output = pipeline.update({ data, transforms, axes })
87
+ // const arr = output.getData('hist.counts').getArray() // Float32Array
88
+ // const all = output.getArrays() // { 'hist.counts': Float32Array, ... }
89
+ // pipeline.destroy()
90
+ export class ComputePipeline extends GlBase {
91
+ constructor() {
92
+ super()
93
+ const canvas = typeof OffscreenCanvas !== 'undefined'
94
+ ? new OffscreenCanvas(1, 1)
95
+ : document.createElement('canvas')
96
+ this._initRegl(canvas)
97
+ this.filterAxisRegistry = new FilterAxisRegistry()
98
+ }
99
+
100
+ // Runs the given transforms over data and returns a ComputeOutput.
101
+ //
102
+ // axes: { [quantityKind]: { min, max } } — sets filter axis ranges before computing.
103
+ // Transforms that access a filter axis will see the configured range.
104
+ async update({ data, transforms = [], axes = {} } = {}) {
105
+ const epoch = ++this._initEpoch
106
+
107
+ if (data !== undefined) {
108
+ this._rawData = normalizeData(data)
109
+ }
110
+
111
+ this._dataTransformNodes = []
112
+ this.filterAxisRegistry = new FilterAxisRegistry()
113
+
114
+ if (this._rawData != null) {
115
+ const fresh = new DataGroup({})
116
+ fresh._children = { ...this._rawData._children }
117
+ this.currentData = fresh
118
+ } else {
119
+ this.currentData = new DataGroup({})
120
+ }
121
+
122
+ // Run transforms; filter axes are registered and data extents set during this step.
123
+ // At this point filter ranges are all null (open bounds).
124
+ await this._processTransforms(transforms, epoch)
125
+ if (this._initEpoch !== epoch) return new ComputeOutput(this.currentData, this.regl)
126
+
127
+ // Apply axes config to set filter ranges on any registered filter axis.
128
+ for (const [axisId, axisConfig] of Object.entries(axes)) {
129
+ if (this.filterAxisRegistry.hasAxis(axisId)) {
130
+ this.filterAxisRegistry.setRange(
131
+ axisId,
132
+ axisConfig.min !== undefined ? axisConfig.min : null,
133
+ axisConfig.max !== undefined ? axisConfig.max : null
134
+ )
135
+ }
136
+ }
137
+
138
+ // Refresh transforms whose output depends on any filter axis that now has a range set.
139
+ for (const node of this._dataTransformNodes) {
140
+ await node.refreshIfNeeded(this)
141
+ if (this._initEpoch !== epoch) return new ComputeOutput(this.currentData, this.regl)
142
+ }
143
+
144
+ return new ComputeOutput(this.currentData, this.regl)
145
+ }
146
+
147
+ destroy() {
148
+ if (this.regl) {
149
+ this.regl.destroy()
150
+ this.regl = null
151
+ }
152
+ }
153
+ }