gladly-plot 0.0.4 → 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 (60) hide show
  1. package/README.md +9 -2
  2. package/package.json +10 -11
  3. package/src/axes/Axis.js +401 -0
  4. package/src/{AxisLink.js → axes/AxisLink.js} +6 -2
  5. package/src/{AxisQuantityKindRegistry.js → axes/AxisQuantityKindRegistry.js} +7 -0
  6. package/src/axes/AxisRegistry.js +179 -0
  7. package/src/axes/Camera.js +47 -0
  8. package/src/axes/ColorAxisRegistry.js +101 -0
  9. package/src/{FilterAxisRegistry.js → axes/FilterAxisRegistry.js} +63 -0
  10. package/src/axes/TickLabelAtlas.js +99 -0
  11. package/src/axes/ZoomController.js +463 -0
  12. package/src/colorscales/BivariateColorscales.js +205 -0
  13. package/src/colorscales/ColorscaleRegistry.js +144 -0
  14. package/src/compute/ComputationRegistry.js +179 -0
  15. package/src/compute/axisFilter.js +59 -0
  16. package/src/compute/conv.js +286 -0
  17. package/src/compute/elementwise.js +72 -0
  18. package/src/compute/fft.js +378 -0
  19. package/src/compute/filter.js +229 -0
  20. package/src/compute/hist.js +285 -0
  21. package/src/compute/kde.js +120 -0
  22. package/src/compute/scatter2dInterpolate.js +277 -0
  23. package/src/compute/util.js +196 -0
  24. package/src/core/ComputePipeline.js +153 -0
  25. package/src/core/GlBase.js +141 -0
  26. package/src/core/Layer.js +59 -0
  27. package/src/core/LayerType.js +433 -0
  28. package/src/core/Plot.js +1213 -0
  29. package/src/core/PlotGroup.js +204 -0
  30. package/src/core/ShaderQueue.js +73 -0
  31. package/src/data/ColumnData.js +269 -0
  32. package/src/data/Computation.js +95 -0
  33. package/src/data/Data.js +270 -0
  34. package/src/{Colorbar.js → floats/Colorbar.js} +19 -5
  35. package/src/floats/Colorbar2d.js +77 -0
  36. package/src/{Filterbar.js → floats/Filterbar.js} +18 -4
  37. package/src/{FilterbarFloat.js → floats/Float.js} +73 -30
  38. package/src/{EpsgUtils.js → geo/EpsgUtils.js} +1 -1
  39. package/src/index.js +47 -22
  40. package/src/layers/BarsLayer.js +168 -0
  41. package/src/{ColorbarLayer.js → layers/ColorbarLayer.js} +12 -16
  42. package/src/layers/ColorbarLayer2d.js +86 -0
  43. package/src/{FilterbarLayer.js → layers/FilterbarLayer.js} +6 -5
  44. package/src/layers/LinesLayer.js +185 -0
  45. package/src/layers/PointsLayer.js +118 -0
  46. package/src/layers/ScatterShared.js +98 -0
  47. package/src/{TileLayer.js → layers/TileLayer.js} +24 -20
  48. package/src/math/mat4.js +100 -0
  49. package/src/Axis.js +0 -48
  50. package/src/AxisRegistry.js +0 -54
  51. package/src/ColorAxisRegistry.js +0 -49
  52. package/src/ColorscaleRegistry.js +0 -52
  53. package/src/Data.js +0 -67
  54. package/src/Float.js +0 -159
  55. package/src/Layer.js +0 -44
  56. package/src/LayerType.js +0 -209
  57. package/src/Plot.js +0 -1073
  58. package/src/ScatterLayer.js +0 -287
  59. /package/src/{MatplotlibColorscales.js → colorscales/MatplotlibColorscales.js} +0 -0
  60. /package/src/{LayerTypeRegistry.js → core/LayerTypeRegistry.js} +0 -0
