gladly-plot 0.0.1

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.
@@ -0,0 +1,138 @@
1
+ import { Plot } from "./Plot.js"
2
+ import { linkAxes } from "./AxisLink.js"
3
+ import "./FilterbarLayer.js"
4
+
5
+ const DEFAULT_MARGINS = {
6
+ horizontal: { top: 5, right: 40, bottom: 45, left: 40 },
7
+ vertical: { top: 30, right: 10, bottom: 30, left: 50 }
8
+ }
9
+
10
+ export class Filterbar extends Plot {
11
+ constructor(container, targetPlot, filterAxisName, { orientation = "horizontal", margin } = {}) {
12
+ super(container, { margin: margin ?? DEFAULT_MARGINS[orientation] })
13
+
14
+ this._targetPlot = targetPlot
15
+ this._filterAxisName = filterAxisName
16
+ this._orientation = orientation
17
+ this._spatialAxis = orientation === "horizontal" ? "xaxis_bottom" : "yaxis_left"
18
+
19
+ this.update({
20
+ data: {},
21
+ config: {
22
+ layers: [{ filterbar: { filterAxis: filterAxisName, orientation } }]
23
+ }
24
+ })
25
+
26
+ // Link the filterbar's spatial axis to the target's filter axis.
27
+ // Zoom/pan on the filterbar propagates back to update the filter range.
28
+ this._spatialLink = linkAxes(this.axes[this._spatialAxis], targetPlot.axes[filterAxisName])
29
+
30
+ // Re-render (with sync) whenever the target plot renders.
31
+ this._syncCallback = () => this.render()
32
+ targetPlot._renderCallbacks.add(this._syncCallback)
33
+
34
+ this._addCheckboxOverlays()
35
+ }
36
+
37
+ _addCheckboxOverlays() {
38
+ const makeLabel = (title) => {
39
+ const label = document.createElement('label')
40
+ Object.assign(label.style, {
41
+ position: 'absolute',
42
+ zIndex: '3',
43
+ display: 'flex',
44
+ flexDirection: 'column',
45
+ alignItems: 'center',
46
+ gap: '2px',
47
+ fontSize: '11px',
48
+ color: '#555',
49
+ cursor: 'pointer',
50
+ userSelect: 'none'
51
+ })
52
+ label.title = title
53
+ const cb = document.createElement('input')
54
+ cb.type = 'checkbox'
55
+ Object.assign(cb.style, { margin: '0', cursor: 'pointer' })
56
+ const span = document.createElement('span')
57
+ span.textContent = '∞'
58
+ label.appendChild(cb)
59
+ label.appendChild(span)
60
+ return label
61
+ }
62
+
63
+ this._minLabel = makeLabel('open min bound')
64
+ this._maxLabel = makeLabel('open max bound')
65
+ this._minInput = this._minLabel.querySelector('input')
66
+ this._maxInput = this._maxLabel.querySelector('input')
67
+
68
+ if (this._orientation === 'horizontal') {
69
+ Object.assign(this._minLabel.style, { left: '2px', bottom: '2px' })
70
+ Object.assign(this._maxLabel.style, { right: '2px', bottom: '2px' })
71
+ } else {
72
+ Object.assign(this._maxLabel.style, { top: '2px', left: '2px' })
73
+ Object.assign(this._minLabel.style, { bottom: '2px', left: '2px' })
74
+ }
75
+
76
+ this.container.appendChild(this._minLabel)
77
+ this.container.appendChild(this._maxLabel)
78
+
79
+ this._minInput.addEventListener('change', () => this._onCheckboxChange())
80
+ this._maxInput.addEventListener('change', () => this._onCheckboxChange())
81
+ }
82
+
83
+ _onCheckboxChange() {
84
+ const registry = this._targetPlot.filterAxisRegistry
85
+ if (!registry) return
86
+
87
+ const range = registry.getRange(this._filterAxisName)
88
+ const current = this.getAxisDomain(this._spatialAxis)
89
+ const extent = registry.getDataExtent(this._filterAxisName)
90
+
91
+ const minOpen = this._minInput.checked
92
+ const maxOpen = this._maxInput.checked
93
+
94
+ // When closing an open bound, use the current filterbar view edge so
95
+ // unchecking ∞ restores whatever the filterbar is currently displaying.
96
+ const currentMin = range?.min ?? current?.[0] ?? (extent ? extent[0] : 0)
97
+ const currentMax = range?.max ?? current?.[1] ?? (extent ? extent[1] : 1)
98
+
99
+ registry.setRange(
100
+ this._filterAxisName,
101
+ minOpen ? null : currentMin,
102
+ maxOpen ? null : currentMax
103
+ )
104
+ this._targetPlot.scheduleRender()
105
+ }
106
+
107
+ render() {
108
+ if (this.axisRegistry && this._targetPlot) {
109
+ const registry = this._targetPlot.filterAxisRegistry
110
+ if (registry) {
111
+ const range = registry.getRange(this._filterAxisName)
112
+ const extent = registry.getDataExtent(this._filterAxisName)
113
+ if (range) {
114
+ // For open bounds, keep the current axis edge so toggling ∞ does not
115
+ // shift the filterbar's view. Fall back to data extent only on the
116
+ // initial render when the axis has no domain yet.
117
+ const current = this.getAxisDomain(this._spatialAxis)
118
+ const displayMin = range.min ?? current?.[0] ?? (extent ? extent[0] : 0)
119
+ const displayMax = range.max ?? current?.[1] ?? (extent ? extent[1] : 1)
120
+ if (displayMin < displayMax) {
121
+ this.setAxisDomain(this._spatialAxis, [displayMin, displayMax])
122
+ }
123
+ if (this._minInput) this._minInput.checked = range.min === null
124
+ if (this._maxInput) this._maxInput.checked = range.max === null
125
+ }
126
+ }
127
+ const scaleType = this._targetPlot._getScaleTypeFloat(this._filterAxisName) > 0.5 ? "log" : "linear"
128
+ this.axisRegistry.setScaleType(this._spatialAxis, scaleType)
129
+ }
130
+ super.render()
131
+ }
132
+
133
+ destroy() {
134
+ this._spatialLink.unlink()
135
+ this._targetPlot._renderCallbacks.delete(this._syncCallback)
136
+ super.destroy()
137
+ }
138
+ }
@@ -0,0 +1,157 @@
1
+ import { Filterbar } from "./Filterbar.js"
2
+ import { Plot } from "./Plot.js"
3
+
4
+ const DRAG_BAR_HEIGHT = 12
5
+ 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
+ }
12
+
13
+ export class FilterbarFloat {
14
+ constructor(parentPlot, filterAxisName, {
15
+ orientation = "horizontal",
16
+ x = 10,
17
+ y = 100,
18
+ width,
19
+ height,
20
+ margin
21
+ } = {}) {
22
+ const defaults = DEFAULT_SIZE[orientation]
23
+ const w = width ?? defaults.width
24
+ const h = height ?? defaults.height
25
+
26
+ // Outer floating container
27
+ this._el = document.createElement('div')
28
+ Object.assign(this._el.style, {
29
+ position: 'absolute',
30
+ left: x + 'px',
31
+ top: y + 'px',
32
+ width: w + 'px',
33
+ height: h + 'px',
34
+ zIndex: '10',
35
+ boxSizing: 'border-box',
36
+ background: 'rgba(255,255,255,0.88)',
37
+ border: '1px solid #aaa',
38
+ borderRadius: '4px',
39
+ boxShadow: '0 2px 8px rgba(0,0,0,0.25)',
40
+ overflow: 'hidden'
41
+ })
42
+
43
+ const parentEl = parentPlot.container
44
+ if (getComputedStyle(parentEl).position === 'static') {
45
+ parentEl.style.position = 'relative'
46
+ }
47
+ parentEl.appendChild(this._el)
48
+
49
+ // Drag bar — thin strip at the top; dragging here moves the float
50
+ this._dragBar = document.createElement('div')
51
+ Object.assign(this._dragBar.style, {
52
+ position: 'absolute',
53
+ top: '0',
54
+ left: '0',
55
+ right: '0',
56
+ height: DRAG_BAR_HEIGHT + 'px',
57
+ cursor: 'grab',
58
+ background: 'rgba(0,0,0,0.07)',
59
+ borderBottom: '1px solid rgba(0,0,0,0.12)',
60
+ zIndex: '1'
61
+ })
62
+ this._el.appendChild(this._dragBar)
63
+
64
+ // Resize handle — bottom-right corner
65
+ this._resizeHandle = document.createElement('div')
66
+ Object.assign(this._resizeHandle.style, {
67
+ position: 'absolute',
68
+ right: '0',
69
+ bottom: '0',
70
+ width: '12px',
71
+ height: '12px',
72
+ cursor: 'se-resize',
73
+ background: 'rgba(0,0,0,0.18)',
74
+ borderTopLeftRadius: '3px',
75
+ zIndex: '3'
76
+ })
77
+ this._el.appendChild(this._resizeHandle)
78
+
79
+ // Sub-container for the filterbar — sits below the drag bar
80
+ this._filterbarEl = document.createElement('div')
81
+ Object.assign(this._filterbarEl.style, {
82
+ position: 'absolute',
83
+ top: DRAG_BAR_HEIGHT + 'px',
84
+ left: '0',
85
+ right: '0',
86
+ bottom: '0'
87
+ })
88
+ this._el.appendChild(this._filterbarEl)
89
+
90
+ this._filterbar = new Filterbar(this._filterbarEl, parentPlot, filterAxisName, { orientation, margin })
91
+
92
+ this._setupInteraction()
93
+ }
94
+
95
+ _setupInteraction() {
96
+ let mode = null // 'drag' | 'resize'
97
+ let startX, startY, startLeft, startTop, startW, startH
98
+
99
+ const onDragBarMouseDown = (e) => {
100
+ mode = 'drag'
101
+ startX = e.clientX
102
+ startY = e.clientY
103
+ startLeft = parseInt(this._el.style.left, 10)
104
+ startTop = parseInt(this._el.style.top, 10)
105
+ this._dragBar.style.cursor = 'grabbing'
106
+ e.preventDefault()
107
+ }
108
+
109
+ const onResizeMouseDown = (e) => {
110
+ mode = 'resize'
111
+ startX = e.clientX
112
+ startY = e.clientY
113
+ startW = this._el.offsetWidth
114
+ startH = this._el.offsetHeight
115
+ e.preventDefault()
116
+ e.stopPropagation()
117
+ }
118
+
119
+ const onMouseMove = (e) => {
120
+ if (!mode) return
121
+ const dx = e.clientX - startX
122
+ const dy = e.clientY - startY
123
+ if (mode === 'drag') {
124
+ this._el.style.left = (startLeft + dx) + 'px'
125
+ this._el.style.top = (startTop + dy) + 'px'
126
+ } else {
127
+ this._el.style.width = Math.max(MIN_WIDTH, startW + dx) + 'px'
128
+ this._el.style.height = Math.max(MIN_HEIGHT, startH + dy) + 'px'
129
+ }
130
+ }
131
+
132
+ const onMouseUp = () => {
133
+ if (mode === 'drag') this._dragBar.style.cursor = 'grab'
134
+ mode = null
135
+ }
136
+
137
+ this._dragBar.addEventListener('mousedown', onDragBarMouseDown)
138
+ this._resizeHandle.addEventListener('mousedown', onResizeMouseDown)
139
+ document.addEventListener('mousemove', onMouseMove)
140
+ document.addEventListener('mouseup', onMouseUp)
141
+
142
+ this._cleanupInteraction = () => {
143
+ this._dragBar.removeEventListener('mousedown', onDragBarMouseDown)
144
+ this._resizeHandle.removeEventListener('mousedown', onResizeMouseDown)
145
+ document.removeEventListener('mousemove', onMouseMove)
146
+ document.removeEventListener('mouseup', onMouseUp)
147
+ }
148
+ }
149
+
150
+ destroy() {
151
+ this._cleanupInteraction()
152
+ this._filterbar.destroy()
153
+ this._el.remove()
154
+ }
155
+ }
156
+
157
+ Plot._FilterbarFloatClass = FilterbarFloat
@@ -0,0 +1,49 @@
1
+ import { LayerType } from "./LayerType.js"
2
+ import { registerLayerType } from "./LayerTypeRegistry.js"
3
+
4
+ export const filterbarLayerType = new LayerType({
5
+ name: "filterbar",
6
+
7
+ getAxisConfig: function(parameters) {
8
+ const { filterAxis, orientation = "horizontal" } = parameters
9
+ return {
10
+ xAxis: orientation === "horizontal" ? "xaxis_bottom" : null,
11
+ xAxisQuantityKind: orientation === "horizontal" ? filterAxis : undefined,
12
+ yAxis: orientation === "vertical" ? "yaxis_left" : null,
13
+ yAxisQuantityKind: orientation === "vertical" ? filterAxis : undefined,
14
+ }
15
+ },
16
+
17
+ // Nothing is rendered — vertexCount is always 0.
18
+ // These minimal shaders satisfy the WebGL compiler but never execute.
19
+ vert: `
20
+ precision mediump float;
21
+ uniform vec2 xDomain;
22
+ uniform vec2 yDomain;
23
+ void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
24
+ `,
25
+ frag: `
26
+ precision mediump float;
27
+ void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); }
28
+ `,
29
+
30
+ schema: () => ({
31
+ $schema: "https://json-schema.org/draft/2020-12/schema",
32
+ type: "object",
33
+ properties: {
34
+ filterAxis: { type: "string", description: "Quantity kind of the filter axis to display" },
35
+ orientation: { type: "string", enum: ["horizontal", "vertical"], default: "horizontal" }
36
+ },
37
+ required: ["filterAxis"]
38
+ }),
39
+
40
+ createLayer: function() {
41
+ return [{
42
+ attributes: {},
43
+ uniforms: {},
44
+ vertexCount: 0,
45
+ }]
46
+ }
47
+ })
48
+
49
+ registerLayerType("filterbar", filterbarLayerType)
package/src/Float.js ADDED
@@ -0,0 +1,159 @@
1
+ import { Colorbar } from "./Colorbar.js"
2
+ import { Plot } from "./Plot.js"
3
+
4
+ const DRAG_BAR_HEIGHT = 12
5
+ const MIN_WIDTH = 80
6
+ const MIN_HEIGHT = DRAG_BAR_HEIGHT + 30
7
+
8
+ // Default sizes include the drag bar so the colorbar content area is unchanged.
9
+ const DEFAULT_SIZE = {
10
+ horizontal: { width: 220, height: 70 + DRAG_BAR_HEIGHT },
11
+ vertical: { width: 70, height: 220 + DRAG_BAR_HEIGHT }
12
+ }
13
+
14
+ export class Float {
15
+ constructor(parentPlot, colorAxisName, {
16
+ orientation = "horizontal",
17
+ x = 10,
18
+ y = 10,
19
+ width,
20
+ height,
21
+ margin
22
+ } = {}) {
23
+ const defaults = DEFAULT_SIZE[orientation]
24
+ const w = width ?? defaults.width
25
+ const h = height ?? defaults.height
26
+
27
+ // Outer floating container
28
+ this._el = document.createElement('div')
29
+ Object.assign(this._el.style, {
30
+ position: 'absolute',
31
+ left: x + 'px',
32
+ top: y + 'px',
33
+ width: w + 'px',
34
+ height: h + 'px',
35
+ zIndex: '10',
36
+ boxSizing: 'border-box',
37
+ background: 'rgba(255,255,255,0.88)',
38
+ border: '1px solid #aaa',
39
+ borderRadius: '4px',
40
+ boxShadow: '0 2px 8px rgba(0,0,0,0.25)',
41
+ overflow: 'hidden'
42
+ })
43
+
44
+ // Ensure parent is positioned so our absolute child is contained within it
45
+ const parentEl = parentPlot.container
46
+ if (getComputedStyle(parentEl).position === 'static') {
47
+ parentEl.style.position = 'relative'
48
+ }
49
+ parentEl.appendChild(this._el)
50
+
51
+ // Drag bar — thin strip at the top; dragging here moves the float
52
+ this._dragBar = document.createElement('div')
53
+ Object.assign(this._dragBar.style, {
54
+ position: 'absolute',
55
+ top: '0',
56
+ left: '0',
57
+ right: '0',
58
+ height: DRAG_BAR_HEIGHT + 'px',
59
+ cursor: 'grab',
60
+ background: 'rgba(0,0,0,0.07)',
61
+ borderBottom: '1px solid rgba(0,0,0,0.12)',
62
+ zIndex: '1'
63
+ })
64
+ this._el.appendChild(this._dragBar)
65
+
66
+ // Resize handle — bottom-right corner
67
+ this._resizeHandle = document.createElement('div')
68
+ Object.assign(this._resizeHandle.style, {
69
+ position: 'absolute',
70
+ right: '0',
71
+ bottom: '0',
72
+ width: '12px',
73
+ height: '12px',
74
+ cursor: 'se-resize',
75
+ background: 'rgba(0,0,0,0.18)',
76
+ borderTopLeftRadius: '3px',
77
+ zIndex: '3'
78
+ })
79
+ this._el.appendChild(this._resizeHandle)
80
+
81
+ // Sub-container for the colorbar — sits below the drag bar
82
+ this._colorbarEl = document.createElement('div')
83
+ Object.assign(this._colorbarEl.style, {
84
+ position: 'absolute',
85
+ top: DRAG_BAR_HEIGHT + 'px',
86
+ left: '0',
87
+ right: '0',
88
+ bottom: '0'
89
+ })
90
+ this._el.appendChild(this._colorbarEl)
91
+
92
+ this._colorbar = new Colorbar(this._colorbarEl, parentPlot, colorAxisName, { orientation, margin })
93
+
94
+ this._setupInteraction()
95
+ }
96
+
97
+ _setupInteraction() {
98
+ let mode = null // 'drag' | 'resize'
99
+ let startX, startY, startLeft, startTop, startW, startH
100
+
101
+ const onDragBarMouseDown = (e) => {
102
+ mode = 'drag'
103
+ startX = e.clientX
104
+ startY = e.clientY
105
+ startLeft = parseInt(this._el.style.left, 10)
106
+ startTop = parseInt(this._el.style.top, 10)
107
+ this._dragBar.style.cursor = 'grabbing'
108
+ e.preventDefault()
109
+ }
110
+
111
+ const onResizeMouseDown = (e) => {
112
+ mode = 'resize'
113
+ startX = e.clientX
114
+ startY = e.clientY
115
+ startW = this._el.offsetWidth
116
+ startH = this._el.offsetHeight
117
+ e.preventDefault()
118
+ e.stopPropagation()
119
+ }
120
+
121
+ const onMouseMove = (e) => {
122
+ if (!mode) return
123
+ const dx = e.clientX - startX
124
+ const dy = e.clientY - startY
125
+ if (mode === 'drag') {
126
+ this._el.style.left = (startLeft + dx) + 'px'
127
+ this._el.style.top = (startTop + dy) + 'px'
128
+ } else {
129
+ this._el.style.width = Math.max(MIN_WIDTH, startW + dx) + 'px'
130
+ this._el.style.height = Math.max(MIN_HEIGHT, startH + dy) + 'px'
131
+ }
132
+ }
133
+
134
+ const onMouseUp = () => {
135
+ if (mode === 'drag') this._dragBar.style.cursor = 'grab'
136
+ mode = null
137
+ }
138
+
139
+ this._dragBar.addEventListener('mousedown', onDragBarMouseDown)
140
+ this._resizeHandle.addEventListener('mousedown', onResizeMouseDown)
141
+ document.addEventListener('mousemove', onMouseMove)
142
+ document.addEventListener('mouseup', onMouseUp)
143
+
144
+ this._cleanupInteraction = () => {
145
+ this._dragBar.removeEventListener('mousedown', onDragBarMouseDown)
146
+ this._resizeHandle.removeEventListener('mousedown', onResizeMouseDown)
147
+ document.removeEventListener('mousemove', onMouseMove)
148
+ document.removeEventListener('mouseup', onMouseUp)
149
+ }
150
+ }
151
+
152
+ destroy() {
153
+ this._cleanupInteraction()
154
+ this._colorbar.destroy()
155
+ this._el.remove()
156
+ }
157
+ }
158
+
159
+ Plot._FloatClass = Float
package/src/Layer.js ADDED
@@ -0,0 +1,43 @@
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 = {} }) {
3
+ // Validate that all attributes are typed arrays
4
+ for (const [key, value] of Object.entries(attributes)) {
5
+ if (!(value instanceof Float32Array)) {
6
+ throw new Error(`Attribute '${key}' must be Float32Array`)
7
+ }
8
+ }
9
+
10
+ // Validate colorAxes: must be an array of quantity kind strings
11
+ for (const quantityKind of colorAxes) {
12
+ if (typeof quantityKind !== 'string') {
13
+ throw new Error(`Color axis quantity kind must be a string, got ${typeof quantityKind}`)
14
+ }
15
+ }
16
+
17
+ // Validate filterAxes: must be an array of quantity kind strings
18
+ for (const quantityKind of filterAxes) {
19
+ if (typeof quantityKind !== 'string') {
20
+ throw new Error(`Filter axis quantity kind must be a string, got ${typeof quantityKind}`)
21
+ }
22
+ }
23
+
24
+ this.type = type
25
+ this.attributes = attributes
26
+ this.uniforms = uniforms
27
+ this.nameMap = nameMap
28
+ this.domains = domains
29
+ this.lineWidth = lineWidth
30
+ this.primitive = primitive
31
+ this.xAxis = xAxis
32
+ this.yAxis = yAxis
33
+ this.xAxisQuantityKind = xAxisQuantityKind
34
+ this.yAxisQuantityKind = yAxisQuantityKind
35
+ // colorAxes: string[] — quantity kinds of color axes; attribute named by quantityKind holds the data
36
+ this.colorAxes = colorAxes
37
+ // filterAxes: string[] — quantity kinds of filter axes; attribute named by quantityKind holds the data
38
+ this.filterAxes = filterAxes
39
+ this.vertexCount = vertexCount
40
+ this.instanceCount = instanceCount
41
+ this.attributeDivisors = attributeDivisors
42
+ }
43
+ }