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,144 @@
|
|
|
1
|
+
const colorscales = new Map()
|
|
2
|
+
const colorscales2d = new Map()
|
|
3
|
+
|
|
4
|
+
export function registerColorscale(name, glslFn) {
|
|
5
|
+
colorscales.set(name, glslFn)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function register2DColorscale(name, glslFn) {
|
|
9
|
+
colorscales2d.set(name, glslFn)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getRegisteredColorscales() {
|
|
13
|
+
return colorscales
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getRegistered2DColorscales() {
|
|
17
|
+
return colorscales2d
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getColorscaleIndex(name) {
|
|
21
|
+
let idx = 0
|
|
22
|
+
for (const key of colorscales.keys()) {
|
|
23
|
+
if (key === name) return idx
|
|
24
|
+
idx++
|
|
25
|
+
}
|
|
26
|
+
// 2D colorscales use negative indices: -(idx + 1)
|
|
27
|
+
let idx2d = 0
|
|
28
|
+
for (const key of colorscales2d.keys()) {
|
|
29
|
+
if (key === name) return -(idx2d + 1)
|
|
30
|
+
idx2d++
|
|
31
|
+
}
|
|
32
|
+
return 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function get2DColorscaleIndex(name) {
|
|
36
|
+
let idx = 0
|
|
37
|
+
for (const key of colorscales2d.keys()) {
|
|
38
|
+
if (key === name) return -(idx + 1)
|
|
39
|
+
idx++
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildColorGlsl() {
|
|
45
|
+
if (colorscales.size === 0 && colorscales2d.size === 0) return ''
|
|
46
|
+
|
|
47
|
+
const parts = []
|
|
48
|
+
|
|
49
|
+
// 1D colorscale functions
|
|
50
|
+
for (const glslFn of colorscales.values()) {
|
|
51
|
+
parts.push(glslFn)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2D colorscale functions (fn signature: vec4 colorscale_2d_<name>(vec2 t))
|
|
55
|
+
for (const glslFn of colorscales2d.values()) {
|
|
56
|
+
parts.push(glslFn)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// map_color — 1D dispatch, operates in already-transformed (possibly log) space
|
|
60
|
+
parts.push('vec4 map_color(int cs, vec2 range, float value) {')
|
|
61
|
+
parts.push(' float t = clamp((value - range.x) / (range.y - range.x), 0.0, 1.0);')
|
|
62
|
+
let idx = 0
|
|
63
|
+
for (const name of colorscales.keys()) {
|
|
64
|
+
parts.push(` if (cs == ${idx}) return colorscale_${name}(t);`)
|
|
65
|
+
idx++
|
|
66
|
+
}
|
|
67
|
+
parts.push(' return vec4(0.5, 0.5, 0.5, 1.0);')
|
|
68
|
+
parts.push('}')
|
|
69
|
+
|
|
70
|
+
// map_color_2d — 2D dispatch, takes normalized vec2 t in [0,1]x[0,1]
|
|
71
|
+
parts.push('vec4 map_color_2d(int cs, vec2 t) {')
|
|
72
|
+
let idx2d = 0
|
|
73
|
+
for (const name of colorscales2d.keys()) {
|
|
74
|
+
parts.push(` if (cs == ${idx2d}) return colorscale_2d_${name}(t);`)
|
|
75
|
+
idx2d++
|
|
76
|
+
}
|
|
77
|
+
parts.push(' return vec4(0.5, 0.5, 0.5, 1.0);')
|
|
78
|
+
parts.push('}')
|
|
79
|
+
|
|
80
|
+
// map_color_s — 1D with scale type, alpha blending, and picking
|
|
81
|
+
parts.push('vec4 map_color_s(int cs, vec2 range, float v, float scaleType, float useAlpha) {')
|
|
82
|
+
parts.push(' float vt = scaleType > 0.5 ? log(v) : v;')
|
|
83
|
+
parts.push(' float r0 = scaleType > 0.5 ? log(range.x) : range.x;')
|
|
84
|
+
parts.push(' float r1 = scaleType > 0.5 ? log(range.y) : range.y;')
|
|
85
|
+
parts.push(' float t = clamp((vt - r0) / (r1 - r0), 0.0, 1.0);')
|
|
86
|
+
parts.push(' vec4 color = map_color(cs, vec2(r0, r1), vt);')
|
|
87
|
+
parts.push(' if (useAlpha > 0.5) color.a = t;')
|
|
88
|
+
parts.push(' return gladly_apply_color(color);')
|
|
89
|
+
parts.push('}')
|
|
90
|
+
|
|
91
|
+
// gladly_map_color_raw — like map_color_s but without picking/apply, used internally by map_color_s_2d
|
|
92
|
+
parts.push('vec4 gladly_map_color_raw(int cs, vec2 range, float v, float scaleType) {')
|
|
93
|
+
parts.push(' float vt = scaleType > 0.5 ? log(v) : v;')
|
|
94
|
+
parts.push(' float r0 = scaleType > 0.5 ? log(range.x) : range.x;')
|
|
95
|
+
parts.push(' float r1 = scaleType > 0.5 ? log(range.y) : range.y;')
|
|
96
|
+
parts.push(' return map_color(cs, vec2(r0, r1), vt);')
|
|
97
|
+
parts.push('}')
|
|
98
|
+
|
|
99
|
+
// gladly_normalize_color — normalize a data value to [0,1] for 2D colorscale lookup
|
|
100
|
+
parts.push('float gladly_normalize_color(vec2 range, float v, float scaleType) {')
|
|
101
|
+
parts.push(' float vt = scaleType > 0.5 ? log(v) : v;')
|
|
102
|
+
parts.push(' float r0 = scaleType > 0.5 ? log(range.x) : range.x;')
|
|
103
|
+
parts.push(' float r1 = scaleType > 0.5 ? log(range.y) : range.y;')
|
|
104
|
+
parts.push(' return clamp((vt - r0) / (r1 - r0), 0.0, 1.0);')
|
|
105
|
+
parts.push('}')
|
|
106
|
+
|
|
107
|
+
// map_color_s_2d — blend two 1D colorscales, or dispatch to a true 2D colorscale.
|
|
108
|
+
// A true 2D colorscale is selected when cs_a < 0 && cs_a == cs_b (both axes share the
|
|
109
|
+
// same 2D colorscale, identified by the negative index -(idx+1)).
|
|
110
|
+
// useAlpha_a / useAlpha_b: if > 0.5, the normalised value for that axis modulates alpha.
|
|
111
|
+
parts.push('vec4 map_color_s_2d(int cs_a, vec2 range_a, float v_a, float type_a, float useAlpha_a,')
|
|
112
|
+
parts.push(' int cs_b, vec2 range_b, float v_b, float type_b, float useAlpha_b) {')
|
|
113
|
+
|
|
114
|
+
parts.push(' bool a_nan = isnan(v_a);')
|
|
115
|
+
parts.push(' bool b_nan = isnan(v_b);')
|
|
116
|
+
parts.push(' vec4 c;')
|
|
117
|
+
|
|
118
|
+
parts.push(' if (cs_a < 0 && cs_a == cs_b) {')
|
|
119
|
+
parts.push(' float ta = a_nan ? 0.0 : gladly_normalize_color(range_a, v_a, type_a);')
|
|
120
|
+
parts.push(' float tb = b_nan ? 0.0 : gladly_normalize_color(range_b, v_b, type_b);')
|
|
121
|
+
parts.push(' c = map_color_2d(-(cs_a + 1), vec2(ta, tb));')
|
|
122
|
+
parts.push(' } else if (cs_a >= 0) {')
|
|
123
|
+
parts.push(' if (!a_nan) c = gladly_map_color_raw(cs_a, range_a, v_a, type_a);')
|
|
124
|
+
parts.push(' else if (!b_nan) c = gladly_map_color_raw(cs_b, range_b, v_b, type_b);')
|
|
125
|
+
parts.push(' else c = vec4(0.0);')
|
|
126
|
+
parts.push(' } else {')
|
|
127
|
+
parts.push(' // fallback (cs_a < 0 but not equal to cs_b)')
|
|
128
|
+
parts.push(' if (!a_nan && !b_nan) {')
|
|
129
|
+
parts.push(' vec4 ca = gladly_map_color_raw(cs_a, range_a, v_a, type_a);')
|
|
130
|
+
parts.push(' vec4 cb = gladly_map_color_raw(cs_b, range_b, v_b, type_b);')
|
|
131
|
+
parts.push(' c = (ca + cb) / 2.0;')
|
|
132
|
+
parts.push(' } else if (!a_nan) c = gladly_map_color_raw(cs_a, range_a, v_a, type_a);')
|
|
133
|
+
parts.push(' else if (!b_nan) c = gladly_map_color_raw(cs_b, range_b, v_b, type_b);')
|
|
134
|
+
parts.push(' else c = vec4(0.0);')
|
|
135
|
+
parts.push(' }')
|
|
136
|
+
|
|
137
|
+
parts.push(' if (!a_nan && useAlpha_a > 0.5) c.a = gladly_normalize_color(range_a, v_a, type_a);')
|
|
138
|
+
parts.push(' if (!b_nan && useAlpha_b > 0.5) c.a *= gladly_normalize_color(range_b, v_b, type_b);')
|
|
139
|
+
|
|
140
|
+
parts.push(' return gladly_apply_color(c);')
|
|
141
|
+
parts.push('}')
|
|
142
|
+
|
|
143
|
+
return parts.join('\n')
|
|
144
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { ColumnData } from '../data/ColumnData.js'
|
|
2
|
+
|
|
3
|
+
const textureComputations = new Map()
|
|
4
|
+
const glslComputations = new Map()
|
|
5
|
+
const computedDataRegistry = new Map()
|
|
6
|
+
|
|
7
|
+
// ─── Registration ─────────────────────────────────────────────────────────────
|
|
8
|
+
export function registerTextureComputation(name, computation) {
|
|
9
|
+
textureComputations.set(name, computation)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function registerGlslComputation(name, computation) {
|
|
13
|
+
glslComputations.set(name, computation)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function registerComputedData(name, instance) {
|
|
17
|
+
computedDataRegistry.set(name, instance)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getComputedData(name) {
|
|
21
|
+
return computedDataRegistry.get(name)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getRegisteredComputedData() {
|
|
25
|
+
return computedDataRegistry
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── resolveExprToColumn ─────────────────────────────────────────────────────
|
|
29
|
+
// Turns any expression (string col name, { compName: params }, ColumnData) into ColumnData.
|
|
30
|
+
export async function resolveExprToColumn(expr, data, regl, plot) {
|
|
31
|
+
if (expr instanceof ColumnData) return expr
|
|
32
|
+
|
|
33
|
+
if (typeof expr === 'string') {
|
|
34
|
+
const col = data?.getData(expr)
|
|
35
|
+
if (!col) throw new Error(`Column '${expr}' not found in data`)
|
|
36
|
+
return col
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof expr === 'object' && expr !== null) {
|
|
40
|
+
const keys = Object.keys(expr)
|
|
41
|
+
if (keys.length === 1) {
|
|
42
|
+
const compName = keys[0]
|
|
43
|
+
const params = expr[compName]
|
|
44
|
+
|
|
45
|
+
if (textureComputations.has(compName)) {
|
|
46
|
+
const comp = textureComputations.get(compName)
|
|
47
|
+
const resolvedInputs = await resolveParams(params, data, regl, plot)
|
|
48
|
+
return await comp.createColumn(regl, resolvedInputs, plot)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (glslComputations.has(compName)) {
|
|
52
|
+
const comp = glslComputations.get(compName)
|
|
53
|
+
const resolvedInputs = await resolveParams(params, data, regl, plot)
|
|
54
|
+
return comp.createColumn(resolvedInputs)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw new Error(`Cannot resolve expression to column: ${JSON.stringify(expr)}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Resolve a params dict recursively: column refs -> ColumnData, scalars pass through.
|
|
63
|
+
async function resolveParams(params, data, regl, plot) {
|
|
64
|
+
if (params === null || params === undefined) return params
|
|
65
|
+
if (typeof params === 'number' || typeof params === 'boolean') return params
|
|
66
|
+
if (params instanceof ColumnData) return params
|
|
67
|
+
if (params instanceof Float32Array) return params
|
|
68
|
+
|
|
69
|
+
if (typeof params === 'string') {
|
|
70
|
+
const col = data?.getData(params)
|
|
71
|
+
return col ?? params // fall back to string value if not a known column
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof params === 'object') {
|
|
75
|
+
const keys = Object.keys(params)
|
|
76
|
+
if (keys.length === 1 &&
|
|
77
|
+
(textureComputations.has(keys[0]) || glslComputations.has(keys[0]))) {
|
|
78
|
+
return await resolveExprToColumn(params, data, regl, plot)
|
|
79
|
+
}
|
|
80
|
+
const resolved = {}
|
|
81
|
+
for (const [k, v] of Object.entries(params)) {
|
|
82
|
+
resolved[k] = await resolveParams(v, data, regl, plot)
|
|
83
|
+
}
|
|
84
|
+
return resolved
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return params
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── resolveAttributeExpr ────────────────────────────────────────────────────
|
|
91
|
+
// Entry point from LayerType.createDrawCommand. Returns:
|
|
92
|
+
// { kind: 'buffer', value: Float32Array } — fixed geometry
|
|
93
|
+
// { kind: 'computed', glslExpr, textures, col } — data column
|
|
94
|
+
export async function resolveAttributeExpr(regl, expr, attrShaderName, plot) {
|
|
95
|
+
if (expr instanceof Float32Array) {
|
|
96
|
+
return { kind: 'buffer', value: expr }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const data = plot ? plot.currentData : null
|
|
100
|
+
const col = (expr instanceof ColumnData)
|
|
101
|
+
? expr
|
|
102
|
+
: await resolveExprToColumn(expr, data, regl, plot)
|
|
103
|
+
|
|
104
|
+
const safePath = attrShaderName.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
105
|
+
const { glslExpr, textures } = col.resolve(safePath, regl)
|
|
106
|
+
return { kind: 'computed', glslExpr, textures, col }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── resolveQuantityKind ─────────────────────────────────────────────────────
|
|
110
|
+
export function resolveQuantityKind(expr, data) {
|
|
111
|
+
if (expr instanceof ColumnData) return expr.quantityKind
|
|
112
|
+
if (typeof expr === 'string') {
|
|
113
|
+
return (data ? data.getQuantityKind(expr) : null) ?? expr
|
|
114
|
+
}
|
|
115
|
+
if (expr && typeof expr === 'object') {
|
|
116
|
+
const keys = Object.keys(expr)
|
|
117
|
+
if (keys.length === 1) {
|
|
118
|
+
const compName = keys[0]
|
|
119
|
+
const comp = textureComputations.get(compName) ?? glslComputations.get(compName)
|
|
120
|
+
if (comp) return comp.getQuantityKind(expr[compName], data)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Schema builders ──────────────────────────────────────────────────────────
|
|
127
|
+
export const EXPRESSION_REF = { '$ref': '#/$defs/expression' }
|
|
128
|
+
export const EXPRESSION_REF_OPT = { '$ref': '#/$defs/expression_opt' }
|
|
129
|
+
|
|
130
|
+
export function buildTransformSchema(data) {
|
|
131
|
+
const defs = {}
|
|
132
|
+
for (const [name, comp] of computedDataRegistry) {
|
|
133
|
+
defs[`params_computeddata_${name}`] = comp.schema(data)
|
|
134
|
+
}
|
|
135
|
+
defs.transform_expression = {
|
|
136
|
+
anyOf: [...computedDataRegistry].map(([name]) => ({
|
|
137
|
+
type: 'object',
|
|
138
|
+
title: name,
|
|
139
|
+
properties: { [name]: { '$ref': `#/$defs/params_computeddata_${name}` } },
|
|
140
|
+
required: [name],
|
|
141
|
+
additionalProperties: false
|
|
142
|
+
}))
|
|
143
|
+
}
|
|
144
|
+
return { '$defs': defs }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function computationSchema(data) {
|
|
148
|
+
const cols = data ? data.columns() : []
|
|
149
|
+
const defs = {}
|
|
150
|
+
|
|
151
|
+
for (const [name, comp] of textureComputations) {
|
|
152
|
+
defs[`params_${name}`] = comp.schema(data)
|
|
153
|
+
}
|
|
154
|
+
for (const [name, comp] of glslComputations) {
|
|
155
|
+
defs[`params_${name}`] = comp.schema(data)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
defs.expression = {
|
|
159
|
+
anyOf: [
|
|
160
|
+
...cols.map(col => ({ type: 'string', const: col, enum: [col], title: col, readOnly: true })),
|
|
161
|
+
...[...textureComputations, ...glslComputations].map(([name]) => ({
|
|
162
|
+
type: 'object',
|
|
163
|
+
title: name,
|
|
164
|
+
properties: { [name]: { '$ref': `#/$defs/params_${name}` } },
|
|
165
|
+
required: [name],
|
|
166
|
+
additionalProperties: false
|
|
167
|
+
}))
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
defs.expression_opt = {
|
|
172
|
+
anyOf: [
|
|
173
|
+
{ type: 'string', const: 'none', enum: ['none'], title: 'none', readOnly: true },
|
|
174
|
+
...defs.expression.anyOf
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { '$defs': defs, '$ref': '#/$defs/expression' }
|
|
179
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { registerTextureComputation, EXPRESSION_REF } from "./ComputationRegistry.js"
|
|
2
|
+
import { TextureComputation } from "../data/Computation.js"
|
|
3
|
+
import { ArrayColumn, uploadToTexture } from "../data/ColumnData.js"
|
|
4
|
+
import makeHistogram from "./hist.js"
|
|
5
|
+
|
|
6
|
+
// Texture computation that filters the input data by a filter axis range and
|
|
7
|
+
// then builds a histogram texture from the surviving values.
|
|
8
|
+
//
|
|
9
|
+
// params:
|
|
10
|
+
// input — ColumnData of values normalised to [0, 1] (for histogram bins); must be ArrayColumn
|
|
11
|
+
// filterValues — ColumnData of raw filter-column values (same length as input); must be ArrayColumn
|
|
12
|
+
// filterAxisId — string: axis ID / quantity kind whose domain drives the filter
|
|
13
|
+
// bins — optional number of histogram bins
|
|
14
|
+
//
|
|
15
|
+
// getAxisDomain(filterAxisId) returns [min|null, max|null] where null means
|
|
16
|
+
// unbounded. The computation is re-run automatically whenever the domain changes.
|
|
17
|
+
class FilteredHistogramComputation extends TextureComputation {
|
|
18
|
+
compute(regl, inputs, getAxisDomain) {
|
|
19
|
+
const inputCol = inputs.input
|
|
20
|
+
const filterCol = inputs.filterValues
|
|
21
|
+
if (!(inputCol instanceof ArrayColumn)) throw new Error('filteredHistogram: input must be ArrayColumn')
|
|
22
|
+
if (!(filterCol instanceof ArrayColumn)) throw new Error('filteredHistogram: filterValues must be ArrayColumn')
|
|
23
|
+
|
|
24
|
+
const inputArr = inputCol.array
|
|
25
|
+
const filterArr = filterCol.array
|
|
26
|
+
const { filterAxisId, bins } = inputs
|
|
27
|
+
const domain = getAxisDomain(filterAxisId) // [min|null, max|null] or null
|
|
28
|
+
const filterMin = domain?.[0] ?? null
|
|
29
|
+
const filterMax = domain?.[1] ?? null
|
|
30
|
+
|
|
31
|
+
// Build a compact array of the normalised values that pass the filter.
|
|
32
|
+
const filtered = []
|
|
33
|
+
for (let i = 0; i < inputArr.length; i++) {
|
|
34
|
+
const fv = filterArr[i]
|
|
35
|
+
if (filterMin !== null && fv < filterMin) continue
|
|
36
|
+
if (filterMax !== null && fv > filterMax) continue
|
|
37
|
+
filtered.push(inputArr[i])
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const filteredTex = uploadToTexture(regl, new Float32Array(filtered))
|
|
41
|
+
return makeHistogram(regl, filteredTex, { bins })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
schema(data) {
|
|
45
|
+
return {
|
|
46
|
+
type: 'object',
|
|
47
|
+
title: 'filteredHistogram',
|
|
48
|
+
properties: {
|
|
49
|
+
input: EXPRESSION_REF,
|
|
50
|
+
filterValues: EXPRESSION_REF,
|
|
51
|
+
filterAxisId: { type: 'string' },
|
|
52
|
+
bins: { type: 'number' }
|
|
53
|
+
},
|
|
54
|
+
required: ['input', 'filterValues', 'filterAxisId']
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
registerTextureComputation('filteredHistogram', new FilteredHistogramComputation())
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { fftConvolution } from "./fft.js"
|
|
2
|
+
import { registerTextureComputation, EXPRESSION_REF, resolveQuantityKind } from "./ComputationRegistry.js"
|
|
3
|
+
import { TextureComputation } from "../data/Computation.js"
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
============================================================
|
|
7
|
+
Utilities
|
|
8
|
+
============================================================
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const MAX_KERNEL_LOOP = 1024;
|
|
12
|
+
|
|
13
|
+
function nextPow2(n) {
|
|
14
|
+
return 1 << Math.ceil(Math.log2(n));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Internal: builds a 1-value-per-texel R-channel texture (not exposed via sampleColumn)
|
|
18
|
+
function make1DTexture(regl, data, width) {
|
|
19
|
+
return regl.texture({
|
|
20
|
+
data,
|
|
21
|
+
width,
|
|
22
|
+
height: 1,
|
|
23
|
+
format: "rgba",
|
|
24
|
+
type: "float",
|
|
25
|
+
wrap: "clamp",
|
|
26
|
+
min: "nearest",
|
|
27
|
+
mag: "nearest"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Internal: creates an R-channel FBO of given width
|
|
32
|
+
function makeFBO(regl, width) {
|
|
33
|
+
return regl.framebuffer({
|
|
34
|
+
color: regl.texture({
|
|
35
|
+
width,
|
|
36
|
+
height: 1,
|
|
37
|
+
format: "rgba",
|
|
38
|
+
type: "float"
|
|
39
|
+
}),
|
|
40
|
+
depth: false,
|
|
41
|
+
stencil: false,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Repack a 1-value-per-texel R-channel texture into a 4-packed RGBA texture.
|
|
46
|
+
// The source texture has elements at (i % srcW, i / srcW).r
|
|
47
|
+
// The output has elements packed 4 per texel, _dataLength set.
|
|
48
|
+
function repackToQuadTexture(regl, sourceTex, N) {
|
|
49
|
+
const nTexels = Math.ceil(N / 4)
|
|
50
|
+
const w = Math.min(nTexels, regl.limits.maxTextureSize)
|
|
51
|
+
const h = Math.ceil(nTexels / w)
|
|
52
|
+
const outputTex = regl.texture({ width: w, height: h, type: 'float', format: 'rgba' })
|
|
53
|
+
const outputFBO = regl.framebuffer({ color: outputTex, depth: false, stencil: false })
|
|
54
|
+
|
|
55
|
+
regl({
|
|
56
|
+
framebuffer: outputFBO,
|
|
57
|
+
vert: `#version 300 es
|
|
58
|
+
in vec2 position;
|
|
59
|
+
void main() { gl_Position = vec4(position, 0.0, 1.0); }`,
|
|
60
|
+
frag: `#version 300 es
|
|
61
|
+
precision highp float;
|
|
62
|
+
uniform sampler2D sourceTex;
|
|
63
|
+
uniform int totalLength;
|
|
64
|
+
out vec4 fragColor;
|
|
65
|
+
void main() {
|
|
66
|
+
ivec2 srcSz = textureSize(sourceTex, 0);
|
|
67
|
+
int texelI = int(gl_FragCoord.y) * ${w} + int(gl_FragCoord.x);
|
|
68
|
+
int base = texelI * 4;
|
|
69
|
+
float v0 = base + 0 < totalLength ? texelFetch(sourceTex, ivec2((base+0) % srcSz.x, (base+0) / srcSz.x), 0).r : 0.0;
|
|
70
|
+
float v1 = base + 1 < totalLength ? texelFetch(sourceTex, ivec2((base+1) % srcSz.x, (base+1) / srcSz.x), 0).r : 0.0;
|
|
71
|
+
float v2 = base + 2 < totalLength ? texelFetch(sourceTex, ivec2((base+2) % srcSz.x, (base+2) / srcSz.x), 0).r : 0.0;
|
|
72
|
+
float v3 = base + 3 < totalLength ? texelFetch(sourceTex, ivec2((base+3) % srcSz.x, (base+3) / srcSz.x), 0).r : 0.0;
|
|
73
|
+
fragColor = vec4(v0, v1, v2, v3);
|
|
74
|
+
}`,
|
|
75
|
+
attributes: { position: [[-1,-1],[1,-1],[-1,1],[1,1]] },
|
|
76
|
+
uniforms: { sourceTex, totalLength: N },
|
|
77
|
+
count: 4,
|
|
78
|
+
primitive: 'triangle strip'
|
|
79
|
+
})()
|
|
80
|
+
|
|
81
|
+
outputTex._dataLength = N
|
|
82
|
+
return outputTex
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/*
|
|
86
|
+
============================================================
|
|
87
|
+
1) Single-pass convolution (kernel ≤ 1024)
|
|
88
|
+
============================================================
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
function singlePassConvolution(regl) {
|
|
92
|
+
return regl({
|
|
93
|
+
frag: `#version 300 es
|
|
94
|
+
precision highp float;
|
|
95
|
+
uniform sampler2D signal, kernel;
|
|
96
|
+
uniform int N, K;
|
|
97
|
+
out vec4 outColor;
|
|
98
|
+
|
|
99
|
+
void main() {
|
|
100
|
+
int x = int(gl_FragCoord.x);
|
|
101
|
+
float sum = 0.0;
|
|
102
|
+
|
|
103
|
+
for (int i = 0; i < ${MAX_KERNEL_LOOP}; i++) {
|
|
104
|
+
if (i >= K) break;
|
|
105
|
+
int xi = x - i;
|
|
106
|
+
if (xi < 0 || xi >= N) continue;
|
|
107
|
+
|
|
108
|
+
float s = texelFetch(signal, ivec2(xi, 0), 0).r;
|
|
109
|
+
float k = texelFetch(kernel, ivec2(i, 0), 0).r;
|
|
110
|
+
sum += s * k;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
outColor = vec4(sum, 0, 0, 1);
|
|
114
|
+
}`,
|
|
115
|
+
vert: `#version 300 es
|
|
116
|
+
in vec2 position;
|
|
117
|
+
void main() {
|
|
118
|
+
gl_Position = vec4(position, 0, 1);
|
|
119
|
+
}`,
|
|
120
|
+
attributes: {
|
|
121
|
+
position: [[-1,-1],[1,-1],[-1,1],[1,1]]
|
|
122
|
+
},
|
|
123
|
+
uniforms: {
|
|
124
|
+
signal: regl.prop("signal"),
|
|
125
|
+
kernel: regl.prop("kernel"),
|
|
126
|
+
N: regl.prop("N"),
|
|
127
|
+
K: regl.prop("K")
|
|
128
|
+
},
|
|
129
|
+
framebuffer: regl.prop("fbo"),
|
|
130
|
+
count: 4,
|
|
131
|
+
primitive: "triangle strip"
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/*
|
|
136
|
+
============================================================
|
|
137
|
+
2) Two-pass chunked convolution (arbitrary kernel size)
|
|
138
|
+
============================================================
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
function chunkedConvolution(regl) {
|
|
142
|
+
const partialPass = regl({
|
|
143
|
+
frag: `#version 300 es
|
|
144
|
+
precision highp float;
|
|
145
|
+
uniform sampler2D signal, kernel2D;
|
|
146
|
+
uniform int N, chunkOffset;
|
|
147
|
+
out vec4 outColor;
|
|
148
|
+
|
|
149
|
+
void main() {
|
|
150
|
+
int x = int(gl_FragCoord.x);
|
|
151
|
+
float sum = 0.0;
|
|
152
|
+
|
|
153
|
+
for (int i = 0; i < ${MAX_KERNEL_LOOP}; i++) {
|
|
154
|
+
int kIndex = chunkOffset + i;
|
|
155
|
+
int xi = x - kIndex;
|
|
156
|
+
if (xi < 0 || xi >= N) continue;
|
|
157
|
+
|
|
158
|
+
float s = texelFetch(signal, ivec2(xi, 0), 0).r;
|
|
159
|
+
float k = texelFetch(kernel2D, ivec2(i, 0), 0).r;
|
|
160
|
+
sum += s * k;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
outColor = vec4(sum, 0, 0, 1);
|
|
164
|
+
}`,
|
|
165
|
+
vert: `#version 300 es
|
|
166
|
+
in vec2 position;
|
|
167
|
+
void main() {
|
|
168
|
+
gl_Position = vec4(position, 0, 1);
|
|
169
|
+
}`,
|
|
170
|
+
attributes: {
|
|
171
|
+
position: [[-1,-1],[1,-1],[-1,1],[1,1]]
|
|
172
|
+
},
|
|
173
|
+
uniforms: {
|
|
174
|
+
signal: regl.prop("signal"),
|
|
175
|
+
kernel2D: regl.prop("kernel"),
|
|
176
|
+
N: regl.prop("N"),
|
|
177
|
+
chunkOffset: regl.prop("offset")
|
|
178
|
+
},
|
|
179
|
+
framebuffer: regl.prop("fbo"),
|
|
180
|
+
blend: {
|
|
181
|
+
enable: true,
|
|
182
|
+
func: { src: "one", dst: "one" }
|
|
183
|
+
},
|
|
184
|
+
count: 4,
|
|
185
|
+
primitive: "triangle strip"
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return function run({ signalTex, kernel, N }) {
|
|
189
|
+
const chunks = Math.ceil(kernel.length / MAX_KERNEL_LOOP);
|
|
190
|
+
const fbo = makeFBO(regl, N);
|
|
191
|
+
|
|
192
|
+
regl.clear({ framebuffer: fbo, color: [0,0,0,0] });
|
|
193
|
+
|
|
194
|
+
for (let c = 0; c < chunks; c++) {
|
|
195
|
+
const slice = kernel.slice(
|
|
196
|
+
c * MAX_KERNEL_LOOP,
|
|
197
|
+
(c + 1) * MAX_KERNEL_LOOP
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const kernelTex = make1DTexture(regl, slice, MAX_KERNEL_LOOP);
|
|
201
|
+
|
|
202
|
+
partialPass({
|
|
203
|
+
signal: signalTex,
|
|
204
|
+
kernel: kernelTex,
|
|
205
|
+
N,
|
|
206
|
+
offset: c * MAX_KERNEL_LOOP,
|
|
207
|
+
fbo
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return fbo.color[0];
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/*
|
|
216
|
+
============================================================
|
|
217
|
+
Adaptive wrapper
|
|
218
|
+
============================================================
|
|
219
|
+
*/
|
|
220
|
+
|
|
221
|
+
export default function adaptiveConvolution(regl, signalArray, kernelArray) {
|
|
222
|
+
const single = singlePassConvolution(regl);
|
|
223
|
+
const chunked = chunkedConvolution(regl);
|
|
224
|
+
|
|
225
|
+
const N = signalArray.length;
|
|
226
|
+
const K = kernelArray.length;
|
|
227
|
+
|
|
228
|
+
const signalTex = make1DTexture(regl, signalArray, N);
|
|
229
|
+
|
|
230
|
+
let result;
|
|
231
|
+
|
|
232
|
+
// Case 1: single pass
|
|
233
|
+
if (K <= MAX_KERNEL_LOOP) {
|
|
234
|
+
const kernelTex = make1DTexture(regl, kernelArray, K);
|
|
235
|
+
const fbo = makeFBO(regl, N);
|
|
236
|
+
|
|
237
|
+
single({
|
|
238
|
+
signal: signalTex,
|
|
239
|
+
kernel: kernelTex,
|
|
240
|
+
N,
|
|
241
|
+
K,
|
|
242
|
+
fbo
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
result = fbo.color[0];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Case 2: chunked
|
|
249
|
+
else if (K <= 8192) {
|
|
250
|
+
result = chunked({
|
|
251
|
+
signalTex,
|
|
252
|
+
kernel: kernelArray,
|
|
253
|
+
N
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Case 3: FFT
|
|
258
|
+
else {
|
|
259
|
+
result = fftConvolution(regl, signalArray, kernelArray);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Repack internal R-channel result into 4-packed output for sampleColumn
|
|
263
|
+
return repackToQuadTexture(regl, result, N);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
class ConvolutionComputation extends TextureComputation {
|
|
267
|
+
getQuantityKind(params, data) { return resolveQuantityKind(params.signal, data) }
|
|
268
|
+
compute(regl, params, data, getAxisDomain) {
|
|
269
|
+
const signal = typeof params.signal === 'string' ? data.getData(params.signal) : params.signal
|
|
270
|
+
const kernel = typeof params.kernel === 'string' ? data.getData(params.kernel) : params.kernel
|
|
271
|
+
return adaptiveConvolution(regl, signal, kernel)
|
|
272
|
+
}
|
|
273
|
+
schema(data) {
|
|
274
|
+
return {
|
|
275
|
+
type: 'object',
|
|
276
|
+
title: 'convolution',
|
|
277
|
+
properties: {
|
|
278
|
+
signal: EXPRESSION_REF,
|
|
279
|
+
kernel: EXPRESSION_REF
|
|
280
|
+
},
|
|
281
|
+
required: ['signal', 'kernel']
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
registerTextureComputation('convolution', new ConvolutionComputation())
|