gladly-plot 0.0.3 → 0.0.5

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.
Files changed (44) hide show
  1. package/README.md +1 -0
  2. package/package.json +16 -8
  3. package/src/axes/Axis.js +253 -0
  4. package/src/{AxisQuantityKindRegistry.js → axes/AxisQuantityKindRegistry.js} +7 -0
  5. package/src/{AxisRegistry.js → axes/AxisRegistry.js} +48 -0
  6. package/src/axes/ColorAxisRegistry.js +93 -0
  7. package/src/{FilterAxisRegistry.js → axes/FilterAxisRegistry.js} +63 -0
  8. package/src/axes/ZoomController.js +141 -0
  9. package/src/colorscales/BivariateColorscales.js +205 -0
  10. package/src/colorscales/ColorscaleRegistry.js +124 -0
  11. package/src/compute/ComputationRegistry.js +237 -0
  12. package/src/compute/axisFilter.js +47 -0
  13. package/src/compute/conv.js +230 -0
  14. package/src/compute/fft.js +292 -0
  15. package/src/compute/filter.js +227 -0
  16. package/src/compute/hist.js +180 -0
  17. package/src/compute/kde.js +102 -0
  18. package/src/{Layer.js → core/Layer.js} +4 -3
  19. package/src/{LayerType.js → core/LayerType.js} +72 -7
  20. package/src/core/Plot.js +735 -0
  21. package/src/{Colorbar.js → floats/Colorbar.js} +19 -5
  22. package/src/floats/Colorbar2d.js +77 -0
  23. package/src/{Filterbar.js → floats/Filterbar.js} +18 -4
  24. package/src/{FilterbarFloat.js → floats/Float.js} +17 -30
  25. package/src/{EpsgUtils.js → geo/EpsgUtils.js} +1 -1
  26. package/src/index.js +35 -22
  27. package/src/{ColorbarLayer.js → layers/ColorbarLayer.js} +2 -2
  28. package/src/layers/ColorbarLayer2d.js +97 -0
  29. package/src/{FilterbarLayer.js → layers/FilterbarLayer.js} +2 -2
  30. package/src/layers/HistogramLayer.js +212 -0
  31. package/src/layers/LinesLayer.js +199 -0
  32. package/src/layers/PointsLayer.js +114 -0
  33. package/src/layers/ScatterShared.js +142 -0
  34. package/src/{TileLayer.js → layers/TileLayer.js} +4 -4
  35. package/src/Axis.js +0 -48
  36. package/src/ColorAxisRegistry.js +0 -49
  37. package/src/ColorscaleRegistry.js +0 -52
  38. package/src/Float.js +0 -159
  39. package/src/Plot.js +0 -1068
  40. package/src/ScatterLayer.js +0 -133
  41. /package/src/{AxisLink.js → axes/AxisLink.js} +0 -0
  42. /package/src/{MatplotlibColorscales.js → colorscales/MatplotlibColorscales.js} +0 -0
  43. /package/src/{Data.js → core/Data.js} +0 -0
  44. /package/src/{LayerTypeRegistry.js → core/LayerTypeRegistry.js} +0 -0
@@ -1,6 +1,9 @@
1
- import { Plot } from "./Plot.js"
2
- import { linkAxes } from "./AxisLink.js"
3
- import "./ColorbarLayer.js"
1
+ import { Plot } from "../core/Plot.js"
2
+ import { getScaleTypeFloat } from "../axes/AxisQuantityKindRegistry.js"
3
+ import { linkAxes } from "../axes/AxisLink.js"
4
+ import "../layers/ColorbarLayer.js"
5
+
6
+ const DRAG_BAR_HEIGHT = 12
4
7
 
5
8
  const DEFAULT_MARGINS = {
6
9
  horizontal: { top: 5, right: 40, bottom: 45, left: 40 },
@@ -34,7 +37,7 @@ export class Colorbar extends Plot {
34
37
 
35
38
  _getScaleTypeFloat(quantityKind) {
36
39
  if (quantityKind === this._colorAxisName && this._targetPlot) {
37
- return this._targetPlot._getScaleTypeFloat(quantityKind)
40
+ return getScaleTypeFloat(quantityKind, this._targetPlot.currentConfig?.axes)
38
41
  }
39
42
  return super._getScaleTypeFloat(quantityKind)
40
43
  }
@@ -50,7 +53,7 @@ export class Colorbar extends Plot {
50
53
  }
51
54
  const colorscale = this._targetPlot.colorAxisRegistry?.getColorscale(this._colorAxisName)
52
55
  if (colorscale) this.colorAxisRegistry.ensureColorAxis(this._colorAxisName, colorscale)
53
- const scaleType = this._targetPlot._getScaleTypeFloat(this._colorAxisName) > 0.5 ? "log" : "linear"
56
+ const scaleType = getScaleTypeFloat(this._colorAxisName, this._targetPlot.currentConfig?.axes) > 0.5 ? "log" : "linear"
54
57
  this.axisRegistry.setScaleType(this._spatialAxis, scaleType)
55
58
  }
56
59
  super.render()
@@ -62,3 +65,14 @@ export class Colorbar extends Plot {
62
65
  super.destroy()
63
66
  }
64
67
  }
