gladly-plot 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/package.json +2 -2
  2. package/src/axes/Axis.js +253 -0
  3. package/src/{AxisQuantityKindRegistry.js → axes/AxisQuantityKindRegistry.js} +7 -0
  4. package/src/{AxisRegistry.js → axes/AxisRegistry.js} +48 -0
  5. package/src/axes/ColorAxisRegistry.js +93 -0
  6. package/src/{FilterAxisRegistry.js → axes/FilterAxisRegistry.js} +63 -0
  7. package/src/axes/ZoomController.js +141 -0
  8. package/src/colorscales/BivariateColorscales.js +205 -0
  9. package/src/colorscales/ColorscaleRegistry.js +124 -0
  10. package/src/compute/ComputationRegistry.js +237 -0
  11. package/src/compute/axisFilter.js +47 -0
  12. package/src/compute/conv.js +230 -0
  13. package/src/compute/fft.js +292 -0
  14. package/src/compute/filter.js +227 -0
  15. package/src/compute/hist.js +180 -0
  16. package/src/compute/kde.js +102 -0
  17. package/src/{Layer.js → core/Layer.js} +4 -3
  18. package/src/{LayerType.js → core/LayerType.js} +72 -7
  19. package/src/core/Plot.js +735 -0
  20. package/src/{Colorbar.js → floats/Colorbar.js} +19 -5
  21. package/src/floats/Colorbar2d.js +77 -0
  22. package/src/{Filterbar.js → floats/Filterbar.js} +18 -4
  23. package/src/{FilterbarFloat.js → floats/Float.js} +17 -30
  24. package/src/{EpsgUtils.js → geo/EpsgUtils.js} +1 -1
  25. package/src/index.js +35 -22
  26. package/src/{ColorbarLayer.js → layers/ColorbarLayer.js} +2 -2
  27. package/src/layers/ColorbarLayer2d.js +97 -0
  28. package/src/{FilterbarLayer.js → layers/FilterbarLayer.js} +2 -2
  29. package/src/layers/HistogramLayer.js +212 -0
  30. package/src/layers/LinesLayer.js +199 -0
  31. package/src/layers/PointsLayer.js +114 -0
  32. package/src/layers/ScatterShared.js +142 -0
  33. package/src/{TileLayer.js → layers/TileLayer.js} +4 -4
  34. package/src/Axis.js +0 -48
  35. package/src/ColorAxisRegistry.js +0 -49
  36. package/src/ColorscaleRegistry.js +0 -52
  37. package/src/Float.js +0 -159
  38. package/src/Plot.js +0 -1073
  39. package/src/ScatterLayer.js +0 -287
  40. /package/src/{AxisLink.js → axes/AxisLink.js} +0 -0
  41. /package/src/{MatplotlibColorscales.js → colorscales/MatplotlibColorscales.js} +0 -0
  42. /package/src/{Data.js → core/Data.js} +0 -0
  43. /package/src/{LayerTypeRegistry.js → core/LayerTypeRegistry.js} +0 -0
