gladly-plot 0.0.5 → 0.0.6

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 +251 -92
  26. package/src/core/Plot.js +630 -152
  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,173 +1,143 @@
1
- import { registerTextureComputation, TextureComputation, EXPRESSION_REF } from "./ComputationRegistry.js"
2
-
3
- /**
4
- * Auto-select number of histogram bins using Scott's rule.
5
- * For large datasets, uses a random subset to estimate std for speed.
6
- * @param {Float32Array | Array} data - input array
7
- * @param {Object} options
8
- * - sampleCutoff: max points to use for std estimation (default 50000)
9
- * @returns {number} - suggested number of bins
10
- */
11
- function autoBinsScott(data, options = {}) {
12
- const N = data.length;
13
- const sampleCutoff = options.sampleCutoff || 50000;
14
-
15
- let sampleData;
1
+ import { registerTextureComputation, registerComputedData, EXPRESSION_REF, EXPRESSION_REF_OPT } from "./ComputationRegistry.js"
2
+ import { TextureComputation, ComputedData } from "../data/Computation.js"
3
+ import { ArrayColumn, uploadToTexture, SAMPLE_COLUMN_GLSL } from "../data/ColumnData.js"
16
4
 
5
+ function autoBinsScott(data, options = {}) {
6
+ const N = data.length
7
+ const sampleCutoff = options.sampleCutoff || 50000
8
+ let sampleData
17
9
  if (N > sampleCutoff) {
18
- // Randomly sample sampleCutoff points
19
- sampleData = new Float32Array(sampleCutoff);
10
+ sampleData = new Float32Array(sampleCutoff)
20
11
  for (let i = 0; i < sampleCutoff; i++) {
21
- const idx = Math.floor(Math.random() * N);
22
- sampleData[i] = data[idx];
12
+ sampleData[i] = data[Math.floor(Math.random() * N)]
23
13
  }
24
14
  } else {
25
- sampleData = data;
15
+ sampleData = data
26
16
  }
27
-
28
- const n = sampleData.length;
29
-
30
- // Compute mean
31
- const mean = sampleData.reduce((a, b) => a + b, 0) / n;
32
-
33
- // Compute standard deviation
34
- let std;
17
+ const n = sampleData.length
18
+ const mean = sampleData.reduce((a, b) => a + b, 0) / n
19
+ let std
35
20
  if (N <= sampleCutoff) {
36
- // small dataset: population std
37
- const variance = sampleData.reduce((a, b) => a + (b - mean) ** 2, 0) / n;
38
- std = Math.sqrt(variance);
21
+ std = Math.sqrt(sampleData.reduce((a, b) => a + (b - mean) ** 2, 0) / n)
39
22
  } else {
40
- // large dataset: sample std
41
- const variance = sampleData.reduce((a, b) => a + (b - mean) ** 2, 0) / (n - 1);
42
- std = Math.sqrt(variance);
23
+ std = Math.sqrt(sampleData.reduce((a, b) => a + (b - mean) ** 2, 0) / (n - 1))
43
24
  }
44
-
45
- // Compute bin width
46
- const binWidth = 3.5 * std / Math.cbrt(N);
47
-
48
- // Determine number of bins
49
- let min = data[0], max = data[0];
25
+ const binWidth = 3.5 * std / Math.cbrt(N)
26
+ let min = data[0], max = data[0]
50
27
  for (let i = 1; i < data.length; i++) {
51
- if (data[i] < min) min = data[i];
52
- if (data[i] > max) max = data[i];
28
+ if (data[i] < min) min = data[i]
29
+ if (data[i] > max) max = data[i]
53
30
  }
54
- const bins = Math.max(1, Math.ceil((max - min) / binWidth));
55
-
56
- return bins;
31
+ return Math.max(1, Math.ceil((max - min) / binWidth))
57
32
  }
58
33
 
59
- /**
60
- * Automatically determine number of bins using Freedman–Diaconis or Sturges
61
- * @param {Float32Array} data - input array
62
- * @param {Object} options
63
- * - maxBins: maximum bins allowed (GPU-friendly)
64
- * @returns {number} - number of bins
65
- */
66
34
  function autoBins(data, options = {}) {
67
- const N = data.length;
68
- const maxBins = options.maxBins || 2048;
69
-
70
- if (N < 30) {
71
- // Small dataset → use Sturges
72
- return Math.min(Math.ceil(Math.log2(N) + 1), maxBins);
73
- }
74
-
35
+ const N = data.length
36
+ const maxBins = options.maxBins || 2048
37
+ if (N < 30) return Math.min(Math.ceil(Math.log2(N) + 1), maxBins)
75
38
  return autoBinsScott(data, options)
76
39
  }
77
40
 
78
- /**
79
- * Create a histogram texture from CPU array or GPU texture.
80
- * @param {regl} regl - regl context
81
- * @param {Float32Array | Texture} input - CPU array in [0,1] or GPU texture
82
- * @param {Object} options
83
- * - bins: number of bins (optional, overrides auto)
84
- * - useGPU: force GPU histogram
85
- * - maxBins: max number of bins for auto calculation
86
- * @returns {Texture} - histogram texture
87
- */
88
- export default function makeHistogram(regl, input, options = {}) {
89
- let bins = options.bins;
90
- const useGPU = options.useGPU || false;
91
-
92
- // Auto bins if not provided and input is CPU array
93
- if (!bins && input instanceof Float32Array) {
94
- bins = autoBins(input, { maxBins: options.maxBins || 2048 });
95
- } else if (!bins) {
96
- bins = options.maxBins || 1024; // default for GPU textures
97
- }
41
+ // Build a histogram texture from a column texture (4 values per texel).
42
+ // Assumes input values are already normalized to [0, 1].
43
+ // Returns a 4-packed texture: bins values packed 4 per texel.
44
+ export default function makeHistogram(regl, inputTex, options = {}) {
45
+ const N = inputTex._dataLength ?? inputTex.width * inputTex.height * 4
46
+ const bins = options.bins || 1024
98
47
 
99
- // Allocate histogram texture and framebuffer
100
- const histTex = regl.texture({
101
- width: bins,
102
- height: 1,
103
- type: 'float',
104
- format: 'rgba'
105
- });
106
- const histFBO = regl.framebuffer({ color: histTex });
107
-
108
- // Clear histogram
109
- regl.clear({ color: [0, 0, 0, 0], framebuffer: histFBO });
110
-
111
- if (input instanceof Float32Array && !useGPU) {
112
- // CPU histogram — pack counts into the R channel of each RGBA texel
113
- const histData = new Float32Array(bins);
114
- const N = input.length;
115
- for (let i = 0; i < N; i++) {
116
- const b = Math.floor(input[i] * bins);
117
- histData[Math.min(b, bins - 1)] += 1;
118
- }
119
- // RGBA format: 4 floats per texel. Store count in R, leave G/B/A as 0.
120
- const packedData = new Float32Array(bins * 4);
121
- for (let i = 0; i < bins; i++) packedData[i * 4] = histData[i];
122
- histTex.subimage({ data: packedData, width: bins, height: 1 });
48
+ const nTexels = Math.ceil(bins / 4)
49
+ const wTexels = Math.min(nTexels, regl.limits.maxTextureSize)
50
+ const hTexels = Math.ceil(nTexels / wTexels)
123
51
 
124
- } else {
125
- // GPU histogram
126
- const dataTex = (input instanceof Float32Array)
127
- ? regl.texture({ data: input, shape: [input.length, 1], type: 'float' })
128
- : input;
129
-
130
- const N = (input instanceof Float32Array) ? input.length : dataTex.width;
131
-
132
- const drawPoints = regl({
133
- framebuffer: histFBO,
134
- blend: { enable: true, func: { src: 'one', dst: 'one' } },
135
- vert: `
136
- precision highp float;
137
- attribute float value;
138
- void main() {
139
- float x = (floor(value * ${bins}.0) + 0.5)/${bins}.0*2.0 - 1.0;
140
- gl_Position = vec4(x, 0.0, 0.0, 1.0);
141
- gl_PointSize = 1.0;
142
- }
143
- `,
144
- frag: `
145
- precision highp float;
146
- out vec4 fragColor;
147
- void main() { fragColor = vec4(1.0, 0.0, 0.0, 1.0); }
148
- `,
149
- attributes: {
150
- value: () => (input instanceof Float32Array)
151
- ? input
152
- : Array.from({ length: N }, (_, i) => i / (N - 1))
153
- },
154
- count: N,
155
- primitive: 'points'
156
- });
52
+ const histTex = regl.texture({ width: wTexels, height: hTexels, type: 'float', format: 'rgba' })
53
+ const histFBO = regl.framebuffer({ color: histTex, depth: false, stencil: false })
54
+ regl.clear({ color: [0, 0, 0, 0], framebuffer: histFBO })
157
55
 
158
- drawPoints();
159
- }
56
+ const pickIds = new Float32Array(N)
57
+ for (let i = 0; i < N; i++) pickIds[i] = i
160
58
 
161
- return histTex;
59
+ const drawPoints = regl({
60
+ framebuffer: histFBO,
61
+ blend: { enable: true, func: { src: 'one', dst: 'one' } },
62
+ vert: `#version 300 es
63
+ precision highp float;
64
+ precision highp sampler2D;
65
+ in float a_pickId;
66
+ uniform sampler2D u_inputTex;
67
+ ${SAMPLE_COLUMN_GLSL}
68
+ out float v_chan;
69
+ void main() {
70
+ float value = sampleColumn(u_inputTex, a_pickId);
71
+ int b = int(clamp(floor(value * float(${bins})), 0.0, float(${bins - 1})));
72
+ int texelI = b / 4;
73
+ int chan = b % 4;
74
+ float tx = (float(texelI % ${wTexels}) + 0.5) / float(${wTexels}) * 2.0 - 1.0;
75
+ float ty = (float(texelI / ${wTexels}) + 0.5) / float(${hTexels}) * 2.0 - 1.0;
76
+ gl_Position = vec4(tx, ty, 0.0, 1.0);
77
+ gl_PointSize = 1.0;
78
+ v_chan = float(chan);
79
+ }`,
80
+ frag: `#version 300 es
81
+ precision highp float;
82
+ in float v_chan;
83
+ out vec4 fragColor;
84
+ void main() {
85
+ int c = int(v_chan + 0.5);
86
+ fragColor = vec4(float(c == 0), float(c == 1), float(c == 2), float(c == 3));
87
+ }`,
88
+ attributes: { a_pickId: pickIds },
89
+ uniforms: { u_inputTex: inputTex },
90
+ count: N,
91
+ primitive: 'points'
92
+ })
93
+
94
+ drawPoints()
95
+ histTex._dataLength = bins
96
+ return histTex
162
97
  }
163
98
 
99
+ const TDR_STEP_MS = 500
100
+
101
+ // ─── HistogramComputation (TextureComputation — inline expression usage) ──────
164
102
  class HistogramComputation extends TextureComputation {
165
- compute(regl, params) {
166
- return makeHistogram(regl, params.input, { bins: params.bins })
103
+ async compute(regl, inputs, getAxisDomain) {
104
+ const inputCol = inputs.input // ColumnData
105
+
106
+ let bins = inputs.bins
107
+ if (!bins && inputCol instanceof ArrayColumn) {
108
+ bins = autoBins(inputCol.array, { maxBins: inputs.maxBins || 2048 })
109
+ } else if (!bins) {
110
+ bins = inputs.maxBins || 1024
111
+ }
112
+
113
+ // Normalize to [0,1] for GPU histogram (CPU work)
114
+ let normalizedTex
115
+ const t0 = performance.now()
116
+ if (inputCol instanceof ArrayColumn) {
117
+ const arr = inputCol.array
118
+ let min = arr[0], max = arr[0]
119
+ for (let i = 1; i < arr.length; i++) {
120
+ if (arr[i] < min) min = arr[i]
121
+ if (arr[i] > max) max = arr[i]
122
+ }
123
+ const range = max - min || 1
124
+ const normalized = new Float32Array(arr.length)
125
+ for (let i = 0; i < arr.length; i++) normalized[i] = (arr[i] - min) / range
126
+ normalizedTex = uploadToTexture(regl, normalized)
127
+ } else {
128
+ // Already a GPU texture — assume values are in [0,1]
129
+ normalizedTex = inputCol.toTexture(regl)
130
+ }
131
+ if (performance.now() - t0 > TDR_STEP_MS)
132
+ await new Promise(r => requestAnimationFrame(r))
133
+
134
+ return makeHistogram(regl, normalizedTex, { bins })
167
135
  }
136
+
168
137
  schema(data) {
169
138
  return {
170
139
  type: 'object',
140
+ title: 'histogram',
171
141
  properties: {
172
142
  input: EXPRESSION_REF,
173
143
  bins: { type: 'number' }
@@ -178,3 +148,138 @@ class HistogramComputation extends TextureComputation {
178
148
  }
179
149
 
180
150
  registerTextureComputation('histogram', new HistogramComputation())
151
+
152
+ // ─── HistogramData (ComputedData — top-level transform) ───────────────────────
153
+ class HistogramData extends ComputedData {
154
+ columns() { return ['binCenters', 'counts'] }
155
+
156
+ filterAxes(params, data) {
157
+ if (!params.filter) return {}
158
+ const qk = data ? (data.getQuantityKind(params.filter) ?? params.filter) : params.filter
159
+ return { filter: qk }
160
+ }
161
+
162
+ async compute(regl, params, data, getAxisDomain) {
163
+ const srcCol = data.getData(params.input)
164
+ if (!(srcCol instanceof ArrayColumn)) {
165
+ throw new Error(`HistogramData: input '${params.input}' must be a plain data column`)
166
+ }
167
+ const srcV = srcCol.array
168
+
169
+ let min = Infinity, max = -Infinity
170
+ for (let i = 0; i < srcV.length; i++) {
171
+ if (srcV[i] < min) min = srcV[i]
172
+ if (srcV[i] > max) max = srcV[i]
173
+ }
174
+ const range = max - min || 1
175
+
176
+ const bins = params.bins || Math.max(10, Math.min(200, Math.ceil(Math.sqrt(srcV.length))))
177
+ const binWidth = range / bins
178
+
179
+ // Normalize full input to [0,1] for bin assignment.
180
+ const normalized = new Float32Array(srcV.length)
181
+ for (let i = 0; i < srcV.length; i++) normalized[i] = (srcV[i] - min) / range
182
+
183
+ // Build optional filter mask and track filter axis for axis-reactivity.
184
+ let filterMask = null
185
+ const filterDataExtents = {}
186
+ if (params.filter) {
187
+ const filterCol = data.getData(params.filter)
188
+ if (!(filterCol instanceof ArrayColumn)) {
189
+ throw new Error(`HistogramData: filter '${params.filter}' must be a plain data column`)
190
+ }
191
+ const filterArr = filterCol.array
192
+ const filterQK = data.getQuantityKind(params.filter) ?? params.filter
193
+
194
+ let fMin = filterArr[0], fMax = filterArr[0]
195
+ for (let i = 1; i < filterArr.length; i++) {
196
+ if (filterArr[i] < fMin) fMin = filterArr[i]
197
+ if (filterArr[i] > fMax) fMax = filterArr[i]
198
+ }
199
+ filterDataExtents[filterQK] = [fMin, fMax]
200
+
201
+ // Calling getAxisDomain registers the filter axis as an accessed axis so the
202
+ // ComputedDataNode recomputes whenever the filter range changes.
203
+ const domain = getAxisDomain(filterQK)
204
+ const fRangeMin = domain?.[0] ?? null
205
+ const fRangeMax = domain?.[1] ?? null
206
+
207
+ if (fRangeMin !== null || fRangeMax !== null) {
208
+ filterMask = new Uint8Array(srcV.length)
209
+ for (let i = 0; i < filterArr.length; i++) {
210
+ if (fRangeMin !== null && filterArr[i] < fRangeMin) continue
211
+ if (fRangeMax !== null && filterArr[i] > fRangeMax) continue
212
+ filterMask[i] = 1
213
+ }
214
+ }
215
+ }
216
+
217
+ // Bin centers always span the full input range so bars don't shift when filtering.
218
+ const nTexels = Math.ceil(bins / 4)
219
+ const centersW = Math.min(nTexels, regl.limits.maxTextureSize)
220
+ const centersH = Math.ceil(nTexels / centersW)
221
+ const centersData = new Float32Array(centersW * centersH * 4)
222
+ for (let i = 0; i < bins; i++) centersData[i] = min + (i + 0.5) * binWidth
223
+ const binCentersTex = regl.texture({ data: centersData, shape: [centersW, centersH], type: 'float', format: 'rgba' })
224
+ binCentersTex._dataLength = bins
225
+
226
+ // Build histogram from filtered (or full) normalized values.
227
+ let countInput = normalized
228
+ if (filterMask) {
229
+ const filtered = []
230
+ for (let i = 0; i < srcV.length; i++) {
231
+ if (filterMask[i]) filtered.push(normalized[i])
232
+ }
233
+ countInput = new Float32Array(filtered)
234
+ }
235
+ const uploadStart = performance.now()
236
+ const normalizedTex = uploadToTexture(regl, countInput)
237
+ if (performance.now() - uploadStart > TDR_STEP_MS)
238
+ await new Promise(r => requestAnimationFrame(r))
239
+ const countsTex = makeHistogram(regl, normalizedTex, { bins })
240
+ countsTex._dataLength = bins
241
+
242
+ const histCpu = new Float32Array(bins)
243
+ for (let i = 0; i < countInput.length; i++) {
244
+ histCpu[Math.min(Math.floor(countInput[i] * bins), bins - 1)] += 1
245
+ }
246
+ const maxCount = Math.max(...histCpu, 0)
247
+
248
+ const xQK = (typeof params.input === 'string' && data)
249
+ ? (data.getQuantityKind(params.input) ?? params.input)
250
+ : null
251
+
252
+ return {
253
+ binCenters: binCentersTex,
254
+ counts: countsTex,
255
+ _meta: {
256
+ domains: {
257
+ binCenters: [min, max],
258
+ counts: [0, maxCount],
259
+ },
260
+ quantityKinds: {
261
+ binCenters: xQK,
262
+ counts: 'count',
263
+ },
264
+ binHalfWidth: binWidth / 2,
265
+ filterDataExtents,
266
+ }
267
+ }
268
+ }
269
+
270
+ schema(data) {
271
+ const cols = data ? data.columns() : []
272
+ return {
273
+ type: 'object',
274
+ title: 'HistogramData',
275
+ properties: {
276
+ input: { type: 'string', enum: cols, description: 'Input data column' },
277
+ bins: { type: 'integer', description: 'Number of bins (0 for auto)', default: 0 },
278
+ filter: { ...EXPRESSION_REF_OPT, description: 'Filter column — registers a filter axis (null for none)' }
279
+ },
280
+ required: ['input', 'filter']
281
+ }
282
+ }
283
+ }
284
+
285
+ registerComputedData('HistogramData', new HistogramData())
@@ -1,71 +1,76 @@
1
- import { registerTextureComputation, TextureComputation, EXPRESSION_REF } from "./ComputationRegistry.js"
1
+ import { registerTextureComputation, EXPRESSION_REF, resolveQuantityKind } from "./ComputationRegistry.js"
2
+ import { TextureComputation } from "../data/Computation.js"
3
+ import { SAMPLE_COLUMN_GLSL } from "../data/ColumnData.js"
2
4
 
3
5
  /**
4
6
  * Smooth a histogram to produce a KDE texture
5
7
  * @param {regl} regl - regl context
6
- * @param {Float32Array | Texture} histInput - histogram data
8
+ * @param {Texture} histTex - 4-packed histogram texture (_dataLength = bins)
7
9
  * @param {Object} options
8
10
  * - bandwidth: Gaussian sigma in bins (default 5)
9
- * - bins: output bins (default same as input)
10
- * @returns {Texture} - smoothed KDE texture
11
+ * - bins: output bins (default same as input via _dataLength)
12
+ * @returns {Texture} - smoothed KDE texture (4-packed, _dataLength = bins)
11
13
  */
12
- export default function smoothKDE(regl, histInput, options = {}) {
13
- const bins = options.bins || (histInput instanceof Float32Array ? histInput.length : histInput.width);
14
- const bandwidth = options.bandwidth || 5.0;
14
+ export default function smoothKDE(regl, histTex, options = {}) {
15
+ const bins = options.bins || histTex._dataLength || histTex.width * 4
16
+ const bandwidth = options.bandwidth || 5.0
15
17
 
16
- const histTex = (histInput instanceof Float32Array)
17
- ? regl.texture({ data: histInput, shape: [bins, 1], type: 'float' })
18
- : histInput;
18
+ const nTexels = Math.ceil(bins / 4)
19
+ const wTexels = Math.min(nTexels, regl.limits.maxTextureSize)
20
+ const hTexels = Math.ceil(nTexels / wTexels)
19
21
 
20
- const kdeTex = regl.texture({ width: bins, height: 1, type: 'float', format: 'rgba' });
21
- const kdeFBO = regl.framebuffer({ color: kdeTex });
22
+ const kdeTex = regl.texture({ width: wTexels, height: hTexels, type: 'float', format: 'rgba' })
23
+ const kdeFBO = regl.framebuffer({ color: kdeTex, depth: false, stencil: false })
22
24
 
23
- const kernelRadius = Math.ceil(bandwidth * 3);
24
- const kernelSize = kernelRadius * 2 + 1;
25
- const kernel = new Float32Array(kernelSize);
26
- let sum = 0;
25
+ const kernelRadius = Math.ceil(bandwidth * 3)
26
+ const kernelSize = kernelRadius * 2 + 1
27
+ const kernel = new Float32Array(kernelSize)
28
+ let sum = 0
27
29
  for (let i = -kernelRadius; i <= kernelRadius; i++) {
28
- const w = Math.exp(-0.5 * (i / bandwidth) ** 2);
29
- kernel[i + kernelRadius] = w;
30
- sum += w;
30
+ const w = Math.exp(-0.5 * (i / bandwidth) ** 2)
31
+ kernel[i + kernelRadius] = w
32
+ sum += w
31
33
  }
32
- for (let i = 0; i < kernel.length; i++) kernel[i] /= sum;
34
+ for (let i = 0; i < kernel.length; i++) kernel[i] /= sum
33
35
 
34
- const kernelTex = regl.texture({ data: kernel, shape: [kernelSize, 1], type: 'float' });
36
+ // Kernel texture stays R-channel (internal, not exposed via sampleColumn)
37
+ const kernelData = new Float32Array(kernelSize * 4)
38
+ for (let i = 0; i < kernelSize; i++) kernelData[i * 4] = kernel[i]
39
+ const kernelTex = regl.texture({ data: kernelData, shape: [kernelSize, 1], type: 'float', format: 'rgba' })
35
40
 
36
41
  const drawKDE = regl({
37
42
  framebuffer: kdeFBO,
38
- vert: `
43
+ vert: `#version 300 es
39
44
  precision highp float;
40
- attribute float bin;
41
- void main() {
42
- float x = (bin + 0.5)/${bins}.0*2.0 - 1.0;
43
- gl_Position = vec4(x, 0.0, 0.0, 1.0);
44
- }
45
+ in vec2 position;
46
+ void main() { gl_Position = vec4(position, 0.0, 1.0); }
45
47
  `,
46
- frag: `
47
- #version 300 es
48
+ frag: `#version 300 es
48
49
  precision highp float;
50
+ precision highp sampler2D;
49
51
  uniform sampler2D histTex;
50
52
  uniform sampler2D kernelTex;
51
53
  uniform int kernelRadius;
52
54
  uniform int bins;
53
55
  out vec4 fragColor;
56
+ ${SAMPLE_COLUMN_GLSL}
54
57
  void main() {
55
- float idx = gl_FragCoord.x - 0.5;
56
- float sum = 0.0;
57
- for (int i=-16; i<=16; i++) {
58
- if (i+16 >= kernelRadius*2+1) break;
59
- int sampleIdx = int(clamp(idx + float(i), 0.0, float(bins-1)));
60
- float h = texelFetch(histTex, ivec2(sampleIdx,0),0).r;
61
- float w = texelFetch(kernelTex, ivec2(i+kernelRadius,0),0).r;
62
- sum += h * w;
58
+ int texelI = int(gl_FragCoord.y) * ${wTexels} + int(gl_FragCoord.x);
59
+ int base = texelI * 4;
60
+ float s0 = 0.0, s1 = 0.0, s2 = 0.0, s3 = 0.0;
61
+ for (int i = -16; i <= 16; i++) {
62
+ if (i + 16 >= kernelRadius * 2 + 1) break;
63
+ float kw = texelFetch(kernelTex, ivec2(i + kernelRadius, 0), 0).r;
64
+ s0 += sampleColumn(histTex, float(clamp(base + 0 + i, 0, bins - 1))) * kw;
65
+ s1 += sampleColumn(histTex, float(clamp(base + 1 + i, 0, bins - 1))) * kw;
66
+ s2 += sampleColumn(histTex, float(clamp(base + 2 + i, 0, bins - 1))) * kw;
67
+ s3 += sampleColumn(histTex, float(clamp(base + 3 + i, 0, bins - 1))) * kw;
63
68
  }
64
- fragColor = vec4(sum, 0.0, 0.0, 1.0);
69
+ fragColor = vec4(s0, s1, s2, s3);
65
70
  }
66
71
  `,
67
72
  attributes: {
68
- bin: Array.from({ length: bins }, (_, i) => i)
73
+ position: [[-1, -1], [1, -1], [-1, 1], [1, 1]]
69
74
  },
70
75
  uniforms: {
71
76
  histTex,
@@ -73,22 +78,35 @@ export default function smoothKDE(regl, histInput, options = {}) {
73
78
  kernelRadius,
74
79
  bins
75
80
  },
76
- count: bins,
77
- primitive: 'points'
78
- });
81
+ count: 4,
82
+ primitive: 'triangle strip'
83
+ })
79
84
 
80
- drawKDE();
85
+ drawKDE()
81
86
 
82
- return kdeTex;
87
+ kdeTex._dataLength = bins
88
+ return kdeTex
83
89
  }
84
90
 
91
+ const TDR_STEP_MS = 500
92
+
85
93
  class KdeComputation extends TextureComputation {
86
- compute(regl, params) {
87
- return smoothKDE(regl, params.input, { bins: params.bins, bandwidth: params.bandwidth })
94
+ async compute(regl, inputs, getAxisDomain) {
95
+ const t0 = performance.now()
96
+ const inputTex = inputs.input.toTexture(regl)
97
+ if (performance.now() - t0 > TDR_STEP_MS)
98
+ await new Promise(r => requestAnimationFrame(r))
99
+ return smoothKDE(regl, inputTex, { bins: inputs.bins, bandwidth: inputs.bandwidth })
88
100
  }
101
+
102
+ getQuantityKind(params, data) {
103
+ return resolveQuantityKind(params.input, data)
104
+ }
105
+
89
106
  schema(data) {
90
107
  return {
91
108
  type: 'object',
109
+ title: 'kde',
92
110
  properties: {
93
111
  input: EXPRESSION_REF,
94
112
  bins: { type: 'number' },