gladly-plot 0.0.4 → 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 (60) hide show
  1. package/README.md +9 -2
  2. package/package.json +10 -11
  3. package/src/axes/Axis.js +401 -0
  4. package/src/{AxisLink.js → axes/AxisLink.js} +6 -2
  5. package/src/{AxisQuantityKindRegistry.js → axes/AxisQuantityKindRegistry.js} +7 -0
  6. package/src/axes/AxisRegistry.js +179 -0
  7. package/src/axes/Camera.js +47 -0
  8. package/src/axes/ColorAxisRegistry.js +101 -0
  9. package/src/{FilterAxisRegistry.js → axes/FilterAxisRegistry.js} +63 -0
  10. package/src/axes/TickLabelAtlas.js +99 -0
  11. package/src/axes/ZoomController.js +463 -0
  12. package/src/colorscales/BivariateColorscales.js +205 -0
  13. package/src/colorscales/ColorscaleRegistry.js +144 -0
  14. package/src/compute/ComputationRegistry.js +179 -0
  15. package/src/compute/axisFilter.js +59 -0
  16. package/src/compute/conv.js +286 -0
  17. package/src/compute/elementwise.js +72 -0
  18. package/src/compute/fft.js +378 -0
  19. package/src/compute/filter.js +229 -0
  20. package/src/compute/hist.js +285 -0
  21. package/src/compute/kde.js +120 -0
  22. package/src/compute/scatter2dInterpolate.js +277 -0
  23. package/src/compute/util.js +196 -0
  24. package/src/core/ComputePipeline.js +153 -0
  25. package/src/core/GlBase.js +141 -0
  26. package/src/core/Layer.js +59 -0
  27. package/src/core/LayerType.js +433 -0
  28. package/src/core/Plot.js +1213 -0
  29. package/src/core/PlotGroup.js +204 -0
  30. package/src/core/ShaderQueue.js +73 -0
  31. package/src/data/ColumnData.js +269 -0
  32. package/src/data/Computation.js +95 -0
  33. package/src/data/Data.js +270 -0
  34. package/src/{Colorbar.js → floats/Colorbar.js} +19 -5
  35. package/src/floats/Colorbar2d.js +77 -0
  36. package/src/{Filterbar.js → floats/Filterbar.js} +18 -4
  37. package/src/{FilterbarFloat.js → floats/Float.js} +73 -30
  38. package/src/{EpsgUtils.js → geo/EpsgUtils.js} +1 -1
  39. package/src/index.js +47 -22
  40. package/src/layers/BarsLayer.js +168 -0
  41. package/src/{ColorbarLayer.js → layers/ColorbarLayer.js} +12 -16
  42. package/src/layers/ColorbarLayer2d.js +86 -0
  43. package/src/{FilterbarLayer.js → layers/FilterbarLayer.js} +6 -5
  44. package/src/layers/LinesLayer.js +185 -0
  45. package/src/layers/PointsLayer.js +118 -0
  46. package/src/layers/ScatterShared.js +98 -0
  47. package/src/{TileLayer.js → layers/TileLayer.js} +24 -20
  48. package/src/math/mat4.js +100 -0
  49. package/src/Axis.js +0 -48
  50. package/src/AxisRegistry.js +0 -54
  51. package/src/ColorAxisRegistry.js +0 -49
  52. package/src/ColorscaleRegistry.js +0 -52
  53. package/src/Data.js +0 -67
  54. package/src/Float.js +0 -159
  55. package/src/Layer.js +0 -44
  56. package/src/LayerType.js +0 -209
  57. package/src/Plot.js +0 -1073
  58. package/src/ScatterLayer.js +0 -287
  59. /package/src/{MatplotlibColorscales.js → colorscales/MatplotlibColorscales.js} +0 -0
  60. /package/src/{LayerTypeRegistry.js → core/LayerTypeRegistry.js} +0 -0
