gladly-plot 0.0.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gladly-plot",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "GPU-powered multi-axis plotting library with regl + d3",
5
5
  "type": "module",
6
6
 
@@ -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')
@@ -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 attributes = Object.fromEntries(
52
- Object.entries(layer.attributes).map(([key, buffer]) => {
53
- const divisor = layer.attributeDivisors[key]
54
- const attrObj = divisor !== undefined ? { buffer, divisor } : { buffer }
55
- return [shaderName(key), attrObj]
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 injectInto = (src, helpers) => {
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, colorGlsl, filterGlsl]),
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
@@ -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,6 +386,7 @@ 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)) {
389
+ layer.configLayerIndex = configLayerIndex
388
390
  layer.draw = layer.type.createDrawCommand(this.regl, layer, this)
389
391
  this.layers.push(layer)
390
392
  }
@@ -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
  }
@@ -41,11 +41,10 @@ export const scatterLayerType = new LayerType({
41
41
  uniform int colorscale;
42
42
  uniform vec2 color_range;
43
43
  uniform float color_scale_type;
44
+ uniform float alphaBlend;
44
45
  varying float value;
45
46
  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);
47
+ gl_FragColor = map_color_s(colorscale, color_range, value, color_scale_type, alphaBlend);
49
48
  }
50
49
  `,
51
50
  schema: (data) => {
@@ -116,7 +115,7 @@ export const scatterLayerType = new LayerType({
116
115
 
117
116
  return [{
118
117
  attributes: { x, y, [vQK]: v },
119
- uniforms: {},
118
+ uniforms: { alphaBlend: alphaBlend ? 1.0 : 0.0 },
120
119
  domains,
121
120
  nameMap: {
122
121
  [vQK]: 'color_data',