68
+
69
+ // Register the colorbar float factory so Plot._syncFloats can create colorbar floats.
70
+ Plot.registerFloatFactory('colorbar', {
71
+ factory: (parentPlot, container, opts) =>
72
+ new Colorbar(container, parentPlot, opts.axisName, { orientation: opts.orientation }),
73
+ defaultSize: (opts) => {
74
+ const h = opts.orientation === 'horizontal' ? 70 + DRAG_BAR_HEIGHT : 220 + DRAG_BAR_HEIGHT
75
+ const w = opts.orientation === 'horizontal' ? 220 : 70
76
+ return { width: w, height: h }
77
+ }
78
+ })
@@ -0,0 +1,77 @@
1
+ import { Plot } from "../core/Plot.js"
2
+ import { getScaleTypeFloat } from "../axes/AxisQuantityKindRegistry.js"
3
+ import { linkAxes } from "../axes/AxisLink.js"
4
+ import "../layers/ColorbarLayer2d.js"
5
+
6
+ const DRAG_BAR_HEIGHT = 12
7
+
8
+ // Margins leave room for tick labels on both spatial axes.
9
+ const DEFAULT_MARGIN = { top: 10, right: 50, bottom: 45, left: 55 }
10
+
11
+ export class Colorbar2d extends Plot {
12
+ constructor(container, targetPlot, xAxis, yAxis, { margin } = {}) {
13
+ super(container, { margin: margin ?? DEFAULT_MARGIN })
14
+
15
+ this._targetPlot = targetPlot
16
+ this._xAxis = xAxis
17
+ this._yAxis = yAxis
18
+
19
+ this.update({
20
+ data: {},
21
+ config: {
22
+ layers: [{ colorbar2d: { xAxis, yAxis } }]
23
+ }
24
+ })
25
+
26
+ // Link the colorbar's spatial axes to the target's color axes so zoom/pan propagates.
27
+ this._xLink = linkAxes(this.axes["xaxis_bottom"], targetPlot.axes[xAxis])
28
+ this._yLink = linkAxes(this.axes["yaxis_left"], targetPlot.axes[yAxis])
29
+
30
+ // Re-render (with sync) whenever the target plot renders.
31
+ this._syncCallback = () => this.render()
32
+ targetPlot._renderCallbacks.add(this._syncCallback)
33
+ }
34
+
35
+ _getScaleTypeFloat(quantityKind) {
36
+ if ((quantityKind === this._xAxis || quantityKind === this._yAxis) && this._targetPlot) {
37
+ return getScaleTypeFloat(quantityKind, this._targetPlot.currentConfig?.axes)
38
+ }
39
+ return super._getScaleTypeFloat(quantityKind)
40
+ }
41
+
42
+ render() {
43
+ // Sync range, colorscale, and scale type for both color axes from the target plot.
44
+ if (this.colorAxisRegistry && this.axisRegistry && this._targetPlot) {
45
+ for (const [colorAxisName, spatialAxisId] of [
46
+ [this._xAxis, "xaxis_bottom"],
47
+ [this._yAxis, "yaxis_left"]
48
+ ]) {
49
+ const range = this._targetPlot.getAxisDomain(colorAxisName)
50
+ if (range) {
51
+ this.setAxisDomain(spatialAxisId, range)
52
+ this.setAxisDomain(colorAxisName, range)
53
+ }
54
+ const colorscale = this._targetPlot.colorAxisRegistry?.getColorscale(colorAxisName)
55
+ if (colorscale) this.colorAxisRegistry.ensureColorAxis(colorAxisName, colorscale)
56
+ const scaleType = getScaleTypeFloat(colorAxisName, this._targetPlot.currentConfig?.axes) > 0.5 ? "log" : "linear"
57
+ this.axisRegistry.setScaleType(spatialAxisId, scaleType)
58
+ }
59
+ }
60
+ super.render()
61
+ }
62
+
63
+ destroy() {
64
+ this._xLink.unlink()
65
+ this._yLink.unlink()
66
+ this._targetPlot._renderCallbacks.delete(this._syncCallback)
67
+ super.destroy()
68
+ }
69
+ }
70
+
71
+ // Register the colorbar2d float factory so Plot._syncFloats can create 2D colorbar floats.
72
+ // Default size is square since both axes carry equal weight.
73
+ Plot.registerFloatFactory('colorbar2d', {
74
+ factory: (parentPlot, container, opts) =>
75
+ new Colorbar2d(container, parentPlot, opts.xAxis, opts.yAxis),
76
+ defaultSize: () => ({ width: 250, height: 250 + DRAG_BAR_HEIGHT })
77
+ })
@@ -1,6 +1,9 @@
1
- import { Plot } from "./Plot.js"
2
- import { linkAxes } from "./AxisLink.js"
3
- import "./FilterbarLayer.js"
1
+ import { Plot } from "../core/Plot.js"
2
+ import { getScaleTypeFloat } from "../axes/AxisQuantityKindRegistry.js"
3
+ import { linkAxes } from "../axes/AxisLink.js"
4
+ import "../layers/FilterbarLayer.js"
5
+
6
+ const DRAG_BAR_HEIGHT = 12
4
7
 
