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 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",
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
- "files": ["src"],
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
- "@json-editor/json-editor": "^2.15.1"
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 or not yet laid out (e.g. inside display:none tab).
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 = width - this.margin.left - this.margin.right
108
- this.plotHeight = height - this.margin.top - this.margin.bottom
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
- this.forceUpdate()
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 {
@@ -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
- export const scatterLayerType = new LayerType({
7
- name: "scatter",
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
- getAxisConfig: function(parameters, data) {
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
- vert: `
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
- createLayer: function(parameters, data) {
175
+ }
176
+
177
+ _createLayer(parameters, data) {
93
178
  const d = Data.wrap(data)
94
- const { xData, yData, vData, alphaBlend = false } = parameters
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 x = d.getData(xData)
101
- const y = d.getData(yData)
102
- const v = d.getData(vData)
192
+ const srcX = d.getData(xData)
193
+ const srcY = d.getData(yData)
194
+ const srcV = d.getData(vData)
103
195
 
104
- if (!x) throw new Error(`Data column '${xData}' not found`)
105
- if (!y) throw new Error(`Data column '${yData}' not found`)
106
- if (!v) throw new Error(`Data column '${vData}' not found`)
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]: v },
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: alphaBlend ? {
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)