gladly-plot 0.0.3 → 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/Plot.js +10 -5
- package/src/ScatterLayer.js +204 -50
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/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 {
|
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,38 +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
|
-
uniform float alphaBlend;
|
|
45
|
-
varying float value;
|
|
46
|
-
void main() {
|
|
47
|
-
gl_FragColor = map_color_s(colorscale, color_range, value, color_scale_type, alphaBlend);
|
|
48
|
-
}
|
|
49
|
-
`,
|
|
50
|
-
schema: (data) => {
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
schema(data) {
|
|
51
112
|
const dataProperties = Data.wrap(data).columns()
|
|
52
113
|
return {
|
|
53
114
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -84,26 +145,57 @@ export const scatterLayerType = new LayerType({
|
|
|
84
145
|
type: "boolean",
|
|
85
146
|
default: false,
|
|
86
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)"
|
|
87
171
|
}
|
|
88
172
|
},
|
|
89
173
|
required: ["xData", "yData", "vData"]
|
|
90
174
|
}
|
|
91
|
-
}
|
|
92
|
-
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_createLayer(parameters, data) {
|
|
93
178
|
const d = Data.wrap(data)
|
|
94
|
-
const {
|
|
179
|
+
const {
|
|
180
|
+
xData, yData, vData,
|
|
181
|
+
alphaBlend = false,
|
|
182
|
+
mode = "points",
|
|
183
|
+
lineSegmentIdData,
|
|
184
|
+
lineColorMode = "gradient",
|
|
185
|
+
lineWidth = 1.0,
|
|
186
|
+
} = parameters
|
|
95
187
|
|
|
96
188
|
const xQK = d.getQuantityKind(xData) ?? xData
|
|
97
189
|
const yQK = d.getQuantityKind(yData) ?? yData
|
|
98
190
|
const vQK = d.getQuantityKind(vData) ?? vData
|
|
99
191
|
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const
|
|
192
|
+
const srcX = d.getData(xData)
|
|
193
|
+
const srcY = d.getData(yData)
|
|
194
|
+
const srcV = d.getData(vData)
|
|
103
195
|
|
|
104
|
-
if (!
|
|
105
|
-
if (!
|
|
106
|
-
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`)
|
|
107
199
|
|
|
108
200
|
const domains = {}
|
|
109
201
|
const xDomain = d.getDomain(xData)
|
|
@@ -113,8 +205,58 @@ export const scatterLayerType = new LayerType({
|
|
|
113
205
|
if (yDomain) domains[yQK] = yDomain
|
|
114
206
|
if (vDomain) domains[vQK] = vDomain
|
|
115
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
|
|
116
258
|
return [{
|
|
117
|
-
attributes: { x, y, [vQK]:
|
|
259
|
+
attributes: { x: srcX, y: srcY, [vQK]: srcV },
|
|
118
260
|
uniforms: { alphaBlend: alphaBlend ? 1.0 : 0.0 },
|
|
119
261
|
domains,
|
|
120
262
|
nameMap: {
|
|
@@ -123,11 +265,23 @@ export const scatterLayerType = new LayerType({
|
|
|
123
265
|
[`color_range_${vQK}`]: 'color_range',
|
|
124
266
|
[`color_scale_type_${vQK}`]: 'color_scale_type',
|
|
125
267
|
},
|
|
126
|
-
blend:
|
|
127
|
-
enable: true,
|
|
128
|
-
func: { srcRGB: 'src alpha', dstRGB: 'one minus src alpha', srcAlpha: 0, dstAlpha: 1 },
|
|
129
|
-
} : null,
|
|
268
|
+
blend: blendConfig,
|
|
130
269
|
}]
|
|
131
270
|
}
|
|
132
|
-
|
|
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()
|
|
133
287
|
registerLayerType("scatter", scatterLayerType)
|