gladly-plot 0.0.3 → 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.
- package/README.md +1 -0
- package/package.json +16 -8
- package/src/axes/Axis.js +253 -0
- package/src/{AxisQuantityKindRegistry.js → axes/AxisQuantityKindRegistry.js} +7 -0
- package/src/{AxisRegistry.js → axes/AxisRegistry.js} +48 -0
- package/src/axes/ColorAxisRegistry.js +93 -0
- package/src/{FilterAxisRegistry.js → axes/FilterAxisRegistry.js} +63 -0
- package/src/axes/ZoomController.js +141 -0
- package/src/colorscales/BivariateColorscales.js +205 -0
- package/src/colorscales/ColorscaleRegistry.js +124 -0
- package/src/compute/ComputationRegistry.js +237 -0
- package/src/compute/axisFilter.js +47 -0
- package/src/compute/conv.js +230 -0
- package/src/compute/fft.js +292 -0
- package/src/compute/filter.js +227 -0
- package/src/compute/hist.js +180 -0
- package/src/compute/kde.js +102 -0
- package/src/{Layer.js → core/Layer.js} +4 -3
- package/src/{LayerType.js → core/LayerType.js} +72 -7
- package/src/core/Plot.js +735 -0
- package/src/{Colorbar.js → floats/Colorbar.js} +19 -5
- package/src/floats/Colorbar2d.js +77 -0
- package/src/{Filterbar.js → floats/Filterbar.js} +18 -4
- package/src/{FilterbarFloat.js → floats/Float.js} +17 -30
- package/src/{EpsgUtils.js → geo/EpsgUtils.js} +1 -1
- package/src/index.js +35 -22
- package/src/{ColorbarLayer.js → layers/ColorbarLayer.js} +2 -2
- package/src/layers/ColorbarLayer2d.js +97 -0
- package/src/{FilterbarLayer.js → layers/FilterbarLayer.js} +2 -2
- package/src/layers/HistogramLayer.js +212 -0
- package/src/layers/LinesLayer.js +199 -0
- package/src/layers/PointsLayer.js +114 -0
- package/src/layers/ScatterShared.js +142 -0
- package/src/{TileLayer.js → layers/TileLayer.js} +4 -4
- package/src/Axis.js +0 -48
- package/src/ColorAxisRegistry.js +0 -49
- package/src/ColorscaleRegistry.js +0 -52
- package/src/Float.js +0 -159
- package/src/Plot.js +0 -1068
- package/src/ScatterLayer.js +0 -133
- /package/src/{AxisLink.js → axes/AxisLink.js} +0 -0
- /package/src/{MatplotlibColorscales.js → colorscales/MatplotlibColorscales.js} +0 -0
- /package/src/{Data.js → core/Data.js} +0 -0
- /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
|
|
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 (
|
|
6
|
-
throw new Error(`Attribute '${key}' must be
|
|
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 "
|
|
3
|
-
import { buildFilterGlsl } from "
|
|
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 :
|
|
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(
|
|
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(
|
|
199
|
+
vert: injectPickIdAssignment(injectInto(vertSrc, [spatialGlsl, filterGlsl, pickVertDecls, ...allGlobalDecls])),
|
|
135
200
|
frag: injectInto(this.frag, [buildApplyColorGlsl(), colorGlsl, filterGlsl]),
|
|
136
201
|
attributes,
|
|
137
202
|
uniforms,
|