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
|
@@ -1,21 +1,10 @@
|
|
|
1
|
+
import { ColumnData } from '../data/ColumnData.js'
|
|
2
|
+
|
|
1
3
|
const textureComputations = new Map()
|
|
2
4
|
const glslComputations = new Map()
|
|
5
|
+
const computedDataRegistry = new Map()
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
schema(data) { throw new Error('Not implemented') }
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export class TextureComputation extends Computation {
|
|
9
|
-
compute(regl, params, getAxisDomain) { throw new Error('Not implemented') }
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export class GlslComputation extends Computation {
|
|
13
|
-
glsl(resolvedParams) { throw new Error('Not implemented') }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Use in computation schema() methods for params that can be a Float32Array or sub-expression
|
|
17
|
-
export const EXPRESSION_REF = { '$ref': '#/$defs/expression' }
|
|
18
|
-
|
|
7
|
+
// ─── Registration ─────────────────────────────────────────────────────────────
|
|
19
8
|
export function registerTextureComputation(name, computation) {
|
|
20
9
|
textureComputations.set(name, computation)
|
|
21
10
|
}
|
|
@@ -24,214 +13,167 @@ export function registerGlslComputation(name, computation) {
|
|
|
24
13
|
glslComputations.set(name, computation)
|
|
25
14
|
}
|
|
26
15
|
|
|
27
|
-
export function
|
|
28
|
-
|
|
29
|
-
const defs = {}
|
|
30
|
-
|
|
31
|
-
for (const [name, comp] of textureComputations) {
|
|
32
|
-
defs[`params_${name}`] = comp.schema(data)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
for (const [name, comp] of glslComputations) {
|
|
36
|
-
defs[`params_${name}`] = comp.schema(data)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
defs.expression = {
|
|
40
|
-
anyOf: [
|
|
41
|
-
{ type: 'string', enum: cols },
|
|
42
|
-
...[...textureComputations, ...glslComputations].map(([name]) => ({
|
|
43
|
-
type: 'object',
|
|
44
|
-
properties: { [name]: { '$ref': `#/$defs/params_${name}` } },
|
|
45
|
-
required: [name],
|
|
46
|
-
additionalProperties: false
|
|
47
|
-
}))
|
|
48
|
-
]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return { '$defs': defs, '$ref': '#/$defs/expression' }
|
|
16
|
+
export function registerComputedData(name, instance) {
|
|
17
|
+
computedDataRegistry.set(name, instance)
|
|
52
18
|
}
|
|
53
19
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
value !== null &&
|
|
58
|
-
typeof value === 'object' &&
|
|
59
|
-
typeof value.width === 'number' &&
|
|
60
|
-
typeof value.subimage === 'function'
|
|
61
|
-
)
|
|
20
|
+
export function getComputedData(name) {
|
|
21
|
+
return computedDataRegistry.get(name)
|
|
62
22
|
}
|
|
63
23
|
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
if (a == null || b == null) return a === b
|
|
67
|
-
return a[0] === b[0] && a[1] === b[1]
|
|
24
|
+
export function getRegisteredComputedData() {
|
|
25
|
+
return computedDataRegistry
|
|
68
26
|
}
|
|
69
27
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
function
|
|
73
|
-
if (expr instanceof
|
|
74
|
-
|
|
75
|
-
if (typeof expr === '
|
|
76
|
-
|
|
28
|
+
// ─── resolveExprToColumn ─────────────────────────────────────────────────────
|
|
29
|
+
// Turns any expression (string col name, { compName: params }, ColumnData) into ColumnData.
|
|
30
|
+
export async function resolveExprToColumn(expr, data, regl, plot) {
|
|
31
|
+
if (expr instanceof ColumnData) return expr
|
|
32
|
+
|
|
33
|
+
if (typeof expr === 'string') {
|
|
34
|
+
const col = data?.getData(expr)
|
|
35
|
+
if (!col) throw new Error(`Column '${expr}' not found in data`)
|
|
36
|
+
return col
|
|
37
|
+
}
|
|
77
38
|
|
|
78
39
|
if (typeof expr === 'object' && expr !== null) {
|
|
79
40
|
const keys = Object.keys(expr)
|
|
80
|
-
|
|
81
|
-
// Single-key object: check if it names a registered computation.
|
|
82
41
|
if (keys.length === 1) {
|
|
83
42
|
const compName = keys[0]
|
|
43
|
+
const params = expr[compName]
|
|
44
|
+
|
|
84
45
|
if (textureComputations.has(compName)) {
|
|
85
46
|
const comp = textureComputations.get(compName)
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
return comp.compute(regl, resolvedParams, getAxisDomain)
|
|
47
|
+
const resolvedInputs = await resolveParams(params, data, regl, plot)
|
|
48
|
+
return await comp.createColumn(regl, resolvedInputs, plot)
|
|
89
49
|
}
|
|
50
|
+
|
|
90
51
|
if (glslComputations.has(compName)) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
52
|
+
const comp = glslComputations.get(compName)
|
|
53
|
+
const resolvedInputs = await resolveParams(params, data, regl, plot)
|
|
54
|
+
return comp.createColumn(resolvedInputs)
|
|
94
55
|
}
|
|
95
56
|
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw new Error(`Cannot resolve expression to column: ${JSON.stringify(expr)}`)
|
|
60
|
+
}
|
|
96
61
|
|
|
97
|
-
|
|
62
|
+
// Resolve a params dict recursively: column refs -> ColumnData, scalars pass through.
|
|
63
|
+
async function resolveParams(params, data, regl, plot) {
|
|
64
|
+
if (params === null || params === undefined) return params
|
|
65
|
+
if (typeof params === 'number' || typeof params === 'boolean') return params
|
|
66
|
+
if (params instanceof ColumnData) return params
|
|
67
|
+
if (params instanceof Float32Array) return params
|
|
68
|
+
|
|
69
|
+
if (typeof params === 'string') {
|
|
70
|
+
const col = data?.getData(params)
|
|
71
|
+
return col ?? params // fall back to string value if not a known column
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof params === 'object') {
|
|
75
|
+
const keys = Object.keys(params)
|
|
76
|
+
if (keys.length === 1 &&
|
|
77
|
+
(textureComputations.has(keys[0]) || glslComputations.has(keys[0]))) {
|
|
78
|
+
return await resolveExprToColumn(params, data, regl, plot)
|
|
79
|
+
}
|
|
98
80
|
const resolved = {}
|
|
99
|
-
for (const [k, v] of Object.entries(
|
|
100
|
-
resolved[k] =
|
|
81
|
+
for (const [k, v] of Object.entries(params)) {
|
|
82
|
+
resolved[k] = await resolveParams(v, data, regl, plot)
|
|
101
83
|
}
|
|
102
84
|
return resolved
|
|
103
85
|
}
|
|
104
86
|
|
|
105
|
-
|
|
87
|
+
return params
|
|
106
88
|
}
|
|
107
89
|
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
90
|
+
// ─── resolveAttributeExpr ────────────────────────────────────────────────────
|
|
91
|
+
// Entry point from LayerType.createDrawCommand. Returns:
|
|
92
|
+
// { kind: 'buffer', value: Float32Array } — fixed geometry
|
|
93
|
+
// { kind: 'computed', glslExpr, textures, col } — data column
|
|
94
|
+
export async function resolveAttributeExpr(regl, expr, attrShaderName, plot) {
|
|
111
95
|
if (expr instanceof Float32Array) {
|
|
112
|
-
|
|
113
|
-
context.bufferAttrs[attrName] = expr
|
|
114
|
-
context.globalDecls.push(`attribute float ${attrName};`)
|
|
115
|
-
return attrName
|
|
96
|
+
return { kind: 'buffer', value: expr }
|
|
116
97
|
}
|
|
117
98
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
context.scalarUniforms[widthName] = expr.width
|
|
123
|
-
context.globalDecls.push(`uniform sampler2D ${uniformName};`)
|
|
124
|
-
context.globalDecls.push(`uniform float ${widthName};`)
|
|
125
|
-
return `texture2D(${uniformName}, vec2((a_pickId + 0.5) / ${widthName}, 0.5)).r`
|
|
126
|
-
}
|
|
99
|
+
const data = plot ? plot.currentData : null
|
|
100
|
+
const col = (expr instanceof ColumnData)
|
|
101
|
+
? expr
|
|
102
|
+
: await resolveExprToColumn(expr, data, regl, plot)
|
|
127
103
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return uniformName
|
|
133
|
-
}
|
|
104
|
+
const safePath = attrShaderName.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
105
|
+
const { glslExpr, textures } = col.resolve(safePath, regl)
|
|
106
|
+
return { kind: 'computed', glslExpr, textures, col }
|
|
107
|
+
}
|
|
134
108
|
|
|
135
|
-
|
|
109
|
+
// ─── resolveQuantityKind ─────────────────────────────────────────────────────
|
|
110
|
+
export function resolveQuantityKind(expr, data) {
|
|
111
|
+
if (expr instanceof ColumnData) return expr.quantityKind
|
|
112
|
+
if (typeof expr === 'string') {
|
|
113
|
+
return (data ? data.getQuantityKind(expr) : null) ?? expr
|
|
114
|
+
}
|
|
115
|
+
if (expr && typeof expr === 'object') {
|
|
136
116
|
const keys = Object.keys(expr)
|
|
137
|
-
|
|
138
117
|
if (keys.length === 1) {
|
|
139
118
|
const compName = keys[0]
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
if (textureComputations.has(compName)) {
|
|
143
|
-
const comp = textureComputations.get(compName)
|
|
144
|
-
const uniformName = `u_cgen_${path}`
|
|
145
|
-
const widthName = `u_cgen_${path}_width`
|
|
146
|
-
|
|
147
|
-
// Live reference updated when axis domains change.
|
|
148
|
-
const liveRef = {
|
|
149
|
-
texture: null,
|
|
150
|
-
accessedAxes: new Set(),
|
|
151
|
-
cachedDomains: {}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const makeTrackingGetter = (ref, currentPlot) => (axisId) => {
|
|
155
|
-
ref.accessedAxes.add(axisId)
|
|
156
|
-
return currentPlot ? currentPlot.getAxisDomain(axisId) : null
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Initial computation with axis tracking.
|
|
160
|
-
const initGetter = makeTrackingGetter(liveRef, plot)
|
|
161
|
-
const resolvedParams = resolveToRawValue(regl, params, path, initGetter)
|
|
162
|
-
liveRef.texture = comp.compute(regl, resolvedParams, initGetter)
|
|
163
|
-
|
|
164
|
-
// Cache the domains accessed during initial computation.
|
|
165
|
-
for (const axisId of liveRef.accessedAxes) {
|
|
166
|
-
liveRef.cachedDomains[axisId] = plot ? plot.getAxisDomain(axisId) : null
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Use a function so regl reads the current texture each frame.
|
|
170
|
-
context.textureUniforms[uniformName] = () => liveRef.texture
|
|
171
|
-
// Texture width is constant across recomputes (same bins count).
|
|
172
|
-
context.scalarUniforms[widthName] = liveRef.texture.width
|
|
173
|
-
context.globalDecls.push(`uniform sampler2D ${uniformName};`)
|
|
174
|
-
context.globalDecls.push(`uniform float ${widthName};`)
|
|
175
|
-
|
|
176
|
-
context.axisUpdaters.push({
|
|
177
|
-
refreshIfNeeded(currentPlot) {
|
|
178
|
-
if (liveRef.accessedAxes.size === 0) return
|
|
179
|
-
|
|
180
|
-
let needsRecompute = false
|
|
181
|
-
for (const axisId of liveRef.accessedAxes) {
|
|
182
|
-
if (!domainsEqual(currentPlot.getAxisDomain(axisId), liveRef.cachedDomains[axisId])) {
|
|
183
|
-
needsRecompute = true
|
|
184
|
-
break
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
if (!needsRecompute) return
|
|
188
|
-
|
|
189
|
-
// Recompute with fresh axis tracking so new dependencies are captured.
|
|
190
|
-
const newRef = { accessedAxes: new Set(), cachedDomains: {} }
|
|
191
|
-
const newGetter = makeTrackingGetter(newRef, currentPlot)
|
|
192
|
-
const newParams = resolveToRawValue(regl, params, path, newGetter)
|
|
193
|
-
newRef.texture = comp.compute(regl, newParams, newGetter)
|
|
194
|
-
for (const axisId of newRef.accessedAxes) {
|
|
195
|
-
newRef.cachedDomains[axisId] = currentPlot.getAxisDomain(axisId)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Update live ref in-place so the dynamic uniform picks up the new texture.
|
|
199
|
-
liveRef.texture = newRef.texture
|
|
200
|
-
liveRef.accessedAxes = newRef.accessedAxes
|
|
201
|
-
liveRef.cachedDomains = newRef.cachedDomains
|
|
202
|
-
}
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
return `texture2D(${uniformName}, vec2((a_pickId + 0.5) / ${widthName}, 0.5)).r`
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (glslComputations.has(compName)) {
|
|
209
|
-
const comp = glslComputations.get(compName)
|
|
210
|
-
const resolvedGlslParams = {}
|
|
211
|
-
for (const [k, v] of Object.entries(params)) {
|
|
212
|
-
resolvedGlslParams[k] = resolveToGlslExpr(regl, v, `${path}_${k}`, context, plot)
|
|
213
|
-
}
|
|
214
|
-
return comp.glsl(resolvedGlslParams)
|
|
215
|
-
}
|
|
119
|
+
const comp = textureComputations.get(compName) ?? glslComputations.get(compName)
|
|
120
|
+
if (comp) return comp.getQuantityKind(expr[compName], data)
|
|
216
121
|
}
|
|
217
122
|
}
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Schema builders ──────────────────────────────────────────────────────────
|
|
127
|
+
export const EXPRESSION_REF = { '$ref': '#/$defs/expression' }
|
|
128
|
+
export const EXPRESSION_REF_OPT = { '$ref': '#/$defs/expression_opt' }
|
|
218
129
|
|
|
219
|
-
|
|
130
|
+
export function buildTransformSchema(data) {
|
|
131
|
+
const defs = {}
|
|
132
|
+
for (const [name, comp] of computedDataRegistry) {
|
|
133
|
+
defs[`params_computeddata_${name}`] = comp.schema(data)
|
|
134
|
+
}
|
|
135
|
+
defs.transform_expression = {
|
|
136
|
+
anyOf: [...computedDataRegistry].map(([name]) => ({
|
|
137
|
+
type: 'object',
|
|
138
|
+
title: name,
|
|
139
|
+
properties: { [name]: { '$ref': `#/$defs/params_computeddata_${name}` } },
|
|
140
|
+
required: [name],
|
|
141
|
+
additionalProperties: false
|
|
142
|
+
}))
|
|
143
|
+
}
|
|
144
|
+
return { '$defs': defs }
|
|
220
145
|
}
|
|
221
146
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
147
|
+
export function computationSchema(data) {
|
|
148
|
+
const cols = data ? data.columns() : []
|
|
149
|
+
const defs = {}
|
|
150
|
+
|
|
151
|
+
for (const [name, comp] of textureComputations) {
|
|
152
|
+
defs[`params_${name}`] = comp.schema(data)
|
|
153
|
+
}
|
|
154
|
+
for (const [name, comp] of glslComputations) {
|
|
155
|
+
defs[`params_${name}`] = comp.schema(data)
|
|
226
156
|
}
|
|
227
157
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
158
|
+
defs.expression = {
|
|
159
|
+
anyOf: [
|
|
160
|
+
...cols.map(col => ({ type: 'string', const: col, enum: [col], title: col, readOnly: true })),
|
|
161
|
+
...[...textureComputations, ...glslComputations].map(([name]) => ({
|
|
162
|
+
type: 'object',
|
|
163
|
+
title: name,
|
|
164
|
+
properties: { [name]: { '$ref': `#/$defs/params_${name}` } },
|
|
165
|
+
required: [name],
|
|
166
|
+
additionalProperties: false
|
|
167
|
+
}))
|
|
168
|
+
]
|
|
234
169
|
}
|
|
235
|
-
|
|
236
|
-
|
|
170
|
+
|
|
171
|
+
defs.expression_opt = {
|
|
172
|
+
anyOf: [
|
|
173
|
+
{ type: 'string', const: 'none', enum: ['none'], title: 'none', readOnly: true },
|
|
174
|
+
...defs.expression.anyOf
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { '$defs': defs, '$ref': '#/$defs/expression' }
|
|
237
179
|
}
|
|
@@ -1,38 +1,50 @@
|
|
|
1
|
-
import { registerTextureComputation,
|
|
1
|
+
import { registerTextureComputation, EXPRESSION_REF } from "./ComputationRegistry.js"
|
|
2
|
+
import { TextureComputation } from "../data/Computation.js"
|
|
3
|
+
import { ArrayColumn, uploadToTexture } from "../data/ColumnData.js"
|
|
2
4
|
import makeHistogram from "./hist.js"
|
|
3
5
|
|
|
4
6
|
// Texture computation that filters the input data by a filter axis range and
|
|
5
7
|
// then builds a histogram texture from the surviving values.
|
|
6
8
|
//
|
|
7
9
|
// params:
|
|
8
|
-
// input —
|
|
9
|
-
// filterValues —
|
|
10
|
+
// input — ColumnData of values normalised to [0, 1] (for histogram bins); must be ArrayColumn
|
|
11
|
+
// filterValues — ColumnData of raw filter-column values (same length as input); must be ArrayColumn
|
|
10
12
|
// filterAxisId — string: axis ID / quantity kind whose domain drives the filter
|
|
11
13
|
// bins — optional number of histogram bins
|
|
12
14
|
//
|
|
13
15
|
// getAxisDomain(filterAxisId) returns [min|null, max|null] where null means
|
|
14
16
|
// unbounded. The computation is re-run automatically whenever the domain changes.
|
|
15
17
|
class FilteredHistogramComputation extends TextureComputation {
|
|
16
|
-
compute(regl,
|
|
17
|
-
const
|
|
18
|
+
compute(regl, inputs, getAxisDomain) {
|
|
19
|
+
const inputCol = inputs.input
|
|
20
|
+
const filterCol = inputs.filterValues
|
|
21
|
+
if (!(inputCol instanceof ArrayColumn)) throw new Error('filteredHistogram: input must be ArrayColumn')
|
|
22
|
+
if (!(filterCol instanceof ArrayColumn)) throw new Error('filteredHistogram: filterValues must be ArrayColumn')
|
|
23
|
+
|
|
24
|
+
const inputArr = inputCol.array
|
|
25
|
+
const filterArr = filterCol.array
|
|
26
|
+
const { filterAxisId, bins } = inputs
|
|
18
27
|
const domain = getAxisDomain(filterAxisId) // [min|null, max|null] or null
|
|
19
28
|
const filterMin = domain?.[0] ?? null
|
|
20
29
|
const filterMax = domain?.[1] ?? null
|
|
21
30
|
|
|
22
31
|
// Build a compact array of the normalised values that pass the filter.
|
|
23
32
|
const filtered = []
|
|
24
|
-
for (let i = 0; i <
|
|
25
|
-
const fv =
|
|
33
|
+
for (let i = 0; i < inputArr.length; i++) {
|
|
34
|
+
const fv = filterArr[i]
|
|
26
35
|
if (filterMin !== null && fv < filterMin) continue
|
|
27
36
|
if (filterMax !== null && fv > filterMax) continue
|
|
28
|
-
filtered.push(
|
|
37
|
+
filtered.push(inputArr[i])
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
const filteredTex = uploadToTexture(regl, new Float32Array(filtered))
|
|
41
|
+
return makeHistogram(regl, filteredTex, { bins })
|
|
32
42
|
}
|
|
43
|
+
|
|
33
44
|
schema(data) {
|
|
34
45
|
return {
|
|
35
46
|
type: 'object',
|
|
47
|
+
title: 'filteredHistogram',
|
|
36
48
|
properties: {
|
|
37
49
|
input: EXPRESSION_REF,
|
|
38
50
|
filterValues: EXPRESSION_REF,
|
package/src/compute/conv.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fftConvolution } from "./fft.js"
|
|
2
|
-
import { registerTextureComputation,
|
|
2
|
+
import { registerTextureComputation, EXPRESSION_REF, resolveQuantityKind } from "./ComputationRegistry.js"
|
|
3
|
+
import { TextureComputation } from "../data/Computation.js"
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
============================================================
|
|
@@ -13,6 +14,7 @@ function nextPow2(n) {
|
|
|
13
14
|
return 1 << Math.ceil(Math.log2(n));
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
// Internal: builds a 1-value-per-texel R-channel texture (not exposed via sampleColumn)
|
|
16
18
|
function make1DTexture(regl, data, width) {
|
|
17
19
|
return regl.texture({
|
|
18
20
|
data,
|
|
@@ -26,6 +28,7 @@ function make1DTexture(regl, data, width) {
|
|
|
26
28
|
});
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
// Internal: creates an R-channel FBO of given width
|
|
29
32
|
function makeFBO(regl, width) {
|
|
30
33
|
return regl.framebuffer({
|
|
31
34
|
color: regl.texture({
|
|
@@ -33,10 +36,52 @@ function makeFBO(regl, width) {
|
|
|
33
36
|
height: 1,
|
|
34
37
|
format: "rgba",
|
|
35
38
|
type: "float"
|
|
36
|
-
})
|
|
39
|
+
}),
|
|
40
|
+
depth: false,
|
|
41
|
+
stencil: false,
|
|
37
42
|
});
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
// Repack a 1-value-per-texel R-channel texture into a 4-packed RGBA texture.
|
|
46
|
+
// The source texture has elements at (i % srcW, i / srcW).r
|
|
47
|
+
// The output has elements packed 4 per texel, _dataLength set.
|
|
48
|
+
function repackToQuadTexture(regl, sourceTex, N) {
|
|
49
|
+
const nTexels = Math.ceil(N / 4)
|
|
50
|
+
const w = Math.min(nTexels, regl.limits.maxTextureSize)
|
|
51
|
+
const h = Math.ceil(nTexels / w)
|
|
52
|
+
const outputTex = regl.texture({ width: w, height: h, type: 'float', format: 'rgba' })
|
|
53
|
+
const outputFBO = regl.framebuffer({ color: outputTex, depth: false, stencil: false })
|
|
54
|
+
|
|
55
|
+
regl({
|
|
56
|
+
framebuffer: outputFBO,
|
|
57
|
+
vert: `#version 300 es
|
|
58
|
+
in vec2 position;
|
|
59
|
+
void main() { gl_Position = vec4(position, 0.0, 1.0); }`,
|
|
60
|
+
frag: `#version 300 es
|
|
61
|
+
precision highp float;
|
|
62
|
+
uniform sampler2D sourceTex;
|
|
63
|
+
uniform int totalLength;
|
|
64
|
+
out vec4 fragColor;
|
|
65
|
+
void main() {
|
|
66
|
+
ivec2 srcSz = textureSize(sourceTex, 0);
|
|
67
|
+
int texelI = int(gl_FragCoord.y) * ${w} + int(gl_FragCoord.x);
|
|
68
|
+
int base = texelI * 4;
|
|
69
|
+
float v0 = base + 0 < totalLength ? texelFetch(sourceTex, ivec2((base+0) % srcSz.x, (base+0) / srcSz.x), 0).r : 0.0;
|
|
70
|
+
float v1 = base + 1 < totalLength ? texelFetch(sourceTex, ivec2((base+1) % srcSz.x, (base+1) / srcSz.x), 0).r : 0.0;
|
|
71
|
+
float v2 = base + 2 < totalLength ? texelFetch(sourceTex, ivec2((base+2) % srcSz.x, (base+2) / srcSz.x), 0).r : 0.0;
|
|
72
|
+
float v3 = base + 3 < totalLength ? texelFetch(sourceTex, ivec2((base+3) % srcSz.x, (base+3) / srcSz.x), 0).r : 0.0;
|
|
73
|
+
fragColor = vec4(v0, v1, v2, v3);
|
|
74
|
+
}`,
|
|
75
|
+
attributes: { position: [[-1,-1],[1,-1],[-1,1],[1,1]] },
|
|
76
|
+
uniforms: { sourceTex, totalLength: N },
|
|
77
|
+
count: 4,
|
|
78
|
+
primitive: 'triangle strip'
|
|
79
|
+
})()
|
|
80
|
+
|
|
81
|
+
outputTex._dataLength = N
|
|
82
|
+
return outputTex
|
|
83
|
+
}
|
|
84
|
+
|
|
40
85
|
/*
|
|
41
86
|
============================================================
|
|
42
87
|
1) Single-pass convolution (kernel ≤ 1024)
|
|
@@ -182,6 +227,8 @@ export default function adaptiveConvolution(regl, signalArray, kernelArray) {
|
|
|
182
227
|
|
|
183
228
|
const signalTex = make1DTexture(regl, signalArray, N);
|
|
184
229
|
|
|
230
|
+
let result;
|
|
231
|
+
|
|
185
232
|
// Case 1: single pass
|
|
186
233
|
if (K <= MAX_KERNEL_LOOP) {
|
|
187
234
|
const kernelTex = make1DTexture(regl, kernelArray, K);
|
|
@@ -195,12 +242,12 @@ export default function adaptiveConvolution(regl, signalArray, kernelArray) {
|
|
|
195
242
|
fbo
|
|
196
243
|
});
|
|
197
244
|
|
|
198
|
-
|
|
245
|
+
result = fbo.color[0];
|
|
199
246
|
}
|
|
200
247
|
|
|
201
248
|
// Case 2: chunked
|
|
202
|
-
if (K <= 8192) {
|
|
203
|
-
|
|
249
|
+
else if (K <= 8192) {
|
|
250
|
+
result = chunked({
|
|
204
251
|
signalTex,
|
|
205
252
|
kernel: kernelArray,
|
|
206
253
|
N
|
|
@@ -208,16 +255,25 @@ export default function adaptiveConvolution(regl, signalArray, kernelArray) {
|
|
|
208
255
|
}
|
|
209
256
|
|
|
210
257
|
// Case 3: FFT
|
|
211
|
-
|
|
258
|
+
else {
|
|
259
|
+
result = fftConvolution(regl, signalArray, kernelArray);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Repack internal R-channel result into 4-packed output for sampleColumn
|
|
263
|
+
return repackToQuadTexture(regl, result, N);
|
|
212
264
|
}
|
|
213
265
|
|
|
214
266
|
class ConvolutionComputation extends TextureComputation {
|
|
215
|
-
|
|
216
|
-
|
|
267
|
+
getQuantityKind(params, data) { return resolveQuantityKind(params.signal, data) }
|
|
268
|
+
compute(regl, params, data, getAxisDomain) {
|
|
269
|
+
const signal = typeof params.signal === 'string' ? data.getData(params.signal) : params.signal
|
|
270
|
+
const kernel = typeof params.kernel === 'string' ? data.getData(params.kernel) : params.kernel
|
|
271
|
+
return adaptiveConvolution(regl, signal, kernel)
|
|
217
272
|
}
|
|
218
273
|
schema(data) {
|
|
219
274
|
return {
|
|
220
275
|
type: 'object',
|
|
276
|
+
title: 'convolution',
|
|
221
277
|
properties: {
|
|
222
278
|
signal: EXPRESSION_REF,
|
|
223
279
|
kernel: EXPRESSION_REF
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { registerComputedData, EXPRESSION_REF, resolveExprToColumn } from "./ComputationRegistry.js"
|
|
2
|
+
import { ComputedData } from "../data/Computation.js"
|
|
3
|
+
|
|
4
|
+
const TDR_STEP_MS = 500
|
|
5
|
+
|
|
6
|
+
class ElementwiseData extends ComputedData {
|
|
7
|
+
columns(params) {
|
|
8
|
+
if (!params?.columns) return []
|
|
9
|
+
return params.columns.map(c => c.dst)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async compute(regl, params, data, getAxisDomain) {
|
|
13
|
+
const plotProxy = { currentData: data, getAxisDomain }
|
|
14
|
+
|
|
15
|
+
let N = params.dataLength ?? null
|
|
16
|
+
if (N == null) {
|
|
17
|
+
for (const { src } of params.columns) {
|
|
18
|
+
const col = await resolveExprToColumn(src, data, regl, plotProxy)
|
|
19
|
+
if (col?.length !== null) { N = col.length; break }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (N == null) throw new Error('ElementwiseData: cannot determine data length; set dataLength param')
|
|
23
|
+
|
|
24
|
+
const result = {}
|
|
25
|
+
const quantityKinds = {}
|
|
26
|
+
let stepStart = performance.now()
|
|
27
|
+
|
|
28
|
+
for (const { dst, src, quantityKind } of params.columns) {
|
|
29
|
+
const col = await resolveExprToColumn(src, data, regl, plotProxy)
|
|
30
|
+
const tex = col.toTexture(regl)
|
|
31
|
+
tex._dataLength = N
|
|
32
|
+
result[dst] = tex
|
|
33
|
+
if (quantityKind) quantityKinds[dst] = quantityKind
|
|
34
|
+
|
|
35
|
+
if (performance.now() - stepStart > TDR_STEP_MS) {
|
|
36
|
+
await new Promise(r => requestAnimationFrame(r))
|
|
37
|
+
stepStart = performance.now()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Object.keys(quantityKinds).length > 0) result._meta = { quantityKinds }
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
schema(data) {
|
|
46
|
+
return {
|
|
47
|
+
type: 'object',
|
|
48
|
+
title: 'ElementwiseData',
|
|
49
|
+
properties: {
|
|
50
|
+
dataLength: {
|
|
51
|
+
type: 'integer',
|
|
52
|
+
description: 'Override output length (optional, auto-detected from column refs)'
|
|
53
|
+
},
|
|
54
|
+
columns: {
|
|
55
|
+
type: 'array',
|
|
56
|
+
items: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
dst: { type: 'string', description: 'Output column name' },
|
|
60
|
+
src: EXPRESSION_REF,
|
|
61
|
+
quantityKind: { type: 'string', description: 'Quantity kind for axis matching (optional)' }
|
|
62
|
+
},
|
|
63
|
+
required: ['dst', 'src']
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
required: ['columns']
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
registerComputedData('ElementwiseData', new ElementwiseData())
|