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.
- package/README.md +9 -2
- package/package.json +10 -11
- package/src/axes/Axis.js +401 -0
- package/src/{AxisLink.js → axes/AxisLink.js} +6 -2
- package/src/{AxisQuantityKindRegistry.js → axes/AxisQuantityKindRegistry.js} +7 -0
- package/src/axes/AxisRegistry.js +179 -0
- package/src/axes/Camera.js +47 -0
- package/src/axes/ColorAxisRegistry.js +101 -0
- package/src/{FilterAxisRegistry.js → axes/FilterAxisRegistry.js} +63 -0
- package/src/axes/TickLabelAtlas.js +99 -0
- package/src/axes/ZoomController.js +463 -0
- package/src/colorscales/BivariateColorscales.js +205 -0
- package/src/colorscales/ColorscaleRegistry.js +144 -0
- package/src/compute/ComputationRegistry.js +179 -0
- package/src/compute/axisFilter.js +59 -0
- package/src/compute/conv.js +286 -0
- package/src/compute/elementwise.js +72 -0
- package/src/compute/fft.js +378 -0
- package/src/compute/filter.js +229 -0
- package/src/compute/hist.js +285 -0
- package/src/compute/kde.js +120 -0
- 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 +59 -0
- package/src/core/LayerType.js +433 -0
- package/src/core/Plot.js +1213 -0
- 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/{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} +73 -30
- package/src/{EpsgUtils.js → geo/EpsgUtils.js} +1 -1
- package/src/index.js +47 -22
- package/src/layers/BarsLayer.js +168 -0
- package/src/{ColorbarLayer.js → layers/ColorbarLayer.js} +12 -16
- package/src/layers/ColorbarLayer2d.js +86 -0
- package/src/{FilterbarLayer.js → layers/FilterbarLayer.js} +6 -5
- package/src/layers/LinesLayer.js +185 -0
- package/src/layers/PointsLayer.js +118 -0
- package/src/layers/ScatterShared.js +98 -0
- package/src/{TileLayer.js → layers/TileLayer.js} +24 -20
- package/src/math/mat4.js +100 -0
- package/src/Axis.js +0 -48
- package/src/AxisRegistry.js +0 -54
- package/src/ColorAxisRegistry.js +0 -49
- package/src/ColorscaleRegistry.js +0 -52
- package/src/Data.js +0 -67
- package/src/Float.js +0 -159
- package/src/Layer.js +0 -44
- package/src/LayerType.js +0 -209
- package/src/Plot.js +0 -1073
- package/src/ScatterLayer.js +0 -287
- /package/src/{MatplotlibColorscales.js → colorscales/MatplotlibColorscales.js} +0 -0
- /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())
|