@@ -0,0 +1,285 @@
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"
4
+
5
+ function autoBinsScott(data, options = {}) {
6
+ const N = data.length
7
+ const sampleCutoff = options.sampleCutoff || 50000
8
+ let sampleData
9
+ if (N > sampleCutoff) {
10
+ sampleData = new Float32Array(sampleCutoff)
11
+ for (let i = 0; i < sampleCutoff; i++) {
12
+ sampleData[i] = data[Math.floor(Math.random() * N)]
13
+ }
14
+ } else {
15
+ sampleData = data
16
+ }
17
+ const n = sampleData.length
18
+ const mean = sampleData.reduce((a, b) => a + b, 0) / n
19
+ let std
20
+ if (N <= sampleCutoff) {
21
+ std = Math.sqrt(sampleData.reduce((a, b) => a + (b - mean) ** 2, 0) / n)
22
+ } else {
23
+ std = Math.sqrt(sampleData.reduce((a, b) => a + (b - mean) ** 2, 0) / (n - 1))
24
+ }
25
+ const binWidth = 3.5 * std / Math.cbrt(N)
26
+ let min = data[0], max = data[0]
27
+ for (let i = 1; i < data.length; i++) {
28
+ if (data[i] < min) min = data[i]
29
+ if (data[i] > max) max = data[i]
30
+ }
31
+ return Math.max(1, Math.ceil((max - min) / binWidth))
32
+ }
33
+
34
+ function autoBins(data, options = {}) {
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)
38
+ return autoBinsScott(data, options)
39
+ }
40
+
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
47
+
48
+ const nTexels = Math.ceil(bins / 4)
49
+ const wTexels = Math.min(nTexels, regl.limits.maxTextureSize)
50
+ const hTexels = Math.ceil(nTexels / wTexels)
51
+
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 })
55
+
56
+ const pickIds = new Float32Array(N)
57
+ for (let i = 0; i < N; i++) pickIds[i] = i
58
+
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
97
+ }
98
+
99
+ const TDR_STEP_MS = 500
100
+
101
+ // ─── HistogramComputation (TextureComputation — inline expression usage) ──────
102
+ class HistogramComputation extends TextureComputation {
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 })
135
+ }
136
+
137
+ schema(data) {
138
+ return {
139
+ type: 'object',
140
+ title: 'histogram',
141
+ properties: {
142
+ input: EXPRESSION_REF,
143
+ bins: { type: 'number' }
144
+ },
145
+ required: ['input']
146
+ }
147
+ }
148
+ }
149
+
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())
@@ -0,0 +1,120 @@
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"
4
+
5
+ /**
6
+ * Smooth a histogram to produce a KDE texture
7
+ * @param {regl} regl - regl context
8
+ * @param {Texture} histTex - 4-packed histogram texture (_dataLength = bins)
9
+ * @param {Object} options
10
+ * - bandwidth: Gaussian sigma in bins (default 5)
11
+ * - bins: output bins (default same as input via _dataLength)
12
+ * @returns {Texture} - smoothed KDE texture (4-packed, _dataLength = bins)
13
+ */
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
17
+
18
+ const nTexels = Math.ceil(bins / 4)
19
+ const wTexels = Math.min(nTexels, regl.limits.maxTextureSize)
20
+ const hTexels = Math.ceil(nTexels / wTexels)
21
+
22
+ const kdeTex = regl.texture({ width: wTexels, height: hTexels, type: 'float', format: 'rgba' })
23
+ const kdeFBO = regl.framebuffer({ color: kdeTex, depth: false, stencil: false })
24
+
25
+ const kernelRadius = Math.ceil(bandwidth * 3)
26
+ const kernelSize = kernelRadius * 2 + 1
27
+ const kernel = new Float32Array(kernelSize)
28
+ let sum = 0
29
+ for (let i = -kernelRadius; i <= kernelRadius; i++) {
30
+ const w = Math.exp(-0.5 * (i / bandwidth) ** 2)
31
+ kernel[i + kernelRadius] = w
32
+ sum += w
33
+ }
34
+ for (let i = 0; i < kernel.length; i++) kernel[i] /= sum
35
+
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' })
40
+
41
+ const drawKDE = regl({
42
+ framebuffer: kdeFBO,
43
+ vert: `#version 300 es
44
+ precision highp float;
45
+ in vec2 position;
46
+ void main() { gl_Position = vec4(position, 0.0, 1.0); }
47
+ `,
48
+ frag: `#version 300 es
49
+ precision highp float;
50
+ precision highp sampler2D;
51
+ uniform sampler2D histTex;
52
+ uniform sampler2D kernelTex;
53
+ uniform int kernelRadius;
54
+ uniform int bins;
55
+ out vec4 fragColor;
56
+ ${SAMPLE_COLUMN_GLSL}
57
+ void main() {
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;
68
+ }
69
+ fragColor = vec4(s0, s1, s2, s3);
70
+ }
71
+ `,
72
+ attributes: {
73
+ position: [[-1, -1], [1, -1], [-1, 1], [1, 1]]
74
+ },
75
+ uniforms: {
76
+ histTex,
77
+ kernelTex,
78
+ kernelRadius,
79
+ bins
80
+ },
81
+ count: 4,
82
+ primitive: 'triangle strip'
83
+ })
84
+
85
+ drawKDE()
86
+
87
+ kdeTex._dataLength = bins
88
+ return kdeTex
89
+ }
90
+
91
+ const TDR_STEP_MS = 500
92
+
93
+ class KdeComputation extends TextureComputation {
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 })
100
+ }
101
+
102
+ getQuantityKind(params, data) {
103
+ return resolveQuantityKind(params.input, data)
104
+ }
105
+
106
+ schema(data) {
107
+ return {
108
+ type: 'object',
109
+ title: 'kde',
110
+ properties: {
111
+ input: EXPRESSION_REF,
112
+ bins: { type: 'number' },
113
+ bandwidth: { type: 'number' }
114
+ },
115
+ required: ['input']
116
+ }
117
+ }
118
+ }
119
+
120
+ registerTextureComputation('kde', new KdeComputation())
@@ -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())