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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import reglInit from "regl"
|
|
2
|
+
import { FilterAxisRegistry } from "../axes/FilterAxisRegistry.js"
|
|
3
|
+
import { Axis } from "../axes/Axis.js"
|
|
4
|
+
import { DataGroup, ComputedDataNode } from "../data/Data.js"
|
|
5
|
+
import { getComputedData } from "../compute/ComputationRegistry.js"
|
|
6
|
+
|
|
7
|
+
export class GlBase {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.regl = null
|
|
10
|
+
this.currentData = null
|
|
11
|
+
this._rawData = null
|
|
12
|
+
this._dataTransformNodes = []
|
|
13
|
+
this.filterAxisRegistry = null
|
|
14
|
+
this._axisCache = new Map()
|
|
15
|
+
this._axesProxy = null
|
|
16
|
+
this._initEpoch = 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_initRegl(canvas) {
|
|
20
|
+
const gl = canvas.getContext('webgl2', { desynchronized: true })
|
|
21
|
+
if (!gl) throw new Error('WebGL 2.0 is required but not supported')
|
|
22
|
+
|
|
23
|
+
const origGetExtension = gl.getExtension.bind(gl)
|
|
24
|
+
gl.getExtension = (name) => {
|
|
25
|
+
const lname = name.toLowerCase()
|
|
26
|
+
const wgl2CoreExts = ['oes_texture_float', 'oes_texture_float_linear']
|
|
27
|
+
if (wgl2CoreExts.includes(lname)) return origGetExtension(name) ?? {}
|
|
28
|
+
if (lname === 'angle_instanced_arrays') {
|
|
29
|
+
return origGetExtension(name) ?? {
|
|
30
|
+
vertexAttribDivisorANGLE: gl.vertexAttribDivisor.bind(gl),
|
|
31
|
+
drawArraysInstancedANGLE: gl.drawArraysInstanced.bind(gl),
|
|
32
|
+
drawElementsInstancedANGLE: gl.drawElementsInstanced.bind(gl),
|
|
33
|
+
VERTEX_ATTRIB_ARRAY_DIVISOR_ANGLE: 0x88FE
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return origGetExtension(name)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const GL_RGBA = 0x1908, GL_FLOAT = 0x1406, GL_RGBA32F = 0x8814
|
|
40
|
+
const origTexImage2D = gl.texImage2D.bind(gl)
|
|
41
|
+
gl.texImage2D = function (...args) {
|
|
42
|
+
if (args.length >= 8 && args[2] === GL_RGBA && args[7] === GL_FLOAT) {
|
|
43
|
+
args = [...args]
|
|
44
|
+
args[2] = GL_RGBA32F
|
|
45
|
+
}
|
|
46
|
+
return origTexImage2D(...args)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.regl = reglInit({
|
|
50
|
+
gl,
|
|
51
|
+
extensions: ['OES_texture_float', 'EXT_color_buffer_float', 'ANGLE_instanced_arrays'],
|
|
52
|
+
optionalExtensions: ['OES_texture_float_linear'],
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns a stable Axis instance for the given axis name, creating one on first access.
|
|
58
|
+
* The same instance is returned across update() calls so links survive updates.
|
|
59
|
+
*
|
|
60
|
+
* Usage: plot.axes.xaxis_bottom, pipeline.axes["velocity_ms"], etc.
|
|
61
|
+
*/
|
|
62
|
+
get axes() {
|
|
63
|
+
if (!this._axesProxy) {
|
|
64
|
+
this._axesProxy = new Proxy(this._axisCache, {
|
|
65
|
+
get: (cache, name) => {
|
|
66
|
+
if (typeof name !== 'string') return undefined
|
|
67
|
+
if (!cache.has(name)) cache.set(name, new Axis(this, name))
|
|
68
|
+
return cache.get(name)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
return this._axesProxy
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_getAxis(name) {
|
|
76
|
+
if (!this._axisCache.has(name)) this._axisCache.set(name, new Axis(this, name))
|
|
77
|
+
return this._axisCache.get(name)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// For filter axes the axis ID is the quantity kind. Overridden by Plot for spatial axes.
|
|
81
|
+
getAxisQuantityKind(axisId) {
|
|
82
|
+
return axisId
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Default: filter axes only. Overridden by Plot to add spatial + color axes.
|
|
86
|
+
getAxisDomain(axisId) {
|
|
87
|
+
const filterRange = this.filterAxisRegistry?.getRange(axisId)
|
|
88
|
+
if (filterRange) return [filterRange.min, filterRange.max]
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Default: filter axes only. Overridden by Plot to add spatial + color axes.
|
|
93
|
+
setAxisDomain(axisId, domain) {
|
|
94
|
+
if (this.filterAxisRegistry?.hasAxis(axisId)) {
|
|
95
|
+
this.filterAxisRegistry.setRange(axisId, domain[0], domain[1])
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// No-op in base class. Overridden by Plot to schedule a WebGL render frame.
|
|
100
|
+
scheduleRender() {}
|
|
101
|
+
|
|
102
|
+
async _processTransforms(transforms, epoch) {
|
|
103
|
+
if (!transforms || transforms.length === 0) return
|
|
104
|
+
|
|
105
|
+
const TDR_STEP_MS = 500
|
|
106
|
+
for (const { name, transform: spec } of transforms) {
|
|
107
|
+
const entries = Object.entries(spec)
|
|
108
|
+
if (entries.length !== 1) throw new Error(`Transform '${name}' must have exactly one key`)
|
|
109
|
+
const [className, params] = entries[0]
|
|
110
|
+
|
|
111
|
+
const computedData = getComputedData(className)
|
|
112
|
+
if (!computedData) throw new Error(`Unknown computed data type: '${className}'`)
|
|
113
|
+
|
|
114
|
+
const filterAxes = computedData.filterAxes(params, this.currentData)
|
|
115
|
+
for (const quantityKind of Object.values(filterAxes)) {
|
|
116
|
+
this.filterAxisRegistry.ensureFilterAxis(quantityKind)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const node = new ComputedDataNode(computedData, params)
|
|
120
|
+
const stepStart = performance.now()
|
|
121
|
+
try {
|
|
122
|
+
await node._initialize(this.regl, this.currentData, this)
|
|
123
|
+
} catch (e) {
|
|
124
|
+
throw new Error(`Transform '${name}' (${className}) failed to initialize: ${e.message}`, { cause: e })
|
|
125
|
+
}
|
|
126
|
+
if (performance.now() - stepStart > TDR_STEP_MS)
|
|
127
|
+
await new Promise(r => requestAnimationFrame(r))
|
|
128
|
+
if (this._initEpoch !== epoch) return
|
|
129
|
+
|
|
130
|
+
const filterDataExtents = node._meta?.filterDataExtents ?? {}
|
|
131
|
+
for (const [qk, extent] of Object.entries(filterDataExtents)) {
|
|
132
|
+
if (this.filterAxisRegistry.hasAxis(qk)) {
|
|
133
|
+
this.filterAxisRegistry.setDataExtent(qk, extent[0], extent[1])
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.currentData._children[name] = node
|
|
138
|
+
this._dataTransformNodes.push(node)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
package/src/core/Layer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export class Layer {
|
|
2
|
-
constructor({ type, attributes, uniforms,
|
|
2
|
+
constructor({ type, attributes, uniforms, domains = {}, lineWidth = 1, primitive = "points", xAxis = "xaxis_bottom", yAxis = "yaxis_left", zAxis = null, xAxisQuantityKind, yAxisQuantityKind, zAxisQuantityKind, colorAxes = {}, colorAxes2d = {}, filterAxes = {}, vertexCount = null, instanceCount = null, attributeDivisors = {}, blend = null }) {
|
|
3
3
|
// Validate that all attributes are non-null/undefined
|
|
4
4
|
// (Float32Array, regl textures, numbers, and expression objects are all valid)
|
|
5
5
|
for (const [key, value] of Object.entries(attributes)) {
|
|
@@ -8,34 +8,48 @@ export class Layer {
|
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
// Validate colorAxes: must be
|
|
12
|
-
for (const quantityKind of colorAxes) {
|
|
11
|
+
// Validate colorAxes: must be a dict mapping GLSL name suffix to quantity kind string
|
|
12
|
+
for (const quantityKind of Object.values(colorAxes)) {
|
|
13
13
|
if (typeof quantityKind !== 'string') {
|
|
14
14
|
throw new Error(`Color axis quantity kind must be a string, got ${typeof quantityKind}`)
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// Validate filterAxes: must be
|
|
19
|
-
for (const quantityKind of filterAxes) {
|
|
18
|
+
// Validate filterAxes: must be a dict mapping GLSL name suffix to quantity kind string
|
|
19
|
+
for (const quantityKind of Object.values(filterAxes)) {
|
|
20
20
|
if (typeof quantityKind !== 'string') {
|
|
21
21
|
throw new Error(`Filter axis quantity kind must be a string, got ${typeof quantityKind}`)
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
if (!type?.suppressWarnings) {
|
|
26
|
+
if (vertexCount !== null && vertexCount === 0) {
|
|
27
|
+
console.warn(`[gladly] Layer '${type?.name ?? 'unknown'}': vertexCount is 0 — this layer will draw nothing`)
|
|
28
|
+
}
|
|
29
|
+
if (instanceCount !== null && instanceCount === 0) {
|
|
30
|
+
console.warn(`[gladly] Layer '${type?.name ?? 'unknown'}': instanceCount is 0 — this layer will draw nothing`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
this.type = type
|
|
26
35
|
this.attributes = attributes
|
|
27
36
|
this.uniforms = uniforms
|
|
28
|
-
this.nameMap = nameMap
|
|
29
37
|
this.domains = domains
|
|
30
38
|
this.lineWidth = lineWidth
|
|
31
39
|
this.primitive = primitive
|
|
32
40
|
this.xAxis = xAxis
|
|
33
41
|
this.yAxis = yAxis
|
|
42
|
+
this.zAxis = zAxis
|
|
34
43
|
this.xAxisQuantityKind = xAxisQuantityKind
|
|
35
44
|
this.yAxisQuantityKind = yAxisQuantityKind
|
|
36
|
-
|
|
45
|
+
this.zAxisQuantityKind = zAxisQuantityKind
|
|
46
|
+
// colorAxes: Record<suffix, qk> — maps GLSL name suffix to quantity kind for each color axis
|
|
47
|
+
// e.g. { '': 'temperature_K' } or { '': 'temp_K', '2': 'pressure_Pa' }
|
|
37
48
|
this.colorAxes = colorAxes
|
|
38
|
-
//
|
|
49
|
+
// colorAxes2d: Record<suffix2d, [suffix1, suffix2]> — maps a 2D function name suffix to a pair
|
|
50
|
+
// of colorAxes suffixes; generates map_color_2d_SUFFIX(vec2) GLSL wrapper
|
|
51
|
+
this.colorAxes2d = colorAxes2d
|
|
52
|
+
// filterAxes: Record<suffix, qk> — maps GLSL name suffix to quantity kind for each filter axis
|
|
39
53
|
this.filterAxes = filterAxes
|
|
40
54
|
this.vertexCount = vertexCount
|
|
41
55
|
this.instanceCount = instanceCount
|