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.
- package/LICENSE +21 -0
- package/README.md +34 -0
- package/package.json +28 -0
- package/src/Axis.js +48 -0
- package/src/AxisLink.js +31 -0
- package/src/AxisQuantityKindRegistry.js +23 -0
- package/src/AxisRegistry.js +54 -0
- package/src/ColorAxisRegistry.js +49 -0
- package/src/Colorbar.js +64 -0
- package/src/ColorbarLayer.js +74 -0
- package/src/ColorscaleRegistry.js +49 -0
- package/src/FilterAxisRegistry.js +76 -0
- package/src/Filterbar.js +138 -0
- package/src/FilterbarFloat.js +157 -0
- package/src/FilterbarLayer.js +49 -0
- package/src/Float.js +159 -0
- package/src/Layer.js +43 -0
- package/src/LayerType.js +169 -0
- package/src/LayerTypeRegistry.js +19 -0
- package/src/MatplotlibColorscales.js +564 -0
- package/src/Plot.js +976 -0
- package/src/ScatterLayer.js +107 -0
- package/src/index.js +21 -0
package/src/Filterbar.js
ADDED
|
@@ -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
|
+
}
|