gladly-plot 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/package.json +10 -11
- package/src/axes/Axis.js +320 -172
- package/src/axes/AxisLink.js +6 -2
- package/src/axes/AxisRegistry.js +116 -39
- package/src/axes/Camera.js +47 -0
- package/src/axes/ColorAxisRegistry.js +10 -2
- package/src/axes/FilterAxisRegistry.js +1 -1
- package/src/axes/TickLabelAtlas.js +99 -0
- package/src/axes/ZoomController.js +446 -124
- package/src/colorscales/ColorscaleRegistry.js +30 -10
- package/src/compute/ComputationRegistry.js +126 -184
- package/src/compute/axisFilter.js +21 -9
- package/src/compute/conv.js +64 -8
- package/src/compute/elementwise.js +72 -0
- package/src/compute/fft.js +106 -20
- package/src/compute/filter.js +105 -103
- package/src/compute/hist.js +247 -142
- package/src/compute/kde.js +64 -46
- package/src/compute/scatter2dInterpolate.js +277 -0
- package/src/compute/util.js +196 -0
- package/src/core/ComputePipeline.js +153 -0
- package/src/core/GlBase.js +141 -0
- package/src/core/Layer.js +22 -8
- package/src/core/LayerType.js +253 -92
- package/src/core/Plot.js +644 -162
- package/src/core/PlotGroup.js +204 -0
- package/src/core/ShaderQueue.js +73 -0
- package/src/data/ColumnData.js +269 -0
- package/src/data/Computation.js +95 -0
- package/src/data/Data.js +270 -0
- package/src/floats/Float.js +56 -0
- package/src/index.js +16 -4
- package/src/layers/BarsLayer.js +168 -0
- package/src/layers/ColorbarLayer.js +10 -14
- package/src/layers/ColorbarLayer2d.js +13 -24
- package/src/layers/FilterbarLayer.js +4 -3
- package/src/layers/LinesLayer.js +108 -122
- package/src/layers/PointsLayer.js +73 -69
- package/src/layers/ScatterShared.js +62 -106
- package/src/layers/TileLayer.js +20 -16
- package/src/math/mat4.js +100 -0
- package/src/core/Data.js +0 -67
- package/src/layers/HistogramLayer.js +0 -212
|
@@ -1,142 +1,98 @@
|
|
|
1
1
|
import { LayerType } from "../core/LayerType.js"
|
|
2
|
-
import {
|
|
3
|
-
import { Data } from "../
|
|
2
|
+
import { AXIS_GEOMETRY } from "../axes/AxisRegistry.js"
|
|
3
|
+
import { Data } from "../data/Data.js"
|
|
4
|
+
import { computationSchema, EXPRESSION_REF, EXPRESSION_REF_OPT, resolveQuantityKind } from "../compute/ComputationRegistry.js"
|
|
5
|
+
|
|
6
|
+
const X_AXES = Object.keys(AXIS_GEOMETRY).filter(a => AXIS_GEOMETRY[a].dir === 'x')
|
|
7
|
+
const Y_AXES = Object.keys(AXIS_GEOMETRY).filter(a => AXIS_GEOMETRY[a].dir === 'y')
|
|
8
|
+
const Z_AXES = Object.keys(AXIS_GEOMETRY).filter(a => AXIS_GEOMETRY[a].dir === 'z')
|
|
4
9
|
|
|
5
10
|
export class ScatterLayerTypeBase extends LayerType {
|
|
6
11
|
_getAxisConfig(parameters, data) {
|
|
7
12
|
const d = Data.wrap(data)
|
|
8
|
-
const {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
const
|
|
13
|
+
const {
|
|
14
|
+
xData, yData, zData: zDataRaw,
|
|
15
|
+
vData: vDataRaw, vData2: vData2Raw, fData: fDataRaw,
|
|
16
|
+
xAxis = "xaxis_bottom", yAxis = "yaxis_left", zAxis = "none",
|
|
17
|
+
} = parameters
|
|
18
|
+
const vDataIn = (vDataRaw == null || vDataRaw === "none") ? null : vDataRaw
|
|
19
|
+
const vData2In = (vData2Raw == null || vData2Raw === "none") ? null : vData2Raw
|
|
20
|
+
const fData = (fDataRaw == null || fDataRaw === "none") ? null : fDataRaw
|
|
21
|
+
const zData = (zDataRaw == null || zDataRaw === "none") ? null : zDataRaw
|
|
22
|
+
const zAxisResolved = (zAxis == null || zAxis === "none") ? null : zAxis
|
|
23
|
+
const vData = vDataIn
|
|
24
|
+
const vData2 = vData2In
|
|
25
|
+
const colorAxisQuantityKinds = {}
|
|
26
|
+
const vQK = vData ? resolveQuantityKind(vData, d) : null
|
|
27
|
+
const vQK2 = vData2 ? resolveQuantityKind(vData2, d) : null
|
|
28
|
+
if (vQK) colorAxisQuantityKinds[''] = vQK
|
|
29
|
+
if (vQK2) colorAxisQuantityKinds['2'] = vQK2
|
|
30
|
+
const colorAxis2dQuantityKinds = vData && vData2 ? { '': ['', '2'] } : {}
|
|
31
|
+
const filterAxisQuantityKinds = fData ? { '': resolveQuantityKind(fData, d) } : {}
|
|
14
32
|
return {
|
|
15
33
|
xAxis,
|
|
16
|
-
xAxisQuantityKind:
|
|
34
|
+
xAxisQuantityKind: resolveQuantityKind(xData, d) ?? undefined,
|
|
17
35
|
yAxis,
|
|
18
|
-
yAxisQuantityKind:
|
|
36
|
+
yAxisQuantityKind: resolveQuantityKind(yData, d) ?? undefined,
|
|
37
|
+
zAxis: zData ? (zAxisResolved ?? "zaxis_bottom_left") : null,
|
|
38
|
+
zAxisQuantityKind: zData ? (resolveQuantityKind(zData, d) ?? undefined) : undefined,
|
|
19
39
|
colorAxisQuantityKinds,
|
|
40
|
+
colorAxis2dQuantityKinds,
|
|
20
41
|
filterAxisQuantityKinds,
|
|
21
42
|
}
|
|
22
43
|
}
|
|
23
44
|
|
|
24
|
-
_commonSchemaProperties(
|
|
45
|
+
_commonSchemaProperties(data) {
|
|
25
46
|
return {
|
|
26
|
-
xData:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
type: "string",
|
|
33
|
-
enum: dataProperties,
|
|
34
|
-
description: "Property name in data object for y coordinates"
|
|
35
|
-
},
|
|
36
|
-
vData: {
|
|
37
|
-
type: "string",
|
|
38
|
-
enum: ["none"].concat(dataProperties),
|
|
39
|
-
description: "Primary property name in data object for color values"
|
|
40
|
-
},
|
|
41
|
-
vData2: {
|
|
42
|
-
type: "string",
|
|
43
|
-
enum: ["none"].concat(dataProperties),
|
|
44
|
-
description: "Optional secondary property name for 2D color mapping"
|
|
45
|
-
},
|
|
46
|
-
fData: {
|
|
47
|
-
type: "string",
|
|
48
|
-
enum: ["none"].concat(dataProperties),
|
|
49
|
-
description: "Optional property name for filter axis values"
|
|
50
|
-
},
|
|
47
|
+
xData: EXPRESSION_REF,
|
|
48
|
+
yData: EXPRESSION_REF,
|
|
49
|
+
zData: EXPRESSION_REF_OPT,
|
|
50
|
+
vData: EXPRESSION_REF_OPT,
|
|
51
|
+
vData2: EXPRESSION_REF_OPT,
|
|
52
|
+
fData: EXPRESSION_REF_OPT,
|
|
51
53
|
xAxis: {
|
|
52
54
|
type: "string",
|
|
53
|
-
enum:
|
|
55
|
+
enum: X_AXES,
|
|
54
56
|
default: "xaxis_bottom",
|
|
55
|
-
description: "Which x-axis to use for this layer"
|
|
57
|
+
description: "Which x-axis to use for this layer",
|
|
56
58
|
},
|
|
57
59
|
yAxis: {
|
|
58
60
|
type: "string",
|
|
59
|
-
enum:
|
|
61
|
+
enum: Y_AXES,
|
|
60
62
|
default: "yaxis_left",
|
|
61
|
-
description: "Which y-axis to use for this layer"
|
|
63
|
+
description: "Which y-axis to use for this layer",
|
|
62
64
|
},
|
|
63
|
-
|
|
64
|
-
type: "
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
zAxis: {
|
|
66
|
+
type: "string",
|
|
67
|
+
enum: ["none", ...Z_AXES],
|
|
68
|
+
default: "none",
|
|
69
|
+
description: "Which z-axis to use for this layer (enables 3D mode)",
|
|
67
70
|
},
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
const { xData, yData, vData: vDataOrig, vData2: vData2Orig, fData: fDataOrig, alphaBlend = false } = parameters
|
|
73
|
-
const vData = vDataOrig == "none" ? null : vDataOrig
|
|
74
|
-
const vData2 = vData2Orig == "none" ? null : vData2Orig
|
|
75
|
-
const fData = fDataOrig == "none" ? null : fDataOrig
|
|
76
|
-
|
|
77
|
-
const xQK = d.getQuantityKind(xData) ?? xData
|
|
78
|
-
const yQK = d.getQuantityKind(yData) ?? yData
|
|
79
|
-
const vQK = vData ? (d.getQuantityKind(vData) ?? vData) : null
|
|
80
|
-
const vQK2 = vData2 ? (d.getQuantityKind(vData2) ?? vData2) : null
|
|
81
|
-
const fQK = fData ? (d.getQuantityKind(fData) ?? fData) : null
|
|
82
|
-
|
|
83
|
-
const srcX = d.getData(xData)
|
|
84
|
-
const srcY = d.getData(yData)
|
|
85
|
-
const srcV = vData ? d.getData(vData) : null
|
|
86
|
-
const srcV2 = vData2 ? d.getData(vData2) : null
|
|
87
|
-
const srcF = fData ? d.getData(fData) : null
|
|
88
|
-
|
|
89
|
-
if (!srcX) throw new Error(`Data column '${xData}' not found`)
|
|
90
|
-
if (!srcY) throw new Error(`Data column '${yData}' not found`)
|
|
91
|
-
if (vData && !srcV) throw new Error(`Data column '${vData}' not found`)
|
|
92
|
-
if (vData2 && !srcV2) throw new Error(`Data column '${vData2}' not found`)
|
|
93
|
-
if (fData && !srcF) throw new Error(`Data column '${fData}' not found`)
|
|
94
|
-
|
|
95
|
-
return { xData, yData, vData, vData2, fData, alphaBlend, xQK, yQK, vQK, vQK2, fQK, srcX, srcY, srcV, srcV2, srcF }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
_buildDomains(d, xData, yData, vData, vData2, xQK, yQK, vQK, vQK2) {
|
|
74
|
+
_buildDomains(d, xData, yData, zData, vData, vData2, xQK, yQK, zQK, vQK, vQK2) {
|
|
99
75
|
const domains = {}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
76
|
+
if (xQK && typeof xData === 'string') {
|
|
77
|
+
const xDomain = d.getDomain(xData)
|
|
78
|
+
if (xDomain) domains[xQK] = xDomain
|
|
79
|
+
}
|
|
80
|
+
if (yQK && typeof yData === 'string') {
|
|
81
|
+
const yDomain = d.getDomain(yData)
|
|
82
|
+
if (yDomain) domains[yQK] = yDomain
|
|
83
|
+
}
|
|
84
|
+
if (zData && zQK && typeof zData === 'string') {
|
|
85
|
+
const zDomain = d.getDomain(zData)
|
|
86
|
+
if (zDomain) domains[zQK] = zDomain
|
|
87
|
+
}
|
|
88
|
+
if (vData && vQK && typeof vData === 'string') {
|
|
108
89
|
const vDomain = d.getDomain(vData)
|
|
109
90
|
if (vDomain) domains[vQK] = vDomain
|
|
110
91
|
}
|
|
111
|
-
|
|
112
|
-
if (vData2) {
|
|
92
|
+
if (vData2 && vQK2 && typeof vData2 === 'string') {
|
|
113
93
|
const vDomain2 = d.getDomain(vData2)
|
|
114
94
|
if (vDomain2) domains[vQK2] = vDomain2
|
|
115
95
|
}
|
|
116
|
-
|
|
117
96
|
return domains
|
|
118
97
|
}
|
|
119
|
-
|
|
120
|
-
_buildNameMap(vData, vQK, vData2, vQK2, fData, fQK) {
|
|
121
|
-
return {
|
|
122
|
-
...(vData ? {
|
|
123
|
-
[`colorscale_${vQK}`]: 'colorscale',
|
|
124
|
-
[`color_range_${vQK}`]: 'color_range',
|
|
125
|
-
[`color_scale_type_${vQK}`]: 'color_scale_type',
|
|
126
|
-
} : {}),
|
|
127
|
-
...(vData2 ? {
|
|
128
|
-
[`colorscale_${vQK2}`]: 'colorscale2',
|
|
129
|
-
[`color_range_${vQK2}`]: 'color_range2',
|
|
130
|
-
[`color_scale_type_${vQK2}`]: 'color_scale_type2',
|
|
131
|
-
} : {}),
|
|
132
|
-
...(fData ? { [`filter_range_${fQK}`]: 'filter_range' } : {}),
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
_buildBlendConfig(alphaBlend) {
|
|
137
|
-
return alphaBlend ? {
|
|
138
|
-
enable: true,
|
|
139
|
-
func: { srcRGB: 'src alpha', dstRGB: 'one minus src alpha', srcAlpha: 0, dstAlpha: 1 },
|
|
140
|
-
} : null
|
|
141
|
-
}
|
|
142
98
|
}
|
package/src/layers/TileLayer.js
CHANGED
|
@@ -410,15 +410,15 @@ class TileManager {
|
|
|
410
410
|
|
|
411
411
|
// ─── GLSL ─────────────────────────────────────────────────────────────────────
|
|
412
412
|
|
|
413
|
-
const TILE_VERT =
|
|
413
|
+
const TILE_VERT = `#version 300 es
|
|
414
414
|
precision mediump float;
|
|
415
|
-
|
|
416
|
-
|
|
415
|
+
in vec2 position;
|
|
416
|
+
in vec2 uv;
|
|
417
417
|
uniform vec2 xDomain;
|
|
418
418
|
uniform vec2 yDomain;
|
|
419
419
|
uniform float xScaleType;
|
|
420
420
|
uniform float yScaleType;
|
|
421
|
-
|
|
421
|
+
out vec2 vUv;
|
|
422
422
|
|
|
423
423
|
float normalize_axis(float v, vec2 domain, float scaleType) {
|
|
424
424
|
float vt = scaleType > 0.5 ? log(v) : v;
|
|
@@ -426,24 +426,28 @@ const TILE_VERT = `
|
|
|
426
426
|
float d1 = scaleType > 0.5 ? log(domain.y) : domain.y;
|
|
427
427
|
return (vt - d0) / (d1 - d0);
|
|
428
428
|
}
|
|
429
|
+
vec4 plot_pos(vec2 pos) {
|
|
430
|
+
float nx = normalize_axis(pos.x, xDomain, xScaleType);
|
|
431
|
+
float ny = normalize_axis(pos.y, yDomain, yScaleType);
|
|
432
|
+
return vec4(nx * 2.0 - 1.0, ny * 2.0 - 1.0, 0.0, 1.0);
|
|
433
|
+
}
|
|
429
434
|
|
|
430
435
|
void main() {
|
|
431
|
-
|
|
432
|
-
float ny = normalize_axis(position.y, yDomain, yScaleType);
|
|
433
|
-
gl_Position = vec4(nx * 2.0 - 1.0, ny * 2.0 - 1.0, 0.0, 1.0);
|
|
436
|
+
gl_Position = plot_pos(position);
|
|
434
437
|
vUv = uv;
|
|
435
438
|
}
|
|
436
439
|
`
|
|
437
440
|
|
|
438
|
-
const TILE_FRAG =
|
|
441
|
+
const TILE_FRAG = `#version 300 es
|
|
439
442
|
precision mediump float;
|
|
440
443
|
uniform sampler2D tileTexture;
|
|
441
444
|
uniform float opacity;
|
|
442
|
-
|
|
445
|
+
in vec2 vUv;
|
|
446
|
+
out vec4 fragColor;
|
|
443
447
|
|
|
444
448
|
void main() {
|
|
445
|
-
vec4 color =
|
|
446
|
-
|
|
449
|
+
vec4 color = texture(tileTexture, vUv);
|
|
450
|
+
fragColor = vec4(color.rgb, color.a * opacity);
|
|
447
451
|
}
|
|
448
452
|
`
|
|
449
453
|
|
|
@@ -587,12 +591,12 @@ class TileLayerType extends LayerType {
|
|
|
587
591
|
xAxisQuantityKind: crsToQkX(effectivePlotCrs),
|
|
588
592
|
yAxis,
|
|
589
593
|
yAxisQuantityKind: crsToQkY(effectivePlotCrs),
|
|
590
|
-
colorAxisQuantityKinds:
|
|
591
|
-
filterAxisQuantityKinds:
|
|
594
|
+
colorAxisQuantityKinds: {},
|
|
595
|
+
filterAxisQuantityKinds: {},
|
|
592
596
|
}
|
|
593
597
|
}
|
|
594
598
|
|
|
595
|
-
createLayer(parameters, _data) {
|
|
599
|
+
createLayer(regl, parameters, _data) {
|
|
596
600
|
const {
|
|
597
601
|
xAxis = 'xaxis_bottom',
|
|
598
602
|
yAxis = 'yaxis_left',
|
|
@@ -609,8 +613,8 @@ class TileLayerType extends LayerType {
|
|
|
609
613
|
yAxis,
|
|
610
614
|
xAxisQuantityKind: crsToQkX(effectivePlotCrs),
|
|
611
615
|
yAxisQuantityKind: crsToQkY(effectivePlotCrs),
|
|
612
|
-
colorAxes:
|
|
613
|
-
filterAxes:
|
|
616
|
+
colorAxes: {},
|
|
617
|
+
filterAxes: {},
|
|
614
618
|
vertexCount: 0,
|
|
615
619
|
instanceCount: null,
|
|
616
620
|
attributes: {},
|
package/src/math/mat4.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Column-major 4×4 matrix utilities matching WebGL/GLSL convention.
|
|
2
|
+
// Element at row r, column c is stored at index c*4+r.
|
|
3
|
+
|
|
4
|
+
export function mat4Identity() {
|
|
5
|
+
return new Float32Array([
|
|
6
|
+
1, 0, 0, 0,
|
|
7
|
+
0, 1, 0, 0,
|
|
8
|
+
0, 0, 1, 0,
|
|
9
|
+
0, 0, 0, 1,
|
|
10
|
+
])
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// a * b (left-multiplies a by b).
|
|
14
|
+
export function mat4Multiply(a, b) {
|
|
15
|
+
const out = new Float32Array(16)
|
|
16
|
+
for (let col = 0; col < 4; col++) {
|
|
17
|
+
for (let row = 0; row < 4; row++) {
|
|
18
|
+
let s = 0
|
|
19
|
+
for (let k = 0; k < 4; k++) s += a[k * 4 + row] * b[col * 4 + k]
|
|
20
|
+
out[col * 4 + row] = s
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Perspective projection. fovY in radians, aspect = width/height.
|
|
27
|
+
export function mat4Perspective(fovY, aspect, near, far) {
|
|
28
|
+
const f = 1 / Math.tan(fovY / 2)
|
|
29
|
+
const nf = 1 / (near - far)
|
|
30
|
+
return new Float32Array([
|
|
31
|
+
f / aspect, 0, 0, 0,
|
|
32
|
+
0, f, 0, 0,
|
|
33
|
+
0, 0, (far + near) * nf, -1,
|
|
34
|
+
0, 0, 2 * far * near * nf, 0,
|
|
35
|
+
])
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// View matrix looking from eye toward center with the given up vector.
|
|
39
|
+
export function mat4LookAt(eye, center, up) {
|
|
40
|
+
const f = vec3Normalize([center[0] - eye[0], center[1] - eye[1], center[2] - eye[2]])
|
|
41
|
+
const r = vec3Normalize(vec3Cross(f, up))
|
|
42
|
+
const u = vec3Cross(r, f)
|
|
43
|
+
return new Float32Array([
|
|
44
|
+
r[0], u[0], -f[0], 0,
|
|
45
|
+
r[1], u[1], -f[1], 0,
|
|
46
|
+
r[2], u[2], -f[2], 0,
|
|
47
|
+
-vec3Dot(r, eye), -vec3Dot(u, eye), vec3Dot(f, eye), 1,
|
|
48
|
+
])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Multiply a 4×4 column-major matrix by a vec4.
|
|
52
|
+
export function mat4MulVec4(m, v) {
|
|
53
|
+
return [
|
|
54
|
+
m[0]*v[0] + m[4]*v[1] + m[8] *v[2] + m[12]*v[3],
|
|
55
|
+
m[1]*v[0] + m[5]*v[1] + m[9] *v[2] + m[13]*v[3],
|
|
56
|
+
m[2]*v[0] + m[6]*v[1] + m[10]*v[2] + m[14]*v[3],
|
|
57
|
+
m[3]*v[0] + m[7]*v[1] + m[11]*v[2] + m[15]*v[3],
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function vec3Normalize(v) {
|
|
62
|
+
const len = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
|
|
63
|
+
return len > 1e-10 ? [v[0]/len, v[1]/len, v[2]/len] : [0, 0, 0]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function vec3Cross(a, b) {
|
|
67
|
+
return [
|
|
68
|
+
a[1]*b[2] - a[2]*b[1],
|
|
69
|
+
a[2]*b[0] - a[0]*b[2],
|
|
70
|
+
a[0]*b[1] - a[1]*b[0],
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function vec3Dot(a, b) {
|
|
75
|
+
return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Convert spherical coords (azimuth theta around y-axis, elevation phi from equator)
|
|
79
|
+
// to Cartesian.
|
|
80
|
+
export function sphericalToCartesian(theta, phi, r) {
|
|
81
|
+
return [
|
|
82
|
+
r * Math.cos(phi) * Math.sin(theta),
|
|
83
|
+
r * Math.sin(phi),
|
|
84
|
+
r * Math.cos(phi) * Math.cos(theta),
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Project a 3D model-space point to HTML screen coordinates (y from top) using an MVP
|
|
89
|
+
// matrix that maps model space to full-canvas NDC, and the canvas dimensions.
|
|
90
|
+
// Returns [htmlX, htmlY] or null if the point is behind the camera (w ≤ 0).
|
|
91
|
+
export function projectToScreen(point, mvp, canvasWidth, canvasHeight) {
|
|
92
|
+
const clip = mat4MulVec4(mvp, [point[0], point[1], point[2], 1.0])
|
|
93
|
+
if (clip[3] <= 0) return null
|
|
94
|
+
const ndcX = clip[0] / clip[3]
|
|
95
|
+
const ndcY = clip[1] / clip[3]
|
|
96
|
+
return [
|
|
97
|
+
(ndcX + 1) * 0.5 * canvasWidth,
|
|
98
|
+
(1 - (ndcY + 1) * 0.5) * canvasHeight,
|
|
99
|
+
]
|
|
100
|
+
}
|
package/src/core/Data.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
export class Data {
|
|
2
|
-
constructor(raw) {
|
|
3
|
-
raw = raw ?? {}
|
|
4
|
-
// Columnar format: { data: {col: Float32Array}, quantity_kinds?: {...}, domains?: {...} }
|
|
5
|
-
// Detected by the presence of a top-level `data` property that is a plain object.
|
|
6
|
-
if (raw.data != null && typeof raw.data === 'object' && !(raw.data instanceof Float32Array)) {
|
|
7
|
-
this._columnar = true
|
|
8
|
-
this._data = raw.data
|
|
9
|
-
this._quantityKinds = raw.quantity_kinds ?? {}
|
|
10
|
-
this._rawDomains = raw.domains ?? {}
|
|
11
|
-
} else {
|
|
12
|
-
this._columnar = false
|
|
13
|
-
this._raw = raw
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
static wrap(data) {
|
|
18
|
-
if (data != null && typeof data.columns === 'function' && typeof data.getData === 'function') {
|
|
19
|
-
return data
|
|
20
|
-
}
|
|
21
|
-
return new Data(data)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
_entry(col) {
|
|
25
|
-
if (this._columnar) {
|
|
26
|
-
const rawDomain = this._rawDomains[col]
|
|
27
|
-
let domain
|
|
28
|
-
if (Array.isArray(rawDomain)) {
|
|
29
|
-
domain = [rawDomain[0], rawDomain[1]]
|
|
30
|
-
} else if (rawDomain && typeof rawDomain === 'object') {
|
|
31
|
-
domain = [rawDomain.min, rawDomain.max]
|
|
32
|
-
}
|
|
33
|
-
return { data: this._data[col], quantityKind: this._quantityKinds[col], domain }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const v = this._raw[col]
|
|
37
|
-
if (v instanceof Float32Array) {
|
|
38
|
-
return { data: v, quantityKind: undefined, domain: undefined }
|
|
39
|
-
}
|
|
40
|
-
if (v && typeof v === 'object') {
|
|
41
|
-
let domain
|
|
42
|
-
if (Array.isArray(v.domain)) {
|
|
43
|
-
domain = [v.domain[0], v.domain[1]]
|
|
44
|
-
} else if (v.domain && typeof v.domain === 'object') {
|
|
45
|
-
domain = [v.domain.min, v.domain.max]
|
|
46
|
-
}
|
|
47
|
-
return { data: v.data, quantityKind: v.quantity_kind, domain }
|
|
48
|
-
}
|
|
49
|
-
return { data: undefined, quantityKind: undefined, domain: undefined }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
columns() {
|
|
53
|
-
return this._columnar ? Object.keys(this._data) : Object.keys(this._raw)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
getData(col) {
|
|
57
|
-
return this._entry(col).data
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
getQuantityKind(col) {
|
|
61
|
-
return this._entry(col).quantityKind
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
getDomain(col) {
|
|
65
|
-
return this._entry(col).domain
|
|
66
|
-
}
|
|
67
|
-
}
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { LayerType } from "../core/LayerType.js"
|
|
2
|
-
import { Data } from "../core/Data.js"
|
|
3
|
-
import { registerLayerType } from "../core/LayerTypeRegistry.js"
|
|
4
|
-
import { AXES } from "../axes/AxisRegistry.js"
|
|
5
|
-
|
|
6
|
-
// Ensure the 'histogram' and 'filteredHistogram' texture computations are registered.
|
|
7
|
-
import "../compute/hist.js"
|
|
8
|
-
import "../compute/axisFilter.js"
|
|
9
|
-
|
|
10
|
-
// Each bar is a quad drawn as a triangle strip (4 vertices).
|
|
11
|
-
// Per-instance: x_center (bin centre in data space), a_pickId (bin index).
|
|
12
|
-
// Per-vertex: a_corner ∈ {0,1,2,3} — selects which corner of the rectangle.
|
|
13
|
-
// corner 0: bottom-left corner 1: bottom-right
|
|
14
|
-
// corner 2: top-left corner 3: top-right
|
|
15
|
-
// The `count` attribute is resolved via the 'histogram' texture computation so
|
|
16
|
-
// its value equals the bin count sampled at a_pickId.
|
|
17
|
-
|
|
18
|
-
const HIST_VERT = `
|
|
19
|
-
precision mediump float;
|
|
20
|
-
|
|
21
|
-
attribute float a_corner;
|
|
22
|
-
attribute float x_center;
|
|
23
|
-
attribute float count;
|
|
24
|
-
|
|
25
|
-
uniform vec2 xDomain;
|
|
26
|
-
uniform vec2 yDomain;
|
|
27
|
-
uniform float xScaleType;
|
|
28
|
-
uniform float yScaleType;
|
|
29
|
-
uniform float u_binHalfWidth;
|
|
30
|
-
|
|
31
|
-
void main() {
|
|
32
|
-
float side = mod(a_corner, 2.0); // 0 = left, 1 = right
|
|
33
|
-
float top = floor(a_corner / 2.0); // 0 = bottom, 1 = top
|
|
34
|
-
|
|
35
|
-
float bx = x_center + (side * 2.0 - 1.0) * u_binHalfWidth;
|
|
36
|
-
float by = top * count;
|
|
37
|
-
|
|
38
|
-
float nx = normalize_axis(bx, xDomain, xScaleType);
|
|
39
|
-
float ny = normalize_axis(by, yDomain, yScaleType);
|
|
40
|
-
gl_Position = vec4(nx * 2.0 - 1.0, ny * 2.0 - 1.0, 0.0, 1.0);
|
|
41
|
-
}
|
|
42
|
-
`
|
|
43
|
-
|
|
44
|
-
const HIST_FRAG = `
|
|
45
|
-
precision mediump float;
|
|
46
|
-
uniform vec4 u_color;
|
|
47
|
-
void main() {
|
|
48
|
-
gl_FragColor = gladly_apply_color(u_color);
|
|
49
|
-
}
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
class HistogramLayerType extends LayerType {
|
|
53
|
-
constructor() {
|
|
54
|
-
super({ name: "histogram", vert: HIST_VERT, frag: HIST_FRAG })
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
_getAxisConfig(parameters, data) {
|
|
58
|
-
const d = Data.wrap(data)
|
|
59
|
-
const { vData, xAxis = "xaxis_bottom", yAxis = "yaxis_left", filterColumn } = parameters
|
|
60
|
-
const activeFilter = filterColumn && filterColumn !== "none" ? filterColumn : null
|
|
61
|
-
const filterQK = activeFilter ? (d.getQuantityKind(activeFilter) ?? activeFilter) : null
|
|
62
|
-
return {
|
|
63
|
-
xAxis,
|
|
64
|
-
xAxisQuantityKind: d.getQuantityKind(vData) ?? vData,
|
|
65
|
-
yAxis,
|
|
66
|
-
yAxisQuantityKind: "count",
|
|
67
|
-
...(filterQK ? { filterAxisQuantityKinds: [filterQK] } : {}),
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
schema(data) {
|
|
72
|
-
const dataProperties = Data.wrap(data).columns()
|
|
73
|
-
return {
|
|
74
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
75
|
-
type: "object",
|
|
76
|
-
properties: {
|
|
77
|
-
vData: {
|
|
78
|
-
type: "string",
|
|
79
|
-
enum: dataProperties,
|
|
80
|
-
description: "Data column to histogram"
|
|
81
|
-
},
|
|
82
|
-
filterColumn: {
|
|
83
|
-
type: "string",
|
|
84
|
-
enum: ["none", ...dataProperties],
|
|
85
|
-
description: "Data column used to filter points via its filter axis, or 'none'"
|
|
86
|
-
},
|
|
87
|
-
bins: {
|
|
88
|
-
type: "integer",
|
|
89
|
-
description: "Number of bins (auto-selected by sqrt rule if omitted)"
|
|
90
|
-
},
|
|
91
|
-
color: {
|
|
92
|
-
type: "array",
|
|
93
|
-
items: { type: "number" },
|
|
94
|
-
minItems: 4,
|
|
95
|
-
maxItems: 4,
|
|
96
|
-
default: [0.2, 0.5, 0.8, 1.0],
|
|
97
|
-
description: "Bar colour as [R, G, B, A] in [0, 1]"
|
|
98
|
-
},
|
|
99
|
-
xAxis: {
|
|
100
|
-
type: "string",
|
|
101
|
-
enum: AXES.filter(a => a.includes("x")),
|
|
102
|
-
default: "xaxis_bottom"
|
|
103
|
-
},
|
|
104
|
-
yAxis: {
|
|
105
|
-
type: "string",
|
|
106
|
-
enum: AXES.filter(a => a.includes("y")),
|
|
107
|
-
default: "yaxis_left"
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
required: ["vData", "filterColumn"]
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
_createLayer(parameters, data) {
|
|
115
|
-
const d = Data.wrap(data)
|
|
116
|
-
const {
|
|
117
|
-
vData,
|
|
118
|
-
bins: requestedBins = null,
|
|
119
|
-
color = [0.2, 0.5, 0.8, 1.0],
|
|
120
|
-
filterColumn = "none",
|
|
121
|
-
} = parameters
|
|
122
|
-
|
|
123
|
-
const srcV = d.getData(vData)
|
|
124
|
-
if (!srcV) throw new Error(`Data column '${vData}' not found`)
|
|
125
|
-
const vQK = d.getQuantityKind(vData) ?? vData
|
|
126
|
-
|
|
127
|
-
// --- Optional filter column ---
|
|
128
|
-
const activeFilter = filterColumn && filterColumn !== "none" ? filterColumn : null
|
|
129
|
-
const filterQK = activeFilter ? (d.getQuantityKind(activeFilter) ?? activeFilter) : null
|
|
130
|
-
const srcF = activeFilter ? d.getData(activeFilter) : null
|
|
131
|
-
if (activeFilter && !srcF) throw new Error(`Data column '${activeFilter}' not found`)
|
|
132
|
-
|
|
133
|
-
// --- Compute min/max for normalization ---
|
|
134
|
-
let min = Infinity, max = -Infinity
|
|
135
|
-
for (let i = 0; i < srcV.length; i++) {
|
|
136
|
-
if (srcV[i] < min) min = srcV[i]
|
|
137
|
-
if (srcV[i] > max) max = srcV[i]
|
|
138
|
-
}
|
|
139
|
-
const range = max - min || 1
|
|
140
|
-
|
|
141
|
-
// --- Choose bin count ---
|
|
142
|
-
const bins = requestedBins || Math.max(10, Math.min(200, Math.ceil(Math.sqrt(srcV.length))))
|
|
143
|
-
const binWidth = range / bins
|
|
144
|
-
|
|
145
|
-
// --- Normalize data to [0, 1] for the histogram computation ---
|
|
146
|
-
const normalized = new Float32Array(srcV.length)
|
|
147
|
-
for (let i = 0; i < srcV.length; i++) {
|
|
148
|
-
normalized[i] = (srcV[i] - min) / range
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// --- CPU histogram for domain (y-axis range) estimation ---
|
|
152
|
-
// Uses unfiltered data so the y-axis scale stays stable while the filter moves.
|
|
153
|
-
const histCpu = new Float32Array(bins)
|
|
154
|
-
for (let i = 0; i < srcV.length; i++) {
|
|
155
|
-
const b = Math.min(Math.floor(normalized[i] * bins), bins - 1)
|
|
156
|
-
histCpu[b] += 1
|
|
157
|
-
}
|
|
158
|
-
const maxCount = Math.max(...histCpu)
|
|
159
|
-
|
|
160
|
-
// --- Per-instance: bin centre positions in data space ---
|
|
161
|
-
const x_center = new Float32Array(bins)
|
|
162
|
-
for (let i = 0; i < bins; i++) {
|
|
163
|
-
x_center[i] = min + (i + 0.5) * binWidth
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// --- Per-vertex: corner indices 0–3 (triangle-strip quad) ---
|
|
167
|
-
const a_corner = new Float32Array([0, 1, 2, 3])
|
|
168
|
-
|
|
169
|
-
// --- Build count attribute expression ---
|
|
170
|
-
// When a filter column is provided, use filteredHistogram so the computation
|
|
171
|
-
// re-runs whenever the filter axis domain changes.
|
|
172
|
-
const countAttr = filterQK
|
|
173
|
-
? { filteredHistogram: { input: normalized, filterValues: srcF, filterAxisId: filterQK, bins } }
|
|
174
|
-
: { histogram: { input: normalized, bins } }
|
|
175
|
-
|
|
176
|
-
// --- Compute filter column extent for the filterbar display range ---
|
|
177
|
-
const filterDomains = {}
|
|
178
|
-
if (filterQK && srcF) {
|
|
179
|
-
let fMin = Infinity, fMax = -Infinity
|
|
180
|
-
for (let i = 0; i < srcF.length; i++) {
|
|
181
|
-
if (srcF[i] < fMin) fMin = srcF[i]
|
|
182
|
-
if (srcF[i] > fMax) fMax = srcF[i]
|
|
183
|
-
}
|
|
184
|
-
filterDomains[filterQK] = [fMin, fMax]
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return [{
|
|
188
|
-
attributes: {
|
|
189
|
-
a_corner, // per-vertex (no divisor)
|
|
190
|
-
x_center, // per-instance (divisor 1)
|
|
191
|
-
// GPU histogram via computed attribute; sampled at a_pickId (= bin index)
|
|
192
|
-
count: countAttr,
|
|
193
|
-
},
|
|
194
|
-
attributeDivisors: { x_center: 1 },
|
|
195
|
-
uniforms: {
|
|
196
|
-
u_binHalfWidth: binWidth / 2,
|
|
197
|
-
u_color: color,
|
|
198
|
-
},
|
|
199
|
-
vertexCount: 4,
|
|
200
|
-
instanceCount: bins,
|
|
201
|
-
primitive: "triangle strip",
|
|
202
|
-
domains: {
|
|
203
|
-
[vQK]: [min, max],
|
|
204
|
-
count: [0, maxCount],
|
|
205
|
-
...filterDomains,
|
|
206
|
-
},
|
|
207
|
-
}]
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export const histogramLayerType = new HistogramLayerType()
|
|
212
|
-
registerLayerType("histogram", histogramLayerType)
|