gladly-plot 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +16 -8
- package/src/axes/Axis.js +253 -0
- package/src/{AxisQuantityKindRegistry.js → axes/AxisQuantityKindRegistry.js} +7 -0
- package/src/{AxisRegistry.js → axes/AxisRegistry.js} +48 -0
- package/src/axes/ColorAxisRegistry.js +93 -0
- package/src/{FilterAxisRegistry.js → axes/FilterAxisRegistry.js} +63 -0
- package/src/axes/ZoomController.js +141 -0
- package/src/colorscales/BivariateColorscales.js +205 -0
- package/src/colorscales/ColorscaleRegistry.js +124 -0
- package/src/compute/ComputationRegistry.js +237 -0
- package/src/compute/axisFilter.js +47 -0
- package/src/compute/conv.js +230 -0
- package/src/compute/fft.js +292 -0
- package/src/compute/filter.js +227 -0
- package/src/compute/hist.js +180 -0
- package/src/compute/kde.js +102 -0
- package/src/{Layer.js → core/Layer.js} +4 -3
- package/src/{LayerType.js → core/LayerType.js} +72 -7
- package/src/core/Plot.js +735 -0
- package/src/{Colorbar.js → floats/Colorbar.js} +19 -5
- package/src/floats/Colorbar2d.js +77 -0
- package/src/{Filterbar.js → floats/Filterbar.js} +18 -4
- package/src/{FilterbarFloat.js → floats/Float.js} +17 -30
- package/src/{EpsgUtils.js → geo/EpsgUtils.js} +1 -1
- package/src/index.js +35 -22
- package/src/{ColorbarLayer.js → layers/ColorbarLayer.js} +2 -2
- package/src/layers/ColorbarLayer2d.js +97 -0
- package/src/{FilterbarLayer.js → layers/FilterbarLayer.js} +2 -2
- package/src/layers/HistogramLayer.js +212 -0
- package/src/layers/LinesLayer.js +199 -0
- package/src/layers/PointsLayer.js +114 -0
- package/src/layers/ScatterShared.js +142 -0
- package/src/{TileLayer.js → layers/TileLayer.js} +4 -4
- package/src/Axis.js +0 -48
- package/src/ColorAxisRegistry.js +0 -49
- package/src/ColorscaleRegistry.js +0 -52
- package/src/Float.js +0 -159
- package/src/Plot.js +0 -1068
- package/src/ScatterLayer.js +0 -133
- /package/src/{AxisLink.js → axes/AxisLink.js} +0 -0
- /package/src/{MatplotlibColorscales.js → colorscales/MatplotlibColorscales.js} +0 -0
- /package/src/{Data.js → core/Data.js} +0 -0
- /package/src/{LayerTypeRegistry.js → core/LayerTypeRegistry.js} +0 -0
|
@@ -0,0 +1,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())
|