5
8
  const DEFAULT_MARGINS = {
6
9
  horizontal: { top: 5, right: 40, bottom: 45, left: 40 },
@@ -124,7 +127,7 @@ export class Filterbar extends Plot {
124
127
  if (this._maxInput) this._maxInput.checked = range.max === null
125
128
  }
126
129
  }
127
- const scaleType = this._targetPlot._getScaleTypeFloat(this._filterAxisName) > 0.5 ? "log" : "linear"
130
+ const scaleType = getScaleTypeFloat(this._filterAxisName, this._targetPlot.currentConfig?.axes) > 0.5 ? "log" : "linear"
128
131
  this.axisRegistry.setScaleType(this._spatialAxis, scaleType)
129
132
  }
130
133
  super.render()
@@ -136,3 +139,14 @@ export class Filterbar extends Plot {
136
139
  super.destroy()
137
140
  }
138
141
  }
142
+
143
+ // Register the filterbar float factory so Plot._syncFloats can create filterbar floats.
144
+ Plot.registerFloatFactory('filterbar', {
145
+ factory: (parentPlot, container, opts) =>
146
+ new Filterbar(container, parentPlot, opts.axisName, { orientation: opts.orientation }),
147
+ defaultSize: (opts) => {
148
+ const h = opts.orientation === 'horizontal' ? 70 + DRAG_BAR_HEIGHT : 220 + DRAG_BAR_HEIGHT
149
+ const w = opts.orientation === 'horizontal' ? 220 : 80
150
+ return { width: w, height: h }
151
+ }
152
+ })
@@ -1,36 +1,24 @@
1
- import { Filterbar } from "./Filterbar.js"
2
- import { Plot } from "./Plot.js"
3
-
4
1
  const DRAG_BAR_HEIGHT = 12
5
2
  const MIN_WIDTH = 80
6
- const MIN_HEIGHT = DRAG_BAR_HEIGHT + 40
7
-
8
- const DEFAULT_SIZE = {
9
- horizontal: { width: 220, height: 70 + DRAG_BAR_HEIGHT },
10
- vertical: { width: 80, height: 220 + DRAG_BAR_HEIGHT }
11
- }
3
+ const MIN_HEIGHT = DRAG_BAR_HEIGHT + 30
12
4
 
