gladly-plot 0.0.4 → 0.0.5

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 (43) hide show
  1. package/package.json +2 -2
  2. package/src/axes/Axis.js +253 -0
  3. package/src/{AxisQuantityKindRegistry.js → axes/AxisQuantityKindRegistry.js} +7 -0
  4. package/src/{AxisRegistry.js → axes/AxisRegistry.js} +48 -0
  5. package/src/axes/ColorAxisRegistry.js +93 -0
  6. package/src/{FilterAxisRegistry.js → axes/FilterAxisRegistry.js} +63 -0
  7. package/src/axes/ZoomController.js +141 -0
  8. package/src/colorscales/BivariateColorscales.js +205 -0
  9. package/src/colorscales/ColorscaleRegistry.js +124 -0
  10. package/src/compute/ComputationRegistry.js +237 -0
  11. package/src/compute/axisFilter.js +47 -0
  12. package/src/compute/conv.js +230 -0
  13. package/src/compute/fft.js +292 -0
  14. package/src/compute/filter.js +227 -0
  15. package/src/compute/hist.js +180 -0
  16. package/src/compute/kde.js +102 -0
  17. package/src/{Layer.js → core/Layer.js} +4 -3
  18. package/src/{LayerType.js → core/LayerType.js} +72 -7
  19. package/src/core/Plot.js +735 -0
  20. package/src/{Colorbar.js → floats/Colorbar.js} +19 -5
  21. package/src/floats/Colorbar2d.js +77 -0
  22. package/src/{Filterbar.js → floats/Filterbar.js} +18 -4
  23. package/src/{FilterbarFloat.js → floats/Float.js} +17 -30
  24. package/src/{EpsgUtils.js → geo/EpsgUtils.js} +1 -1
  25. package/src/index.js +35 -22
  26. package/src/{ColorbarLayer.js → layers/ColorbarLayer.js} +2 -2
  27. package/src/layers/ColorbarLayer2d.js +97 -0
  28. package/src/{FilterbarLayer.js → layers/FilterbarLayer.js} +2 -2
  29. package/src/layers/HistogramLayer.js +212 -0
  30. package/src/layers/LinesLayer.js +199 -0
  31. package/src/layers/PointsLayer.js +114 -0
  32. package/src/layers/ScatterShared.js +142 -0
  33. package/src/{TileLayer.js → layers/TileLayer.js} +4 -4
  34. package/src/Axis.js +0 -48
  35. package/src/ColorAxisRegistry.js +0 -49
  36. package/src/ColorscaleRegistry.js +0 -52
  37. package/src/Float.js +0 -159
  38. package/src/Plot.js +0 -1073
  39. package/src/ScatterLayer.js +0 -287
  40. /package/src/{AxisLink.js → axes/AxisLink.js} +0 -0
  41. /package/src/{MatplotlibColorscales.js → colorscales/MatplotlibColorscales.js} +0 -0
  42. /package/src/{Data.js → core/Data.js} +0 -0
  43. /package/src/{LayerTypeRegistry.js → core/LayerTypeRegistry.js} +0 -0
