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.
- package/README.md +9 -2
- package/package.json +10 -11
- package/src/axes/Axis.js +320 -172
- package/src/axes/AxisLink.js +6 -2
- package/src/axes/AxisRegistry.js +116 -39
- package/src/axes/Camera.js +47 -0
- package/src/axes/ColorAxisRegistry.js +10 -2
- package/src/axes/FilterAxisRegistry.js +1 -1
- package/src/axes/TickLabelAtlas.js +99 -0
- package/src/axes/ZoomController.js +446 -124
- package/src/colorscales/ColorscaleRegistry.js +30 -10
- package/src/compute/ComputationRegistry.js +126 -184
- package/src/compute/axisFilter.js +21 -9
- package/src/compute/conv.js +64 -8
- package/src/compute/elementwise.js +72 -0
- package/src/compute/fft.js +106 -20
- package/src/compute/filter.js +105 -103
- package/src/compute/hist.js +247 -142
- package/src/compute/kde.js +64 -46
- package/src/compute/scatter2dInterpolate.js +277 -0
- package/src/compute/util.js +196 -0
- package/src/core/ComputePipeline.js +153 -0
- package/src/core/GlBase.js +141 -0
- package/src/core/Layer.js +22 -8
- package/src/core/LayerType.js +253 -92
- package/src/core/Plot.js +644 -162
- package/src/core/PlotGroup.js +204 -0
- package/src/core/ShaderQueue.js +73 -0
- package/src/data/ColumnData.js +269 -0
- package/src/data/Computation.js +95 -0
- package/src/data/Data.js +270 -0
- package/src/floats/Float.js +56 -0
- package/src/index.js +16 -4
- package/src/layers/BarsLayer.js +168 -0
- package/src/layers/ColorbarLayer.js +10 -14
- package/src/layers/ColorbarLayer2d.js +13 -24
- package/src/layers/FilterbarLayer.js +4 -3
- package/src/layers/LinesLayer.js +108 -122
- package/src/layers/PointsLayer.js +73 -69
- package/src/layers/ScatterShared.js +62 -106
- package/src/layers/TileLayer.js +20 -16
- package/src/math/mat4.js +100 -0
- package/src/core/Data.js +0 -67
- 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
|
+
}
|