gladly-plot 0.0.1 → 0.0.3

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
@@ -13,8 +13,10 @@ Gladly combines WebGL rendering (via regl) with D3.js for interactive axes and z
13
13
  - 🎯 Interactive multi-axis support (up to 4 axes)
14
14
  - 🔍 Zoom and pan interactions
15
15
  - 🧩 Extensible layer type registry
16
- - 📏 Unit-aware axis management
17
- - ~250 lines of focused source code
16
+ - 📏 Quantity and unit-aware axis management
17
+ - 🎨 Supports all standard colorscales
18
+ - 🔗 Subplot axis linking
19
+ - 🌈 Axis to coloring or filtering linking
18
20
 
19
21
  ## Documentation
20
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gladly-plot",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "GPU-powered multi-axis plotting library with regl + d3",
5
5
  "type": "module",
6
6
 
@@ -18,6 +18,8 @@
18
18
 
19
19
  "dependencies": {
20
20
  "d3": "^7.8.5",
21
+ "proj4": "^2.15.0",
22
+ "projnames": "^0.0.2",
21
23
  "regl": "^2.1.0"
22
24
  },
23
25
 
@@ -16,9 +16,9 @@ export class AxisRegistry {
16
16
  }
17
17
 
18
18
  ensureAxis(axisName, axisQuantityKind, scaleOverride) {
19
- if (!AXES.includes(axisName)) throw `Unknown axis ${axisName}`
19
+ if (!AXES.includes(axisName)) throw new Error(`Unknown axis ${axisName}`)
20
20
  if (this.axisQuantityKinds[axisName] && this.axisQuantityKinds[axisName] !== axisQuantityKind)
21
- throw `Axis quantity kind mismatch on axis ${axisName}: ${this.axisQuantityKinds[axisName]} vs ${axisQuantityKind}`
21
+ throw new Error(`Axis quantity kind mismatch on axis ${axisName}: ${this.axisQuantityKinds[axisName]} vs ${axisQuantityKind}`)
22
22
 
23
23
  if (!this.scales[axisName]) {
24
24
  const quantityKindDef = getAxisQuantityKind(axisQuantityKind)
@@ -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(' return map_color(cs, vec2(r0, r1), vt);')
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/Data.js ADDED
@@ -0,0 +1,67 @@
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
+ }
@@ -0,0 +1,123 @@
1
+ import proj4 from 'proj4'
2
+ import { byEpsg } from 'projnames'
3
+ import { registerAxisQuantityKind } from './AxisQuantityKindRegistry.js'
4
+
5
+ /**
6
+ * Parse an EPSG CRS string or number to a plain integer code.
7
+ * Accepts: 26911, "26911", "EPSG:26911", "epsg:26911"
8
+ */
9
+ export function parseCrsCode(crs) {
10
+ if (typeof crs === 'number') return crs
11
+ const m = String(crs).match(/(\d+)$/)
12
+ return m ? parseInt(m[1]) : null
13
+ }
14
+
15
+ /** "EPSG:26911" → "epsg_26911_x" */
16
+ export function crsToQkX(crs) {
17
+ return `epsg_${parseCrsCode(crs)}_x`
18
+ }
19
+
20
+ /** "EPSG:26911" → "epsg_26911_y" */
21
+ export function crsToQkY(crs) {
22
+ return `epsg_${parseCrsCode(crs)}_y`
23
+ }
24
+
25
+ /** "epsg_26911_x" or "epsg_26911_y" → 26911, or null if not matching */
26
+ export function qkToEpsgCode(qk) {
27
+ const m = String(qk).match(/^epsg_(\d+)_[xy]$/)
28
+ return m ? parseInt(m[1]) : null
29
+ }
30
+
31
+ /**
32
+ * Register a proj4 CRS definition and auto-register the matching
33
+ * epsg_CODE_x / epsg_CODE_y quantity kinds with labels from projnames.
34
+ * Useful for offline/air-gapped environments where network access is unavailable.
35
+ *
36
+ * @param {number} epsgCode - e.g. 26911
37
+ * @param {string} proj4string - e.g. "+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs"
38
+ */
39
+ export function registerEpsgDef(epsgCode, proj4string) {
40
+ proj4.defs(`EPSG:${epsgCode}`, proj4string)
41
+ _registerQuantityKinds(epsgCode)
42
+ }
43
+
44
+ // ─── Internal auto-fetch machinery ───────────────────────────────────────────
45
+
46
+ // Labels for EPSG codes absent from (or wrong in) the projnames package.
47
+ const EPSG_LABEL_OVERRIDES = {
48
+ 4326: 'WGS 84',
49
+ }
50
+
51
+ // Tracks codes whose quantity kinds have been registered (avoids duplicate work)
52
+ const _registeredQkCodes = new Set()
53
+
54
+ // In-flight fetch promises keyed by code (deduplicates concurrent requests)
55
+ const _pendingFetches = new Map()
56
+
57
+ function _registerQuantityKinds(code) {
58
+ if (_registeredQkCodes.has(code)) return
59
+ _registeredQkCodes.add(code)
60
+ const name = EPSG_LABEL_OVERRIDES[code] ?? byEpsg[code] ?? `EPSG:${code}`
61
+ registerAxisQuantityKind(`epsg_${code}_x`, { label: `${name} X`, scale: 'linear' })
62
+ registerAxisQuantityKind(`epsg_${code}_y`, { label: `${name} Y`, scale: 'linear' })
63
+ }
64
+
65
+ /**
66
+ * Ensure a CRS is defined in proj4 and has quantity kinds registered.
67
+ * - If already known to proj4, only registers quantity kinds (synchronous work).
68
+ * - Otherwise fetches the proj4 string from epsg.io (async).
69
+ * - Concurrent calls for the same code share a single in-flight request.
70
+ *
71
+ * @param {string|number} crs - e.g. "EPSG:26911", 26911
72
+ * @returns {Promise<void>}
73
+ */
74
+ export async function ensureCrsDefined(crs) {
75
+ const code = parseCrsCode(crs)
76
+ if (!code) throw new Error(`Cannot parse CRS code from: ${crs}`)
77
+
78
+ const key = `EPSG:${code}`
79
+
80
+ // Register quantity kinds first — this is always synchronous (projnames lookup)
81
+ _registerQuantityKinds(code)
82
+
83
+ // If proj4 already has this definition (including built-ins 4326 and 3857), we're done
84
+ if (proj4.defs(key)) return
85
+
86
+ // Deduplicate concurrent fetches for the same code
87
+ if (!_pendingFetches.has(code)) {
88
+ const p = fetch(`https://epsg.io/${code}.proj4`)
89
+ .then(res => {
90
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
91
+ return res.text()
92
+ })
93
+ .then(def => {
94
+ proj4.defs(key, def.trim())
95
+ })
96
+ .finally(() => {
97
+ _pendingFetches.delete(code)
98
+ })
99
+ _pendingFetches.set(code, p)
100
+ }
101
+
102
+ try {
103
+ await _pendingFetches.get(code)
104
+ } catch (err) {
105
+ throw new Error(`Failed to fetch proj4 definition for ${key}: ${err.message}`)
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Reproject a [x, y] point from one CRS to another.
111
+ * Both CRS strings are parsed via parseCrsCode (accepts "EPSG:N", "N", N).
112
+ * The CRS must already be registered (via registerEpsgDef or ensureCrsDefined).
113
+ *
114
+ * @param {string|number} fromCrs
115
+ * @param {string|number} toCrs
116
+ * @param {[number, number]} point
117
+ * @returns {[number, number]}
118
+ */
119
+ export function reproject(fromCrs, toCrs, point) {
120
+ const from = `EPSG:${parseCrsCode(fromCrs)}`
121
+ const to = `EPSG:${parseCrsCode(toCrs)}`
122
+ return proj4(from, to).forward(point)
123
+ }
@@ -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/Layer.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export class Layer {
2
- constructor({ type, attributes, uniforms, nameMap = {}, domains = {}, lineWidth = 1, primitive = "points", xAxis = "xaxis_bottom", yAxis = "yaxis_left", xAxisQuantityKind, yAxisQuantityKind, colorAxes = [], filterAxes = [], vertexCount = null, instanceCount = null, attributeDivisors = {} }) {
2
+ constructor({ type, attributes, uniforms, nameMap = {}, domains = {}, lineWidth = 1, primitive = "points", xAxis = "xaxis_bottom", yAxis = "yaxis_left", xAxisQuantityKind, yAxisQuantityKind, colorAxes = [], filterAxes = [], vertexCount = null, instanceCount = null, attributeDivisors = {}, blend = null }) {
3
3
  // Validate that all attributes are typed arrays
4
4
  for (const [key, value] of Object.entries(attributes)) {
5
5
  if (!(value instanceof Float32Array)) {
@@ -39,5 +39,6 @@ export class Layer {
39
39
  this.vertexCount = vertexCount
40
40
  this.instanceCount = instanceCount
41
41
  this.attributeDivisors = attributeDivisors
42
+ this.blend = blend
42
43
  }
43
44
  }
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,
@@ -22,7 +60,7 @@ export class LayerType {
22
60
  // Optional dynamic resolver — overrides statics wherever it returns a non-undefined value
23
61
  getAxisConfig,
24
62
  // GPU rendering
25
- vert, frag, schema, createLayer
63
+ vert, frag, schema, createLayer, createDrawCommand
26
64
  }) {
27
65
  this.name = name
28
66
  // Static declarations stored as-is (undefined = not declared)
@@ -38,6 +76,7 @@ export class LayerType {
38
76
  if (schema) this._schema = schema
39
77
  if (createLayer) this._createLayer = createLayer
40
78
  if (getAxisConfig) this._getAxisConfig = getAxisConfig
79
+ if (createDrawCommand) this.createDrawCommand = createDrawCommand
41
80
  }
42
81
 
43
82
  createDrawCommand(regl, layer) {
@@ -47,19 +86,29 @@ export class LayerType {
47
86
  // Build a single-entry uniform object with renamed key reading from the internal prop name.
48
87
  const u = (internalName) => ({ [shaderName(internalName)]: regl.prop(internalName) })
49
88
 
50
- const attributes = Object.fromEntries(
51
- Object.entries(layer.attributes).map(([key, buffer]) => {
52
- const divisor = layer.attributeDivisors[key]
53
- const attrObj = divisor !== undefined ? { buffer, divisor } : { buffer }
54
- return [shaderName(key), attrObj]
55
- })
56
- )
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
+ }
57
104
 
58
105
  const uniforms = {
59
106
  ...u("xDomain"),
60
107
  ...u("yDomain"),
61
108
  ...u("xScaleType"),
62
109
  ...u("yScaleType"),
110
+ u_pickingMode: regl.prop('u_pickingMode'),
111
+ u_pickLayerIndex: regl.prop('u_pickLayerIndex'),
63
112
  ...Object.fromEntries(
64
113
  Object.entries(layer.uniforms).map(([key, value]) => [shaderName(key), value])
65
114
  )
@@ -79,28 +128,18 @@ export class LayerType {
79
128
  const spatialGlsl = buildSpatialGlsl()
80
129
  const colorGlsl = layer.colorAxes.length > 0 ? buildColorGlsl() : ''
81
130
  const filterGlsl = layer.filterAxes.length > 0 ? buildFilterGlsl() : ''
82
- const injectInto = (src, helpers) => {
83
- const injected = helpers.filter(Boolean).join('\n')
84
- if (!injected) return src
85
- const versionRe = /^[ \t]*#version[^\n]*\n?/
86
- const versionMatch = src.match(versionRe)
87
- const version = versionMatch ? versionMatch[0] : ''
88
- const rest = version ? src.slice(version.length) : src
89
- const precisionRe = /^\s*precision\s+\S+\s+\S+\s*;\s*$/mg
90
- const precisions = rest.match(precisionRe) ?? []
91
- const body = rest.replace(precisionRe, '')
92
- return version + precisions.join('\n') + '\n' + injected + '\n' + body
93
- }
131
+ const pickVertDecls = `attribute float a_pickId;\nvarying float v_pickId;`
94
132
 
95
133
  const drawConfig = {
96
- vert: injectInto(this.vert, [spatialGlsl, colorGlsl, filterGlsl]),
97
- frag: injectInto(this.frag, [colorGlsl, filterGlsl]),
134
+ vert: injectPickIdAssignment(injectInto(this.vert, [spatialGlsl, filterGlsl, pickVertDecls])),
135
+ frag: injectInto(this.frag, [buildApplyColorGlsl(), colorGlsl, filterGlsl]),
98
136
  attributes,
99
137
  uniforms,
100
138
  viewport: regl.prop("viewport"),
101
139
  primitive: layer.primitive,
102
140
  lineWidth: layer.lineWidth,
103
- count: regl.prop("count")
141
+ count: regl.prop("count"),
142
+ ...(layer.blend ? { blend: layer.blend } : {})
104
143
  }
105
144
 
106
145
  if (layer.instanceCount !== null) {
@@ -158,6 +197,7 @@ export class LayerType {
158
197
  vertexCount: gpuConfig.vertexCount ?? null,
159
198
  instanceCount: gpuConfig.instanceCount ?? null,
160
199
  attributeDivisors: gpuConfig.attributeDivisors ?? {},
200
+ blend: gpuConfig.blend ?? null,
161
201
  xAxis: axisConfig.xAxis,
162
202
  yAxis: axisConfig.yAxis,
163
203
  xAxisQuantityKind: axisConfig.xAxisQuantityKind,
package/src/Plot.js CHANGED
@@ -355,7 +355,8 @@ export class Plot {
355
355
  }
356
356
 
357
357
  _processLayers(layersConfig, data) {
358
- for (const layerSpec of layersConfig) {
358
+ for (let configLayerIndex = 0; configLayerIndex < layersConfig.length; configLayerIndex++) {
359
+ const layerSpec = layersConfig[configLayerIndex]
359
360
  const entries = Object.entries(layerSpec)
360
361
  if (entries.length !== 1) {
361
362
  throw new Error("Each layer specification must have exactly one layer type key")
@@ -385,7 +386,8 @@ export class Plot {
385
386
 
386
387
  // Create one draw command per GPU config returned by the layer type.
387
388
  for (const layer of layerType.createLayer(parameters, data)) {
388
- layer.draw = layer.type.createDrawCommand(this.regl, layer)
389
+ layer.configLayerIndex = configLayerIndex
390
+ layer.draw = layer.type.createDrawCommand(this.regl, layer, this)
389
391
  this.layers.push(layer)
390
392
  }
391
393
  }
@@ -811,7 +813,9 @@ export class Plot {
811
813
  xScaleType: xIsLog ? 1.0 : 0.0,
812
814
  yScaleType: yIsLog ? 1.0 : 0.0,
813
815
  viewport: viewport,
814
- count: layer.vertexCount ?? layer.attributes.x?.length ?? 0
816
+ count: layer.vertexCount ?? layer.attributes.x?.length ?? 0,
817
+ u_pickingMode: 0.0,
818
+ u_pickLayerIndex: 0.0,
815
819
  }
816
820
 
817
821
  if (layer.instanceCount !== null) {
@@ -973,4 +977,92 @@ export class Plot {
973
977
 
974
978
  fullOverlay.call(zoomBehavior)
975
979
  }
980
+
981
+ lookup(x, y) {
982
+ const result = {}
983
+ if (!this.axisRegistry) return result
984
+ const plotX = x - this.margin.left
985
+ const plotY = y - this.margin.top
986
+ for (const axisId of AXES) {
987
+ const scale = this.axisRegistry.getScale(axisId)
988
+ if (!scale) continue
989
+ const qk = this.axisRegistry.axisQuantityKinds[axisId]
990
+ const value = axisId.includes('y') ? scale.invert(plotY) : scale.invert(plotX)
991
+ result[axisId] = value
992
+ if (qk) result[qk] = value
993
+ }
994
+ return result
995
+ }
996
+
997
+ on(eventType, callback) {
998
+ const handler = (e) => {
999
+ if (!this.container.contains(e.target)) return
1000
+ const rect = this.container.getBoundingClientRect()
1001
+ const x = e.clientX - rect.left
1002
+ const y = e.clientY - rect.top
1003
+ callback(e, this.lookup(x, y))
1004
+ }
1005
+ window.addEventListener(eventType, handler, { capture: true })
1006
+ return { remove: () => window.removeEventListener(eventType, handler, { capture: true }) }
1007
+ }
1008
+
1009
+ pick(x, y) {
1010
+ if (!this.regl || !this.layers.length) return null
1011
+
1012
+ const fbo = this.regl.framebuffer({
1013
+ width: this.width, height: this.height,
1014
+ colorFormat: 'rgba', colorType: 'uint8', depth: false,
1015
+ })
1016
+
1017
+ const glX = Math.round(x)
1018
+ const glY = this.height - Math.round(y) - 1
1019
+
1020
+ let result = null
1021
+ this.regl({ framebuffer: fbo })(() => {
1022
+ this.regl.clear({ color: [0, 0, 0, 0] })
1023
+ const viewport = {
1024
+ x: this.margin.left, y: this.margin.bottom,
1025
+ width: this.plotWidth, height: this.plotHeight
1026
+ }
1027
+ for (let i = 0; i < this.layers.length; i++) {
1028
+ const layer = this.layers[i]
1029
+ const xIsLog = layer.xAxis ? this.axisRegistry.isLogScale(layer.xAxis) : false
1030
+ const yIsLog = layer.yAxis ? this.axisRegistry.isLogScale(layer.yAxis) : false
1031
+ const props = {
1032
+ xDomain: layer.xAxis ? this.axisRegistry.getScale(layer.xAxis).domain() : [0, 1],
1033
+ yDomain: layer.yAxis ? this.axisRegistry.getScale(layer.yAxis).domain() : [0, 1],
1034
+ xScaleType: xIsLog ? 1.0 : 0.0,
1035
+ yScaleType: yIsLog ? 1.0 : 0.0,
1036
+ viewport,
1037
+ count: layer.vertexCount ?? layer.attributes.x?.length ?? 0,
1038
+ u_pickingMode: 1.0,
1039
+ u_pickLayerIndex: i,
1040
+ }
1041
+ if (layer.instanceCount !== null) props.instances = layer.instanceCount
1042
+ for (const qk of layer.colorAxes) {
1043
+ props[`colorscale_${qk}`] = this.colorAxisRegistry.getColorscaleIndex(qk)
1044
+ const range = this.colorAxisRegistry.getRange(qk)
1045
+ props[`color_range_${qk}`] = range ?? [0, 1]
1046
+ props[`color_scale_type_${qk}`] = this._getScaleTypeFloat(qk)
1047
+ }
1048
+ for (const qk of layer.filterAxes) {
1049
+ props[`filter_range_${qk}`] = this.filterAxisRegistry.getRangeUniform(qk)
1050
+ props[`filter_scale_type_${qk}`] = this._getScaleTypeFloat(qk)
1051
+ }
1052
+ layer.draw(props)
1053
+ }
1054
+ const pixels = this.regl.read({ x: glX, y: glY, width: 1, height: 1 })
1055
+ if (pixels[0] === 0) {
1056
+ result = null
1057
+ } else {
1058
+ const layerIndex = pixels[0] - 1
1059
+ const dataIndex = (pixels[1] << 16) | (pixels[2] << 8) | pixels[3]
1060
+ const layer = this.layers[layerIndex]
1061
+ result = { layerIndex, configLayerIndex: layer.configLayerIndex, dataIndex, layer }
1062
+ }
1063
+ })
1064
+
1065
+ fbo.destroy()
1066
+ return result
1067
+ }
976
1068
  }
@@ -1,18 +1,20 @@
1
1
  import { LayerType } from "./LayerType.js"
2
2
  import { AXES } from "./AxisRegistry.js"
3
3
  import { registerLayerType } from "./LayerTypeRegistry.js"
4
+ import { Data } from "./Data.js"
4
5
 
5
6
  export const scatterLayerType = new LayerType({
6
7
  name: "scatter",
7
8
 
8
- getAxisConfig: function(parameters) {
9
+ getAxisConfig: function(parameters, data) {
10
+ const d = Data.wrap(data)
9
11
  const { xData, yData, vData, xAxis, yAxis } = parameters
10
12
  return {
11
13
  xAxis,
12
- xAxisQuantityKind: xData,
14
+ xAxisQuantityKind: d.getQuantityKind(xData) ?? xData,
13
15
  yAxis,
14
- yAxisQuantityKind: yData,
15
- colorAxisQuantityKinds: [vData],
16
+ yAxisQuantityKind: d.getQuantityKind(yData) ?? yData,
17
+ colorAxisQuantityKinds: [d.getQuantityKind(vData) ?? vData],
16
18
  }
17
19
  },
18
20
 
@@ -39,13 +41,14 @@ export const scatterLayerType = new LayerType({
39
41
  uniform int colorscale;
40
42
  uniform vec2 color_range;
41
43
  uniform float color_scale_type;
44
+ uniform float alphaBlend;
42
45
  varying float value;
43
46
  void main() {
44
- gl_FragColor = map_color_s(colorscale, color_range, value, color_scale_type);
47
+ gl_FragColor = map_color_s(colorscale, color_range, value, color_scale_type, alphaBlend);
45
48
  }
46
49
  `,
47
50
  schema: (data) => {
48
- const dataProperties = data ? Object.keys(data) : []
51
+ const dataProperties = Data.wrap(data).columns()
49
52
  return {
50
53
  $schema: "https://json-schema.org/draft/2020-12/schema",
51
54
  type: "object",
@@ -76,31 +79,54 @@ export const scatterLayerType = new LayerType({
76
79
  enum: AXES.filter(a => a.includes("y")),
77
80
  default: "yaxis_left",
78
81
  description: "Which y-axis to use for this layer"
82
+ },
83
+ alphaBlend: {
84
+ type: "boolean",
85
+ default: false,
86
+ description: "Map the normalized color value to alpha so low values fade to transparent"
79
87
  }
80
88
  },
81
89
  required: ["xData", "yData", "vData"]
82
90
  }
83
91
  },
84
92
  createLayer: function(parameters, data) {
85
- const { xData, yData, vData } = parameters
93
+ const d = Data.wrap(data)
94
+ const { xData, yData, vData, alphaBlend = false } = parameters
95
+
96
+ const xQK = d.getQuantityKind(xData) ?? xData
97
+ const yQK = d.getQuantityKind(yData) ?? yData
98
+ const vQK = d.getQuantityKind(vData) ?? vData
99
+
100
+ const x = d.getData(xData)
101
+ const y = d.getData(yData)
102
+ const v = d.getData(vData)
86
103
 
87
- const x = data[xData]
88
- const y = data[yData]
89
- const v = data[vData]
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`)
90
107
 
91
- if (!x) throw new Error(`Data property '${xData}' not found in data object`)
92
- if (!y) throw new Error(`Data property '${yData}' not found in data object`)
93
- if (!v) throw new Error(`Data property '${vData}' not found in data object`)
108
+ const domains = {}
109
+ const xDomain = d.getDomain(xData)
110
+ const yDomain = d.getDomain(yData)
111
+ const vDomain = d.getDomain(vData)
112
+ if (xDomain) domains[xQK] = xDomain
113
+ if (yDomain) domains[yQK] = yDomain
114
+ if (vDomain) domains[vQK] = vDomain
94
115
 
95
116
  return [{
96
- attributes: { x, y, [vData]: v },
97
- uniforms: {},
117
+ attributes: { x, y, [vQK]: v },
118
+ uniforms: { alphaBlend: alphaBlend ? 1.0 : 0.0 },
119
+ domains,
98
120
  nameMap: {
99
- [vData]: 'color_data',
100
- [`colorscale_${vData}`]: 'colorscale',
101
- [`color_range_${vData}`]: 'color_range',
102
- [`color_scale_type_${vData}`]: 'color_scale_type',
121
+ [vQK]: 'color_data',
122
+ [`colorscale_${vQK}`]: 'colorscale',
123
+ [`color_range_${vQK}`]: 'color_range',
124
+ [`color_scale_type_${vQK}`]: 'color_scale_type',
103
125
  },
126
+ blend: alphaBlend ? {
127
+ enable: true,
128
+ func: { srcRGB: 'src alpha', dstRGB: 'one minus src alpha', srcAlpha: 0, dstAlpha: 1 },
129
+ } : null,
104
130
  }]
105
131
  }
106
132
  })