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
@@ -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
- export class Computation {
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 computationSchema(data) {
28
- const cols = data ? data.columns() : []
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
- // Duck-type check for regl textures.
55
- export function isTexture(value) {
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 domainsEqual(a, b) {
65
- if (a === b) return true
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
- // Resolve expr to a raw JS value (Float32Array / texture / number).
71
- // Used for texture computation params GLSL expressions are not permitted here.
72
- function resolveToRawValue(regl, expr, path, getAxisDomain) {
73
- if (expr instanceof Float32Array) return expr
74
- if (isTexture(expr)) return expr
75
- if (typeof expr === 'number') return expr
76
- if (typeof expr === 'string') return expr
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 params = expr[compName]
87
- const resolvedParams = resolveToRawValue(regl, params, path, getAxisDomain)
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
- throw new Error(
92
- `GLSL computation '${compName}' cannot be used as a texture computation parameter`
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
- // Plain params dict: resolve each value recursively.
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(expr)) {
100
- resolved[k] = resolveToRawValue(regl, v, `${path}_${k}`, getAxisDomain)
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
- throw new Error(`Cannot resolve to raw value: ${JSON.stringify(expr)}`)
87
+ return params
106
88
  }
107
89
 
108
- // Resolve expr to a GLSL expression string.
109
- // Side effects: populates context.bufferAttrs, textureUniforms, scalarUniforms, globalDecls, axisUpdaters.
110
- function resolveToGlslExpr(regl, expr, path, context, plot) {
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
- const attrName = `a_cgen_${path}`
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
- if (isTexture(expr)) {
119
- const uniformName = `u_cgen_${path}`
120
- const widthName = `u_cgen_${path}_width`
121
- context.textureUniforms[uniformName] = expr
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
- if (typeof expr === 'number') {
129
- const uniformName = `u_cgen_${path}`
130
- context.scalarUniforms[uniformName] = expr
131
- context.globalDecls.push(`uniform float ${uniformName};`)
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
- if (typeof expr === 'object' && expr !== null) {
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 params = expr[compName]
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
- throw new Error(`Cannot resolve to GLSL expression: ${JSON.stringify(expr)}`)
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
- // Top-level resolver: decides whether expr is a plain buffer or a computed attribute.
223
- export function resolveAttributeExpr(regl, expr, attrShaderName, plot) {
224
- if (expr instanceof Float32Array) {
225
- return { kind: 'buffer', value: expr }
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
- const context = {
229
- bufferAttrs: {},
230
- textureUniforms: {},
231
- scalarUniforms: {},
232
- globalDecls: [],
233
- axisUpdaters: []
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
- const glslExpr = resolveToGlslExpr(regl, expr, attrShaderName, context, plot)
236
- return { kind: 'computed', glslExpr, context }
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, TextureComputation, EXPRESSION_REF } from "./ComputationRegistry.js"
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 — Float32Array of values normalised to [0, 1] (for histogram bins)
9
- // filterValues — Float32Array of raw filter-column values (same length as input)
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, params, getAxisDomain) {
17
- const { input, filterValues, filterAxisId, bins } = params
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 < input.length; i++) {
25
- const fv = filterValues[i]
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(input[i])
37
+ filtered.push(inputArr[i])
29
38
  }
30
39
 
31
- return makeHistogram(regl, new Float32Array(filtered), { bins })
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,
@@ -1,5 +1,6 @@
1
1
  import { fftConvolution } from "./fft.js"
2
- import { registerTextureComputation, TextureComputation, EXPRESSION_REF } from "./ComputationRegistry.js"
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
- return fbo.color[0];
245
+ result = fbo.color[0];
199
246
  }
200
247
 
201
248
  // Case 2: chunked
202
- if (K <= 8192) {
203
- return chunked({
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
- return fftConvolution(regl, signalArray, kernelArray);
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
- compute(regl, params) {
216
- return adaptiveConvolution(regl, params.signal, params.kernel)
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())