gladly-plot 0.0.2 → 0.0.4
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 +1 -0
- package/package.json +15 -7
- package/src/ColorbarLayer.js +1 -1
- package/src/ColorscaleRegistry.js +5 -2
- package/src/FilterbarLayer.js +1 -1
- package/src/LayerType.js +58 -21
- package/src/Plot.js +104 -7
- package/src/ScatterLayer.js +205 -52
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ Gladly combines WebGL rendering (via regl) with D3.js for interactive axes and z
|
|
|
17
17
|
- 🎨 Supports all standard colorscales
|
|
18
18
|
- 🔗 Subplot axis linking
|
|
19
19
|
- 🌈 Axis to coloring or filtering linking
|
|
20
|
+
- 🌎 Basemap layer with XYZ,WMS and WMTS support and CRS reprojection
|
|
20
21
|
|
|
21
22
|
## Documentation
|
|
22
23
|
|
package/package.json
CHANGED
|
@@ -1,30 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gladly-plot",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "GPU-powered multi-axis plotting library with regl + d3",
|
|
5
5
|
"type": "module",
|
|
6
|
-
|
|
7
6
|
"exports": {
|
|
8
7
|
".": "./src/index.js"
|
|
9
8
|
},
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
13
12
|
"scripts": {
|
|
13
|
+
"prepare": "node scripts/fix-numjs-wasm.js",
|
|
14
14
|
"dev": "parcel serve example/index.html --open",
|
|
15
15
|
"build:example": "parcel build example/index.html --dist-dir dist-example --public-url ./",
|
|
16
16
|
"preview": "npm run build:example && npx serve dist-example"
|
|
17
17
|
},
|
|
18
|
-
|
|
19
18
|
"dependencies": {
|
|
20
19
|
"d3": "^7.8.5",
|
|
21
20
|
"proj4": "^2.15.0",
|
|
22
21
|
"projnames": "^0.0.2",
|
|
23
22
|
"regl": "^2.1.0"
|
|
24
23
|
},
|
|
24
|
+
"browserslist": [
|
|
25
|
+
"last 2 Chrome versions",
|
|
26
|
+
"last 2 Edge versions",
|
|
27
|
+
"last 2 Firefox versions",
|
|
28
|
+
"last 2 Safari versions"
|
|
29
|
+
],
|
|
25
30
|
|
|
26
31
|
"devDependencies": {
|
|
32
|
+
"@jayce789/numjs": "^2.2.6",
|
|
33
|
+
"@json-editor/json-editor": "^2.15.1",
|
|
27
34
|
"parcel": "^2.9.0",
|
|
28
|
-
"
|
|
35
|
+
"process": "^0.11.10",
|
|
36
|
+
"url": "^0.11.4"
|
|
29
37
|
}
|
|
30
38
|
}
|
package/src/ColorbarLayer.js
CHANGED
|
@@ -41,7 +41,7 @@ export const colorbarLayerType = new LayerType({
|
|
|
41
41
|
float r0 = color_scale_type > 0.5 ? log(color_range.x) : color_range.x;
|
|
42
42
|
float r1 = color_scale_type > 0.5 ? log(color_range.y) : color_range.y;
|
|
43
43
|
float v = r0 + tval * (r1 - r0);
|
|
44
|
-
gl_FragColor = map_color(colorscale, vec2(r0, r1), v);
|
|
44
|
+
gl_FragColor = gladly_apply_color(map_color(colorscale, vec2(r0, r1), v));
|
|
45
45
|
}
|
|
46
46
|
`,
|
|
47
47
|
|
|
@@ -38,11 +38,14 @@ export function buildColorGlsl() {
|
|
|
38
38
|
parts.push(' return vec4(0.5, 0.5, 0.5, 1.0);')
|
|
39
39
|
parts.push('}')
|
|
40
40
|
|
|
41
|
-
parts.push('vec4 map_color_s(int cs, vec2 range, float v, float scaleType) {')
|
|
41
|
+
parts.push('vec4 map_color_s(int cs, vec2 range, float v, float scaleType, float useAlpha) {')
|
|
42
42
|
parts.push(' float vt = scaleType > 0.5 ? log(v) : v;')
|
|
43
43
|
parts.push(' float r0 = scaleType > 0.5 ? log(range.x) : range.x;')
|
|
44
44
|
parts.push(' float r1 = scaleType > 0.5 ? log(range.y) : range.y;')
|
|
45
|
-
parts.push('
|
|
45
|
+
parts.push(' float t = clamp((vt - r0) / (r1 - r0), 0.0, 1.0);')
|
|
46
|
+
parts.push(' vec4 color = map_color(cs, vec2(r0, r1), vt);')
|
|
47
|
+
parts.push(' if (useAlpha > 0.5) color.a = t;')
|
|
48
|
+
parts.push(' return gladly_apply_color(color);')
|
|
46
49
|
parts.push('}')
|
|
47
50
|
|
|
48
51
|
return parts.join('\n')
|
package/src/FilterbarLayer.js
CHANGED
|
@@ -24,7 +24,7 @@ export const filterbarLayerType = new LayerType({
|
|
|
24
24
|
`,
|
|
25
25
|
frag: `
|
|
26
26
|
precision mediump float;
|
|
27
|
-
void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); }
|
|
27
|
+
void main() { gl_FragColor = gladly_apply_color(vec4(0.0, 0.0, 0.0, 0.0)); }
|
|
28
28
|
`,
|
|
29
29
|
|
|
30
30
|
schema: () => ({
|
package/src/LayerType.js
CHANGED
|
@@ -11,6 +11,44 @@ function buildSpatialGlsl() {
|
|
|
11
11
|
}`
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function buildApplyColorGlsl() {
|
|
15
|
+
return `uniform float u_pickingMode;
|
|
16
|
+
uniform float u_pickLayerIndex;
|
|
17
|
+
varying float v_pickId;
|
|
18
|
+
vec4 gladly_apply_color(vec4 color) {
|
|
19
|
+
if (u_pickingMode > 0.5) {
|
|
20
|
+
float layerIdx = u_pickLayerIndex + 1.0;
|
|
21
|
+
float dataIdx = floor(v_pickId + 0.5);
|
|
22
|
+
return vec4(
|
|
23
|
+
layerIdx / 255.0,
|
|
24
|
+
floor(dataIdx / 65536.0) / 255.0,
|
|
25
|
+
floor(mod(dataIdx, 65536.0) / 256.0) / 255.0,
|
|
26
|
+
mod(dataIdx, 256.0) / 255.0
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return color;
|
|
30
|
+
}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function injectPickIdAssignment(src) {
|
|
34
|
+
const lastBrace = src.lastIndexOf('}')
|
|
35
|
+
if (lastBrace === -1) return src
|
|
36
|
+
return src.slice(0, lastBrace) + ' v_pickId = a_pickId;\n}'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function injectInto(src, helpers) {
|
|
40
|
+
const injected = helpers.filter(Boolean).join('\n')
|
|
41
|
+
if (!injected) return src
|
|
42
|
+
const versionRe = /^[ \t]*#version[^\n]*\n?/
|
|
43
|
+
const versionMatch = src.match(versionRe)
|
|
44
|
+
const version = versionMatch ? versionMatch[0] : ''
|
|
45
|
+
const rest = version ? src.slice(version.length) : src
|
|
46
|
+
const precisionRe = /^\s*precision\s+\S+\s+\S+\s*;\s*$/mg
|
|
47
|
+
const precisions = rest.match(precisionRe) ?? []
|
|
48
|
+
const body = rest.replace(precisionRe, '')
|
|
49
|
+
return version + precisions.join('\n') + '\n' + injected + '\n' + body
|
|
50
|
+
}
|
|
51
|
+
|
|
14
52
|
export class LayerType {
|
|
15
53
|
constructor({
|
|
16
54
|
name,
|
|
@@ -48,19 +86,29 @@ export class LayerType {
|
|
|
48
86
|
// Build a single-entry uniform object with renamed key reading from the internal prop name.
|
|
49
87
|
const u = (internalName) => ({ [shaderName(internalName)]: regl.prop(internalName) })
|
|
50
88
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
89
|
+
const isInstanced = layer.instanceCount !== null
|
|
90
|
+
const pickCount = isInstanced ? layer.instanceCount : (layer.vertexCount ?? layer.attributes.x?.length ?? 0)
|
|
91
|
+
const pickIds = new Float32Array(pickCount)
|
|
92
|
+
for (let i = 0; i < pickCount; i++) pickIds[i] = i
|
|
93
|
+
|
|
94
|
+
const attributes = {
|
|
95
|
+
...Object.fromEntries(
|
|
96
|
+
Object.entries(layer.attributes).map(([key, buffer]) => {
|
|
97
|
+
const divisor = layer.attributeDivisors[key]
|
|
98
|
+
const attrObj = divisor !== undefined ? { buffer, divisor } : { buffer }
|
|
99
|
+
return [shaderName(key), attrObj]
|
|
100
|
+
})
|
|
101
|
+
),
|
|
102
|
+
a_pickId: isInstanced ? { buffer: regl.buffer(pickIds), divisor: 1 } : regl.buffer(pickIds)
|
|
103
|
+
}
|
|
58
104
|
|
|
59
105
|
const uniforms = {
|
|
60
106
|
...u("xDomain"),
|
|
61
107
|
...u("yDomain"),
|
|
62
108
|
...u("xScaleType"),
|
|
63
109
|
...u("yScaleType"),
|
|
110
|
+
u_pickingMode: regl.prop('u_pickingMode'),
|
|
111
|
+
u_pickLayerIndex: regl.prop('u_pickLayerIndex'),
|
|
64
112
|
...Object.fromEntries(
|
|
65
113
|
Object.entries(layer.uniforms).map(([key, value]) => [shaderName(key), value])
|
|
66
114
|
)
|
|
@@ -80,22 +128,11 @@ export class LayerType {
|
|
|
80
128
|
const spatialGlsl = buildSpatialGlsl()
|
|
81
129
|
const colorGlsl = layer.colorAxes.length > 0 ? buildColorGlsl() : ''
|
|
82
130
|
const filterGlsl = layer.filterAxes.length > 0 ? buildFilterGlsl() : ''
|
|
83
|
-
const
|
|
84
|
-
const injected = helpers.filter(Boolean).join('\n')
|
|
85
|
-
if (!injected) return src
|
|
86
|
-
const versionRe = /^[ \t]*#version[^\n]*\n?/
|
|
87
|
-
const versionMatch = src.match(versionRe)
|
|
88
|
-
const version = versionMatch ? versionMatch[0] : ''
|
|
89
|
-
const rest = version ? src.slice(version.length) : src
|
|
90
|
-
const precisionRe = /^\s*precision\s+\S+\s+\S+\s*;\s*$/mg
|
|
91
|
-
const precisions = rest.match(precisionRe) ?? []
|
|
92
|
-
const body = rest.replace(precisionRe, '')
|
|
93
|
-
return version + precisions.join('\n') + '\n' + injected + '\n' + body
|
|
94
|
-
}
|
|
131
|
+
const pickVertDecls = `attribute float a_pickId;\nvarying float v_pickId;`
|
|
95
132
|
|
|
96
133
|
const drawConfig = {
|
|
97
|
-
vert: injectInto(this.vert, [spatialGlsl,
|
|
98
|
-
frag: injectInto(this.frag, [colorGlsl, filterGlsl]),
|
|
134
|
+
vert: injectPickIdAssignment(injectInto(this.vert, [spatialGlsl, filterGlsl, pickVertDecls])),
|
|
135
|
+
frag: injectInto(this.frag, [buildApplyColorGlsl(), colorGlsl, filterGlsl]),
|
|
99
136
|
attributes,
|
|
100
137
|
uniforms,
|
|
101
138
|
viewport: regl.prop("viewport"),
|
package/src/Plot.js
CHANGED
|
@@ -90,11 +90,13 @@ export class Plot {
|
|
|
90
90
|
|
|
91
91
|
const width = this.container.clientWidth
|
|
92
92
|
const height = this.container.clientHeight
|
|
93
|
+
const plotWidth = width - this.margin.left - this.margin.right
|
|
94
|
+
const plotHeight = height - this.margin.top - this.margin.bottom
|
|
93
95
|
|
|
94
|
-
// Container is hidden
|
|
96
|
+
// Container is hidden, not yet laid out, or too small to fit the margins.
|
|
95
97
|
// Store config/data and return; ResizeObserver will call forceUpdate() once
|
|
96
98
|
// the container gets real dimensions.
|
|
97
|
-
if (width === 0 || height === 0) {
|
|
99
|
+
if (width === 0 || height === 0 || plotWidth <= 0 || plotHeight <= 0) {
|
|
98
100
|
return
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -104,8 +106,8 @@ export class Plot {
|
|
|
104
106
|
|
|
105
107
|
this.width = width
|
|
106
108
|
this.height = height
|
|
107
|
-
this.plotWidth =
|
|
108
|
-
this.plotHeight =
|
|
109
|
+
this.plotWidth = plotWidth
|
|
110
|
+
this.plotHeight = plotHeight
|
|
109
111
|
|
|
110
112
|
if (this.regl) {
|
|
111
113
|
this.regl.destroy()
|
|
@@ -221,7 +223,10 @@ export class Plot {
|
|
|
221
223
|
_setupResizeObserver() {
|
|
222
224
|
if (typeof ResizeObserver !== 'undefined') {
|
|
223
225
|
this.resizeObserver = new ResizeObserver(() => {
|
|
224
|
-
|
|
226
|
+
// Defer to next animation frame so the ResizeObserver callback exits
|
|
227
|
+
// before any DOM/layout changes happen, avoiding the "loop completed
|
|
228
|
+
// with undelivered notifications" browser error.
|
|
229
|
+
requestAnimationFrame(() => this.forceUpdate())
|
|
225
230
|
})
|
|
226
231
|
this.resizeObserver.observe(this.container)
|
|
227
232
|
} else {
|
|
@@ -355,7 +360,8 @@ export class Plot {
|
|
|
355
360
|
}
|
|
356
361
|
|
|
357
362
|
_processLayers(layersConfig, data) {
|
|
358
|
-
for (
|
|
363
|
+
for (let configLayerIndex = 0; configLayerIndex < layersConfig.length; configLayerIndex++) {
|
|
364
|
+
const layerSpec = layersConfig[configLayerIndex]
|
|
359
365
|
const entries = Object.entries(layerSpec)
|
|
360
366
|
if (entries.length !== 1) {
|
|
361
367
|
throw new Error("Each layer specification must have exactly one layer type key")
|
|
@@ -385,6 +391,7 @@ export class Plot {
|
|
|
385
391
|
|
|
386
392
|
// Create one draw command per GPU config returned by the layer type.
|
|
387
393
|
for (const layer of layerType.createLayer(parameters, data)) {
|
|
394
|
+
layer.configLayerIndex = configLayerIndex
|
|
388
395
|
layer.draw = layer.type.createDrawCommand(this.regl, layer, this)
|
|
389
396
|
this.layers.push(layer)
|
|
390
397
|
}
|
|
@@ -811,7 +818,9 @@ export class Plot {
|
|
|
811
818
|
xScaleType: xIsLog ? 1.0 : 0.0,
|
|
812
819
|
yScaleType: yIsLog ? 1.0 : 0.0,
|
|
813
820
|
viewport: viewport,
|
|
814
|
-
count: layer.vertexCount ?? layer.attributes.x?.length ?? 0
|
|
821
|
+
count: layer.vertexCount ?? layer.attributes.x?.length ?? 0,
|
|
822
|
+
u_pickingMode: 0.0,
|
|
823
|
+
u_pickLayerIndex: 0.0,
|
|
815
824
|
}
|
|
816
825
|
|
|
817
826
|
if (layer.instanceCount !== null) {
|
|
@@ -973,4 +982,92 @@ export class Plot {
|
|
|
973
982
|
|
|
974
983
|
fullOverlay.call(zoomBehavior)
|
|
975
984
|
}
|
|
985
|
+
|
|
986
|
+
lookup(x, y) {
|
|
987
|
+
const result = {}
|
|
988
|
+
if (!this.axisRegistry) return result
|
|
989
|
+
const plotX = x - this.margin.left
|
|
990
|
+
const plotY = y - this.margin.top
|
|
991
|
+
for (const axisId of AXES) {
|
|
992
|
+
const scale = this.axisRegistry.getScale(axisId)
|
|
993
|
+
if (!scale) continue
|
|
994
|
+
const qk = this.axisRegistry.axisQuantityKinds[axisId]
|
|
995
|
+
const value = axisId.includes('y') ? scale.invert(plotY) : scale.invert(plotX)
|
|
996
|
+
result[axisId] = value
|
|
997
|
+
if (qk) result[qk] = value
|
|
998
|
+
}
|
|
999
|
+
return result
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
on(eventType, callback) {
|
|
1003
|
+
const handler = (e) => {
|
|
1004
|
+
if (!this.container.contains(e.target)) return
|
|
1005
|
+
const rect = this.container.getBoundingClientRect()
|
|
1006
|
+
const x = e.clientX - rect.left
|
|
1007
|
+
const y = e.clientY - rect.top
|
|
1008
|
+
callback(e, this.lookup(x, y))
|
|
1009
|
+
}
|
|
1010
|
+
window.addEventListener(eventType, handler, { capture: true })
|
|
1011
|
+
return { remove: () => window.removeEventListener(eventType, handler, { capture: true }) }
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
pick(x, y) {
|
|
1015
|
+
if (!this.regl || !this.layers.length) return null
|
|
1016
|
+
|
|
1017
|
+
const fbo = this.regl.framebuffer({
|
|
1018
|
+
width: this.width, height: this.height,
|
|
1019
|
+
colorFormat: 'rgba', colorType: 'uint8', depth: false,
|
|
1020
|
+
})
|
|
1021
|
+
|
|
1022
|
+
const glX = Math.round(x)
|
|
1023
|
+
const glY = this.height - Math.round(y) - 1
|
|
1024
|
+
|
|
1025
|
+
let result = null
|
|
1026
|
+
this.regl({ framebuffer: fbo })(() => {
|
|
1027
|
+
this.regl.clear({ color: [0, 0, 0, 0] })
|
|
1028
|
+
const viewport = {
|
|
1029
|
+
x: this.margin.left, y: this.margin.bottom,
|
|
1030
|
+
width: this.plotWidth, height: this.plotHeight
|
|
1031
|
+
}
|
|
1032
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
1033
|
+
const layer = this.layers[i]
|
|
1034
|
+
const xIsLog = layer.xAxis ? this.axisRegistry.isLogScale(layer.xAxis) : false
|
|
1035
|
+
const yIsLog = layer.yAxis ? this.axisRegistry.isLogScale(layer.yAxis) : false
|
|
1036
|
+
const props = {
|
|
1037
|
+
xDomain: layer.xAxis ? this.axisRegistry.getScale(layer.xAxis).domain() : [0, 1],
|
|
1038
|
+
yDomain: layer.yAxis ? this.axisRegistry.getScale(layer.yAxis).domain() : [0, 1],
|
|
1039
|
+
xScaleType: xIsLog ? 1.0 : 0.0,
|
|
1040
|
+
yScaleType: yIsLog ? 1.0 : 0.0,
|
|
1041
|
+
viewport,
|
|
1042
|
+
count: layer.vertexCount ?? layer.attributes.x?.length ?? 0,
|
|
1043
|
+
u_pickingMode: 1.0,
|
|
1044
|
+
u_pickLayerIndex: i,
|
|
1045
|
+
}
|
|
1046
|
+
if (layer.instanceCount !== null) props.instances = layer.instanceCount
|
|
1047
|
+
for (const qk of layer.colorAxes) {
|
|
1048
|
+
props[`colorscale_${qk}`] = this.colorAxisRegistry.getColorscaleIndex(qk)
|
|
1049
|
+
const range = this.colorAxisRegistry.getRange(qk)
|
|
1050
|
+
props[`color_range_${qk}`] = range ?? [0, 1]
|
|
1051
|
+
props[`color_scale_type_${qk}`] = this._getScaleTypeFloat(qk)
|
|
1052
|
+
}
|
|
1053
|
+
for (const qk of layer.filterAxes) {
|
|
1054
|
+
props[`filter_range_${qk}`] = this.filterAxisRegistry.getRangeUniform(qk)
|
|
1055
|
+
props[`filter_scale_type_${qk}`] = this._getScaleTypeFloat(qk)
|
|
1056
|
+
}
|
|
1057
|
+
layer.draw(props)
|
|
1058
|
+
}
|
|
1059
|
+
const pixels = this.regl.read({ x: glX, y: glY, width: 1, height: 1 })
|
|
1060
|
+
if (pixels[0] === 0) {
|
|
1061
|
+
result = null
|
|
1062
|
+
} else {
|
|
1063
|
+
const layerIndex = pixels[0] - 1
|
|
1064
|
+
const dataIndex = (pixels[1] << 16) | (pixels[2] << 8) | pixels[3]
|
|
1065
|
+
const layer = this.layers[layerIndex]
|
|
1066
|
+
result = { layerIndex, configLayerIndex: layer.configLayerIndex, dataIndex, layer }
|
|
1067
|
+
}
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
fbo.destroy()
|
|
1071
|
+
return result
|
|
1072
|
+
}
|
|
976
1073
|
}
|
package/src/ScatterLayer.js
CHANGED
|
@@ -3,10 +3,100 @@ import { AXES } from "./AxisRegistry.js"
|
|
|
3
3
|
import { registerLayerType } from "./LayerTypeRegistry.js"
|
|
4
4
|
import { Data } from "./Data.js"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const POINTS_VERT = `
|
|
7
|
+
precision mediump float;
|
|
8
|
+
attribute float x;
|
|
9
|
+
attribute float y;
|
|
10
|
+
attribute float color_data;
|
|
11
|
+
uniform vec2 xDomain;
|
|
12
|
+
uniform vec2 yDomain;
|
|
13
|
+
uniform float xScaleType;
|
|
14
|
+
uniform float yScaleType;
|
|
15
|
+
varying float value;
|
|
16
|
+
void main() {
|
|
17
|
+
float nx = normalize_axis(x, xDomain, xScaleType);
|
|
18
|
+
float ny = normalize_axis(y, yDomain, yScaleType);
|
|
19
|
+
gl_Position = vec4(nx*2.0-1.0, ny*2.0-1.0, 0, 1);
|
|
20
|
+
gl_PointSize = 4.0;
|
|
21
|
+
value = color_data;
|
|
22
|
+
}
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
const POINTS_FRAG = `
|
|
26
|
+
precision mediump float;
|
|
27
|
+
uniform int colorscale;
|
|
28
|
+
uniform vec2 color_range;
|
|
29
|
+
uniform float color_scale_type;
|
|
30
|
+
uniform float alphaBlend;
|
|
31
|
+
varying float value;
|
|
32
|
+
void main() {
|
|
33
|
+
gl_FragColor = map_color_s(colorscale, color_range, value, color_scale_type, alphaBlend);
|
|
34
|
+
}
|
|
35
|
+
`
|
|
36
|
+
|
|
37
|
+
// Lines mode uses instanced rendering:
|
|
38
|
+
// - Template: 2 vertices with a_endPoint in {0.0, 1.0} (divisor=0 → interpolates)
|
|
39
|
+
// - Per-segment: a_x0/x1, a_y0/y1, a_v0/v1, a_seg0/seg1 (divisor=1 → constant per instance)
|
|
40
|
+
//
|
|
41
|
+
// Because a_v0 and a_v1 are instanced, they are the same at both template vertices for a given
|
|
42
|
+
// segment, so varyings set from them are constant across the line (no GPU interpolation).
|
|
43
|
+
// Only v_t (from a_endPoint) interpolates, giving the position along the segment.
|
|
44
|
+
//
|
|
45
|
+
// Segment boundary handling: when a_seg0 != a_seg1, collapse both template vertices to
|
|
46
|
+
// (a_x0, a_y0) producing a zero-length degenerate line that the rasterizer discards.
|
|
47
|
+
|
|
48
|
+
const LINES_VERT = `
|
|
49
|
+
precision mediump float;
|
|
50
|
+
attribute float a_endPoint;
|
|
51
|
+
attribute float a_x0, a_y0;
|
|
52
|
+
attribute float a_x1, a_y1;
|
|
53
|
+
attribute float a_v0, a_v1;
|
|
54
|
+
attribute float a_seg0, a_seg1;
|
|
55
|
+
uniform vec2 xDomain;
|
|
56
|
+
uniform vec2 yDomain;
|
|
57
|
+
uniform float xScaleType;
|
|
58
|
+
uniform float yScaleType;
|
|
59
|
+
varying float v_color_start;
|
|
60
|
+
varying float v_color_end;
|
|
61
|
+
varying float v_t;
|
|
62
|
+
void main() {
|
|
63
|
+
float same_seg = abs(a_seg0 - a_seg1) < 0.5 ? 1.0 : 0.0;
|
|
64
|
+
float t = same_seg * a_endPoint;
|
|
65
|
+
float x = mix(a_x0, a_x1, t);
|
|
66
|
+
float y = mix(a_y0, a_y1, t);
|
|
67
|
+
float nx = normalize_axis(x, xDomain, xScaleType);
|
|
68
|
+
float ny = normalize_axis(y, yDomain, yScaleType);
|
|
69
|
+
gl_Position = vec4(nx * 2.0 - 1.0, ny * 2.0 - 1.0, 0, 1);
|
|
70
|
+
v_color_start = a_v0;
|
|
71
|
+
v_color_end = a_v1;
|
|
72
|
+
v_t = a_endPoint;
|
|
73
|
+
}
|
|
74
|
+
`
|
|
75
|
+
|
|
76
|
+
const LINES_FRAG = `
|
|
77
|
+
precision mediump float;
|
|
78
|
+
uniform int colorscale;
|
|
79
|
+
uniform vec2 color_range;
|
|
80
|
+
uniform float color_scale_type;
|
|
81
|
+
uniform float alphaBlend;
|
|
82
|
+
uniform float u_lineColorMode;
|
|
83
|
+
varying float v_color_start;
|
|
84
|
+
varying float v_color_end;
|
|
85
|
+
varying float v_t;
|
|
86
|
+
void main() {
|
|
87
|
+
float value = u_lineColorMode > 0.5
|
|
88
|
+
? (v_t < 0.5 ? v_color_start : v_color_end)
|
|
89
|
+
: mix(v_color_start, v_color_end, v_t);
|
|
90
|
+
gl_FragColor = map_color_s(colorscale, color_range, value, color_scale_type, alphaBlend);
|
|
91
|
+
}
|
|
92
|
+
`
|
|
93
|
+
|
|
94
|
+
class ScatterLayerType extends LayerType {
|
|
95
|
+
constructor() {
|
|
96
|
+
super({ name: "scatter", vert: POINTS_VERT, frag: POINTS_FRAG })
|
|
97
|
+
}
|
|
8
98
|
|
|
9
|
-
|
|
99
|
+
_getAxisConfig(parameters, data) {
|
|
10
100
|
const d = Data.wrap(data)
|
|
11
101
|
const { xData, yData, vData, xAxis, yAxis } = parameters
|
|
12
102
|
return {
|
|
@@ -16,39 +106,9 @@ export const scatterLayerType = new LayerType({
|
|
|
16
106
|
yAxisQuantityKind: d.getQuantityKind(yData) ?? yData,
|
|
17
107
|
colorAxisQuantityKinds: [d.getQuantityKind(vData) ?? vData],
|
|
18
108
|
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
precision mediump float;
|
|
23
|
-
attribute float x;
|
|
24
|
-
attribute float y;
|
|
25
|
-
attribute float color_data;
|
|
26
|
-
uniform vec2 xDomain;
|
|
27
|
-
uniform vec2 yDomain;
|
|
28
|
-
uniform float xScaleType;
|
|
29
|
-
uniform float yScaleType;
|
|
30
|
-
varying float value;
|
|
31
|
-
void main() {
|
|
32
|
-
float nx = normalize_axis(x, xDomain, xScaleType);
|
|
33
|
-
float ny = normalize_axis(y, yDomain, yScaleType);
|
|
34
|
-
gl_Position = vec4(nx*2.0-1.0, ny*2.0-1.0, 0, 1);
|
|
35
|
-
gl_PointSize = 4.0;
|
|
36
|
-
value = color_data;
|
|
37
|
-
}
|
|
38
|
-
`,
|
|
39
|
-
frag: `
|
|
40
|
-
precision mediump float;
|
|
41
|
-
uniform int colorscale;
|
|
42
|
-
uniform vec2 color_range;
|
|
43
|
-
uniform float color_scale_type;
|
|
44
|
-
varying float value;
|
|
45
|
-
void main() {
|
|
46
|
-
float t = clamp((value - color_range.x) / (color_range.y - color_range.x), 0.0, 1.0);
|
|
47
|
-
vec4 color = map_color_s(colorscale, color_range, value, color_scale_type);
|
|
48
|
-
gl_FragColor = vec4(color.rgb, t);
|
|
49
|
-
}
|
|
50
|
-
`,
|
|
51
|
-
schema: (data) => {
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
schema(data) {
|
|
52
112
|
const dataProperties = Data.wrap(data).columns()
|
|
53
113
|
return {
|
|
54
114
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -85,26 +145,57 @@ export const scatterLayerType = new LayerType({
|
|
|
85
145
|
type: "boolean",
|
|
86
146
|
default: false,
|
|
87
147
|
description: "Map the normalized color value to alpha so low values fade to transparent"
|
|
148
|
+
},
|
|
149
|
+
mode: {
|
|
150
|
+
type: "string",
|
|
151
|
+
enum: ["points", "lines"],
|
|
152
|
+
default: "points",
|
|
153
|
+
description: "Render as individual points or connected lines"
|
|
154
|
+
},
|
|
155
|
+
lineSegmentIdData: {
|
|
156
|
+
type: "string",
|
|
157
|
+
enum: dataProperties,
|
|
158
|
+
description: "Column for segment IDs; only consecutive points sharing the same ID are connected"
|
|
159
|
+
},
|
|
160
|
+
lineColorMode: {
|
|
161
|
+
type: "string",
|
|
162
|
+
enum: ["gradient", "midpoint"],
|
|
163
|
+
default: "gradient",
|
|
164
|
+
description: "Color mode for lines: gradient interpolates vData linearly; midpoint uses each endpoint's color up to the segment center"
|
|
165
|
+
},
|
|
166
|
+
lineWidth: {
|
|
167
|
+
type: "number",
|
|
168
|
+
default: 1.0,
|
|
169
|
+
minimum: 1,
|
|
170
|
+
description: "Line width in pixels (note: browsers may clamp values above 1)"
|
|
88
171
|
}
|
|
89
172
|
},
|
|
90
173
|
required: ["xData", "yData", "vData"]
|
|
91
174
|
}
|
|
92
|
-
}
|
|
93
|
-
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_createLayer(parameters, data) {
|
|
94
178
|
const d = Data.wrap(data)
|
|
95
|
-
const {
|
|
179
|
+
const {
|
|
180
|
+
xData, yData, vData,
|
|
181
|
+
alphaBlend = false,
|
|
182
|
+
mode = "points",
|
|
183
|
+
lineSegmentIdData,
|
|
184
|
+
lineColorMode = "gradient",
|
|
185
|
+
lineWidth = 1.0,
|
|
186
|
+
} = parameters
|
|
96
187
|
|
|
97
188
|
const xQK = d.getQuantityKind(xData) ?? xData
|
|
98
189
|
const yQK = d.getQuantityKind(yData) ?? yData
|
|
99
190
|
const vQK = d.getQuantityKind(vData) ?? vData
|
|
100
191
|
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
192
|
+
const srcX = d.getData(xData)
|
|
193
|
+
const srcY = d.getData(yData)
|
|
194
|
+
const srcV = d.getData(vData)
|
|
104
195
|
|
|
105
|
-
if (!
|
|
106
|
-
if (!
|
|
107
|
-
if (!
|
|
196
|
+
if (!srcX) throw new Error(`Data column '${xData}' not found`)
|
|
197
|
+
if (!srcY) throw new Error(`Data column '${yData}' not found`)
|
|
198
|
+
if (!srcV) throw new Error(`Data column '${vData}' not found`)
|
|
108
199
|
|
|
109
200
|
const domains = {}
|
|
110
201
|
const xDomain = d.getDomain(xData)
|
|
@@ -114,9 +205,59 @@ export const scatterLayerType = new LayerType({
|
|
|
114
205
|
if (yDomain) domains[yQK] = yDomain
|
|
115
206
|
if (vDomain) domains[vQK] = vDomain
|
|
116
207
|
|
|
208
|
+
const blendConfig = alphaBlend ? {
|
|
209
|
+
enable: true,
|
|
210
|
+
func: { srcRGB: 'src alpha', dstRGB: 'one minus src alpha', srcAlpha: 0, dstAlpha: 1 },
|
|
211
|
+
} : null
|
|
212
|
+
|
|
213
|
+
if (mode === "lines") {
|
|
214
|
+
const N = srcX.length
|
|
215
|
+
const segIds = lineSegmentIdData ? d.getData(lineSegmentIdData) : null
|
|
216
|
+
// Zero-init array used when no segment IDs: abs(0-0) < 0.5 → always same segment
|
|
217
|
+
const zeroSegs = new Float32Array(N - 1)
|
|
218
|
+
const seg0 = segIds ? segIds.subarray(0, N - 1) : zeroSegs
|
|
219
|
+
const seg1 = segIds ? segIds.subarray(1, N) : zeroSegs
|
|
220
|
+
|
|
221
|
+
return [{
|
|
222
|
+
attributes: {
|
|
223
|
+
a_endPoint: new Float32Array([0.0, 1.0]),
|
|
224
|
+
a_x0: srcX.subarray(0, N - 1),
|
|
225
|
+
a_x1: srcX.subarray(1, N),
|
|
226
|
+
a_y0: srcY.subarray(0, N - 1),
|
|
227
|
+
a_y1: srcY.subarray(1, N),
|
|
228
|
+
a_v0: srcV.subarray(0, N - 1),
|
|
229
|
+
a_v1: srcV.subarray(1, N),
|
|
230
|
+
a_seg0: seg0,
|
|
231
|
+
a_seg1: seg1,
|
|
232
|
+
},
|
|
233
|
+
attributeDivisors: {
|
|
234
|
+
a_x0: 1, a_x1: 1,
|
|
235
|
+
a_y0: 1, a_y1: 1,
|
|
236
|
+
a_v0: 1, a_v1: 1,
|
|
237
|
+
a_seg0: 1, a_seg1: 1,
|
|
238
|
+
},
|
|
239
|
+
uniforms: {
|
|
240
|
+
alphaBlend: alphaBlend ? 1.0 : 0.0,
|
|
241
|
+
u_lineColorMode: lineColorMode === "midpoint" ? 1.0 : 0.0,
|
|
242
|
+
},
|
|
243
|
+
nameMap: {
|
|
244
|
+
[`colorscale_${vQK}`]: 'colorscale',
|
|
245
|
+
[`color_range_${vQK}`]: 'color_range',
|
|
246
|
+
[`color_scale_type_${vQK}`]: 'color_scale_type',
|
|
247
|
+
},
|
|
248
|
+
domains,
|
|
249
|
+
primitive: "lines",
|
|
250
|
+
lineWidth,
|
|
251
|
+
vertexCount: 2,
|
|
252
|
+
instanceCount: N - 1,
|
|
253
|
+
blend: blendConfig,
|
|
254
|
+
}]
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Points mode — existing behaviour
|
|
117
258
|
return [{
|
|
118
|
-
attributes: { x, y, [vQK]:
|
|
119
|
-
uniforms: {},
|
|
259
|
+
attributes: { x: srcX, y: srcY, [vQK]: srcV },
|
|
260
|
+
uniforms: { alphaBlend: alphaBlend ? 1.0 : 0.0 },
|
|
120
261
|
domains,
|
|
121
262
|
nameMap: {
|
|
122
263
|
[vQK]: 'color_data',
|
|
@@ -124,11 +265,23 @@ export const scatterLayerType = new LayerType({
|
|
|
124
265
|
[`color_range_${vQK}`]: 'color_range',
|
|
125
266
|
[`color_scale_type_${vQK}`]: 'color_scale_type',
|
|
126
267
|
},
|
|
127
|
-
blend:
|
|
128
|
-
enable: true,
|
|
129
|
-
func: { srcRGB: 'src alpha', dstRGB: 'one minus src alpha', srcAlpha: 0, dstAlpha: 1 },
|
|
130
|
-
} : null,
|
|
268
|
+
blend: blendConfig,
|
|
131
269
|
}]
|
|
132
270
|
}
|
|
133
|
-
|
|
271
|
+
|
|
272
|
+
// Swap vert/frag to the lines variants before letting the parent build the draw command,
|
|
273
|
+
// then restore. JS is single-threaded so the temporary swap is safe.
|
|
274
|
+
createDrawCommand(regl, layer) {
|
|
275
|
+
if (layer.primitive === "lines") {
|
|
276
|
+
this.vert = LINES_VERT
|
|
277
|
+
this.frag = LINES_FRAG
|
|
278
|
+
} else {
|
|
279
|
+
this.vert = POINTS_VERT
|
|
280
|
+
this.frag = POINTS_FRAG
|
|
281
|
+
}
|
|
282
|
+
return super.createDrawCommand(regl, layer)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export const scatterLayerType = new ScatterLayerType()
|
|
134
287
|
registerLayerType("scatter", scatterLayerType)
|