13
- export class FilterbarFloat {
14
- constructor(parentPlot, filterAxisName, {
15
- orientation = "horizontal",
5
+ // Generic draggable, resizable floating container that wraps any Plot-like widget.
6
+ // factory(container) must return an object with a destroy() method.
7
+ export class Float {
8
+ constructor(parentPlot, factory, {
16
9
  x = 10,
17
- y = 100,
18
- width,
19
- height,
20
- margin
10
+ y = 10,
11
+ width = 220,
12
+ height = 82
21
13
  } = {}) {
22
- const defaults = DEFAULT_SIZE[orientation]
23
- const w = width ?? defaults.width
24
- const h = height ?? defaults.height
25
-
26
14
  // Outer floating container
27
15
  this._el = document.createElement('div')
28
16
  Object.assign(this._el.style, {
29
17
  position: 'absolute',
30
18
  left: x + 'px',
31
19
  top: y + 'px',
32
- width: w + 'px',
33
- height: h + 'px',
20
+ width: width + 'px',
21
+ height: height + 'px',
34
22
  zIndex: '10',
35
23
  boxSizing: 'border-box',
36
24
  background: 'rgba(255,255,255,0.88)',
@@ -40,6 +28,7 @@ export class FilterbarFloat {
40
28
  overflow: 'hidden'
41
29
  })
42
30
 
31
+ // Ensure parent is positioned so our absolute child is contained within it
43
32
  const parentEl = parentPlot.container
44
33
  if (getComputedStyle(parentEl).position === 'static') {
45
34
  parentEl.style.position = 'relative'
@@ -76,18 +65,18 @@ export class FilterbarFloat {
76
65
  })
77
66
  this._el.appendChild(this._resizeHandle)
78
67
 
79
- // Sub-container for the filterbar — sits below the drag bar
80
- this._filterbarEl = document.createElement('div')
81
- Object.assign(this._filterbarEl.style, {
68
+ // Content area — sits below the drag bar, fills the rest of the float
69
+ this._contentEl = document.createElement('div')
70
+ Object.assign(this._contentEl.style, {
82
71
  position: 'absolute',
83
72
  top: DRAG_BAR_HEIGHT + 'px',
84
73
  left: '0',
85
74
  right: '0',
86
75
  bottom: '0'
87
76
  })
88
- this._el.appendChild(this._filterbarEl)
77
+ this._el.appendChild(this._contentEl)
89
78
 
90
- this._filterbar = new Filterbar(this._filterbarEl, parentPlot, filterAxisName, { orientation, margin })
79
+ this._widget = factory(this._contentEl)
91
80
 
92
81
  this._setupInteraction()
93
82
  }
@@ -149,9 +138,7 @@ export class FilterbarFloat {
149
138
 
150
139
  destroy() {
151
140
  this._cleanupInteraction()
152
- this._filterbar.destroy()
141
+ this._widget.destroy()
153
142
  this._el.remove()
154
143
  }
155
144
  }
156
-
157
- Plot._FilterbarFloatClass = FilterbarFloat
@@ -1,6 +1,6 @@
1
1
  import proj4 from 'proj4'
2
2
  import { byEpsg } from 'projnames'
3
- import { registerAxisQuantityKind } from './AxisQuantityKindRegistry.js'
3
+ import { registerAxisQuantityKind } from '../axes/AxisQuantityKindRegistry.js'
4
4
 
5
5
  /**
6
6
  * Parse an EPSG CRS string or number to a plain integer code.
package/src/index.js CHANGED
@@ -1,24 +1,37 @@
1
- export { LayerType } from "./LayerType.js"
2
- export { Layer } from "./Layer.js"
3
- export { Data } from "./Data.js"
4
- export { AxisRegistry, AXES } from "./AxisRegistry.js"
5
- export { ColorAxisRegistry } from "./ColorAxisRegistry.js"
6
- export { FilterAxisRegistry, buildFilterGlsl } from "./FilterAxisRegistry.js"
7
- export { Plot } from "./Plot.js"
8
- export { scatterLayerType } from "./ScatterLayer.js"
9
- export { registerLayerType, getLayerType, getRegisteredLayerTypes } from "./LayerTypeRegistry.js"
10
- export { registerAxisQuantityKind, getAxisQuantityKind, getRegisteredAxisQuantityKinds } from "./AxisQuantityKindRegistry.js"
11
- export { registerColorscale, getRegisteredColorscales, getColorscaleIndex, buildColorGlsl } from "./ColorscaleRegistry.js"
12
- export { Axis } from "./Axis.js"
13
- export { linkAxes } from "./AxisLink.js"
14
- export { Colorbar } from "./Colorbar.js"
15
- export { colorbarLayerType } from "./ColorbarLayer.js"
16
- export { Float } from "./Float.js"
17
- export { Filterbar } from "./Filterbar.js"
18
- export { filterbarLayerType } from "./FilterbarLayer.js"
19
- export { FilterbarFloat } from "./FilterbarFloat.js"
20
- export { tileLayerType, TileLayerType } from "./TileLayer.js"
21
- export { registerEpsgDef, parseCrsCode, crsToQkX, crsToQkY, qkToEpsgCode, reproject } from "./EpsgUtils.js"
1
+ export { LayerType } from "./core/LayerType.js"
2
+ export { Layer } from "./core/Layer.js"
3
+ export { Data } from "./core/Data.js"
4
+ export { AxisRegistry, AXES } from "./axes/AxisRegistry.js"
5
+ export { ColorAxisRegistry } from "./axes/ColorAxisRegistry.js"
6
+ export { FilterAxisRegistry, buildFilterGlsl } from "./axes/FilterAxisRegistry.js"
7
+ export { Plot } from "./core/Plot.js"
8
+ export { pointsLayerType } from "./layers/PointsLayer.js"
9
+ export { linesLayerType } from "./layers/LinesLayer.js"
10
+ export { registerLayerType, getLayerType, getRegisteredLayerTypes } from "./core/LayerTypeRegistry.js"
11
+ export { registerAxisQuantityKind, getAxisQuantityKind, getRegisteredAxisQuantityKinds } from "./axes/AxisQuantityKindRegistry.js"
12
+ export { registerColorscale, register2DColorscale, getRegisteredColorscales, getRegistered2DColorscales, getColorscaleIndex, get2DColorscaleIndex, buildColorGlsl } from "./colorscales/ColorscaleRegistry.js"
13
+ export { Axis } from "./axes/Axis.js"
14
+ export { linkAxes } from "./axes/AxisLink.js"
15
+ export { Colorbar } from "./floats/Colorbar.js"
16
+ export { colorbarLayerType } from "./layers/ColorbarLayer.js"
17
+ export { Colorbar2d } from "./floats/Colorbar2d.js"
18
+ export { colorbar2dLayerType } from "./layers/ColorbarLayer2d.js"
19
+ export { Float } from "./floats/Float.js"
20
+ export { Filterbar } from "./floats/Filterbar.js"
21
+ export { filterbarLayerType } from "./layers/FilterbarLayer.js"
22
+ export { tileLayerType, TileLayerType } from "./layers/TileLayer.js"
23
+ export { histogramLayerType } from "./layers/HistogramLayer.js"
24
+ export { registerEpsgDef, parseCrsCode, crsToQkX, crsToQkY, qkToEpsgCode, reproject } from "./geo/EpsgUtils.js"
25
+ export { Computation, TextureComputation, GlslComputation, EXPRESSION_REF, computationSchema, registerTextureComputation, registerGlslComputation, isTexture } from "./compute/ComputationRegistry.js"
22
26
 
23
27
  // Register all matplotlib colorscales (side-effect import)
24
- import "./MatplotlibColorscales.js"
28
+ import "./colorscales/MatplotlibColorscales.js"
29
+ import "./colorscales/BivariateColorscales.js"
30
+
31
+ // Register all compute texture computations (side-effect imports)
32
+ import "./compute/filter.js"
33
+ import "./compute/kde.js"
34
+ import "./compute/fft.js"
35
+ import "./compute/conv.js"
36
+ import "./compute/hist.js"
37
+ import "./compute/axisFilter.js"
@@ -1,5 +1,5 @@
1
- import { LayerType } from "./LayerType.js"
2
- import { registerLayerType } from "./LayerTypeRegistry.js"
1
+ import { LayerType } from "../core/LayerType.js"
2
+ import { registerLayerType } from "../core/LayerTypeRegistry.js"
3
3
 
4
4
  // Four vertices for a triangle-strip quad covering the entire clip space.
5
5
  const quadCx = new Float32Array([-1, 1, -1, 1])
@@ -0,0 +1,97 @@
1
+ import { LayerType } from "../core/LayerType.js"
2
+ import { registerLayerType } from "../core/LayerTypeRegistry.js"
3
+
4
+ // Four vertices for a triangle-strip quad covering the entire clip space.
5
+ const quadCx = new Float32Array([-1, 1, -1, 1])
6
+ const quadCy = new Float32Array([-1, -1, 1, 1])
7
+
8
+ export const colorbar2dLayerType = new LayerType({
9
+ name: "colorbar2d",
10
+
11
+ getAxisConfig: function(parameters) {
12
+ const { xAxis, yAxis } = parameters
13
+ return {
14
+ xAxis: "xaxis_bottom",
15
+ xAxisQuantityKind: xAxis,
16
+ yAxis: "yaxis_left",
17
+ yAxisQuantityKind: yAxis,
18
+ colorAxisQuantityKinds: [xAxis, yAxis],
19
+ }
20
+ },
21
+
22
+ vert: `
23
+ precision mediump float;
24
+ attribute float cx;
25
+ attribute float cy;
26
+ varying float tval_x;
27
+ varying float tval_y;
28
+ void main() {
29
+ gl_Position = vec4(cx, cy, 0.0, 1.0);
30
+ tval_x = (cx + 1.0) / 2.0;
31
+ tval_y = (cy + 1.0) / 2.0;
32
+ }
33
+ `,
34
+
35
+ // tval_x/tval_y are [0,1] positions in the colorbar quad. We convert them to actual data
36
+ // values in each axis's range (undoing the log transform if needed), then pass those raw
37
+ // values to map_color_s_2d which re-applies the scale type internally. The exp() call
38
+ // is the inverse of the log() that map_color_s_2d will apply, so log-scale roundtrips
39
+ // correctly and linear-scale is a no-op (exp(log(v)) == v, but for linear vt == v anyway).
40
+ frag: `
41
+ precision mediump float;
42
+ uniform int colorscale_a;
43
+ uniform vec2 color_range_a;
44
+ uniform float color_scale_type_a;
45
+ uniform int colorscale_b;
46
+ uniform vec2 color_range_b;
47
+ uniform float color_scale_type_b;
48
+ varying float tval_x;
49
+ varying float tval_y;
50
+ void main() {
51
+ float r0_a = color_scale_type_a > 0.5 ? log(color_range_a.x) : color_range_a.x;
52
+ float r1_a = color_scale_type_a > 0.5 ? log(color_range_a.y) : color_range_a.y;
53
+ float vt_a = r0_a + tval_x * (r1_a - r0_a);
54
+ float v_a = color_scale_type_a > 0.5 ? exp(vt_a) : vt_a;
55
+
56
+ float r0_b = color_scale_type_b > 0.5 ? log(color_range_b.x) : color_range_b.x;
57
+ float r1_b = color_scale_type_b > 0.5 ? log(color_range_b.y) : color_range_b.y;
58
+ float vt_b = r0_b + tval_y * (r1_b - r0_b);
59
+ float v_b = color_scale_type_b > 0.5 ? exp(vt_b) : vt_b;
60
+
61
+ gl_FragColor = map_color_s_2d(
62
+ colorscale_a, color_range_a, v_a, color_scale_type_a,
63
+ colorscale_b, color_range_b, v_b, color_scale_type_b
64
+ );
65
+ }
66
+ `,
67
+
68
+ schema: () => ({
69
+ $schema: "https://json-schema.org/draft/2020-12/schema",
70
+ type: "object",
71
+ properties: {
72
+ xAxis: { type: "string", description: "Quantity kind for the x axis (color axis A)" },
73
+ yAxis: { type: "string", description: "Quantity kind for the y axis (color axis B)" }
74
+ },
75
+ required: ["xAxis", "yAxis"]
76
+ }),
77
+
78
+ createLayer: function(parameters) {
79
+ const { xAxis, yAxis } = parameters
80
+ return [{
81
+ attributes: { cx: quadCx, cy: quadCy },
82
+ uniforms: {},
83
+ primitive: "triangle strip",
84
+ vertexCount: 4,
85
+ nameMap: {
86
+ [`colorscale_${xAxis}`]: 'colorscale_a',
87
+ [`color_range_${xAxis}`]: 'color_range_a',
88
+ [`color_scale_type_${xAxis}`]: 'color_scale_type_a',
89
+ [`colorscale_${yAxis}`]: 'colorscale_b',
90
+ [`color_range_${yAxis}`]: 'color_range_b',
91
+ [`color_scale_type_${yAxis}`]: 'color_scale_type_b',
92
+ },
93
+ }]
94
+ }
95
+ })
96
+
97
+ registerLayerType("colorbar2d", colorbar2dLayerType)
@@ -1,5 +1,5 @@
1
- import { LayerType } from "./LayerType.js"
2
- import { registerLayerType } from "./LayerTypeRegistry.js"
1
+ import { LayerType } from "../core/LayerType.js"
2
+ import { registerLayerType } from "../core/LayerTypeRegistry.js"
3
3
 
4
4
  export const filterbarLayerType = new LayerType({
5
5
  name: "filterbar",
@@ -0,0 +1,212 @@
1
+ import { LayerType } from "../core/LayerType.js"
2
+ import { Data } from "../core/Data.js"
3
+ import { registerLayerType } from "../core/LayerTypeRegistry.js"
4
+ import { AXES } from "../axes/AxisRegistry.js"
5
+
6
+ // Ensure the 'histogram' and 'filteredHistogram' texture computations are registered.
7
+ import "../compute/hist.js"
8
+ import "../compute/axisFilter.js"
9
+
10
+ // Each bar is a quad drawn as a triangle strip (4 vertices).
11
+ // Per-instance: x_center (bin centre in data space), a_pickId (bin index).
12
+ // Per-vertex: a_corner ∈ {0,1,2,3} — selects which corner of the rectangle.
13
+ // corner 0: bottom-left corner 1: bottom-right
14
+ // corner 2: top-left corner 3: top-right
15
+ // The `count` attribute is resolved via the 'histogram' texture computation so
16
+ // its value equals the bin count sampled at a_pickId.
17
+
18
+ const HIST_VERT = `
19
+ precision mediump float;
20
+
21
+ attribute float a_corner;
22
+ attribute float x_center;
23
+ attribute float count;
24
+
25
+ uniform vec2 xDomain;
26
+ uniform vec2 yDomain;
27
+ uniform float xScaleType;
28
+ uniform float yScaleType;
29
+ uniform float u_binHalfWidth;
30
+
31
+ void main() {
32
+ float side = mod(a_corner, 2.0); // 0 = left, 1 = right
33
+ float top = floor(a_corner / 2.0); // 0 = bottom, 1 = top
34
+
35
+ float bx = x_center + (side * 2.0 - 1.0) * u_binHalfWidth;
36
+ float by = top * count;
37
+
38
+ float nx = normalize_axis(bx, xDomain, xScaleType);
39
+ float ny = normalize_axis(by, yDomain, yScaleType);
40
+ gl_Position = vec4(nx * 2.0 - 1.0, ny * 2.0 - 1.0, 0.0, 1.0);
41
+ }
42
+ `
43
+
44
+ const HIST_FRAG = `
45
+ precision mediump float;
46
+ uniform vec4 u_color;
47
+ void main() {
48
+ gl_FragColor = gladly_apply_color(u_color);
49
+ }
50
+ `
51
+
52
+ class HistogramLayerType extends LayerType {
53
+ constructor() {
54
+ super({ name: "histogram", vert: HIST_VERT, frag: HIST_FRAG })
55
+ }
56
+
57
+ _getAxisConfig(parameters, data) {
58
+ const d = Data.wrap(data)
59
+ const { vData, xAxis = "xaxis_bottom", yAxis = "yaxis_left", filterColumn } = parameters
60
+ const activeFilter = filterColumn && filterColumn !== "none" ? filterColumn : null
61
+ const filterQK = activeFilter ? (d.getQuantityKind(activeFilter) ?? activeFilter) : null
62
+ return {
63
+ xAxis,
64
+ xAxisQuantityKind: d.getQuantityKind(vData) ?? vData,
65
+ yAxis,
66
+ yAxisQuantityKind: "count",
67
+ ...(filterQK ? { filterAxisQuantityKinds: [filterQK] } : {}),
68
+ }
69
+ }
70
+
71
+ schema(data) {
72
+ const dataProperties = Data.wrap(data).columns()
73
+ return {
74
+ $schema: "https://json-schema.org/draft/2020-12/schema",
75
+ type: "object",
76
+ properties: {
77
+ vData: {
78
+ type: "string",
79
+ enum: dataProperties,
80
+ description: "Data column to histogram"
81
+ },
82
+ filterColumn: {
83
+ type: "string",
84
+ enum: ["none", ...dataProperties],
85
+ description: "Data column used to filter points via its filter axis, or 'none'"
86
+ },
87
+ bins: {
88
+ type: "integer",
89
+ description: "Number of bins (auto-selected by sqrt rule if omitted)"
90
+ },
91
+ color: {
92
+ type: "array",
93
+ items: { type: "number" },
94
+ minItems: 4,
95
+ maxItems: 4,
96
+ default: [0.2, 0.5, 0.8, 1.0],
97
+ description: "Bar colour as [R, G, B, A] in [0, 1]"
98
+ },
99
+ xAxis: {
100
+ type: "string",
101
+ enum: AXES.filter(a => a.includes("x")),
102
+ default: "xaxis_bottom"
103
+ },
104
+ yAxis: {
105
+ type: "string",
106
+ enum: AXES.filter(a => a.includes("y")),
107
+ default: "yaxis_left"
108
+ }
109
+ },
110
+ required: ["vData", "filterColumn"]
111
+ }
112
+ }
113
+
114
+ _createLayer(parameters, data) {
115
+ const d = Data.wrap(data)
116
+ const {
117
+ vData,
118
+ bins: requestedBins = null,
119
+ color = [0.2, 0.5, 0.8, 1.0],
120
+ filterColumn = "none",
121
+ } = parameters
122
+
123
+ const srcV = d.getData(vData)
124
+ if (!srcV) throw new Error(`Data column '${vData}' not found`)
125
+ const vQK = d.getQuantityKind(vData) ?? vData
126
+
127
+ // --- Optional filter column ---
128
+ const activeFilter = filterColumn && filterColumn !== "none" ? filterColumn : null
129
+ const filterQK = activeFilter ? (d.getQuantityKind(activeFilter) ?? activeFilter) : null
130
+ const srcF = activeFilter ? d.getData(activeFilter) : null
131
+ if (activeFilter && !srcF) throw new Error(`Data column '${activeFilter}' not found`)
132
+
133
+ // --- Compute min/max for normalization ---
134
+ let min = Infinity, max = -Infinity
135
+ for (let i = 0; i < srcV.length; i++) {
136
+ if (srcV[i] < min) min = srcV[i]
137
+ if (srcV[i] > max) max = srcV[i]
138
+ }
139
+ const range = max - min || 1
140
+
141
+ // --- Choose bin count ---
142
+ const bins = requestedBins || Math.max(10, Math.min(200, Math.ceil(Math.sqrt(srcV.length))))
143
+ const binWidth = range / bins
144
+
145
+ // --- Normalize data to [0, 1] for the histogram computation ---
146
+ const normalized = new Float32Array(srcV.length)
147
+ for (let i = 0; i < srcV.length; i++) {
148
+ normalized[i] = (srcV[i] - min) / range
149
+ }
150
+
151
+ // --- CPU histogram for domain (y-axis range) estimation ---
152
+ // Uses unfiltered data so the y-axis scale stays stable while the filter moves.
153
+ const histCpu = new Float32Array(bins)
154
+ for (let i = 0; i < srcV.length; i++) {
155
+ const b = Math.min(Math.floor(normalized[i] * bins), bins - 1)
156
+ histCpu[b] += 1
157
+ }
158
+ const maxCount = Math.max(...histCpu)
159
+
160
+ // --- Per-instance: bin centre positions in data space ---
161
+ const x_center = new Float32Array(bins)
162
+ for (let i = 0; i < bins; i++) {
163
+ x_center[i] = min + (i + 0.5) * binWidth
164
+ }
165
+
166
+ // --- Per-vertex: corner indices 0–3 (triangle-strip quad) ---
167
+ const a_corner = new Float32Array([0, 1, 2, 3])
168
+
169
+ // --- Build count attribute expression ---
170
+ // When a filter column is provided, use filteredHistogram so the computation
171
+ // re-runs whenever the filter axis domain changes.
172
+ const countAttr = filterQK
173
+ ? { filteredHistogram: { input: normalized, filterValues: srcF, filterAxisId: filterQK, bins } }
174
+ : { histogram: { input: normalized, bins } }
175
+
176
+ // --- Compute filter column extent for the filterbar display range ---
177
+ const filterDomains = {}
178
+ if (filterQK && srcF) {
179
+ let fMin = Infinity, fMax = -Infinity
180
+ for (let i = 0; i < srcF.length; i++) {
181
+ if (srcF[i] < fMin) fMin = srcF[i]
182
+ if (srcF[i] > fMax) fMax = srcF[i]
183
+ }
184
+ filterDomains[filterQK] = [fMin, fMax]
185
+ }
186
+
187
+ return [{
188
+ attributes: {
189
+ a_corner, // per-vertex (no divisor)
190
+ x_center, // per-instance (divisor 1)
191
+ // GPU histogram via computed attribute; sampled at a_pickId (= bin index)
192
+ count: countAttr,
193
+ },
194
+ attributeDivisors: { x_center: 1 },
195
+ uniforms: {
196
+ u_binHalfWidth: binWidth / 2,
197
+ u_color: color,
198
+ },
199
+ vertexCount: 4,
200
+ instanceCount: bins,
201
+ primitive: "triangle strip",
202
+ domains: {
203
+ [vQK]: [min, max],
204
+ count: [0, maxCount],
205
+ ...filterDomains,
206
+ },
207
+ }]
208
+ }
209
+ }
210
+
211
+ export const histogramLayerType = new HistogramLayerType()
212
+ registerLayerType("histogram", histogramLayerType)