@@ -0,0 +1,47 @@
1
+ import {
2
+ mat4Identity, mat4Multiply, mat4Perspective, mat4LookAt,
3
+ sphericalToCartesian, vec3Normalize, vec3Cross,
4
+ } from '../math/mat4.js'
5
+
6
+ /**
7
+ * Camera manages the MVP matrix for the plot.
8
+ *
9
+ * In 2D mode (is3D=false): getMVP() returns the identity matrix.
10
+ * In 3D mode (is3D=true): orbit camera (y-up) with perspective projection.
11
+ * Mouse interaction is handled by ZoomController.
12
+ */
13
+ export class Camera {
14
+ constructor(is3D) {
15
+ this._is3D = is3D
16
+ this._theta = Math.PI / 4 // azimuth (rotation around y-axis)
17
+ this._phi = Math.PI / 6 // elevation (clamped away from poles)
18
+ this._radius = 3.0
19
+ this._fov = Math.PI / 4 // vertical field-of-view (45°)
20
+ this._aspect = 1.0 // width / height, updated by resize()
21
+ }
22
+
23
+ resize(width, height) {
24
+ this._aspect = width / height
25
+ }
26
+
27
+ // Returns the camera MVP matrix as a column-major Float32Array[16].
28
+ // In 2D mode this is the identity (data coordinates already map to NDC via domain uniforms).
29
+ getMVP() {
30
+ if (!this._is3D) return mat4Identity()
31
+ const eye = sphericalToCartesian(this._theta, this._phi, this._radius)
32
+ const view = mat4LookAt(eye, [0, 0, 0], [0, 1, 0])
33
+ const proj = mat4Perspective(this._fov, this._aspect, 0.1, 100)
34
+ return mat4Multiply(proj, view)
35
+ }
36
+
37
+ // Returns the camera right and up unit vectors in world space.
38
+ // Used to orient billboard quads so they always face the camera.
39
+ getCameraVectors() {
40
+ if (!this._is3D) return { right: [1, 0, 0], up: [0, 1, 0] }
41
+ const eye = sphericalToCartesian(this._theta, this._phi, this._radius)
42
+ const fwd = vec3Normalize([-eye[0], -eye[1], -eye[2]])
43
+ const right = vec3Normalize(vec3Cross(fwd, [0, 1, 0]))
44
+ const up = vec3Normalize(vec3Cross(right, fwd))
45
+ return { right, up }
46
+ }
47
+ }
@@ -0,0 +1,101 @@
1
+ import { getAxisQuantityKind, getScaleTypeFloat } from './AxisQuantityKindRegistry.js'
2
+ import { getColorscaleIndex } from '../colorscales/ColorscaleRegistry.js'
3
+
4
+ export class ColorAxisRegistry {
5
+ constructor() {
6
+ this._axes = new Map()
7
+ }
8
+
9
+ ensureColorAxis(quantityKind, colorscaleOverride = null) {
10
+ if (!this._axes.has(quantityKind)) {
11
+ this._axes.set(quantityKind, { colorscaleOverride, range: null, alphaBlend: 0.0 })
12
+ } else if (colorscaleOverride !== null) {
13
+ this._axes.get(quantityKind).colorscaleOverride = colorscaleOverride
14
+ }
15
+ }
16
+
17
+ getAlphaBlend(quantityKind) {
18
+ return this._axes.get(quantityKind)?.alphaBlend ?? 0.0
19
+ }
20
+
21
+ setRange(quantityKind, min, max) {
22
+ if (!this._axes.has(quantityKind)) {
23
+ throw new Error(`Color axis '${quantityKind}' not found in registry`)
24
+ }
25
+ this._axes.get(quantityKind).range = [min, max]
26
+ }
27
+
28
+ getRange(quantityKind) {
29
+ return this._axes.get(quantityKind)?.range ?? null
30
+ }
31
+
32
+ getColorscale(quantityKind) {
33
+ const entry = this._axes.get(quantityKind)
34
+ if (!entry) return null
35
+ if (entry.colorscaleOverride) return entry.colorscaleOverride
36
+ const unitDef = getAxisQuantityKind(quantityKind)
37
+ return unitDef.colorscale ?? null
38
+ }
39
+
40
+ getColorscaleIndex(quantityKind) {
41
+ const colorscale = this.getColorscale(quantityKind)
42
+ if (colorscale === null) return 0
43
+ return getColorscaleIndex(colorscale)
44
+ }
45
+
46
+ hasAxis(quantityKind) {
47
+ return this._axes.has(quantityKind)
48
+ }
49
+
50
+ getQuantityKinds() {
51
+ return Array.from(this._axes.keys())
52
+ }
53
+
54
+ applyAutoDomainsFromLayers(layers, axesOverrides) {
55
+ for (const quantityKind of this.getQuantityKinds()) {
56
+ const override = axesOverrides[quantityKind]
57
+ if (override?.colorscale && override?.colorscale != "none")
58
+ this.ensureColorAxis(quantityKind, override.colorscale)
59
+ if (override?.alpha_blend !== undefined)
60
+ this._axes.get(quantityKind).alphaBlend = override.alpha_blend
61
+
62
+ let min = Infinity, max = -Infinity
63
+
64
+ for (const layer of layers) {
65
+ for (const qk of Object.values(layer.colorAxes)) {
66
+ if (qk !== quantityKind) continue
67
+ if (layer.domains[qk] !== undefined) {
68
+ const [dMin, dMax] = layer.domains[qk]
69
+ if (dMin < min) min = dMin
70
+ if (dMax > max) max = dMax
71
+ } else {
72
+ const data = layer.attributes[qk]
73
+ if (!data) continue
74
+ for (let i = 0; i < data.length; i++) {
75
+ if (data[i] < min) min = data[i]
76
+ if (data[i] > max) max = data[i]
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ if (min !== Infinity) {
83
+ this.setRange(quantityKind, override?.min ?? min, override?.max ?? max)
84
+ } else if (override?.min !== undefined && override?.max !== undefined) {
85
+ this.setRange(quantityKind, override.min, override.max)
86
+ }
87
+ }
88
+
89
+ for (const quantityKind of this.getQuantityKinds()) {
90
+ if (getScaleTypeFloat(quantityKind, axesOverrides) <= 0.5) continue
91
+ const range = this.getRange(quantityKind)
92
+ if (!range) continue
93
+ if (range[0] <= 0 || range[1] <= 0) {
94
+ throw new Error(
95
+ `Color axis '${quantityKind}' uses log scale but has non-positive range [${range[0]}, ${range[1]}]. ` +
96
+ `All data values and min/max must be > 0 for log scale.`
97
+ )
98
+ }
99
+ }
100
+ }
101
+ }
@@ -1,3 +1,5 @@
1
+ import { getScaleTypeFloat } from './AxisQuantityKindRegistry.js'
2
+
1
3
  export class FilterAxisRegistry {
2
4
  constructor() {
3
5
  // quantityKind -> { min: number|null, max: number|null, dataExtent: [number,number]|null }
@@ -57,6 +59,67 @@ export class FilterAxisRegistry {
57
59
  getQuantityKinds() {
58
60
  return Array.from(this._axes.keys())
59
61
  }
62
+
63
+ applyAutoDomainsFromLayers(layers, axesOverrides) {
64
+ for (const quantityKind of this.getQuantityKinds()) {
65
+ let extMin = Infinity, extMax = -Infinity
66
+
67
+ for (const layer of layers) {
68
+ for (const qk of Object.values(layer.filterAxes)) {
69
+ if (qk !== quantityKind) continue
70
+ if (layer.domains[qk] !== undefined) {
71
+ const [dMin, dMax] = layer.domains[qk]
72
+ if (dMin < extMin) extMin = dMin
73
+ if (dMax > extMax) extMax = dMax
74
+ } else {
75
+ const data = layer.attributes[qk]
76
+ if (!data) continue
77
+ for (let i = 0; i < data.length; i++) {
78
+ if (data[i] < extMin) extMin = data[i]
79
+ if (data[i] > extMax) extMax = data[i]
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ if (extMin !== Infinity) this.setDataExtent(quantityKind, extMin, extMax)
86
+
87
+ if (axesOverrides[quantityKind]) {
88
+ const override = axesOverrides[quantityKind]
89
+ this.setRange(
90
+ quantityKind,
91
+ override.min !== undefined ? override.min : null,
92
+ override.max !== undefined ? override.max : null
93
+ )
94
+ }
95
+ }
96
+
97
+ for (const quantityKind of this.getQuantityKinds()) {
98
+ if (getScaleTypeFloat(quantityKind, axesOverrides) <= 0.5) continue
99
+ const extent = this.getDataExtent(quantityKind)
100
+ if (extent && extent[0] <= 0) {
101
+ throw new Error(
102
+ `Filter axis '${quantityKind}' uses log scale but data minimum is ${extent[0]}. ` +
103
+ `All data values must be > 0 for log scale.`
104
+ )
105
+ }
106
+ const filterRange = this.getRange(quantityKind)
107
+ if (filterRange) {
108
+ if (filterRange.min !== null && filterRange.min <= 0) {
109
+ throw new Error(
110
+ `Filter axis '${quantityKind}' uses log scale but min is ${filterRange.min}. ` +
111
+ `min must be > 0 for log scale.`
112
+ )
113
+ }
114
+ if (filterRange.max !== null && filterRange.max <= 0) {
115
+ throw new Error(
116
+ `Filter axis '${quantityKind}' uses log scale but max is ${filterRange.max}. ` +
117
+ `max must be > 0 for log scale.`
118
+ )
119
+ }
120
+ }
121
+ }
122
+ }
60
123
  }
61
124
 
62
125
  // Injects a GLSL helper used by layer shaders to apply filter axis bounds.
@@ -0,0 +1,99 @@
1
+ // Canvas-backed texture atlas for tick and axis title labels.
2
+ // Labels are rendered with Canvas 2D and packed into a single GPU texture (shelf packing).
3
+ // UV coordinates use the convention: v = canvas_y / ATLAS_SIZE (v=0 = canvas top).
4
+ // The atlas texture is uploaded with flipY: false, so texture(sampler, (u,v)) samples
5
+ // canvas pixel (u*W, v*H). Billboard quads use matching UV assignments.
6
+
7
+ const ATLAS_SIZE = 1024
8
+ const FONT = '12px sans-serif'
9
+ const PADDING = 2
10
+ const ROW_HEIGHT = 16 // fixed text-line height in pixels
11
+
12
+ export class TickLabelAtlas {
13
+ constructor(regl) {
14
+ this._regl = regl
15
+ this._canvas = null
16
+ this._ctx = null
17
+ this._texture = null
18
+ this._entries = new Map() // text → { u, v, uw, vh, pw, ph } | null (pending)
19
+ this._needsRebuild = false
20
+ }
21
+
22
+ // Mark a set of label strings as needed. Call before flush().
23
+ markLabels(labels) {
24
+ for (const l of labels) {
25
+ if (!this._entries.has(l)) {
26
+ this._entries.set(l, null)
27
+ this._needsRebuild = true
28
+ }
29
+ }
30
+ }
31
+
32
+ // Re-render the atlas canvas and re-upload the GPU texture if anything changed.
33
+ flush() {
34
+ if (!this._needsRebuild) return
35
+ this._needsRebuild = false
36
+
37
+ if (!this._canvas) {
38
+ this._canvas = document.createElement('canvas')
39
+ this._canvas.width = ATLAS_SIZE
40
+ this._canvas.height = ATLAS_SIZE
41
+ this._ctx = this._canvas.getContext('2d')
42
+ }
43
+
44
+ const ctx = this._ctx
45
+ ctx.clearRect(0, 0, ATLAS_SIZE, ATLAS_SIZE)
46
+ ctx.font = FONT
47
+ ctx.fillStyle = '#000'
48
+ ctx.textBaseline = 'top'
49
+
50
+ const rowH = ROW_HEIGHT + PADDING * 2
51
+ let x = 0, y = 0
52
+
53
+ for (const label of this._entries.keys()) {
54
+ const metrics = ctx.measureText(label)
55
+ const pw = Math.ceil(metrics.width) + PADDING * 2
56
+ const ph = rowH
57
+
58
+ if (x + pw > ATLAS_SIZE) { x = 0; y += rowH }
59
+ if (y + ph > ATLAS_SIZE) {
60
+ console.warn('[gladly] TickLabelAtlas: atlas is full; some labels may be missing')
61
+ break
62
+ }
63
+
64
+ ctx.fillText(label, x + PADDING, y + PADDING)
65
+
66
+ this._entries.set(label, {
67
+ u: x / ATLAS_SIZE,
68
+ v: y / ATLAS_SIZE,
69
+ uw: pw / ATLAS_SIZE,
70
+ vh: ph / ATLAS_SIZE,
71
+ pw,
72
+ ph,
73
+ })
74
+ x += pw
75
+ }
76
+
77
+ // flipY: false — v = canvas_y / ATLAS_SIZE; v=0 samples canvas top.
78
+ if (!this._texture) {
79
+ this._texture = this._regl.texture({
80
+ width: ATLAS_SIZE,
81
+ height: ATLAS_SIZE,
82
+ format: 'rgba',
83
+ mag: 'nearest',
84
+ min: 'nearest',
85
+ flipY: false,
86
+ })
87
+ }
88
+ this._texture.subimage({ data: this._canvas, x: 0, y: 0, flipY: false })
89
+ }
90
+
91
+ // Returns the atlas entry for a label, or null if it hasn't been built yet.
92
+ getEntry(label) { return this._entries.get(label) ?? null }
93
+
94
+ get texture() { return this._texture }
95
+
96
+ destroy() {
97
+ if (this._texture) { this._texture.destroy(); this._texture = null }
98
+ }
99
+ }