gladly-plot 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +9 -2
  2. package/package.json +10 -11
  3. package/src/axes/Axis.js +320 -172
  4. package/src/axes/AxisLink.js +6 -2
  5. package/src/axes/AxisRegistry.js +116 -39
  6. package/src/axes/Camera.js +47 -0
  7. package/src/axes/ColorAxisRegistry.js +10 -2
  8. package/src/axes/FilterAxisRegistry.js +1 -1
  9. package/src/axes/TickLabelAtlas.js +99 -0
  10. package/src/axes/ZoomController.js +446 -124
  11. package/src/colorscales/ColorscaleRegistry.js +30 -10
  12. package/src/compute/ComputationRegistry.js +126 -184
  13. package/src/compute/axisFilter.js +21 -9
  14. package/src/compute/conv.js +64 -8
  15. package/src/compute/elementwise.js +72 -0
  16. package/src/compute/fft.js +106 -20
  17. package/src/compute/filter.js +105 -103
  18. package/src/compute/hist.js +247 -142
  19. package/src/compute/kde.js +64 -46
  20. package/src/compute/scatter2dInterpolate.js +277 -0
  21. package/src/compute/util.js +196 -0
  22. package/src/core/ComputePipeline.js +153 -0
  23. package/src/core/GlBase.js +141 -0
  24. package/src/core/Layer.js +22 -8
  25. package/src/core/LayerType.js +251 -92
  26. package/src/core/Plot.js +630 -152
  27. package/src/core/PlotGroup.js +204 -0
  28. package/src/core/ShaderQueue.js +73 -0
  29. package/src/data/ColumnData.js +269 -0
  30. package/src/data/Computation.js +95 -0
  31. package/src/data/Data.js +270 -0
  32. package/src/floats/Float.js +56 -0
  33. package/src/index.js +16 -4
  34. package/src/layers/BarsLayer.js +168 -0
  35. package/src/layers/ColorbarLayer.js +10 -14
  36. package/src/layers/ColorbarLayer2d.js +13 -24
  37. package/src/layers/FilterbarLayer.js +4 -3
  38. package/src/layers/LinesLayer.js +108 -122
  39. package/src/layers/PointsLayer.js +73 -69
  40. package/src/layers/ScatterShared.js +62 -106
  41. package/src/layers/TileLayer.js +20 -16
  42. package/src/math/mat4.js +100 -0
  43. package/src/core/Data.js +0 -67
  44. 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, nameMap = {}, domains = {}, lineWidth = 1, primitive = "points", xAxis = "xaxis_bottom", yAxis = "yaxis_left", xAxisQuantityKind, yAxisQuantityKind, colorAxes = [], filterAxes = [], vertexCount = null, instanceCount = null, attributeDivisors = {}, blend = null }) {
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 an array of quantity kind strings
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 an array of quantity kind strings
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
- // colorAxes: string[] — quantity kinds of color axes; attribute named by quantityKind holds the data
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
- // filterAxes: string[] — quantity kinds of filter axes; attribute named by quantityKind holds the data
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