@@ -0,0 +1,227 @@
1
+ import { registerTextureComputation, TextureComputation, EXPRESSION_REF } from "./ComputationRegistry.js"
2
+
3
+ function toTexture(regl, input, length) {
4
+ if (input instanceof Float32Array) {
5
+ return regl.texture({ data: input, shape: [length, 1], type: 'float', format: 'rgba' });
6
+ }
7
+ return input; // already a texture
8
+ }
9
+
10
+ function subtractTextures(regl, texA, texB) {
11
+ const length = texA.width;
12
+ const outputTex = regl.texture({ width: length, height: 1, type: 'float', format: 'rgba' });
13
+ const outputFBO = regl.framebuffer({ color: outputTex });
14
+
15
+ const drawSub = regl({
16
+ framebuffer: outputFBO,
17
+ vert: `
18
+ precision highp float;
19
+ attribute float bin;
20
+ void main() {
21
+ float x = (bin + 0.5)/${length}.0*2.0 - 1.0;
22
+ gl_Position = vec4(x,0.0,0.0,1.0);
23
+ }
24
+ `,
25
+ frag: `
26
+ precision highp float;
27
+ uniform sampler2D texA;
28
+ uniform sampler2D texB;
29
+ out vec4 fragColor;
30
+ void main() {
31
+ int idx = int(gl_FragCoord.x-0.5);
32
+ float a = texelFetch(texA, ivec2(idx,0),0).r;
33
+ float b = texelFetch(texB, ivec2(idx,0),0).r;
34
+ fragColor = vec4(a-b,0.0,0.0,1.0);
35
+ }
36
+ `,
37
+ attributes: { bin: Array.from({ length }, (_, i) => i) },
38
+ uniforms: { texA, texB },
39
+ count: length,
40
+ primitive: 'points'
41
+ });
42
+
43
+ drawSub();
44
+ return outputTex;
45
+ }
46
+
47
+ /**
48
+ * Generic 1D convolution filter
49
+ * @param {regl} regl - regl context
50
+ * @param {Float32Array | Texture} input - CPU array or GPU texture
51
+ * @param {Float32Array} kernel - 1D kernel
52
+ * @returns {Texture} - filtered output texture
53
+ */
54
+ function filter1D(regl, input, kernel) {
55
+ const length = input instanceof Float32Array ? input.length : input.width;
56
+ const inputTex = toTexture(regl, input, length);
57
+
58
+ const kernelTex = regl.texture({ data: kernel, shape: [kernel.length, 1], type: 'float' });
59
+
60
+ const outputTex = regl.texture({ width: length, height: 1, type: 'float', format: 'rgba' });
61
+ const outputFBO = regl.framebuffer({ color: outputTex });
62
+
63
+ const radius = Math.floor(kernel.length / 2);
64
+
65
+ const drawFilter = regl({
66
+ framebuffer: outputFBO,
67
+ vert: `
68
+ precision highp float;
69
+ attribute float bin;
70
+ void main() {
71
+ float x = (bin + 0.5)/${length}.0*2.0 - 1.0;
72
+ gl_Position = vec4(x, 0.0, 0.0, 1.0);
73
+ }
74
+ `,
75
+ frag: `
76
+ #version 300 es
77
+ precision highp float;
78
+ uniform sampler2D inputTex;
79
+ uniform sampler2D kernelTex;
80
+ uniform int radius;
81
+ uniform int length;
82
+ out vec4 fragColor;
83
+
84
+ void main() {
85
+ float idx = gl_FragCoord.x - 0.5;
86
+ float sum = 0.0;
87
+ for (int i=-16; i<=16; i++) { // max kernel radius 16
88
+ if (i+16 >= radius*2+1) break;
89
+ int sampleIdx = int(clamp(idx + float(i), 0.0, float(length-1)));
90
+ float val = texelFetch(inputTex, ivec2(sampleIdx,0),0).r;
91
+ float w = texelFetch(kernelTex, ivec2(i+radius,0),0).r;
92
+ sum += val * w;
93
+ }
94
+ fragColor = vec4(sum,0.0,0.0,1.0);
95
+ }
96
+ `,
97
+ attributes: {
98
+ bin: Array.from({ length }, (_, i) => i)
99
+ },
100
+ uniforms: {
101
+ inputTex,
102
+ kernelTex,
103
+ radius,
104
+ length
105
+ },
106
+ count: length,
107
+ primitive: 'points'
108
+ });
109
+
110
+ drawFilter();
111
+ return outputTex;
112
+ }
113
+
114
+ // Gaussian kernel helper
115
+ function gaussianKernel(size, sigma) {
116
+ const radius = Math.floor(size / 2);
117
+ const kernel = new Float32Array(size);
118
+ let sum = 0;
119
+ for (let i = -radius; i <= radius; i++) {
120
+ const v = Math.exp(-0.5 * (i / sigma) ** 2);
121
+ kernel[i + radius] = v;
122
+ sum += v;
123
+ }
124
+ for (let i = 0; i < size; i++) kernel[i] /= sum;
125
+ return kernel;
126
+ }
127
+
128
+ /**
129
+ * Low-pass filter
130
+ */
131
+ function lowPass(regl, input, sigma = 3, kernelSize = null) {
132
+ const size = kernelSize || Math.ceil(sigma*6)|1; // ensure odd
133
+ const kernel = gaussianKernel(size, sigma);
134
+ return filter1D(regl, input, kernel);
135
+ }
136
+
137
+ /**
138
+ * High-pass filter: subtract low-pass
139
+ */
140
+ function highPass(regl, input, sigma = 3, kernelSize = null) {
141
+ const low = lowPass(regl, input, sigma, kernelSize);
142
+ // high = input - low (using a shader)
143
+ return subtractTextures(regl, input, low);
144
+ }
145
+
146
+ /**
147
+ * Band-pass filter: difference of low-pass filters
148
+ */
149
+ function bandPass(regl, input, sigmaLow, sigmaHigh) {
150
+ const lowHigh = lowPass(regl, input, sigmaHigh);
151
+ const lowLow = lowPass(regl, input, sigmaLow);
152
+ return subtractTextures(regl, lowHigh, lowLow);
153
+ }
154
+
155
+ export { filter1D, gaussianKernel, lowPass, highPass, bandPass }
156
+
157
+ class Filter1DComputation extends TextureComputation {
158
+ compute(regl, params) {
159
+ return filter1D(regl, params.input, params.kernel)
160
+ }
161
+ schema(data) {
162
+ return {
163
+ type: 'object',
164
+ properties: {
165
+ input: EXPRESSION_REF,
166
+ kernel: EXPRESSION_REF
167
+ },
168
+ required: ['input', 'kernel']
169
+ }
170
+ }
171
+ }
172
+
173
+ class LowPassComputation extends TextureComputation {
174
+ compute(regl, params) {
175
+ return lowPass(regl, params.input, params.sigma, params.kernelSize)
176
+ }
177
+ schema(data) {
178
+ return {
179
+ type: 'object',
180
+ properties: {
181
+ input: EXPRESSION_REF,
182
+ sigma: { type: 'number' },
183
+ kernelSize: { type: 'number' }
184
+ },
185
+ required: ['input']
186
+ }
187
+ }
188
+ }
189
+
190
+ class HighPassComputation extends TextureComputation {
191
+ compute(regl, params) {
192
+ return highPass(regl, params.input, params.sigma, params.kernelSize)
193
+ }
194
+ schema(data) {
195
+ return {
196
+ type: 'object',
197
+ properties: {
198
+ input: EXPRESSION_REF,
199
+ sigma: { type: 'number' },
200
+ kernelSize: { type: 'number' }
201
+ },
202
+ required: ['input']
203
+ }
204
+ }
205
+ }
206
+
207
+ class BandPassComputation extends TextureComputation {
208
+ compute(regl, params) {
209
+ return bandPass(regl, params.input, params.sigmaLow, params.sigmaHigh)
210
+ }
211
+ schema(data) {
212
+ return {
213
+ type: 'object',
214
+ properties: {
215
+ input: EXPRESSION_REF,
216
+ sigmaLow: { type: 'number' },
217
+ sigmaHigh: { type: 'number' }
218
+ },
219
+ required: ['input', 'sigmaLow', 'sigmaHigh']
220
+ }
221
+ }
222
+ }
223
+
224
+ registerTextureComputation('filter1D', new Filter1DComputation())
225
+ registerTextureComputation('lowPass', new LowPassComputation())
226
+ registerTextureComputation('highPass', new HighPassComputation())
227
+ registerTextureComputation('bandPass', new BandPassComputation())
@@ -0,0 +1,180 @@
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;
16
+
17
+ if (N > sampleCutoff) {
18
+ // Randomly sample sampleCutoff points
19
+ sampleData = new Float32Array(sampleCutoff);
20
+ for (let i = 0; i < sampleCutoff; i++) {
21
+ const idx = Math.floor(Math.random() * N);
22
+ sampleData[i] = data[idx];
23
+ }
24
+ } else {
25
+ sampleData = data;
26
+ }
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;
35
+ 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);
39
+ } 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);
43
+ }
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];
50
+ for (let i = 1; i < data.length; i++) {
51
+ if (data[i] < min) min = data[i];
52
+ if (data[i] > max) max = data[i];
53
+ }
54
+ const bins = Math.max(1, Math.ceil((max - min) / binWidth));
55
+
56
+ return bins;
57
+ }
58
+
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
+ 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
+
75
+ return autoBinsScott(data, options)
76
+ }
77
+
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
+ }
98
+
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 });
123
+
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
+ });
157
+
158
+ drawPoints();
159
+ }
160
+
161
+ return histTex;
162
+ }
163
+
164
+ class HistogramComputation extends TextureComputation {
165
+ compute(regl, params) {
166
+ return makeHistogram(regl, params.input, { bins: params.bins })
167
+ }
168
+ schema(data) {
169
+ return {
170
+ type: 'object',
171
+ properties: {
172
+ input: EXPRESSION_REF,
173
+ bins: { type: 'number' }
174
+ },
175
+ required: ['input']
176
+ }
177
+ }
178
+ }
179
+
180
+ registerTextureComputation('histogram', new HistogramComputation())
@@ -0,0 +1,102 @@
1
+ import { registerTextureComputation, TextureComputation, EXPRESSION_REF } from "./ComputationRegistry.js"
2
+
3
+ /**
4
+ * Smooth a histogram to produce a KDE texture
5
+ * @param {regl} regl - regl context
6
+ * @param {Float32Array | Texture} histInput - histogram data
7
+ * @param {Object} options
8
+ * - bandwidth: Gaussian sigma in bins (default 5)
9
+ * - bins: output bins (default same as input)
10
+ * @returns {Texture} - smoothed KDE texture
11
+ */
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;
15
+
16
+ const histTex = (histInput instanceof Float32Array)
17
+ ? regl.texture({ data: histInput, shape: [bins, 1], type: 'float' })
18
+ : histInput;
19
+
20
+ const kdeTex = regl.texture({ width: bins, height: 1, type: 'float', format: 'rgba' });
21
+ const kdeFBO = regl.framebuffer({ color: kdeTex });
22
+
23
+ const kernelRadius = Math.ceil(bandwidth * 3);
24
+ const kernelSize = kernelRadius * 2 + 1;
25
+ const kernel = new Float32Array(kernelSize);
26
+ let sum = 0;
27
+ 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;
31
+ }
32
+ for (let i = 0; i < kernel.length; i++) kernel[i] /= sum;
33
+
34
+ const kernelTex = regl.texture({ data: kernel, shape: [kernelSize, 1], type: 'float' });
35
+
36
+ const drawKDE = regl({
37
+ framebuffer: kdeFBO,
38
+ vert: `
39
+ 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
+ `,
46
+ frag: `
47
+ #version 300 es
48
+ precision highp float;
49
+ uniform sampler2D histTex;
50
+ uniform sampler2D kernelTex;
51
+ uniform int kernelRadius;
52
+ uniform int bins;
53
+ out vec4 fragColor;
54
+ 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;
63
+ }
64
+ fragColor = vec4(sum, 0.0, 0.0, 1.0);
65
+ }
66
+ `,
67
+ attributes: {
68
+ bin: Array.from({ length: bins }, (_, i) => i)
69
+ },
70
+ uniforms: {
71
+ histTex,
72
+ kernelTex,
73
+ kernelRadius,
74
+ bins
75
+ },
76
+ count: bins,
77
+ primitive: 'points'
78
+ });
79
+
80
+ drawKDE();
81
+
82
+ return kdeTex;
83
+ }
84
+
85
+ class KdeComputation extends TextureComputation {
86
+ compute(regl, params) {
87
+ return smoothKDE(regl, params.input, { bins: params.bins, bandwidth: params.bandwidth })
88
+ }
89
+ schema(data) {
90
+ return {
91
+ type: 'object',
92
+ properties: {
93
+ input: EXPRESSION_REF,
94
+ bins: { type: 'number' },
95
+ bandwidth: { type: 'number' }
96
+ },
97
+ required: ['input']
98
+ }
99
+ }
100
+ }
101
+
102
+ registerTextureComputation('kde', new KdeComputation())
@@ -1,9 +1,10 @@
1
1
  export class Layer {
2
2
  constructor({ type, attributes, uniforms, nameMap = {}, domains = {}, lineWidth = 1, primitive = "points", xAxis = "xaxis_bottom", yAxis = "yaxis_left", xAxisQuantityKind, yAxisQuantityKind, colorAxes = [], filterAxes = [], vertexCount = null, instanceCount = null, attributeDivisors = {}, blend = null }) {
3
- // Validate that all attributes are typed arrays
3
+ // Validate that all attributes are non-null/undefined
4
+ // (Float32Array, regl textures, numbers, and expression objects are all valid)
4
5
  for (const [key, value] of Object.entries(attributes)) {
5
- if (!(value instanceof Float32Array)) {
6
- throw new Error(`Attribute '${key}' must be Float32Array`)
6
+ if (value == null) {
7
+ throw new Error(`Attribute '${key}' must not be null or undefined`)
7
8
  }
8
9
  }
9
10
 
@@ -1,6 +1,7 @@
1
1
  import { Layer } from "./Layer.js"
2
- import { buildColorGlsl } from "./ColorscaleRegistry.js"
3
- import { buildFilterGlsl } from "./FilterAxisRegistry.js"
2
+ import { buildColorGlsl } from "../colorscales/ColorscaleRegistry.js"
3
+ import { buildFilterGlsl } from "../axes/FilterAxisRegistry.js"
4
+ import { resolveAttributeExpr } from "../compute/ComputationRegistry.js"
4
5
 
5
6
  function buildSpatialGlsl() {
6
7
  return `float normalize_axis(float v, vec2 domain, float scaleType) {
@@ -30,6 +31,22 @@ vec4 gladly_apply_color(vec4 color) {
30
31
  }`
31
32
  }
32
33
 
34
+ // Removes `attribute [precision] type varName;` from GLSL source.
35
+ function removeAttributeDecl(src, varName) {
36
+ return src.replace(
37
+ new RegExp(`[ \\t]*attribute\\s+(?:(?:lowp|mediump|highp)\\s+)?\\w+\\s+${varName}\\s*;[ \\t]*\\n?`, 'g'),
38
+ ''
39
+ )
40
+ }
41
+
42
+ // Injects code immediately after `void main() {`.
43
+ function injectIntoMainStart(src, code) {
44
+ return src.replace(
45
+ /void\s+main\s*\(\s*(?:void\s*)?\)\s*\{/,
46
+ match => `${match}\n ${code}`
47
+ )
48
+ }
49
+
33
50
  function injectPickIdAssignment(src) {
34
51
  const lastBrace = src.lastIndexOf('}')
35
52
  if (lastBrace === -1) return src
@@ -79,29 +96,75 @@ export class LayerType {
79
96
  if (createDrawCommand) this.createDrawCommand = createDrawCommand
80
97
  }
81
98
 
82
- createDrawCommand(regl, layer) {
99
+ createDrawCommand(regl, layer, plot) {
83
100
  const nm = layer.nameMap
84
101
  // Rename an internal name to the shader-visible name via nameMap (identity if absent).
85
102
  const shaderName = (internalName) => nm[internalName] ?? internalName
86
103
  // Build a single-entry uniform object with renamed key reading from the internal prop name.
87
104
  const u = (internalName) => ({ [shaderName(internalName)]: regl.prop(internalName) })
88
105
 
106
+ // --- Resolve computed attributes ---
107
+ let vertSrc = this.vert
108
+ const originalBufferAttrs = {} // internal_name → Float32Array
109
+ const computedBufferAttrs = {} // shader_name → Float32Array (from context.bufferAttrs)
110
+ const allTextureUniforms = {}
111
+ const allScalarUniforms = {}
112
+ const allGlobalDecls = []
113
+ const mainInjections = []
114
+ const allAxisUpdaters = []
115
+
116
+ for (const [key, expr] of Object.entries(layer.attributes)) {
117
+ const result = resolveAttributeExpr(regl, expr, shaderName(key), plot)
118
+ if (result.kind === 'buffer') {
119
+ originalBufferAttrs[key] = result.value
120
+ } else {
121
+ vertSrc = removeAttributeDecl(vertSrc, shaderName(key))
122
+ Object.assign(computedBufferAttrs, result.context.bufferAttrs)
123
+ Object.assign(allTextureUniforms, result.context.textureUniforms)
124
+ Object.assign(allScalarUniforms, result.context.scalarUniforms)
125
+ allGlobalDecls.push(...result.context.globalDecls)
126
+ mainInjections.push(`float ${shaderName(key)} = ${result.glslExpr};`)
127
+ allAxisUpdaters.push(...result.context.axisUpdaters)
128
+ }
129
+ }
130
+
131
+ layer._axisUpdaters = allAxisUpdaters
132
+
133
+ if (mainInjections.length > 0) {
134
+ vertSrc = injectIntoMainStart(vertSrc, mainInjections.join('\n '))
135
+ }
136
+
137
+ // Merged buffer attrs by shader name — used for vertex count fallback.
138
+ const allShaderBuffers = {
139
+ ...Object.fromEntries(Object.entries(originalBufferAttrs).map(([k, v]) => [shaderName(k), v])),
140
+ ...computedBufferAttrs
141
+ }
142
+
89
143
  const isInstanced = layer.instanceCount !== null
90
- const pickCount = isInstanced ? layer.instanceCount : (layer.vertexCount ?? layer.attributes.x?.length ?? 0)
144
+ const pickCount = isInstanced ? layer.instanceCount :
145
+ (layer.vertexCount ?? allShaderBuffers[shaderName('x')]?.length
146
+ ?? Object.values(layer.attributes).find(v => v instanceof Float32Array)?.length ?? 0)
91
147
  const pickIds = new Float32Array(pickCount)
92
148
  for (let i = 0; i < pickCount; i++) pickIds[i] = i
93
149
 
150
+ // --- Build regl attributes ---
94
151
  const attributes = {
152
+ // Original buffer attrs with possible divisors
95
153
  ...Object.fromEntries(
96
- Object.entries(layer.attributes).map(([key, buffer]) => {
154
+ Object.entries(originalBufferAttrs).map(([key, buffer]) => {
97
155
  const divisor = layer.attributeDivisors[key]
98
156
  const attrObj = divisor !== undefined ? { buffer, divisor } : { buffer }
99
157
  return [shaderName(key), attrObj]
100
158
  })
101
159
  ),
160
+ // Computed buffer attrs (no divisors; already shader-named)
161
+ ...Object.fromEntries(
162
+ Object.entries(computedBufferAttrs).map(([shaderKey, buffer]) => [shaderKey, { buffer }])
163
+ ),
102
164
  a_pickId: isInstanced ? { buffer: regl.buffer(pickIds), divisor: 1 } : regl.buffer(pickIds)
103
165
  }
104
166
 
167
+ // --- Build uniforms ---
105
168
  const uniforms = {
106
169
  ...u("xDomain"),
107
170
  ...u("yDomain"),
@@ -111,7 +174,9 @@ export class LayerType {
111
174
  u_pickLayerIndex: regl.prop('u_pickLayerIndex'),
112
175
  ...Object.fromEntries(
113
176
  Object.entries(layer.uniforms).map(([key, value]) => [shaderName(key), value])
114
- )
177
+ ),
178
+ ...allTextureUniforms,
179
+ ...allScalarUniforms
115
180
  }
116
181
 
117
182
  // Add per-color-axis uniforms (colorscale index + range + scale type), keyed by quantity kind
@@ -131,7 +196,7 @@ export class LayerType {
131
196
  const pickVertDecls = `attribute float a_pickId;\nvarying float v_pickId;`
132
197
 
133
198
  const drawConfig = {
134
- vert: injectPickIdAssignment(injectInto(this.vert, [spatialGlsl, filterGlsl, pickVertDecls])),
199
+ vert: injectPickIdAssignment(injectInto(vertSrc, [spatialGlsl, filterGlsl, pickVertDecls, ...allGlobalDecls])),
135
200
  frag: injectInto(this.frag, [buildApplyColorGlsl(), colorGlsl, filterGlsl]),
136
201
  attributes,
137
202
  uniforms,