gladly-plot 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/package.json +10 -11
- package/src/axes/Axis.js +320 -172
- package/src/axes/AxisLink.js +6 -2
- package/src/axes/AxisRegistry.js +116 -39
- package/src/axes/Camera.js +47 -0
- package/src/axes/ColorAxisRegistry.js +10 -2
- package/src/axes/FilterAxisRegistry.js +1 -1
- package/src/axes/TickLabelAtlas.js +99 -0
- package/src/axes/ZoomController.js +446 -124
- package/src/colorscales/ColorscaleRegistry.js +30 -10
- package/src/compute/ComputationRegistry.js +126 -184
- package/src/compute/axisFilter.js +21 -9
- package/src/compute/conv.js +64 -8
- package/src/compute/elementwise.js +72 -0
- package/src/compute/fft.js +106 -20
- package/src/compute/filter.js +105 -103
- package/src/compute/hist.js +247 -142
- package/src/compute/kde.js +64 -46
- package/src/compute/scatter2dInterpolate.js +277 -0
- package/src/compute/util.js +196 -0
- package/src/core/ComputePipeline.js +153 -0
- package/src/core/GlBase.js +141 -0
- package/src/core/Layer.js +22 -8
- package/src/core/LayerType.js +253 -92
- package/src/core/Plot.js +644 -162
- package/src/core/PlotGroup.js +204 -0
- package/src/core/ShaderQueue.js +73 -0
- package/src/data/ColumnData.js +269 -0
- package/src/data/Computation.js +95 -0
- package/src/data/Data.js +270 -0
- package/src/floats/Float.js +56 -0
- package/src/index.js +16 -4
- package/src/layers/BarsLayer.js +168 -0
- package/src/layers/ColorbarLayer.js +10 -14
- package/src/layers/ColorbarLayer2d.js +13 -24
- package/src/layers/FilterbarLayer.js +4 -3
- package/src/layers/LinesLayer.js +108 -122
- package/src/layers/PointsLayer.js +73 -69
- package/src/layers/ScatterShared.js +62 -106
- package/src/layers/TileLayer.js +20 -16
- package/src/math/mat4.js +100 -0
- package/src/core/Data.js +0 -67
- package/src/layers/HistogramLayer.js +0 -212
package/src/compute/hist.js
CHANGED
|
@@ -1,173 +1,143 @@
|
|
|
1
|
-
import { registerTextureComputation,
|
|
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
|
-
|
|
19
|
-
sampleData = new Float32Array(sampleCutoff);
|
|
10
|
+
sampleData = new Float32Array(sampleCutoff)
|
|
20
11
|
for (let i = 0; i < sampleCutoff; i++) {
|
|
21
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
*
|
|
83
|
-
|
|
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
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
159
|
-
|
|
56
|
+
const pickIds = new Float32Array(N)
|
|
57
|
+
for (let i = 0; i < N; i++) pickIds[i] = i
|
|
160
58
|
|
|
161
|
-
|
|
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,
|
|
166
|
-
|
|
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())
|
package/src/compute/kde.js
CHANGED
|
@@ -1,71 +1,76 @@
|
|
|
1
|
-
import { registerTextureComputation,
|
|
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 {
|
|
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,
|
|
13
|
-
const bins = options.bins ||
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
float
|
|
61
|
-
|
|
62
|
-
|
|
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(
|
|
69
|
+
fragColor = vec4(s0, s1, s2, s3);
|
|
65
70
|
}
|
|
66
71
|
`,
|
|
67
72
|
attributes: {
|
|
68
|
-
|
|
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:
|
|
77
|
-
primitive: '
|
|
78
|
-
})
|
|
81
|
+
count: 4,
|
|
82
|
+
primitive: 'triangle strip'
|
|
83
|
+
})
|
|
79
84
|
|
|
80
|
-
drawKDE()
|
|
85
|
+
drawKDE()
|
|
81
86
|
|
|
82
|
-
|
|
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,
|
|
87
|
-
|
|
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' },
|