@@ -0,0 +1,205 @@
1
+ import { register2DColorscale } from './ColorscaleRegistry.js'
2
+
3
+
4
+ //////////////////////////////
5
+ // 1. Bilinear 4-corner colormap
6
+ //////////////////////////////
7
+ register2DColorscale("bilinear4corner", `
8
+ vec4 colorscale_2d_bilinear4corner(vec2 t) {
9
+ vec3 c00 = vec3(0.0, 0.0, 1.0);
10
+ vec3 c10 = vec3(1.0, 0.0, 0.0);
11
+ vec3 c01 = vec3(0.0, 1.0, 0.0);
12
+ vec3 c11 = vec3(1.0, 1.0, 0.0);
13
+ vec3 rgb = (1.0 - t.x)*(1.0 - t.y)*c00 +
14
+ t.x*(1.0 - t.y)*c10 +
15
+ (1.0 - t.x)*t.y*c01 +
16
+ t.x*t.y*c11;
17
+ return vec4(rgb,1.0);
18
+ }
19
+ `);
20
+
21
+ register2DColorscale("Gred", `
22
+ vec4 colorscale_2d_Gred(vec2 t) {
23
+ vec3 c00 = vec3(0.0, 0.0, 0.0);
24
+ vec3 c10 = vec3(1.0, 0.0, 0.0);
25
+ vec3 c01 = vec3(0.0, 1.0, 0.0);
26
+ vec3 c11 = vec3(1.0, 1.0, 1.0);
27
+ vec3 rgb = (1.0 - t.x)*(1.0 - t.y)*c00 +
28
+ t.x*(1.0 - t.y)*c10 +
29
+ (1.0 - t.x)*t.y*c01 +
30
+ t.x*t.y*c11;
31
+ return vec4(rgb,1.0);
32
+ }
33
+ `);
34
+
35
+ register2DColorscale("Reen", `
36
+ vec4 colorscale_2d_Reen(vec2 t) {
37
+ vec3 c00 = vec3(1.0, 0.0, 0.0);
38
+ vec3 c10 = vec3(0.0, 0.0, 0.0);
39
+ vec3 c01 = vec3(1.0, 1.0, 1.0);
40
+ vec3 c11 = vec3(0.0, 1.0, 0.0);
41
+ vec3 rgb = (1.0 - t.x)*(1.0 - t.y)*c00 +
42
+ t.x*(1.0 - t.y)*c10 +
43
+ (1.0 - t.x)*t.y*c01 +
44
+ t.x*t.y*c11;
45
+ return vec4(rgb,1.0);
46
+ }
47
+ `);
48
+
49
+
50
+ //////////////////////////////
51
+ // 2. HSV Phase-Magnitude Map
52
+ //////////////////////////////
53
+ register2DColorscale("hsv_phase_magnitude", `
54
+ vec4 colorscale_2d_hsv_phase_magnitude(vec2 t) {
55
+ float angle = atan(t.y - 0.5, t.x - 0.5);
56
+ float r = length(t - vec2(0.5));
57
+ float H = (angle + 3.1415926)/(2.0*3.1415926);
58
+ float S = 1.0;
59
+ float V = clamp(r*1.4142136,0.0,1.0);
60
+ float c = V*S;
61
+ float h = H*6.0;
62
+ float x = c*(1.0 - abs(mod(h,2.0)-1.0));
63
+ vec3 rgb;
64
+ if(h<1.0) rgb = vec3(c,x,0.0);
65
+ else if(h<2.0) rgb = vec3(x,c,0.0);
66
+ else if(h<3.0) rgb = vec3(0.0,c,x);
67
+ else if(h<4.0) rgb = vec3(0.0,x,c);
68
+ else if(h<5.0) rgb = vec3(x,0.0,c);
69
+ else rgb = vec3(c,0.0,x);
70
+ float m = V - c;
71
+ rgb += vec3(m);
72
+ return vec4(rgb,1.0);
73
+ }
74
+ `);
75
+
76
+ //////////////////////////////
77
+ // 3. Diverging × Diverging Map
78
+ //////////////////////////////
79
+ register2DColorscale("diverging_diverging", `
80
+ vec4 colorscale_2d_diverging_diverging(vec2 t) {
81
+ vec3 blue = vec3(0.230,0.299,0.754);
82
+ vec3 white = vec3(1.0);
83
+ vec3 red = vec3(0.706,0.016,0.150);
84
+ vec3 rgbX = (t.x<0.5) ? mix(blue,white,t.x*2.0) : mix(white,red,(t.x-0.5)*2.0);
85
+ vec3 rgbY = (t.y<0.5) ? mix(blue,white,t.y*2.0) : mix(white,red,(t.y-0.5)*2.0);
86
+ vec3 rgb = 0.5*(rgbX+rgbY);
87
+ return vec4(rgb,1.0);
88
+ }
89
+ `);
90
+
91
+ //////////////////////////////
92
+ // 4. Lightness × Hue Map
93
+ //////////////////////////////
94
+ register2DColorscale("lightness_hue", `
95
+ vec4 colorscale_2d_lightness_hue(vec2 t) {
96
+ float H = t.x;
97
+ float L = t.y;
98
+ float C = 1.0 - abs(2.0*L-1.0);
99
+ float X = C*(1.0 - abs(mod(H*6.0,2.0)-1.0));
100
+ vec3 rgb;
101
+ if(H<1.0/6.0) rgb = vec3(C,X,0.0);
102
+ else if(H<2.0/6.0) rgb = vec3(X,C,0.0);
103
+ else if(H<3.0/6.0) rgb = vec3(0.0,C,X);
104
+ else if(H<4.0/6.0) rgb = vec3(0.0,X,C);
105
+ else if(H<5.0/6.0) rgb = vec3(X,0.0,C);
106
+ else rgb = vec3(C,0.0,X);
107
+ float m = L - 0.5*C;
108
+ rgb += vec3(m);
109
+ return vec4(rgb,1.0);
110
+ }
111
+ `);
112
+
113
+ //////////////////////////////
114
+ // 5. Brewer 3x3 Bivariate Grid (no vec3 c[3][3])
115
+ //////////////////////////////
116
+ register2DColorscale("brewer_3x3", `
117
+ vec4 colorscale_2d_brewer_3x3(vec2 t) {
118
+ float fx = clamp(t.x*2.0,0.0,2.0);
119
+ float fy = clamp(t.y*2.0,0.0,2.0);
120
+ int ix = int(fx);
121
+ int iy = int(fy);
122
+ vec3 rgb;
123
+ if(ix==0 && iy==0) rgb = vec3(215.0,25.0,28.0)/255.0;
124
+ else if(ix==1 && iy==0) rgb = vec3(253.0,174.0,97.0)/255.0;
125
+ else if(ix==2 && iy==0) rgb = vec3(255.0,255.0,191.0)/255.0;
126
+ else if(ix==0 && iy==1) rgb = vec3(224.0,130.0,20.0)/255.0;
127
+ else if(ix==1 && iy==1) rgb = vec3(255.0,255.0,179.0)/255.0;
128
+ else if(ix==2 && iy==1) rgb = vec3(171.0,221.0,164.0)/255.0;
129
+ else if(ix==0 && iy==2) rgb = vec3(26.0,150.0,65.0)/255.0;
130
+ else if(ix==1 && iy==2) rgb = vec3(166.0,217.0,106.0)/255.0;
131
+ else rgb = vec3(102.0,194.0,165.0)/255.0;
132
+ return vec4(rgb,1.0);
133
+ }
134
+ `);
135
+
136
+ //////////////////////////////
137
+ // 6. Moreland 5x5 Perceptual Grid (flattened)
138
+ //////////////////////////////
139
+ register2DColorscale("moreland_5x5", `
140
+ vec4 colorscale_2d_moreland_5x5(vec2 t) {
141
+ float fx = clamp(t.x*4.0,0.0,4.0);
142
+ float fy = clamp(t.y*4.0,0.0,4.0);
143
+ int ix = int(fx);
144
+ int iy = int(fy);
145
+ vec3 rgb;
146
+ if(ix==0 && iy==0) rgb = vec3(0.230,0.299,0.754);
147
+ else if(ix==1 && iy==0) rgb = vec3(0.375,0.544,0.837);
148
+ else if(ix==2 && iy==0) rgb = vec3(0.625,0.732,0.941);
149
+ else if(ix==3 && iy==0) rgb = vec3(0.843,0.867,0.996);
150
+ else if(ix==4 && iy==0) rgb = vec3(0.980,0.957,0.996);
151
+ else if(ix==0 && iy==1) rgb = vec3(0.266,0.353,0.819);
152
+ else if(ix==1 && iy==1) rgb = vec3(0.420,0.585,0.876);
153
+ else if(ix==2 && iy==1) rgb = vec3(0.666,0.762,0.961);
154
+ else if(ix==3 && iy==1) rgb = vec3(0.876,0.888,0.996);
155
+ else if(ix==4 && iy==1) rgb = vec3(0.992,0.969,0.996);
156
+ else if(ix==0 && iy==2) rgb = vec3(0.305,0.407,0.875);
157
+ else if(ix==1 && iy==2) rgb = vec3(0.466,0.625,0.911);
158
+ else if(ix==2 && iy==2) rgb = vec3(0.710,0.791,0.976);
159
+ else if(ix==3 && iy==2) rgb = vec3(0.905,0.908,0.996);
160
+ else if(ix==4 && iy==2) rgb = vec3(0.996,0.980,0.996);
161
+ else if(ix==0 && iy==3) rgb = vec3(0.349,0.460,0.926);
162
+ else if(ix==1 && iy==3) rgb = vec3(0.514,0.664,0.944);
163
+ else if(ix==2 && iy==3) rgb = vec3(0.753,0.817,0.988);
164
+ else if(ix==3 && iy==3) rgb = vec3(0.933,0.926,0.996);
165
+ else if(ix==4 && iy==3) rgb = vec3(0.996,0.988,0.996);
166
+ else if(ix==0 && iy==4) rgb = vec3(0.403,0.509,0.965);
167
+ else if(ix==1 && iy==4) rgb = vec3(0.563,0.700,0.972);
168
+ else if(ix==2 && iy==4) rgb = vec3(0.796,0.843,0.996);
169
+ else if(ix==3 && iy==4) rgb = vec3(0.960,0.944,0.996);
170
+ else rgb = vec3(1.000,1.000,1.000);
171
+ return vec4(rgb,1.0);
172
+ }
173
+ `);
174
+
175
+ //////////////////////////////
176
+ // 7. Boy's Surface / Orientation Map
177
+ //////////////////////////////
178
+ register2DColorscale("boys_surface", `
179
+ vec4 colorscale_2d_boys_surface(vec2 t) {
180
+ float u = t.x*2.0-1.0;
181
+ float v = t.y*2.0-1.0;
182
+ float x = u*(1.0-v*v/2.0);
183
+ float y = v*(1.0-u*u/2.0);
184
+ float z = (u*u-v*v)/2.0;
185
+ vec3 rgb = normalize(vec3(abs(x),abs(y),abs(z)));
186
+ return vec4(rgb,1.0);
187
+ }
188
+ `);
189
+
190
+ //////////////////////////////
191
+ // 8. Diverging × Sequential Map
192
+ //////////////////////////////
193
+ register2DColorscale("diverging_sequential", `
194
+ vec4 colorscale_2d_diverging_sequential(vec2 t) {
195
+ vec3 blue = vec3(0.230,0.299,0.754);
196
+ vec3 white = vec3(1.0);
197
+ vec3 red = vec3(0.706,0.016,0.150);
198
+ vec3 seqStart = vec3(1.0,1.0,0.8);
199
+ vec3 seqEnd = vec3(0.2,0.8,0.2);
200
+ vec3 rgbX = (t.x<0.5)? mix(blue,white,t.x*2.0) : mix(white,red,(t.x-0.5)*2.0);
201
+ vec3 rgbY = mix(seqStart,seqEnd,t.y);
202
+ vec3 rgb = 0.5*(rgbX+rgbY);
203
+ return vec4(rgb,1.0);
204
+ }
205
+ `);
@@ -0,0 +1,124 @@
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
+ parts.push('vec4 map_color_s_2d(int cs_a, vec2 range_a, float v_a, float type_a,')
111
+ parts.push(' int cs_b, vec2 range_b, float v_b, float type_b) {')
112
+ parts.push(' if (cs_a < 0 && cs_a == cs_b) {')
113
+ parts.push(' float ta = gladly_normalize_color(range_a, v_a, type_a);')
114
+ parts.push(' float tb = gladly_normalize_color(range_b, v_b, type_b);')
115
+ parts.push(' return gladly_apply_color(map_color_2d(-(cs_a + 1), vec2(ta, tb)));')
116
+ parts.push(' }')
117
+ parts.push(' return gladly_apply_color(')
118
+ parts.push(' (gladly_map_color_raw(cs_a, range_a, v_a, type_a) +')
119
+ parts.push(' gladly_map_color_raw(cs_b, range_b, v_b, type_b)) / 2.0')
120
+ parts.push(' );')
121
+ parts.push('}')
122
+
123
+ return parts.join('\n')
124
+ }
@@ -0,0 +1,237 @@
1
+ const textureComputations = new Map()
2
+ const glslComputations = new Map()
3
+
4
+ export class Computation {
5
+ schema(data) { throw new Error('Not implemented') }
6
+ }
7
+
8
+ export class TextureComputation extends Computation {
9
+ compute(regl, params, getAxisDomain) { throw new Error('Not implemented') }
10
+ }
11
+
12
+ export class GlslComputation extends Computation {
13
+ glsl(resolvedParams) { throw new Error('Not implemented') }
14
+ }
15
+
16
+ // Use in computation schema() methods for params that can be a Float32Array or sub-expression
17
+ export const EXPRESSION_REF = { '$ref': '#/$defs/expression' }
18
+
19
+ export function registerTextureComputation(name, computation) {
20
+ textureComputations.set(name, computation)
21
+ }
22
+
23
+ export function registerGlslComputation(name, computation) {
24
+ glslComputations.set(name, computation)
25
+ }
26
+
27
+ export function computationSchema(data) {
28
+ const cols = data ? data.columns() : []
29
+ const defs = {}
30
+
31
+ for (const [name, comp] of textureComputations) {
32
+ defs[`params_${name}`] = comp.schema(data)
33
+ }
34
+
35
+ for (const [name, comp] of glslComputations) {
36
+ defs[`params_${name}`] = comp.schema(data)
37
+ }
38
+
39
+ defs.expression = {
40
+ anyOf: [
41
+ { type: 'string', enum: cols },
42
+ ...[...textureComputations, ...glslComputations].map(([name]) => ({
43
+ type: 'object',
44
+ properties: { [name]: { '$ref': `#/$defs/params_${name}` } },
45
+ required: [name],
46
+ additionalProperties: false
47
+ }))
48
+ ]
49
+ }
50
+
51
+ return { '$defs': defs, '$ref': '#/$defs/expression' }
52
+ }
53
+
54
+ // Duck-type check for regl textures.
55
+ export function isTexture(value) {
56
+ return (
57
+ value !== null &&
58
+ typeof value === 'object' &&
59
+ typeof value.width === 'number' &&
60
+ typeof value.subimage === 'function'
61
+ )
62
+ }
63
+
64
+ function domainsEqual(a, b) {
65
+ if (a === b) return true
66
+ if (a == null || b == null) return a === b
67
+ return a[0] === b[0] && a[1] === b[1]
68
+ }
69
+
70
+ // Resolve expr to a raw JS value (Float32Array / texture / number).
71
+ // Used for texture computation params — GLSL expressions are not permitted here.
72
+ function resolveToRawValue(regl, expr, path, getAxisDomain) {
73
+ if (expr instanceof Float32Array) return expr
74
+ if (isTexture(expr)) return expr
75
+ if (typeof expr === 'number') return expr
76
+ if (typeof expr === 'string') return expr
77
+
78
+ if (typeof expr === 'object' && expr !== null) {
79
+ const keys = Object.keys(expr)
80
+
81
+ // Single-key object: check if it names a registered computation.
82
+ if (keys.length === 1) {
83
+ const compName = keys[0]
84
+ if (textureComputations.has(compName)) {
85
+ const comp = textureComputations.get(compName)
86
+ const params = expr[compName]
87
+ const resolvedParams = resolveToRawValue(regl, params, path, getAxisDomain)
88
+ return comp.compute(regl, resolvedParams, getAxisDomain)
89
+ }
90
+ if (glslComputations.has(compName)) {
91
+ throw new Error(
92
+ `GLSL computation '${compName}' cannot be used as a texture computation parameter`
93
+ )
94
+ }
95
+ }
96
+
97
+ // Plain params dict: resolve each value recursively.
98
+ const resolved = {}
99
+ for (const [k, v] of Object.entries(expr)) {
100
+ resolved[k] = resolveToRawValue(regl, v, `${path}_${k}`, getAxisDomain)
101
+ }
102
+ return resolved
103
+ }
104
+
105
+ throw new Error(`Cannot resolve to raw value: ${JSON.stringify(expr)}`)
106
+ }
107
+
108
+ // Resolve expr to a GLSL expression string.
109
+ // Side effects: populates context.bufferAttrs, textureUniforms, scalarUniforms, globalDecls, axisUpdaters.
110
+ function resolveToGlslExpr(regl, expr, path, context, plot) {
111
+ if (expr instanceof Float32Array) {
112
+ const attrName = `a_cgen_${path}`
113
+ context.bufferAttrs[attrName] = expr
114
+ context.globalDecls.push(`attribute float ${attrName};`)
115
+ return attrName
116
+ }
117
+
118
+ if (isTexture(expr)) {
119
+ const uniformName = `u_cgen_${path}`
120
+ const widthName = `u_cgen_${path}_width`
121
+ context.textureUniforms[uniformName] = expr
122
+ context.scalarUniforms[widthName] = expr.width
123
+ context.globalDecls.push(`uniform sampler2D ${uniformName};`)
124
+ context.globalDecls.push(`uniform float ${widthName};`)
125
+ return `texture2D(${uniformName}, vec2((a_pickId + 0.5) / ${widthName}, 0.5)).r`
126
+ }
127
+
128
+ if (typeof expr === 'number') {
129
+ const uniformName = `u_cgen_${path}`
130
+ context.scalarUniforms[uniformName] = expr
131
+ context.globalDecls.push(`uniform float ${uniformName};`)
132
+ return uniformName
133
+ }
134
+
135
+ if (typeof expr === 'object' && expr !== null) {
136
+ const keys = Object.keys(expr)
137
+
138
+ if (keys.length === 1) {
139
+ const compName = keys[0]
140
+ const params = expr[compName]
141
+
142
+ if (textureComputations.has(compName)) {
143
+ const comp = textureComputations.get(compName)
144
+ const uniformName = `u_cgen_${path}`
145
+ const widthName = `u_cgen_${path}_width`
146
+
147
+ // Live reference updated when axis domains change.
148
+ const liveRef = {
149
+ texture: null,
150
+ accessedAxes: new Set(),
151
+ cachedDomains: {}
152
+ }
153
+
154
+ const makeTrackingGetter = (ref, currentPlot) => (axisId) => {
155
+ ref.accessedAxes.add(axisId)
156
+ return currentPlot ? currentPlot.getAxisDomain(axisId) : null
157
+ }
158
+
159
+ // Initial computation with axis tracking.
160
+ const initGetter = makeTrackingGetter(liveRef, plot)
161
+ const resolvedParams = resolveToRawValue(regl, params, path, initGetter)
162
+ liveRef.texture = comp.compute(regl, resolvedParams, initGetter)
163
+
164
+ // Cache the domains accessed during initial computation.
165
+ for (const axisId of liveRef.accessedAxes) {
166
+ liveRef.cachedDomains[axisId] = plot ? plot.getAxisDomain(axisId) : null
167
+ }
168
+
169
+ // Use a function so regl reads the current texture each frame.
170
+ context.textureUniforms[uniformName] = () => liveRef.texture
171
+ // Texture width is constant across recomputes (same bins count).
172
+ context.scalarUniforms[widthName] = liveRef.texture.width
173
+ context.globalDecls.push(`uniform sampler2D ${uniformName};`)
174
+ context.globalDecls.push(`uniform float ${widthName};`)
175
+
176
+ context.axisUpdaters.push({
177
+ refreshIfNeeded(currentPlot) {
178
+ if (liveRef.accessedAxes.size === 0) return
179
+
180
+ let needsRecompute = false
181
+ for (const axisId of liveRef.accessedAxes) {
182
+ if (!domainsEqual(currentPlot.getAxisDomain(axisId), liveRef.cachedDomains[axisId])) {
183
+ needsRecompute = true
184
+ break
185
+ }
186
+ }
187
+ if (!needsRecompute) return
188
+
189
+ // Recompute with fresh axis tracking so new dependencies are captured.
190
+ const newRef = { accessedAxes: new Set(), cachedDomains: {} }
191
+ const newGetter = makeTrackingGetter(newRef, currentPlot)
192
+ const newParams = resolveToRawValue(regl, params, path, newGetter)
193
+ newRef.texture = comp.compute(regl, newParams, newGetter)
194
+ for (const axisId of newRef.accessedAxes) {
195
+ newRef.cachedDomains[axisId] = currentPlot.getAxisDomain(axisId)
196
+ }
197
+
198
+ // Update live ref in-place so the dynamic uniform picks up the new texture.
199
+ liveRef.texture = newRef.texture
200
+ liveRef.accessedAxes = newRef.accessedAxes
201
+ liveRef.cachedDomains = newRef.cachedDomains
202
+ }
203
+ })
204
+
205
+ return `texture2D(${uniformName}, vec2((a_pickId + 0.5) / ${widthName}, 0.5)).r`
206
+ }
207
+
208
+ if (glslComputations.has(compName)) {
209
+ const comp = glslComputations.get(compName)
210
+ const resolvedGlslParams = {}
211
+ for (const [k, v] of Object.entries(params)) {
212
+ resolvedGlslParams[k] = resolveToGlslExpr(regl, v, `${path}_${k}`, context, plot)
213
+ }
214
+ return comp.glsl(resolvedGlslParams)
215
+ }
216
+ }
217
+ }
218
+
219
+ throw new Error(`Cannot resolve to GLSL expression: ${JSON.stringify(expr)}`)
220
+ }
221
+
222
+ // Top-level resolver: decides whether expr is a plain buffer or a computed attribute.
223
+ export function resolveAttributeExpr(regl, expr, attrShaderName, plot) {
224
+ if (expr instanceof Float32Array) {
225
+ return { kind: 'buffer', value: expr }
226
+ }
227
+
228
+ const context = {
229
+ bufferAttrs: {},
230
+ textureUniforms: {},
231
+ scalarUniforms: {},
232
+ globalDecls: [],
233
+ axisUpdaters: []
234
+ }
235
+ const glslExpr = resolveToGlslExpr(regl, expr, attrShaderName, context, plot)
236
+ return { kind: 'computed', glslExpr, context }
237
+ }
@@ -0,0 +1,47 @@
1
+ import { registerTextureComputation, TextureComputation, EXPRESSION_REF } from "./ComputationRegistry.js"
2
+ import makeHistogram from "./hist.js"
3
+
4
+ // Texture computation that filters the input data by a filter axis range and
5
+ // then builds a histogram texture from the surviving values.
6
+ //
7
+ // params:
8
+ // input — Float32Array of values normalised to [0, 1] (for histogram bins)
9
+ // filterValues — Float32Array of raw filter-column values (same length as input)
10
+ // filterAxisId — string: axis ID / quantity kind whose domain drives the filter
11
+ // bins — optional number of histogram bins
12
+ //
13
+ // getAxisDomain(filterAxisId) returns [min|null, max|null] where null means
14
+ // unbounded. The computation is re-run automatically whenever the domain changes.
15
+ class FilteredHistogramComputation extends TextureComputation {
16
+ compute(regl, params, getAxisDomain) {
17
+ const { input, filterValues, filterAxisId, bins } = params
18
+ const domain = getAxisDomain(filterAxisId) // [min|null, max|null] or null
19
+ const filterMin = domain?.[0] ?? null
20
+ const filterMax = domain?.[1] ?? null
21
+
22
+ // Build a compact array of the normalised values that pass the filter.
23
+ const filtered = []
24
+ for (let i = 0; i < input.length; i++) {
25
+ const fv = filterValues[i]
26
+ if (filterMin !== null && fv < filterMin) continue
27
+ if (filterMax !== null && fv > filterMax) continue
28
+ filtered.push(input[i])
29
+ }
30
+
31
+ return makeHistogram(regl, new Float32Array(filtered), { bins })
32
+ }
33
+ schema(data) {
34
+ return {
35
+ type: 'object',
36
+ properties: {
37
+ input: EXPRESSION_REF,
38
+ filterValues: EXPRESSION_REF,
39
+ filterAxisId: { type: 'string' },
40
+ bins: { type: 'number' }
41
+ },
42
+ required: ['input', 'filterValues', 'filterAxisId']
43
+ }
44
+ }
45
+ }
46
+
47
+ registerTextureComputation('filteredHistogram', new FilteredHistogramComputation())