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.
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 +253 -92
  26. package/src/core/Plot.js +644 -162
  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
@@ -1,142 +1,98 @@
1
1
  import { LayerType } from "../core/LayerType.js"
2
- import { AXES } from "../axes/AxisRegistry.js"
3
- import { Data } from "../core/Data.js"
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 { xData, yData, vData, vData2, fData, xAxis, yAxis } = parameters
9
- const colorAxisQuantityKinds = [d.getQuantityKind(vData) ?? vData]
10
- if (vData2) {
11
- colorAxisQuantityKinds.push(d.getQuantityKind(vData2) ?? vData2)
12
- }
13
- const filterAxisQuantityKinds = fData ? [d.getQuantityKind(fData) ?? fData] : []
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: d.getQuantityKind(xData) ?? xData,
34
+ xAxisQuantityKind: resolveQuantityKind(xData, d) ?? undefined,
17
35
  yAxis,
18
- yAxisQuantityKind: d.getQuantityKind(yData) ?? yData,
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(dataProperties) {
45
+ _commonSchemaProperties(data) {
25
46
  return {
26
- xData: {
27
- type: "string",
28
- enum: dataProperties,
29
- description: "Property name in data object for x coordinates"
30
- },
31
- yData: {
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: AXES.filter(a => a.includes("x")),
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: AXES.filter(a => a.includes("y")),
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
- alphaBlend: {
64
- type: "boolean",
65
- default: false,
66
- description: "Map the normalized color value to alpha so low values fade to transparent"
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
- _resolveColorData(parameters, d) {
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
- const xDomain = d.getDomain(xData)
102
- if (xDomain) domains[xQK] = xDomain
103
-
104
- const yDomain = d.getDomain(yData)
105
- if (yDomain) domains[yQK] = yDomain
106
-
107
- if (vData) {
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
  }
@@ -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
- attribute vec2 position;
416
- attribute vec2 uv;
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
- varying vec2 vUv;
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
- float nx = normalize_axis(position.x, xDomain, xScaleType);
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
- varying vec2 vUv;
445
+ in vec2 vUv;
446
+ out vec4 fragColor;
443
447
 
444
448
  void main() {
445
- vec4 color = texture2D(tileTexture, vUv);
446
- gl_FragColor = vec4(color.rgb, color.a * opacity);
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: {},
@@ -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)